Repository: ccxt/ccxt
Pull request: feat(binance) - margin field in fetchCurrencies & margin data in fetchMarkets
fixes #8695

Problem statement:
Binance: Exchange Error  {"code":-3028,"msg":"Not a valid margin pair."}
Hi ccxt team!

I am getting constant `{"code":-3028,"msg":"Not a valid margin pair."}` errors from Binance when executing market orders on them even tho I am using the 'margin' defaultType on initialization .

- OS: macOS
- Programming Language version: node 12.x
- CCXT version: ccxt@1.43.94


```
// init binance in margin mode
const binance = new ccxt.binance({
  options: {
    'defaultType': 'margin',
  },
})

// fetch markets, with explicit refresh and explicit type
const allMarkets = await this.exchange.loadMarkets(true,  {type: 'margin'})

// then I filter the markets to only give me margin=true pairs
const marginMarkets =  markets.filter((m) => {
      return m.active && m.margin && m.type === 'margin' && m.info.isMarginTradingAllowed
})
```
Even tho the filtered `marginMarkets` now **only** contains pairs with `margin: true` I still get the above mentioned error message from the exchange on some pairs.

Here is one of the pairs which are in `marginMarkets` but should **not** be as they dont seem to be supported.
Also they do not show up in the Binance `GET /sapi/v1/margin/allPairs` listing.

Notice how it has `"isMarginTradingAllowed": true`, `"margin": true` and `"type": "margin"` set to indicate margin trading is allowed and possible.

```
// binance.markets["ENJ/BTC"]:

{
  "limits": {
    "amount": {
      "min": 1,
      "max": 90000000
    },
    "price": {
      "min": 1e-8,
      "max": 1000
    },
    "cost": {
      "min": 0.0001
    },
    "market": {
      "min": 0,
      "max": 418478.30902778
    }
  },
  "precision": {
    "base": 8,
    "quote": 8,
    "amount": 0,
    "price": 8
  },
  "tierBased": false,
  "percentage": true,
  "taker": 0.001,
  "maker": 0.001,
  "id": "ENJBTC",
  "lowercaseId": "enjbtc",
  "symbol": "ENJ/BTC",
  "base": "ENJ",
  "quote": "BTC",
  "baseId": "ENJ",
  "quoteId": "BTC",
  "info": {
    "symbol": "ENJBTC",
    "status": "TRADING",
    "baseAsset": "ENJ",
    "baseAssetPrecision": 8,
    "quoteAsset": "BTC",
    "quotePrecision": 8,
    "quoteAssetPrecision": 8,
    "baseCommissionPrecision": 8,
    "quoteCommissionPrecision": 8,
    "orderTypes": ["LIMIT", "LIMIT_MAKER", "MARKET", "STOP_LOSS_LIMIT", "TAKE_PROFIT_LIMIT"],
    "icebergAllowed": true,
    "ocoAllowed": true,
    "quoteOrderQtyMarketAllowed": true,
    "isSpotTradingAllowed": true,
    "isMarginTradingAllowed": true,
    "filters": [{
      "filterType": "PRICE_FILTER",
      "minPrice": "0.00000001",
      "maxPrice": "1000.00000000",
      "tickSize": "0.00000001"
    }, {
      "filterType": "PERCENT_PRICE",
      "multiplierUp": "5",
      "multiplierDown": "0.2",
      "avgPriceMins": 5
    }, {
      "filterType": "LOT_SIZE",
      "minQty": "1.00000000",
      "maxQty": "90000000.00000000",
      "stepSize": "1.00000000"
    }, {
      "filterType": "MIN_NOTIONAL",
      "minNotional": "0.00010000",
      "applyToMarket": true,
      "avgPriceMins": 5
    }, {
      "filterType": "ICEBERG_PARTS",
      "limit": 10
    }, {
      "filterType": "MARKET_LOT_SIZE",
      "minQty": "0.00000000",
      "maxQty": "418478.30902778",
      "stepSize": "0.00000000"
    }, {
      "filterType": "MAX_NUM_ORDERS",
      "maxNumOrders": 200
    }, {
      "filterType": "MAX_NUM_ALGO_ORDERS",
      "maxNumAlgoOrders": 5
    }],
    "permissions": ["SPOT", "MARGIN"]
  },
  "type": "margin",
  "spot": false,
  "margin": true,
  "future": false,
  "delivery": false,
  "active": true
}
```

Executing a type MARKET trade on this pair results in the error mentioned above. The code works perfectly in all spot markets, so price and amount rounding issues are not the issue. 

