perp-cli 0.6.3 → 0.7.1

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.
Files changed (177) hide show
  1. package/README.md +7 -2
  2. package/dist/api/public/hyperliquid.d.ts +2 -2
  3. package/dist/api/public/hyperliquid.js +0 -10
  4. package/dist/api/public/lighter.d.ts +2 -1
  5. package/dist/api/public/pacifica.d.ts +2 -1
  6. package/dist/arb/index.d.ts +7 -4
  7. package/dist/arb/index.js +5 -4
  8. package/dist/arb/sizing.d.ts +1 -1
  9. package/dist/arb/state.d.ts +1 -1
  10. package/dist/arb/state.js +4 -3
  11. package/dist/arb/utils.d.ts +0 -10
  12. package/dist/arb/utils.js +0 -42
  13. package/dist/bot/apex/guard.d.ts +26 -0
  14. package/dist/bot/apex/guard.js +86 -0
  15. package/dist/bot/apex/index.d.ts +6 -0
  16. package/dist/bot/apex/index.js +4 -0
  17. package/dist/bot/apex/pulse.d.ts +22 -0
  18. package/dist/bot/apex/pulse.js +107 -0
  19. package/dist/bot/apex/radar.d.ts +17 -0
  20. package/dist/bot/apex/radar.js +150 -0
  21. package/dist/bot/conditions.d.ts +1 -1
  22. package/dist/bot/config.d.ts +5 -1
  23. package/dist/bot/config.js +1 -1
  24. package/dist/bot/engine.d.ts +23 -2
  25. package/dist/bot/engine.js +296 -351
  26. package/dist/bot/index.d.ts +16 -0
  27. package/dist/bot/index.js +10 -0
  28. package/dist/bot/indicators.d.ts +29 -0
  29. package/dist/bot/indicators.js +144 -0
  30. package/dist/bot/reflect.d.ts +34 -0
  31. package/dist/bot/reflect.js +155 -0
  32. package/dist/bot/strategies/aggressive-taker.d.ts +36 -0
  33. package/dist/bot/strategies/aggressive-taker.js +125 -0
  34. package/dist/bot/strategies/apex-strategy.d.ts +36 -0
  35. package/dist/bot/strategies/apex-strategy.js +185 -0
  36. package/dist/bot/strategies/avellaneda-mm.d.ts +27 -0
  37. package/dist/bot/strategies/avellaneda-mm.js +111 -0
  38. package/dist/bot/strategies/basis-arb.d.ts +27 -0
  39. package/dist/bot/strategies/basis-arb.js +97 -0
  40. package/dist/bot/strategies/claude-agent.d.ts +31 -0
  41. package/dist/bot/strategies/claude-agent.js +155 -0
  42. package/dist/bot/strategies/dca-strategy.d.ts +25 -0
  43. package/dist/bot/strategies/dca-strategy.js +59 -0
  44. package/dist/bot/strategies/engine-mm.d.ts +29 -0
  45. package/dist/bot/strategies/engine-mm.js +141 -0
  46. package/dist/bot/strategies/funding-arb-strategy.d.ts +32 -0
  47. package/dist/bot/strategies/funding-arb-strategy.js +168 -0
  48. package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
  49. package/dist/bot/strategies/funding-arb-v2.js +222 -0
  50. package/dist/bot/strategies/funding-auto.d.ts +1 -0
  51. package/dist/bot/strategies/funding-auto.js +572 -0
  52. package/dist/bot/strategies/grid-mm.d.ts +25 -0
  53. package/dist/bot/strategies/grid-mm.js +99 -0
  54. package/dist/bot/strategies/grid-strategy.d.ts +39 -0
  55. package/dist/bot/strategies/grid-strategy.js +176 -0
  56. package/dist/bot/strategies/hedge-agent.d.ts +25 -0
  57. package/dist/bot/strategies/hedge-agent.js +79 -0
  58. package/dist/bot/strategies/liquidation-mm.d.ts +25 -0
  59. package/dist/bot/strategies/liquidation-mm.js +108 -0
  60. package/dist/bot/strategies/mean-reversion.d.ts +35 -0
  61. package/dist/bot/strategies/mean-reversion.js +118 -0
  62. package/dist/bot/strategies/momentum-breakout.d.ts +36 -0
  63. package/dist/bot/strategies/momentum-breakout.js +159 -0
  64. package/dist/bot/strategies/regime-mm.d.ts +25 -0
  65. package/dist/bot/strategies/regime-mm.js +111 -0
  66. package/dist/bot/strategies/rfq-agent.d.ts +25 -0
  67. package/dist/bot/strategies/rfq-agent.js +71 -0
  68. package/dist/bot/strategies/simple-mm.d.ts +25 -0
  69. package/dist/bot/strategies/simple-mm.js +76 -0
  70. package/dist/bot/strategy-registry.d.ts +9 -0
  71. package/dist/bot/strategy-registry.js +14 -0
  72. package/dist/bot/strategy-types.d.ts +64 -0
  73. package/dist/bot/strategy-types.js +5 -0
  74. package/dist/bot/trade-journal.d.ts +27 -0
  75. package/dist/bot/trade-journal.js +53 -0
  76. package/dist/bot/tui/BotDashboard.d.ts +49 -0
  77. package/dist/bot/tui/BotDashboard.js +93 -0
  78. package/dist/bot/tui/index.d.ts +6 -0
  79. package/dist/bot/tui/index.js +8 -0
  80. package/dist/bridge-engine.d.ts +0 -1
  81. package/dist/bridge-engine.js +0 -1
  82. package/dist/cache.d.ts +0 -1
  83. package/dist/cache.js +0 -1
  84. package/dist/commands/account.d.ts +1 -1
  85. package/dist/commands/account.js +106 -75
  86. package/dist/commands/agent.d.ts +1 -1
  87. package/dist/commands/arb/index.d.ts +1 -1
  88. package/dist/commands/arb/index.js +1 -1
  89. package/dist/commands/arb-auto.d.ts +1 -1
  90. package/dist/commands/arb-auto.js +1 -1
  91. package/dist/commands/arb.d.ts +1 -1
  92. package/dist/commands/bot.d.ts +1 -1
  93. package/dist/commands/bot.js +198 -14
  94. package/dist/commands/dashboard.d.ts +1 -1
  95. package/dist/commands/dashboard.js +1 -1
  96. package/dist/commands/funds.d.ts +1 -1
  97. package/dist/commands/funds.js +33 -20
  98. package/dist/commands/history.d.ts +1 -1
  99. package/dist/commands/history.js +5 -4
  100. package/dist/commands/manage.d.ts +2 -3
  101. package/dist/commands/manage.js +11 -18
  102. package/dist/commands/market.d.ts +1 -1
  103. package/dist/commands/market.js +7 -7
  104. package/dist/commands/plan.d.ts +1 -1
  105. package/dist/commands/rebalance.d.ts +1 -1
  106. package/dist/commands/rebalance.js +8 -9
  107. package/dist/commands/risk.d.ts +1 -1
  108. package/dist/commands/settings.d.ts +1 -1
  109. package/dist/commands/settings.js +3 -1
  110. package/dist/commands/trade.d.ts +1 -1
  111. package/dist/commands/trade.js +39 -52
  112. package/dist/commands/wallet.d.ts +0 -2
  113. package/dist/commands/wallet.js +0 -5
  114. package/dist/constants.d.ts +0 -3
  115. package/dist/constants.js +0 -4
  116. package/dist/cross-chain-margin.d.ts +1 -1
  117. package/dist/dashboard/index.d.ts +4 -0
  118. package/dist/dashboard/index.js +3 -0
  119. package/dist/dashboard/server.d.ts +1 -1
  120. package/dist/dashboard/server.js +1 -1
  121. package/dist/dashboard/ws-feeds.d.ts +1 -1
  122. package/dist/event-stream.d.ts +1 -1
  123. package/dist/exchanges/aster.d.ts +58 -0
  124. package/dist/exchanges/aster.js +409 -0
  125. package/dist/exchanges/capabilities.d.ts +74 -0
  126. package/dist/exchanges/capabilities.js +44 -0
  127. package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
  128. package/dist/exchanges/hyperliquid-spot.js +20 -12
  129. package/dist/exchanges/hyperliquid.d.ts +8 -2
  130. package/dist/exchanges/hyperliquid.js +21 -6
  131. package/dist/exchanges/index.d.ts +11 -0
  132. package/dist/exchanges/index.js +10 -0
  133. package/dist/exchanges/interface.d.ts +3 -0
  134. package/dist/exchanges/lighter-spot.d.ts +13 -0
  135. package/dist/exchanges/lighter-spot.js +98 -26
  136. package/dist/exchanges/lighter.d.ts +9 -3
  137. package/dist/exchanges/lighter.js +18 -8
  138. package/dist/exchanges/nado.d.ts +52 -0
  139. package/dist/exchanges/nado.js +223 -0
  140. package/dist/exchanges/pacifica.d.ts +7 -1
  141. package/dist/exchanges/pacifica.js +17 -2
  142. package/dist/exchanges/registry.d.ts +13 -0
  143. package/dist/exchanges/registry.js +37 -0
  144. package/dist/exchanges/spot-interface.d.ts +9 -0
  145. package/dist/funding/rates.js +1 -1
  146. package/dist/index.js +2 -6
  147. package/dist/jobs.js +1 -1
  148. package/dist/liquidity.d.ts +1 -2
  149. package/dist/liquidity.js +12 -5
  150. package/dist/mcp-server.js +4 -2
  151. package/dist/pacifica/constants.d.ts +0 -3
  152. package/dist/pacifica/constants.js +0 -4
  153. package/dist/pacifica/types/account.d.ts +0 -8
  154. package/dist/pacifica/types/lake.d.ts +0 -6
  155. package/dist/pacifica/types/position.d.ts +0 -12
  156. package/dist/pacifica/types/ws.d.ts +0 -24
  157. package/dist/pacifica/types/ws.js +1 -41
  158. package/dist/plan-executor.d.ts +1 -1
  159. package/dist/rebalance.d.ts +1 -7
  160. package/dist/rebalance.js +0 -6
  161. package/dist/risk.d.ts +1 -1
  162. package/dist/risk.js +3 -1
  163. package/dist/smart-order.d.ts +1 -1
  164. package/dist/strategies/dca.d.ts +1 -1
  165. package/dist/strategies/funding-arb.d.ts +1 -1
  166. package/dist/strategies/funding-arb.js +193 -181
  167. package/dist/strategies/grid.d.ts +1 -1
  168. package/dist/strategies/grid.js +1 -1
  169. package/dist/strategies/index.d.ts +12 -0
  170. package/dist/strategies/index.js +7 -0
  171. package/dist/strategies/split-order.d.ts +1 -1
  172. package/dist/strategies/trailing-stop.d.ts +1 -1
  173. package/dist/strategies/twap.d.ts +1 -1
  174. package/dist/trade-validator.d.ts +1 -1
  175. package/package.json +7 -1
  176. package/skills/perp-cli/SKILL.md +20 -0
  177. package/skills/perp-cli/references/commands.md +22 -8
