perp-cli 0.7.1 → 0.7.3
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/dist/arb/sizing.d.ts +1 -0
- package/dist/arb/sizing.js +25 -6
- package/dist/arb/utils.js +1 -0
- package/dist/bot/conditions.js +1 -2
- package/dist/bot/config.d.ts +3 -1
- package/dist/bot/config.js +4 -2
- package/dist/bot/engine.js +63 -18
- package/dist/bot/index.d.ts +1 -1
- package/dist/bot/index.js +1 -1
- package/dist/bot/presets.js +1 -0
- package/dist/bot/strategies/dca-strategy.js +8 -4
- package/dist/bot/strategies/funding-arb-strategy.d.ts +1 -1
- package/dist/bot/strategies/funding-arb-strategy.js +126 -14
- package/dist/bot/strategies/grid-strategy.js +56 -51
- package/dist/bot/tui/BotDashboard.d.ts +7 -0
- package/dist/bot/tui/BotDashboard.js +3 -2
- package/dist/bot/tui/index.d.ts +1 -1
- package/dist/cli-spec.js +1 -1
- package/dist/commands/account.js +1 -1
- package/dist/commands/arb/index.js +12 -4
- package/dist/commands/arb-auto.d.ts +2 -0
- package/dist/commands/arb-auto.js +41 -18
- package/dist/commands/arb.d.ts +1 -0
- package/dist/commands/arb.js +26 -5
- package/dist/commands/bot.js +47 -10
- package/dist/commands/bridge.js +1 -1
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/funds.js +1 -1
- package/dist/commands/history.js +6 -2
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +5 -0
- package/dist/commands/market.js +20 -11
- package/dist/commands/rebalance.js +3 -3
- package/dist/commands/risk.js +1 -1
- package/dist/commands/settings.js +1 -1
- package/dist/commands/trade.js +3 -3
- package/dist/commands/wallet.js +2 -2
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -0
- package/dist/constants.js +1 -0
- package/dist/exchanges/aster.d.ts +4 -0
- package/dist/exchanges/aster.js +39 -28
- package/dist/exchanges/edgex.d.ts +62 -0
- package/dist/exchanges/edgex.js +552 -0
- package/dist/exchanges/index.d.ts +1 -0
- package/dist/exchanges/index.js +1 -0
- package/dist/exchanges/registry.js +5 -0
- package/dist/funding/normalize.js +1 -0
- package/dist/funding/rates.d.ts +1 -1
- package/dist/funding/rates.js +36 -3
- package/dist/index.js +19 -6
- package/dist/strategies/funding-arb.js +28 -5
- package/package.json +1 -1
package/dist/arb/sizing.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { ExchangeAdapter } from "../exchanges/index.js";
|
|
|
8
8
|
export declare function computeMatchedSize(sizeUsd: number, price: number, longExchange: string, shortExchange: string, opts?: {
|
|
9
9
|
longSizeDecimals?: number;
|
|
10
10
|
shortSizeDecimals?: number;
|
|
11
|
+
lotSize?: number;
|
|
11
12
|
}): {
|
|
12
13
|
size: string;
|
|
13
14
|
notional: number;
|
package/dist/arb/sizing.js
CHANGED
|
@@ -10,9 +10,16 @@ export function computeMatchedSize(sizeUsd, price, longExchange, shortExchange,
|
|
|
10
10
|
const rawSize = sizeUsd / price;
|
|
11
11
|
// Use explicitly provided decimals if available, else conservative defaults
|
|
12
12
|
const szDecimals = Math.min(opts?.longSizeDecimals ?? getSizeDecimals(longExchange), opts?.shortSizeDecimals ?? getSizeDecimals(shortExchange));
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
// If lotSize is provided, round to nearest multiple of lotSize (floor)
|
|
14
|
+
let roundedSize;
|
|
15
|
+
if (opts?.lotSize && opts.lotSize > 0) {
|
|
16
|
+
roundedSize = Math.floor(rawSize / opts.lotSize) * opts.lotSize;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
// Round DOWN to the least precise exchange's decimals
|
|
20
|
+
const factor = Math.pow(10, szDecimals);
|
|
21
|
+
roundedSize = Math.floor(rawSize * factor) / factor;
|
|
22
|
+
}
|
|
16
23
|
if (roundedSize <= 0)
|
|
17
24
|
return null;
|
|
18
25
|
// Verify notional meets minimum for both exchanges
|
|
@@ -20,15 +27,27 @@ export function computeMatchedSize(sizeUsd, price, longExchange, shortExchange,
|
|
|
20
27
|
const minNotional = Math.max(getMinNotional(longExchange), getMinNotional(shortExchange));
|
|
21
28
|
if (notional < minNotional) {
|
|
22
29
|
// Try rounding UP instead
|
|
23
|
-
|
|
30
|
+
let roundedUp;
|
|
31
|
+
if (opts?.lotSize && opts.lotSize > 0) {
|
|
32
|
+
roundedUp = Math.ceil(rawSize / opts.lotSize) * opts.lotSize;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const factor = Math.pow(10, szDecimals);
|
|
36
|
+
roundedUp = Math.ceil(rawSize * factor) / factor;
|
|
37
|
+
}
|
|
24
38
|
const notionalUp = roundedUp * price;
|
|
25
39
|
// Only round up if the increase is small (< 20% over requested)
|
|
26
40
|
if (notionalUp <= sizeUsd * 1.2) {
|
|
27
|
-
return { size: roundedUp
|
|
41
|
+
return { size: formatSize(roundedUp, szDecimals, opts?.lotSize), notional: notionalUp };
|
|
28
42
|
}
|
|
29
43
|
return null; // Can't meet minimum
|
|
30
44
|
}
|
|
31
|
-
return { size: roundedSize
|
|
45
|
+
return { size: formatSize(roundedSize, szDecimals, opts?.lotSize), notional };
|
|
46
|
+
}
|
|
47
|
+
function formatSize(size, szDecimals, lotSize) {
|
|
48
|
+
if (lotSize && lotSize >= 1)
|
|
49
|
+
return String(Math.round(size));
|
|
50
|
+
return size.toFixed(szDecimals);
|
|
32
51
|
}
|
|
33
52
|
/** Size decimal precision by exchange (conservative fallbacks — prefer explicit decimals via opts) */
|
|
34
53
|
function getSizeDecimals(exchange) {
|
package/dist/arb/utils.js
CHANGED
|
@@ -7,6 +7,7 @@ export const SETTLEMENT_SCHEDULES = {
|
|
|
7
7
|
hyperliquid: Array.from({ length: 24 }, (_, i) => i), // every hour
|
|
8
8
|
pacifica: Array.from({ length: 24 }, (_, i) => i), // every hour
|
|
9
9
|
lighter: Array.from({ length: 24 }, (_, i) => i), // every hour
|
|
10
|
+
aster: [0, 8, 16], // every 8 hours (Binance convention)
|
|
10
11
|
};
|
|
11
12
|
/**
|
|
12
13
|
* Get the most recent settlement time for an exchange.
|
package/dist/bot/conditions.js
CHANGED
|
@@ -41,7 +41,7 @@ export async function getMarketSnapshot(adapter, symbol) {
|
|
|
41
41
|
const markets = await adapter.getMarkets();
|
|
42
42
|
const m = markets.find(mk => mk.symbol.toUpperCase() === symbol.toUpperCase());
|
|
43
43
|
if (!m) {
|
|
44
|
-
|
|
44
|
+
throw new Error(`Market "${symbol}" not found on ${adapter.name}`);
|
|
45
45
|
}
|
|
46
46
|
const price = parseFloat(m.markPrice);
|
|
47
47
|
const fundingRate = parseFloat(m.fundingRate);
|
|
@@ -124,7 +124,6 @@ export function evaluateCondition(condition, snapshot, context) {
|
|
|
124
124
|
const elapsed = (Date.now() - context.startTime) / 1000;
|
|
125
125
|
return elapsed > val;
|
|
126
126
|
}
|
|
127
|
-
// Custom exit conditions (not standard ConditionType but used in exit logic)
|
|
128
127
|
case "max_drawdown":
|
|
129
128
|
return (context.peakEquity - context.equity) > val;
|
|
130
129
|
default:
|
package/dist/bot/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ConditionType = "price_above" | "price_below" | "volatility_above" | "volatility_below" | "funding_rate_above" | "funding_rate_below" | "spread_above" | "rsi_above" | "rsi_below" | "balance_above" | "balance_below" | "time_after" | "always";
|
|
1
|
+
export type ConditionType = "price_above" | "price_below" | "volatility_above" | "volatility_below" | "funding_rate_above" | "funding_rate_below" | "spread_above" | "rsi_above" | "rsi_below" | "balance_above" | "balance_below" | "time_after" | "max_drawdown" | "always";
|
|
2
2
|
export interface Condition {
|
|
3
3
|
type: ConditionType;
|
|
4
4
|
value: number | string;
|
|
@@ -31,6 +31,7 @@ export interface DCAStrategyParams {
|
|
|
31
31
|
interval_sec: number;
|
|
32
32
|
total_orders: number;
|
|
33
33
|
price_limit?: number;
|
|
34
|
+
side?: "buy" | "sell";
|
|
34
35
|
}
|
|
35
36
|
export interface FundingArbStrategyParams {
|
|
36
37
|
type: "funding-arb";
|
|
@@ -56,6 +57,7 @@ export interface BotConfig {
|
|
|
56
57
|
monitor_interval_sec: number;
|
|
57
58
|
}
|
|
58
59
|
export declare function loadBotConfig(pathOrInline: string): BotConfig;
|
|
60
|
+
export declare function parseStrategy(type: string, raw: Record<string, unknown>): StrategyParams;
|
|
59
61
|
export declare function quickGridConfig(opts: {
|
|
60
62
|
exchange: string;
|
|
61
63
|
symbol: string;
|
package/dist/bot/config.js
CHANGED
|
@@ -53,7 +53,7 @@ function parseBotConfig(raw) {
|
|
|
53
53
|
};
|
|
54
54
|
return { name, exchange, symbol, strategy, entry_conditions: entryConds, exit_conditions: exitConds, risk, monitor_interval_sec: monitorInterval };
|
|
55
55
|
}
|
|
56
|
-
function parseStrategy(type, raw) {
|
|
56
|
+
export function parseStrategy(type, raw) {
|
|
57
57
|
switch (type) {
|
|
58
58
|
case "grid":
|
|
59
59
|
return {
|
|
@@ -65,7 +65,7 @@ function parseStrategy(type, raw) {
|
|
|
65
65
|
range_pct: raw.range_pct !== undefined ? Number(raw.range_pct) : 3,
|
|
66
66
|
upper: raw.upper !== undefined ? Number(raw.upper) : undefined,
|
|
67
67
|
lower: raw.lower !== undefined ? Number(raw.lower) : undefined,
|
|
68
|
-
rebalance: raw.rebalance !== false,
|
|
68
|
+
rebalance: raw.rebalance !== false && raw.rebalance !== "false",
|
|
69
69
|
rebalance_cooldown: Number(raw.rebalance_cooldown ?? 60),
|
|
70
70
|
leverage: raw.leverage !== undefined ? Number(raw.leverage) : undefined,
|
|
71
71
|
};
|
|
@@ -76,6 +76,7 @@ function parseStrategy(type, raw) {
|
|
|
76
76
|
interval_sec: Number(raw.interval_sec ?? raw.interval ?? 60),
|
|
77
77
|
total_orders: Number(raw.total_orders ?? raw.orders ?? 0),
|
|
78
78
|
price_limit: raw.price_limit !== undefined ? Number(raw.price_limit) : undefined,
|
|
79
|
+
side: raw.side !== undefined ? String(raw.side) : undefined,
|
|
79
80
|
};
|
|
80
81
|
case "funding-arb":
|
|
81
82
|
return {
|
|
@@ -149,6 +150,7 @@ export function quickDCAConfig(opts) {
|
|
|
149
150
|
interval_sec: opts.intervalSec,
|
|
150
151
|
total_orders: opts.orders,
|
|
151
152
|
price_limit: opts.priceLimit,
|
|
153
|
+
side: opts.side ?? "buy",
|
|
152
154
|
},
|
|
153
155
|
entry_conditions: entryConds,
|
|
154
156
|
exit_conditions: [
|
package/dist/bot/engine.js
CHANGED
|
@@ -56,9 +56,11 @@ mode = "headless") {
|
|
|
56
56
|
dailyStartDate: new Date().toISOString().slice(0, 10),
|
|
57
57
|
fills: 0,
|
|
58
58
|
totalPnl: 0,
|
|
59
|
+
initialEquity: 0,
|
|
59
60
|
rebalanceCount: 0,
|
|
60
61
|
lastRebalance: 0,
|
|
61
62
|
strategyActive: false,
|
|
63
|
+
riskBreached: false,
|
|
62
64
|
};
|
|
63
65
|
// Resolve strategy from registry
|
|
64
66
|
const strategyFactory = getStrategy(config.strategy.type);
|
|
@@ -84,7 +86,7 @@ mode = "headless") {
|
|
|
84
86
|
let tuiUnmount;
|
|
85
87
|
let paused = false;
|
|
86
88
|
// Wrap log for TUI/JSON modes
|
|
87
|
-
|
|
89
|
+
let effectiveLog = mode === "json"
|
|
88
90
|
? () => { } // JSON mode suppresses console log; NDJSON emitted per tick
|
|
89
91
|
: mode === "tui"
|
|
90
92
|
? (msg) => tuiEmitter.emitLog(msg)
|
|
@@ -105,7 +107,7 @@ mode = "headless") {
|
|
|
105
107
|
}
|
|
106
108
|
catch {
|
|
107
109
|
// Fallback to headless if TUI fails (e.g., non-TTY)
|
|
108
|
-
|
|
110
|
+
effectiveLog = log;
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
const shutdown = () => { running = false; state.phase = "stopped"; };
|
|
@@ -130,6 +132,7 @@ mode = "headless") {
|
|
|
130
132
|
state.equity = parseFloat(bal.equity);
|
|
131
133
|
state.peakEquity = state.equity;
|
|
132
134
|
state.dailyStartEquity = state.equity;
|
|
135
|
+
state.initialEquity = state.equity;
|
|
133
136
|
effectiveLog(` Starting equity: $${state.equity.toFixed(2)}`);
|
|
134
137
|
}
|
|
135
138
|
catch {
|
|
@@ -162,6 +165,7 @@ mode = "headless") {
|
|
|
162
165
|
state.dailyStartDate = today;
|
|
163
166
|
}
|
|
164
167
|
state.dailyPnl = state.equity - state.dailyStartEquity;
|
|
168
|
+
state.totalPnl = state.equity - state.initialEquity;
|
|
165
169
|
}
|
|
166
170
|
catch { /* non-critical */ }
|
|
167
171
|
const context = {
|
|
@@ -217,6 +221,7 @@ mode = "headless") {
|
|
|
217
221
|
? `drawdown $${drawdown.toFixed(2)} > limit $${config.risk.max_drawdown}`
|
|
218
222
|
: "exit condition met";
|
|
219
223
|
effectiveLog(` Exiting: ${reason}`);
|
|
224
|
+
state.riskBreached = riskBreached;
|
|
220
225
|
state.phase = "exiting";
|
|
221
226
|
}
|
|
222
227
|
else {
|
|
@@ -237,19 +242,29 @@ mode = "headless") {
|
|
|
237
242
|
effectiveLog(` Pausing ${config.risk.pause_after_loss_sec}s after loss`);
|
|
238
243
|
state.phase = "paused";
|
|
239
244
|
await sleep(config.risk.pause_after_loss_sec * 1000);
|
|
245
|
+
// Data will be refreshed on the next loop iteration (re-fetches snapshot + equity)
|
|
240
246
|
state.phase = "monitoring";
|
|
241
247
|
effectiveLog(` Resuming monitoring...`);
|
|
242
248
|
}
|
|
243
|
-
else {
|
|
244
|
-
//
|
|
249
|
+
else if (state.riskBreached) {
|
|
250
|
+
// Risk breach (drawdown or daily loss exceeded) — stop completely
|
|
245
251
|
effectiveLog(` Bot stopped.`);
|
|
246
252
|
break;
|
|
247
253
|
}
|
|
254
|
+
else {
|
|
255
|
+
// Normal exit condition (not risk breach) — return to monitoring
|
|
256
|
+
state.phase = "monitoring";
|
|
257
|
+
state.riskBreached = false;
|
|
258
|
+
effectiveLog(` Exit condition met, returning to monitoring...`);
|
|
259
|
+
}
|
|
248
260
|
}
|
|
249
261
|
// ── Emit state update for TUI / JSON ──
|
|
250
|
-
const positions = await
|
|
251
|
-
|
|
252
|
-
|
|
262
|
+
const [positions, openOrders, exchangeBalances] = await Promise.all([
|
|
263
|
+
extraAdapters ? fetchAllPositions(adapter, config.symbol, extraAdapters) : fetchPositions(adapter, config.symbol, adapter.name),
|
|
264
|
+
fetchOpenOrders(adapter, config.symbol),
|
|
265
|
+
extraAdapters ? fetchExchangeBalances(adapter, extraAdapters) : Promise.resolve(undefined),
|
|
266
|
+
]);
|
|
267
|
+
const tuiState = buildTuiState(state, config, snapshot, positions, openOrders, strategyCtx, exchangeBalances);
|
|
253
268
|
if (mode === "tui") {
|
|
254
269
|
tuiEmitter.emitState(tuiState);
|
|
255
270
|
}
|
|
@@ -351,18 +366,16 @@ async function executeActions(adapter, symbol, actions, log, dryRun = false) {
|
|
|
351
366
|
// ── Enriched snapshot ──
|
|
352
367
|
async function getEnrichedSnapshot(adapter, symbol) {
|
|
353
368
|
const base = await getMarketSnapshot(adapter, symbol);
|
|
354
|
-
const [orderbook, klines
|
|
369
|
+
const [orderbook, klines] = await Promise.all([
|
|
355
370
|
adapter.getOrderbook(symbol).catch(() => ({ bids: [], asks: [] })),
|
|
356
371
|
adapter.getKlines(symbol, "1h", Date.now() - 24 * 60 * 60 * 1000, Date.now()).catch(() => []),
|
|
357
|
-
adapter.getMarkets().catch(() => []),
|
|
358
372
|
]);
|
|
359
|
-
|
|
360
|
-
return { ...base, klines, orderbook, openInterest: market?.openInterest ?? "0" };
|
|
373
|
+
return { ...base, klines, orderbook, openInterest: "0" };
|
|
361
374
|
}
|
|
362
375
|
// ── Utils ──
|
|
363
|
-
/** Count fill-producing actions (
|
|
376
|
+
/** Count fill-producing actions (all order placements) */
|
|
364
377
|
function countFills(actions) {
|
|
365
|
-
return actions.filter(a => a.type === "place_order"
|
|
378
|
+
return actions.filter(a => a.type === "place_order").length;
|
|
366
379
|
}
|
|
367
380
|
function logStatus(log, state, snapshot, phase) {
|
|
368
381
|
const runtime = formatDuration(Math.floor((Date.now() - state.startTime) / 1000));
|
|
@@ -390,11 +403,12 @@ function sleep(ms) {
|
|
|
390
403
|
return new Promise(r => setTimeout(r, ms));
|
|
391
404
|
}
|
|
392
405
|
// ── TUI helpers ──
|
|
393
|
-
async function fetchPositions(adapter, symbol) {
|
|
406
|
+
async function fetchPositions(adapter, symbol, exchangeName) {
|
|
394
407
|
try {
|
|
395
408
|
const positions = await adapter.getPositions();
|
|
409
|
+
const isAll = symbol.toUpperCase() === "ALL";
|
|
396
410
|
return positions
|
|
397
|
-
.filter(p => p.symbol.toUpperCase().
|
|
411
|
+
.filter(p => (isAll || p.symbol.toUpperCase() === symbol.toUpperCase() || p.symbol.toUpperCase() === symbol.toUpperCase() + "-PERP") && parseFloat(p.size) !== 0)
|
|
398
412
|
.map(p => ({
|
|
399
413
|
symbol: p.symbol,
|
|
400
414
|
side: p.side,
|
|
@@ -402,17 +416,47 @@ async function fetchPositions(adapter, symbol) {
|
|
|
402
416
|
entryPrice: p.entryPrice,
|
|
403
417
|
markPrice: p.markPrice,
|
|
404
418
|
unrealizedPnl: p.unrealizedPnl,
|
|
419
|
+
exchange: exchangeName ?? adapter.name,
|
|
405
420
|
}));
|
|
406
421
|
}
|
|
407
422
|
catch {
|
|
408
423
|
return [];
|
|
409
424
|
}
|
|
410
425
|
}
|
|
426
|
+
async function fetchExchangeBalances(primaryAdapter, extraAdapters) {
|
|
427
|
+
const adapters = [[primaryAdapter.name, primaryAdapter]];
|
|
428
|
+
if (extraAdapters) {
|
|
429
|
+
for (const [name, a] of extraAdapters)
|
|
430
|
+
adapters.push([name, a]);
|
|
431
|
+
}
|
|
432
|
+
const results = [];
|
|
433
|
+
await Promise.all(adapters.map(async ([name, a]) => {
|
|
434
|
+
try {
|
|
435
|
+
const bal = await a.getBalance();
|
|
436
|
+
results.push({ exchange: name, equity: bal.equity, available: bal.available });
|
|
437
|
+
}
|
|
438
|
+
catch { /* skip */ }
|
|
439
|
+
}));
|
|
440
|
+
return results;
|
|
441
|
+
}
|
|
442
|
+
async function fetchAllPositions(primaryAdapter, symbol, extraAdapters) {
|
|
443
|
+
const adapters = [[primaryAdapter.name, primaryAdapter]];
|
|
444
|
+
if (extraAdapters) {
|
|
445
|
+
for (const [name, a] of extraAdapters)
|
|
446
|
+
adapters.push([name, a]);
|
|
447
|
+
}
|
|
448
|
+
const all = [];
|
|
449
|
+
await Promise.all(adapters.map(async ([name, a]) => {
|
|
450
|
+
const pos = await fetchPositions(a, symbol, name);
|
|
451
|
+
all.push(pos);
|
|
452
|
+
}));
|
|
453
|
+
return all.flat();
|
|
454
|
+
}
|
|
411
455
|
async function fetchOpenOrders(adapter, symbol) {
|
|
412
456
|
try {
|
|
413
457
|
const orders = await adapter.getOpenOrders();
|
|
414
458
|
return orders
|
|
415
|
-
.filter(o => o.symbol.toUpperCase().
|
|
459
|
+
.filter(o => o.symbol.toUpperCase() === symbol.toUpperCase() || o.symbol.toUpperCase() === symbol.toUpperCase() + "-PERP")
|
|
416
460
|
.map(o => ({
|
|
417
461
|
orderId: o.orderId,
|
|
418
462
|
side: o.side,
|
|
@@ -425,7 +469,7 @@ async function fetchOpenOrders(adapter, symbol) {
|
|
|
425
469
|
return [];
|
|
426
470
|
}
|
|
427
471
|
}
|
|
428
|
-
function buildTuiState(state, config, snapshot, positions, openOrders, strategyCtx) {
|
|
472
|
+
function buildTuiState(state, config, snapshot, positions, openOrders, strategyCtx, exchangeBalances) {
|
|
429
473
|
// Extract serializable strategy state from the Map
|
|
430
474
|
const strategyState = {};
|
|
431
475
|
for (const [k, v] of strategyCtx.state) {
|
|
@@ -453,12 +497,13 @@ function buildTuiState(state, config, snapshot, positions, openOrders, strategyC
|
|
|
453
497
|
exchange: config.exchange,
|
|
454
498
|
price: snapshot.price,
|
|
455
499
|
volume24h: snapshot.volume24h ?? 0,
|
|
456
|
-
openInterest: "0",
|
|
500
|
+
openInterest: snapshot.openInterest ?? "0",
|
|
457
501
|
fundingRate: snapshot.fundingRate,
|
|
458
502
|
volatility24h: snapshot.volatility24h,
|
|
459
503
|
positions,
|
|
460
504
|
openOrders,
|
|
461
505
|
strategyState,
|
|
462
506
|
tick: strategyCtx.tick,
|
|
507
|
+
exchangeBalances,
|
|
463
508
|
};
|
|
464
509
|
}
|
package/dist/bot/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { loadBotConfig, quickGridConfig, quickDCAConfig, } from "./config.js";
|
|
1
|
+
export { loadBotConfig, parseStrategy, quickGridConfig, quickDCAConfig, } from "./config.js";
|
|
2
2
|
export type { ConditionType, Condition, RiskConfig, GridStrategyParams, DCAStrategyParams, FundingArbStrategyParams, GenericStrategyParams, StrategyParams, BotConfig, } from "./config.js";
|
|
3
3
|
export { runBot } from "./engine.js";
|
|
4
4
|
export type { BotLog, BotOutputMode } from "./engine.js";
|
package/dist/bot/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// bot/ barrel — public API
|
|
2
|
-
export { loadBotConfig, quickGridConfig, quickDCAConfig, } from "./config.js";
|
|
2
|
+
export { loadBotConfig, parseStrategy, quickGridConfig, quickDCAConfig, } from "./config.js";
|
|
3
3
|
export { runBot } from "./engine.js";
|
|
4
4
|
export { PRESETS, getPreset, getPresetsByStrategy } from "./presets.js";
|
|
5
5
|
export { evaluateCondition, evaluateAllConditions, getMarketSnapshot, calculateRSI } from "./conditions.js";
|
package/dist/bot/presets.js
CHANGED
|
@@ -34,19 +34,23 @@ export class DCAStrategy {
|
|
|
34
34
|
// Check order limit
|
|
35
35
|
if (params.total_orders > 0 && dcaOrdersPlaced >= params.total_orders)
|
|
36
36
|
return [];
|
|
37
|
-
// Check price limit
|
|
37
|
+
// Check price limit (buy: skip if price too high, sell: skip if price too low)
|
|
38
38
|
if (params.price_limit) {
|
|
39
|
-
|
|
39
|
+
const side = params.side ?? "buy";
|
|
40
|
+
if (side === "buy" && snapshot.price > params.price_limit)
|
|
41
|
+
return [];
|
|
42
|
+
if (side === "sell" && snapshot.price < params.price_limit)
|
|
40
43
|
return [];
|
|
41
44
|
}
|
|
42
45
|
// Place market buy order
|
|
43
46
|
ctx.state.set("dcaOrdersPlaced", dcaOrdersPlaced + 1);
|
|
44
47
|
ctx.state.set("dcaLastOrder", Date.now());
|
|
48
|
+
const side = params.side ?? "buy";
|
|
45
49
|
const progress = params.total_orders > 0 ? ` (${dcaOrdersPlaced + 1}/${params.total_orders})` : "";
|
|
46
|
-
ctx.log(` [DCA] Order #${dcaOrdersPlaced + 1}${progress}:
|
|
50
|
+
ctx.log(` [DCA] Order #${dcaOrdersPlaced + 1}${progress}: ${side} ${params.amount} ${ctx.symbol} @ $${snapshot.price.toFixed(2)}`);
|
|
47
51
|
return [{
|
|
48
52
|
type: "place_order",
|
|
49
|
-
side
|
|
53
|
+
side,
|
|
50
54
|
price: "0",
|
|
51
55
|
size: String(params.amount),
|
|
52
56
|
orderType: "market",
|
|
@@ -27,6 +27,6 @@ export declare class FundingArbStrategy implements Strategy {
|
|
|
27
27
|
private _config;
|
|
28
28
|
init(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<void>;
|
|
29
29
|
onTick(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
|
|
30
|
-
onStop(
|
|
30
|
+
onStop(ctx: StrategyContext): Promise<StrategyAction[]>;
|
|
31
31
|
private fetchRates;
|
|
32
32
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { registerStrategy } from "../strategy-registry.js";
|
|
2
2
|
import { checkArbLiquidity } from "../../liquidity.js";
|
|
3
3
|
import { computeMatchedSize, reconcileArbFills } from "../../arb-sizing.js";
|
|
4
|
+
import { computeAnnualSpread } from "../../funding.js";
|
|
4
5
|
export class FundingArbStrategy {
|
|
5
6
|
name = "funding-arb";
|
|
6
7
|
describe() {
|
|
@@ -23,13 +24,14 @@ export class FundingArbStrategy {
|
|
|
23
24
|
this._config = ctx.config;
|
|
24
25
|
const params = this.params;
|
|
25
26
|
ctx.state.set("arbRunning", true);
|
|
27
|
+
ctx.state.set("arbOpenPositions", []);
|
|
26
28
|
ctx.state.set("arbPositions", 0);
|
|
27
|
-
ctx.log(` [ARB] Funding arb ready | spread >= ${params.min_spread}% | size $${params.size_usd}`);
|
|
29
|
+
ctx.log(` [ARB] Funding arb ready | spread >= ${params.min_spread}% | close < ${params.close_spread}% | size $${params.size_usd}`);
|
|
28
30
|
ctx.log(` [ARB] Exchanges: ${params.exchanges.join(", ")}`);
|
|
29
31
|
}
|
|
30
32
|
async onTick(ctx, _snapshot) {
|
|
31
33
|
const params = this.params;
|
|
32
|
-
const
|
|
34
|
+
const openPositions = ctx.state.get("arbOpenPositions");
|
|
33
35
|
// Get extra adapters from context state (set by engine)
|
|
34
36
|
const extraAdapters = ctx.state.get("extraAdapters");
|
|
35
37
|
// Build adapter map from primary + extras
|
|
@@ -52,7 +54,42 @@ export class FundingArbStrategy {
|
|
|
52
54
|
map.set(r.symbol.toUpperCase(), { rate: r.rate, price: r.price });
|
|
53
55
|
ratesByExchange.set(name, map);
|
|
54
56
|
}
|
|
55
|
-
//
|
|
57
|
+
// ── Check existing positions for close_spread ──
|
|
58
|
+
const toClose = [];
|
|
59
|
+
for (const pos of openPositions) {
|
|
60
|
+
const longRate = ratesByExchange.get(pos.longExchange)?.get(pos.symbol)?.rate;
|
|
61
|
+
const shortRate = ratesByExchange.get(pos.shortExchange)?.get(pos.symbol)?.rate;
|
|
62
|
+
if (longRate === undefined || shortRate === undefined)
|
|
63
|
+
continue;
|
|
64
|
+
const currentSpread = computeAnnualSpread(shortRate, pos.shortExchange, longRate, pos.longExchange);
|
|
65
|
+
if (currentSpread < params.close_spread) {
|
|
66
|
+
ctx.log(` [ARB] Closing ${pos.symbol}: spread ${currentSpread.toFixed(1)}% < close_spread ${params.close_spread}% (entry: ${pos.entrySpread.toFixed(1)}%)`);
|
|
67
|
+
const longAdapter = adapters.get(pos.longExchange);
|
|
68
|
+
const shortAdapter = adapters.get(pos.shortExchange);
|
|
69
|
+
if (longAdapter && shortAdapter) {
|
|
70
|
+
try {
|
|
71
|
+
await Promise.all([
|
|
72
|
+
longAdapter.marketOrder(pos.symbol, "sell", pos.size),
|
|
73
|
+
shortAdapter.marketOrder(pos.symbol, "buy", pos.size),
|
|
74
|
+
]);
|
|
75
|
+
toClose.push(pos);
|
|
76
|
+
ctx.log(` [ARB] Closed ${pos.symbol} position`);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
80
|
+
ctx.log(` [ARB] Close failed for ${pos.symbol}: ${msg}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Remove closed positions
|
|
86
|
+
if (toClose.length > 0) {
|
|
87
|
+
const remaining = openPositions.filter(p => !toClose.includes(p));
|
|
88
|
+
ctx.state.set("arbOpenPositions", remaining);
|
|
89
|
+
ctx.state.set("arbPositions", remaining.length);
|
|
90
|
+
}
|
|
91
|
+
// ── Find new opportunities ──
|
|
92
|
+
const arbPositions = ctx.state.get("arbOpenPositions").length;
|
|
56
93
|
const exchangeNames = [...ratesByExchange.keys()];
|
|
57
94
|
const opportunities = [];
|
|
58
95
|
const allSymbols = new Set();
|
|
@@ -77,8 +114,7 @@ export class FundingArbStrategy {
|
|
|
77
114
|
}
|
|
78
115
|
}
|
|
79
116
|
if (minExchange && maxExchange && minExchange !== maxExchange) {
|
|
80
|
-
const
|
|
81
|
-
const spread = cas(maxRate, maxExchange, minRate, minExchange);
|
|
117
|
+
const spread = computeAnnualSpread(maxRate, maxExchange, minRate, minExchange);
|
|
82
118
|
if (spread >= params.min_spread) {
|
|
83
119
|
opportunities.push({
|
|
84
120
|
symbol: sym,
|
|
@@ -110,17 +146,41 @@ export class FundingArbStrategy {
|
|
|
110
146
|
if (!liq.viable)
|
|
111
147
|
return [];
|
|
112
148
|
// Compute matched size
|
|
113
|
-
|
|
149
|
+
let matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange);
|
|
114
150
|
if (!matched) {
|
|
115
151
|
ctx.log(` [ARB] Skip ${best.symbol}: can't compute matched size (min notional or precision issue)`);
|
|
116
152
|
return [];
|
|
117
153
|
}
|
|
118
154
|
try {
|
|
119
155
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
156
|
+
try {
|
|
157
|
+
await Promise.all([
|
|
158
|
+
longAdapter.marketOrder(best.symbol, "buy", matched.size),
|
|
159
|
+
shortAdapter.marketOrder(best.symbol, "sell", matched.size),
|
|
160
|
+
]);
|
|
161
|
+
}
|
|
162
|
+
catch (orderErr) {
|
|
163
|
+
// Parse lot size from error and retry with corrected size
|
|
164
|
+
const errMsg = orderErr instanceof Error ? orderErr.message : String(orderErr);
|
|
165
|
+
const lotMatch = errMsg.match(/not a multiple of lot size (\d+(?:\.\d+)?)/);
|
|
166
|
+
if (lotMatch) {
|
|
167
|
+
const lotSize = parseFloat(lotMatch[1]);
|
|
168
|
+
ctx.log(` [ARB] Lot size ${lotSize} detected, recalculating...`);
|
|
169
|
+
matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange, { lotSize });
|
|
170
|
+
if (!matched) {
|
|
171
|
+
ctx.log(` [ARB] Skip ${best.symbol}: can't meet lot size ${lotSize}`);
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
ctx.log(` [ARB] Retry: ${matched.size} ${best.symbol} ($${matched.notional.toFixed(0)}/leg)`);
|
|
175
|
+
await Promise.all([
|
|
176
|
+
longAdapter.marketOrder(best.symbol, "buy", matched.size),
|
|
177
|
+
shortAdapter.marketOrder(best.symbol, "sell", matched.size),
|
|
178
|
+
]);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
throw orderErr;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
124
184
|
// Verify fills match, correct if needed
|
|
125
185
|
try {
|
|
126
186
|
const recon = await reconcileArbFills(longAdapter, shortAdapter, best.symbol, (msg) => ctx.log(` ${msg}`));
|
|
@@ -128,9 +188,22 @@ export class FundingArbStrategy {
|
|
|
128
188
|
ctx.log(` [ARB] WARNING: fills not matched after correction attempt`);
|
|
129
189
|
}
|
|
130
190
|
}
|
|
131
|
-
catch {
|
|
132
|
-
|
|
133
|
-
|
|
191
|
+
catch (reconErr) {
|
|
192
|
+
const reconMsg = reconErr instanceof Error ? reconErr.message : String(reconErr);
|
|
193
|
+
ctx.log(` [ARB] WARNING: Fill reconciliation failed: ${reconMsg}`);
|
|
194
|
+
}
|
|
195
|
+
// Track position in array
|
|
196
|
+
const currentPositions = ctx.state.get("arbOpenPositions");
|
|
197
|
+
currentPositions.push({
|
|
198
|
+
symbol: best.symbol,
|
|
199
|
+
longExchange: best.longExchange,
|
|
200
|
+
shortExchange: best.shortExchange,
|
|
201
|
+
entrySpread: best.spread,
|
|
202
|
+
size: matched.size,
|
|
203
|
+
});
|
|
204
|
+
ctx.state.set("arbOpenPositions", currentPositions);
|
|
205
|
+
ctx.state.set("arbPositions", currentPositions.length);
|
|
206
|
+
ctx.log(` [ARB] Position opened! (${currentPositions.length}/${params.max_positions})`);
|
|
134
207
|
// Return noop -- we already executed directly via adapters
|
|
135
208
|
// (arb requires multi-adapter coordination, can't be expressed as single-adapter actions)
|
|
136
209
|
return [{ type: "noop" }];
|
|
@@ -148,7 +221,46 @@ export class FundingArbStrategy {
|
|
|
148
221
|
}
|
|
149
222
|
return [];
|
|
150
223
|
}
|
|
151
|
-
async onStop(
|
|
224
|
+
async onStop(ctx) {
|
|
225
|
+
const openPositions = ctx.state.get("arbOpenPositions");
|
|
226
|
+
const extraAdapters = ctx.state.get("extraAdapters");
|
|
227
|
+
// Close all open arb positions
|
|
228
|
+
if (openPositions && openPositions.length > 0) {
|
|
229
|
+
const adapters = new Map();
|
|
230
|
+
adapters.set(ctx.adapter.name.toLowerCase(), ctx.adapter);
|
|
231
|
+
if (extraAdapters) {
|
|
232
|
+
for (const [name, a] of extraAdapters)
|
|
233
|
+
adapters.set(name, a);
|
|
234
|
+
}
|
|
235
|
+
for (const pos of openPositions) {
|
|
236
|
+
const longAdapter = adapters.get(pos.longExchange);
|
|
237
|
+
const shortAdapter = adapters.get(pos.shortExchange);
|
|
238
|
+
if (longAdapter && shortAdapter) {
|
|
239
|
+
try {
|
|
240
|
+
ctx.log(` [ARB] Closing ${pos.symbol} on stop (${pos.size})`);
|
|
241
|
+
await Promise.all([
|
|
242
|
+
longAdapter.marketOrder(pos.symbol, "sell", pos.size),
|
|
243
|
+
shortAdapter.marketOrder(pos.symbol, "buy", pos.size),
|
|
244
|
+
]);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
248
|
+
ctx.log(` [ARB] Close on stop failed for ${pos.symbol}: ${msg}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
ctx.state.set("arbOpenPositions", []);
|
|
253
|
+
ctx.state.set("arbPositions", 0);
|
|
254
|
+
}
|
|
255
|
+
// Cancel on extra adapters directly (M2: was only cancelling on primary adapter)
|
|
256
|
+
if (extraAdapters) {
|
|
257
|
+
for (const [, a] of extraAdapters) {
|
|
258
|
+
try {
|
|
259
|
+
await a.cancelAllOrders();
|
|
260
|
+
}
|
|
261
|
+
catch { /* best effort */ }
|
|
262
|
+
}
|
|
263
|
+
}
|
|
152
264
|
return [{ type: "cancel_all" }];
|
|
153
265
|
}
|
|
154
266
|
async fetchRates(adapter, _exchangeName) {
|