```
binance.createOrder(
          order.symbol, //  "ENJ/BTC"
          order.type, // "MARKET"
          order.side, // "buy"
          order.amount, // 5
          undefined, 
          {
             sideEffectType: 'MARGIN_BUY' // enable auto-borrow
          } 
        )
```
Does this problem arise from binance API delivering wrong results, by CCXT providing wrong data for market pairs or from my approach in getting margin-only markets? (I *know*, I could use the implicit method for `sapi/v1/margin/allPairs` but I'd rather use the unified call - which should yield the same data)

Hints from issue discussion:
@strikeout thx for your report, we will investigate and will be back to you asap.
Small update:

I've checked the binance Website and API directly and it seems that, at least the above mentioned pair `ENJ/BTC`, does in fact allow margin trading, but only on *isolated* margin.
I have not found any indication or differentiator via the API market data endpoint about this tho. 
@strikeout thanks for pointing that out, it really helps save the time. Apparently, we can only throw an exception from the exchange at this point...
Can we maybe introduce a new property (array) or two (bools), like I did, to the market to indicate which "type" of margin is supported? 

I'm now doing the following locally:
```
// fetch isolated margin pairs, sets .marginIso = true 
        let isos = await this.exchange.sapiGetMarginIsolatedAllPairs();
        isos = isos.forEach((m) => {
          const mm = this.exchange.markets_by_id[m.symbol];
          this.exchange.markets[mm.symbol].marginIso = true
        });
// fetch cross margin pairs, sets .marginCross = true 
        let cross = await this.exchange.sapiGetMarginAllPairs();
        cross = cross.forEach((m) => {
          const mm = this.exchange.markets_by_id[m.symbol];
          this.exchange.markets[mm.symbol].marginCross = true
        });
```
@strikeout yes, that is possible, let me think a bit on how we can better unify this across the exchanges, i will be back to you shortly.
I am facing the same issue, can't distinguish between altcoins with leverage 
@Piercarlomaia have you set your `defaultType` to `'margin'`? Can you paste your versions, your code and your request+response output in verbose mode? https://github.com/ccxt/ccxt/wiki/FAQ#what-is-required-to-get-help
it's probably that the pair does not support cross margin, and is only tradable in isolated margin. 
Sadly been looking at ccxt and binanace API's to find out if the pair is cross margin or isolated with no luck. 


there are official api endpoints for them, i.e.:
A) Get all marginable assets - https://binance-docs.github.io/apidocs/spot/en/#get-all-margin-assets-market_data , using ccxt you can call: `data = binance.sapiGetMarginAllAssets();`
B) Get all cross-margin pairs: https://binance-docs.github.io/apidocs/spot/en/#get-all-cross-margin-pairs-market_data , using ccxt you can call: `data = binance.sapiGetMarginAllPairs();`
C) Get all isoslated-margin pairs: https://binance-docs.github.io/apidocs/spot/en/#get-all-isolated-margin-symbol-user_data , using ccxt you can call: `data = binance.sapiGetMarginIsolatedAllPairs();`

let us know if you have any questions

Commit message: Update binance.ts

