perp-cli 0.9.1 → 0.9.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 -1
- package/dist/arb/sizing.js +3 -3
- package/dist/bot/engine.js +28 -4
- package/dist/bot/strategies/spot-perp-executor.js +4 -2
- package/dist/commands/bot.js +1 -1
- package/dist/exchanges/lighter-spot.js +4 -3
- package/dist/exchanges/lighter.js +3 -3
- package/package.json +1 -1
package/dist/arb/sizing.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare function computeMatchedSize(sizeUsd: number, price: number, longE
|
|
|
17
17
|
* Compute matched size for spot-perp arb.
|
|
18
18
|
* Uses spot exchange's size decimals for the spot leg.
|
|
19
19
|
*/
|
|
20
|
-
export declare function computeSpotPerpMatchedSize(sizeUsd: number, price: number, spotExchange: string, perpExchange: string, spotSizeDecimals?: number): {
|
|
20
|
+
export declare function computeSpotPerpMatchedSize(sizeUsd: number, price: number, spotExchange: string, perpExchange: string, spotSizeDecimals?: number, perpSizeDecimals?: number): {
|
|
21
21
|
size: string;
|
|
22
22
|
notional: number;
|
|
23
23
|
} | null;
|
package/dist/arb/sizing.js
CHANGED
|
@@ -77,12 +77,12 @@ function getMinNotional(exchange) {
|
|
|
77
77
|
* Compute matched size for spot-perp arb.
|
|
78
78
|
* Uses spot exchange's size decimals for the spot leg.
|
|
79
79
|
*/
|
|
80
|
-
export function computeSpotPerpMatchedSize(sizeUsd, price, spotExchange, perpExchange, spotSizeDecimals) {
|
|
80
|
+
export function computeSpotPerpMatchedSize(sizeUsd, price, spotExchange, perpExchange, spotSizeDecimals, perpSizeDecimals) {
|
|
81
81
|
if (price <= 0)
|
|
82
82
|
return null;
|
|
83
|
-
// If explicit
|
|
83
|
+
// If explicit decimals provided, use them; otherwise derive from exchange
|
|
84
84
|
const spotDec = spotSizeDecimals ?? getSizeDecimals(`spot:${spotExchange}`);
|
|
85
|
-
const perpDec = getSizeDecimals(perpExchange);
|
|
85
|
+
const perpDec = perpSizeDecimals ?? getSizeDecimals(perpExchange);
|
|
86
86
|
const szDecimals = Math.min(spotDec, perpDec);
|
|
87
87
|
const rawSize = sizeUsd / price;
|
|
88
88
|
const factor = Math.pow(10, szDecimals);
|
package/dist/bot/engine.js
CHANGED
|
@@ -114,6 +114,21 @@ mode = "headless") {
|
|
|
114
114
|
const shutdown = () => { running = false; state.phase = "stopped"; };
|
|
115
115
|
process.on("SIGINT", shutdown);
|
|
116
116
|
process.on("SIGTERM", shutdown);
|
|
117
|
+
// Direct stdin 'q' handler as fallback (ink's useInput may not work in all terminals)
|
|
118
|
+
let stdinHandler;
|
|
119
|
+
if (process.stdin.isTTY) {
|
|
120
|
+
process.stdin.setRawMode?.(true);
|
|
121
|
+
process.stdin.resume();
|
|
122
|
+
stdinHandler = (data) => {
|
|
123
|
+
const ch = data.toString();
|
|
124
|
+
if (ch === "q" || ch === "Q" || ch === "\x03") { // q, Q, or Ctrl+C
|
|
125
|
+
shutdown();
|
|
126
|
+
if (tuiUnmount)
|
|
127
|
+
tuiUnmount();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
process.stdin.on("data", stdinHandler);
|
|
131
|
+
}
|
|
117
132
|
if (mode !== "tui") {
|
|
118
133
|
// Header (headless / json modes print header to console)
|
|
119
134
|
log(chalk.cyan.bold(`\n ╔═══════════════════════════════════════╗`));
|
|
@@ -308,6 +323,11 @@ mode = "headless") {
|
|
|
308
323
|
finally {
|
|
309
324
|
process.removeListener("SIGINT", shutdown);
|
|
310
325
|
process.removeListener("SIGTERM", shutdown);
|
|
326
|
+
if (stdinHandler) {
|
|
327
|
+
process.stdin.removeListener("data", stdinHandler);
|
|
328
|
+
process.stdin.setRawMode?.(false);
|
|
329
|
+
process.stdin.pause();
|
|
330
|
+
}
|
|
311
331
|
// Cleanup: cancel any remaining orders
|
|
312
332
|
if (state.strategyActive) {
|
|
313
333
|
effectiveLog(` Cleaning up orders...`);
|
|
@@ -442,8 +462,10 @@ async function fetchPositions(adapter, symbol, exchangeName) {
|
|
|
442
462
|
async function fetchExchangeBalances(primaryAdapter, extraAdapters) {
|
|
443
463
|
const adapters = [[primaryAdapter.name, primaryAdapter]];
|
|
444
464
|
if (extraAdapters) {
|
|
445
|
-
for (const [name, a] of extraAdapters)
|
|
446
|
-
|
|
465
|
+
for (const [name, a] of extraAdapters) {
|
|
466
|
+
if (name !== primaryAdapter.name)
|
|
467
|
+
adapters.push([name, a]);
|
|
468
|
+
}
|
|
447
469
|
}
|
|
448
470
|
const results = [];
|
|
449
471
|
await Promise.all(adapters.map(async ([name, a]) => {
|
|
@@ -523,8 +545,10 @@ async function fetchSpotPositions(adapter) {
|
|
|
523
545
|
async function fetchAllPositions(primaryAdapter, symbol, extraAdapters) {
|
|
524
546
|
const adapters = [[primaryAdapter.name, primaryAdapter]];
|
|
525
547
|
if (extraAdapters) {
|
|
526
|
-
for (const [name, a] of extraAdapters)
|
|
527
|
-
|
|
548
|
+
for (const [name, a] of extraAdapters) {
|
|
549
|
+
if (name !== primaryAdapter.name)
|
|
550
|
+
adapters.push([name, a]);
|
|
551
|
+
}
|
|
528
552
|
}
|
|
529
553
|
const all = [];
|
|
530
554
|
await Promise.all(adapters.map(async ([name, a]) => {
|
|
@@ -68,9 +68,11 @@ export class SpotPerpExecutor {
|
|
|
68
68
|
// Compute matched size
|
|
69
69
|
const spotMarkets = await spotAdapter.getSpotMarkets();
|
|
70
70
|
const spotDec = spotMarkets.find(m => m.baseToken.toUpperCase() === symbol.toUpperCase())?.sizeDecimals;
|
|
71
|
-
const
|
|
71
|
+
const perpMarkets = await perpAdapter.getMarkets();
|
|
72
|
+
const perpDec = perpMarkets.find(m => matchSymbol(m.symbol, perpSymbol) || matchSymbol(m.symbol, symbol))?.sizeDecimals;
|
|
73
|
+
const matched = computeSpotPerpMatchedSize(liq.adjustedSizeUsd, price, spotExchange, perpExchange, spotDec, perpDec);
|
|
72
74
|
if (!matched) {
|
|
73
|
-
log(` [SPA] Skip ${symbol}: matched size fail`);
|
|
75
|
+
log(` [SPA] Skip ${symbol}: matched size fail (spotDec=${spotDec}, perpDec=${perpDec}, price=${price.toFixed(2)}, sizeUsd=${liq.adjustedSizeUsd.toFixed(2)})`);
|
|
74
76
|
return { success: false, error: "matched size fail" };
|
|
75
77
|
}
|
|
76
78
|
log(` [SPA] Opening: ${matched.size} ${symbol} ($${matched.notional.toFixed(0)}) ${spotExchange}-spot<>${perpExchange}`);
|
package/dist/commands/bot.js
CHANGED
|
@@ -213,7 +213,7 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
|
|
|
213
213
|
bot
|
|
214
214
|
.command("delta-neutral")
|
|
215
215
|
.description("Spot-long + perp-short delta-neutral funding rate farming")
|
|
216
|
-
.option("--exchanges <list>", "Comma-separated perp exchange names", "lighter,aster")
|
|
216
|
+
.option("--exchanges <list>", "Comma-separated perp exchange names", "pacifica,lighter,hyperliquid,aster")
|
|
217
217
|
.option("--min-rate <pct>", "Min annualized rate to enter (%)", "10")
|
|
218
218
|
.option("--close-rate <pct>", "Close when avg rate drops below (%)", "2")
|
|
219
219
|
.option("--size <usd>", "Position size per leg ($)", "50")
|
|
@@ -231,10 +231,10 @@ export class LighterSpotAdapter {
|
|
|
231
231
|
orderType: 1, // MARKET
|
|
232
232
|
timeInForce: 0, // IOC
|
|
233
233
|
});
|
|
234
|
-
// Verify fill via tx endpoint
|
|
234
|
+
// Verify fill via tx endpoint (best-effort: tx was accepted by sendTx, so likely executed)
|
|
235
235
|
const txResult = result;
|
|
236
236
|
if (txResult.tx_hash) {
|
|
237
|
-
for (let attempt = 0; attempt <
|
|
237
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
238
238
|
await new Promise(r => setTimeout(r, 500));
|
|
239
239
|
try {
|
|
240
240
|
const tx = await this._restGet("/tx", { by: "hash", value: txResult.tx_hash });
|
|
@@ -249,7 +249,8 @@ export class LighterSpotAdapter {
|
|
|
249
249
|
throw e;
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
|
-
|
|
252
|
+
// tx was accepted (sendTx 200) but verification timed out — warn, don't throw
|
|
253
|
+
console.error(`[lighter-spot] Warning: spot ${side} ${symbol} tx not confirmed after 5s (hash=${txResult.tx_hash}) — assuming executed`);
|
|
253
254
|
}
|
|
254
255
|
return result;
|
|
255
256
|
}
|
|
@@ -483,7 +483,7 @@ export class LighterAdapter {
|
|
|
483
483
|
const txResult = result;
|
|
484
484
|
if (txResult.tx_hash) {
|
|
485
485
|
// Poll tx status (may take a moment to process)
|
|
486
|
-
for (let attempt = 0; attempt <
|
|
486
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
487
487
|
await new Promise(r => setTimeout(r, 500));
|
|
488
488
|
try {
|
|
489
489
|
const tx = await this.restGet("/tx", { by: "hash", value: txResult.tx_hash });
|
|
@@ -519,8 +519,8 @@ export class LighterAdapter {
|
|
|
519
519
|
// tx query failed, retry
|
|
520
520
|
}
|
|
521
521
|
}
|
|
522
|
-
//
|
|
523
|
-
|
|
522
|
+
// tx was accepted (sendTx 200) but verification timed out — warn, don't throw
|
|
523
|
+
console.error(`[lighter] Warning: market ${side} ${symbol} tx not confirmed after 5s (hash=${txResult.tx_hash}) — assuming executed`);
|
|
524
524
|
}
|
|
525
525
|
return result;
|
|
526
526
|
}
|