perp-cli 0.7.50 → 0.9.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/dist/bot/engine.d.ts +1 -0
- package/dist/bot/engine.js +1 -0
- package/dist/bot/strategies/funding-arb-strategy.js +2 -2
- package/dist/bot/strategies/funding-arb-utils.d.ts +49 -0
- package/dist/bot/strategies/funding-arb-utils.js +239 -0
- package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +42 -0
- package/dist/bot/strategies/funding-arb-v2-strategy.js +873 -0
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +43 -0
- package/dist/bot/strategies/spot-perp-arb-strategy.js +547 -0
- package/dist/bot/strategies/spot-perp-executor.d.ts +81 -0
- package/dist/bot/strategies/spot-perp-executor.js +271 -0
- package/dist/commands/bot.js +80 -1
- package/dist/commands/trade.js +131 -0
- package/dist/exchanges/aster.d.ts +1 -1
- package/dist/exchanges/aster.js +5 -5
- package/dist/exchanges/hyperliquid.d.ts +7 -0
- package/dist/exchanges/hyperliquid.js +23 -0
- package/dist/exchanges/lighter-spot.js +20 -0
- package/dist/exchanges/lighter.js +45 -1
- package/dist/exchanges/pacifica.js +42 -1
- package/dist/exchanges/spot-interface.js +4 -0
- package/package.json +1 -1
package/dist/bot/engine.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ import "./strategies/rfq-agent.js";
|
|
|
19
19
|
import "./strategies/claude-agent.js";
|
|
20
20
|
import "./strategies/apex-strategy.js";
|
|
21
21
|
import "./strategies/funding-auto.js";
|
|
22
|
+
import "./strategies/spot-perp-arb-strategy.js";
|
|
22
23
|
export type BotLog = (msg: string) => void;
|
|
23
24
|
export type BotOutputMode = "tui" | "json" | "headless";
|
|
24
25
|
export declare function runBot(adapter: ExchangeAdapter, config: BotConfig, jobId?: string, log?: BotLog, extraAdapters?: Map<string, ExchangeAdapter>, // for funding-arb (multi-exchange)
|
package/dist/bot/engine.js
CHANGED
|
@@ -22,6 +22,7 @@ import "./strategies/rfq-agent.js";
|
|
|
22
22
|
import "./strategies/claude-agent.js";
|
|
23
23
|
import "./strategies/apex-strategy.js";
|
|
24
24
|
import "./strategies/funding-auto.js";
|
|
25
|
+
import "./strategies/spot-perp-arb-strategy.js";
|
|
25
26
|
// ── TUI state emitter ──
|
|
26
27
|
class TuiEmitter {
|
|
27
28
|
stateListeners = [];
|
|
@@ -324,8 +324,8 @@ export class FundingArbStrategy {
|
|
|
324
324
|
// Close if funding flipped (losing money) or spread below close threshold
|
|
325
325
|
const minHoldMs = (params.min_hold_hours ?? 2) * 60 * 60 * 1000;
|
|
326
326
|
const holdTime = pos.entryTime > 0 ? Date.now() - pos.entryTime : Infinity;
|
|
327
|
-
const
|
|
328
|
-
const shouldClose =
|
|
327
|
+
const deeplyNegative = signedSpread < -20; // urgent: losing significant funding
|
|
328
|
+
const shouldClose = deeplyNegative || (holdTime >= minHoldMs && signedSpread < params.close_spread);
|
|
329
329
|
if (shouldClose) {
|
|
330
330
|
// Cooldown: skip if close recently failed for this symbol
|
|
331
331
|
const closeKey = `${pos.symbol}:close`;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for funding arbitrage strategies (v1 + v2).
|
|
3
|
+
* Extracted to keep individual strategy files under size limits.
|
|
4
|
+
*/
|
|
5
|
+
import type { StrategyContext } from "../strategy-types.js";
|
|
6
|
+
import type { ExchangeAdapter } from "../../exchanges/index.js";
|
|
7
|
+
import type { SpotAdapter } from "../../exchanges/spot-interface.js";
|
|
8
|
+
export type RateEntry = {
|
|
9
|
+
rate: number;
|
|
10
|
+
price: number;
|
|
11
|
+
sizeDecimals?: number;
|
|
12
|
+
maxLeverage?: number;
|
|
13
|
+
fundingHours?: number;
|
|
14
|
+
};
|
|
15
|
+
export type RateMap = Map<string, Map<string, RateEntry>>;
|
|
16
|
+
/** Build adapter map from primary adapter + extraAdapters in context state. */
|
|
17
|
+
export declare function buildAdapterMap(ctx: StrategyContext): Map<string, ExchangeAdapter>;
|
|
18
|
+
/** Get or create a spot adapter for a given exchange name + perp adapter instance. */
|
|
19
|
+
export declare function getSpotAdapter(name: string, adapter: ExchangeAdapter): Promise<SpotAdapter | null>;
|
|
20
|
+
/** Transfer USDC from perp to spot account (exchange-specific). */
|
|
21
|
+
export declare function transferUsdcToSpot(spotAdapter: SpotAdapter, exchangeName: string, amount: number): Promise<void>;
|
|
22
|
+
/** Transfer USDC from spot to perp account (exchange-specific). */
|
|
23
|
+
export declare function transferUsdcToPerp(spotAdapter: SpotAdapter, exchangeName: string, amount: number): Promise<void>;
|
|
24
|
+
/** Resolve perp symbol for a given base symbol on an exchange. */
|
|
25
|
+
export declare function getPerpSymbol(baseSymbol: string, _exchangeName: string): string;
|
|
26
|
+
/** Look up rate from ratesByExchange, trying symbol, symbol-PERP, and symbol without -PERP. */
|
|
27
|
+
export declare function findRate(ratesByExchange: RateMap, exchange: string, symbol: string): RateEntry | undefined;
|
|
28
|
+
/** Match a symbol against a target, accounting for -PERP suffix variations. */
|
|
29
|
+
export declare function matchSymbol(s: string, target: string): boolean;
|
|
30
|
+
/** Get a price estimate for a symbol from the perp adapter. */
|
|
31
|
+
export declare function getPriceEstimate(perpAdapter: ExchangeAdapter, perpSymbol: string, fallbackSymbol: string): Promise<number>;
|
|
32
|
+
export interface RecoveredPosition {
|
|
33
|
+
symbol: string;
|
|
34
|
+
mode: "spot-perp" | "perp-perp";
|
|
35
|
+
longExchange: string;
|
|
36
|
+
shortExchange: string;
|
|
37
|
+
size: string;
|
|
38
|
+
}
|
|
39
|
+
/** Recover arb positions from exchange state (perp-perp + spot-perp, same + cross exchange). */
|
|
40
|
+
export declare function recoverArbPositions(adapters: Map<string, ExchangeAdapter>, log: (msg: string) => void): Promise<RecoveredPosition[]>;
|
|
41
|
+
/** Fetch funding rates from an exchange adapter. */
|
|
42
|
+
export declare function fetchRates(adapter: ExchangeAdapter, exchangeName: string): Promise<{
|
|
43
|
+
symbol: string;
|
|
44
|
+
rate: number;
|
|
45
|
+
price: number;
|
|
46
|
+
sizeDecimals?: number;
|
|
47
|
+
maxLeverage?: number;
|
|
48
|
+
fundingHours?: number;
|
|
49
|
+
}[]>;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for funding arbitrage strategies (v1 + v2).
|
|
3
|
+
* Extracted to keep individual strategy files under size limits.
|
|
4
|
+
*/
|
|
5
|
+
// ── Adapter helpers ──
|
|
6
|
+
/** Build adapter map from primary adapter + extraAdapters in context state. */
|
|
7
|
+
export function buildAdapterMap(ctx) {
|
|
8
|
+
const extraAdapters = ctx.state.get("extraAdapters");
|
|
9
|
+
const adapters = new Map();
|
|
10
|
+
adapters.set(ctx.adapter.name.toLowerCase(), ctx.adapter);
|
|
11
|
+
if (extraAdapters) {
|
|
12
|
+
for (const [name, a] of extraAdapters)
|
|
13
|
+
adapters.set(name, a);
|
|
14
|
+
}
|
|
15
|
+
return adapters;
|
|
16
|
+
}
|
|
17
|
+
const spotAdapterCache = new Map();
|
|
18
|
+
/** Get or create a spot adapter for a given exchange name + perp adapter instance. */
|
|
19
|
+
export async function getSpotAdapter(name, adapter) {
|
|
20
|
+
const cached = spotAdapterCache.get(name);
|
|
21
|
+
if (cached)
|
|
22
|
+
return cached;
|
|
23
|
+
try {
|
|
24
|
+
let spot = null;
|
|
25
|
+
if (name === "hyperliquid") {
|
|
26
|
+
const { HyperliquidSpotAdapter } = await import("../../exchanges/hyperliquid-spot.js");
|
|
27
|
+
const { HyperliquidAdapter } = await import("../../exchanges/hyperliquid.js");
|
|
28
|
+
if (adapter instanceof HyperliquidAdapter) {
|
|
29
|
+
const instance = new HyperliquidSpotAdapter(adapter);
|
|
30
|
+
await instance.init();
|
|
31
|
+
spot = instance;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (name === "lighter") {
|
|
35
|
+
const { LighterSpotAdapter } = await import("../../exchanges/lighter-spot.js");
|
|
36
|
+
const { LighterAdapter } = await import("../../exchanges/lighter.js");
|
|
37
|
+
if (adapter instanceof LighterAdapter) {
|
|
38
|
+
const instance = new LighterSpotAdapter(adapter);
|
|
39
|
+
await instance.init();
|
|
40
|
+
spot = instance;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (spot)
|
|
44
|
+
spotAdapterCache.set(name, spot);
|
|
45
|
+
return spot;
|
|
46
|
+
}
|
|
47
|
+
catch { /* not supported */ }
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
// ── Transfer helpers ──
|
|
51
|
+
/** Transfer USDC from perp to spot account (exchange-specific). */
|
|
52
|
+
export async function transferUsdcToSpot(spotAdapter, exchangeName, amount) {
|
|
53
|
+
if (exchangeName === "hyperliquid") {
|
|
54
|
+
const { HyperliquidSpotAdapter } = await import("../../exchanges/hyperliquid-spot.js");
|
|
55
|
+
if (spotAdapter instanceof HyperliquidSpotAdapter) {
|
|
56
|
+
await spotAdapter.transferUsdcToSpot(amount);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (exchangeName === "lighter") {
|
|
61
|
+
const { LighterSpotAdapter } = await import("../../exchanges/lighter-spot.js");
|
|
62
|
+
if (spotAdapter instanceof LighterSpotAdapter) {
|
|
63
|
+
await spotAdapter.transferUsdcToSpot(amount);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Transfer USDC from spot to perp account (exchange-specific). */
|
|
69
|
+
export async function transferUsdcToPerp(spotAdapter, exchangeName, amount) {
|
|
70
|
+
if (exchangeName === "hyperliquid") {
|
|
71
|
+
const { HyperliquidSpotAdapter } = await import("../../exchanges/hyperliquid-spot.js");
|
|
72
|
+
if (spotAdapter instanceof HyperliquidSpotAdapter) {
|
|
73
|
+
await spotAdapter.transferUsdcToPerp(amount);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (exchangeName === "lighter") {
|
|
78
|
+
const { LighterSpotAdapter } = await import("../../exchanges/lighter-spot.js");
|
|
79
|
+
if (spotAdapter instanceof LighterSpotAdapter) {
|
|
80
|
+
await spotAdapter.transferUsdcToPerp(amount);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// ── Rate / symbol helpers ──
|
|
86
|
+
/** Resolve perp symbol for a given base symbol on an exchange. */
|
|
87
|
+
export function getPerpSymbol(baseSymbol, _exchangeName) {
|
|
88
|
+
return baseSymbol.replace(/-PERP$/, "").toUpperCase();
|
|
89
|
+
}
|
|
90
|
+
/** Look up rate from ratesByExchange, trying symbol, symbol-PERP, and symbol without -PERP. */
|
|
91
|
+
export function findRate(ratesByExchange, exchange, symbol) {
|
|
92
|
+
const map = ratesByExchange.get(exchange);
|
|
93
|
+
if (!map)
|
|
94
|
+
return undefined;
|
|
95
|
+
const upper = symbol.toUpperCase();
|
|
96
|
+
return map.get(upper) ?? map.get(upper + "-PERP") ?? map.get(upper.replace(/-PERP$/, ""));
|
|
97
|
+
}
|
|
98
|
+
/** Match a symbol against a target, accounting for -PERP suffix variations. */
|
|
99
|
+
export function matchSymbol(s, target) {
|
|
100
|
+
const u = s.toUpperCase();
|
|
101
|
+
const t = target.toUpperCase();
|
|
102
|
+
return u === t || u === t + "-PERP" || u.replace(/-PERP$/, "") === t;
|
|
103
|
+
}
|
|
104
|
+
/** Get a price estimate for a symbol from the perp adapter. */
|
|
105
|
+
export async function getPriceEstimate(perpAdapter, perpSymbol, fallbackSymbol) {
|
|
106
|
+
try {
|
|
107
|
+
const markets = await perpAdapter.getMarkets();
|
|
108
|
+
const market = markets.find(m => m.symbol.toUpperCase() === perpSymbol.toUpperCase() ||
|
|
109
|
+
m.symbol.toUpperCase() === fallbackSymbol.toUpperCase() ||
|
|
110
|
+
m.symbol.toUpperCase() === `${fallbackSymbol.toUpperCase()}-PERP`);
|
|
111
|
+
return market ? parseFloat(market.markPrice) : 0;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Recover arb positions from exchange state (perp-perp + spot-perp, same + cross exchange). */
|
|
118
|
+
export async function recoverArbPositions(adapters, log) {
|
|
119
|
+
const positionsByExchange = new Map();
|
|
120
|
+
for (const [name, a] of adapters) {
|
|
121
|
+
try {
|
|
122
|
+
const positions = await a.getPositions();
|
|
123
|
+
positionsByExchange.set(name, positions.map(p => ({ symbol: p.symbol.toUpperCase(), side: p.side, size: p.size })));
|
|
124
|
+
}
|
|
125
|
+
catch { /* skip */ }
|
|
126
|
+
}
|
|
127
|
+
const recovered = [];
|
|
128
|
+
const used = new Set();
|
|
129
|
+
// Cross-exchange perp-perp
|
|
130
|
+
for (const [exA, posA] of positionsByExchange) {
|
|
131
|
+
for (const pA of posA) {
|
|
132
|
+
const keyA = `${exA}:${pA.symbol}`;
|
|
133
|
+
if (used.has(keyA))
|
|
134
|
+
continue;
|
|
135
|
+
for (const [exB, posB] of positionsByExchange) {
|
|
136
|
+
if (exA === exB)
|
|
137
|
+
continue;
|
|
138
|
+
for (const pB of posB) {
|
|
139
|
+
const keyB = `${exB}:${pB.symbol}`;
|
|
140
|
+
if (used.has(keyB) || pA.symbol !== pB.symbol || pA.side === pB.side)
|
|
141
|
+
continue;
|
|
142
|
+
const longEx = pA.side === "long" ? exA : exB;
|
|
143
|
+
const shortEx = pA.side === "short" ? exA : exB;
|
|
144
|
+
recovered.push({ symbol: pA.symbol, mode: "perp-perp", longExchange: longEx, shortExchange: shortEx, size: pA.side === "long" ? pA.size : pB.size });
|
|
145
|
+
used.add(keyA);
|
|
146
|
+
used.add(keyB);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
if (used.has(keyA))
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Same-exchange spot-perp hedges
|
|
155
|
+
for (const [name, a] of adapters) {
|
|
156
|
+
try {
|
|
157
|
+
const spotAdapter = await getSpotAdapter(name, a);
|
|
158
|
+
if (!spotAdapter)
|
|
159
|
+
continue;
|
|
160
|
+
const bals = await spotAdapter.getSpotBalances();
|
|
161
|
+
const nonUsdc = bals.filter(b => Number(b.total) > 0 && !b.token.toUpperCase().startsWith("USDC"));
|
|
162
|
+
if (nonUsdc.length === 0)
|
|
163
|
+
continue;
|
|
164
|
+
for (const perp of positionsByExchange.get(name) ?? []) {
|
|
165
|
+
const perpKey = `${name}:${perp.symbol}`;
|
|
166
|
+
if (used.has(perpKey))
|
|
167
|
+
continue;
|
|
168
|
+
const base = perp.symbol.replace(/-PERP$/, "").toUpperCase();
|
|
169
|
+
const spotBal = nonUsdc.find(b => b.token.toUpperCase().replace(/-SPOT$/, "") === base);
|
|
170
|
+
if (spotBal && perp.side === "short") {
|
|
171
|
+
recovered.push({ symbol: base, mode: "spot-perp", longExchange: `${name}-spot`, shortExchange: name, size: perp.size });
|
|
172
|
+
used.add(perpKey);
|
|
173
|
+
log(` Recovered spot-perp: ${base} ${name}-spot<>${name}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch { /* no spot */ }
|
|
178
|
+
}
|
|
179
|
+
// Cross-exchange spot-perp
|
|
180
|
+
for (const [spotExName, spotExAdapter] of adapters) {
|
|
181
|
+
try {
|
|
182
|
+
const spotAdapter = await getSpotAdapter(spotExName, spotExAdapter);
|
|
183
|
+
if (!spotAdapter)
|
|
184
|
+
continue;
|
|
185
|
+
const bals = await spotAdapter.getSpotBalances();
|
|
186
|
+
const nonUsdc = bals.filter(b => Number(b.total) > 0 && !b.token.toUpperCase().startsWith("USDC"));
|
|
187
|
+
for (const bal of nonUsdc) {
|
|
188
|
+
const base = bal.token.toUpperCase().replace(/-SPOT$/, "");
|
|
189
|
+
for (const [perpExName, perpPositions] of positionsByExchange) {
|
|
190
|
+
if (perpExName === spotExName)
|
|
191
|
+
continue;
|
|
192
|
+
for (const perp of perpPositions) {
|
|
193
|
+
const perpKey = `${perpExName}:${perp.symbol}`;
|
|
194
|
+
if (used.has(perpKey))
|
|
195
|
+
continue;
|
|
196
|
+
if (perp.symbol.replace(/-PERP$/, "").toUpperCase() === base && perp.side === "short") {
|
|
197
|
+
recovered.push({ symbol: base, mode: "spot-perp", longExchange: `${spotExName}-spot`, shortExchange: perpExName, size: perp.size });
|
|
198
|
+
used.add(perpKey);
|
|
199
|
+
log(` Recovered cross spot-perp: ${base} ${spotExName}-spot<>${perpExName}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch { /* no spot */ }
|
|
206
|
+
}
|
|
207
|
+
return recovered;
|
|
208
|
+
}
|
|
209
|
+
/** Fetch funding rates from an exchange adapter. */
|
|
210
|
+
export async function fetchRates(adapter, exchangeName) {
|
|
211
|
+
try {
|
|
212
|
+
const markets = await adapter.getMarkets();
|
|
213
|
+
const withRates = markets.filter(m => m.fundingRate != null);
|
|
214
|
+
// Bootstrap aster funding hours lazily
|
|
215
|
+
if (exchangeName === "aster" && "getFundingHours" in adapter) {
|
|
216
|
+
const aster = adapter;
|
|
217
|
+
const uncached = withRates.filter(m => {
|
|
218
|
+
const c = adapter?._fundingHoursCache?.get?.(m.symbol);
|
|
219
|
+
return c === undefined;
|
|
220
|
+
});
|
|
221
|
+
for (const m of uncached.slice(0, 20)) {
|
|
222
|
+
m.fundingHours = await aster.getFundingHours(m.symbol);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return withRates
|
|
226
|
+
.filter(m => m.fundingHours != null) // skip symbols with unknown funding period (aster not yet bootstrapped)
|
|
227
|
+
.map(m => ({
|
|
228
|
+
symbol: m.symbol,
|
|
229
|
+
rate: parseFloat(m.fundingRate),
|
|
230
|
+
price: parseFloat(m.markPrice),
|
|
231
|
+
sizeDecimals: m.sizeDecimals,
|
|
232
|
+
maxLeverage: m.maxLeverage,
|
|
233
|
+
fundingHours: m.fundingHours,
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
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: "string";
|
|
15
|
+
required: boolean;
|
|
16
|
+
description: string;
|
|
17
|
+
default?: undefined;
|
|
18
|
+
} | {
|
|
19
|
+
name: string;
|
|
20
|
+
type: "boolean";
|
|
21
|
+
required: boolean;
|
|
22
|
+
default: boolean;
|
|
23
|
+
description: string;
|
|
24
|
+
})[];
|
|
25
|
+
};
|
|
26
|
+
private _config;
|
|
27
|
+
private _failCooldown;
|
|
28
|
+
private _fundingScoreCache;
|
|
29
|
+
private get params();
|
|
30
|
+
init(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<void>;
|
|
31
|
+
onTick(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
|
|
32
|
+
onStop(ctx: StrategyContext): Promise<StrategyAction[]>;
|
|
33
|
+
private scoreFunding;
|
|
34
|
+
private calcSignedSpread;
|
|
35
|
+
private checkCloseByHistory;
|
|
36
|
+
private findBestPerpShort;
|
|
37
|
+
private rotateShort;
|
|
38
|
+
private openSpotPerp;
|
|
39
|
+
private closeSpotPerp;
|
|
40
|
+
private openPerpPerp;
|
|
41
|
+
private closePerpPerp;
|
|
42
|
+
}
|