perp-cli 0.9.8 → 0.11.0

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.
@@ -2,6 +2,9 @@ import { getMarketSnapshot, evaluateAllConditions } from "./conditions.js";
2
2
  import { updateJobState } from "../jobs.js";
3
3
  import { getStrategy } from "./strategy-registry.js";
4
4
  import chalk from "chalk";
5
+ import { appendFileSync, mkdirSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
5
8
  // Auto-register built-in strategies
6
9
  import "./strategies/grid-strategy.js";
7
10
  import "./strategies/dca-strategy.js";
@@ -87,14 +90,30 @@ mode = "headless") {
87
90
  const tuiEmitter = new TuiEmitter();
88
91
  let tuiUnmount;
89
92
  let paused = false;
90
- // Wrap log for TUI/JSON modes
93
+ // Log file: always write to ~/.perp/logs/<bot-name>.log
94
+ const logDir = join(homedir(), ".perp", "logs");
95
+ try {
96
+ mkdirSync(logDir, { recursive: true });
97
+ }
98
+ catch { /* ignore */ }
99
+ const logFile = join(logDir, `${config.name}.log`);
100
+ const writeLog = (msg) => {
101
+ const ts = new Date().toISOString();
102
+ const clean = msg.replace(/\x1b\[[0-9;]*m/g, ""); // strip ANSI colors
103
+ try {
104
+ appendFileSync(logFile, `${ts} ${clean}\n`);
105
+ }
106
+ catch { /* ignore */ }
107
+ };
108
+ // Wrap log for TUI/JSON modes + always write to file
91
109
  let effectiveLog = mode === "json"
92
- ? () => { } // JSON mode suppresses console log; NDJSON emitted per tick
110
+ ? (msg) => { writeLog(msg); } // JSON mode suppresses console; still log to file
93
111
  : mode === "tui"
94
- ? (msg) => tuiEmitter.emitLog(msg)
95
- : log;
112
+ ? (msg) => { tuiEmitter.emitLog(msg); writeLog(msg); }
113
+ : (msg) => { log(msg); writeLog(msg); };
96
114
  // Update strategy context to use effective log
97
115
  strategyCtx.log = effectiveLog;
116
+ effectiveLog(` Log file: ${logFile}`);
98
117
  if (mode === "tui") {
99
118
  try {
100
119
  const { startDashboard } = await import("./tui/index.js");
@@ -211,7 +211,7 @@ const arbConservative = {
211
211
  buildConfig: (exchange, _symbol) => ({
212
212
  name: `arb-conservative-${Date.now().toString(36)}`,
213
213
  exchange,
214
- symbol: "ETH",
214
+ symbol: "ALL",
215
215
  strategy: {
216
216
  type: "funding-arb",
217
217
  min_spread: 30,
@@ -234,7 +234,7 @@ const arbStandard = {
234
234
  buildConfig: (exchange, _symbol) => ({
235
235
  name: `arb-standard-${Date.now().toString(36)}`,
236
236
  exchange,
237
- symbol: "ETH",
237
+ symbol: "ALL",
238
238
  strategy: {
239
239
  type: "funding-arb",
240
240
  min_spread: 20,
@@ -257,7 +257,7 @@ const arbAggressive = {
257
257
  buildConfig: (exchange, _symbol) => ({
258
258
  name: `arb-aggressive-${Date.now().toString(36)}`,
259
259
  exchange,
260
- symbol: "ETH",
260
+ symbol: "ALL",
261
261
  strategy: {
262
262
  type: "funding-arb",
263
263
  min_spread: 10,
@@ -26,7 +26,6 @@ export declare class FundingArbStrategy implements Strategy {
26
26
  private get params();
27
27
  private _config;
28
28
  private _failCooldown;
29
- private _spotAdapterCache;
30
29
  init(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<void>;
31
30
  onTick(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
32
31
  onStop(ctx: StrategyContext): Promise<StrategyAction[]>;
@@ -36,26 +35,4 @@ export declare class FundingArbStrategy implements Strategy {
36
35
  private closeSpotPerp;
37
36
  /** Open a spot-perp position: transfer USDC to spot, buy spot, short perp */
38
37
  private openSpotPerp;
39
- /** Transfer USDC from perp to spot account (exchange-specific) */
40
- private transferUsdcToSpot;
41
- /** Transfer USDC from spot to perp account (exchange-specific) */
42
- private transferUsdcToPerp;
43
- /**
44
- * Get spot adapter for a given exchange name + perp adapter instance.
45
- * Returns null if the exchange doesn't support spot or instantiation fails.
46
- */
47
- private getSpotAdapter;
48
- /**
49
- * Resolve the perp symbol for a given base symbol on an exchange.
50
- * HL getPositions() returns symbols like "PURR" or "PURR-PERP" — we normalize here.
51
- */
52
- private getPerpSymbol;
53
- /** Look up rate from ratesByExchange, trying symbol, symbol-PERP, and symbol without -PERP */
54
- private findRate;
55
- /**
56
- * Get a price estimate for a symbol from the perp adapter.
57
- * Tries the base symbol first, then with -PERP suffix.
58
- */
59
- private getPriceEstimate;
60
- private fetchRates;
61
38
  }
@@ -3,6 +3,8 @@ import { checkArbLiquidity, checkSpotPerpLiquidity } from "../../liquidity.js";
3
3
  import { computeMatchedSize, computeSpotPerpMatchedSize, reconcileArbFills } from "../../arb-sizing.js";
4
4
  import { getFundingHours } from "../../funding.js";
5
5
  import { logExecution, pruneExecutionLog } from "../../execution-log.js";
6
+ import { annualizeHourlyRate } from "../../funding/normalize.js";
7
+ import { isPositionGone, transferUsdcToSpot, transferUsdcToPerp, getSpotAdapter, getPerpSymbol, findRate, getPriceEstimate, fetchRates, } from "./funding-arb-utils.js";
6
8
  export class FundingArbStrategy {
7
9
  name = "funding-arb";
8
10
  describe() {
@@ -25,7 +27,6 @@ export class FundingArbStrategy {
25
27
  }
26
28
  _config = {};
27
29
  _failCooldown = new Map(); // "symbol:action" → timestamp of next retry
28
- _spotAdapterCache = new Map();
29
30
  async init(ctx, _snapshot) {
30
31
  // Prune old execution logs (keep 30 days)
31
32
  try {
@@ -241,7 +242,7 @@ export class FundingArbStrategy {
241
242
  // Fetch rates — all exchanges if room for new positions, only position exchanges if full
242
243
  const ratesByExchange = new Map();
243
244
  for (const [name, a] of exchangesToScan) {
244
- const rates = await this.fetchRates(a, name);
245
+ const rates = await fetchRates(a, name);
245
246
  const map = new Map();
246
247
  for (const r of rates)
247
248
  map.set(r.symbol.toUpperCase(), { rate: r.rate, price: r.price, sizeDecimals: r.sizeDecimals, maxLeverage: r.maxLeverage, fundingHours: r.fundingHours });
@@ -277,20 +278,20 @@ export class FundingArbStrategy {
277
278
  if (p.mode === "spot-perp") {
278
279
  // For spot-perp: long side is spot (rate=0), short side is perp
279
280
  const perpExchange = p.shortExchange;
280
- const perpSymbol = this.getPerpSymbol(p.symbol, perpExchange);
281
- const rateInfo = this.findRate(ratesByExchange, perpExchange, perpSymbol)
282
- ?? this.findRate(ratesByExchange, perpExchange, p.symbol);
281
+ const perpSymbol = getPerpSymbol(p.symbol, perpExchange);
282
+ const rateInfo = findRate(ratesByExchange, perpExchange, perpSymbol)
283
+ ?? findRate(ratesByExchange, perpExchange, p.symbol);
283
284
  const shortRate = rateInfo?.rate;
284
285
  const fundingH = rateInfo?.fundingHours ?? getFundingHours(perpExchange);
285
- signedSpread = shortRate !== undefined ? (shortRate / fundingH) * 8760 * 100 : 0;
286
+ signedSpread = shortRate !== undefined ? annualizeHourlyRate(shortRate / fundingH) : 0;
286
287
  }
287
288
  else {
288
- const longInfo = this.findRate(ratesByExchange, p.longExchange, p.symbol);
289
- const shortInfo = this.findRate(ratesByExchange, p.shortExchange, p.symbol);
289
+ const longInfo = findRate(ratesByExchange, p.longExchange, p.symbol);
290
+ const shortInfo = findRate(ratesByExchange, p.shortExchange, p.symbol);
290
291
  const longHourly = longInfo ? longInfo.rate / (longInfo.fundingHours ?? getFundingHours(p.longExchange)) : undefined;
291
292
  const shortHourly = shortInfo ? shortInfo.rate / (shortInfo.fundingHours ?? getFundingHours(p.shortExchange)) : undefined;
292
293
  signedSpread = (longHourly !== undefined && shortHourly !== undefined)
293
- ? (shortHourly - longHourly) * 8760 * 100
294
+ ? annualizeHourlyRate(shortHourly - longHourly)
294
295
  : 0;
295
296
  }
296
297
  const sign = signedSpread >= 0 ? "+" : "";
@@ -304,22 +305,22 @@ export class FundingArbStrategy {
304
305
  let signedSpread;
305
306
  if (pos.mode === "spot-perp") {
306
307
  const perpExchange = pos.shortExchange;
307
- const perpSymbol = this.getPerpSymbol(pos.symbol, perpExchange);
308
- const rateInfo = this.findRate(ratesByExchange, perpExchange, perpSymbol)
309
- ?? this.findRate(ratesByExchange, perpExchange, pos.symbol);
308
+ const perpSymbol = getPerpSymbol(pos.symbol, perpExchange);
309
+ const rateInfo = findRate(ratesByExchange, perpExchange, perpSymbol)
310
+ ?? findRate(ratesByExchange, perpExchange, pos.symbol);
310
311
  if (rateInfo === undefined)
311
312
  continue;
312
313
  const fundingH = rateInfo.fundingHours ?? getFundingHours(perpExchange);
313
- signedSpread = (rateInfo.rate / fundingH) * 8760 * 100;
314
+ signedSpread = annualizeHourlyRate(rateInfo.rate / fundingH);
314
315
  }
315
316
  else {
316
- const longInfo = this.findRate(ratesByExchange, pos.longExchange, pos.symbol);
317
- const shortInfo = this.findRate(ratesByExchange, pos.shortExchange, pos.symbol);
317
+ const longInfo = findRate(ratesByExchange, pos.longExchange, pos.symbol);
318
+ const shortInfo = findRate(ratesByExchange, pos.shortExchange, pos.symbol);
318
319
  if (longInfo === undefined || shortInfo === undefined)
319
320
  continue;
320
321
  const longHourly = longInfo.rate / (longInfo.fundingHours ?? getFundingHours(pos.longExchange));
321
322
  const shortHourly = shortInfo.rate / (shortInfo.fundingHours ?? getFundingHours(pos.shortExchange));
322
- signedSpread = (shortHourly - longHourly) * 8760 * 100;
323
+ signedSpread = annualizeHourlyRate(shortHourly - longHourly);
323
324
  }
324
325
  // Close if funding flipped (losing money) or spread below close threshold
325
326
  const minHoldMs = (params.min_hold_hours ?? 2) * 60 * 60 * 1000;
@@ -343,7 +344,6 @@ export class FundingArbStrategy {
343
344
  const longAdapter = adapters.get(pos.longExchange);
344
345
  const shortAdapter = adapters.get(pos.shortExchange);
345
346
  if (longAdapter && shortAdapter) {
346
- const isPositionGone = (msg) => msg.includes("ReduceOnly") || msg.includes("reduceOnly") || msg.includes("-2022");
347
347
  let closeLongOk = false;
348
348
  let longPositionGone = false;
349
349
  try {
@@ -439,7 +439,7 @@ export class FundingArbStrategy {
439
439
  let minExchange = "", maxExchange = "";
440
440
  let minRate = 0, maxRate = 0;
441
441
  for (const exName of scanExchangeNames) {
442
- const rateInfo = this.findRate(ratesByExchange, exName, sym);
442
+ const rateInfo = findRate(ratesByExchange, exName, sym);
443
443
  if (!rateInfo)
444
444
  continue;
445
445
  const hourlyRate = rateInfo.rate / (rateInfo.fundingHours ?? getFundingHours(exName));
@@ -455,7 +455,7 @@ export class FundingArbStrategy {
455
455
  }
456
456
  }
457
457
  if (minExchange && maxExchange && minExchange !== maxExchange) {
458
- const spread = (maxHourly - minHourly) * 8760 * 100;
458
+ const spread = annualizeHourlyRate(maxHourly - minHourly);
459
459
  if (spread >= params.min_spread) {
460
460
  opportunities.push({
461
461
  symbol: sym,
@@ -473,7 +473,7 @@ export class FundingArbStrategy {
473
473
  const spotPerpMinSpread = params.spot_perp_min_spread ?? params.min_spread;
474
474
  for (const [name, a] of adapters) {
475
475
  try {
476
- const spotAdapter = await this.getSpotAdapter(name, a);
476
+ const spotAdapter = await getSpotAdapter(name, a);
477
477
  if (!spotAdapter)
478
478
  continue;
479
479
  const spotMarkets = await spotAdapter.getSpotMarkets();
@@ -490,7 +490,7 @@ export class FundingArbStrategy {
490
490
  if (!spotSymbols.has(base))
491
491
  continue;
492
492
  const hourlyRate = rate / (rateEntry.fundingHours ?? getFundingHours(name));
493
- const annualSpread = hourlyRate * 8760 * 100;
493
+ const annualSpread = annualizeHourlyRate(hourlyRate);
494
494
  if (annualSpread >= spotPerpMinSpread) {
495
495
  opportunities.push({
496
496
  symbol: base,
@@ -545,13 +545,13 @@ export class FundingArbStrategy {
545
545
  const shortAdapter = adapters.get(best.shortExchange);
546
546
  if (!longAdapter || !shortAdapter)
547
547
  continue;
548
- const price = this.findRate(ratesByExchange, best.longExchange, best.symbol)?.price ?? 0;
548
+ const price = findRate(ratesByExchange, best.longExchange, best.symbol)?.price ?? 0;
549
549
  if (price <= 0)
550
550
  continue;
551
551
  // Determine leverage for this symbol
552
552
  const configLeverage = params.leverage ?? 3;
553
- const longMaxLev = this.findRate(ratesByExchange, best.longExchange, best.symbol)?.maxLeverage ?? 1;
554
- const shortMaxLev = this.findRate(ratesByExchange, best.shortExchange, best.symbol)?.maxLeverage ?? 1;
553
+ const longMaxLev = findRate(ratesByExchange, best.longExchange, best.symbol)?.maxLeverage ?? 1;
554
+ const shortMaxLev = findRate(ratesByExchange, best.shortExchange, best.symbol)?.maxLeverage ?? 1;
555
555
  const leverage = Math.min(configLeverage, longMaxLev, shortMaxLev);
556
556
  // Set leverage on both exchanges before ordering
557
557
  try {
@@ -595,8 +595,8 @@ export class FundingArbStrategy {
595
595
  if (!liq.viable)
596
596
  continue;
597
597
  // Compute matched size (use actual precision from exchange market data)
598
- const longDec = this.findRate(ratesByExchange, best.longExchange, best.symbol)?.sizeDecimals;
599
- const shortDec = this.findRate(ratesByExchange, best.shortExchange, best.symbol)?.sizeDecimals;
598
+ const longDec = findRate(ratesByExchange, best.longExchange, best.symbol)?.sizeDecimals;
599
+ const shortDec = findRate(ratesByExchange, best.shortExchange, best.symbol)?.sizeDecimals;
600
600
  let matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange, { longSizeDecimals: longDec, shortSizeDecimals: shortDec });
601
601
  if (!matched) {
602
602
  ctx.log(` [ARB] Skip ${best.symbol}: can't compute matched size (min notional or precision issue)`);
@@ -818,7 +818,7 @@ export class FundingArbStrategy {
818
818
  return false;
819
819
  }
820
820
  try {
821
- const spotAdapter = await this.getSpotAdapter(exchangeName, perpAdapter);
821
+ const spotAdapter = await getSpotAdapter(exchangeName, perpAdapter);
822
822
  if (!spotAdapter) {
823
823
  ctx.log(` [ARB] Close spot-perp failed: spot adapter not available for ${exchangeName}`);
824
824
  return false;
@@ -838,11 +838,11 @@ export class FundingArbStrategy {
838
838
  }
839
839
  // Step 2: Transfer USDC back to perp (use 95% of notional as estimate)
840
840
  try {
841
- const perpSymbol = this.getPerpSymbol(pos.symbol, pos.shortExchange);
842
- const priceEstimate = await this.getPriceEstimate(perpAdapter, perpSymbol, pos.symbol);
841
+ const perpSymbol = getPerpSymbol(pos.symbol, pos.shortExchange);
842
+ const priceEstimate = await getPriceEstimate(perpAdapter, perpSymbol, pos.symbol);
843
843
  const proceeds = priceEstimate * parseFloat(pos.size) * 0.95;
844
844
  if (proceeds > 1) {
845
- await this.transferUsdcToPerp(spotAdapter, exchangeName, Math.floor(proceeds));
845
+ await transferUsdcToPerp(spotAdapter, exchangeName, Math.floor(proceeds));
846
846
  ctx.log(` [ARB] Transferred ~$${Math.floor(proceeds)} USDC back to perp`);
847
847
  }
848
848
  }
@@ -852,7 +852,7 @@ export class FundingArbStrategy {
852
852
  // Non-fatal: continue to close perp short
853
853
  }
854
854
  // Step 3: Close perp short
855
- const perpSymbol = this.getPerpSymbol(pos.symbol, pos.shortExchange);
855
+ const perpSymbol = getPerpSymbol(pos.symbol, pos.shortExchange);
856
856
  try {
857
857
  await perpAdapter.marketOrder(perpSymbol, "buy", pos.size, { reduceOnly: true });
858
858
  ctx.log(` [ARB] Closed perp short ${pos.size} ${perpSymbol}`);
@@ -891,21 +891,21 @@ export class FundingArbStrategy {
891
891
  return false;
892
892
  }
893
893
  try {
894
- const spotAdapter = await this.getSpotAdapter(exchangeName, perpAdapter);
894
+ const spotAdapter = await getSpotAdapter(exchangeName, perpAdapter);
895
895
  if (!spotAdapter) {
896
896
  ctx.log(` [ARB] Open spot-perp failed: spot adapter not available for ${exchangeName}`);
897
897
  return false;
898
898
  }
899
899
  // Determine leverage for perp leg
900
900
  const configLeverage = params.leverage ?? 3;
901
- const perpSymbolUpper = this.getPerpSymbol(opp.symbol, opp.shortExchange).toUpperCase();
902
- const perpMaxLev = this.findRate(ratesByExchange, opp.shortExchange, perpSymbolUpper)?.maxLeverage
903
- ?? this.findRate(ratesByExchange, opp.shortExchange, opp.symbol.toUpperCase())?.maxLeverage
901
+ const perpSymbolUpper = getPerpSymbol(opp.symbol, opp.shortExchange).toUpperCase();
902
+ const perpMaxLev = findRate(ratesByExchange, opp.shortExchange, perpSymbolUpper)?.maxLeverage
903
+ ?? findRate(ratesByExchange, opp.shortExchange, opp.symbol.toUpperCase())?.maxLeverage
904
904
  ?? 1;
905
905
  const leverage = Math.min(configLeverage, perpMaxLev);
906
906
  // Set leverage on perp before ordering
907
907
  try {
908
- await perpAdapter.setLeverage(this.getPerpSymbol(opp.symbol, opp.shortExchange), leverage, "cross");
908
+ await perpAdapter.setLeverage(getPerpSymbol(opp.symbol, opp.shortExchange), leverage, "cross");
909
909
  }
910
910
  catch (err) {
911
911
  const msg = err instanceof Error ? err.message : String(err);
@@ -943,8 +943,8 @@ export class FundingArbStrategy {
943
943
  ctx.log(` [ARB] Balance check failed for spot-perp ${opp.symbol}: ${msg}`);
944
944
  return false;
945
945
  }
946
- const perpSymbol = this.getPerpSymbol(opp.symbol, opp.shortExchange);
947
- const priceEstimate = await this.getPriceEstimate(perpAdapter, perpSymbol, opp.symbol);
946
+ const perpSymbol = getPerpSymbol(opp.symbol, opp.shortExchange);
947
+ const priceEstimate = await getPriceEstimate(perpAdapter, perpSymbol, opp.symbol);
948
948
  if (priceEstimate <= 0) {
949
949
  ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: can't determine price`);
950
950
  return false;
@@ -965,7 +965,7 @@ export class FundingArbStrategy {
965
965
  ctx.log(` [ARB] Opening spot-perp: ${matched.size} ${opp.symbol} ($${matched.notional.toFixed(0)}, slippage ~${liq.spotSlippage.toFixed(2)}%/${liq.perpSlippage.toFixed(2)}%)`);
966
966
  // Step 1: Transfer USDC to spot
967
967
  const transferAmt = Math.ceil(matched.notional * 1.02); // slight buffer for price movement
968
- await this.transferUsdcToSpot(spotAdapter, exchangeName, transferAmt);
968
+ await transferUsdcToSpot(spotAdapter, exchangeName, transferAmt);
969
969
  ctx.log(` [ARB] Transferred $${transferAmt} USDC to spot`);
970
970
  // Step 2: Buy spot
971
971
  let spotResult;
@@ -979,7 +979,7 @@ export class FundingArbStrategy {
979
979
  ctx.log(` [ARB] Open spot-perp failed (spot buy) for ${opp.symbol}: ${msg}`);
980
980
  logExecution({ type: "arb_entry", exchange: opp.longExchange, symbol: opp.symbol, side: "buy", size: matched.size, status: "failed", error: msg, dryRun: false, meta: { mode: "spot-perp", leg: "spot" } });
981
981
  try {
982
- await this.transferUsdcToPerp(spotAdapter, exchangeName, transferAmt);
982
+ await transferUsdcToPerp(spotAdapter, exchangeName, transferAmt);
983
983
  ctx.log(` [ARB] Transferred $${transferAmt} USDC back to perp after spot buy failure`);
984
984
  }
985
985
  catch { /* best effort */ }
@@ -1002,7 +1002,7 @@ export class FundingArbStrategy {
1002
1002
  logExecution({ type: "multi_leg_rollback", exchange: opp.longExchange, symbol: opp.symbol, side: "sell", size: matched.size, status: "success", dryRun: false });
1003
1003
  // Transfer USDC back to perp account
1004
1004
  try {
1005
- await this.transferUsdcToPerp(spotAdapter, opp.longExchange.replace("-spot", ""), Math.ceil(matched.notional * 1.02));
1005
+ await transferUsdcToPerp(spotAdapter, opp.longExchange.replace("-spot", ""), Math.ceil(matched.notional * 1.02));
1006
1006
  }
1007
1007
  catch { /* best effort */ }
1008
1008
  }
@@ -1036,140 +1036,5 @@ export class FundingArbStrategy {
1036
1036
  return false;
1037
1037
  }
1038
1038
  }
1039
- /** Transfer USDC from perp to spot account (exchange-specific) */
1040
- async transferUsdcToSpot(spotAdapter, exchangeName, amount) {
1041
- if (exchangeName === "hyperliquid") {
1042
- const { HyperliquidSpotAdapter } = await import("../../exchanges/hyperliquid-spot.js");
1043
- if (spotAdapter instanceof HyperliquidSpotAdapter) {
1044
- await spotAdapter.transferUsdcToSpot(amount);
1045
- return;
1046
- }
1047
- }
1048
- else if (exchangeName === "lighter") {
1049
- const { LighterSpotAdapter } = await import("../../exchanges/lighter-spot.js");
1050
- if (spotAdapter instanceof LighterSpotAdapter) {
1051
- await spotAdapter.transferUsdcToSpot(amount);
1052
- return;
1053
- }
1054
- }
1055
- }
1056
- /** Transfer USDC from spot to perp account (exchange-specific) */
1057
- async transferUsdcToPerp(spotAdapter, exchangeName, amount) {
1058
- if (exchangeName === "hyperliquid") {
1059
- const { HyperliquidSpotAdapter } = await import("../../exchanges/hyperliquid-spot.js");
1060
- if (spotAdapter instanceof HyperliquidSpotAdapter) {
1061
- await spotAdapter.transferUsdcToPerp(amount);
1062
- return;
1063
- }
1064
- }
1065
- else if (exchangeName === "lighter") {
1066
- const { LighterSpotAdapter } = await import("../../exchanges/lighter-spot.js");
1067
- if (spotAdapter instanceof LighterSpotAdapter) {
1068
- await spotAdapter.transferUsdcToPerp(amount);
1069
- return;
1070
- }
1071
- }
1072
- }
1073
- /**
1074
- * Get spot adapter for a given exchange name + perp adapter instance.
1075
- * Returns null if the exchange doesn't support spot or instantiation fails.
1076
- */
1077
- async getSpotAdapter(name, adapter) {
1078
- const cached = this._spotAdapterCache.get(name);
1079
- if (cached)
1080
- return cached;
1081
- try {
1082
- let spot = null;
1083
- if (name === "hyperliquid") {
1084
- const { HyperliquidSpotAdapter } = await import("../../exchanges/hyperliquid-spot.js");
1085
- const { HyperliquidAdapter } = await import("../../exchanges/hyperliquid.js");
1086
- if (adapter instanceof HyperliquidAdapter) {
1087
- const instance = new HyperliquidSpotAdapter(adapter);
1088
- await instance.init();
1089
- spot = instance;
1090
- }
1091
- }
1092
- else if (name === "lighter") {
1093
- const { LighterSpotAdapter } = await import("../../exchanges/lighter-spot.js");
1094
- const { LighterAdapter } = await import("../../exchanges/lighter.js");
1095
- if (adapter instanceof LighterAdapter) {
1096
- const instance = new LighterSpotAdapter(adapter);
1097
- await instance.init();
1098
- spot = instance;
1099
- }
1100
- }
1101
- if (spot)
1102
- this._spotAdapterCache.set(name, spot);
1103
- return spot;
1104
- }
1105
- catch { /* not supported */ }
1106
- return null;
1107
- }
1108
- /**
1109
- * Resolve the perp symbol for a given base symbol on an exchange.
1110
- * HL getPositions() returns symbols like "PURR" or "PURR-PERP" — we normalize here.
1111
- */
1112
- getPerpSymbol(baseSymbol, exchangeName) {
1113
- const base = baseSymbol.replace(/-PERP$/, "").toUpperCase();
1114
- void exchangeName;
1115
- return base;
1116
- }
1117
- /** Look up rate from ratesByExchange, trying symbol, symbol-PERP, and symbol without -PERP */
1118
- findRate(ratesByExchange, exchange, symbol) {
1119
- const map = ratesByExchange.get(exchange);
1120
- if (!map)
1121
- return undefined;
1122
- const upper = symbol.toUpperCase();
1123
- return map.get(upper)
1124
- ?? map.get(upper + "-PERP")
1125
- ?? map.get(upper.replace(/-PERP$/, ""));
1126
- }
1127
- /**
1128
- * Get a price estimate for a symbol from the perp adapter.
1129
- * Tries the base symbol first, then with -PERP suffix.
1130
- */
1131
- async getPriceEstimate(perpAdapter, perpSymbol, fallbackSymbol) {
1132
- try {
1133
- const markets = await perpAdapter.getMarkets();
1134
- const market = markets.find(m => m.symbol.toUpperCase() === perpSymbol.toUpperCase() ||
1135
- m.symbol.toUpperCase() === fallbackSymbol.toUpperCase() ||
1136
- m.symbol.toUpperCase() === `${fallbackSymbol.toUpperCase()}-PERP`);
1137
- return market ? parseFloat(market.markPrice) : 0;
1138
- }
1139
- catch {
1140
- return 0;
1141
- }
1142
- }
1143
- async fetchRates(adapter, exchangeName) {
1144
- try {
1145
- const markets = await adapter.getMarkets();
1146
- const withRates = markets.filter(m => m.fundingRate != null);
1147
- // Bootstrap aster funding hours lazily (1 API call per unknown symbol, cached permanently)
1148
- if (exchangeName === "aster" && "getFundingHours" in adapter) {
1149
- const aster = adapter;
1150
- // Only bootstrap symbols not yet cached (up to 20 per tick to complete faster)
1151
- const uncached = withRates.filter(m => {
1152
- const cached = adapter?._fundingHoursCache?.get?.(m.symbol);
1153
- return cached === undefined; // only truly uncached, not symbols confirmed as 1h
1154
- });
1155
- const toBootstrap = uncached.slice(0, 20);
1156
- for (const m of toBootstrap) {
1157
- const fh = await aster.getFundingHours(m.symbol);
1158
- m.fundingHours = fh;
1159
- }
1160
- }
1161
- return withRates.map(m => ({
1162
- symbol: m.symbol,
1163
- rate: parseFloat(m.fundingRate),
1164
- price: parseFloat(m.markPrice),
1165
- sizeDecimals: m.sizeDecimals,
1166
- maxLeverage: m.maxLeverage,
1167
- fundingHours: m.fundingHours,
1168
- }));
1169
- }
1170
- catch {
1171
- return [];
1172
- }
1173
- }
1174
1039
  }
1175
1040
  registerStrategy("funding-arb", (_config) => new FundingArbStrategy());
@@ -1,5 +1,5 @@
1
1
  import { registerStrategy } from "../strategy-registry.js";
2
- import { toHourlyRate, computeAnnualSpread } from "../../funding/normalize.js";
2
+ import { toHourlyRate, computeAnnualSpread, annualizeHourlyRate } from "../../funding/normalize.js";
3
3
  /**
4
4
  * Minimum notional per exchange (USD):
5
5
  * - HL: ~$2 (0.001 ETH)
@@ -203,7 +203,7 @@ class FundingAutoStrategy {
203
203
  continue;
204
204
  }
205
205
  const hourlyRate = toHourlyRate(shortInfo.rate, pos.shortExchange);
206
- const annualized = Math.abs(hourlyRate) * 8760 * 100;
206
+ const annualized = annualizeHourlyRate(Math.abs(hourlyRate));
207
207
  const pnl = await this.calculateRealPnl(adapters.get(pos.shortExchange), pos.symbol);
208
208
  pos.accumulatedFunding = pnl.funding;
209
209
  pos.totalCost = pnl.fees;
@@ -165,7 +165,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
165
165
  const config = {
166
166
  name: `arb-funding-${Date.now().toString(36)}`,
167
167
  exchange: primaryAdapter.name,
168
- symbol: "ETH", // multi-symbol scanning
168
+ symbol: "ALL", // multi-symbol scanning
169
169
  strategy: {
170
170
  type: "funding-arb",
171
171
  min_spread: parseFloat(opts.minSpread),
@@ -243,7 +243,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
243
243
  const config = {
244
244
  name: `delta-neutral-${Date.now().toString(36)}`,
245
245
  exchange: primaryAdapter.name,
246
- symbol: "ETH",
246
+ symbol: "ALL",
247
247
  strategy: {
248
248
  type: "spot-perp-arb",
249
249
  exchanges: exchangeNames,
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerOwsCommands(program: Command, isJson: () => boolean): void;