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

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.
@@ -75757,6 +75757,12 @@ class BaseExchange {
75757
75757
  });
75758
75758
  }
75759
75759
  }
75760
+ async previewSpotMarginHedge(_payload) {
75761
+ throw new Error("Spot margin hedge preview is not supported on this exchange");
75762
+ }
75763
+ async placeSpotMarginHedge(_payload) {
75764
+ throw new Error("Spot margin hedge placement is not supported on this exchange");
75765
+ }
75760
75766
  async placeSpotLimitOrders(_payload) {}
75761
75767
  async buildCumulative(payload) {
75762
75768
  const {
@@ -76164,6 +76170,210 @@ class BaseExchange {
76164
76170
 
76165
76171
  // src/exchanges/binance/index.ts
76166
76172
  var import_p_limit = __toESM(require_p_limit(), 1);
76173
+
76174
+ // src/exchanges/binance/spot-margin-hedge.ts
76175
+ function roundNumber(value2, digits = 8) {
76176
+ return Number(value2.toFixed(digits));
76177
+ }
76178
+ var STOP_LIMIT_SPREAD_PERCENT = 0.0005;
76179
+ function normalizeOrderInput(order) {
76180
+ return {
76181
+ price: roundNumber(order.price),
76182
+ quantity: roundNumber(order.quantity)
76183
+ };
76184
+ }
76185
+ function sortOrderInputs(orders) {
76186
+ return [...orders].map(normalizeOrderInput).sort((left, right) => left.price - right.price);
76187
+ }
76188
+ function normalizeOrderForComparison(order) {
76189
+ const type = (order.type || "limit").toLowerCase();
76190
+ const raw_stop = "stop" in order && order.stop !== undefined ? order.stop : ("stopPrice" in order) ? order.stopPrice : undefined;
76191
+ const parsed_stop = raw_stop === undefined || raw_stop === null || raw_stop === "" ? undefined : Number(raw_stop);
76192
+ return {
76193
+ side: order.side,
76194
+ type,
76195
+ price: roundNumber(order.price),
76196
+ quantity: roundNumber(order.quantity),
76197
+ stop: parsed_stop !== undefined && Number.isFinite(parsed_stop) && parsed_stop > 0 ? roundNumber(parsed_stop) : undefined
76198
+ };
76199
+ }
76200
+ function normalizeOrders(orders) {
76201
+ return [...orders].map(normalizeOrderForComparison).sort((left, right) => {
76202
+ if (left.side !== right.side)
76203
+ return left.side.localeCompare(right.side);
76204
+ if (left.type !== right.type)
76205
+ return left.type.localeCompare(right.type);
76206
+ if (left.price !== right.price)
76207
+ return left.price - right.price;
76208
+ return left.quantity - right.quantity;
76209
+ });
76210
+ }
76211
+ function toPlannedOrders(orders) {
76212
+ return normalizeOrders(orders).map((order) => ({
76213
+ side: order.side,
76214
+ type: order.type,
76215
+ price: order.price,
76216
+ quantity: order.quantity,
76217
+ stop: order.stop
76218
+ }));
76219
+ }
76220
+ function buildOrderKey(order) {
76221
+ return [
76222
+ order.side,
76223
+ order.type,
76224
+ order.price,
76225
+ order.quantity,
76226
+ order.stop ?? ""
76227
+ ].join(":");
76228
+ }
76229
+ function buildDiff(options) {
76230
+ const expectedKeys = options.expected.map(buildOrderKey);
76231
+ const actualKeys = options.actual.map(buildOrderKey);
76232
+ return {
76233
+ missing: expectedKeys.filter((key) => !actualKeys.includes(key)),
76234
+ extra: actualKeys.filter((key) => !expectedKeys.includes(key))
76235
+ };
76236
+ }
76237
+ function getQuoteAsset(symbol) {
76238
+ const quoteAssets = ["USDT", "USDC", "BUSD", "BTC", "ETH"];
76239
+ const target = quoteAssets.find((asset) => symbol.endsWith(asset));
76240
+ return target || symbol.slice(-4);
76241
+ }
76242
+ function sumNotional(orders) {
76243
+ return roundNumber(orders.reduce((sum, order) => sum + order.price * order.quantity, 0), 8);
76244
+ }
76245
+ function sumQuantity(orders) {
76246
+ return roundNumber(orders.reduce((sum, order) => sum + order.quantity, 0), 8);
76247
+ }
76248
+ function buildLongOrderPlacement(options) {
76249
+ const { order, current_price } = options;
76250
+ if (order.price > current_price) {
76251
+ return {
76252
+ side: "buy",
76253
+ type: "stop_loss_limit",
76254
+ price: order.price,
76255
+ stop: roundNumber(order.price * (1 - STOP_LIMIT_SPREAD_PERCENT)),
76256
+ quantity: order.quantity
76257
+ };
76258
+ }
76259
+ return {
76260
+ side: "buy",
76261
+ type: "limit",
76262
+ price: order.price,
76263
+ quantity: order.quantity
76264
+ };
76265
+ }
76266
+ function inferSpotMarginHedgeInputFromLiveOrders(options) {
76267
+ const long_orders = sortOrderInputs([
76268
+ ...options.spot_orders.filter((order) => order.side === "buy").map((order) => ({
76269
+ price: order.price,
76270
+ quantity: order.quantity
76271
+ })),
76272
+ ...options.margin_orders.filter((order) => order.side === "buy").map((order) => ({
76273
+ price: order.price,
76274
+ quantity: order.quantity
76275
+ }))
76276
+ ]);
76277
+ const short_orders = sortOrderInputs([
76278
+ ...options.spot_orders.filter((order) => order.side === "sell").map((order) => ({
76279
+ price: order.price,
76280
+ quantity: order.quantity
76281
+ })),
76282
+ ...options.margin_orders.filter((order) => order.side === "sell").map((order) => ({
76283
+ price: order.price,
76284
+ quantity: order.quantity
76285
+ }))
76286
+ ]);
76287
+ return {
76288
+ symbol: options.symbol,
76289
+ margin_type: options.margin_type,
76290
+ borrow_amount: sumNotional(long_orders),
76291
+ long_orders,
76292
+ short_orders,
76293
+ placement_overrides: {
76294
+ spot: toPlannedOrders(options.spot_orders),
76295
+ margin: toPlannedOrders(options.margin_orders)
76296
+ }
76297
+ };
76298
+ }
76299
+ function buildSpotMarginHedgePlan(options) {
76300
+ const max_spot_buy_orders = options.max_spot_buy_orders ?? 5;
76301
+ const long_orders = sortOrderInputs(options.long_orders);
76302
+ const short_orders = sortOrderInputs(options.short_orders);
76303
+ const spot_long_orders = long_orders.slice(0, max_spot_buy_orders);
76304
+ const margin_long_orders = long_orders.slice(max_spot_buy_orders);
76305
+ const default_spot_orders_to_create = [
76306
+ ...spot_long_orders.map((order) => buildLongOrderPlacement({
76307
+ order,
76308
+ current_price: options.current_price
76309
+ })),
76310
+ ...short_orders.map((order) => ({
76311
+ side: "sell",
76312
+ type: "limit",
76313
+ price: order.price,
76314
+ quantity: order.quantity
76315
+ }))
76316
+ ];
76317
+ const default_margin_orders_to_create = margin_long_orders.map((order) => buildLongOrderPlacement({
76318
+ order,
76319
+ current_price: options.current_price
76320
+ }));
76321
+ const spot_orders_to_create = options.placement_overrides?.spot || default_spot_orders_to_create;
76322
+ const margin_orders_to_create = options.placement_overrides?.margin || default_margin_orders_to_create;
76323
+ const borrow_delta = roundNumber(Math.max(0, options.borrow_amount - options.snapshot.borrowed_quote_amount));
76324
+ const spot_long_notional = sumNotional(spot_long_orders);
76325
+ const transfer_to_spot_amount = roundNumber(Math.max(0, spot_long_notional - options.snapshot.spot_quote_free));
76326
+ const short_quantity = sumQuantity(short_orders);
76327
+ const transfer_base_to_spot_amount = roundNumber(Math.max(0, short_quantity - options.snapshot.spot_base_free));
76328
+ return {
76329
+ borrow_amount: options.borrow_amount,
76330
+ borrow_delta,
76331
+ transfer_to_spot_amount,
76332
+ transfer_to_spot_asset: getQuoteAsset(options.symbol),
76333
+ transfer_base_to_spot_amount,
76334
+ orders_to_cancel: {
76335
+ spot: options.snapshot.spot_orders,
76336
+ margin: options.snapshot.margin_orders
76337
+ },
76338
+ orders_to_create: {
76339
+ spot: spot_orders_to_create,
76340
+ margin: margin_orders_to_create
76341
+ },
76342
+ normalized: {
76343
+ spot: normalizeOrders(spot_orders_to_create),
76344
+ margin: normalizeOrders(margin_orders_to_create)
76345
+ }
76346
+ };
76347
+ }
76348
+ function compareSpotMarginHedgeOrders(options) {
76349
+ const actualSpot = normalizeOrders(options.spot_orders);
76350
+ const actualMargin = normalizeOrders(options.margin_orders);
76351
+ const spotDiff = buildDiff({
76352
+ expected: options.plan.normalized.spot,
76353
+ actual: actualSpot
76354
+ });
76355
+ const marginDiff = buildDiff({
76356
+ expected: options.plan.normalized.margin,
76357
+ actual: actualMargin
76358
+ });
76359
+ return {
76360
+ matches: spotDiff.missing.length === 0 && spotDiff.extra.length === 0 && marginDiff.missing.length === 0 && marginDiff.extra.length === 0,
76361
+ spot: {
76362
+ expected: options.plan.normalized.spot,
76363
+ actual: actualSpot,
76364
+ missing: spotDiff.missing,
76365
+ extra: spotDiff.extra
76366
+ },
76367
+ margin: {
76368
+ expected: options.plan.normalized.margin,
76369
+ actual: actualMargin,
76370
+ missing: marginDiff.missing,
76371
+ extra: marginDiff.extra
76372
+ }
76373
+ };
76374
+ }
76375
+
76376
+ // src/exchanges/binance/index.ts
76167
76377
  var CONSTANTS = {
76168
76378
  SPOT_TO_FIAT: "MAIN_C2C",
76169
76379
  SPOT_TO_USDT_FUTURE: "MAIN_UMFUTURE",
@@ -76652,12 +76862,20 @@ async function getCrossMarginAccountDetails(client) {
76652
76862
  async function cancelMarginOrders(client, symbol, isIsolated = false, orders) {
76653
76863
  try {
76654
76864
  if (orders && orders.length > 0) {
76655
- const cancelPromises = orders.map((order) => client.marginAccountCancelOrder({
76656
- symbol,
76657
- orderId: order.orderId,
76658
- origClientOrderId: order.origClientOrderId,
76659
- isIsolated: isIsolated ? "TRUE" : "FALSE"
76660
- }));
76865
+ const cancelPromises = orders.map((order) => {
76866
+ const payload = {
76867
+ symbol,
76868
+ isIsolated: isIsolated ? "TRUE" : "FALSE"
76869
+ };
76870
+ if (order.orderId !== undefined) {
76871
+ payload.orderId = order.orderId;
76872
+ }
76873
+ const origClientOrderId = order.origClientOrderId ?? order.clientOrderId;
76874
+ if (origClientOrderId !== undefined) {
76875
+ payload.origClientOrderId = origClientOrderId;
76876
+ }
76877
+ return client.marginAccountCancelOrder(payload);
76878
+ });
76661
76879
  return await Promise.all(cancelPromises);
76662
76880
  } else {
76663
76881
  return await client.marginAccountCancelOpenOrders({
@@ -76912,7 +77130,7 @@ async function placeStopOrder(client, payload) {
76912
77130
  });
76913
77131
  }
76914
77132
  }
76915
- const spread = 1.00005;
77133
+ const spread = payload.stop > 30000 ? 1.00005 : 1.0002;
76916
77134
  const order = {
76917
77135
  kind: payload.kind,
76918
77136
  side: payload.kind === "long" ? "sell" : "buy",
@@ -77319,6 +77537,16 @@ async function getAllOpenOrders(payload) {
77319
77537
  const response = await client.getAllOpenOrders();
77320
77538
  return response;
77321
77539
  }
77540
+ function getPrintfFormatFromStepSize(stepSize, fallback) {
77541
+ if (stepSize === undefined || stepSize === null) {
77542
+ return fallback;
77543
+ }
77544
+ const normalized = String(stepSize);
77545
+ const [, decimalPart = ""] = normalized.split(".");
77546
+ const trimmed = decimalPart.replace(/0+$/, "");
77547
+ const digits = trimmed.length;
77548
+ return `%.${digits}f`;
77549
+ }
77322
77550
 
77323
77551
  class BinanceExchange extends BaseExchange {
77324
77552
  main_client;
@@ -77755,6 +77983,30 @@ class BinanceExchange extends BaseExchange {
77755
77983
  throw error;
77756
77984
  }
77757
77985
  }
77986
+ async getSpotSymbolFormats(symbol) {
77987
+ if (!this.main_client || typeof this.main_client.getExchangeInfo !== "function") {
77988
+ return {
77989
+ price_places: "%.8f",
77990
+ decimal_places: "%.8f"
77991
+ };
77992
+ }
77993
+ try {
77994
+ const exchange_info = await this.main_client.getExchangeInfo();
77995
+ const symbol_info = exchange_info?.symbols?.find((item) => item.symbol === symbol.toUpperCase());
77996
+ const price_filter = symbol_info?.filters?.find((filter) => filter.filterType === "PRICE_FILTER");
77997
+ const lot_size_filter = symbol_info?.filters?.find((filter) => filter.filterType === "LOT_SIZE");
77998
+ return {
77999
+ price_places: getPrintfFormatFromStepSize(price_filter?.tickSize, "%.8f"),
78000
+ decimal_places: getPrintfFormatFromStepSize(lot_size_filter?.stepSize, "%.8f")
78001
+ };
78002
+ } catch (error) {
78003
+ console.error("Error fetching spot symbol formats:", error);
78004
+ return {
78005
+ price_places: "%.8f",
78006
+ decimal_places: "%.8f"
78007
+ };
78008
+ }
78009
+ }
77758
78010
  async createMarginLimitOrders(payload) {
77759
78011
  if (!this.main_client) {
77760
78012
  throw new Error("Main client not available for margin trading");
@@ -77785,6 +78037,200 @@ class BinanceExchange extends BaseExchange {
77785
78037
  }
77786
78038
  return await getMarginOpenOrders(this.main_client, symbol, isIsolated);
77787
78039
  }
78040
+ async getSpotMarginHedgeSnapshot(payload) {
78041
+ if (!this.main_client) {
78042
+ throw new Error("Main client not available for spot and margin trading");
78043
+ }
78044
+ if (payload.margin_type !== "isolated") {
78045
+ throw new Error(`Unsupported margin type: ${payload.margin_type}. Only isolated is supported for replay right now.`);
78046
+ }
78047
+ const symbol = payload.symbol.toUpperCase();
78048
+ const quoteAssets = ["USDT", "USDC", "BUSD", "BTC", "ETH"];
78049
+ const quoteAsset = quoteAssets.find((asset) => symbol.endsWith(asset)) || symbol.slice(-4);
78050
+ const baseAsset = symbol.slice(0, symbol.length - quoteAsset.length);
78051
+ const [spot_orders, margin_orders, spot_balances, isolated_margin_position] = await Promise.all([
78052
+ this.getSpotOpenOrders(symbol),
78053
+ this.getMarginOpenOrders(symbol, true),
78054
+ this.getSpotBalances([baseAsset, quoteAsset]),
78055
+ this.getIsolatedMarginPosition(symbol)
78056
+ ]);
78057
+ const current_price = await this.getSpotCurrentPrice(symbol);
78058
+ const spot_quote_balance = spot_balances.find((balance) => balance.asset.toUpperCase() === quoteAsset);
78059
+ const spot_base_balance = spot_balances.find((balance) => balance.asset.toUpperCase() === baseAsset);
78060
+ const isolated_asset = isolated_margin_position?.assets?.[0];
78061
+ const borrowed_quote_amount = parseFloat(isolated_asset?.quoteAsset?.borrowed?.toString?.() || "0");
78062
+ return {
78063
+ symbol,
78064
+ margin_type: payload.margin_type,
78065
+ quote_asset: quoteAsset,
78066
+ base_asset: baseAsset,
78067
+ borrowed_quote_amount,
78068
+ spot_quote_free: parseFloat(spot_quote_balance?.free?.toString?.() || "0"),
78069
+ spot_base_free: parseFloat(spot_base_balance?.free?.toString?.() || "0"),
78070
+ spot_orders,
78071
+ margin_orders,
78072
+ current_price,
78073
+ spot_balances,
78074
+ isolated_margin_position
78075
+ };
78076
+ }
78077
+ async previewSpotMarginHedge(payload) {
78078
+ const snapshot = payload.snapshot ? {
78079
+ ...payload.snapshot,
78080
+ current_price: payload.snapshot.current_price ?? await this.getSpotCurrentPrice(payload.symbol)
78081
+ } : await this.getSpotMarginHedgeSnapshot({
78082
+ symbol: payload.symbol,
78083
+ margin_type: payload.margin_type
78084
+ });
78085
+ const shouldInferLongOrders = !payload.long_orders || payload.long_orders.length === 0;
78086
+ const shouldInferShortOrders = !payload.short_orders || payload.short_orders.length === 0;
78087
+ const inferred_input = shouldInferLongOrders || shouldInferShortOrders ? inferSpotMarginHedgeInputFromLiveOrders({
78088
+ symbol: payload.symbol,
78089
+ margin_type: payload.margin_type,
78090
+ spot_orders: snapshot.spot_orders,
78091
+ margin_orders: snapshot.margin_orders
78092
+ }) : undefined;
78093
+ const long_orders = shouldInferLongOrders ? inferred_input?.long_orders || [] : payload.long_orders || [];
78094
+ const short_orders = shouldInferShortOrders ? inferred_input?.short_orders || [] : payload.short_orders || [];
78095
+ const borrow_amount = payload.borrow_amount ?? (long_orders.length > 0 ? sumNotional(long_orders) : inferred_input?.borrow_amount ?? 0);
78096
+ const requested_input = {
78097
+ borrow_amount,
78098
+ symbol: payload.symbol,
78099
+ margin_type: payload.margin_type,
78100
+ long_orders,
78101
+ short_orders,
78102
+ current_price: snapshot.current_price,
78103
+ max_spot_buy_orders: payload.max_spot_buy_orders,
78104
+ placement_overrides: inferred_input?.placement_overrides
78105
+ };
78106
+ const plan = buildSpotMarginHedgePlan({
78107
+ ...requested_input,
78108
+ snapshot
78109
+ });
78110
+ const comparison = compareSpotMarginHedgeOrders({
78111
+ plan,
78112
+ spot_orders: snapshot.spot_orders,
78113
+ margin_orders: snapshot.margin_orders
78114
+ });
78115
+ return {
78116
+ snapshot,
78117
+ inferred_input,
78118
+ requested_input,
78119
+ plan,
78120
+ comparison
78121
+ };
78122
+ }
78123
+ async placeSpotMarginHedge(payload) {
78124
+ const preview = await this.previewSpotMarginHedge(payload);
78125
+ if (!payload.place) {
78126
+ return preview;
78127
+ }
78128
+ if (!this.main_client) {
78129
+ throw new Error("Main client not available for spot and margin trading");
78130
+ }
78131
+ if (payload.margin_type !== "isolated") {
78132
+ throw new Error(`Unsupported margin type: ${payload.margin_type}. Only isolated placement is supported right now.`);
78133
+ }
78134
+ const symbol = payload.symbol.toUpperCase();
78135
+ const quoteAssets = ["USDT", "USDC", "BUSD", "BTC", "ETH"];
78136
+ const quote_asset = quoteAssets.find((asset) => symbol.endsWith(asset)) || symbol.slice(-4);
78137
+ const base_asset = symbol.slice(0, symbol.length - quote_asset.length);
78138
+ const requested_input = preview.requested_input;
78139
+ if (preview.snapshot.spot_orders.length > 0) {
78140
+ await this.cancelSpotOrders(symbol, preview.snapshot.spot_orders.map((order) => ({
78141
+ orderId: order.orderId || order.order_id || order.id,
78142
+ origClientOrderId: order.origClientOrderId
78143
+ })));
78144
+ }
78145
+ if (preview.snapshot.margin_orders.length > 0) {
78146
+ await this.cancelMarginOrders(symbol, true, preview.snapshot.margin_orders.map((order) => ({
78147
+ orderId: order.orderId || order.order_id || order.id,
78148
+ origClientOrderId: order.origClientOrderId
78149
+ })));
78150
+ }
78151
+ const refreshed_snapshot = await this.getSpotMarginHedgeSnapshot({
78152
+ symbol,
78153
+ margin_type: payload.margin_type
78154
+ });
78155
+ const symbol_formats = await this.getSpotSymbolFormats(symbol);
78156
+ const execution_plan = buildSpotMarginHedgePlan({
78157
+ borrow_amount: requested_input.borrow_amount,
78158
+ symbol: requested_input.symbol,
78159
+ margin_type: requested_input.margin_type,
78160
+ long_orders: requested_input.long_orders,
78161
+ short_orders: requested_input.short_orders,
78162
+ current_price: refreshed_snapshot.current_price,
78163
+ max_spot_buy_orders: requested_input.max_spot_buy_orders,
78164
+ snapshot: refreshed_snapshot
78165
+ });
78166
+ let borrow_result = null;
78167
+ if (execution_plan.borrow_delta > 0) {
78168
+ borrow_result = await this.main_client.submitMarginAccountBorrowRepay({
78169
+ asset: quote_asset,
78170
+ amount: execution_plan.borrow_delta,
78171
+ isIsolated: "TRUE",
78172
+ symbol,
78173
+ type: "BORROW"
78174
+ });
78175
+ }
78176
+ let quote_transfer_result = null;
78177
+ if (execution_plan.transfer_to_spot_amount > 0) {
78178
+ quote_transfer_result = await this.main_client.isolatedMarginAccountTransfer({
78179
+ asset: quote_asset,
78180
+ amount: execution_plan.transfer_to_spot_amount,
78181
+ symbol,
78182
+ transFrom: "ISOLATED_MARGIN",
78183
+ transTo: "SPOT"
78184
+ });
78185
+ }
78186
+ let base_transfer_result = null;
78187
+ if (execution_plan.transfer_base_to_spot_amount > 0) {
78188
+ base_transfer_result = await this.main_client.isolatedMarginAccountTransfer({
78189
+ asset: base_asset,
78190
+ amount: execution_plan.transfer_base_to_spot_amount,
78191
+ symbol,
78192
+ transFrom: "ISOLATED_MARGIN",
78193
+ transTo: "SPOT"
78194
+ });
78195
+ }
78196
+ const spot_result = execution_plan.orders_to_create.spot.length > 0 ? await this.createSpotLimitOrders({
78197
+ symbol,
78198
+ orders: execution_plan.orders_to_create.spot,
78199
+ price_places: symbol_formats.price_places,
78200
+ decimal_places: symbol_formats.decimal_places
78201
+ }) : [];
78202
+ const margin_result = execution_plan.orders_to_create.margin.length > 0 ? await this.createMarginLimitOrders({
78203
+ symbol,
78204
+ orders: execution_plan.orders_to_create.margin,
78205
+ isIsolated: true,
78206
+ price_places: symbol_formats.price_places,
78207
+ decimal_places: symbol_formats.decimal_places
78208
+ }) : [];
78209
+ return {
78210
+ ...preview,
78211
+ execution_plan,
78212
+ refreshed_snapshot,
78213
+ execution: {
78214
+ borrow: borrow_result ? {
78215
+ asset: quote_asset,
78216
+ amount: execution_plan.borrow_delta,
78217
+ response: borrow_result
78218
+ } : null,
78219
+ quote_transfer: quote_transfer_result ? {
78220
+ asset: quote_asset,
78221
+ amount: execution_plan.transfer_to_spot_amount,
78222
+ response: quote_transfer_result
78223
+ } : null,
78224
+ base_transfer: base_transfer_result ? {
78225
+ asset: base_asset,
78226
+ amount: execution_plan.transfer_base_to_spot_amount,
78227
+ response: base_transfer_result
78228
+ } : null,
78229
+ spot_orders: spot_result,
78230
+ margin_orders: margin_result
78231
+ }
78232
+ };
78233
+ }
77788
78234
  async getLastUserTrade(payload) {
77789
78235
  return await getLastUserTrade(this.client, payload.symbol);
77790
78236
  }