Patch to review:
diff --git a/ts/src/binance.ts b/ts/src/binance.ts
index ceceba5548e5e..c62f3f3a482df 100644
--- a/ts/src/binance.ts
+++ b/ts/src/binance.ts
@@ -2626,6 +2626,7 @@ export default class binance extends Exchange {
          * @name binance#fetchCurrencies
          * @description fetches all available currencies on an exchange
          * @see https://developers.binance.com/docs/wallet/capital/all-coins-info
+         * @see https://developers.binance.com/docs/margin_trading/market-data/Get-All-Margin-Assets
          * @param {object} [params] extra parameters specific to the exchange API endpoint
          * @returns {object} an associative dictionary of currencies
          */
@@ -2645,9 +2646,13 @@ export default class binance extends Exchange {
         if (apiBackup !== undefined) {
             return undefined;
         }
-        const response = await this.sapiGetCapitalConfigGetall (params);
+        const promises = [ this.sapiGetCapitalConfigGetall (params), this.sapiGetMarginAllAssets (params) ];
+        const results = await Promise.all (promises);
+        const responseCurrencies = results[0];
+        const responseMarginables = results[1];
+        const marginablesById = this.indexBy (responseMarginables, 'assetName');
         const result: Dict = {};
-        for (let i = 0; i < response.length; i++) {
+        for (let i = 0; i < responseCurrencies.length; i++) {
             //
             //    {
             //        "coin": "LINK",
@@ -2743,7 +2748,7 @@ export default class binance extends Exchange {
             //        ]
             //    }
             //
-            const entry = response[i];
+            const entry = responseCurrencies[i];
             const id = this.safeString (entry, 'coin');
             const name = this.safeString (entry, 'name');
             const code = this.safeCurrencyCode (id);
@@ -2798,6 +2803,17 @@ export default class binance extends Exchange {
             }
             const trading = this.safeBool (entry, 'trading');
             const active = (isWithdrawEnabled && isDepositEnabled && trading);
+            const marginEntry = this.safeDict (marginablesById, id, {});
+            //
+            //     {
+            //         assetName: "BTC",
+            //         assetFullName: "Bitcoin",
+            //         isBorrowable: true,
+            //         isMortgageable: true,
+            //         userMinBorrow: "0",
+            //         userMinRepay: "0",
+            //     }
+            //
             result[code] = {
                 'id': id,
                 'name': name,
@@ -2811,6 +2827,7 @@ export default class binance extends Exchange {
                 'fee': fee,
                 'fees': fees,
                 'limits': this.limits,
+                'margin': this.safeBool (marginEntry, 'isBorrowable'),
             };
         }
         return result;
@@ -2825,6 +2842,8 @@ export default class binance extends Exchange {
          * @see https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Exchange-Information     // swap
          * @see https://developers.binance.com/docs/derivatives/coin-margined-futures/market-data/Exchange-Information              // future
          * @see https://developers.binance.com/docs/derivatives/option/market-data/Exchange-Information                             // option
+         * @see https://developers.binance.com/docs/margin_trading/market-data/Get-All-Cross-Margin-Pairs                             // cross margin
+         * @see https://developers.binance.com/docs/margin_trading/market-data/Get-All-Isolated-Margin-Symbol                             // isolated margin
          * @param {object} [params] extra parameters specific to the exchange API endpoint
          * @returns {object[]} an array of objects representing market data
          */
@@ -2843,6 +2862,10 @@ export default class binance extends Exchange {
             const marketType = fetchMarkets[i];
             if (marketType === 'spot') {
                 promisesRaw.push (this.publicGetExchangeInfo (params));
+                if (this.checkRequiredCredentials (false)) {
+                    promisesRaw.push (this.sapiGetMarginAllPairs (params));
+                    promisesRaw.push (this.sapiGetMarginIsolatedAllPairs (params));
+                }
             } else if (marketType === 'linear') {
                 promisesRaw.push (this.fapiPublicGetExchangeInfo (params));
             } else if (marketType === 'inverse') {
@@ -2855,10 +2878,21 @@ export default class binance extends Exchange {
         }
         const promises = await Promise.all (promisesRaw);
         let markets = [];
+        this.options['crossMarginPairsData'] = {};
+        this.options['isolatedMarginPairsData'] = {};
         for (let i = 0; i < fetchMarkets.length; i++) {
             const promise = this.safeDict (promises, i);
-            const promiseMarkets = this.safeList2 (promise, 'symbols', 'optionSymbols', []);
-            markets = this.arrayConcat (markets, promiseMarkets);
+            if (Array.isArray (promise)) {
+                // first one is the cross-margin promise
+                if (this.options['crossMarginPairsData'] === undefined) {
+                    this.options['crossMarginPairsData'] = this.indexBy (promise, 'symbol');
+                } else {
+                    this.options['isolatedMarginPairsData'] = this.indexBy (promise, 'symbol');
+                }
+            } else {
+                const promiseMarkets = this.safeList2 (promise, 'symbols', 'optionSymbols', []);
+                markets = this.arrayConcat (markets, promiseMarkets);
+            }
         }
         //
         // spot / margin
@@ -2904,6 +2938,20 @@ export default class binance extends Exchange {
         //         ],
         //     }
         //
+        // cross & isolated pairs response:
+        //
+        //     [
+        //         {
+        //           symbol: "BTCUSDT",
+        //           base: "BTC",
+        //           quote: "USDT",
+        //           isMarginTrade: true,
+        //           isBuyAllowed: true,
+        //           isSellAllowed: true,
+        //           id: "376870555451677893", // doesn't exist in isolated
+        //         },
+        //     ]
+        //
         // futures/usdt-margined (fapi)
         //
         //     {
@@ -3135,6 +3183,15 @@ export default class binance extends Exchange {
             }
         }
         const isMarginTradingAllowed = this.safeBool (market, 'isMarginTradingAllowed', false);
+        let marginMode = undefined;
+        if (spot) {
+            const hasCrossMargin = (id in this.options['crossMarginPairsData']);
+            const hasIsolatedMargin = (id in this.options['isolatedMarginPairsData']);
+            marginMode = {
+                'cross': hasCrossMargin,
+                'isolated': hasIsolatedMargin,
+            };
+        }
         let unifiedType = undefined;
         if (spot) {
             unifiedType = 'spot';
@@ -3163,6 +3220,7 @@ export default class binance extends Exchange {
             'type': unifiedType,
             'spot': spot,
             'margin': spot && isMarginTradingAllowed,
+            'marginMode': marginMode,
             'swap': swap,
             'future': future,
             'option': option,


Code review comments:
diff_hunk: @@ -2855,10 +2878,21 @@ export default class binance extends Exchange {
         }
         const promises = await Promise.all (promisesRaw);
         let markets = [];
+        this.options['crossMarginPairsData'] = {};
+        this.options['isolatedMarginPairsData'] = {};
         for (let i = 0; i < fetchMarkets.length; i++) {
             const promise = this.safeDict (promises, i);
-            const promiseMarkets = this.safeList2 (promise, 'symbols', 'optionSymbols', []);
-            markets = this.arrayConcat (markets, promiseMarkets);
+            if (Array.isArray (promise)) {
path: ts/src/binance.ts
text: @user1:
this is not correct, if we use safeDict it's not expected to check if it's an array 
![image](https://github.com/ccxt/ccxt/assets/43336371/ce7fea32-49ce-45e6-9ff4-eee7bbecfaaa)


@user1:
Also relying on the promise type dict vs array to infer the content of promise  seems a bit unsafe