From d2dfe26dede39505ac2c0e7d5dabde8decd7dfde Mon Sep 17 00:00:00 2001 From: Pablo Date: Sun, 7 Dec 2025 04:02:16 +0100 Subject: [PATCH 1/7] feat: support algo trades --- src/http-client.js | 111 +++++++++- test/futures-algo-orders.js | 396 ++++++++++++++++++++++++++++++++++++ types/base.d.ts | 14 ++ types/futures.d.ts | 123 ++++++++++- 4 files changed, 632 insertions(+), 12 deletions(-) create mode 100644 test/futures-algo-orders.js diff --git a/src/http-client.js b/src/http-client.js index e09aaa42..58911713 100644 --- a/src/http-client.js +++ b/src/http-client.js @@ -570,15 +570,95 @@ export default opts => { futuresFundingRate: payload => checkParams('fundingRate', payload, ['symbol']) && pubCall('/fapi/v1/fundingRate', payload), - futuresOrder: payload => order(privCall, payload, '/fapi/v1/order'), + futuresOrder: payload => { + // Check if this is a conditional order type that should be routed to algo endpoint + const orderType = payload?.type?.toUpperCase() + const conditionalTypes = [ + 'STOP', + 'STOP_MARKET', + 'TAKE_PROFIT', + 'TAKE_PROFIT_MARKET', + 'TRAILING_STOP_MARKET', + ] + + if (orderType && conditionalTypes.includes(orderType)) { + // Route to algo order endpoint + const algoPayload = { ...payload } + if (!algoPayload.clientAlgoId) { + algoPayload.clientAlgoId = futuresP() + } + delete algoPayload.newClientOrderId + algoPayload.algoType = 'CONDITIONAL' + if (algoPayload.stopPrice && !algoPayload.triggerPrice) { + algoPayload.triggerPrice = algoPayload.stopPrice + delete algoPayload.stopPrice + } + return privCall('/fapi/v1/algoOrder', algoPayload, 'POST') + } + // Use regular order endpoint + return order(privCall, payload, '/fapi/v1/order') + }, futuresBatchOrders: payload => privCall('/fapi/v1/batchOrders', payload, 'POST'), - futuresGetOrder: payload => privCall('/fapi/v1/order', payload), - futuresCancelOrder: payload => privCall('/fapi/v1/order', payload, 'DELETE'), - futuresCancelAllOpenOrders: payload => - privCall('/fapi/v1/allOpenOrders', payload, 'DELETE'), + futuresGetOrder: payload => { + // Check if this is a request for a conditional/algo order + const isConditional = payload?.conditional + const hasAlgoId = payload?.algoId || payload?.clientAlgoId + if (payload && 'conditional' in payload) { + delete payload.conditional + } + + if (isConditional || hasAlgoId) { + return privCall('/fapi/v1/algoOrder', payload) + } + return privCall('/fapi/v1/order', payload) + }, + futuresCancelOrder: payload => { + // Check if this is a request for a conditional/algo order + const isConditional = payload?.conditional + const hasAlgoId = payload?.algoId || payload?.clientAlgoId + if (payload && 'conditional' in payload) { + delete payload.conditional + } + + if (isConditional || hasAlgoId) { + return privCall('/fapi/v1/algoOrder', payload, 'DELETE') + } + return privCall('/fapi/v1/order', payload, 'DELETE') + }, + futuresCancelAllOpenOrders: payload => { + const isConditional = payload?.conditional + if (payload && 'conditional' in payload) { + delete payload.conditional + } + + if (isConditional) { + return privCall('/fapi/v1/algoOpenOrders', payload, 'DELETE') + } + return privCall('/fapi/v1/allOpenOrders', payload, 'DELETE') + }, futuresCancelBatchOrders: payload => privCall('/fapi/v1/batchOrders', payload, 'DELETE'), - futuresOpenOrders: payload => privCall('/fapi/v1/openOrders', payload), - futuresAllOrders: payload => privCall('/fapi/v1/allOrders', payload), + futuresOpenOrders: payload => { + const isConditional = payload?.conditional + if (payload && 'conditional' in payload) { + delete payload.conditional + } + + if (isConditional) { + return privCall('/fapi/v1/openAlgoOrders', payload) + } + return privCall('/fapi/v1/openOrders', payload) + }, + futuresAllOrders: payload => { + const isConditional = payload?.conditional + if (payload && 'conditional' in payload) { + delete payload.conditional + } + + if (isConditional) { + return privCall('/fapi/v1/allAlgoOrders', payload) + } + return privCall('/fapi/v1/allOrders', payload) + }, futuresPositionRisk: payload => privCall('/fapi/v2/positionRisk', payload), futuresLeverageBracket: payload => privCall('/fapi/v1/leverageBracket', payload), futuresAccountBalance: payload => privCall('/fapi/v2/balance', payload), @@ -595,6 +675,23 @@ export default opts => { getMultiAssetsMargin: payload => privCall('/fapi/v1/multiAssetsMargin', payload), setMultiAssetsMargin: payload => privCall('/fapi/v1/multiAssetsMargin', payload, 'POST'), + // Algo Orders (Conditional Orders) + futuresCreateAlgoOrder: payload => { + if (!payload.clientAlgoId) { + payload.clientAlgoId = futuresP() + } + if (!payload.algoType) { + payload.algoType = 'CONDITIONAL' + } + return privCall('/fapi/v1/algoOrder', payload, 'POST') + }, + futuresCancelAlgoOrder: payload => privCall('/fapi/v1/algoOrder', payload, 'DELETE'), + futuresCancelAllAlgoOpenOrders: payload => + privCall('/fapi/v1/algoOpenOrders', payload, 'DELETE'), + futuresGetAlgoOrder: payload => privCall('/fapi/v1/algoOrder', payload), + futuresGetOpenAlgoOrders: payload => privCall('/fapi/v1/openAlgoOrders', payload), + futuresGetAllAlgoOrders: payload => privCall('/fapi/v1/allAlgoOrders', payload), + // Delivery endpoints deliveryPing: () => pubCall('/dapi/v1/ping').then(() => true), deliveryTime: () => pubCall('/dapi/v1/time').then(r => r.serverTime), diff --git a/test/futures-algo-orders.js b/test/futures-algo-orders.js new file mode 100644 index 00000000..c757ead3 --- /dev/null +++ b/test/futures-algo-orders.js @@ -0,0 +1,396 @@ +import test from 'ava' + +import Binance from 'index' + +import { checkFields } from './utils' +import { binanceConfig, hasTestCredentials } from './config' + +const main = () => { + if (!hasTestCredentials()) { + return test('[FUTURES ALGO] ⚠️ Skipping tests.', t => { + t.log('Provide an API_KEY and API_SECRET to run futures algo order tests.') + t.pass() + }) + } + + // Create client with testnet and proxy + const client = Binance(binanceConfig) + + // Helper to get current LTC futures price for realistic test orders + let currentLTCPrice = null + const getCurrentPrice = async () => { + if (currentLTCPrice) return currentLTCPrice + const prices = await client.futuresPrices({ symbol: 'LTCUSDT' }) + currentLTCPrice = parseFloat(prices.LTCUSDT) + return currentLTCPrice + } + + // Helper to get position information + const getPositionSide = async () => { + const positions = await client.futuresPositionRisk({ symbol: 'LTCUSDT' }) + return positions[0]?.positionSide || 'BOTH' + } + + // ===== Explicit Algo Order Methods Tests ===== + + test('[FUTURES ALGO] futuresCreateAlgoOrder - create a STOP_MARKET algo order', async t => { + const currentPrice = await getCurrentPrice() + const positionSide = await getPositionSide() + const triggerPrice = currentPrice * 2.0 + + const order = await client.futuresCreateAlgoOrder({ + symbol: 'LTCUSDT', + side: 'BUY', + positionSide, + type: 'STOP_MARKET', + algoType: 'CONDITIONAL', + quantity: '1', + triggerPrice: triggerPrice.toFixed(2), + }) + + t.truthy(order) + checkFields(t, order, ['symbol', 'algoId', 'clientAlgoId', 'algoType']) + t.is(order.symbol, 'LTCUSDT') + t.is(order.algoType, 'CONDITIONAL') + + // Clean up - cancel the algo order, ignore errors if already cancelled + try { + await client.futuresCancelAlgoOrder({ + symbol: 'LTCUSDT', + algoId: order.algoId, + }) + } catch (e) { + // Ignore if order already cancelled, triggered, or filled + } + }) + + test('[FUTURES ALGO] futuresCreateAlgoOrder - create a TAKE_PROFIT_MARKET algo order', async t => { + const currentPrice = await getCurrentPrice() + const positionSide = await getPositionSide() + const triggerPrice = currentPrice * 0.2 + + const order = await client.futuresCreateAlgoOrder({ + symbol: 'LTCUSDT', + side: 'BUY', + positionSide, + type: 'TAKE_PROFIT_MARKET', + algoType: 'CONDITIONAL', + quantity: '1', + triggerPrice: triggerPrice.toFixed(2), + }) + + t.truthy(order) + checkFields(t, order, ['symbol', 'algoId', 'clientAlgoId', 'algoType']) + t.is(order.symbol, 'LTCUSDT') + + // Clean up - try to cancel but don't fail test if it doesn't exist + try { + await client.futuresCancelAlgoOrder({ + symbol: 'LTCUSDT', + algoId: order.algoId, + }) + } catch (e) { + // Ignore if order no longer exists + } + }) + + test('[FUTURES ALGO] futuresGetAlgoOrder - query a specific algo order', async t => { + const currentPrice = await getCurrentPrice() + const positionSide = await getPositionSide() + + // Create an algo order first + const createOrder = await client.futuresCreateAlgoOrder({ + symbol: 'LTCUSDT', + side: 'BUY', + positionSide, + type: 'STOP_MARKET', + algoType: 'CONDITIONAL', + quantity: '1', + triggerPrice: (currentPrice * 2.0).toFixed(2), + }) + + try { + // Query the order + const order = await client.futuresGetAlgoOrder({ + symbol: 'LTCUSDT', + algoId: createOrder.algoId, + }) + + t.truthy(order) + checkFields(t, order, ['symbol', 'algoId', 'clientAlgoId']) + t.is(order.algoId.toString(), createOrder.algoId.toString()) + t.is(order.symbol, 'LTCUSDT') + } finally { + // Clean up - try to cancel even if query fails + try { + await client.futuresCancelAlgoOrder({ + symbol: 'LTCUSDT', + algoId: createOrder.algoId, + }) + } catch (e) { + // Ignore if already cancelled + } + } + }) + + test('[FUTURES ALGO] futuresCancelAlgoOrder - cancel an algo order', async t => { + const currentPrice = await getCurrentPrice() + const positionSide = await getPositionSide() + + // Create an algo order + const order = await client.futuresCreateAlgoOrder({ + symbol: 'LTCUSDT', + side: 'BUY', + positionSide, + type: 'STOP_MARKET', + algoType: 'CONDITIONAL', + quantity: '1', + triggerPrice: (currentPrice * 2.0).toFixed(2), + }) + + // Cancel the order + try { + const result = await client.futuresCancelAlgoOrder({ + symbol: 'LTCUSDT', + algoId: order.algoId, + }) + + t.truthy(result) + checkFields(t, result, ['algoId']) + t.is(result.algoId.toString(), order.algoId.toString()) + } catch (e) { + // If order was already triggered/cancelled, just verify we got the right error + t.is(e.code, -2011) + } + }) + + test('[FUTURES ALGO] futuresGetOpenAlgoOrders - get all open algo orders', async t => { + const orders = await client.futuresGetOpenAlgoOrders({ + symbol: 'LTCUSDT', + }) + + t.true(Array.isArray(orders)) + // Orders array may be empty if no open algo orders + if (orders.length > 0) { + const [order] = orders + checkFields(t, order, ['symbol', 'algoId', 'clientAlgoId', 'side', 'type', 'algoType']) + } + }) + + test('[FUTURES ALGO] futuresGetAllAlgoOrders - get algo orders history', async t => { + const orders = await client.futuresGetAllAlgoOrders({ + symbol: 'LTCUSDT', + }) + + t.true(Array.isArray(orders)) + // History may be empty if no algo orders have been placed + }) + + test('[FUTURES ALGO] futuresCancelAllAlgoOpenOrders - cancel all open algo orders', async t => { + // Create a couple of algo orders first + const currentPrice = await getCurrentPrice() + const positionSide = await getPositionSide() + + await client.futuresCreateAlgoOrder({ + symbol: 'LTCUSDT', + side: 'BUY', + positionSide, + type: 'STOP_MARKET', + algoType: 'CONDITIONAL', + quantity: '1', + triggerPrice: (currentPrice * 2.0).toFixed(2), + }) + + await client.futuresCreateAlgoOrder({ + symbol: 'LTCUSDT', + side: 'BUY', + positionSide, + type: 'TAKE_PROFIT_MARKET', + algoType: 'CONDITIONAL', + quantity: '1', + triggerPrice: (currentPrice * 0.2).toFixed(2), + }) + + // Cancel all algo orders + const result = await client.futuresCancelAllAlgoOpenOrders({ + symbol: 'LTCUSDT', + }) + + t.truthy(result) + // Should return success response + t.true('code' in result || 'msg' in result) + }) + + // ===== Auto-Routing Tests ===== + + test('[FUTURES ALGO] futuresOrder - auto-routes STOP_MARKET to algo endpoint', async t => { + const currentPrice = await getCurrentPrice() + const positionSide = await getPositionSide() + + // Create a conditional order using the regular futuresOrder method + // It should automatically route to algo endpoint + const order = await client.futuresOrder({ + symbol: 'LTCUSDT', + side: 'BUY', + positionSide, + type: 'STOP_MARKET', + quantity: '1', + stopPrice: (currentPrice * 2.0).toFixed(2), + }) + + t.truthy(order) + // Should have algoId instead of orderId because it was routed to algo endpoint + t.truthy(order.algoId) + t.is(order.symbol, 'LTCUSDT') + + // Clean up - ignore errors if order no longer exists + try { + await client.futuresCancelAlgoOrder({ + symbol: 'LTCUSDT', + algoId: order.algoId, + }) + } catch (e) { + // Ignore if order already cancelled, triggered, or filled + } + }) + + test('[FUTURES ALGO] futuresOrder - auto-routes TAKE_PROFIT_MARKET to algo endpoint', async t => { + const currentPrice = await getCurrentPrice() + const positionSide = await getPositionSide() + + const order = await client.futuresOrder({ + symbol: 'LTCUSDT', + side: 'BUY', + positionSide, + type: 'TAKE_PROFIT_MARKET', + quantity: '1', + stopPrice: (currentPrice * 0.2).toFixed(2), + }) + + t.truthy(order) + t.truthy(order.algoId) + + // Clean up - ignore errors if order no longer exists + try { + await client.futuresCancelAlgoOrder({ + symbol: 'LTCUSDT', + algoId: order.algoId, + }) + } catch (e) { + // Ignore if order already cancelled, triggered, or filled + } + }) + + test('[FUTURES ALGO] futuresGetOrder with conditional=true - query algo order', async t => { + const currentPrice = await getCurrentPrice() + const positionSide = await getPositionSide() + + // Create an algo order + const createOrder = await client.futuresCreateAlgoOrder({ + symbol: 'LTCUSDT', + side: 'BUY', + positionSide, + type: 'STOP_MARKET', + algoType: 'CONDITIONAL', + quantity: '1', + triggerPrice: (currentPrice * 2.0).toFixed(2), + }) + + try { + // Query using futuresGetOrder with conditional=true + const order = await client.futuresGetOrder({ + symbol: 'LTCUSDT', + algoId: createOrder.algoId, + conditional: true, + }) + + t.truthy(order) + t.is(order.algoId.toString(), createOrder.algoId.toString()) + } finally { + // Clean up - try to cancel even if query fails + try { + await client.futuresCancelAlgoOrder({ + symbol: 'LTCUSDT', + algoId: createOrder.algoId, + }) + } catch (e) { + // Ignore if already cancelled + } + } + }) + + test('[FUTURES ALGO] futuresOpenOrders with conditional=true - get open algo orders', async t => { + const orders = await client.futuresOpenOrders({ + symbol: 'LTCUSDT', + conditional: true, + }) + + t.true(Array.isArray(orders)) + }) + + test('[FUTURES ALGO] futuresAllOrders with conditional=true - get algo orders history', async t => { + const orders = await client.futuresAllOrders({ + symbol: 'LTCUSDT', + conditional: true, + }) + + t.true(Array.isArray(orders)) + }) + + test('[FUTURES ALGO] futuresCancelOrder with conditional=true - cancel algo order', async t => { + const currentPrice = await getCurrentPrice() + const positionSide = await getPositionSide() + + // Create an algo order + const order = await client.futuresCreateAlgoOrder({ + symbol: 'LTCUSDT', + side: 'BUY', + positionSide, + type: 'STOP_MARKET', + algoType: 'CONDITIONAL', + quantity: '1', + triggerPrice: (currentPrice * 2.0).toFixed(2), + }) + + // Cancel using futuresCancelOrder with conditional=true + try { + const result = await client.futuresCancelOrder({ + symbol: 'LTCUSDT', + algoId: order.algoId, + conditional: true, + }) + + t.truthy(result) + t.is(result.algoId.toString(), order.algoId.toString()) + } catch (e) { + // If order was already triggered/cancelled, just verify we got the right error + t.is(e.code, -2011) + } + }) + + test('[FUTURES ALGO] futuresCancelAllOpenOrders with conditional=true', async t => { + const currentPrice = await getCurrentPrice() + const positionSide = await getPositionSide() + + // Create a couple of algo orders + await client.futuresCreateAlgoOrder({ + symbol: 'LTCUSDT', + side: 'BUY', + positionSide, + type: 'STOP_MARKET', + algoType: 'CONDITIONAL', + quantity: '1', + triggerPrice: (currentPrice * 2.0).toFixed(2), + }) + + // Cancel all using futuresCancelAllOpenOrders with conditional=true + const result = await client.futuresCancelAllOpenOrders({ + symbol: 'LTCUSDT', + conditional: true, + }) + + t.truthy(result) + }) +} + +main() diff --git a/types/base.d.ts b/types/base.d.ts index 4c42af70..59cdb75f 100644 --- a/types/base.d.ts +++ b/types/base.d.ts @@ -54,6 +54,20 @@ export enum TimeInForce { FOK = 'FOK' // Fill or Kill } +export enum OrderStatus { + NEW = 'NEW', + PARTIALLY_FILLED = 'PARTIALLY_FILLED', + FILLED = 'FILLED', + CANCELED = 'CANCELED', + PENDING_CANCEL = 'PENDING_CANCEL', + REJECTED = 'REJECTED', + EXPIRED = 'EXPIRED', + ACCEPTED = 'ACCEPTED', + TRIGGERING = 'TRIGGERING', + TRIGGERED = 'TRIGGERED', + FINISHED = 'FINISHED' +} + export enum TradingType { SPOT = 'SPOT', MARGIN = 'MARGIN' diff --git a/types/futures.d.ts b/types/futures.d.ts index f4fd3775..c4192cac 100644 --- a/types/futures.d.ts +++ b/types/futures.d.ts @@ -222,7 +222,7 @@ export interface FuturesEndpoints extends BinanceRestClient { commissionAsset: string; }>; }>>; - futuresGetOrder(payload: { symbol: string; orderId?: number; origClientOrderId?: string }): Promise<{ + futuresGetOrder(payload: { symbol: string; orderId?: number; origClientOrderId?: string; conditional?: boolean; algoId?: number; clientAlgoId?: string }): Promise<{ symbol: string; orderId: number; clientOrderId: string; @@ -244,7 +244,7 @@ export interface FuturesEndpoints extends BinanceRestClient { commissionAsset: string; }>; }>; - futuresCancelOrder(payload: { symbol: string; orderId?: number; origClientOrderId?: string }): Promise<{ + futuresCancelOrder(payload: { symbol: string; orderId?: number; origClientOrderId?: string; conditional?: boolean; algoId?: number; clientAlgoId?: string }): Promise<{ symbol: string; origClientOrderId: string; orderId: number; @@ -260,7 +260,7 @@ export interface FuturesEndpoints extends BinanceRestClient { type: OrderType; side: OrderSide; }>; - futuresCancelAllOpenOrders(payload: { symbol: string }): Promise>; - futuresOpenOrders(payload?: { symbol?: string }): Promise; }>>; - futuresAllOrders(payload: { symbol: string; orderId?: number; startTime?: number; endTime?: number; limit?: number }): Promise; + + // Algo Orders (Conditional Orders) + futuresCreateAlgoOrder(payload: { + symbol: string; + side: OrderSide; + type: 'STOP' | 'TAKE_PROFIT' | 'STOP_MARKET' | 'TAKE_PROFIT_MARKET' | 'TRAILING_STOP_MARKET'; + algoType?: 'CONDITIONAL'; + positionSide?: 'BOTH' | 'LONG' | 'SHORT'; + timeInForce?: TimeInForce; + quantity?: string; + price?: string; + triggerPrice?: string; + workingType?: 'MARK_PRICE' | 'CONTRACT_PRICE'; + priceMatch?: string; + closePosition?: boolean; + priceProtect?: string; + reduceOnly?: string; + activationPrice?: string; + callbackRate?: string; + clientAlgoId?: string; + selfTradePreventionMode?: 'EXPIRE_TAKER' | 'EXPIRE_MAKER' | 'EXPIRE_BOTH' | 'NONE'; + goodTillDate?: number; + newOrderRespType?: 'ACK' | 'RESULT'; + recvWindow?: number; + }): Promise<{ + symbol: string; + algoId: number; + clientAlgoId: string; + transactTime: number; + algoType: string; + side: OrderSide; + type: string; + status: OrderStatus; + }>; + + futuresCancelAlgoOrder(payload: { + symbol: string; + algoId?: number; + clientAlgoId?: string; + recvWindow?: number; + }): Promise<{ + symbol: string; + algoId: number; + clientAlgoId: string; + status: OrderStatus; + }>; + + futuresCancelAllAlgoOpenOrders(payload: { + symbol: string; + recvWindow?: number; + }): Promise<{ + code: number; + msg: string; + }>; + + futuresGetAlgoOrder(payload: { + symbol: string; + algoId?: number; + clientAlgoId?: string; + recvWindow?: number; + }): Promise<{ + symbol: string; + algoId: number; + clientAlgoId: string; + side: OrderSide; + type: string; + algoType: string; + quantity: string; + price?: string; + triggerPrice?: string; + status: OrderStatus; + createTime: number; + updateTime: number; + }>; + + futuresGetOpenAlgoOrders(payload?: { + symbol?: string; + recvWindow?: number; + }): Promise>; + + futuresGetAllAlgoOrders(payload: { + symbol: string; + startTime?: number; + endTime?: number; + limit?: number; + recvWindow?: number; + }): Promise>; } \ No newline at end of file From 6190e05f27b5ce46dda04c548791087eed9821fb Mon Sep 17 00:00:00 2001 From: Pablo Date: Sun, 7 Dec 2025 04:21:48 +0100 Subject: [PATCH 2/7] add static tests --- test/static-tests.js | 192 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/test/static-tests.js b/test/static-tests.js index 0004ac76..69013eab 100644 --- a/test/static-tests.js +++ b/test/static-tests.js @@ -377,3 +377,195 @@ test.serial('[REST] delivery MarketBuy', async t => { t.is(obj.quantity, '0.1') t.true(obj.newClientOrderId.startsWith(CONTRACT_PREFIX)) }) + +// Algo order tests +test.serial('[REST] Futures Create Algo Order', async t => { + await binance.futuresCreateAlgoOrder({ + symbol: 'BTCUSDT', + side: 'BUY', + type: 'STOP_MARKET', + quantity: '1', + triggerPrice: '50000', + }) + t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/algoOrder')) + const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/algoOrder?', '')) + t.is(obj.symbol, 'BTCUSDT') + t.is(obj.side, 'BUY') + t.is(obj.type, 'STOP_MARKET') + t.is(obj.quantity, '1') + t.is(obj.triggerPrice, '50000') + t.is(obj.algoType, 'CONDITIONAL') + t.true(obj.clientAlgoId.startsWith(CONTRACT_PREFIX)) +}) + +test.serial('[REST] Futures Cancel Algo Order', async t => { + await binance.futuresCancelAlgoOrder({ + symbol: 'BTCUSDT', + algoId: '12345', + }) + const url = 'https://fapi.binance.com/fapi/v1/algoOrder' + t.true(interceptedUrl.startsWith(url)) + const obj = urlToObject(interceptedUrl.replace(url, '')) + t.is(obj.symbol, 'BTCUSDT') + t.is(obj.algoId, '12345') +}) + +test.serial('[REST] Futures Get Algo Order', async t => { + await binance.futuresGetAlgoOrder({ + symbol: 'BTCUSDT', + algoId: '12345', + }) + t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/algoOrder')) + const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/algoOrder', '')) + t.is(obj.symbol, 'BTCUSDT') + t.is(obj.algoId, '12345') +}) + +test.serial('[REST] Futures Get Open Algo Orders', async t => { + await binance.futuresGetOpenAlgoOrders({ + symbol: 'BTCUSDT', + }) + t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/openAlgoOrders')) + const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/openAlgoOrders', '')) + t.is(obj.symbol, 'BTCUSDT') +}) + +test.serial('[REST] Futures Get All Algo Orders', async t => { + await binance.futuresGetAllAlgoOrders({ + symbol: 'BTCUSDT', + startTime: '1609459200000', + endTime: '1609545600000', + }) + t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/allAlgoOrders')) + const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/allAlgoOrders', '')) + t.is(obj.symbol, 'BTCUSDT') + t.is(obj.startTime, '1609459200000') + t.is(obj.endTime, '1609545600000') +}) + +test.serial('[REST] Futures Cancel All Algo Open Orders', async t => { + await binance.futuresCancelAllAlgoOpenOrders({ + symbol: 'BTCUSDT', + }) + const url = 'https://fapi.binance.com/fapi/v1/algoOpenOrders' + t.true(interceptedUrl.startsWith(url)) + const obj = urlToObject(interceptedUrl.replace(url, '')) + t.is(obj.symbol, 'BTCUSDT') +}) + +test.serial('[REST] Futures Order Auto-routes STOP_MARKET to Algo', async t => { + await binance.futuresOrder({ + symbol: 'BTCUSDT', + side: 'BUY', + type: 'STOP_MARKET', + quantity: '1', + stopPrice: '50000', + }) + t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/algoOrder')) + const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/algoOrder?', '')) + t.is(obj.symbol, 'BTCUSDT') + t.is(obj.side, 'BUY') + t.is(obj.type, 'STOP_MARKET') + t.is(obj.quantity, '1') + t.is(obj.triggerPrice, '50000') + t.is(obj.algoType, 'CONDITIONAL') + t.true(obj.clientAlgoId.startsWith(CONTRACT_PREFIX)) + t.is(obj.newClientOrderId, undefined) +}) + +test.serial('[REST] Futures Order Auto-routes TAKE_PROFIT_MARKET to Algo', async t => { + await binance.futuresOrder({ + symbol: 'BTCUSDT', + side: 'SELL', + type: 'TAKE_PROFIT_MARKET', + quantity: '1', + stopPrice: '60000', + }) + t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/algoOrder')) + const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/algoOrder?', '')) + t.is(obj.symbol, 'BTCUSDT') + t.is(obj.side, 'SELL') + t.is(obj.type, 'TAKE_PROFIT_MARKET') + t.is(obj.quantity, '1') + t.is(obj.triggerPrice, '60000') + t.is(obj.algoType, 'CONDITIONAL') +}) + +test.serial('[REST] Futures Order Auto-routes TRAILING_STOP_MARKET to Algo', async t => { + await binance.futuresOrder({ + symbol: 'BTCUSDT', + side: 'SELL', + type: 'TRAILING_STOP_MARKET', + quantity: '1', + callbackRate: '1.0', + }) + t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/algoOrder')) + const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/algoOrder?', '')) + t.is(obj.symbol, 'BTCUSDT') + t.is(obj.side, 'SELL') + t.is(obj.type, 'TRAILING_STOP_MARKET') + t.is(obj.quantity, '1') + t.is(obj.callbackRate, '1.0') + t.is(obj.algoType, 'CONDITIONAL') +}) + +test.serial('[REST] Futures GetOrder with conditional parameter', async t => { + await binance.futuresGetOrder({ + symbol: 'BTCUSDT', + algoId: '12345', + conditional: true, + }) + t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/algoOrder')) + const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/algoOrder', '')) + t.is(obj.symbol, 'BTCUSDT') + t.is(obj.algoId, '12345') + t.is(obj.conditional, undefined) +}) + +test.serial('[REST] Futures CancelOrder with conditional parameter', async t => { + await binance.futuresCancelOrder({ + symbol: 'BTCUSDT', + algoId: '12345', + conditional: true, + }) + const url = 'https://fapi.binance.com/fapi/v1/algoOrder' + t.true(interceptedUrl.startsWith(url)) + const obj = urlToObject(interceptedUrl.replace(url, '')) + t.is(obj.symbol, 'BTCUSDT') + t.is(obj.algoId, '12345') + t.is(obj.conditional, undefined) +}) + +test.serial('[REST] Futures OpenOrders with conditional parameter', async t => { + await binance.futuresOpenOrders({ + symbol: 'BTCUSDT', + conditional: true, + }) + t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/openAlgoOrders')) + const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/openAlgoOrders', '')) + t.is(obj.symbol, 'BTCUSDT') + t.is(obj.conditional, undefined) +}) + +test.serial('[REST] Futures AllOrders with conditional parameter', async t => { + await binance.futuresAllOrders({ + symbol: 'BTCUSDT', + conditional: true, + }) + t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/allAlgoOrders')) + const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/allAlgoOrders', '')) + t.is(obj.symbol, 'BTCUSDT') + t.is(obj.conditional, undefined) +}) + +test.serial('[REST] Futures CancelAllOpenOrders with conditional parameter', async t => { + await binance.futuresCancelAllOpenOrders({ + symbol: 'BTCUSDT', + conditional: true, + }) + const url = 'https://fapi.binance.com/fapi/v1/algoOpenOrders' + t.true(interceptedUrl.startsWith(url)) + const obj = urlToObject(interceptedUrl.replace(url, '')) + t.is(obj.symbol, 'BTCUSDT') + t.is(obj.conditional, undefined) +}) From 127666978353007bc7c63a6105b12ef20ce20864 Mon Sep 17 00:00:00 2001 From: Pablo Date: Sun, 7 Dec 2025 04:23:39 +0100 Subject: [PATCH 3/7] increase timeout --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d375d803..ae2333d2 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "build": "rm -rf dist && babel src -d dist", "prepare": "npm run build", "test": "npm run test:ava && npm run test:browser", - "test:ava": "ava --timeout=10s -v", + "test:ava": "ava --timeout=20s -v", "test:browser": "node test/browser/browser-test-runner.mjs && ava test/browser/crypto-browser-playwright.js test/browser/websocket-browser.test.js --timeout=15s -v", "test:browser:signature": "node test/browser/browser-test-runner.mjs", "test:browser:websocket": "ava test/browser/websocket-browser.test.js --timeout=15s -v", From 70e5f59bbbaa439efd96f55c7a3a43be36bc3cf7 Mon Sep 17 00:00:00 2001 From: Pablo Date: Sun, 7 Dec 2025 10:21:57 +0100 Subject: [PATCH 4/7] lint --- test/static-tests.js | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/test/static-tests.js b/test/static-tests.js index 69013eab..ffaa1c8a 100644 --- a/test/static-tests.js +++ b/test/static-tests.js @@ -388,7 +388,9 @@ test.serial('[REST] Futures Create Algo Order', async t => { triggerPrice: '50000', }) t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/algoOrder')) - const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/algoOrder?', '')) + const obj = urlToObject( + interceptedUrl.replace('https://fapi.binance.com/fapi/v1/algoOrder?', ''), + ) t.is(obj.symbol, 'BTCUSDT') t.is(obj.side, 'BUY') t.is(obj.type, 'STOP_MARKET') @@ -416,7 +418,9 @@ test.serial('[REST] Futures Get Algo Order', async t => { algoId: '12345', }) t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/algoOrder')) - const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/algoOrder', '')) + const obj = urlToObject( + interceptedUrl.replace('https://fapi.binance.com/fapi/v1/algoOrder', ''), + ) t.is(obj.symbol, 'BTCUSDT') t.is(obj.algoId, '12345') }) @@ -426,7 +430,9 @@ test.serial('[REST] Futures Get Open Algo Orders', async t => { symbol: 'BTCUSDT', }) t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/openAlgoOrders')) - const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/openAlgoOrders', '')) + const obj = urlToObject( + interceptedUrl.replace('https://fapi.binance.com/fapi/v1/openAlgoOrders', ''), + ) t.is(obj.symbol, 'BTCUSDT') }) @@ -437,7 +443,9 @@ test.serial('[REST] Futures Get All Algo Orders', async t => { endTime: '1609545600000', }) t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/allAlgoOrders')) - const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/allAlgoOrders', '')) + const obj = urlToObject( + interceptedUrl.replace('https://fapi.binance.com/fapi/v1/allAlgoOrders', ''), + ) t.is(obj.symbol, 'BTCUSDT') t.is(obj.startTime, '1609459200000') t.is(obj.endTime, '1609545600000') @@ -462,7 +470,9 @@ test.serial('[REST] Futures Order Auto-routes STOP_MARKET to Algo', async t => { stopPrice: '50000', }) t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/algoOrder')) - const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/algoOrder?', '')) + const obj = urlToObject( + interceptedUrl.replace('https://fapi.binance.com/fapi/v1/algoOrder?', ''), + ) t.is(obj.symbol, 'BTCUSDT') t.is(obj.side, 'BUY') t.is(obj.type, 'STOP_MARKET') @@ -482,7 +492,9 @@ test.serial('[REST] Futures Order Auto-routes TAKE_PROFIT_MARKET to Algo', async stopPrice: '60000', }) t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/algoOrder')) - const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/algoOrder?', '')) + const obj = urlToObject( + interceptedUrl.replace('https://fapi.binance.com/fapi/v1/algoOrder?', ''), + ) t.is(obj.symbol, 'BTCUSDT') t.is(obj.side, 'SELL') t.is(obj.type, 'TAKE_PROFIT_MARKET') @@ -500,7 +512,9 @@ test.serial('[REST] Futures Order Auto-routes TRAILING_STOP_MARKET to Algo', asy callbackRate: '1.0', }) t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/algoOrder')) - const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/algoOrder?', '')) + const obj = urlToObject( + interceptedUrl.replace('https://fapi.binance.com/fapi/v1/algoOrder?', ''), + ) t.is(obj.symbol, 'BTCUSDT') t.is(obj.side, 'SELL') t.is(obj.type, 'TRAILING_STOP_MARKET') @@ -516,7 +530,9 @@ test.serial('[REST] Futures GetOrder with conditional parameter', async t => { conditional: true, }) t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/algoOrder')) - const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/algoOrder', '')) + const obj = urlToObject( + interceptedUrl.replace('https://fapi.binance.com/fapi/v1/algoOrder', ''), + ) t.is(obj.symbol, 'BTCUSDT') t.is(obj.algoId, '12345') t.is(obj.conditional, undefined) @@ -542,7 +558,9 @@ test.serial('[REST] Futures OpenOrders with conditional parameter', async t => { conditional: true, }) t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/openAlgoOrders')) - const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/openAlgoOrders', '')) + const obj = urlToObject( + interceptedUrl.replace('https://fapi.binance.com/fapi/v1/openAlgoOrders', ''), + ) t.is(obj.symbol, 'BTCUSDT') t.is(obj.conditional, undefined) }) @@ -553,7 +571,9 @@ test.serial('[REST] Futures AllOrders with conditional parameter', async t => { conditional: true, }) t.true(interceptedUrl.startsWith('https://fapi.binance.com/fapi/v1/allAlgoOrders')) - const obj = urlToObject(interceptedUrl.replace('https://fapi.binance.com/fapi/v1/allAlgoOrders', '')) + const obj = urlToObject( + interceptedUrl.replace('https://fapi.binance.com/fapi/v1/allAlgoOrders', ''), + ) t.is(obj.symbol, 'BTCUSDT') t.is(obj.conditional, undefined) }) From 75b3cb67f66fe299d1160c75efbb93c3b7563c73 Mon Sep 17 00:00:00 2001 From: Pablo Date: Sun, 7 Dec 2025 10:41:03 +0100 Subject: [PATCH 5/7] increase timeout --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ae2333d2..c4bb6cf5 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "build": "rm -rf dist && babel src -d dist", "prepare": "npm run build", "test": "npm run test:ava && npm run test:browser", - "test:ava": "ava --timeout=20s -v", + "test:ava": "ava --timeout=60s -v", "test:browser": "node test/browser/browser-test-runner.mjs && ava test/browser/crypto-browser-playwright.js test/browser/websocket-browser.test.js --timeout=15s -v", "test:browser:signature": "node test/browser/browser-test-runner.mjs", "test:browser:websocket": "ava test/browser/websocket-browser.test.js --timeout=15s -v", From c1d3d6c2c7128b0cfa6f607a6514944d988e6c31 Mon Sep 17 00:00:00 2001 From: Pablo Date: Sun, 7 Dec 2025 11:08:03 +0100 Subject: [PATCH 6/7] increase timeout --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c4bb6cf5..dd076631 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "build": "rm -rf dist && babel src -d dist", "prepare": "npm run build", "test": "npm run test:ava && npm run test:browser", - "test:ava": "ava --timeout=60s -v", + "test:ava": "ava --timeout=90s -v", "test:browser": "node test/browser/browser-test-runner.mjs && ava test/browser/crypto-browser-playwright.js test/browser/websocket-browser.test.js --timeout=15s -v", "test:browser:signature": "node test/browser/browser-test-runner.mjs", "test:browser:websocket": "ava test/browser/websocket-browser.test.js --timeout=15s -v", From c32114d38eb744e2a699e92836c726e621b7b1ff Mon Sep 17 00:00:00 2001 From: Pablo Date: Sun, 7 Dec 2025 11:51:21 +0100 Subject: [PATCH 7/7] copilot changes --- src/http-client.js | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/http-client.js b/src/http-client.js index 58911713..b74194d0 100644 --- a/src/http-client.js +++ b/src/http-client.js @@ -603,61 +603,71 @@ export default opts => { // Check if this is a request for a conditional/algo order const isConditional = payload?.conditional const hasAlgoId = payload?.algoId || payload?.clientAlgoId + let payloadCopy = payload if (payload && 'conditional' in payload) { - delete payload.conditional + payloadCopy = { ...payload } + delete payloadCopy.conditional } if (isConditional || hasAlgoId) { - return privCall('/fapi/v1/algoOrder', payload) + return privCall('/fapi/v1/algoOrder', payloadCopy) } - return privCall('/fapi/v1/order', payload) + return privCall('/fapi/v1/order', payloadCopy) }, futuresCancelOrder: payload => { // Check if this is a request for a conditional/algo order const isConditional = payload?.conditional const hasAlgoId = payload?.algoId || payload?.clientAlgoId + let payloadCopy = payload if (payload && 'conditional' in payload) { - delete payload.conditional + payloadCopy = { ...payload } + delete payloadCopy.conditional } if (isConditional || hasAlgoId) { - return privCall('/fapi/v1/algoOrder', payload, 'DELETE') + return privCall('/fapi/v1/algoOrder', payloadCopy, 'DELETE') } - return privCall('/fapi/v1/order', payload, 'DELETE') + return privCall('/fapi/v1/order', payloadCopy, 'DELETE') }, futuresCancelAllOpenOrders: payload => { const isConditional = payload?.conditional + let payloadCopy = payload if (payload && 'conditional' in payload) { - delete payload.conditional + payloadCopy = { ...payload } + delete payloadCopy.conditional } if (isConditional) { - return privCall('/fapi/v1/algoOpenOrders', payload, 'DELETE') + return privCall('/fapi/v1/algoOpenOrders', payloadCopy, 'DELETE') } - return privCall('/fapi/v1/allOpenOrders', payload, 'DELETE') + return privCall('/fapi/v1/allOpenOrders', payloadCopy, 'DELETE') }, futuresCancelBatchOrders: payload => privCall('/fapi/v1/batchOrders', payload, 'DELETE'), futuresOpenOrders: payload => { const isConditional = payload?.conditional + let payloadCopy = payload if (payload && 'conditional' in payload) { - delete payload.conditional + payloadCopy = { ...payload } + delete payloadCopy.conditional } if (isConditional) { - return privCall('/fapi/v1/openAlgoOrders', payload) + return privCall('/fapi/v1/openAlgoOrders', payloadCopy) } - return privCall('/fapi/v1/openOrders', payload) + return privCall('/fapi/v1/openOrders', payloadCopy) }, futuresAllOrders: payload => { const isConditional = payload?.conditional + let payloadCopy = payload if (payload && 'conditional' in payload) { - delete payload.conditional + payloadCopy = { ...payload } + delete payloadCopy.conditional } if (isConditional) { - return privCall('/fapi/v1/allAlgoOrders', payload) + return privCall('/fapi/v1/allAlgoOrders', payloadCopy) } - return privCall('/fapi/v1/allOrders', payload) + return privCall('/fapi/v1/allOrders', payloadCopy) }, futuresPositionRisk: payload => privCall('/fapi/v2/positionRisk', payload), futuresLeverageBracket: payload => privCall('/fapi/v1/leverageBracket', payload),