@gbozee/ultimate 0.0.2-next.73 → 0.0.2-next.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -71936,9 +71936,18 @@ class BaseExchange {
71936
71936
  async previewSpotMarginHedge(_payload) {
71937
71937
  throw new Error("Spot margin hedge preview is not supported on this exchange");
71938
71938
  }
71939
+ async previewFuturesReplay(_payload) {
71940
+ throw new Error("Futures replay preview is not supported on this exchange");
71941
+ }
71939
71942
  async placeSpotMarginHedge(_payload) {
71940
71943
  throw new Error("Spot margin hedge placement is not supported on this exchange");
71941
71944
  }
71945
+ async placeFuturesReplay(_payload) {
71946
+ throw new Error("Futures replay placement is not supported on this exchange");
71947
+ }
71948
+ async placeFuturesExactTrade(_payload) {
71949
+ throw new Error("Futures exact trade placement is not supported on this exchange");
71950
+ }
71942
71951
  async placeSpotLimitOrders(_payload) {}
71943
71952
  async buildCumulative(payload) {
71944
71953
  const {
@@ -72347,19 +72356,198 @@ class BaseExchange {
72347
72356
  // src/exchanges/binance/index.ts
72348
72357
  var import_p_limit = __toESM(require_p_limit(), 1);
72349
72358
 
72350
- // src/exchanges/binance/spot-margin-hedge.ts
72359
+ // src/exchanges/binance/futures-replay.ts
72351
72360
  function roundNumber(value2, digits = 8) {
72352
72361
  return Number(value2.toFixed(digits));
72353
72362
  }
72354
- var STOP_LIMIT_SPREAD_PERCENT = 0.0005;
72363
+ function parseNumber2(value2) {
72364
+ if (value2 === undefined || value2 === null || value2 === "")
72365
+ return;
72366
+ const parsed = Number(value2);
72367
+ if (!Number.isFinite(parsed) || parsed <= 0)
72368
+ return;
72369
+ return roundNumber(parsed);
72370
+ }
72355
72371
  function normalizeOrderInput(order) {
72372
+ const stop = parseNumber2(order.stop) ?? parseNumber2(order.stopPrice) ?? parseNumber2(order.triggerPrice);
72373
+ const type = order.type ? order.type.toUpperCase() : undefined;
72374
+ const quantity = order.quantity ?? order.qty ?? 0;
72356
72375
  return {
72357
- price: roundNumber(order.price),
72358
- quantity: roundNumber(order.quantity)
72376
+ price: roundNumber(Number(order.price)),
72377
+ quantity: roundNumber(Number(quantity)),
72378
+ ...stop ? { stop } : {},
72379
+ ...type && type !== "LIMIT" ? { type } : {}
72380
+ };
72381
+ }
72382
+ function sortReplayOrders(orders) {
72383
+ return [...orders].sort((left, right) => {
72384
+ const leftTrigger = left.stop ?? left.price;
72385
+ const rightTrigger = right.stop ?? right.price;
72386
+ if (leftTrigger !== rightTrigger)
72387
+ return leftTrigger - rightTrigger;
72388
+ if (left.price !== right.price)
72389
+ return left.price - right.price;
72390
+ return left.quantity - right.quantity;
72391
+ });
72392
+ }
72393
+ function buildReplayStopLimitPrice(payload) {
72394
+ const spread = payload.stop > 30000 ? 1.00005 : 1.0002;
72395
+ return roundNumber(payload.kind === "long" ? payload.stop * spread ** -1 : payload.stop * spread);
72396
+ }
72397
+ function normalizeRequestedEntryOrders(orders) {
72398
+ return sortReplayOrders(orders.map((order) => ({
72399
+ price: roundNumber(Number(order.price)),
72400
+ quantity: roundNumber(Number(order.quantity))
72401
+ })));
72402
+ }
72403
+ function normalizeRequestedStopOrders(payload) {
72404
+ return sortReplayOrders(payload.orders.map((order) => {
72405
+ const stop = parseNumber2(order.stop) ?? roundNumber(Number(order.price));
72406
+ return {
72407
+ price: order.price !== undefined ? roundNumber(Number(order.price)) : buildReplayStopLimitPrice({
72408
+ kind: payload.kind,
72409
+ stop
72410
+ }),
72411
+ quantity: roundNumber(Number(order.quantity)),
72412
+ stop,
72413
+ type: (order.type || "STOP").toUpperCase()
72414
+ };
72415
+ }));
72416
+ }
72417
+ function normalizeRequestedTakeProfitOrders(orders) {
72418
+ return sortReplayOrders(orders.map((order) => {
72419
+ const stop = parseNumber2(order.stop);
72420
+ const type = order.type?.toUpperCase();
72421
+ return {
72422
+ price: roundNumber(Number(order.price)),
72423
+ quantity: roundNumber(Number(order.quantity)),
72424
+ ...stop ? { stop } : {},
72425
+ ...type ? { type } : stop ? { type: "TAKE_PROFIT" } : {}
72426
+ };
72427
+ }));
72428
+ }
72429
+ function buildOrderKey(order) {
72430
+ return [
72431
+ order.type || "LIMIT",
72432
+ order.price,
72433
+ order.quantity,
72434
+ order.stop ?? ""
72435
+ ].join(":");
72436
+ }
72437
+ function buildComparisonBucket(options) {
72438
+ const expected = sortReplayOrders(options.expected);
72439
+ const actual = sortReplayOrders(options.actual);
72440
+ const expected_keys = expected.map(buildOrderKey);
72441
+ const actual_keys = actual.map(buildOrderKey);
72442
+ return {
72443
+ expected,
72444
+ actual,
72445
+ missing: expected_keys.filter((key) => !actual_keys.includes(key)),
72446
+ extra: actual_keys.filter((key) => !expected_keys.includes(key))
72447
+ };
72448
+ }
72449
+ function buildFuturesReplaySnapshot(payload) {
72450
+ const entry_side = payload.kind === "long" ? "buy" : "sell";
72451
+ const close_side = payload.kind === "long" ? "sell" : "buy";
72452
+ const entry_orders = [];
72453
+ const stop_orders = [];
72454
+ const take_profit_orders = [];
72455
+ for (const order of payload.open_orders) {
72456
+ if (order.kind !== payload.kind)
72457
+ continue;
72458
+ const normalized_order = normalizeOrderInput(order);
72459
+ const type = (normalized_order.type || "LIMIT").toUpperCase();
72460
+ if (order.side === entry_side) {
72461
+ entry_orders.push(normalized_order);
72462
+ continue;
72463
+ }
72464
+ if (order.side !== close_side)
72465
+ continue;
72466
+ if (type.includes("TAKE_PROFIT") || normalized_order.stop === undefined) {
72467
+ take_profit_orders.push(normalized_order);
72468
+ continue;
72469
+ }
72470
+ stop_orders.push(normalized_order);
72471
+ }
72472
+ return {
72473
+ symbol: payload.symbol,
72474
+ kind: payload.kind,
72475
+ current_price: payload.current_price,
72476
+ position: payload.position,
72477
+ open_orders: payload.open_orders.filter((order) => order.kind === payload.kind),
72478
+ entry_orders: sortReplayOrders(entry_orders),
72479
+ stop_orders: sortReplayOrders(stop_orders),
72480
+ take_profit_orders: sortReplayOrders(take_profit_orders)
72481
+ };
72482
+ }
72483
+ function inferFuturesReplayInputFromLiveOrders(payload) {
72484
+ const snapshot = buildFuturesReplaySnapshot(payload);
72485
+ return {
72486
+ symbol: payload.symbol,
72487
+ kind: payload.kind,
72488
+ current_price: payload.current_price,
72489
+ position: payload.position,
72490
+ entry_orders: snapshot.entry_orders,
72491
+ stop_orders: snapshot.stop_orders,
72492
+ take_profit_orders: snapshot.take_profit_orders
72493
+ };
72494
+ }
72495
+ function compareFuturesReplayOrders(payload) {
72496
+ const entry = buildComparisonBucket({
72497
+ expected: payload.entry_orders,
72498
+ actual: payload.snapshot.entry_orders
72499
+ });
72500
+ const stop = buildComparisonBucket({
72501
+ expected: payload.stop_orders,
72502
+ actual: payload.snapshot.stop_orders
72503
+ });
72504
+ const take_profit = buildComparisonBucket({
72505
+ expected: payload.take_profit_orders,
72506
+ actual: payload.snapshot.take_profit_orders
72507
+ });
72508
+ return {
72509
+ matches: entry.missing.length === 0 && entry.extra.length === 0 && stop.missing.length === 0 && stop.extra.length === 0 && take_profit.missing.length === 0 && take_profit.extra.length === 0,
72510
+ entry,
72511
+ stop,
72512
+ take_profit
72513
+ };
72514
+ }
72515
+ function buildFuturesReplayPlan(payload) {
72516
+ return {
72517
+ symbol: payload.symbol,
72518
+ kind: payload.kind,
72519
+ orders_to_cancel: payload.snapshot.open_orders,
72520
+ orders_to_create: {
72521
+ entry: sortReplayOrders(payload.entry_orders),
72522
+ stop: sortReplayOrders(payload.stop_orders),
72523
+ take_profit: sortReplayOrders(payload.take_profit_orders)
72524
+ }
72525
+ };
72526
+ }
72527
+ function normalizeFuturesReplayInput(payload) {
72528
+ return {
72529
+ entry_orders: normalizeRequestedEntryOrders(payload.entry_orders),
72530
+ stop_orders: normalizeRequestedStopOrders({
72531
+ kind: payload.kind,
72532
+ orders: payload.stop_orders
72533
+ }),
72534
+ take_profit_orders: normalizeRequestedTakeProfitOrders(payload.take_profit_orders)
72535
+ };
72536
+ }
72537
+
72538
+ // src/exchanges/binance/spot-margin-hedge.ts
72539
+ function roundNumber2(value2, digits = 8) {
72540
+ return Number(value2.toFixed(digits));
72541
+ }
72542
+ var STOP_LIMIT_SPREAD_PERCENT = 0.0005;
72543
+ function normalizeOrderInput2(order) {
72544
+ return {
72545
+ price: roundNumber2(order.price),
72546
+ quantity: roundNumber2(order.quantity)
72359
72547
  };
72360
72548
  }
72361
72549
  function sortOrderInputs(orders) {
72362
- return [...orders].map(normalizeOrderInput).sort((left, right) => left.price - right.price);
72550
+ return [...orders].map(normalizeOrderInput2).sort((left, right) => left.price - right.price);
72363
72551
  }
72364
72552
  function normalizeOrderForComparison(order) {
72365
72553
  const type = (order.type || "limit").toLowerCase();
@@ -72368,9 +72556,9 @@ function normalizeOrderForComparison(order) {
72368
72556
  return {
72369
72557
  side: order.side,
72370
72558
  type,
72371
- price: roundNumber(order.price),
72372
- quantity: roundNumber(order.quantity),
72373
- stop: parsed_stop !== undefined && Number.isFinite(parsed_stop) && parsed_stop > 0 ? roundNumber(parsed_stop) : undefined
72559
+ price: roundNumber2(order.price),
72560
+ quantity: roundNumber2(order.quantity),
72561
+ stop: parsed_stop !== undefined && Number.isFinite(parsed_stop) && parsed_stop > 0 ? roundNumber2(parsed_stop) : undefined
72374
72562
  };
72375
72563
  }
72376
72564
  function normalizeOrders(orders) {
@@ -72393,7 +72581,7 @@ function toPlannedOrders(orders) {
72393
72581
  stop: order.stop
72394
72582
  }));
72395
72583
  }
72396
- function buildOrderKey(order) {
72584
+ function buildOrderKey2(order) {
72397
72585
  return [
72398
72586
  order.side,
72399
72587
  order.type,
@@ -72403,8 +72591,8 @@ function buildOrderKey(order) {
72403
72591
  ].join(":");
72404
72592
  }
72405
72593
  function buildDiff(options) {
72406
- const expectedKeys = options.expected.map(buildOrderKey);
72407
- const actualKeys = options.actual.map(buildOrderKey);
72594
+ const expectedKeys = options.expected.map(buildOrderKey2);
72595
+ const actualKeys = options.actual.map(buildOrderKey2);
72408
72596
  return {
72409
72597
  missing: expectedKeys.filter((key) => !actualKeys.includes(key)),
72410
72598
  extra: actualKeys.filter((key) => !expectedKeys.includes(key))
@@ -72416,10 +72604,10 @@ function getQuoteAsset(symbol) {
72416
72604
  return target || symbol.slice(-4);
72417
72605
  }
72418
72606
  function sumNotional(orders) {
72419
- return roundNumber(orders.reduce((sum, order) => sum + order.price * order.quantity, 0), 8);
72607
+ return roundNumber2(orders.reduce((sum, order) => sum + order.price * order.quantity, 0), 8);
72420
72608
  }
72421
72609
  function sumQuantity(orders) {
72422
- return roundNumber(orders.reduce((sum, order) => sum + order.quantity, 0), 8);
72610
+ return roundNumber2(orders.reduce((sum, order) => sum + order.quantity, 0), 8);
72423
72611
  }
72424
72612
  function buildLongOrderPlacement(options) {
72425
72613
  const { order, current_price } = options;
@@ -72428,7 +72616,7 @@ function buildLongOrderPlacement(options) {
72428
72616
  side: "buy",
72429
72617
  type: "stop_loss_limit",
72430
72618
  price: order.price,
72431
- stop: roundNumber(order.price * (1 - STOP_LIMIT_SPREAD_PERCENT)),
72619
+ stop: roundNumber2(order.price * (1 - STOP_LIMIT_SPREAD_PERCENT)),
72432
72620
  quantity: order.quantity
72433
72621
  };
72434
72622
  }
@@ -72496,11 +72684,11 @@ function buildSpotMarginHedgePlan(options) {
72496
72684
  }));
72497
72685
  const spot_orders_to_create = options.placement_overrides?.spot || default_spot_orders_to_create;
72498
72686
  const margin_orders_to_create = options.placement_overrides?.margin || default_margin_orders_to_create;
72499
- const borrow_delta = roundNumber(Math.max(0, options.borrow_amount - options.snapshot.borrowed_quote_amount));
72687
+ const borrow_delta = roundNumber2(Math.max(0, options.borrow_amount - options.snapshot.borrowed_quote_amount));
72500
72688
  const spot_long_notional = sumNotional(spot_long_orders);
72501
- const transfer_to_spot_amount = roundNumber(Math.max(0, spot_long_notional - options.snapshot.spot_quote_free));
72689
+ const transfer_to_spot_amount = roundNumber2(Math.max(0, options.margin_type === "cross" ? options.borrow_amount : spot_long_notional - options.snapshot.spot_quote_free));
72502
72690
  const short_quantity = sumQuantity(short_orders);
72503
- const transfer_base_to_spot_amount = roundNumber(Math.max(0, short_quantity - options.snapshot.spot_base_free));
72691
+ const transfer_base_to_spot_amount = roundNumber2(Math.max(0, short_quantity - options.snapshot.spot_base_free));
72504
72692
  return {
72505
72693
  borrow_amount: options.borrow_amount,
72506
72694
  borrow_delta,
@@ -73002,17 +73190,17 @@ async function createMarginLimitOrdersParallel(client, symbol, priceFormat, quan
73002
73190
  return Object.fromEntries(Object.entries(v).filter(([, value2]) => value2 !== undefined));
73003
73191
  };
73004
73192
  const newOrders = orders.map(createMarginOrder);
73005
- const limit = import_p_limit.default(ORDERS_PER_SECOND);
73006
- const results = await Promise.all(newOrders.map((orderPayload) => limit(async () => {
73193
+ const results = [];
73194
+ for (const orderPayload of newOrders) {
73007
73195
  try {
73008
73196
  const result = await client.marginAccountNewOrder(orderPayload);
73009
73197
  console.log("Margin order result:", result);
73010
- return result;
73198
+ results.push(result);
73011
73199
  } catch (error) {
73012
73200
  console.error("Error processing margin order:", error);
73013
73201
  throw error;
73014
73202
  }
73015
- })));
73203
+ }
73016
73204
  return results;
73017
73205
  }
73018
73206
  async function getIsolatedMarginAccountInfo(client, symbol) {
@@ -74159,6 +74347,30 @@ class BinanceExchange extends BaseExchange {
74159
74347
  throw error;
74160
74348
  }
74161
74349
  }
74350
+ async getFuturesSymbolFormats(symbol) {
74351
+ if (typeof this.client.getExchangeInfo !== "function") {
74352
+ return {
74353
+ price_places: "%.8f",
74354
+ decimal_places: "%.8f"
74355
+ };
74356
+ }
74357
+ try {
74358
+ const exchange_info = await this.client.getExchangeInfo();
74359
+ const symbol_info = exchange_info?.symbols?.find((item) => item.symbol === symbol.toUpperCase());
74360
+ const price_filter = symbol_info?.filters?.find((filter) => filter.filterType === "PRICE_FILTER");
74361
+ const lot_size_filter = symbol_info?.filters?.find((filter) => filter.filterType === "LOT_SIZE");
74362
+ return {
74363
+ price_places: getPrintfFormatFromStepSize(price_filter?.tickSize, "%.8f"),
74364
+ decimal_places: getPrintfFormatFromStepSize(lot_size_filter?.stepSize, "%.8f")
74365
+ };
74366
+ } catch (error) {
74367
+ console.error("Error fetching futures symbol formats:", error);
74368
+ return {
74369
+ price_places: "%.8f",
74370
+ decimal_places: "%.8f"
74371
+ };
74372
+ }
74373
+ }
74162
74374
  async getSpotSymbolFormats(symbol) {
74163
74375
  if (!this.main_client || typeof this.main_client.getExchangeInfo !== "function") {
74164
74376
  return {
@@ -74213,28 +74425,146 @@ class BinanceExchange extends BaseExchange {
74213
74425
  }
74214
74426
  return await getMarginOpenOrders(this.main_client, symbol, isIsolated);
74215
74427
  }
74428
+ async getFuturesReplaySnapshot(payload) {
74429
+ const symbol = payload.symbol.toUpperCase();
74430
+ const [open_orders, position_info, current_price] = await Promise.all([
74431
+ this.getOpenOrders({ symbol }),
74432
+ this.getPositionInfo(symbol),
74433
+ this.getCurrentPrice(symbol)
74434
+ ]);
74435
+ return buildFuturesReplaySnapshot({
74436
+ symbol,
74437
+ kind: payload.kind,
74438
+ current_price,
74439
+ position: {
74440
+ size: position_info[payload.kind]?.size || 0
74441
+ },
74442
+ open_orders
74443
+ });
74444
+ }
74445
+ async previewFuturesReplay(payload) {
74446
+ const snapshot = payload.snapshot ? {
74447
+ ...payload.snapshot,
74448
+ current_price: payload.snapshot.current_price ?? await this.getCurrentPrice(payload.symbol)
74449
+ } : await this.getFuturesReplaySnapshot({
74450
+ symbol: payload.symbol,
74451
+ kind: payload.kind
74452
+ });
74453
+ const shouldInferEntryOrders = !payload.entry_orders || payload.entry_orders.length === 0;
74454
+ const shouldInferStopOrders = !payload.stop_orders || payload.stop_orders.length === 0;
74455
+ const shouldInferTakeProfitOrders = !payload.take_profit_orders || payload.take_profit_orders.length === 0;
74456
+ const inferred_input = shouldInferEntryOrders || shouldInferStopOrders || shouldInferTakeProfitOrders ? inferFuturesReplayInputFromLiveOrders({
74457
+ symbol: payload.symbol,
74458
+ kind: payload.kind,
74459
+ current_price: snapshot.current_price,
74460
+ position: snapshot.position,
74461
+ open_orders: snapshot.open_orders
74462
+ }) : undefined;
74463
+ const explicit_input = normalizeFuturesReplayInput({
74464
+ kind: payload.kind,
74465
+ entry_orders: payload.entry_orders || [],
74466
+ stop_orders: payload.stop_orders || [],
74467
+ take_profit_orders: payload.take_profit_orders || []
74468
+ });
74469
+ const entry_orders = shouldInferEntryOrders ? inferred_input?.entry_orders || [] : explicit_input.entry_orders;
74470
+ const stop_orders = shouldInferStopOrders ? inferred_input?.stop_orders || [] : explicit_input.stop_orders;
74471
+ const take_profit_orders = shouldInferTakeProfitOrders ? inferred_input?.take_profit_orders || [] : explicit_input.take_profit_orders;
74472
+ const requested_input = {
74473
+ symbol: payload.symbol,
74474
+ kind: payload.kind,
74475
+ entry_orders,
74476
+ stop_orders,
74477
+ take_profit_orders
74478
+ };
74479
+ const plan = buildFuturesReplayPlan({
74480
+ ...requested_input,
74481
+ snapshot
74482
+ });
74483
+ const comparison = compareFuturesReplayOrders({
74484
+ ...requested_input,
74485
+ snapshot
74486
+ });
74487
+ return {
74488
+ snapshot,
74489
+ inferred_input,
74490
+ requested_input,
74491
+ plan,
74492
+ comparison
74493
+ };
74494
+ }
74495
+ async placeFuturesReplay(payload) {
74496
+ const preview = await this.previewFuturesReplay(payload);
74497
+ if (!payload.place) {
74498
+ return preview;
74499
+ }
74500
+ const symbol = payload.symbol.toUpperCase();
74501
+ await cancelAllOrders(this.client, symbol, {
74502
+ kind: payload.kind
74503
+ });
74504
+ const refreshed_snapshot = await this.getFuturesReplaySnapshot({
74505
+ symbol,
74506
+ kind: payload.kind
74507
+ });
74508
+ const execution_plan = buildFuturesReplayPlan({
74509
+ symbol,
74510
+ kind: payload.kind,
74511
+ entry_orders: preview.requested_input.entry_orders,
74512
+ stop_orders: preview.requested_input.stop_orders,
74513
+ take_profit_orders: preview.requested_input.take_profit_orders,
74514
+ snapshot: refreshed_snapshot
74515
+ });
74516
+ const { price_places, decimal_places } = await this.getFuturesSymbolFormats(symbol);
74517
+ const entry_orders = execution_plan.orders_to_create.entry.map((order) => ({
74518
+ ...order,
74519
+ side: payload.kind === "long" ? "buy" : "sell",
74520
+ kind: payload.kind
74521
+ }));
74522
+ const stop_orders = execution_plan.orders_to_create.stop.map((order) => ({
74523
+ ...order,
74524
+ side: payload.kind === "long" ? "sell" : "buy",
74525
+ kind: payload.kind,
74526
+ type: order.type || "STOP"
74527
+ }));
74528
+ const take_profit_orders = execution_plan.orders_to_create.take_profit.map((order) => ({
74529
+ ...order,
74530
+ side: payload.kind === "long" ? "sell" : "buy",
74531
+ kind: payload.kind,
74532
+ ...order.stop ? { type: order.type || "TAKE_PROFIT" } : {}
74533
+ }));
74534
+ const execution = entry_orders.length > 0 || stop_orders.length > 0 || take_profit_orders.length > 0 ? await createLimitPurchaseOrdersParallel(this.client, symbol, price_places, decimal_places, [...entry_orders, ...stop_orders, ...take_profit_orders]) : [];
74535
+ return {
74536
+ ...preview,
74537
+ execution_plan,
74538
+ refreshed_snapshot,
74539
+ execution
74540
+ };
74541
+ }
74542
+ async placeFuturesExactTrade(payload) {
74543
+ return await this.placeFuturesReplay(payload);
74544
+ }
74216
74545
  async getSpotMarginHedgeSnapshot(payload) {
74217
74546
  if (!this.main_client) {
74218
74547
  throw new Error("Main client not available for spot and margin trading");
74219
74548
  }
74220
- if (payload.margin_type !== "isolated") {
74221
- throw new Error(`Unsupported margin type: ${payload.margin_type}. Only isolated is supported for replay right now.`);
74222
- }
74223
74549
  const symbol = payload.symbol.toUpperCase();
74224
74550
  const quoteAssets = ["USDT", "USDC", "BUSD", "BTC", "ETH"];
74225
74551
  const quoteAsset = quoteAssets.find((asset) => symbol.endsWith(asset)) || symbol.slice(-4);
74226
74552
  const baseAsset = symbol.slice(0, symbol.length - quoteAsset.length);
74227
- const [spot_orders, margin_orders, spot_balances, isolated_margin_position] = await Promise.all([
74553
+ const is_isolated = payload.margin_type === "isolated";
74554
+ const [spot_orders, margin_orders, spot_balances, margin_account] = await Promise.all([
74228
74555
  this.getSpotOpenOrders(symbol),
74229
- this.getMarginOpenOrders(symbol, true),
74556
+ this.getMarginOpenOrders(symbol, is_isolated),
74230
74557
  this.getSpotBalances([baseAsset, quoteAsset]),
74231
- this.getIsolatedMarginPosition(symbol)
74558
+ is_isolated ? this.getIsolatedMarginPosition(symbol) : this.getCrossMarginAccount()
74232
74559
  ]);
74233
74560
  const current_price = await this.getSpotCurrentPrice(symbol);
74234
74561
  const spot_quote_balance = spot_balances.find((balance) => balance.asset.toUpperCase() === quoteAsset);
74235
74562
  const spot_base_balance = spot_balances.find((balance) => balance.asset.toUpperCase() === baseAsset);
74236
- const isolated_asset = isolated_margin_position?.assets?.[0];
74237
- const borrowed_quote_amount = parseFloat(isolated_asset?.quoteAsset?.borrowed?.toString?.() || "0");
74563
+ const isolated_margin_account = is_isolated ? margin_account : undefined;
74564
+ const cross_margin_account = !is_isolated ? margin_account : undefined;
74565
+ const isolated_asset = isolated_margin_account?.assets?.[0];
74566
+ const cross_quote_asset = !is_isolated ? cross_margin_account?.userAssets?.find((asset) => asset.asset?.toUpperCase?.() === quoteAsset) : undefined;
74567
+ const borrowed_quote_amount = parseFloat(isolated_asset?.quoteAsset?.borrowed?.toString?.() || cross_quote_asset?.borrowed?.toString?.() || "0");
74238
74568
  return {
74239
74569
  symbol,
74240
74570
  margin_type: payload.margin_type,
@@ -74247,7 +74577,8 @@ class BinanceExchange extends BaseExchange {
74247
74577
  margin_orders,
74248
74578
  current_price,
74249
74579
  spot_balances,
74250
- isolated_margin_position
74580
+ isolated_margin_position: isolated_margin_account,
74581
+ cross_margin_account
74251
74582
  };
74252
74583
  }
74253
74584
  async previewSpotMarginHedge(payload) {
@@ -74304,14 +74635,12 @@ class BinanceExchange extends BaseExchange {
74304
74635
  if (!this.main_client) {
74305
74636
  throw new Error("Main client not available for spot and margin trading");
74306
74637
  }
74307
- if (payload.margin_type !== "isolated") {
74308
- throw new Error(`Unsupported margin type: ${payload.margin_type}. Only isolated placement is supported right now.`);
74309
- }
74310
74638
  const symbol = payload.symbol.toUpperCase();
74311
74639
  const quoteAssets = ["USDT", "USDC", "BUSD", "BTC", "ETH"];
74312
74640
  const quote_asset = quoteAssets.find((asset) => symbol.endsWith(asset)) || symbol.slice(-4);
74313
74641
  const base_asset = symbol.slice(0, symbol.length - quote_asset.length);
74314
74642
  const requested_input = preview.requested_input;
74643
+ const is_isolated = payload.margin_type === "isolated";
74315
74644
  if (preview.snapshot.spot_orders.length > 0) {
74316
74645
  await this.cancelSpotOrders(symbol, preview.snapshot.spot_orders.map((order) => ({
74317
74646
  orderId: order.orderId || order.order_id || order.id,
@@ -74319,7 +74648,7 @@ class BinanceExchange extends BaseExchange {
74319
74648
  })));
74320
74649
  }
74321
74650
  if (preview.snapshot.margin_orders.length > 0) {
74322
- await this.cancelMarginOrders(symbol, true, preview.snapshot.margin_orders.map((order) => ({
74651
+ await this.cancelMarginOrders(symbol, is_isolated, preview.snapshot.margin_orders.map((order) => ({
74323
74652
  orderId: order.orderId || order.order_id || order.id,
74324
74653
  origClientOrderId: order.origClientOrderId
74325
74654
  })));
@@ -74341,50 +74670,120 @@ class BinanceExchange extends BaseExchange {
74341
74670
  });
74342
74671
  let borrow_result = null;
74343
74672
  if (execution_plan.borrow_delta > 0) {
74344
- borrow_result = await this.main_client.submitMarginAccountBorrowRepay({
74673
+ const borrow_payload = {
74345
74674
  asset: quote_asset,
74346
74675
  amount: execution_plan.borrow_delta,
74347
- isIsolated: "TRUE",
74348
- symbol,
74349
74676
  type: "BORROW"
74677
+ };
74678
+ if (is_isolated) {
74679
+ borrow_payload.isIsolated = "TRUE";
74680
+ borrow_payload.symbol = symbol;
74681
+ }
74682
+ borrow_result = await this.main_client.submitMarginAccountBorrowRepay(borrow_payload);
74683
+ }
74684
+ async function getCrossTransferAmount(asset, amount) {
74685
+ if (is_isolated) {
74686
+ return amount;
74687
+ }
74688
+ if (!this.main_client || typeof this.main_client.queryMaxTransferOutAmount !== "function") {
74689
+ return amount;
74690
+ }
74691
+ const response = await this.main_client.queryMaxTransferOutAmount({
74692
+ asset
74350
74693
  });
74694
+ const allowed_amount = parseFloat(response?.amount?.toString?.() || "0");
74695
+ if (!Number.isFinite(allowed_amount)) {
74696
+ return amount;
74697
+ }
74698
+ return Math.max(0, Math.min(amount, allowed_amount));
74351
74699
  }
74352
74700
  let quote_transfer_result = null;
74701
+ let quote_transfer_amount = execution_plan.transfer_to_spot_amount;
74353
74702
  if (execution_plan.transfer_to_spot_amount > 0) {
74354
- quote_transfer_result = await this.main_client.isolatedMarginAccountTransfer({
74355
- asset: quote_asset,
74356
- amount: execution_plan.transfer_to_spot_amount,
74357
- symbol,
74358
- transFrom: "ISOLATED_MARGIN",
74359
- transTo: "SPOT"
74360
- });
74703
+ quote_transfer_amount = await getCrossTransferAmount.call(this, quote_asset, execution_plan.transfer_to_spot_amount);
74704
+ if (quote_transfer_amount > 0) {
74705
+ quote_transfer_result = is_isolated ? await this.main_client.isolatedMarginAccountTransfer({
74706
+ asset: quote_asset,
74707
+ amount: quote_transfer_amount,
74708
+ symbol,
74709
+ transFrom: "ISOLATED_MARGIN",
74710
+ transTo: "SPOT"
74711
+ }) : await this.main_client.submitUniversalTransfer({
74712
+ type: "MARGIN_MAIN",
74713
+ asset: quote_asset,
74714
+ amount: quote_transfer_amount
74715
+ });
74716
+ }
74361
74717
  }
74362
74718
  let base_transfer_result = null;
74719
+ let base_transfer_amount = execution_plan.transfer_base_to_spot_amount;
74363
74720
  if (execution_plan.transfer_base_to_spot_amount > 0) {
74364
- base_transfer_result = await this.main_client.isolatedMarginAccountTransfer({
74365
- asset: base_asset,
74366
- amount: execution_plan.transfer_base_to_spot_amount,
74367
- symbol,
74368
- transFrom: "ISOLATED_MARGIN",
74369
- transTo: "SPOT"
74370
- });
74721
+ base_transfer_amount = await getCrossTransferAmount.call(this, base_asset, execution_plan.transfer_base_to_spot_amount);
74722
+ if (base_transfer_amount > 0) {
74723
+ base_transfer_result = is_isolated ? await this.main_client.isolatedMarginAccountTransfer({
74724
+ asset: base_asset,
74725
+ amount: base_transfer_amount,
74726
+ symbol,
74727
+ transFrom: "ISOLATED_MARGIN",
74728
+ transTo: "SPOT"
74729
+ }) : await this.main_client.submitUniversalTransfer({
74730
+ type: "MARGIN_MAIN",
74731
+ asset: base_asset,
74732
+ amount: base_transfer_amount
74733
+ });
74734
+ }
74371
74735
  }
74372
- const spot_result = execution_plan.orders_to_create.spot.length > 0 ? await this.createSpotLimitOrders({
74736
+ const spot_quote_available = refreshed_snapshot.spot_quote_free + quote_transfer_amount;
74737
+ const requested_max_spot_buy_orders = requested_input.max_spot_buy_orders ?? 5;
74738
+ const sorted_long_orders = [...requested_input.long_orders].sort((a, b) => a.price - b.price);
74739
+ let affordable_spot_buy_orders = 0;
74740
+ let affordable_spot_buy_notional = 0;
74741
+ for (const order of sorted_long_orders.slice(0, requested_max_spot_buy_orders)) {
74742
+ const next_notional = affordable_spot_buy_notional + order.price * order.quantity;
74743
+ if (next_notional <= spot_quote_available + 0.00000001) {
74744
+ affordable_spot_buy_orders += 1;
74745
+ affordable_spot_buy_notional = next_notional;
74746
+ } else {
74747
+ break;
74748
+ }
74749
+ }
74750
+ const placement_plan = buildSpotMarginHedgePlan({
74751
+ borrow_amount: requested_input.borrow_amount,
74752
+ symbol: requested_input.symbol,
74753
+ margin_type: requested_input.margin_type,
74754
+ long_orders: requested_input.long_orders,
74755
+ short_orders: requested_input.short_orders,
74756
+ current_price: refreshed_snapshot.current_price,
74757
+ max_spot_buy_orders: affordable_spot_buy_orders,
74758
+ snapshot: {
74759
+ ...refreshed_snapshot,
74760
+ borrowed_quote_amount: refreshed_snapshot.borrowed_quote_amount + execution_plan.borrow_delta,
74761
+ spot_quote_free: spot_quote_available,
74762
+ spot_base_free: refreshed_snapshot.spot_base_free + base_transfer_amount
74763
+ }
74764
+ });
74765
+ const spot_result = placement_plan.orders_to_create.spot.length > 0 ? await this.createSpotLimitOrders({
74373
74766
  symbol,
74374
- orders: execution_plan.orders_to_create.spot,
74767
+ orders: placement_plan.orders_to_create.spot,
74375
74768
  price_places: symbol_formats.price_places,
74376
74769
  decimal_places: symbol_formats.decimal_places
74377
74770
  }) : [];
74378
- const margin_result = execution_plan.orders_to_create.margin.length > 0 ? await this.createMarginLimitOrders({
74771
+ const margin_result = placement_plan.orders_to_create.margin.length > 0 ? await this.createMarginLimitOrders({
74379
74772
  symbol,
74380
- orders: execution_plan.orders_to_create.margin,
74381
- isIsolated: true,
74773
+ orders: placement_plan.orders_to_create.margin,
74774
+ isIsolated: is_isolated,
74382
74775
  price_places: symbol_formats.price_places,
74383
74776
  decimal_places: symbol_formats.decimal_places
74384
74777
  }) : [];
74385
74778
  return {
74386
74779
  ...preview,
74387
- execution_plan,
74780
+ execution_plan: {
74781
+ ...placement_plan,
74782
+ borrow_amount: execution_plan.borrow_amount,
74783
+ borrow_delta: execution_plan.borrow_delta,
74784
+ transfer_to_spot_amount: quote_transfer_amount,
74785
+ transfer_base_to_spot_amount: base_transfer_amount
74786
+ },
74388
74787
  refreshed_snapshot,
74389
74788
  execution: {
74390
74789
  borrow: borrow_result ? {
@@ -74394,12 +74793,12 @@ class BinanceExchange extends BaseExchange {
74394
74793
  } : null,
74395
74794
  quote_transfer: quote_transfer_result ? {
74396
74795
  asset: quote_asset,
74397
- amount: execution_plan.transfer_to_spot_amount,
74796
+ amount: quote_transfer_amount,
74398
74797
  response: quote_transfer_result
74399
74798
  } : null,
74400
74799
  base_transfer: base_transfer_result ? {
74401
74800
  asset: base_asset,
74402
- amount: execution_plan.transfer_base_to_spot_amount,
74801
+ amount: base_transfer_amount,
74403
74802
  response: base_transfer_result
74404
74803
  } : null,
74405
74804
  spot_orders: spot_result,