@@ -0,0 +1,59 @@
1
+ import { registerStrategy } from "../strategy-registry.js";
2
+ export class DCAStrategy {
3
+ name = "dca";
4
+ describe() {
5
+ return {
6
+ description: "Dollar-cost averaging with timed intervals",
7
+ params: [
8
+ { name: "amount", type: "number", required: true, description: "Base size per order" },
9
+ { name: "interval_sec", type: "number", required: true, description: "Seconds between orders" },
10
+ { name: "total_orders", type: "number", required: false, default: 0, description: "Max orders (0 = unlimited)" },
11
+ { name: "price_limit", type: "number", required: false, description: "Max price to buy at" },
12
+ ],
13
+ };
14
+ }
15
+ get params() {
16
+ return this._config;
17
+ }
18
+ _config = {};
19
+ async init(ctx, _snapshot) {
20
+ this._config = ctx.config;
21
+ const params = this.params;
22
+ ctx.state.set("dcaOrdersPlaced", 0);
23
+ ctx.state.set("dcaLastOrder", 0);
24
+ ctx.log(` [DCA] Ready: ${params.amount} ${ctx.symbol} every ${params.interval_sec}s`);
25
+ }
26
+ async onTick(ctx, snapshot) {
27
+ const params = this.params;
28
+ const dcaOrdersPlaced = ctx.state.get("dcaOrdersPlaced");
29
+ const dcaLastOrder = ctx.state.get("dcaLastOrder");
30
+ // Check if it's time for next order
31
+ const timeSinceLast = (Date.now() - dcaLastOrder) / 1000;
32
+ if (dcaLastOrder > 0 && timeSinceLast < params.interval_sec)
33
+ return [];
34
+ // Check order limit
35
+ if (params.total_orders > 0 && dcaOrdersPlaced >= params.total_orders)
36
+ return [];
37
+ // Check price limit
38
+ if (params.price_limit) {
39
+ if (snapshot.price > params.price_limit)
40
+ return [];
41
+ }
42
+ // Place market buy order
43
+ ctx.state.set("dcaOrdersPlaced", dcaOrdersPlaced + 1);
44
+ ctx.state.set("dcaLastOrder", Date.now());
45
+ const progress = params.total_orders > 0 ? ` (${dcaOrdersPlaced + 1}/${params.total_orders})` : "";
46
+ ctx.log(` [DCA] Order #${dcaOrdersPlaced + 1}${progress}: buy ${params.amount} ${ctx.symbol} @ $${snapshot.price.toFixed(2)}`);
47
+ return [{
48
+ type: "place_order",
49
+ side: "buy",
50
+ price: "0",
51
+ size: String(params.amount),
52
+ orderType: "market",
53
+ }];
54
+ }
55
+ async onStop(_ctx) {
56
+ return [{ type: "cancel_all" }];
57
+ }
58
+ }
59
+ registerStrategy("dca", (_config) => new DCAStrategy());
@@ -0,0 +1,29 @@
1
+ import type { Strategy, StrategyContext, StrategyAction, EnrichedSnapshot } from "../strategy-types.js";
2
+ export declare class EngineMmStrategy implements Strategy {
3
+ readonly name = "engine-mm";
4
+ describe(): {
5
+ description: string;
6
+ params: ({
7
+ name: string;
8
+ type: "number";
9
+ required: boolean;
10
+ description: string;
11
+ default?: undefined;
12
+ } | {
13
+ name: string;
14
+ type: "number";
15
+ required: boolean;
16
+ default: number;
17
+ description: string;
18
+ })[];
19
+ };
20
+ private get params();
21
+ private _config;
22
+ init(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<void>;
23
+ onTick(ctx: StrategyContext, snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
24
+ onStop(_ctx: StrategyContext): Promise<StrategyAction[]>;
25
+ /** Composite fair value: equal-weight VWAP + EMA(20) + mid-price + orderbook mid */
26
+ private computeFairValue;
27
+ /** Dynamic spread: base_spread * (1 + volatility_factor * ATR/price) */
28
+ private computeDynamicSpread;
29
+ }
@@ -0,0 +1,141 @@
1
+ import { registerStrategy } from "../strategy-registry.js";
2
+ import { calculateEMA, calculateVWAP, calculateATR } from "../indicators.js";
3
+ export class EngineMmStrategy {
4
+ name = "engine-mm";
5
+ describe() {
6
+ return {
7
+ description: "Production quoting engine with composite fair value and dynamic spread",
8
+ params: [
9
+ { name: "baseSpread", type: "number", required: true, description: "Base spread in basis points" },
10
+ { name: "levels", type: "number", required: false, default: 3, description: "Number of levels each side (3-5)" },
11
+ { name: "maxInventory", type: "number", required: true, description: "Maximum inventory position" },
12
+ { name: "skewFactor", type: "number", required: false, default: 0.5, description: "Inventory skew factor (0-1)" },
13
+ { name: "refreshSec", type: "number", required: false, default: 5, description: "Minimum seconds between requotes" },
14
+ ],
15
+ };
16
+ }
17
+ get params() {
18
+ return this._config;
19
+ }
20
+ _config = {};
21
+ async init(ctx, _snapshot) {
22
+ this._config = ctx.config;
23
+ ctx.state.set("inventory", 0);
24
+ ctx.state.set("lastRefresh", 0);
25
+ const p = this.params;
26
+ ctx.log(` [ENGINE-MM] baseSpread=${p.baseSpread}bps, levels=${p.levels}, maxInv=${p.maxInventory}, skew=${p.skewFactor}`);
27
+ }
28
+ async onTick(ctx, snapshot) {
29
+ const params = this.params;
30
+ const now = Date.now();
31
+ const lastRefresh = ctx.state.get("lastRefresh") ?? 0;
32
+ // Rate-limit requotes
33
+ if (now - lastRefresh < (params.refreshSec ?? 5) * 1000) {
34
+ return [{ type: "noop" }];
35
+ }
36
+ ctx.state.set("lastRefresh", now);
37
+ const actions = [];
38
+ actions.push({ type: "cancel_all" });
39
+ // Compute fair value: composite of VWAP + EMA(20) + mid-price + oracle (equal weight)
40
+ const fairValue = this.computeFairValue(snapshot);
41
+ // Compute dynamic spread: base_spread * (1 + volatility_factor * ATR/price)
42
+ const dynamicSpread = this.computeDynamicSpread(snapshot);
43
+ // Inventory skew: shift quotes away from heavy side
44
+ const inventory = ctx.state.get("inventory") ?? 0;
45
+ const inventoryRatio = params.maxInventory > 0 ? inventory / params.maxInventory : 0;
46
+ const skew = inventoryRatio * (params.skewFactor ?? 0.5) * dynamicSpread;
47
+ const levels = params.levels || 3;
48
+ for (let i = 1; i <= levels; i++) {
49
+ const levelSpread = dynamicSpread * (1 + (i - 1) * 0.5); // increasing spread per level
50
+ const bidPrice = fairValue - levelSpread / 2 - skew;
51
+ const askPrice = fairValue + levelSpread / 2 - skew;
52
+ // Reduce size on heavy inventory side
53
+ const baseSize = params.maxInventory / (levels * 2);
54
+ const inventoryPenalty = Math.min(Math.abs(inventoryRatio), 0.8);
55
+ const bidSize = inventory > 0 ? baseSize * (1 - inventoryPenalty) : baseSize;
56
+ const askSize = inventory < 0 ? baseSize * (1 - inventoryPenalty) : baseSize;
57
+ if (bidSize > 0 && bidPrice > 0) {
58
+ actions.push({
59
+ type: "place_order",
60
+ side: "buy",
61
+ price: bidPrice.toFixed(6),
62
+ size: String(bidSize),
63
+ orderType: "limit",
64
+ });
65
+ }
66
+ if (askSize > 0 && askPrice > 0) {
67
+ actions.push({
68
+ type: "place_order",
69
+ side: "sell",
70
+ price: askPrice.toFixed(6),
71
+ size: String(askSize),
72
+ orderType: "limit",
73
+ });
74
+ }
75
+ }
76
+ // Update inventory estimate from price movement
77
+ const prevMid = ctx.state.get("prevMid") ?? snapshot.price;
78
+ if (snapshot.price > prevMid) {
79
+ ctx.state.set("inventory", Math.min(inventory + (params.maxInventory / (levels * 4)), params.maxInventory));
80
+ }
81
+ else if (snapshot.price < prevMid) {
82
+ ctx.state.set("inventory", Math.max(inventory - (params.maxInventory / (levels * 4)), -params.maxInventory));
83
+ }
84
+ ctx.state.set("prevMid", snapshot.price);
85
+ return actions;
86
+ }
87
+ async onStop(_ctx) {
88
+ return [{ type: "cancel_all" }];
89
+ }
90
+ /** Composite fair value: equal-weight VWAP + EMA(20) + mid-price + orderbook mid */
91
+ computeFairValue(snapshot) {
92
+ const components = [];
93
+ // Mid-price (always available)
94
+ components.push(snapshot.price);
95
+ // VWAP from klines
96
+ if (snapshot.klines.length > 0) {
97
+ const candles = snapshot.klines.map(k => ({
98
+ close: parseFloat(k.close),
99
+ volume: parseFloat(k.volume),
100
+ }));
101
+ const vwap = calculateVWAP(candles);
102
+ if (vwap > 0)
103
+ components.push(vwap);
104
+ }
105
+ // EMA(20) from klines
106
+ if (snapshot.klines.length >= 20) {
107
+ const closes = snapshot.klines.map(k => parseFloat(k.close));
108
+ const ema = calculateEMA(closes, 20);
109
+ if (ema.length > 0)
110
+ components.push(ema[ema.length - 1]);
111
+ }
112
+ // Orderbook mid
113
+ if (snapshot.orderbook.bids.length > 0 && snapshot.orderbook.asks.length > 0) {
114
+ const bestBid = parseFloat(snapshot.orderbook.bids[0][0]);
115
+ const bestAsk = parseFloat(snapshot.orderbook.asks[0][0]);
116
+ if (bestBid > 0 && bestAsk > 0) {
117
+ components.push((bestBid + bestAsk) / 2);
118
+ }
119
+ }
120
+ return components.reduce((s, v) => s + v, 0) / components.length;
121
+ }
122
+ /** Dynamic spread: base_spread * (1 + volatility_factor * ATR/price) */
123
+ computeDynamicSpread(snapshot) {
124
+ const baseSpreadFrac = this.params.baseSpread / 10000;
125
+ let volatilityFactor = 1;
126
+ if (snapshot.klines.length >= 15) {
127
+ const candles = snapshot.klines.map(k => ({
128
+ high: parseFloat(k.high),
129
+ low: parseFloat(k.low),
130
+ close: parseFloat(k.close),
131
+ }));
132
+ const atr = calculateATR(candles, 14);
133
+ if (atr.length > 0) {
134
+ const latestATR = atr[atr.length - 1];
135
+ volatilityFactor = 1 + (latestATR / snapshot.price);
136
+ }
137
+ }
138
+ return snapshot.price * baseSpreadFrac * volatilityFactor;
139
+ }
140
+ }
141
+ registerStrategy("engine-mm", (_config) => new EngineMmStrategy());
@@ -0,0 +1,32 @@
1
+ import type { Strategy, StrategyContext, StrategyAction, EnrichedSnapshot } from "../strategy-types.js";
2
+ export declare class FundingArbStrategy implements Strategy {
3
+ readonly name = "funding-arb";
4
+ describe(): {
5
+ description: string;
6
+ params: ({
7
+ name: string;
8
+ type: "number";
9
+ required: boolean;
10
+ description: string;
11
+ default?: undefined;
12
+ } | {
13
+ name: string;
14
+ type: "number";
15
+ required: boolean;
16
+ default: number;
17
+ description: string;
18
+ } | {
19
+ name: string;
20
+ type: "string";
21
+ required: boolean;
22
+ description: string;
23
+ default?: undefined;
24
+ })[];
25
+ };
26
+ private get params();
27
+ private _config;
28
+ init(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<void>;
29
+ onTick(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
30
+ onStop(_ctx: StrategyContext): Promise<StrategyAction[]>;
31
+ private fetchRates;
32
+ }
@@ -0,0 +1,168 @@
1
+ import { registerStrategy } from "../strategy-registry.js";
2
+ import { checkArbLiquidity } from "../../liquidity.js";
3
+ import { computeMatchedSize, reconcileArbFills } from "../../arb-sizing.js";
4
+ export class FundingArbStrategy {
5
+ name = "funding-arb";
6
+ describe() {
7
+ return {
8
+ description: "Cross-exchange funding rate arbitrage",
9
+ params: [
10
+ { name: "min_spread", type: "number", required: true, description: "Minimum annualized spread %" },
11
+ { name: "close_spread", type: "number", required: false, default: 5, description: "Spread to close at" },
12
+ { name: "size_usd", type: "number", required: true, description: "Position size in USD" },
13
+ { name: "max_positions", type: "number", required: false, default: 3, description: "Max concurrent positions" },
14
+ { name: "exchanges", type: "string", required: true, description: "Comma-separated exchange names" },
15
+ ],
16
+ };
17
+ }
18
+ get params() {
19
+ return this._config;
20
+ }
21
+ _config = {};
22
+ async init(ctx, _snapshot) {
23
+ this._config = ctx.config;
24
+ const params = this.params;
25
+ ctx.state.set("arbRunning", true);
26
+ ctx.state.set("arbPositions", 0);
27
+ ctx.log(` [ARB] Funding arb ready | spread >= ${params.min_spread}% | size $${params.size_usd}`);
28
+ ctx.log(` [ARB] Exchanges: ${params.exchanges.join(", ")}`);
29
+ }
30
+ async onTick(ctx, _snapshot) {
31
+ const params = this.params;
32
+ const arbPositions = ctx.state.get("arbPositions");
33
+ // Get extra adapters from context state (set by engine)
34
+ const extraAdapters = ctx.state.get("extraAdapters");
35
+ // Build adapter map from primary + extras
36
+ const adapters = new Map();
37
+ adapters.set(ctx.adapter.name.toLowerCase(), ctx.adapter);
38
+ if (extraAdapters) {
39
+ for (const [name, a] of extraAdapters)
40
+ adapters.set(name, a);
41
+ }
42
+ if (adapters.size < 2) {
43
+ ctx.log(` [ARB] Need 2+ exchanges, have ${adapters.size}. Skipping.`);
44
+ return [];
45
+ }
46
+ // Fetch rates from all exchanges
47
+ const ratesByExchange = new Map();
48
+ for (const [name, a] of adapters) {
49
+ const rates = await this.fetchRates(a, name);
50
+ const map = new Map();
51
+ for (const r of rates)
52
+ map.set(r.symbol.toUpperCase(), { rate: r.rate, price: r.price });
53
+ ratesByExchange.set(name, map);
54
+ }
55
+ // Find opportunities
56
+ const exchangeNames = [...ratesByExchange.keys()];
57
+ const opportunities = [];
58
+ const allSymbols = new Set();
59
+ for (const [, map] of ratesByExchange) {
60
+ for (const sym of map.keys())
61
+ allSymbols.add(sym);
62
+ }
63
+ for (const sym of allSymbols) {
64
+ let minRate = Infinity, maxRate = -Infinity;
65
+ let minExchange = "", maxExchange = "";
66
+ for (const exName of exchangeNames) {
67
+ const rate = ratesByExchange.get(exName)?.get(sym)?.rate;
68
+ if (rate === undefined)
69
+ continue;
70
+ if (rate < minRate) {
71
+ minRate = rate;
72
+ minExchange = exName;
73
+ }
74
+ if (rate > maxRate) {
75
+ maxRate = rate;
76
+ maxExchange = exName;
77
+ }
78
+ }
79
+ if (minExchange && maxExchange && minExchange !== maxExchange) {
80
+ const { computeAnnualSpread: cas } = await import("../../funding.js");
81
+ const spread = cas(maxRate, maxExchange, minRate, minExchange);
82
+ if (spread >= params.min_spread) {
83
+ opportunities.push({
84
+ symbol: sym,
85
+ longExchange: minExchange,
86
+ shortExchange: maxExchange,
87
+ longRate: minRate,
88
+ shortRate: maxRate,
89
+ spread,
90
+ });
91
+ }
92
+ }
93
+ }
94
+ opportunities.sort((a, b) => b.spread - a.spread);
95
+ if (opportunities.length > 0) {
96
+ const top = opportunities.slice(0, 3);
97
+ for (const opp of top) {
98
+ ctx.log(` [ARB] ${opp.symbol}: ${opp.spread.toFixed(1)}% spread (long ${opp.longExchange} ${(opp.longRate * 100).toFixed(4)}% / short ${opp.shortExchange} ${(opp.shortRate * 100).toFixed(4)}%)`);
99
+ }
100
+ // Auto-execute if under position limit
101
+ if (arbPositions < params.max_positions && opportunities.length > 0) {
102
+ const best = opportunities[0];
103
+ const longAdapter = adapters.get(best.longExchange);
104
+ const shortAdapter = adapters.get(best.shortExchange);
105
+ if (longAdapter && shortAdapter) {
106
+ const price = ratesByExchange.get(best.longExchange)?.get(best.symbol)?.price ?? 0;
107
+ if (price > 0) {
108
+ // Liquidity check
109
+ const liq = await checkArbLiquidity(longAdapter, shortAdapter, best.symbol, params.size_usd, 0.5, (msg) => ctx.log(` ${msg}`));
110
+ if (!liq.viable)
111
+ return [];
112
+ // Compute matched size
113
+ const matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange);
114
+ if (!matched) {
115
+ ctx.log(` [ARB] Skip ${best.symbol}: can't compute matched size (min notional or precision issue)`);
116
+ return [];
117
+ }
118
+ try {
119
+ ctx.log(` [ARB] Opening: ${matched.size} ${best.symbol} on both legs ($${matched.notional.toFixed(0)}/leg, slippage ~${liq.longSlippage.toFixed(2)}%/${liq.shortSlippage.toFixed(2)}%)`);
120
+ await Promise.all([
121
+ longAdapter.marketOrder(best.symbol, "buy", matched.size),
122
+ shortAdapter.marketOrder(best.symbol, "sell", matched.size),
123
+ ]);
124
+ // Verify fills match, correct if needed
125
+ try {
126
+ const recon = await reconcileArbFills(longAdapter, shortAdapter, best.symbol, (msg) => ctx.log(` ${msg}`));
127
+ if (!recon.matched) {
128
+ ctx.log(` [ARB] WARNING: fills not matched after correction attempt`);
129
+ }
130
+ }
131
+ catch { /* non-critical */ }
132
+ ctx.state.set("arbPositions", arbPositions + 1);
133
+ ctx.log(` [ARB] Position opened! (${arbPositions + 1}/${params.max_positions})`);
134
+ // Return noop -- we already executed directly via adapters
135
+ // (arb requires multi-adapter coordination, can't be expressed as single-adapter actions)
136
+ return [{ type: "noop" }];
137
+ }
138
+ catch (err) {
139
+ const msg = err instanceof Error ? err.message : String(err);
140
+ ctx.log(` [ARB] Execution failed: ${msg}`);
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+ else {
147
+ ctx.log(` [ARB] No opportunities >= ${params.min_spread}% spread`);
148
+ }
149
+ return [];
150
+ }
151
+ async onStop(_ctx) {
152
+ return [{ type: "cancel_all" }];
153
+ }
154
+ async fetchRates(adapter, _exchangeName) {
155
+ try {
156
+ const markets = await adapter.getMarkets();
157
+ return markets.map(m => ({
158
+ symbol: m.symbol,
159
+ rate: parseFloat(m.fundingRate),
160
+ price: parseFloat(m.markPrice),
161
+ }));
162
+ }
163
+ catch {
164
+ return [];
165
+ }
166
+ }
167
+ }
168
+ registerStrategy("funding-arb", (_config) => new FundingArbStrategy());
@@ -0,0 +1,32 @@
1
+ import type { Strategy, StrategyContext, StrategyAction, EnrichedSnapshot } from "../strategy-types.js";
2
+ export declare class FundingArbV2Strategy implements Strategy {
3
+ readonly name = "funding-arb-v2";
4
+ private _config;
5
+ describe(): {
6
+ description: string;
7
+ params: ({
8
+ name: string;
9
+ type: "number";
10
+ required: boolean;
11
+ default: number;
12
+ description: string;
13
+ } | {
14
+ name: string;
15
+ type: "number";
16
+ required: boolean;
17
+ description: string;
18
+ default?: undefined;
19
+ })[];
20
+ };
21
+ private get minSpread();
22
+ private get closeSpread();
23
+ private get maxPositions();
24
+ private get sizeUsd();
25
+ private get scanIntervalSec();
26
+ init(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<void>;
27
+ onTick(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
28
+ onStop(ctx: StrategyContext): Promise<StrategyAction[]>;
29
+ private getAdapters;
30
+ /** Convert 8h funding rate difference to annualized spread % */
31
+ private annualizeSpread;
32
+ }