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.
- package/README.md +7 -2
- package/dist/api/public/hyperliquid.d.ts +2 -2
- package/dist/api/public/hyperliquid.js +0 -10
- package/dist/api/public/lighter.d.ts +2 -1
- package/dist/api/public/pacifica.d.ts +2 -1
- package/dist/arb/index.d.ts +7 -4
- package/dist/arb/index.js +5 -4
- package/dist/arb/sizing.d.ts +1 -1
- package/dist/arb/state.d.ts +1 -1
- package/dist/arb/state.js +4 -3
- package/dist/arb/utils.d.ts +0 -10
- package/dist/arb/utils.js +0 -42
- package/dist/bot/apex/guard.d.ts +26 -0
- package/dist/bot/apex/guard.js +86 -0
- package/dist/bot/apex/index.d.ts +6 -0
- package/dist/bot/apex/index.js +4 -0
- package/dist/bot/apex/pulse.d.ts +22 -0
- package/dist/bot/apex/pulse.js +107 -0
- package/dist/bot/apex/radar.d.ts +17 -0
- package/dist/bot/apex/radar.js +150 -0
- package/dist/bot/conditions.d.ts +1 -1
- package/dist/bot/config.d.ts +5 -1
- package/dist/bot/config.js +1 -1
- package/dist/bot/engine.d.ts +23 -2
- package/dist/bot/engine.js +296 -351
- package/dist/bot/index.d.ts +16 -0
- package/dist/bot/index.js +10 -0
- package/dist/bot/indicators.d.ts +29 -0
- package/dist/bot/indicators.js +144 -0
- package/dist/bot/reflect.d.ts +34 -0
- package/dist/bot/reflect.js +155 -0
- package/dist/bot/strategies/aggressive-taker.d.ts +36 -0
- package/dist/bot/strategies/aggressive-taker.js +125 -0
- package/dist/bot/strategies/apex-strategy.d.ts +36 -0
- package/dist/bot/strategies/apex-strategy.js +185 -0
- package/dist/bot/strategies/avellaneda-mm.d.ts +27 -0
- package/dist/bot/strategies/avellaneda-mm.js +111 -0
- package/dist/bot/strategies/basis-arb.d.ts +27 -0
- package/dist/bot/strategies/basis-arb.js +97 -0
- package/dist/bot/strategies/claude-agent.d.ts +31 -0
- package/dist/bot/strategies/claude-agent.js +155 -0
- package/dist/bot/strategies/dca-strategy.d.ts +25 -0
- package/dist/bot/strategies/dca-strategy.js +59 -0
- package/dist/bot/strategies/engine-mm.d.ts +29 -0
- package/dist/bot/strategies/engine-mm.js +141 -0
- package/dist/bot/strategies/funding-arb-strategy.d.ts +32 -0
- package/dist/bot/strategies/funding-arb-strategy.js +168 -0
- package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
- package/dist/bot/strategies/funding-arb-v2.js +222 -0
- package/dist/bot/strategies/funding-auto.d.ts +1 -0
- package/dist/bot/strategies/funding-auto.js +572 -0
- package/dist/bot/strategies/grid-mm.d.ts +25 -0
- package/dist/bot/strategies/grid-mm.js +99 -0
- package/dist/bot/strategies/grid-strategy.d.ts +39 -0
- package/dist/bot/strategies/grid-strategy.js +176 -0
- package/dist/bot/strategies/hedge-agent.d.ts +25 -0
- package/dist/bot/strategies/hedge-agent.js +79 -0
- package/dist/bot/strategies/liquidation-mm.d.ts +25 -0
- package/dist/bot/strategies/liquidation-mm.js +108 -0
- package/dist/bot/strategies/mean-reversion.d.ts +35 -0
- package/dist/bot/strategies/mean-reversion.js +118 -0
- package/dist/bot/strategies/momentum-breakout.d.ts +36 -0
- package/dist/bot/strategies/momentum-breakout.js +159 -0
- package/dist/bot/strategies/regime-mm.d.ts +25 -0
- package/dist/bot/strategies/regime-mm.js +111 -0
- package/dist/bot/strategies/rfq-agent.d.ts +25 -0
- package/dist/bot/strategies/rfq-agent.js +71 -0
- package/dist/bot/strategies/simple-mm.d.ts +25 -0
- package/dist/bot/strategies/simple-mm.js +76 -0
- package/dist/bot/strategy-registry.d.ts +9 -0
- package/dist/bot/strategy-registry.js +14 -0
- package/dist/bot/strategy-types.d.ts +64 -0
- package/dist/bot/strategy-types.js +5 -0
- package/dist/bot/trade-journal.d.ts +27 -0
- package/dist/bot/trade-journal.js +53 -0
- package/dist/bot/tui/BotDashboard.d.ts +49 -0
- package/dist/bot/tui/BotDashboard.js +93 -0
- package/dist/bot/tui/index.d.ts +6 -0
- package/dist/bot/tui/index.js +8 -0
- package/dist/bridge-engine.d.ts +0 -1
- package/dist/bridge-engine.js +0 -1
- package/dist/cache.d.ts +0 -1
- package/dist/cache.js +0 -1
- package/dist/commands/account.d.ts +1 -1
- package/dist/commands/account.js +94 -91
- package/dist/commands/agent.d.ts +1 -1
- package/dist/commands/arb/index.d.ts +1 -1
- package/dist/commands/arb/index.js +1 -1
- package/dist/commands/arb-auto.d.ts +1 -1
- package/dist/commands/arb-auto.js +1 -1
- package/dist/commands/arb.d.ts +1 -1
- package/dist/commands/bot.d.ts +1 -1
- package/dist/commands/bot.js +198 -14
- package/dist/commands/dashboard.d.ts +1 -1
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/funds.d.ts +1 -1
- package/dist/commands/funds.js +33 -20
- package/dist/commands/history.d.ts +1 -1
- package/dist/commands/history.js +5 -4
- package/dist/commands/manage.d.ts +2 -3
- package/dist/commands/manage.js +11 -18
- package/dist/commands/market.d.ts +1 -1
- package/dist/commands/market.js +7 -7
- package/dist/commands/plan.d.ts +1 -1
- package/dist/commands/rebalance.d.ts +1 -1
- package/dist/commands/rebalance.js +8 -9
- package/dist/commands/risk.d.ts +1 -1
- package/dist/commands/settings.d.ts +1 -1
- package/dist/commands/settings.js +3 -1
- package/dist/commands/trade.d.ts +1 -1
- package/dist/commands/trade.js +39 -52
- package/dist/commands/wallet.d.ts +0 -2
- package/dist/commands/wallet.js +0 -5
- package/dist/constants.d.ts +0 -3
- package/dist/constants.js +0 -4
- package/dist/cross-chain-margin.d.ts +1 -1
- package/dist/dashboard/index.d.ts +4 -0
- package/dist/dashboard/index.js +3 -0
- package/dist/dashboard/server.d.ts +1 -1
- package/dist/dashboard/server.js +1 -1
- package/dist/dashboard/ws-feeds.d.ts +1 -1
- package/dist/event-stream.d.ts +1 -1
- package/dist/exchanges/aster.d.ts +58 -0
- package/dist/exchanges/aster.js +409 -0
- package/dist/exchanges/capabilities.d.ts +74 -0
- package/dist/exchanges/capabilities.js +44 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
- package/dist/exchanges/hyperliquid-spot.js +20 -12
- package/dist/exchanges/hyperliquid.d.ts +8 -2
- package/dist/exchanges/hyperliquid.js +21 -6
- package/dist/exchanges/index.d.ts +11 -0
- package/dist/exchanges/index.js +10 -0
- package/dist/exchanges/interface.d.ts +3 -0
- package/dist/exchanges/lighter-spot.d.ts +13 -0
- package/dist/exchanges/lighter-spot.js +98 -26
- package/dist/exchanges/lighter.d.ts +9 -3
- package/dist/exchanges/lighter.js +18 -8
- package/dist/exchanges/nado.d.ts +52 -0
- package/dist/exchanges/nado.js +223 -0
- package/dist/exchanges/pacifica.d.ts +7 -1
- package/dist/exchanges/pacifica.js +17 -2
- package/dist/exchanges/registry.d.ts +13 -0
- package/dist/exchanges/registry.js +37 -0
- package/dist/exchanges/spot-interface.d.ts +9 -0
- package/dist/funding/rates.js +1 -1
- package/dist/index.js +2 -6
- package/dist/jobs.js +1 -1
- package/dist/liquidity.d.ts +1 -2
- package/dist/liquidity.js +12 -5
- package/dist/mcp-server.js +4 -2
- package/dist/pacifica/constants.d.ts +0 -3
- package/dist/pacifica/constants.js +0 -4
- package/dist/pacifica/types/account.d.ts +0 -8
- package/dist/pacifica/types/lake.d.ts +0 -6
- package/dist/pacifica/types/position.d.ts +0 -12
- package/dist/pacifica/types/ws.d.ts +0 -24
- package/dist/pacifica/types/ws.js +1 -41
- package/dist/plan-executor.d.ts +1 -1
- package/dist/rebalance.d.ts +1 -7
- package/dist/rebalance.js +0 -6
- package/dist/risk.d.ts +1 -1
- package/dist/risk.js +3 -1
- package/dist/smart-order.d.ts +1 -1
- package/dist/strategies/dca.d.ts +1 -1
- package/dist/strategies/funding-arb.d.ts +1 -1
- package/dist/strategies/funding-arb.js +193 -181
- package/dist/strategies/grid.d.ts +1 -1
- package/dist/strategies/grid.js +1 -1
- package/dist/strategies/index.d.ts +12 -0
- package/dist/strategies/index.js +7 -0
- package/dist/strategies/split-order.d.ts +1 -1
- package/dist/strategies/trailing-stop.d.ts +1 -1
- package/dist/strategies/twap.d.ts +1 -1
- package/dist/trade-validator.d.ts +1 -1
- package/package.json +7 -1
- package/skills/perp-cli/SKILL.md +20 -0
- 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
|
+
}
|