perp-cli 0.6.4 → 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 +94 -91
  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,185 @@
1
+ import { registerStrategy } from "../strategy-registry.js";
2
+ import { scoreOpportunity, detectPulse, tierRank, evaluateGuard, createGuardState, GUARD_PRESETS, } from "../apex/index.js";
3
+ const DEFAULT_CONFIG = {
4
+ maxSlots: 3,
5
+ minRadarScore: 200,
6
+ minPulseTier: "IMMEDIATE_MOVER",
7
+ guardPreset: "moderate",
8
+ dailyLossLimit: 250,
9
+ size: 0.1,
10
+ leverage: 5,
11
+ };
12
+ function resolveConfig(raw) {
13
+ return {
14
+ maxSlots: Number(raw.maxSlots ?? DEFAULT_CONFIG.maxSlots),
15
+ minRadarScore: Number(raw.minRadarScore ?? DEFAULT_CONFIG.minRadarScore),
16
+ minPulseTier: raw.minPulseTier ?? DEFAULT_CONFIG.minPulseTier,
17
+ guardPreset: String(raw.guardPreset ?? DEFAULT_CONFIG.guardPreset),
18
+ dailyLossLimit: Number(raw.dailyLossLimit ?? DEFAULT_CONFIG.dailyLossLimit),
19
+ size: Number(raw.size ?? DEFAULT_CONFIG.size),
20
+ leverage: Number(raw.leverage ?? DEFAULT_CONFIG.leverage),
21
+ };
22
+ }
23
+ export class ApexStrategy {
24
+ name = "apex";
25
+ cfg = DEFAULT_CONFIG;
26
+ guardConfig = GUARD_PRESETS.moderate;
27
+ slots = [];
28
+ pulseHistory = new Map();
29
+ dailyLoss = 0;
30
+ describe() {
31
+ return {
32
+ description: "APEX orchestrator: Radar screening + Pulse momentum + Guard trailing stops. Multi-slot concurrent positions.",
33
+ params: [
34
+ { name: "maxSlots", type: "number", required: false, default: 3, description: "Max concurrent positions" },
35
+ { name: "minRadarScore", type: "number", required: false, default: 200, description: "Minimum Radar score to consider (0-400)" },
36
+ { name: "minPulseTier", type: "string", required: false, default: "IMMEDIATE_MOVER", description: "Minimum Pulse tier for entry" },
37
+ { name: "guardPreset", type: "string", required: false, default: "moderate", description: "Guard stop preset: conservative, moderate, aggressive" },
38
+ { name: "dailyLossLimit", type: "number", required: false, default: 250, description: "Daily loss limit in USD" },
39
+ { name: "size", type: "number", required: true, description: "Position size per slot" },
40
+ { name: "leverage", type: "number", required: false, default: 5, description: "Leverage to set on entry" },
41
+ ],
42
+ };
43
+ }
44
+ async init(ctx, _snapshot) {
45
+ this.cfg = resolveConfig(ctx.config);
46
+ this.guardConfig = GUARD_PRESETS[this.cfg.guardPreset] ?? GUARD_PRESETS.moderate;
47
+ this.slots = [];
48
+ this.pulseHistory.clear();
49
+ this.dailyLoss = 0;
50
+ ctx.log(` [APEX] Initialized | slots=${this.cfg.maxSlots} radar>=${this.cfg.minRadarScore} pulse>=${this.cfg.minPulseTier} guard=${this.cfg.guardPreset}`);
51
+ }
52
+ async onTick(ctx, snapshot) {
53
+ const actions = [];
54
+ // Daily loss circuit breaker
55
+ if (this.dailyLoss >= this.cfg.dailyLossLimit) {
56
+ ctx.log(` [APEX] Daily loss limit reached ($${this.dailyLoss.toFixed(2)}/$${this.cfg.dailyLossLimit})`);
57
+ return [];
58
+ }
59
+ // ── Phase 1: Manage existing slots (Guard) ──
60
+ const closedSlots = [];
61
+ for (let i = 0; i < this.slots.length; i++) {
62
+ const slot = this.slots[i];
63
+ const roe = this.calculateRoe(slot, snapshot.price);
64
+ const { action, newState } = evaluateGuard(roe, slot.guard, this.guardConfig);
65
+ slot.guard = newState;
66
+ if (action === "close") {
67
+ ctx.log(` [APEX] Guard close slot ${slot.symbol} ${slot.side} | ROE=${roe.toFixed(2)}% floor=${newState.currentFloor.toFixed(2)}%`);
68
+ actions.push({
69
+ type: "place_order",
70
+ side: slot.side === "buy" ? "sell" : "buy",
71
+ price: snapshot.price.toFixed(2),
72
+ size: String(slot.size),
73
+ orderType: "market",
74
+ reduceOnly: true,
75
+ });
76
+ // Track daily loss
77
+ if (roe < 0) {
78
+ this.dailyLoss += Math.abs(roe / 100) * slot.size * slot.entryPrice;
79
+ }
80
+ closedSlots.push(i);
81
+ }
82
+ }
83
+ // Remove closed slots (reverse order to preserve indices)
84
+ for (const idx of closedSlots.reverse()) {
85
+ this.slots.splice(idx, 1);
86
+ }
87
+ // ── Phase 2: Record pulse history ──
88
+ const currentOi = parseFloat(snapshot.openInterest);
89
+ const currentVolume = snapshot.volume24h;
90
+ const historyKey = ctx.symbol;
91
+ const existing = this.pulseHistory.get(historyKey) ?? [];
92
+ existing.push({ oi: currentOi, volume: currentVolume });
93
+ // Keep last 10 entries
94
+ if (existing.length > 10)
95
+ existing.shift();
96
+ this.pulseHistory.set(historyKey, existing);
97
+ // ── Phase 3: Look for new entries if slots available ──
98
+ if (this.slots.length < this.cfg.maxSlots) {
99
+ // Already tracking this symbol in a slot?
100
+ const symbolInSlot = this.slots.some(s => s.symbol === ctx.symbol);
101
+ if (!symbolInSlot) {
102
+ // Score with Radar
103
+ const radarScore = scoreOpportunity(snapshot, null);
104
+ radarScore.symbol = ctx.symbol;
105
+ // Detect Pulse
106
+ const pulse = detectPulse(snapshot, this.pulseHistory);
107
+ pulse.symbol = ctx.symbol;
108
+ const meetsRadar = radarScore.total >= this.cfg.minRadarScore;
109
+ const meetsPulse = tierRank(pulse.tier) >= tierRank(this.cfg.minPulseTier);
110
+ if (meetsRadar && meetsPulse) {
111
+ // Determine direction from funding + technicals
112
+ const side = this.determineSide(snapshot, radarScore, pulse);
113
+ ctx.log(` [APEX] Entry signal | ${ctx.symbol} ${side} | radar=${radarScore.total} pulse=${pulse.tier}(${pulse.confidence.toFixed(2)}) ` +
114
+ `OI%=${(pulse.oiChange * 100).toFixed(1)} vol=${pulse.volumeRatio.toFixed(1)}x`);
115
+ // Set leverage first
116
+ actions.push({
117
+ type: "set_leverage",
118
+ leverage: this.cfg.leverage,
119
+ });
120
+ // Enter position
121
+ actions.push({
122
+ type: "place_order",
123
+ side,
124
+ price: snapshot.price.toFixed(2),
125
+ size: String(this.cfg.size),
126
+ orderType: "market",
127
+ });
128
+ this.slots.push({
129
+ symbol: ctx.symbol,
130
+ side,
131
+ entryPrice: snapshot.price,
132
+ size: this.cfg.size,
133
+ guard: createGuardState(),
134
+ });
135
+ }
136
+ else {
137
+ ctx.log(` [APEX] No entry | radar=${radarScore.total}/${this.cfg.minRadarScore} ` +
138
+ `pulse=${pulse.tier} | slots=${this.slots.length}/${this.cfg.maxSlots}`);
139
+ }
140
+ }
141
+ }
142
+ return actions;
143
+ }
144
+ async onStop(ctx) {
145
+ const actions = [];
146
+ // Close all active slots
147
+ for (const slot of this.slots) {
148
+ ctx.log(` [APEX] Closing slot ${slot.symbol} ${slot.side} on stop`);
149
+ actions.push({
150
+ type: "place_order",
151
+ side: slot.side === "buy" ? "sell" : "buy",
152
+ price: "0",
153
+ size: String(slot.size),
154
+ orderType: "market",
155
+ reduceOnly: true,
156
+ });
157
+ }
158
+ this.slots = [];
159
+ return actions;
160
+ }
161
+ // ── Private helpers ──
162
+ calculateRoe(slot, currentPrice) {
163
+ if (slot.entryPrice === 0)
164
+ return 0;
165
+ const priceDelta = currentPrice - slot.entryPrice;
166
+ const direction = slot.side === "buy" ? 1 : -1;
167
+ return (direction * priceDelta / slot.entryPrice) * 100;
168
+ }
169
+ determineSide(snapshot, _radarScore, _pulse) {
170
+ // Funding-based bias: negative funding → longs are cheap → buy
171
+ // Positive funding → shorts are cheap → sell
172
+ const fr = snapshot.fundingRate;
173
+ // RSI confirmation
174
+ const rsi = snapshot.rsi;
175
+ const rsiOversold = !isNaN(rsi) && rsi < 35;
176
+ const rsiOverbought = !isNaN(rsi) && rsi > 65;
177
+ if (fr < -0.0001 || rsiOversold)
178
+ return "buy";
179
+ if (fr > 0.0001 || rsiOverbought)
180
+ return "sell";
181
+ // Default to buy in ambiguous conditions
182
+ return "buy";
183
+ }
184
+ }
185
+ registerStrategy("apex", (_config) => new ApexStrategy());
@@ -0,0 +1,27 @@
1
+ import type { Strategy, StrategyContext, StrategyAction, EnrichedSnapshot } from "../strategy-types.js";
2
+ export declare class AvellanedaMmStrategy implements Strategy {
3
+ readonly name = "avellaneda-mm";
4
+ describe(): {
5
+ description: string;
6
+ params: ({
7
+ name: string;
8
+ type: "number";
9
+ required: boolean;
10
+ default: number;
11
+ description: string;
12
+ } | {
13
+ name: string;
14
+ type: "number";
15
+ required: boolean;
16
+ description: string;
17
+ default?: undefined;
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
+ /** Estimate sigma from ATR or use override */
26
+ private estimateSigma;
27
+ }
@@ -0,0 +1,111 @@
1
+ import { registerStrategy } from "../strategy-registry.js";
2
+ import { calculateATR } from "../indicators.js";
3
+ export class AvellanedaMmStrategy {
4
+ name = "avellaneda-mm";
5
+ describe() {
6
+ return {
7
+ description: "Avellaneda-Stoikov optimal market making with inventory-adjusted reservation price",
8
+ params: [
9
+ { name: "gamma", type: "number", required: false, default: 0.1, description: "Risk aversion parameter" },
10
+ { name: "kappa", type: "number", required: false, default: 1.5, description: "Order arrival rate" },
11
+ { name: "sigma", type: "number", required: false, default: 0, description: "Volatility override (0 = auto)" },
12
+ { name: "maxPosition", type: "number", required: true, description: "Maximum position size" },
13
+ { name: "timeHorizon", type: "number", required: false, default: 86400, description: "Session length in seconds" },
14
+ { name: "size", type: "number", required: true, description: "Order size per level" },
15
+ ],
16
+ };
17
+ }
18
+ get params() {
19
+ return this._config;
20
+ }
21
+ _config = {};
22
+ async init(ctx, _snapshot) {
23
+ this._config = ctx.config;
24
+ ctx.state.set("inventory", 0);
25
+ ctx.state.set("sessionStart", Date.now());
26
+ const p = this.params;
27
+ ctx.log(` [AVELLANEDA-MM] gamma=${p.gamma}, kappa=${p.kappa}, sigma=${p.sigma || "auto"}, maxPos=${p.maxPosition}, T=${p.timeHorizon}s`);
28
+ }
29
+ async onTick(ctx, snapshot) {
30
+ const params = this.params;
31
+ const actions = [];
32
+ actions.push({ type: "cancel_all" });
33
+ const gamma = params.gamma || 0.1;
34
+ const kappa = params.kappa || 1.5;
35
+ const mid = snapshot.price;
36
+ // Estimate sigma (volatility) from ATR or use override
37
+ const sigma = this.estimateSigma(snapshot, params.sigma);
38
+ // Time remaining: fraction of session left
39
+ const sessionStart = ctx.state.get("sessionStart") ?? Date.now();
40
+ const elapsed = (Date.now() - sessionStart) / 1000;
41
+ const timeHorizon = params.timeHorizon || 86400;
42
+ const timeRemaining = Math.max(timeHorizon - elapsed, 1) / timeHorizon; // (T-t)/T normalized
43
+ // Current inventory
44
+ const q = ctx.state.get("inventory") ?? 0;
45
+ // Avellaneda-Stoikov reservation price:
46
+ // r = mid - q * gamma * sigma^2 * (T-t)
47
+ const reservationPrice = mid - q * gamma * (sigma ** 2) * timeRemaining;
48
+ // Optimal spread:
49
+ // delta = gamma * sigma^2 * (T-t) + (2/gamma) * ln(1 + gamma/kappa)
50
+ const optimalSpread = gamma * (sigma ** 2) * timeRemaining + (2 / gamma) * Math.log(1 + gamma / kappa);
51
+ const spreadInPrice = optimalSpread * mid; // convert to price terms
52
+ const bidPrice = reservationPrice - spreadInPrice / 2;
53
+ const askPrice = reservationPrice + spreadInPrice / 2;
54
+ // Clamp to max position: skip orders that would exceed
55
+ const size = params.size;
56
+ if (q < params.maxPosition && bidPrice > 0) {
57
+ actions.push({
58
+ type: "place_order",
59
+ side: "buy",
60
+ price: bidPrice.toFixed(6),
61
+ size: String(size),
62
+ orderType: "limit",
63
+ });
64
+ }
65
+ if (q > -params.maxPosition && askPrice > 0) {
66
+ actions.push({
67
+ type: "place_order",
68
+ side: "sell",
69
+ price: askPrice.toFixed(6),
70
+ size: String(size),
71
+ orderType: "limit",
72
+ });
73
+ }
74
+ // Update inventory estimate
75
+ const prevMid = ctx.state.get("prevMid") ?? mid;
76
+ if (mid > prevMid && q < params.maxPosition) {
77
+ ctx.state.set("inventory", q + size * 0.5);
78
+ }
79
+ else if (mid < prevMid && q > -params.maxPosition) {
80
+ ctx.state.set("inventory", q - size * 0.5);
81
+ }
82
+ ctx.state.set("prevMid", mid);
83
+ if (ctx.tick % 10 === 0) {
84
+ ctx.log(` [AVELLANEDA-MM] r=$${reservationPrice.toFixed(2)}, spread=$${spreadInPrice.toFixed(4)}, q=${q.toFixed(4)}, sigma=${sigma.toFixed(6)}, T-t=${timeRemaining.toFixed(3)}`);
85
+ }
86
+ return actions;
87
+ }
88
+ async onStop(_ctx) {
89
+ return [{ type: "cancel_all" }];
90
+ }
91
+ /** Estimate sigma from ATR or use override */
92
+ estimateSigma(snapshot, sigmaOverride) {
93
+ if (sigmaOverride > 0)
94
+ return sigmaOverride;
95
+ if (snapshot.klines.length >= 15) {
96
+ const candles = snapshot.klines.map(k => ({
97
+ high: parseFloat(k.high),
98
+ low: parseFloat(k.low),
99
+ close: parseFloat(k.close),
100
+ }));
101
+ const atr = calculateATR(candles, 14);
102
+ if (atr.length > 0) {
103
+ // Normalize ATR to fractional volatility
104
+ return atr[atr.length - 1] / snapshot.price;
105
+ }
106
+ }
107
+ // Fallback: use 24h volatility from snapshot
108
+ return snapshot.volatility24h / 100 || 0.02;
109
+ }
110
+ }
111
+ registerStrategy("avellaneda-mm", (_config) => new AvellanedaMmStrategy());
@@ -0,0 +1,27 @@
1
+ import type { Strategy, StrategyContext, StrategyAction, EnrichedSnapshot } from "../strategy-types.js";
2
+ export declare class BasisArbStrategy implements Strategy {
3
+ readonly name = "basis-arb";
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 annualizedBasisThreshold();
22
+ private get size();
23
+ private get holdingPeriodHours();
24
+ init(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<void>;
25
+ onTick(ctx: StrategyContext, snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
26
+ onStop(ctx: StrategyContext): Promise<StrategyAction[]>;
27
+ }
@@ -0,0 +1,97 @@
1
+ import { registerStrategy } from "../strategy-registry.js";
2
+ export class BasisArbStrategy {
3
+ name = "basis-arb";
4
+ _config = {};
5
+ describe() {
6
+ return {
7
+ description: "Basis arbitrage — collect funding by positioning against extreme rates",
8
+ params: [
9
+ { name: "annualizedBasisThreshold", type: "number", required: false, default: 30, description: "Annualized funding % to trigger entry" },
10
+ { name: "size", type: "number", required: true, description: "Position size" },
11
+ { name: "holdingPeriodHours", type: "number", required: false, default: 24, description: "Max hours to hold position" },
12
+ ],
13
+ };
14
+ }
15
+ get annualizedBasisThreshold() { return Number(this._config.annualizedBasisThreshold ?? 30); }
16
+ get size() { return Number(this._config.size ?? 0.1); }
17
+ get holdingPeriodHours() { return Number(this._config.holdingPeriodHours ?? 24); }
18
+ async init(ctx, _snapshot) {
19
+ this._config = ctx.config;
20
+ ctx.state.set("position", null);
21
+ ctx.log(` [BASIS] Ready | threshold=${this.annualizedBasisThreshold}% holdingPeriod=${this.holdingPeriodHours}h size=${this.size}`);
22
+ }
23
+ async onTick(ctx, snapshot) {
24
+ const actions = [];
25
+ const price = snapshot.price;
26
+ const fundingRate = snapshot.fundingRate;
27
+ // Annualize: funding is per 8h period -> 3x daily -> 1095x annual
28
+ const annualized = fundingRate * 3 * 365 * 100; // in %
29
+ const position = ctx.state.get("position");
30
+ if (!position) {
31
+ // Check for entry signal
32
+ if (Math.abs(annualized) >= this.annualizedBasisThreshold) {
33
+ // Positive funding = longs pay shorts -> go short to collect
34
+ // Negative funding = shorts pay longs -> go long to collect
35
+ const side = annualized > 0 ? "sell" : "buy";
36
+ const newPosition = {
37
+ side,
38
+ entry: price,
39
+ entryFunding: fundingRate,
40
+ entryTime: Date.now(),
41
+ size: this.size,
42
+ };
43
+ ctx.state.set("position", newPosition);
44
+ ctx.log(` [BASIS] ${side.toUpperCase()} @ $${price.toFixed(2)} | funding=${annualized.toFixed(1)}% annualized (collecting ${side === "sell" ? "positive" : "negative"} funding)`);
45
+ actions.push({
46
+ type: "place_order",
47
+ side,
48
+ price: price.toFixed(2),
49
+ size: String(this.size),
50
+ orderType: "market",
51
+ });
52
+ }
53
+ return actions;
54
+ }
55
+ // Manage existing position — check exit conditions
56
+ const hoursHeld = (Date.now() - position.entryTime) / (1000 * 60 * 60);
57
+ const currentAnnualized = Math.abs(annualized);
58
+ const holdingExpired = hoursHeld >= this.holdingPeriodHours;
59
+ // Exit when funding normalizes (< threshold/2 for hysteresis) or holding period expires
60
+ const fundingNormalized = currentAnnualized < this.annualizedBasisThreshold / 2;
61
+ // Also exit if funding flipped against us
62
+ const fundingFlipped = (position.side === "sell" && annualized < 0) ||
63
+ (position.side === "buy" && annualized > 0);
64
+ if (holdingExpired || fundingNormalized || fundingFlipped) {
65
+ const reason = holdingExpired
66
+ ? `holding period ${this.holdingPeriodHours}h expired`
67
+ : fundingFlipped
68
+ ? "funding flipped against position"
69
+ : `funding normalized (${currentAnnualized.toFixed(1)}%)`;
70
+ ctx.log(` [BASIS] Close ${position.side} @ $${price.toFixed(2)} — ${reason} (held ${hoursHeld.toFixed(1)}h)`);
71
+ ctx.state.set("position", null);
72
+ actions.push({
73
+ type: "place_order",
74
+ side: position.side === "buy" ? "sell" : "buy",
75
+ price: price.toFixed(2),
76
+ size: String(position.size),
77
+ orderType: "market",
78
+ reduceOnly: true,
79
+ });
80
+ }
81
+ return actions;
82
+ }
83
+ async onStop(ctx) {
84
+ const position = ctx.state.get("position");
85
+ if (!position)
86
+ return [];
87
+ return [{
88
+ type: "place_order",
89
+ side: position.side === "buy" ? "sell" : "buy",
90
+ price: "0",
91
+ size: String(position.size),
92
+ orderType: "market",
93
+ reduceOnly: true,
94
+ }];
95
+ }
96
+ }
97
+ registerStrategy("basis-arb", (_config) => new BasisArbStrategy());
@@ -0,0 +1,31 @@
1
+ import type { Strategy, StrategyContext, StrategyAction, EnrichedSnapshot } from "../strategy-types.js";
2
+ export declare class ClaudeAgentStrategy implements Strategy {
3
+ readonly name = "claude-agent";
4
+ private _config;
5
+ private get params();
6
+ describe(): {
7
+ description: string;
8
+ params: ({
9
+ name: string;
10
+ type: "string";
11
+ required: boolean;
12
+ description: string;
13
+ default?: undefined;
14
+ } | {
15
+ name: string;
16
+ type: "string";
17
+ required: boolean;
18
+ default: string;
19
+ description: string;
20
+ } | {
21
+ name: string;
22
+ type: "number";
23
+ required: boolean;
24
+ default: number;
25
+ description: string;
26
+ })[];
27
+ };
28
+ init(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<void>;
29
+ onTick(ctx: StrategyContext, snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
30
+ onStop(_ctx: StrategyContext): Promise<StrategyAction[]>;
31
+ }
@@ -0,0 +1,155 @@
1
+ import { registerStrategy } from "../strategy-registry.js";
2
+ const DEFAULT_PROMPT = `You are a trading assistant. Given the following market snapshot, decide whether to BUY, SELL, or HOLD.
3
+ Respond with ONLY one of: BUY <size>, SELL <size>, or HOLD.
4
+ Examples: "BUY 0.1", "SELL 0.05", "HOLD"`;
5
+ export class ClaudeAgentStrategy {
6
+ name = "claude-agent";
7
+ _config = {};
8
+ get params() {
9
+ return {
10
+ apiKey: String(this._config.apiKey ?? ""),
11
+ model: String(this._config.model ?? "claude-3-5-haiku-latest"),
12
+ maxPositionSize: Number(this._config.maxPositionSize ?? 0.1),
13
+ promptTemplate: String(this._config.promptTemplate ?? DEFAULT_PROMPT),
14
+ tickInterval: Number(this._config.tickInterval ?? 10),
15
+ apiBase: String(this._config.apiBase ?? "https://api.anthropic.com"),
16
+ };
17
+ }
18
+ describe() {
19
+ return {
20
+ description: "Sends market snapshots to an LLM API for trading decisions; parses BUY/SELL/HOLD responses",
21
+ params: [
22
+ { name: "apiKey", type: "string", required: true, description: "LLM API key (Anthropic or compatible)" },
23
+ { name: "model", type: "string", required: false, default: "claude-3-5-haiku-latest", description: "Model name to use" },
24
+ { name: "maxPositionSize", type: "number", required: false, default: 0.1, description: "Maximum order size per LLM decision" },
25
+ { name: "promptTemplate", type: "string", required: false, default: DEFAULT_PROMPT, description: "System prompt sent to LLM" },
26
+ { name: "tickInterval", type: "number", required: false, default: 10, description: "Ticks between LLM calls" },
27
+ { name: "apiBase", type: "string", required: false, default: "https://api.anthropic.com", description: "API base URL" },
28
+ ],
29
+ };
30
+ }
31
+ async init(ctx, _snapshot) {
32
+ this._config = ctx.config;
33
+ const { apiKey, model, maxPositionSize, tickInterval } = this.params;
34
+ if (!apiKey) {
35
+ ctx.log(" [CLAUDE-AGENT] No apiKey configured — running in noop mode");
36
+ }
37
+ else {
38
+ ctx.log(` [CLAUDE-AGENT] model=${model} maxSize=${maxPositionSize} ` +
39
+ `interval=${tickInterval}ticks`);
40
+ }
41
+ ctx.state.set("lastDecision", "HOLD");
42
+ ctx.state.set("lastCallTick", -1);
43
+ }
44
+ async onTick(ctx, snapshot) {
45
+ const { apiKey, model, maxPositionSize, promptTemplate, tickInterval, apiBase } = this.params;
46
+ if (!apiKey) {
47
+ ctx.log(" [CLAUDE-AGENT] No apiKey — skipping LLM call (noop)");
48
+ return [{ type: "noop" }];
49
+ }
50
+ const lastCallTick = ctx.state.get("lastCallTick");
51
+ if (ctx.tick - lastCallTick < tickInterval) {
52
+ return [{ type: "noop" }];
53
+ }
54
+ ctx.state.set("lastCallTick", ctx.tick);
55
+ const userMessage = buildMarketMessage(snapshot, ctx.symbol);
56
+ let rawDecision;
57
+ try {
58
+ rawDecision = await callLlm(apiBase, apiKey, model, promptTemplate, userMessage);
59
+ ctx.state.set("lastDecision", rawDecision);
60
+ ctx.log(` [CLAUDE-AGENT] LLM response: "${rawDecision}"`);
61
+ }
62
+ catch (err) {
63
+ const msg = err instanceof Error ? err.message : String(err);
64
+ ctx.log(` [CLAUDE-AGENT] LLM call failed: ${msg}`);
65
+ return [{ type: "noop" }];
66
+ }
67
+ return parseDecision(rawDecision, maxPositionSize, snapshot.price);
68
+ }
69
+ async onStop(_ctx) {
70
+ return [{ type: "cancel_all" }];
71
+ }
72
+ }
73
+ // ── Helpers ──
74
+ function buildMarketMessage(snapshot, symbol) {
75
+ const lines = [
76
+ `Symbol: ${symbol}`,
77
+ `Price: ${snapshot.price.toFixed(4)}`,
78
+ `Funding Rate: ${(snapshot.fundingRate * 100).toFixed(4)}%`,
79
+ `24h Volatility: ${snapshot.volatility24h.toFixed(2)}%`,
80
+ `24h Volume: ${snapshot.volume24h.toFixed(2)}`,
81
+ `Open Interest: ${snapshot.openInterest}`,
82
+ ];
83
+ const bestBid = snapshot.orderbook.bids[0];
84
+ const bestAsk = snapshot.orderbook.asks[0];
85
+ if (bestBid)
86
+ lines.push(`Best Bid: ${bestBid[0]} (${bestBid[1]})`);
87
+ if (bestAsk)
88
+ lines.push(`Best Ask: ${bestAsk[0]} (${bestAsk[1]})`);
89
+ return lines.join("\n");
90
+ }
91
+ async function callLlm(apiBase, apiKey, model, systemPrompt, userMessage) {
92
+ const url = `${apiBase.replace(/\/$/, "")}/v1/messages`;
93
+ const body = JSON.stringify({
94
+ model,
95
+ max_tokens: 64,
96
+ system: systemPrompt,
97
+ messages: [{ role: "user", content: userMessage }],
98
+ });
99
+ const response = await fetch(url, {
100
+ method: "POST",
101
+ headers: {
102
+ "Content-Type": "application/json",
103
+ "x-api-key": apiKey,
104
+ "anthropic-version": "2023-06-01",
105
+ },
106
+ body,
107
+ });
108
+ if (!response.ok) {
109
+ const text = await response.text().catch(() => "");
110
+ throw new Error(`LLM API error ${response.status}: ${text}`);
111
+ }
112
+ const data = (await response.json());
113
+ const textBlock = data.content?.find((c) => c.type === "text");
114
+ return textBlock?.text?.trim() ?? "HOLD";
115
+ }
116
+ function parseDecision(raw, maxPositionSize, price) {
117
+ const upper = raw.trim().toUpperCase();
118
+ if (upper === "HOLD" || upper.startsWith("HOLD")) {
119
+ return [{ type: "noop" }];
120
+ }
121
+ // Match "BUY <size>" or "SELL <size>"
122
+ const buyMatch = upper.match(/^BUY\s+([\d.]+)/);
123
+ if (buyMatch) {
124
+ const size = Math.min(parseFloat(buyMatch[1]), maxPositionSize);
125
+ if (size > 0) {
126
+ return [
127
+ {
128
+ type: "place_order",
129
+ side: "buy",
130
+ price: price.toFixed(6),
131
+ size: size.toFixed(6),
132
+ orderType: "market",
133
+ },
134
+ ];
135
+ }
136
+ }
137
+ const sellMatch = upper.match(/^SELL\s+([\d.]+)/);
138
+ if (sellMatch) {
139
+ const size = Math.min(parseFloat(sellMatch[1]), maxPositionSize);
140
+ if (size > 0) {
141
+ return [
142
+ {
143
+ type: "place_order",
144
+ side: "sell",
145
+ price: price.toFixed(6),
146
+ size: size.toFixed(6),
147
+ orderType: "market",
148
+ },
149
+ ];
150
+ }
151
+ }
152
+ // Unrecognised response — treat as noop
153
+ return [{ type: "noop" }];
154
+ }
155
+ registerStrategy("claude-agent", (_config) => new ClaudeAgentStrategy());
@@ -0,0 +1,25 @@
1
+ import type { Strategy, StrategyContext, StrategyAction, EnrichedSnapshot } from "../strategy-types.js";
2
+ export declare class DCAStrategy implements Strategy {
3
+ readonly name = "dca";
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
+ }