perp-cli 0.7.30 → 0.7.36
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.
|
@@ -47,6 +47,8 @@ export declare class FundingArbStrategy implements Strategy {
|
|
|
47
47
|
* HL getPositions() returns symbols like "PURR" or "PURR-PERP" — we normalize here.
|
|
48
48
|
*/
|
|
49
49
|
private getPerpSymbol;
|
|
50
|
+
/** Look up rate from ratesByExchange, trying symbol, symbol-PERP, and symbol without -PERP */
|
|
51
|
+
private findRate;
|
|
50
52
|
/**
|
|
51
53
|
* Get a price estimate for a symbol from the perp adapter.
|
|
52
54
|
* Tries the base symbol first, then with -PERP suffix.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { registerStrategy } from "../strategy-registry.js";
|
|
2
2
|
import { checkArbLiquidity, checkSpotPerpLiquidity } from "../../liquidity.js";
|
|
3
3
|
import { computeMatchedSize, computeSpotPerpMatchedSize, reconcileArbFills } from "../../arb-sizing.js";
|
|
4
|
-
import {
|
|
4
|
+
import { getFundingHours } from "../../funding.js";
|
|
5
5
|
import { logExecution, pruneExecutionLog } from "../../execution-log.js";
|
|
6
6
|
export class FundingArbStrategy {
|
|
7
7
|
name = "funding-arb";
|
|
@@ -153,7 +153,9 @@ export class FundingArbStrategy {
|
|
|
153
153
|
for (const [name, a] of adapters) {
|
|
154
154
|
try {
|
|
155
155
|
const payments = await a.getFundingPayments(200);
|
|
156
|
-
const
|
|
156
|
+
const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
|
|
157
|
+
const recent = payments.filter(p => p.time > oneDayAgo);
|
|
158
|
+
const sum = recent.reduce((s, p) => s + parseFloat(p.payment), 0);
|
|
157
159
|
if (Math.abs(sum) > 0.001) {
|
|
158
160
|
historicalFunding += sum;
|
|
159
161
|
ctx.log(` [FUND] ${name}: $${sum.toFixed(4)} historical funding`);
|
|
@@ -193,7 +195,7 @@ export class FundingArbStrategy {
|
|
|
193
195
|
const rates = await this.fetchRates(a, name);
|
|
194
196
|
const map = new Map();
|
|
195
197
|
for (const r of rates)
|
|
196
|
-
map.set(r.symbol.toUpperCase(), { rate: r.rate, price: r.price, sizeDecimals: r.sizeDecimals, maxLeverage: r.maxLeverage });
|
|
198
|
+
map.set(r.symbol.toUpperCase(), { rate: r.rate, price: r.price, sizeDecimals: r.sizeDecimals, maxLeverage: r.maxLeverage, fundingHours: r.fundingHours });
|
|
197
199
|
ratesByExchange.set(name, map);
|
|
198
200
|
}
|
|
199
201
|
// ── Fetch funding income periodically (every 5 minutes) ──
|
|
@@ -227,15 +229,19 @@ export class FundingArbStrategy {
|
|
|
227
229
|
// For spot-perp: long side is spot (rate=0), short side is perp
|
|
228
230
|
const perpExchange = p.shortExchange;
|
|
229
231
|
const perpSymbol = this.getPerpSymbol(p.symbol, perpExchange);
|
|
230
|
-
const
|
|
231
|
-
??
|
|
232
|
-
|
|
232
|
+
const rateInfo = this.findRate(ratesByExchange, perpExchange, perpSymbol)
|
|
233
|
+
?? this.findRate(ratesByExchange, perpExchange, p.symbol);
|
|
234
|
+
const shortRate = rateInfo?.rate;
|
|
235
|
+
const fundingH = rateInfo?.fundingHours ?? getFundingHours(perpExchange);
|
|
236
|
+
signedSpread = shortRate !== undefined ? (shortRate / fundingH) * 8760 * 100 : 0;
|
|
233
237
|
}
|
|
234
238
|
else {
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
+
const longInfo = this.findRate(ratesByExchange, p.longExchange, p.symbol);
|
|
240
|
+
const shortInfo = this.findRate(ratesByExchange, p.shortExchange, p.symbol);
|
|
241
|
+
const longHourly = longInfo ? longInfo.rate / (longInfo.fundingHours ?? getFundingHours(p.longExchange)) : undefined;
|
|
242
|
+
const shortHourly = shortInfo ? shortInfo.rate / (shortInfo.fundingHours ?? getFundingHours(p.shortExchange)) : undefined;
|
|
243
|
+
signedSpread = (longHourly !== undefined && shortHourly !== undefined)
|
|
244
|
+
? (shortHourly - longHourly) * 8760 * 100
|
|
239
245
|
: 0;
|
|
240
246
|
}
|
|
241
247
|
const sign = signedSpread >= 0 ? "+" : "";
|
|
@@ -250,18 +256,21 @@ export class FundingArbStrategy {
|
|
|
250
256
|
if (pos.mode === "spot-perp") {
|
|
251
257
|
const perpExchange = pos.shortExchange;
|
|
252
258
|
const perpSymbol = this.getPerpSymbol(pos.symbol, perpExchange);
|
|
253
|
-
const
|
|
254
|
-
??
|
|
255
|
-
if (
|
|
259
|
+
const rateInfo = this.findRate(ratesByExchange, perpExchange, perpSymbol)
|
|
260
|
+
?? this.findRate(ratesByExchange, perpExchange, pos.symbol);
|
|
261
|
+
if (rateInfo === undefined)
|
|
256
262
|
continue;
|
|
257
|
-
|
|
263
|
+
const fundingH = rateInfo.fundingHours ?? getFundingHours(perpExchange);
|
|
264
|
+
signedSpread = (rateInfo.rate / fundingH) * 8760 * 100;
|
|
258
265
|
}
|
|
259
266
|
else {
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
if (
|
|
267
|
+
const longInfo = this.findRate(ratesByExchange, pos.longExchange, pos.symbol);
|
|
268
|
+
const shortInfo = this.findRate(ratesByExchange, pos.shortExchange, pos.symbol);
|
|
269
|
+
if (longInfo === undefined || shortInfo === undefined)
|
|
263
270
|
continue;
|
|
264
|
-
|
|
271
|
+
const longHourly = longInfo.rate / (longInfo.fundingHours ?? getFundingHours(pos.longExchange));
|
|
272
|
+
const shortHourly = shortInfo.rate / (shortInfo.fundingHours ?? getFundingHours(pos.shortExchange));
|
|
273
|
+
signedSpread = (shortHourly - longHourly) * 8760 * 100;
|
|
265
274
|
}
|
|
266
275
|
// Close if funding flipped (losing money) or spread below close threshold
|
|
267
276
|
const shouldClose = signedSpread < 0 || signedSpread < params.close_spread;
|
|
@@ -293,19 +302,32 @@ export class FundingArbStrategy {
|
|
|
293
302
|
logExecution({ type: "arb_close", exchange: pos.longExchange, symbol: pos.symbol, side: "sell", size: pos.size, status: "failed", error: msg, dryRun: false, meta: { mode: pos.mode, leg: "long" } });
|
|
294
303
|
this._failCooldown.set(`${pos.symbol}:close`, Date.now() + 5 * 60 * 1000);
|
|
295
304
|
}
|
|
305
|
+
if (!closeLongOk)
|
|
306
|
+
continue; // don't close short if long failed — keep hedge intact
|
|
296
307
|
try {
|
|
297
308
|
const closeShortResult = await shortAdapter.marketOrder(pos.symbol, "buy", pos.size);
|
|
298
309
|
logExecution({ type: "arb_close", exchange: pos.shortExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "success", dryRun: false, meta: { mode: pos.mode, leg: "short", signedSpread: signedSpread, response: closeShortResult } });
|
|
299
310
|
}
|
|
300
311
|
catch (err) {
|
|
301
312
|
const msg = err instanceof Error ? err.message : String(err);
|
|
302
|
-
ctx.log(` [ARB] Close short leg failed for ${pos.symbol}: ${msg}`);
|
|
313
|
+
ctx.log(` [ARB] Close short leg failed for ${pos.symbol}: ${msg} — re-opening long to restore hedge`);
|
|
303
314
|
logExecution({ type: "arb_close", exchange: pos.shortExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "failed", error: msg, dryRun: false, meta: { mode: pos.mode, leg: "short" } });
|
|
315
|
+
// Rollback: re-open long to restore hedge
|
|
316
|
+
try {
|
|
317
|
+
await longAdapter.marketOrder(pos.symbol, "buy", pos.size);
|
|
318
|
+
ctx.log(` [ARB] Rollback: re-opened long ${pos.size} ${pos.symbol} on ${pos.longExchange}`);
|
|
319
|
+
logExecution({ type: "multi_leg_rollback", exchange: pos.longExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "success", dryRun: false });
|
|
320
|
+
}
|
|
321
|
+
catch (rbErr) {
|
|
322
|
+
const rbMsg = rbErr instanceof Error ? rbErr.message : String(rbErr);
|
|
323
|
+
ctx.log(` [ARB] CRITICAL: Close rollback failed for ${pos.symbol}: ${rbMsg}`);
|
|
324
|
+
logExecution({ type: "multi_leg_rollback", exchange: pos.longExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "failed", error: rbMsg, dryRun: false });
|
|
325
|
+
}
|
|
326
|
+
this._failCooldown.set(`${pos.symbol}:close`, Date.now() + 5 * 60 * 1000);
|
|
327
|
+
continue;
|
|
304
328
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
ctx.log(` [ARB] Closed ${pos.symbol} position`);
|
|
308
|
-
}
|
|
329
|
+
toClose.push(pos);
|
|
330
|
+
ctx.log(` [ARB] Closed ${pos.symbol} position`);
|
|
309
331
|
}
|
|
310
332
|
}
|
|
311
333
|
}
|
|
@@ -327,23 +349,27 @@ export class FundingArbStrategy {
|
|
|
327
349
|
}
|
|
328
350
|
// Perp-perp opportunities
|
|
329
351
|
for (const sym of allSymbols) {
|
|
330
|
-
let
|
|
352
|
+
let minHourly = Infinity, maxHourly = -Infinity;
|
|
331
353
|
let minExchange = "", maxExchange = "";
|
|
354
|
+
let minRate = 0, maxRate = 0;
|
|
332
355
|
for (const exName of exchangeNames) {
|
|
333
|
-
const
|
|
334
|
-
if (
|
|
356
|
+
const rateInfo = this.findRate(ratesByExchange, exName, sym);
|
|
357
|
+
if (!rateInfo)
|
|
335
358
|
continue;
|
|
336
|
-
|
|
337
|
-
|
|
359
|
+
const hourlyRate = rateInfo.rate / (rateInfo.fundingHours ?? getFundingHours(exName));
|
|
360
|
+
if (hourlyRate < minHourly) {
|
|
361
|
+
minHourly = hourlyRate;
|
|
338
362
|
minExchange = exName;
|
|
363
|
+
minRate = rateInfo.rate;
|
|
339
364
|
}
|
|
340
|
-
if (
|
|
341
|
-
|
|
365
|
+
if (hourlyRate > maxHourly) {
|
|
366
|
+
maxHourly = hourlyRate;
|
|
342
367
|
maxExchange = exName;
|
|
368
|
+
maxRate = rateInfo.rate;
|
|
343
369
|
}
|
|
344
370
|
}
|
|
345
371
|
if (minExchange && maxExchange && minExchange !== maxExchange) {
|
|
346
|
-
const spread =
|
|
372
|
+
const spread = (maxHourly - minHourly) * 8760 * 100;
|
|
347
373
|
if (spread >= params.min_spread) {
|
|
348
374
|
opportunities.push({
|
|
349
375
|
symbol: sym,
|
|
@@ -369,14 +395,16 @@ export class FundingArbStrategy {
|
|
|
369
395
|
const perpRates = ratesByExchange.get(name);
|
|
370
396
|
if (!perpRates)
|
|
371
397
|
continue;
|
|
372
|
-
for (const [sym,
|
|
398
|
+
for (const [sym, rateEntry] of perpRates) {
|
|
399
|
+
const { rate } = rateEntry;
|
|
373
400
|
// Only consider positive funding (shorts receive, longs pay)
|
|
374
401
|
if (rate <= 0)
|
|
375
402
|
continue;
|
|
376
403
|
const base = sym.replace(/-PERP$/, "").toUpperCase();
|
|
377
404
|
if (!spotSymbols.has(base))
|
|
378
405
|
continue;
|
|
379
|
-
const
|
|
406
|
+
const hourlyRate = rate / (rateEntry.fundingHours ?? getFundingHours(name));
|
|
407
|
+
const annualSpread = hourlyRate * 8760 * 100;
|
|
380
408
|
if (annualSpread >= spotPerpMinSpread) {
|
|
381
409
|
opportunities.push({
|
|
382
410
|
symbol: base,
|
|
@@ -432,13 +460,13 @@ export class FundingArbStrategy {
|
|
|
432
460
|
const shortAdapter = adapters.get(best.shortExchange);
|
|
433
461
|
if (!longAdapter || !shortAdapter)
|
|
434
462
|
continue;
|
|
435
|
-
const price =
|
|
463
|
+
const price = this.findRate(ratesByExchange, best.longExchange, best.symbol)?.price ?? 0;
|
|
436
464
|
if (price <= 0)
|
|
437
465
|
continue;
|
|
438
466
|
// Determine leverage for this symbol
|
|
439
467
|
const configLeverage = params.leverage ?? 3;
|
|
440
|
-
const longMaxLev =
|
|
441
|
-
const shortMaxLev =
|
|
468
|
+
const longMaxLev = this.findRate(ratesByExchange, best.longExchange, best.symbol)?.maxLeverage ?? 1;
|
|
469
|
+
const shortMaxLev = this.findRate(ratesByExchange, best.shortExchange, best.symbol)?.maxLeverage ?? 1;
|
|
442
470
|
const leverage = Math.min(configLeverage, longMaxLev, shortMaxLev);
|
|
443
471
|
// Set leverage on both exchanges before ordering
|
|
444
472
|
try {
|
|
@@ -455,12 +483,12 @@ export class FundingArbStrategy {
|
|
|
455
483
|
longAdapter.getBalance(),
|
|
456
484
|
shortAdapter.getBalance(),
|
|
457
485
|
]);
|
|
458
|
-
// Check margin usage (skip if >
|
|
486
|
+
// Check margin usage (skip if > 70%)
|
|
459
487
|
const longEquity = parseFloat(longBal.equity);
|
|
460
488
|
const shortEquity = parseFloat(shortBal.equity);
|
|
461
489
|
const longMarginPct = longEquity > 0 ? parseFloat(longBal.marginUsed) / longEquity * 100 : 0;
|
|
462
490
|
const shortMarginPct = shortEquity > 0 ? parseFloat(shortBal.marginUsed) / shortEquity * 100 : 0;
|
|
463
|
-
if (longMarginPct >
|
|
491
|
+
if (longMarginPct > 70 || shortMarginPct > 70) {
|
|
464
492
|
skipMargin++;
|
|
465
493
|
continue;
|
|
466
494
|
}
|
|
@@ -486,8 +514,8 @@ export class FundingArbStrategy {
|
|
|
486
514
|
if (!liq.viable)
|
|
487
515
|
continue;
|
|
488
516
|
// Compute matched size (use actual precision from exchange market data)
|
|
489
|
-
const longDec =
|
|
490
|
-
const shortDec =
|
|
517
|
+
const longDec = this.findRate(ratesByExchange, best.longExchange, best.symbol)?.sizeDecimals;
|
|
518
|
+
const shortDec = this.findRate(ratesByExchange, best.shortExchange, best.symbol)?.sizeDecimals;
|
|
491
519
|
let matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange, { longSizeDecimals: longDec, shortSizeDecimals: shortDec });
|
|
492
520
|
if (!matched) {
|
|
493
521
|
ctx.log(` [ARB] Skip ${best.symbol}: can't compute matched size (min notional or precision issue)`);
|
|
@@ -541,6 +569,39 @@ export class FundingArbStrategy {
|
|
|
541
569
|
const reconMsg = reconErr instanceof Error ? reconErr.message : String(reconErr);
|
|
542
570
|
ctx.log(` [ARB] WARNING: Fill reconciliation failed: ${reconMsg}`);
|
|
543
571
|
}
|
|
572
|
+
// Verify both positions actually exist (catches instant liquidation)
|
|
573
|
+
try {
|
|
574
|
+
const [longPositions, shortPositions] = await Promise.all([
|
|
575
|
+
longAdapter.getPositions(),
|
|
576
|
+
shortAdapter.getPositions(),
|
|
577
|
+
]);
|
|
578
|
+
const longExists = longPositions.some(p => p.symbol.toUpperCase() === best.symbol.toUpperCase() && parseFloat(p.size) > 0);
|
|
579
|
+
const shortExists = shortPositions.some(p => p.symbol.toUpperCase() === best.symbol.toUpperCase() && parseFloat(p.size) > 0);
|
|
580
|
+
if (!longExists || !shortExists) {
|
|
581
|
+
ctx.log(` [ARB] Position verification failed for ${best.symbol}: long=${longExists}, short=${shortExists}`);
|
|
582
|
+
logExecution({ type: "arb_entry", exchange: `${best.longExchange}↔${best.shortExchange}`, symbol: best.symbol, side: "verify", size: matched.size, status: "failed", error: `Position missing after fill: long=${longExists} short=${shortExists}`, dryRun: false });
|
|
583
|
+
// Rollback whichever side exists
|
|
584
|
+
if (longExists && !shortExists) {
|
|
585
|
+
try {
|
|
586
|
+
await longAdapter.marketOrder(best.symbol, "sell", matched.size);
|
|
587
|
+
ctx.log(` [ARB] Rollback: closed phantom long ${best.symbol}`);
|
|
588
|
+
}
|
|
589
|
+
catch { /* best effort */ }
|
|
590
|
+
}
|
|
591
|
+
if (shortExists && !longExists) {
|
|
592
|
+
try {
|
|
593
|
+
await shortAdapter.marketOrder(best.symbol, "buy", matched.size);
|
|
594
|
+
ctx.log(` [ARB] Rollback: closed phantom short ${best.symbol}`);
|
|
595
|
+
}
|
|
596
|
+
catch { /* best effort */ }
|
|
597
|
+
}
|
|
598
|
+
this._failCooldown.set(`${best.symbol}:entry`, Date.now() + 10 * 60 * 1000);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
catch (verifyErr) {
|
|
603
|
+
ctx.log(` [ARB] Position verification error for ${best.symbol}: ${verifyErr instanceof Error ? verifyErr.message : String(verifyErr)}`);
|
|
604
|
+
}
|
|
544
605
|
// Track position
|
|
545
606
|
const currentPositions = ctx.state.get("arbOpenPositions");
|
|
546
607
|
currentPositions.push({
|
|
@@ -698,8 +759,8 @@ export class FundingArbStrategy {
|
|
|
698
759
|
// Determine leverage for perp leg
|
|
699
760
|
const configLeverage = params.leverage ?? 3;
|
|
700
761
|
const perpSymbolUpper = this.getPerpSymbol(opp.symbol, opp.shortExchange).toUpperCase();
|
|
701
|
-
const perpMaxLev =
|
|
702
|
-
??
|
|
762
|
+
const perpMaxLev = this.findRate(ratesByExchange, opp.shortExchange, perpSymbolUpper)?.maxLeverage
|
|
763
|
+
?? this.findRate(ratesByExchange, opp.shortExchange, opp.symbol.toUpperCase())?.maxLeverage
|
|
703
764
|
?? 1;
|
|
704
765
|
const leverage = Math.min(configLeverage, perpMaxLev);
|
|
705
766
|
// Set leverage on perp before ordering
|
|
@@ -714,7 +775,7 @@ export class FundingArbStrategy {
|
|
|
714
775
|
// Check margin usage
|
|
715
776
|
const perpEquity = parseFloat(perpBal.equity);
|
|
716
777
|
const perpMarginPct = perpEquity > 0 ? parseFloat(perpBal.marginUsed) / perpEquity * 100 : 0;
|
|
717
|
-
if (perpMarginPct >
|
|
778
|
+
if (perpMarginPct > 70) {
|
|
718
779
|
ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: perp margin usage too high (${perpMarginPct.toFixed(0)}%)`);
|
|
719
780
|
return false;
|
|
720
781
|
}
|
|
@@ -888,14 +949,20 @@ export class FundingArbStrategy {
|
|
|
888
949
|
* HL getPositions() returns symbols like "PURR" or "PURR-PERP" — we normalize here.
|
|
889
950
|
*/
|
|
890
951
|
getPerpSymbol(baseSymbol, exchangeName) {
|
|
891
|
-
// Strip any existing -PERP suffix first
|
|
892
952
|
const base = baseSymbol.replace(/-PERP$/, "").toUpperCase();
|
|
893
|
-
// HL perp symbols are just the base (e.g. "PURR", "ETH", "BTC")
|
|
894
|
-
// Some HIP-3 tokens might need "-PERP" suffix — use base for now
|
|
895
|
-
// since getPositions() returns the base form on HL
|
|
896
953
|
void exchangeName;
|
|
897
954
|
return base;
|
|
898
955
|
}
|
|
956
|
+
/** Look up rate from ratesByExchange, trying symbol, symbol-PERP, and symbol without -PERP */
|
|
957
|
+
findRate(ratesByExchange, exchange, symbol) {
|
|
958
|
+
const map = ratesByExchange.get(exchange);
|
|
959
|
+
if (!map)
|
|
960
|
+
return undefined;
|
|
961
|
+
const upper = symbol.toUpperCase();
|
|
962
|
+
return map.get(upper)
|
|
963
|
+
?? map.get(upper + "-PERP")
|
|
964
|
+
?? map.get(upper.replace(/-PERP$/, ""));
|
|
965
|
+
}
|
|
899
966
|
/**
|
|
900
967
|
* Get a price estimate for a symbol from the perp adapter.
|
|
901
968
|
* Tries the base symbol first, then with -PERP suffix.
|
|
@@ -923,6 +990,7 @@ export class FundingArbStrategy {
|
|
|
923
990
|
price: parseFloat(m.markPrice),
|
|
924
991
|
sizeDecimals: m.sizeDecimals,
|
|
925
992
|
maxLeverage: m.maxLeverage,
|
|
993
|
+
fundingHours: m.fundingHours,
|
|
926
994
|
}));
|
|
927
995
|
}
|
|
928
996
|
catch {
|
package/dist/exchanges/aster.js
CHANGED
|
@@ -77,6 +77,12 @@ export class AsterAdapter {
|
|
|
77
77
|
const maxLev = Number(s.maxLeverage ?? 50);
|
|
78
78
|
const lotFilter = s.filters
|
|
79
79
|
?.find(f => f.filterType === "LOT_SIZE");
|
|
80
|
+
const nextFunding = Number(premium?.nextFundingTime ?? 0);
|
|
81
|
+
const hoursFromMidnight = nextFunding > 0 ? (nextFunding % 86400000) / 3600000 : 0;
|
|
82
|
+
const fundingHours = nextFunding <= 0 ? 1
|
|
83
|
+
: hoursFromMidnight % 8 === 0 ? 8
|
|
84
|
+
: hoursFromMidnight % 4 === 0 ? 4
|
|
85
|
+
: 1;
|
|
80
86
|
return {
|
|
81
87
|
symbol: this._fromApi(sym),
|
|
82
88
|
markPrice: String(premium?.markPrice ?? ticker?.lastPrice ?? "0"),
|
|
@@ -87,6 +93,7 @@ export class AsterAdapter {
|
|
|
87
93
|
maxLeverage: maxLev,
|
|
88
94
|
sizeDecimals: s.quantityPrecision != null ? Number(s.quantityPrecision) : undefined,
|
|
89
95
|
stepSize: lotFilter?.stepSize != null ? String(lotFilter.stepSize) : undefined,
|
|
96
|
+
fundingHours,
|
|
90
97
|
};
|
|
91
98
|
});
|
|
92
99
|
this._marketsCache = result;
|
|
@@ -277,17 +284,51 @@ export class AsterAdapter {
|
|
|
277
284
|
}
|
|
278
285
|
// ── Trading ──
|
|
279
286
|
async marketOrder(symbol, side, size) {
|
|
287
|
+
const apiSymbol = this._toApi(symbol);
|
|
280
288
|
const result = await this._signedPost("/fapi/v1/order", {
|
|
281
|
-
symbol:
|
|
289
|
+
symbol: apiSymbol,
|
|
282
290
|
side: side.toUpperCase(),
|
|
283
291
|
type: "MARKET",
|
|
284
292
|
quantity: size,
|
|
285
293
|
});
|
|
286
|
-
// Validate fill — Binance-style API returns 200 even for 0-fill orders
|
|
287
294
|
const r = result;
|
|
288
295
|
const executedQty = Number(r.executedQty ?? 0);
|
|
296
|
+
const orderId = String(r.orderId ?? "");
|
|
297
|
+
// If immediately filled, return
|
|
298
|
+
if (executedQty > 0 && r.status === "FILLED")
|
|
299
|
+
return result;
|
|
300
|
+
// status=NEW: order accepted but not yet filled — poll for up to 3 seconds
|
|
301
|
+
if (r.status === "NEW" && orderId) {
|
|
302
|
+
for (let i = 0; i < 3; i++) {
|
|
303
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
304
|
+
try {
|
|
305
|
+
const check = await this._signedGet("/fapi/v1/order", {
|
|
306
|
+
symbol: apiSymbol,
|
|
307
|
+
orderId,
|
|
308
|
+
});
|
|
309
|
+
const filledQty = Number(check.executedQty ?? 0);
|
|
310
|
+
if (filledQty > 0)
|
|
311
|
+
return check; // filled
|
|
312
|
+
if (check.status === "CANCELED" || check.status === "EXPIRED" || check.status === "REJECTED") {
|
|
313
|
+
throw new Error(`Market ${side} ${symbol}: order ${check.status} (orderId: ${orderId})`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch (e) {
|
|
317
|
+
if (e instanceof Error && e.message.startsWith("Market "))
|
|
318
|
+
throw e;
|
|
319
|
+
// query failed, retry
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Still not filled after 3s — cancel and throw
|
|
323
|
+
try {
|
|
324
|
+
await this._signedDelete("/fapi/v1/order", { symbol: apiSymbol, orderId });
|
|
325
|
+
}
|
|
326
|
+
catch { /* best effort cancel */ }
|
|
327
|
+
throw new Error(`Market ${side} ${symbol}: order not filled after 3s, cancelled (orderId: ${orderId})`);
|
|
328
|
+
}
|
|
329
|
+
// executedQty === 0 with non-NEW status
|
|
289
330
|
if (executedQty === 0) {
|
|
290
|
-
throw new Error(`Market ${side} ${symbol}: order accepted but 0 filled (status: ${r.status}, orderId: ${
|
|
331
|
+
throw new Error(`Market ${side} ${symbol}: order accepted but 0 filled (status: ${r.status}, orderId: ${orderId})`);
|
|
291
332
|
}
|
|
292
333
|
return result;
|
|
293
334
|
}
|