perp-cli 0.9.3 → 0.9.4
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.
|
@@ -174,10 +174,10 @@ export class SpotPerpArbStrategy {
|
|
|
174
174
|
}
|
|
175
175
|
const display = positions.map(pos => {
|
|
176
176
|
const ann = this.calcAnnualized(pos, ratesByExchange);
|
|
177
|
-
const
|
|
177
|
+
const annStr = ann !== null ? `${ann >= 0 ? "+" : ""}${ann.toFixed(1)}%` : "?%";
|
|
178
178
|
const hist = pos.fundingHistory;
|
|
179
179
|
const posCount = hist.filter(h => h.amount > 0).length;
|
|
180
|
-
return `${pos.symbol} ${pos.spotExchange}-spot<>${pos.perpExchange} ${
|
|
180
|
+
return `${pos.symbol} ${pos.spotExchange}-spot<>${pos.perpExchange} ${annStr} (${posCount}/${hist.length} positive)`;
|
|
181
181
|
});
|
|
182
182
|
ctx.state.set("arbPositionDetails", display);
|
|
183
183
|
ctx.state.set("arbPositions", positions.length);
|
|
@@ -388,13 +388,18 @@ export class SpotPerpArbStrategy {
|
|
|
388
388
|
calcAnnualized(pos, ratesByExchange) {
|
|
389
389
|
const ri = findRate(ratesByExchange, pos.perpExchange, getPerpSymbol(pos.symbol, pos.perpExchange))
|
|
390
390
|
?? findRate(ratesByExchange, pos.perpExchange, pos.symbol);
|
|
391
|
-
|
|
392
|
-
|
|
391
|
+
if (!ri)
|
|
392
|
+
return null; // rate unknown — don't assume 0
|
|
393
|
+
const fH = ri.fundingHours ?? getFundingHours(pos.perpExchange);
|
|
394
|
+
return (ri.rate / fH) * 8760 * 100;
|
|
393
395
|
}
|
|
394
396
|
shouldClose(pos, ratesByExchange, p) {
|
|
395
397
|
const minHoldMs = p.min_hold_hours * 60 * 60 * 1000;
|
|
396
398
|
const holdTime = pos.entryTime > 0 ? Date.now() - pos.entryTime : Infinity;
|
|
397
399
|
const ann = this.calcAnnualized(pos, ratesByExchange);
|
|
400
|
+
// Rate unknown — don't close (rate data not yet available)
|
|
401
|
+
if (ann === null)
|
|
402
|
+
return false;
|
|
398
403
|
// Emergency close: instantaneous spread deeply negative
|
|
399
404
|
if (ann < -50)
|
|
400
405
|
return true;
|
|
@@ -118,6 +118,24 @@ export class SpotPerpExecutor {
|
|
|
118
118
|
}
|
|
119
119
|
return { success: false, error: `spot buy: ${msg}` };
|
|
120
120
|
}
|
|
121
|
+
// Step 1b: Verify spot actually bought (safety net for "assuming executed" false positive)
|
|
122
|
+
try {
|
|
123
|
+
invalidateCache("acct");
|
|
124
|
+
const postBals = await spotAdapter.getSpotBalances();
|
|
125
|
+
const bought = postBals.find(b => b.token.toUpperCase().replace(/-SPOT$/, "") === symbol.toUpperCase());
|
|
126
|
+
const boughtAmt = bought ? parseFloat(bought.total) : 0;
|
|
127
|
+
if (boughtAmt < parseFloat(matched.size) * 0.5) {
|
|
128
|
+
log(` [SPA] Spot buy returned OK but ${symbol} balance too low (${boughtAmt.toFixed(4)}) — aborting`);
|
|
129
|
+
if (actualTransferred > 0) {
|
|
130
|
+
try {
|
|
131
|
+
await transferUsdcToPerp(spotAdapter, spotExchange, actualTransferred);
|
|
132
|
+
}
|
|
133
|
+
catch { /* best effort */ }
|
|
134
|
+
}
|
|
135
|
+
return { success: false, error: "spot buy not confirmed by balance" };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch { /* balance check failed, proceed optimistically */ }
|
|
121
139
|
// Step 2: Short perp
|
|
122
140
|
try {
|
|
123
141
|
await perpAdapter.marketOrder(perpSymbol, "sell", matched.size);
|
|
@@ -202,6 +220,24 @@ export class SpotPerpExecutor {
|
|
|
202
220
|
}
|
|
203
221
|
return false;
|
|
204
222
|
}
|
|
223
|
+
// Step 2b: Verify spot actually sold (safety net for "assuming executed" false positive)
|
|
224
|
+
try {
|
|
225
|
+
invalidateCache("acct");
|
|
226
|
+
const postBals = await spotAdapter.getSpotBalances();
|
|
227
|
+
const remaining = postBals.find(b => b.token.toUpperCase().replace(/-SPOT$/, "") === symbol.toUpperCase());
|
|
228
|
+
const remainingAmt = remaining ? parseFloat(remaining.total) : 0;
|
|
229
|
+
if (remainingAmt >= parseFloat(size) * 0.5) {
|
|
230
|
+
log(` [SPA] CRITICAL: Spot sell returned OK but ${symbol} balance unchanged (${remainingAmt.toFixed(4)}) — re-hedging perp`);
|
|
231
|
+
try {
|
|
232
|
+
await perpAdapter.marketOrder(perpSym, "sell", size);
|
|
233
|
+
}
|
|
234
|
+
catch (rbErr) {
|
|
235
|
+
log(` [SPA] CRITICAL: Perp re-hedge failed: ${rbErr instanceof Error ? rbErr.message : String(rbErr)}`);
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch { /* balance check failed, proceed optimistically */ }
|
|
205
241
|
// Step 3: Transfer USDC back to perp (always — lighter/HL both need this)
|
|
206
242
|
try {
|
|
207
243
|
const spotBals = await spotAdapter.getSpotBalances();
|
|
@@ -239,8 +239,8 @@ export class LighterSpotAdapter {
|
|
|
239
239
|
try {
|
|
240
240
|
const tx = await this._restGet("/tx", { by: "hash", value: txResult.tx_hash });
|
|
241
241
|
const status = Number(tx.status ?? 0);
|
|
242
|
-
if (status
|
|
243
|
-
return result; // executed
|
|
242
|
+
if (status >= 2 && status <= 3)
|
|
243
|
+
return result; // status 2=executed, 3=committed
|
|
244
244
|
if (status > 3)
|
|
245
245
|
throw new Error(`Spot ${side} ${symbol}: tx rejected (status=${status})`);
|
|
246
246
|
}
|
|
@@ -488,8 +488,8 @@ export class LighterAdapter {
|
|
|
488
488
|
try {
|
|
489
489
|
const tx = await this.restGet("/tx", { by: "hash", value: txResult.tx_hash });
|
|
490
490
|
const status = Number(tx.status ?? 0);
|
|
491
|
-
if (status
|
|
492
|
-
// status 3 =
|
|
491
|
+
if (status >= 2 && status <= 3) {
|
|
492
|
+
// status 2 = executed, 3 = committed. Parse event_info for fill details
|
|
493
493
|
try {
|
|
494
494
|
const eventInfo = JSON.parse(String(tx.event_info ?? "{}"));
|
|
495
495
|
const takerOrder = eventInfo.to;
|
|
@@ -507,7 +507,7 @@ export class LighterAdapter {
|
|
|
507
507
|
// status=3 means tx was executed (even if zero-fill IOC — strategy verifies position)
|
|
508
508
|
return result;
|
|
509
509
|
}
|
|
510
|
-
if (status > 3) {
|
|
510
|
+
if (status > 3) { // status 4+ = rejected/failed
|
|
511
511
|
// status > 3 likely means rejected/cancelled
|
|
512
512
|
throw new Error(`Market ${side} ${symbol}: tx rejected (status=${status}, hash=${txResult.tx_hash})`);
|
|
513
513
|
}
|
|
@@ -194,7 +194,7 @@ export class PacificaAdapter {
|
|
|
194
194
|
if (orderId && !(opts?.reduceOnly ?? false)) {
|
|
195
195
|
// Poll order status via history_by_id
|
|
196
196
|
let lastPollError = null;
|
|
197
|
-
for (let attempt = 0; attempt <
|
|
197
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
198
198
|
await new Promise(r => setTimeout(r, 500));
|
|
199
199
|
try {
|
|
200
200
|
const history = await this.client.getOrderHistoryById(Number(orderId));
|
|
@@ -217,8 +217,9 @@ export class PacificaAdapter {
|
|
|
217
217
|
lastPollError = e;
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
|
+
// Order was accepted but verification timed out — warn, don't throw
|
|
220
221
|
const errDetail = lastPollError instanceof Error ? ` (poll error: ${lastPollError.message})` : "";
|
|
221
|
-
|
|
222
|
+
console.error(`[pacifica] Warning: market ${side} ${symbol} order ${orderId} not confirmed after 5s${errDetail} — assuming executed`);
|
|
222
223
|
}
|
|
223
224
|
if (!(opts?.reduceOnly ?? false)) {
|
|
224
225
|
console.error(`[pacifica] Warning: market ${side} ${symbol} accepted but no order_id in response — skipping fill verification`);
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// Save original process.exit before any SDK (Go WASM) can patch it
|
|
3
|
+
const _origExit = process.exit.bind(process);
|
|
2
4
|
import { config } from "dotenv";
|
|
3
5
|
import { resolve } from "path";
|
|
4
6
|
import { createRequire } from "node:module";
|
|
@@ -733,14 +735,14 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
|
|
|
733
735
|
catch {
|
|
734
736
|
program.help();
|
|
735
737
|
}
|
|
736
|
-
setTimeout(() =>
|
|
738
|
+
setTimeout(() => _origExit(0), 500);
|
|
737
739
|
})();
|
|
738
740
|
}
|
|
739
741
|
else {
|
|
740
742
|
program.parseAsync().then(() => {
|
|
741
743
|
// Allow a short delay for any pending output, then exit cleanly.
|
|
742
744
|
// Without this, HL SDK's WebSocket keeps the process alive indefinitely.
|
|
743
|
-
setTimeout(() =>
|
|
745
|
+
setTimeout(() => _origExit(0), 500);
|
|
744
746
|
}).catch(async (err) => {
|
|
745
747
|
const msg = err instanceof Error ? err.message : String(err);
|
|
746
748
|
if (isJson()) {
|
|
@@ -750,6 +752,6 @@ else {
|
|
|
750
752
|
else {
|
|
751
753
|
console.error(chalk.red(msg));
|
|
752
754
|
}
|
|
753
|
-
|
|
755
|
+
_origExit(1);
|
|
754
756
|
});
|
|
755
757
|
}
|