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