perp-cli 0.7.43 → 0.7.47
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.
|
@@ -26,10 +26,13 @@ export declare class FundingArbStrategy implements Strategy {
|
|
|
26
26
|
private get params();
|
|
27
27
|
private _config;
|
|
28
28
|
private _failCooldown;
|
|
29
|
+
private _spotAdapterCache;
|
|
29
30
|
init(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<void>;
|
|
30
31
|
onTick(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
|
|
31
32
|
onStop(ctx: StrategyContext): Promise<StrategyAction[]>;
|
|
32
|
-
/** Close a spot-perp position: sell spot, transfer USDC back, close perp short
|
|
33
|
+
/** Close a spot-perp position: sell spot, transfer USDC back, close perp short.
|
|
34
|
+
* Returns true only if ALL steps succeed. If spot sell succeeds but perp close fails,
|
|
35
|
+
* attempts to re-buy spot as rollback and returns false. */
|
|
33
36
|
private closeSpotPerp;
|
|
34
37
|
/** Open a spot-perp position: transfer USDC to spot, buy spot, short perp */
|
|
35
38
|
private openSpotPerp;
|
|
@@ -25,6 +25,7 @@ export class FundingArbStrategy {
|
|
|
25
25
|
}
|
|
26
26
|
_config = {};
|
|
27
27
|
_failCooldown = new Map(); // "symbol:action" → timestamp of next retry
|
|
28
|
+
_spotAdapterCache = new Map();
|
|
28
29
|
async init(ctx, _snapshot) {
|
|
29
30
|
// Prune old execution logs (keep 30 days)
|
|
30
31
|
try {
|
|
@@ -168,6 +169,7 @@ export class FundingArbStrategy {
|
|
|
168
169
|
ctx.state.set("fundingIncome", historicalFunding);
|
|
169
170
|
ctx.log(` [FUND] Total historical funding: $${historicalFunding.toFixed(4)}`);
|
|
170
171
|
}
|
|
172
|
+
// Note: aster funding intervals are bootstrapped lazily via getFundingHours()
|
|
171
173
|
}
|
|
172
174
|
catch (err) {
|
|
173
175
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -200,15 +202,17 @@ export class FundingArbStrategy {
|
|
|
200
202
|
const marginUsed = parseFloat(bal.marginUsed);
|
|
201
203
|
const marginPct = equity > 0 ? (marginUsed / equity) * 100 : 100;
|
|
202
204
|
exchangeBalances.set(name, { equity, available: parseFloat(bal.available), marginPct });
|
|
203
|
-
if (marginPct <
|
|
205
|
+
if (marginPct < 90) {
|
|
204
206
|
availableAdapters.set(name, a);
|
|
205
207
|
}
|
|
206
208
|
else {
|
|
207
209
|
ctx.log(` [ARB] ${name} margin ${marginPct.toFixed(0)}% — skipping for this tick`);
|
|
208
210
|
}
|
|
209
211
|
}
|
|
210
|
-
catch {
|
|
211
|
-
|
|
212
|
+
catch (err) {
|
|
213
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
214
|
+
ctx.log(` [ARB] getBalance failed for ${name}: ${msg} — skipping`);
|
|
215
|
+
// Don't include in availableAdapters
|
|
212
216
|
}
|
|
213
217
|
}
|
|
214
218
|
// Determine which exchanges need rate fetching.
|
|
@@ -322,8 +326,9 @@ export class FundingArbStrategy {
|
|
|
322
326
|
const sign = signedSpread >= 0 ? "+" : "";
|
|
323
327
|
ctx.log(` [ARB] Closing ${pos.symbol}: signed spread ${sign}${signedSpread.toFixed(1)}% (close_spread=${params.close_spread}%, entry=${pos.entrySpread.toFixed(1)}%)`);
|
|
324
328
|
if (pos.mode === "spot-perp") {
|
|
325
|
-
await this.closeSpotPerp(pos, adapters, ctx);
|
|
326
|
-
|
|
329
|
+
const spotPerpClosed = await this.closeSpotPerp(pos, adapters, ctx);
|
|
330
|
+
if (spotPerpClosed)
|
|
331
|
+
toClose.push(pos);
|
|
327
332
|
}
|
|
328
333
|
else {
|
|
329
334
|
const longAdapter = adapters.get(pos.longExchange);
|
|
@@ -510,12 +515,23 @@ export class FundingArbStrategy {
|
|
|
510
515
|
const leverage = Math.min(configLeverage, longMaxLev, shortMaxLev);
|
|
511
516
|
// Set leverage on both exchanges before ordering
|
|
512
517
|
try {
|
|
513
|
-
await
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
518
|
+
await longAdapter.setLeverage(best.symbol, leverage, "cross");
|
|
519
|
+
}
|
|
520
|
+
catch (err) {
|
|
521
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
522
|
+
ctx.log(` [ARB] setLeverage failed on ${best.longExchange} for ${best.symbol}: ${msg}`);
|
|
523
|
+
this._failCooldown.set(`${best.symbol}:entry`, Date.now() + 5 * 60 * 1000);
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
await shortAdapter.setLeverage(best.symbol, leverage, "cross");
|
|
528
|
+
}
|
|
529
|
+
catch (err) {
|
|
530
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
531
|
+
ctx.log(` [ARB] setLeverage failed on ${best.shortExchange} for ${best.symbol}: ${msg}`);
|
|
532
|
+
this._failCooldown.set(`${best.symbol}:entry`, Date.now() + 5 * 60 * 1000);
|
|
533
|
+
continue;
|
|
517
534
|
}
|
|
518
|
-
catch { /* non-critical, some exchanges set leverage on order */ }
|
|
519
535
|
// Balance + margin check (use pre-fetched balances from tick start)
|
|
520
536
|
let targetSizeUsd = params.size_usd;
|
|
521
537
|
const longBal = exchangeBalances.get(best.longExchange);
|
|
@@ -579,6 +595,20 @@ export class FundingArbStrategy {
|
|
|
579
595
|
const rbMsg = rbErr instanceof Error ? rbErr.message : String(rbErr);
|
|
580
596
|
ctx.log(` [ARB] CRITICAL: Rollback failed for ${best.symbol}: ${rbMsg}`);
|
|
581
597
|
logExecution({ type: "multi_leg_rollback", exchange: best.longExchange, symbol: best.symbol, side: "sell", size: matched.size, status: "failed", error: rbMsg, dryRun: false });
|
|
598
|
+
// Track orphaned long position for manual cleanup
|
|
599
|
+
const currentPositions = ctx.state.get("arbOpenPositions");
|
|
600
|
+
currentPositions.push({
|
|
601
|
+
symbol: best.symbol,
|
|
602
|
+
longExchange: best.longExchange,
|
|
603
|
+
shortExchange: best.shortExchange, // note: short leg doesn't exist
|
|
604
|
+
entrySpread: 0,
|
|
605
|
+
size: matched.size,
|
|
606
|
+
mode: "perp-perp",
|
|
607
|
+
entryTime: 0, // flag as orphaned
|
|
608
|
+
});
|
|
609
|
+
ctx.state.set("arbOpenPositions", currentPositions);
|
|
610
|
+
ctx.state.set("arbPositions", currentPositions.length);
|
|
611
|
+
ctx.log(` [ARB] Tracking orphaned long position for manual cleanup`);
|
|
582
612
|
}
|
|
583
613
|
this._failCooldown.set(`${best.symbol}:entry`, Date.now() + 5 * 60 * 1000);
|
|
584
614
|
continue;
|
|
@@ -674,38 +704,70 @@ export class FundingArbStrategy {
|
|
|
674
704
|
for (const [name, a] of extraAdapters)
|
|
675
705
|
adapters.set(name, a);
|
|
676
706
|
}
|
|
707
|
+
const remainingPositions = [];
|
|
677
708
|
for (const pos of openPositions) {
|
|
678
709
|
if (pos.mode === "spot-perp") {
|
|
679
|
-
await this.closeSpotPerp(pos, adapters, ctx);
|
|
710
|
+
const closed = await this.closeSpotPerp(pos, adapters, ctx);
|
|
711
|
+
if (!closed) {
|
|
712
|
+
ctx.log(` [ARB] CRITICAL: spot-perp ${pos.symbol} could not be fully closed on stop — requires manual cleanup`);
|
|
713
|
+
remainingPositions.push(pos);
|
|
714
|
+
}
|
|
680
715
|
}
|
|
681
716
|
else {
|
|
682
717
|
const longAdapter = adapters.get(pos.longExchange);
|
|
683
718
|
const shortAdapter = adapters.get(pos.shortExchange);
|
|
684
719
|
if (longAdapter && shortAdapter) {
|
|
685
720
|
ctx.log(` [ARB] Closing ${pos.symbol} on stop (${pos.size})`);
|
|
721
|
+
let closeLongOk = false;
|
|
686
722
|
try {
|
|
687
723
|
const closeLongResult = await longAdapter.marketOrder(pos.symbol, "sell", pos.size, { reduceOnly: true });
|
|
688
724
|
logExecution({ type: "arb_close", exchange: pos.longExchange, symbol: pos.symbol, side: "sell", size: pos.size, status: "success", dryRun: false, meta: { mode: pos.mode, leg: "long", stop: true, response: closeLongResult } });
|
|
725
|
+
closeLongOk = true;
|
|
689
726
|
}
|
|
690
727
|
catch (err) {
|
|
691
728
|
const msg = err instanceof Error ? err.message : String(err);
|
|
692
729
|
ctx.log(` [ARB] Close on stop failed (long) for ${pos.symbol}: ${msg}`);
|
|
693
730
|
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", stop: true } });
|
|
694
731
|
}
|
|
732
|
+
if (!closeLongOk) {
|
|
733
|
+
ctx.log(` [ARB] CRITICAL: ${pos.symbol} long close failed on stop — skipping short close to preserve hedge`);
|
|
734
|
+
remainingPositions.push(pos);
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
let closeShortOk = false;
|
|
695
738
|
try {
|
|
696
739
|
const closeShortResult = await shortAdapter.marketOrder(pos.symbol, "buy", pos.size, { reduceOnly: true });
|
|
697
740
|
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", stop: true, response: closeShortResult } });
|
|
741
|
+
closeShortOk = true;
|
|
698
742
|
}
|
|
699
743
|
catch (err) {
|
|
700
744
|
const msg = err instanceof Error ? err.message : String(err);
|
|
701
|
-
ctx.log(` [ARB] Close on stop failed (short) for ${pos.symbol}: ${msg}`);
|
|
745
|
+
ctx.log(` [ARB] Close on stop failed (short) for ${pos.symbol}: ${msg} — re-opening long to restore hedge`);
|
|
702
746
|
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", stop: true } });
|
|
747
|
+
// Rollback: re-open long to restore hedge
|
|
748
|
+
try {
|
|
749
|
+
await longAdapter.marketOrder(pos.symbol, "buy", pos.size);
|
|
750
|
+
ctx.log(` [ARB] Rollback: re-opened long ${pos.size} ${pos.symbol} on ${pos.longExchange}`);
|
|
751
|
+
logExecution({ type: "multi_leg_rollback", exchange: pos.longExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "success", dryRun: false });
|
|
752
|
+
}
|
|
753
|
+
catch (rbErr) {
|
|
754
|
+
const rbMsg = rbErr instanceof Error ? rbErr.message : String(rbErr);
|
|
755
|
+
ctx.log(` [ARB] CRITICAL: Stop rollback failed for ${pos.symbol}: ${rbMsg}`);
|
|
756
|
+
logExecution({ type: "multi_leg_rollback", exchange: pos.longExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "failed", error: rbMsg, dryRun: false });
|
|
757
|
+
}
|
|
703
758
|
}
|
|
759
|
+
if (!closeShortOk) {
|
|
760
|
+
remainingPositions.push(pos);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
ctx.log(` [ARB] CRITICAL: ${pos.symbol} adapters not found on stop — requires manual cleanup`);
|
|
765
|
+
remainingPositions.push(pos);
|
|
704
766
|
}
|
|
705
767
|
}
|
|
706
768
|
}
|
|
707
|
-
ctx.state.set("arbOpenPositions",
|
|
708
|
-
ctx.state.set("arbPositions",
|
|
769
|
+
ctx.state.set("arbOpenPositions", remainingPositions);
|
|
770
|
+
ctx.state.set("arbPositions", remainingPositions.length);
|
|
709
771
|
}
|
|
710
772
|
// Cancel on extra adapters directly
|
|
711
773
|
if (extraAdapters) {
|
|
@@ -718,29 +780,34 @@ export class FundingArbStrategy {
|
|
|
718
780
|
}
|
|
719
781
|
return [{ type: "cancel_all" }];
|
|
720
782
|
}
|
|
721
|
-
/** Close a spot-perp position: sell spot, transfer USDC back, close perp short
|
|
783
|
+
/** Close a spot-perp position: sell spot, transfer USDC back, close perp short.
|
|
784
|
+
* Returns true only if ALL steps succeed. If spot sell succeeds but perp close fails,
|
|
785
|
+
* attempts to re-buy spot as rollback and returns false. */
|
|
722
786
|
async closeSpotPerp(pos, adapters, ctx) {
|
|
723
787
|
const exchangeName = pos.longExchange.replace("-spot", "");
|
|
724
788
|
const perpAdapter = adapters.get(pos.shortExchange) ?? adapters.get(exchangeName);
|
|
725
789
|
if (!perpAdapter) {
|
|
726
790
|
ctx.log(` [ARB] Close spot-perp failed: no adapter for ${pos.shortExchange}`);
|
|
727
|
-
return;
|
|
791
|
+
return false;
|
|
728
792
|
}
|
|
729
793
|
try {
|
|
730
794
|
const spotAdapter = await this.getSpotAdapter(exchangeName, perpAdapter);
|
|
731
795
|
if (!spotAdapter) {
|
|
732
796
|
ctx.log(` [ARB] Close spot-perp failed: spot adapter not available for ${exchangeName}`);
|
|
733
|
-
return;
|
|
797
|
+
return false;
|
|
734
798
|
}
|
|
735
|
-
ctx.log(` [ARB] Closing spot-perp ${pos.symbol}
|
|
799
|
+
ctx.log(` [ARB] Closing spot-perp ${pos.symbol} (spot sell + perp buy)`);
|
|
736
800
|
// Step 1: Sell spot
|
|
801
|
+
let spotSold = false;
|
|
737
802
|
try {
|
|
738
803
|
await spotAdapter.spotMarketOrder(pos.symbol, "sell", pos.size);
|
|
739
804
|
ctx.log(` [ARB] Sold spot ${pos.size} ${pos.symbol}`);
|
|
805
|
+
spotSold = true;
|
|
740
806
|
}
|
|
741
807
|
catch (err) {
|
|
742
808
|
const msg = err instanceof Error ? err.message : String(err);
|
|
743
809
|
ctx.log(` [ARB] Spot sell failed for ${pos.symbol}: ${msg}`);
|
|
810
|
+
return false;
|
|
744
811
|
}
|
|
745
812
|
// Step 2: Transfer USDC back to perp (use 95% of notional as estimate)
|
|
746
813
|
try {
|
|
@@ -755,21 +822,36 @@ export class FundingArbStrategy {
|
|
|
755
822
|
catch (err) {
|
|
756
823
|
const msg = err instanceof Error ? err.message : String(err);
|
|
757
824
|
ctx.log(` [ARB] USDC transfer back failed: ${msg}`);
|
|
825
|
+
// Non-fatal: continue to close perp short
|
|
758
826
|
}
|
|
759
827
|
// Step 3: Close perp short
|
|
828
|
+
const perpSymbol = this.getPerpSymbol(pos.symbol, pos.shortExchange);
|
|
760
829
|
try {
|
|
761
|
-
const perpSymbol = this.getPerpSymbol(pos.symbol, pos.shortExchange);
|
|
762
830
|
await perpAdapter.marketOrder(perpSymbol, "buy", pos.size, { reduceOnly: true });
|
|
763
831
|
ctx.log(` [ARB] Closed perp short ${pos.size} ${perpSymbol}`);
|
|
832
|
+
return true;
|
|
764
833
|
}
|
|
765
834
|
catch (err) {
|
|
766
835
|
const msg = err instanceof Error ? err.message : String(err);
|
|
767
836
|
ctx.log(` [ARB] Perp close failed for ${pos.symbol}: ${msg}`);
|
|
837
|
+
// Rollback: re-buy spot to restore hedge
|
|
838
|
+
if (spotSold) {
|
|
839
|
+
try {
|
|
840
|
+
await spotAdapter.spotMarketOrder(pos.symbol, "buy", pos.size);
|
|
841
|
+
ctx.log(` [ARB] Rollback: re-bought spot ${pos.size} ${pos.symbol} on ${exchangeName}`);
|
|
842
|
+
}
|
|
843
|
+
catch (rbErr) {
|
|
844
|
+
const rbMsg = rbErr instanceof Error ? rbErr.message : String(rbErr);
|
|
845
|
+
ctx.log(` [ARB] CRITICAL: Spot-perp close rollback failed for ${pos.symbol}: ${rbMsg}`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return false;
|
|
768
849
|
}
|
|
769
850
|
}
|
|
770
851
|
catch (err) {
|
|
771
852
|
const msg = err instanceof Error ? err.message : String(err);
|
|
772
853
|
ctx.log(` [ARB] Close spot-perp failed for ${pos.symbol}: ${msg}`);
|
|
854
|
+
return false;
|
|
773
855
|
}
|
|
774
856
|
}
|
|
775
857
|
/** Open a spot-perp position: transfer USDC to spot, buy spot, short perp */
|
|
@@ -798,7 +880,11 @@ export class FundingArbStrategy {
|
|
|
798
880
|
try {
|
|
799
881
|
await perpAdapter.setLeverage(this.getPerpSymbol(opp.symbol, opp.shortExchange), leverage, "cross");
|
|
800
882
|
}
|
|
801
|
-
catch {
|
|
883
|
+
catch (err) {
|
|
884
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
885
|
+
ctx.log(` [ARB] setLeverage failed on ${opp.shortExchange} for ${opp.symbol}: ${msg}`);
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
802
888
|
// Balance check: spot needs full notional, perp needs sizeUsd / leverage margin
|
|
803
889
|
let targetSizeUsd = params.size_usd;
|
|
804
890
|
try {
|
|
@@ -806,7 +892,7 @@ export class FundingArbStrategy {
|
|
|
806
892
|
// Check margin usage
|
|
807
893
|
const perpEquity = parseFloat(perpBal.equity);
|
|
808
894
|
const perpMarginPct = perpEquity > 0 ? parseFloat(perpBal.marginUsed) / perpEquity * 100 : 0;
|
|
809
|
-
if (perpMarginPct >
|
|
895
|
+
if (perpMarginPct > 90) {
|
|
810
896
|
ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: perp margin usage too high (${perpMarginPct.toFixed(0)}%)`);
|
|
811
897
|
return false;
|
|
812
898
|
}
|
|
@@ -825,7 +911,11 @@ export class FundingArbStrategy {
|
|
|
825
911
|
targetSizeUsd = Math.floor(maxNotional);
|
|
826
912
|
}
|
|
827
913
|
}
|
|
828
|
-
catch {
|
|
914
|
+
catch (err) {
|
|
915
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
916
|
+
ctx.log(` [ARB] Balance check failed for spot-perp ${opp.symbol}: ${msg}`);
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
829
919
|
const perpSymbol = this.getPerpSymbol(opp.symbol, opp.shortExchange);
|
|
830
920
|
const priceEstimate = await this.getPriceEstimate(perpAdapter, perpSymbol, opp.symbol);
|
|
831
921
|
if (priceEstimate <= 0) {
|
|
@@ -953,25 +1043,32 @@ export class FundingArbStrategy {
|
|
|
953
1043
|
* Returns null if the exchange doesn't support spot or instantiation fails.
|
|
954
1044
|
*/
|
|
955
1045
|
async getSpotAdapter(name, adapter) {
|
|
1046
|
+
const cached = this._spotAdapterCache.get(name);
|
|
1047
|
+
if (cached)
|
|
1048
|
+
return cached;
|
|
956
1049
|
try {
|
|
1050
|
+
let spot = null;
|
|
957
1051
|
if (name === "hyperliquid") {
|
|
958
1052
|
const { HyperliquidSpotAdapter } = await import("../../exchanges/hyperliquid-spot.js");
|
|
959
1053
|
const { HyperliquidAdapter } = await import("../../exchanges/hyperliquid.js");
|
|
960
1054
|
if (adapter instanceof HyperliquidAdapter) {
|
|
961
|
-
const
|
|
962
|
-
await
|
|
963
|
-
|
|
1055
|
+
const instance = new HyperliquidSpotAdapter(adapter);
|
|
1056
|
+
await instance.init();
|
|
1057
|
+
spot = instance;
|
|
964
1058
|
}
|
|
965
1059
|
}
|
|
966
1060
|
else if (name === "lighter") {
|
|
967
1061
|
const { LighterSpotAdapter } = await import("../../exchanges/lighter-spot.js");
|
|
968
1062
|
const { LighterAdapter } = await import("../../exchanges/lighter.js");
|
|
969
1063
|
if (adapter instanceof LighterAdapter) {
|
|
970
|
-
const
|
|
971
|
-
await
|
|
972
|
-
|
|
1064
|
+
const instance = new LighterSpotAdapter(adapter);
|
|
1065
|
+
await instance.init();
|
|
1066
|
+
spot = instance;
|
|
973
1067
|
}
|
|
974
1068
|
}
|
|
1069
|
+
if (spot)
|
|
1070
|
+
this._spotAdapterCache.set(name, spot);
|
|
1071
|
+
return spot;
|
|
975
1072
|
}
|
|
976
1073
|
catch { /* not supported */ }
|
|
977
1074
|
return null;
|
|
@@ -1011,12 +1108,22 @@ export class FundingArbStrategy {
|
|
|
1011
1108
|
return 0;
|
|
1012
1109
|
}
|
|
1013
1110
|
}
|
|
1014
|
-
async fetchRates(adapter,
|
|
1111
|
+
async fetchRates(adapter, exchangeName) {
|
|
1015
1112
|
try {
|
|
1016
1113
|
const markets = await adapter.getMarkets();
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1114
|
+
const withRates = markets.filter(m => m.fundingRate != null);
|
|
1115
|
+
// Bootstrap aster funding hours lazily (1 API call per unknown symbol, cached permanently)
|
|
1116
|
+
if (exchangeName === "aster" && "getFundingHours" in adapter) {
|
|
1117
|
+
const aster = adapter;
|
|
1118
|
+
// Only bootstrap symbols not yet cached (up to 5 per tick to limit API calls)
|
|
1119
|
+
const uncached = withRates.filter(m => m.fundingHours === undefined || m.fundingHours === 1);
|
|
1120
|
+
const toBootstrap = uncached.slice(0, 5);
|
|
1121
|
+
for (const m of toBootstrap) {
|
|
1122
|
+
const fh = await aster.getFundingHours(m.symbol);
|
|
1123
|
+
m.fundingHours = fh;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return withRates.map(m => ({
|
|
1020
1127
|
symbol: m.symbol,
|
|
1021
1128
|
rate: parseFloat(m.fundingRate),
|
|
1022
1129
|
price: parseFloat(m.markPrice),
|
|
@@ -14,6 +14,7 @@ export declare class AsterAdapter implements ExchangeAdapter {
|
|
|
14
14
|
private _testnet;
|
|
15
15
|
private _marketsCache;
|
|
16
16
|
private _marketsCacheTime;
|
|
17
|
+
private _fundingHoursCache;
|
|
17
18
|
private _accountCache;
|
|
18
19
|
private _positionsCache;
|
|
19
20
|
private _ordersCache;
|
|
@@ -26,6 +27,8 @@ export declare class AsterAdapter implements ExchangeAdapter {
|
|
|
26
27
|
/** Aster API symbol → CLI symbol (ETHUSDT → ETH) */
|
|
27
28
|
private _fromApi;
|
|
28
29
|
init(): Promise<void>;
|
|
30
|
+
/** Get funding interval for a symbol (lazy bootstrap — queries on first access, cached permanently). */
|
|
31
|
+
getFundingHours(symbol: string): Promise<number>;
|
|
29
32
|
getMarkets(): Promise<ExchangeMarketInfo[]>;
|
|
30
33
|
getOrderbook(symbol: string): Promise<{
|
|
31
34
|
bids: [string, string][];
|
package/dist/exchanges/aster.js
CHANGED
|
@@ -14,6 +14,7 @@ export class AsterAdapter {
|
|
|
14
14
|
_testnet;
|
|
15
15
|
_marketsCache = null;
|
|
16
16
|
_marketsCacheTime = 0;
|
|
17
|
+
_fundingHoursCache = new Map();
|
|
17
18
|
_accountCache = null;
|
|
18
19
|
_positionsCache = null;
|
|
19
20
|
_ordersCache = null;
|
|
@@ -46,6 +47,26 @@ export class AsterAdapter {
|
|
|
46
47
|
await this._publicGet("/fapi/v1/time");
|
|
47
48
|
}
|
|
48
49
|
// ── Market Data ──
|
|
50
|
+
/** Get funding interval for a symbol (lazy bootstrap — queries on first access, cached permanently). */
|
|
51
|
+
async getFundingHours(symbol) {
|
|
52
|
+
const key = symbol.toUpperCase();
|
|
53
|
+
const cached = this._fundingHoursCache.get(key);
|
|
54
|
+
if (cached !== undefined)
|
|
55
|
+
return cached;
|
|
56
|
+
try {
|
|
57
|
+
const apiSym = this._toApi(key);
|
|
58
|
+
const data = await this._publicGet("/fapi/v1/fundingRate", { symbol: apiSym, limit: "2" });
|
|
59
|
+
if (Array.isArray(data) && data.length >= 2) {
|
|
60
|
+
const hours = Math.abs(data[1].fundingTime - data[0].fundingTime) / 3600000;
|
|
61
|
+
const rounded = hours <= 1.5 ? 1 : hours <= 5 ? 4 : 8;
|
|
62
|
+
this._fundingHoursCache.set(key, rounded);
|
|
63
|
+
return rounded;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch { /* non-critical */ }
|
|
67
|
+
this._fundingHoursCache.set(key, 1); // default
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
49
70
|
async getMarkets() {
|
|
50
71
|
// Return cached result if fresh
|
|
51
72
|
if (this._marketsCache && Date.now() - this._marketsCacheTime < AsterAdapter.CACHE_TTL) {
|
|
@@ -77,12 +98,9 @@ export class AsterAdapter {
|
|
|
77
98
|
const maxLev = Number(s.maxLeverage ?? 50);
|
|
78
99
|
const lotFilter = s.filters
|
|
79
100
|
?.find(f => f.filterType === "LOT_SIZE");
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const fundingHours =
|
|
83
|
-
: hoursFromMidnight % 8 === 0 ? 8
|
|
84
|
-
: hoursFromMidnight % 4 === 0 ? 4
|
|
85
|
-
: 1;
|
|
101
|
+
// Use cached funding interval if available (populated by loadFundingIntervals()),
|
|
102
|
+
// otherwise default to 1h.
|
|
103
|
+
const fundingHours = this._fundingHoursCache.get(this._fromApi(sym)) ?? 1;
|
|
86
104
|
return {
|
|
87
105
|
symbol: this._fromApi(sym),
|
|
88
106
|
markPrice: String(premium?.markPrice ?? ticker?.lastPrice ?? "0"),
|