|
| 1 | +import { numberInString } from '../lib'; |
| 2 | +import { ExchangeInfo, MainClient } from '../src/index'; |
| 3 | + |
| 4 | +// or |
| 5 | +// import { MainClient } from 'binance'; |
| 6 | + |
| 7 | +const client = new MainClient({ |
| 8 | + // Optional (default: false) - when true, response strings are parsed to floats (only for known keys). |
| 9 | + // beautifyResponses: true, |
| 10 | +}); |
| 11 | + |
| 12 | +interface SymbolInfo { |
| 13 | + tickSize: numberInString; |
| 14 | + qtyStepSize: numberInString; |
| 15 | + minOrderQty: numberInString; |
| 16 | + maxOrderQty: numberInString; |
| 17 | + maxMarketQty: numberInString; |
| 18 | + maxNumOfOrders: number; |
| 19 | + minNotional: numberInString; |
| 20 | + maxNotional: numberInString; |
| 21 | + maxBasePrecisionDecimals: number; |
| 22 | + maxQuotePrecisionDecimals: number; |
| 23 | +} |
| 24 | + |
| 25 | +// Get full exchange info so we can cache it and use it for other functions without making request every time |
| 26 | +async function getExchangeInfo() { |
| 27 | + try { |
| 28 | + const exchangeInfo = await client.getExchangeInfo(); |
| 29 | + return exchangeInfo; |
| 30 | + } catch (error) { |
| 31 | + throw new Error(`Failed to get exchange info: ${error.message}`); |
| 32 | + } |
| 33 | +} |
| 34 | + |
| 35 | +const symbol = 'SOLUSDT'; |
| 36 | +const exchangeInfo = getExchangeInfo(); |
| 37 | + |
| 38 | +async function getSymbolInfo( |
| 39 | + exchangeInfo: ExchangeInfo, |
| 40 | + symbol: string, |
| 41 | +): Promise<SymbolInfo> { |
| 42 | + try { |
| 43 | + // Find the symbol information once |
| 44 | + const symbolInfo = exchangeInfo.symbols.find((s) => s.symbol === symbol); |
| 45 | + // console.log(symbolInfo); |
| 46 | + |
| 47 | + if (!symbolInfo) { |
| 48 | + throw new Error(`Symbol ${symbol} not found in exchange info`); |
| 49 | + } |
| 50 | + |
| 51 | + // Extract filters from the symbol info |
| 52 | + const priceFilter = symbolInfo.filters.find( |
| 53 | + (f) => f.filterType === 'PRICE_FILTER', |
| 54 | + ); |
| 55 | + const lotSizeFilter = symbolInfo.filters.find( |
| 56 | + (f) => f.filterType === 'LOT_SIZE', |
| 57 | + ); |
| 58 | + const marketLotSizeFilter = symbolInfo.filters.find( |
| 59 | + (f) => f.filterType === 'MARKET_LOT_SIZE', |
| 60 | + ); |
| 61 | + const maxNumOrdersFilter = symbolInfo.filters.find( |
| 62 | + (f) => f.filterType === 'MAX_NUM_ORDERS', |
| 63 | + ); |
| 64 | + const notionalFilter = symbolInfo.filters.find( |
| 65 | + (f) => f.filterType === 'NOTIONAL', |
| 66 | + ); |
| 67 | + |
| 68 | + const symbolFilters = { |
| 69 | + tickSize: priceFilter?.tickSize, |
| 70 | + qtyStepSize: lotSizeFilter?.stepSize, |
| 71 | + minOrderQty: lotSizeFilter?.minQty, |
| 72 | + maxOrderQty: lotSizeFilter?.maxQty, |
| 73 | + maxMarketQty: marketLotSizeFilter?.maxQty, |
| 74 | + maxNumOfOrders: maxNumOrdersFilter?.maxNumOrders, |
| 75 | + minNotional: notionalFilter?.minNotional, |
| 76 | + maxNotional: notionalFilter?.maxNotional, |
| 77 | + maxBasePrecisionDecimals: symbolInfo.baseAssetPrecision, |
| 78 | + maxQuotePrecisionDecimals: symbolInfo.quoteAssetPrecision, |
| 79 | + }; |
| 80 | + console.log(symbolFilters); |
| 81 | + |
| 82 | + return symbolFilters; |
| 83 | + } catch (error) { |
| 84 | + throw new Error(`Failed to get symbol info: ${error.message}`); |
| 85 | + } |
| 86 | +} |
| 87 | + |
| 88 | +/** |
| 89 | + * Rounds a price to the correct number of decimal places based on the symbol's tick size |
| 90 | + */ |
| 91 | +function roundToTickSize(price: number, tickSize: string): string { |
| 92 | + if (!tickSize) return price.toString(); |
| 93 | + |
| 94 | + // Calculate precision from tick size (e.g., 0.00010000 → 4 decimal places) |
| 95 | + const precision = tickSize.indexOf('1') - 1; |
| 96 | + if (precision < 0) return Math.floor(price).toString(); |
| 97 | + |
| 98 | + // Round to the nearest tick |
| 99 | + const tickSizeNum = parseFloat(tickSize); |
| 100 | + const rounded = Math.floor(price / tickSizeNum) * tickSizeNum; |
| 101 | + |
| 102 | + // Format with correct precision |
| 103 | + return rounded.toFixed(precision); |
| 104 | +} |
| 105 | + |
| 106 | +/** |
| 107 | + * Rounds a quantity to the correct step size |
| 108 | + */ |
| 109 | +function roundToStepSize(quantity: number, stepSize: string): string { |
| 110 | + if (!stepSize) return quantity.toString(); |
| 111 | + |
| 112 | + // Calculate precision from step size |
| 113 | + const precision = stepSize.indexOf('1') - 1; |
| 114 | + if (precision < 0) return Math.floor(quantity).toString(); |
| 115 | + |
| 116 | + // Round to the nearest step |
| 117 | + const stepSizeNum = parseFloat(stepSize); |
| 118 | + const rounded = Math.floor(quantity / stepSizeNum) * stepSizeNum; |
| 119 | + |
| 120 | + // Format with correct precision |
| 121 | + return rounded.toFixed(precision); |
| 122 | +} |
| 123 | + |
| 124 | +/** |
| 125 | + * Validates and formats an order based on symbol constraints |
| 126 | + */ |
| 127 | +function formatOrderParams( |
| 128 | + symbol: string, |
| 129 | + price: number, |
| 130 | + quantity: number, |
| 131 | + symbolInfo: any, |
| 132 | +): { symbol: string; price: string; quantity: string } { |
| 133 | + try { |
| 134 | + // Check if price is within allowed range |
| 135 | + const minPrice = parseFloat(symbolInfo.tickSize || '0'); |
| 136 | + if (price < minPrice) { |
| 137 | + throw new Error(`Price ${price} is below minimum ${minPrice}`); |
| 138 | + } |
| 139 | + |
| 140 | + // Check if quantity is within allowed range |
| 141 | + const minQty = parseFloat(symbolInfo.minOrderQty || '0'); |
| 142 | + const maxQty = parseFloat(symbolInfo.maxOrderQty || Infinity); |
| 143 | + |
| 144 | + if (quantity < minQty) { |
| 145 | + throw new Error(`Quantity ${quantity} is below minimum ${minQty}`); |
| 146 | + } |
| 147 | + |
| 148 | + if (quantity > maxQty) { |
| 149 | + throw new Error(`Quantity ${quantity} exceeds maximum ${maxQty}`); |
| 150 | + } |
| 151 | + |
| 152 | + // Check notional value (price * quantity) |
| 153 | + const notional = price * quantity; |
| 154 | + const minNotional = parseFloat(symbolInfo.minNotional || '0'); |
| 155 | + |
| 156 | + if (notional < minNotional) { |
| 157 | + throw new Error( |
| 158 | + `Order value ${notional} is below minimum ${minNotional}`, |
| 159 | + ); |
| 160 | + } |
| 161 | + |
| 162 | + // Format price and quantity according to exchange requirements |
| 163 | + const formattedPrice = roundToTickSize(price, symbolInfo.tickSize); |
| 164 | + const formattedQty = roundToStepSize(quantity, symbolInfo.qtyStepSize); |
| 165 | + |
| 166 | + return { |
| 167 | + symbol, |
| 168 | + price: formattedPrice, |
| 169 | + quantity: formattedQty, |
| 170 | + }; |
| 171 | + } catch (error) { |
| 172 | + throw new Error(`Failed to format order: ${error.message}`); |
| 173 | + } |
| 174 | +} |
| 175 | + |
| 176 | +// Example usage |
| 177 | +async function testSymbolUtils() { |
| 178 | + const info = await exchangeInfo; |
| 179 | + if (!info) return; |
| 180 | + |
| 181 | + const symbolFilters = await getSymbolInfo(info, symbol); |
| 182 | + if (!symbolFilters) return; |
| 183 | + |
| 184 | + // Test price formatting |
| 185 | + const testPrice = 23.45678; |
| 186 | + console.log(`Original price: ${testPrice}`); |
| 187 | + console.log( |
| 188 | + `Formatted price: ${roundToTickSize(testPrice, symbolFilters.tickSize.toString())}`, |
| 189 | + ); |
| 190 | + |
| 191 | + // Test quantity formatting |
| 192 | + const testQty = 1.23456; |
| 193 | + console.log(`Original quantity: ${testQty}`); |
| 194 | + console.log( |
| 195 | + `Formatted quantity: ${roundToStepSize( |
| 196 | + testQty, |
| 197 | + symbolFilters.qtyStepSize.toString(), |
| 198 | + )}`, |
| 199 | + ); |
| 200 | + |
| 201 | + // Test full order formatting |
| 202 | + const orderParams = formatOrderParams( |
| 203 | + symbol, |
| 204 | + testPrice, |
| 205 | + testQty, |
| 206 | + symbolFilters, |
| 207 | + ); |
| 208 | + console.log('Formatted order parameters:'); |
| 209 | + console.log(orderParams); |
| 210 | + // example how to use the order params |
| 211 | + const order = await client.submitNewOrder({ |
| 212 | + symbol: orderParams.symbol, |
| 213 | + side: 'BUY', |
| 214 | + type: 'LIMIT', |
| 215 | + quantity: Number(orderParams.quantity), |
| 216 | + price: Number(orderParams.price), |
| 217 | + timeInForce: 'GTC', |
| 218 | + }); |
| 219 | + |
| 220 | + console.log(order); |
| 221 | +} |
| 222 | + |
| 223 | +testSymbolUtils(); |
0 commit comments