perp-cli 0.3.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/LICENSE +21 -0
- package/README.md +293 -0
- package/dist/__tests__/alert-logic.test.d.ts +1 -0
- package/dist/__tests__/alert-logic.test.js +107 -0
- package/dist/__tests__/arb-auto-3dex.test.d.ts +1 -0
- package/dist/__tests__/arb-auto-3dex.test.js +397 -0
- package/dist/__tests__/arb-history-stats.test.d.ts +1 -0
- package/dist/__tests__/arb-history-stats.test.js +176 -0
- package/dist/__tests__/arb-logic.test.d.ts +1 -0
- package/dist/__tests__/arb-logic.test.js +84 -0
- package/dist/__tests__/arb-manage.test.d.ts +1 -0
- package/dist/__tests__/arb-manage.test.js +253 -0
- package/dist/__tests__/arb-new-features.test.d.ts +1 -0
- package/dist/__tests__/arb-new-features.test.js +457 -0
- package/dist/__tests__/arb-sizing.test.d.ts +1 -0
- package/dist/__tests__/arb-sizing.test.js +48 -0
- package/dist/__tests__/arb-state.test.d.ts +1 -0
- package/dist/__tests__/arb-state.test.js +284 -0
- package/dist/__tests__/arb-userflow.test.d.ts +1 -0
- package/dist/__tests__/arb-userflow.test.js +945 -0
- package/dist/__tests__/arb-utils.test.d.ts +1 -0
- package/dist/__tests__/arb-utils.test.js +264 -0
- package/dist/__tests__/bot-conditions.test.d.ts +1 -0
- package/dist/__tests__/bot-conditions.test.js +341 -0
- package/dist/__tests__/client-id-tracker.test.d.ts +1 -0
- package/dist/__tests__/client-id-tracker.test.js +137 -0
- package/dist/__tests__/commands/new-atomic-commands.test.d.ts +1 -0
- package/dist/__tests__/commands/new-atomic-commands.test.js +502 -0
- package/dist/__tests__/commands/order-intent.test.d.ts +1 -0
- package/dist/__tests__/commands/order-intent.test.js +600 -0
- package/dist/__tests__/commands/trade-commands.test.d.ts +1 -0
- package/dist/__tests__/commands/trade-commands.test.js +821 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +86 -0
- package/dist/__tests__/cross-chain-margin.test.d.ts +1 -0
- package/dist/__tests__/cross-chain-margin.test.js +287 -0
- package/dist/__tests__/dex-asset-map.test.d.ts +1 -0
- package/dist/__tests__/dex-asset-map.test.js +191 -0
- package/dist/__tests__/errors.test.d.ts +1 -0
- package/dist/__tests__/errors.test.js +110 -0
- package/dist/__tests__/event-stream.test.d.ts +1 -0
- package/dist/__tests__/event-stream.test.js +276 -0
- package/dist/__tests__/exchanges/interface.test.d.ts +1 -0
- package/dist/__tests__/exchanges/interface.test.js +132 -0
- package/dist/__tests__/exchanges/mock-adapter.d.ts +69 -0
- package/dist/__tests__/exchanges/mock-adapter.js +137 -0
- package/dist/__tests__/execution-log.test.d.ts +1 -0
- package/dist/__tests__/execution-log.test.js +106 -0
- package/dist/__tests__/funding-calc.test.d.ts +1 -0
- package/dist/__tests__/funding-calc.test.js +71 -0
- package/dist/__tests__/funding-history.test.d.ts +1 -0
- package/dist/__tests__/funding-history.test.js +343 -0
- package/dist/__tests__/funding-rates.test.d.ts +1 -0
- package/dist/__tests__/funding-rates.test.js +342 -0
- package/dist/__tests__/funding.test.d.ts +1 -0
- package/dist/__tests__/funding.test.js +173 -0
- package/dist/__tests__/gap-logic.test.d.ts +1 -0
- package/dist/__tests__/gap-logic.test.js +43 -0
- package/dist/__tests__/hip3-dex.test.d.ts +1 -0
- package/dist/__tests__/hip3-dex.test.js +234 -0
- package/dist/__tests__/integration/agent-features.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/agent-features.integration.test.js +553 -0
- package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +13 -0
- package/dist/__tests__/integration/atomic-commands.integration.test.js +246 -0
- package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge-simulation.integration.test.js +453 -0
- package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge-strict.integration.test.js +812 -0
- package/dist/__tests__/integration/bridge.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge.integration.test.js +309 -0
- package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/cli-e2e.integration.test.js +202 -0
- package/dist/__tests__/integration/dex-arb.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/dex-arb.integration.test.js +116 -0
- package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +13 -0
- package/dist/__tests__/integration/envelope-consistency.integration.test.js +205 -0
- package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/hip3-dex.integration.test.js +147 -0
- package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/hyperliquid.integration.test.js +79 -0
- package/dist/__tests__/integration/lighter.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/lighter.integration.test.js +53 -0
- package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +9 -0
- package/dist/__tests__/integration/new-commands-e2e.integration.test.js +236 -0
- package/dist/__tests__/integration/order-verification.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/order-verification.integration.test.js +321 -0
- package/dist/__tests__/integration/pacifica.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/pacifica.integration.test.js +75 -0
- package/dist/__tests__/integration/response-shapes.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/response-shapes.integration.test.js +278 -0
- package/dist/__tests__/liquidity.test.d.ts +1 -0
- package/dist/__tests__/liquidity.test.js +225 -0
- package/dist/__tests__/plan-executor.test.d.ts +1 -0
- package/dist/__tests__/plan-executor.test.js +314 -0
- package/dist/__tests__/position-history.test.d.ts +1 -0
- package/dist/__tests__/position-history.test.js +367 -0
- package/dist/__tests__/retry.test.d.ts +1 -0
- package/dist/__tests__/retry.test.js +310 -0
- package/dist/__tests__/risk-assessment.test.d.ts +1 -0
- package/dist/__tests__/risk-assessment.test.js +145 -0
- package/dist/__tests__/security-adversarial.test.d.ts +1 -0
- package/dist/__tests__/security-adversarial.test.js +574 -0
- package/dist/__tests__/strategies.test.d.ts +1 -0
- package/dist/__tests__/strategies.test.js +539 -0
- package/dist/__tests__/trade-execution.test.d.ts +1 -0
- package/dist/__tests__/trade-execution.test.js +129 -0
- package/dist/__tests__/trade-validator.test.d.ts +1 -0
- package/dist/__tests__/trade-validator.test.js +655 -0
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +76 -0
- package/dist/api/public/hyperliquid.d.ts +18 -0
- package/dist/api/public/hyperliquid.js +82 -0
- package/dist/api/public/index.d.ts +8 -0
- package/dist/api/public/index.js +8 -0
- package/dist/api/public/lighter.d.ts +24 -0
- package/dist/api/public/lighter.js +100 -0
- package/dist/api/public/pacifica.d.ts +17 -0
- package/dist/api/public/pacifica.js +54 -0
- package/dist/api/public/urls.d.ts +12 -0
- package/dist/api/public/urls.js +33 -0
- package/dist/arb/history-stats.d.ts +44 -0
- package/dist/arb/history-stats.js +135 -0
- package/dist/arb/index.d.ts +4 -0
- package/dist/arb/index.js +4 -0
- package/dist/arb/sizing.d.ts +23 -0
- package/dist/arb/sizing.js +96 -0
- package/dist/arb/state.d.ts +51 -0
- package/dist/arb/state.js +112 -0
- package/dist/arb/utils.d.ts +81 -0
- package/dist/arb/utils.js +267 -0
- package/dist/arb-history-stats.d.ts +5 -0
- package/dist/arb-history-stats.js +5 -0
- package/dist/arb-sizing.d.ts +5 -0
- package/dist/arb-sizing.js +5 -0
- package/dist/arb-state.d.ts +5 -0
- package/dist/arb-state.js +5 -0
- package/dist/arb-utils.d.ts +5 -0
- package/dist/arb-utils.js +5 -0
- package/dist/bot/conditions.d.ts +32 -0
- package/dist/bot/conditions.js +141 -0
- package/dist/bot/config.d.ts +76 -0
- package/dist/bot/config.js +160 -0
- package/dist/bot/engine.d.ts +8 -0
- package/dist/bot/engine.js +519 -0
- package/dist/bot/presets.d.ts +11 -0
- package/dist/bot/presets.js +296 -0
- package/dist/bridge-engine.d.ts +133 -0
- package/dist/bridge-engine.js +1487 -0
- package/dist/cache.d.ts +25 -0
- package/dist/cache.js +99 -0
- package/dist/cli-spec.d.ts +50 -0
- package/dist/cli-spec.js +75 -0
- package/dist/client-id-tracker.d.ts +25 -0
- package/dist/client-id-tracker.js +76 -0
- package/dist/commands/account.d.ts +3 -0
- package/dist/commands/account.js +425 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.js +386 -0
- package/dist/commands/alert.d.ts +2 -0
- package/dist/commands/alert.js +421 -0
- package/dist/commands/analytics.d.ts +3 -0
- package/dist/commands/analytics.js +311 -0
- package/dist/commands/arb/index.d.ts +3 -0
- package/dist/commands/arb/index.js +921 -0
- package/dist/commands/arb-auto.d.ts +54 -0
- package/dist/commands/arb-auto.js +1328 -0
- package/dist/commands/arb-manage.d.ts +5 -0
- package/dist/commands/arb-manage.js +5 -0
- package/dist/commands/arb.d.ts +2 -0
- package/dist/commands/arb.js +347 -0
- package/dist/commands/backtest.d.ts +2 -0
- package/dist/commands/backtest.js +327 -0
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.js +412 -0
- package/dist/commands/bridge.d.ts +2 -0
- package/dist/commands/bridge.js +396 -0
- package/dist/commands/dashboard.d.ts +3 -0
- package/dist/commands/dashboard.js +176 -0
- package/dist/commands/deposit.d.ts +4 -0
- package/dist/commands/deposit.js +573 -0
- package/dist/commands/dex.d.ts +3 -0
- package/dist/commands/dex.js +114 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/commands/env.js +136 -0
- package/dist/commands/funding.d.ts +2 -0
- package/dist/commands/funding.js +347 -0
- package/dist/commands/gap.d.ts +2 -0
- package/dist/commands/gap.js +305 -0
- package/dist/commands/health.d.ts +2 -0
- package/dist/commands/health.js +67 -0
- package/dist/commands/history.d.ts +2 -0
- package/dist/commands/history.js +235 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +266 -0
- package/dist/commands/jobs.d.ts +2 -0
- package/dist/commands/jobs.js +133 -0
- package/dist/commands/manage.d.ts +4 -0
- package/dist/commands/manage.js +309 -0
- package/dist/commands/market.d.ts +3 -0
- package/dist/commands/market.js +225 -0
- package/dist/commands/plan.d.ts +3 -0
- package/dist/commands/plan.js +95 -0
- package/dist/commands/portfolio.d.ts +3 -0
- package/dist/commands/portfolio.js +169 -0
- package/dist/commands/rebalance.d.ts +3 -0
- package/dist/commands/rebalance.js +293 -0
- package/dist/commands/risk.d.ts +3 -0
- package/dist/commands/risk.js +169 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +202 -0
- package/dist/commands/settings.d.ts +2 -0
- package/dist/commands/settings.js +102 -0
- package/dist/commands/stream.d.ts +5 -0
- package/dist/commands/stream.js +123 -0
- package/dist/commands/trade.d.ts +3 -0
- package/dist/commands/trade.js +1273 -0
- package/dist/commands/wallet.d.ts +14 -0
- package/dist/commands/wallet.js +602 -0
- package/dist/commands/withdraw.d.ts +3 -0
- package/dist/commands/withdraw.js +187 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +68 -0
- package/dist/cross-chain-margin.d.ts +46 -0
- package/dist/cross-chain-margin.js +107 -0
- package/dist/dashboard/server.d.ts +80 -0
- package/dist/dashboard/server.js +340 -0
- package/dist/dashboard/ui.d.ts +4 -0
- package/dist/dashboard/ui.js +538 -0
- package/dist/dashboard/ws-feeds.d.ts +29 -0
- package/dist/dashboard/ws-feeds.js +660 -0
- package/dist/dex-asset-map.d.ts +80 -0
- package/dist/dex-asset-map.js +201 -0
- package/dist/errors.d.ts +109 -0
- package/dist/errors.js +84 -0
- package/dist/event-stream.d.ts +25 -0
- package/dist/event-stream.js +168 -0
- package/dist/exchanges/hyperliquid.d.ts +212 -0
- package/dist/exchanges/hyperliquid.js +931 -0
- package/dist/exchanges/interface.d.ts +95 -0
- package/dist/exchanges/interface.js +5 -0
- package/dist/exchanges/lighter.d.ts +159 -0
- package/dist/exchanges/lighter.js +793 -0
- package/dist/exchanges/pacifica.d.ts +51 -0
- package/dist/exchanges/pacifica.js +248 -0
- package/dist/execution-log.d.ts +36 -0
- package/dist/execution-log.js +102 -0
- package/dist/funding/history.d.ts +63 -0
- package/dist/funding/history.js +266 -0
- package/dist/funding/index.d.ts +3 -0
- package/dist/funding/index.js +3 -0
- package/dist/funding/normalize.d.ts +39 -0
- package/dist/funding/normalize.js +66 -0
- package/dist/funding/rates.d.ts +45 -0
- package/dist/funding/rates.js +172 -0
- package/dist/funding-history.d.ts +5 -0
- package/dist/funding-history.js +5 -0
- package/dist/funding-rates.d.ts +5 -0
- package/dist/funding-rates.js +5 -0
- package/dist/funding.d.ts +5 -0
- package/dist/funding.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +458 -0
- package/dist/jobs.d.ts +37 -0
- package/dist/jobs.js +152 -0
- package/dist/liquidity.d.ts +34 -0
- package/dist/liquidity.js +100 -0
- package/dist/mcp-server.d.ts +9 -0
- package/dist/mcp-server.js +1206 -0
- package/dist/pacifica/client.d.ts +111 -0
- package/dist/pacifica/client.js +310 -0
- package/dist/pacifica/constants.d.ts +27 -0
- package/dist/pacifica/constants.js +47 -0
- package/dist/pacifica/deposit.d.ts +14 -0
- package/dist/pacifica/deposit.js +78 -0
- package/dist/pacifica/index.d.ts +6 -0
- package/dist/pacifica/index.js +11 -0
- package/dist/pacifica/signing.d.ts +49 -0
- package/dist/pacifica/signing.js +97 -0
- package/dist/pacifica/types/account.d.ts +42 -0
- package/dist/pacifica/types/account.js +1 -0
- package/dist/pacifica/types/index.d.ts +6 -0
- package/dist/pacifica/types/index.js +6 -0
- package/dist/pacifica/types/lake.d.ts +18 -0
- package/dist/pacifica/types/lake.js +1 -0
- package/dist/pacifica/types/market.d.ts +64 -0
- package/dist/pacifica/types/market.js +1 -0
- package/dist/pacifica/types/order.d.ts +92 -0
- package/dist/pacifica/types/order.js +1 -0
- package/dist/pacifica/types/position.d.ts +25 -0
- package/dist/pacifica/types/position.js +1 -0
- package/dist/pacifica/types/ws.d.ts +34 -0
- package/dist/pacifica/types/ws.js +41 -0
- package/dist/pacifica/ws-client.d.ts +42 -0
- package/dist/pacifica/ws-client.js +180 -0
- package/dist/plan-executor.d.ts +48 -0
- package/dist/plan-executor.js +280 -0
- package/dist/position-history.d.ts +68 -0
- package/dist/position-history.js +222 -0
- package/dist/rebalance.d.ts +64 -0
- package/dist/rebalance.js +142 -0
- package/dist/retry.d.ts +74 -0
- package/dist/retry.js +129 -0
- package/dist/risk.d.ts +48 -0
- package/dist/risk.js +156 -0
- package/dist/settings.d.ts +19 -0
- package/dist/settings.js +45 -0
- package/dist/shared-api.d.ts +5 -0
- package/dist/shared-api.js +5 -0
- package/dist/strategies/dca.d.ts +25 -0
- package/dist/strategies/dca.js +114 -0
- package/dist/strategies/funding-arb.d.ts +15 -0
- package/dist/strategies/funding-arb.js +281 -0
- package/dist/strategies/grid.d.ts +34 -0
- package/dist/strategies/grid.js +185 -0
- package/dist/strategies/trailing-stop.d.ts +17 -0
- package/dist/strategies/trailing-stop.js +121 -0
- package/dist/strategies/twap.d.ts +20 -0
- package/dist/strategies/twap.js +78 -0
- package/dist/trade-validator.d.ts +39 -0
- package/dist/trade-validator.js +154 -0
- package/dist/utils.d.ts +38 -0
- package/dist/utils.js +110 -0
- package/package.json +63 -0
- package/skills/perp-cli/SKILL.md +149 -0
- package/skills/perp-cli/references/commands.md +143 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { updateJobState } from "../jobs.js";
|
|
2
|
+
import { logExecution } from "../execution-log.js";
|
|
3
|
+
export async function runTrailingStop(adapter, params, jobId, log = console.log) {
|
|
4
|
+
const { symbol, trailPct } = params;
|
|
5
|
+
const intervalMs = (params.intervalSec ?? 5) * 1000;
|
|
6
|
+
const activationPrice = params.activationPrice;
|
|
7
|
+
const startedAt = Date.now();
|
|
8
|
+
// Auto-detect position side
|
|
9
|
+
const positions = await adapter.getPositions();
|
|
10
|
+
const pos = positions.find(p => {
|
|
11
|
+
const c = p.symbol.toUpperCase();
|
|
12
|
+
const t = symbol.toUpperCase();
|
|
13
|
+
return c === t || c === `${t}-PERP` || c.replace(/-PERP$/, "") === t;
|
|
14
|
+
});
|
|
15
|
+
if (!pos) {
|
|
16
|
+
log(`[TRAIL] No open position for ${symbol}. Exiting.`);
|
|
17
|
+
return { triggered: false, reason: "no_position", runtime: 0 };
|
|
18
|
+
}
|
|
19
|
+
const positionSide = pos.side;
|
|
20
|
+
const closeSide = positionSide === "long" ? "sell" : "buy";
|
|
21
|
+
const posSize = pos.size;
|
|
22
|
+
log(`[TRAIL] ${symbol} ${positionSide} ${posSize} | Trail: ${trailPct}%${activationPrice ? ` | Activation: $${activationPrice}` : ""}`);
|
|
23
|
+
let peakPrice = 0;
|
|
24
|
+
let activated = !activationPrice;
|
|
25
|
+
let running = true;
|
|
26
|
+
const cleanup = () => { running = false; };
|
|
27
|
+
process.on("SIGINT", cleanup);
|
|
28
|
+
process.on("SIGTERM", cleanup);
|
|
29
|
+
if (jobId) {
|
|
30
|
+
updateJobState(jobId, {
|
|
31
|
+
status: "running",
|
|
32
|
+
result: { symbol, positionSide, trailPct, activationPrice },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
while (running) {
|
|
37
|
+
const markets = await adapter.getMarkets();
|
|
38
|
+
const market = markets.find(m => {
|
|
39
|
+
const c = m.symbol.toUpperCase();
|
|
40
|
+
const t = symbol.toUpperCase();
|
|
41
|
+
return c === t || c === `${t}-PERP` || c.replace(/-PERP$/, "") === t;
|
|
42
|
+
});
|
|
43
|
+
if (!market) {
|
|
44
|
+
log(`[TRAIL] Market data for ${symbol} not found, retrying...`);
|
|
45
|
+
await new Promise(r => setTimeout(r, intervalMs));
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const currentPrice = parseFloat(market.markPrice);
|
|
49
|
+
// Check activation
|
|
50
|
+
if (!activated && activationPrice) {
|
|
51
|
+
if (positionSide === "long" && currentPrice >= activationPrice) {
|
|
52
|
+
activated = true;
|
|
53
|
+
log(`[TRAIL] Activated at $${currentPrice.toFixed(2)} (>= $${activationPrice})`);
|
|
54
|
+
}
|
|
55
|
+
else if (positionSide === "short" && currentPrice <= activationPrice) {
|
|
56
|
+
activated = true;
|
|
57
|
+
log(`[TRAIL] Activated at $${currentPrice.toFixed(2)} (<= $${activationPrice})`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
log(`[TRAIL] $${currentPrice.toFixed(2)} | Waiting for activation ($${activationPrice})...`);
|
|
61
|
+
await new Promise(r => setTimeout(r, intervalMs));
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (positionSide === "long") {
|
|
66
|
+
if (currentPrice > peakPrice)
|
|
67
|
+
peakPrice = currentPrice;
|
|
68
|
+
const dropPct = peakPrice > 0 ? ((peakPrice - currentPrice) / peakPrice) * 100 : 0;
|
|
69
|
+
log(`[TRAIL] $${currentPrice.toFixed(2)} | Peak: $${peakPrice.toFixed(2)} | Drop: ${dropPct.toFixed(2)}%`);
|
|
70
|
+
if (dropPct >= trailPct) {
|
|
71
|
+
log(`[TRAIL] TRIGGERED! Price dropped ${dropPct.toFixed(2)}% from peak $${peakPrice.toFixed(2)}`);
|
|
72
|
+
log(`[TRAIL] Closing ${positionSide} ${posSize} ${symbol}...`);
|
|
73
|
+
await adapter.marketOrder(symbol, closeSide, posSize);
|
|
74
|
+
logExecution({
|
|
75
|
+
type: "market_order", exchange: adapter.name, symbol,
|
|
76
|
+
side: closeSide, size: posSize, status: "success", dryRun: false,
|
|
77
|
+
meta: { action: "trailing-stop", trailPct, peakPrice, triggerPrice: currentPrice },
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
triggered: true, reason: "triggered",
|
|
81
|
+
peakPrice, triggerPrice: currentPrice, changePct: dropPct,
|
|
82
|
+
positionSide, runtime: (Date.now() - startedAt) / 1000,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// Short: track trough, trigger on rise
|
|
88
|
+
if (peakPrice === 0 || currentPrice < peakPrice)
|
|
89
|
+
peakPrice = currentPrice;
|
|
90
|
+
const risePct = peakPrice > 0 ? ((currentPrice - peakPrice) / peakPrice) * 100 : 0;
|
|
91
|
+
log(`[TRAIL] $${currentPrice.toFixed(2)} | Trough: $${peakPrice.toFixed(2)} | Rise: ${risePct.toFixed(2)}%`);
|
|
92
|
+
if (risePct >= trailPct) {
|
|
93
|
+
log(`[TRAIL] TRIGGERED! Price rose ${risePct.toFixed(2)}% from trough $${peakPrice.toFixed(2)}`);
|
|
94
|
+
log(`[TRAIL] Closing ${positionSide} ${posSize} ${symbol}...`);
|
|
95
|
+
await adapter.marketOrder(symbol, closeSide, posSize);
|
|
96
|
+
logExecution({
|
|
97
|
+
type: "market_order", exchange: adapter.name, symbol,
|
|
98
|
+
side: closeSide, size: posSize, status: "success", dryRun: false,
|
|
99
|
+
meta: { action: "trailing-stop", trailPct, troughPrice: peakPrice, triggerPrice: currentPrice },
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
triggered: true, reason: "triggered",
|
|
103
|
+
peakPrice, triggerPrice: currentPrice, changePct: risePct,
|
|
104
|
+
positionSide, runtime: (Date.now() - startedAt) / 1000,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
await new Promise(r => setTimeout(r, intervalMs));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
process.removeListener("SIGINT", cleanup);
|
|
113
|
+
process.removeListener("SIGTERM", cleanup);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
triggered: false, reason: "cancelled",
|
|
117
|
+
peakPrice: peakPrice || undefined,
|
|
118
|
+
positionSide,
|
|
119
|
+
runtime: (Date.now() - startedAt) / 1000,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ExchangeAdapter } from "../exchanges/interface.js";
|
|
2
|
+
export interface TWAPParams {
|
|
3
|
+
symbol: string;
|
|
4
|
+
side: "buy" | "sell";
|
|
5
|
+
totalSize: number;
|
|
6
|
+
durationSec: number;
|
|
7
|
+
slices?: number;
|
|
8
|
+
maxSlippage?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface TWAPState {
|
|
11
|
+
filled: number;
|
|
12
|
+
remaining: number;
|
|
13
|
+
slicesDone: number;
|
|
14
|
+
totalSlices: number;
|
|
15
|
+
avgPrice: number;
|
|
16
|
+
errors: number;
|
|
17
|
+
startedAt: number;
|
|
18
|
+
lastSliceAt: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function runTWAP(adapter: ExchangeAdapter, params: TWAPParams, jobId?: string, log?: (msg: string) => void): Promise<TWAPState>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { updateJobState } from "../jobs.js";
|
|
2
|
+
export async function runTWAP(adapter, params, jobId, log = console.log) {
|
|
3
|
+
const totalSlices = params.slices || Math.max(Math.floor(params.durationSec / 30), 2);
|
|
4
|
+
const sliceSize = params.totalSize / totalSlices;
|
|
5
|
+
const intervalMs = (params.durationSec * 1000) / totalSlices;
|
|
6
|
+
const state = {
|
|
7
|
+
filled: 0,
|
|
8
|
+
remaining: params.totalSize,
|
|
9
|
+
slicesDone: 0,
|
|
10
|
+
totalSlices,
|
|
11
|
+
avgPrice: 0,
|
|
12
|
+
errors: 0,
|
|
13
|
+
startedAt: Date.now(),
|
|
14
|
+
lastSliceAt: 0,
|
|
15
|
+
};
|
|
16
|
+
log(`[TWAP] ${params.side.toUpperCase()} ${params.totalSize} ${params.symbol} over ${params.durationSec}s`);
|
|
17
|
+
log(`[TWAP] ${totalSlices} slices, ${sliceSize.toFixed(6)} per slice, ${(intervalMs / 1000).toFixed(1)}s interval`);
|
|
18
|
+
log(`[TWAP] Exchange: ${adapter.name}`);
|
|
19
|
+
let totalCost = 0;
|
|
20
|
+
for (let i = 0; i < totalSlices; i++) {
|
|
21
|
+
if (i > 0) {
|
|
22
|
+
await sleep(intervalMs);
|
|
23
|
+
}
|
|
24
|
+
const thisSlice = i === totalSlices - 1
|
|
25
|
+
? state.remaining // Last slice: fill whatever remains (avoid rounding dust)
|
|
26
|
+
: sliceSize;
|
|
27
|
+
if (thisSlice <= 0)
|
|
28
|
+
break;
|
|
29
|
+
try {
|
|
30
|
+
log(`[TWAP] Slice ${i + 1}/${totalSlices}: ${params.side} ${thisSlice.toFixed(6)} ${params.symbol}...`);
|
|
31
|
+
const result = await adapter.marketOrder(params.symbol, params.side, String(thisSlice));
|
|
32
|
+
state.slicesDone++;
|
|
33
|
+
state.filled += thisSlice;
|
|
34
|
+
state.remaining = params.totalSize - state.filled;
|
|
35
|
+
state.lastSliceAt = Date.now();
|
|
36
|
+
// Try to extract fill price from result
|
|
37
|
+
const fillPrice = Number(result?.price ?? result?.avg_price ?? result?.fill_price ?? 0);
|
|
38
|
+
if (fillPrice > 0) {
|
|
39
|
+
totalCost += thisSlice * fillPrice;
|
|
40
|
+
state.avgPrice = totalCost / state.filled;
|
|
41
|
+
}
|
|
42
|
+
const pct = ((state.filled / params.totalSize) * 100).toFixed(1);
|
|
43
|
+
log(`[TWAP] Filled ${state.filled.toFixed(6)}/${params.totalSize} (${pct}%)${state.avgPrice > 0 ? ` avg $${state.avgPrice.toFixed(4)}` : ""}`);
|
|
44
|
+
// Update job state file if running as background job
|
|
45
|
+
if (jobId) {
|
|
46
|
+
updateJobState(jobId, {
|
|
47
|
+
result: {
|
|
48
|
+
filled: state.filled,
|
|
49
|
+
remaining: state.remaining,
|
|
50
|
+
slicesDone: state.slicesDone,
|
|
51
|
+
totalSlices: state.totalSlices,
|
|
52
|
+
avgPrice: state.avgPrice,
|
|
53
|
+
pctComplete: parseFloat(pct),
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
state.errors++;
|
|
60
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
61
|
+
log(`[TWAP] Slice ${i + 1} error: ${msg}`);
|
|
62
|
+
// Continue — don't abort the whole TWAP for one failed slice
|
|
63
|
+
if (state.errors > totalSlices * 0.5) {
|
|
64
|
+
log(`[TWAP] Too many errors (${state.errors}/${totalSlices}), aborting.`);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const elapsed = ((Date.now() - state.startedAt) / 1000).toFixed(1);
|
|
70
|
+
log(`[TWAP] Complete. Filled ${state.filled.toFixed(6)} in ${elapsed}s, ${state.errors} errors.`);
|
|
71
|
+
if (jobId) {
|
|
72
|
+
updateJobState(jobId, { status: "done", result: { ...state } });
|
|
73
|
+
}
|
|
74
|
+
return state;
|
|
75
|
+
}
|
|
76
|
+
function sleep(ms) {
|
|
77
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
78
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ExchangeAdapter } from "./exchanges/interface.js";
|
|
2
|
+
export interface CheckResult {
|
|
3
|
+
check: "symbol_valid" | "balance_sufficient" | "price_fresh" | "liquidity_ok" | "risk_limits" | "position_exists";
|
|
4
|
+
passed: boolean;
|
|
5
|
+
message: string;
|
|
6
|
+
details?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export interface TradeValidation {
|
|
9
|
+
valid: boolean;
|
|
10
|
+
checks: CheckResult[];
|
|
11
|
+
warnings: string[];
|
|
12
|
+
estimatedCost?: {
|
|
13
|
+
margin: number;
|
|
14
|
+
fee: number;
|
|
15
|
+
slippage: number;
|
|
16
|
+
total: number;
|
|
17
|
+
};
|
|
18
|
+
marketInfo?: {
|
|
19
|
+
symbol: string;
|
|
20
|
+
markPrice: number;
|
|
21
|
+
fundingRate: number;
|
|
22
|
+
maxLeverage: number;
|
|
23
|
+
};
|
|
24
|
+
timestamp: string;
|
|
25
|
+
}
|
|
26
|
+
export interface TradeCheckParams {
|
|
27
|
+
symbol: string;
|
|
28
|
+
side: "buy" | "sell";
|
|
29
|
+
size: number;
|
|
30
|
+
price?: number;
|
|
31
|
+
type?: "market" | "limit" | "stop";
|
|
32
|
+
leverage?: number;
|
|
33
|
+
reduceOnly?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Validate a trade before execution.
|
|
37
|
+
* Runs multiple checks in parallel where possible.
|
|
38
|
+
*/
|
|
39
|
+
export declare function validateTrade(adapter: ExchangeAdapter, params: TradeCheckParams): Promise<TradeValidation>;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { symbolMatch } from "./utils.js";
|
|
2
|
+
const DEFAULT_TAKER_FEE = 0.0005; // 0.05%
|
|
3
|
+
const DEFAULT_MAX_SLIPPAGE = 0.005; // 0.5%
|
|
4
|
+
/**
|
|
5
|
+
* Validate a trade before execution.
|
|
6
|
+
* Runs multiple checks in parallel where possible.
|
|
7
|
+
*/
|
|
8
|
+
export async function validateTrade(adapter, params) {
|
|
9
|
+
const checks = [];
|
|
10
|
+
const warnings = [];
|
|
11
|
+
const sym = params.symbol.toUpperCase();
|
|
12
|
+
// Fetch market data, balance, positions, orderbook in parallel
|
|
13
|
+
const [markets, balance, positions, orderbook] = await Promise.all([
|
|
14
|
+
adapter.getMarkets().catch(() => []),
|
|
15
|
+
adapter.getBalance().catch(() => ({ equity: "0", available: "0", marginUsed: "0", unrealizedPnl: "0" })),
|
|
16
|
+
adapter.getPositions().catch(() => []),
|
|
17
|
+
params.type !== "limit" ? adapter.getOrderbook(sym).catch(() => ({ bids: [], asks: [] })) : Promise.resolve({ bids: [], asks: [] }),
|
|
18
|
+
]);
|
|
19
|
+
// 1. Symbol validity (handle BTC vs BTC-PERP suffix variants)
|
|
20
|
+
const market = markets.find(m => {
|
|
21
|
+
const ms = m.symbol.toUpperCase();
|
|
22
|
+
return ms === sym || ms === `${sym}-PERP` || ms.replace(/-PERP$/, "") === sym;
|
|
23
|
+
});
|
|
24
|
+
if (market) {
|
|
25
|
+
checks.push({ check: "symbol_valid", passed: true, message: `${sym} found on ${adapter.name}`, details: { maxLeverage: market.maxLeverage } });
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
checks.push({ check: "symbol_valid", passed: false, message: `${sym} not found on ${adapter.name}` });
|
|
29
|
+
return { valid: false, checks, warnings, timestamp: new Date().toISOString() };
|
|
30
|
+
}
|
|
31
|
+
const markPrice = Number(market.markPrice);
|
|
32
|
+
const price = params.price ?? markPrice;
|
|
33
|
+
const notional = params.size * price;
|
|
34
|
+
const leverage = params.leverage ?? market.maxLeverage;
|
|
35
|
+
const marginRequired = notional / leverage;
|
|
36
|
+
// 2. Balance check
|
|
37
|
+
const available = Number(balance.available);
|
|
38
|
+
if (params.reduceOnly) {
|
|
39
|
+
// reduce-only doesn't need margin
|
|
40
|
+
checks.push({ check: "balance_sufficient", passed: true, message: "Reduce-only order, no margin needed" });
|
|
41
|
+
}
|
|
42
|
+
else if (available >= marginRequired) {
|
|
43
|
+
checks.push({ check: "balance_sufficient", passed: true, message: `Available $${available.toFixed(2)} >= margin required $${marginRequired.toFixed(2)}`, details: { available, marginRequired, leverage } });
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
checks.push({ check: "balance_sufficient", passed: false, message: `Insufficient balance: $${available.toFixed(2)} available, $${marginRequired.toFixed(2)} required at ${leverage}x`, details: { available, marginRequired, leverage } });
|
|
47
|
+
}
|
|
48
|
+
// 3. Price freshness (if mark price is way off from input price for limits)
|
|
49
|
+
if (params.price && markPrice > 0) {
|
|
50
|
+
const deviation = Math.abs(params.price - markPrice) / markPrice;
|
|
51
|
+
if (deviation > 0.10) {
|
|
52
|
+
checks.push({ check: "price_fresh", passed: false, message: `Price $${params.price} deviates ${(deviation * 100).toFixed(1)}% from mark $${markPrice.toFixed(2)}`, details: { price: params.price, markPrice, deviationPct: deviation * 100 } });
|
|
53
|
+
}
|
|
54
|
+
else if (deviation > 0.03) {
|
|
55
|
+
checks.push({ check: "price_fresh", passed: true, message: `Price deviation ${(deviation * 100).toFixed(1)}% from mark` });
|
|
56
|
+
warnings.push(`Price $${params.price} is ${(deviation * 100).toFixed(1)}% from mark price $${markPrice.toFixed(2)}`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
checks.push({ check: "price_fresh", passed: true, message: "Price within normal range of mark" });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
checks.push({ check: "price_fresh", passed: true, message: markPrice > 0 ? `Mark price: $${markPrice.toFixed(2)}` : "No mark price to compare" });
|
|
64
|
+
}
|
|
65
|
+
// 4. Liquidity check (market orders)
|
|
66
|
+
if (params.type !== "limit" && orderbook.bids.length > 0) {
|
|
67
|
+
const book = params.side === "buy" ? orderbook.asks : orderbook.bids;
|
|
68
|
+
let availableLiquidity = 0;
|
|
69
|
+
let worstPrice = 0;
|
|
70
|
+
for (const [px, sz] of book) {
|
|
71
|
+
availableLiquidity += Number(px) * Number(sz);
|
|
72
|
+
worstPrice = Number(px);
|
|
73
|
+
if (availableLiquidity >= notional)
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
if (availableLiquidity >= notional) {
|
|
77
|
+
const slippage = markPrice > 0 ? Math.abs(worstPrice - markPrice) / markPrice : 0;
|
|
78
|
+
if (slippage > DEFAULT_MAX_SLIPPAGE) {
|
|
79
|
+
checks.push({ check: "liquidity_ok", passed: false, message: `Slippage ${(slippage * 100).toFixed(2)}% exceeds ${(DEFAULT_MAX_SLIPPAGE * 100).toFixed(1)}% threshold`, details: { slippagePct: slippage * 100, worstPrice, liquidityUsd: availableLiquidity } });
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
checks.push({ check: "liquidity_ok", passed: true, message: `Sufficient liquidity ($${availableLiquidity.toFixed(0)}), slippage ~${(slippage * 100).toFixed(3)}%`, details: { slippagePct: slippage * 100, liquidityUsd: availableLiquidity } });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
checks.push({ check: "liquidity_ok", passed: false, message: `Insufficient liquidity: $${availableLiquidity.toFixed(0)} available vs $${notional.toFixed(0)} needed`, details: { liquidityUsd: availableLiquidity, notional } });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
checks.push({ check: "liquidity_ok", passed: true, message: "Liquidity check skipped (limit order or no book data)" });
|
|
91
|
+
}
|
|
92
|
+
// 5. Risk limits (use existing risk system)
|
|
93
|
+
try {
|
|
94
|
+
const { assessRisk, preTradeCheck } = await import("./risk.js");
|
|
95
|
+
const assessment = assessRisk([{ exchange: adapter.name, balance }], positions.map(p => ({ exchange: adapter.name, position: p })));
|
|
96
|
+
const riskResult = preTradeCheck(assessment, notional, leverage);
|
|
97
|
+
if (riskResult.allowed) {
|
|
98
|
+
checks.push({ check: "risk_limits", passed: true, message: "Within risk limits" });
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
checks.push({ check: "risk_limits", passed: false, message: `Risk violation: ${riskResult.reason}`, details: { reason: riskResult.reason } });
|
|
102
|
+
}
|
|
103
|
+
// Surface non-critical violations from the assessment as warnings
|
|
104
|
+
for (const v of assessment.violations) {
|
|
105
|
+
if (v.severity !== "critical") {
|
|
106
|
+
warnings.push(`${v.rule}: ${v.message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
checks.push({ check: "risk_limits", passed: true, message: "Risk check skipped (no limits configured)" });
|
|
112
|
+
}
|
|
113
|
+
// 6. Position check (for reduce-only)
|
|
114
|
+
if (params.reduceOnly) {
|
|
115
|
+
const pos = positions.find(p => symbolMatch(p.symbol, sym));
|
|
116
|
+
if (pos) {
|
|
117
|
+
const posSize = parseFloat(pos.size);
|
|
118
|
+
if (params.size > posSize) {
|
|
119
|
+
checks.push({ check: "position_exists", passed: false, message: `Reduce size ${params.size} exceeds position ${posSize}`, details: { positionSize: posSize, reduceSize: params.size } });
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
checks.push({ check: "position_exists", passed: true, message: `Position exists: ${pos.side} ${pos.size}`, details: { side: pos.side, size: posSize } });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
checks.push({ check: "position_exists", passed: false, message: `No position found for ${sym} (reduce-only requires open position)` });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Estimated cost
|
|
130
|
+
const fee = notional * DEFAULT_TAKER_FEE;
|
|
131
|
+
const estimatedSlippage = params.type === "limit" ? 0 : notional * 0.001;
|
|
132
|
+
const valid = checks.every(c => c.passed);
|
|
133
|
+
if (leverage > market.maxLeverage) {
|
|
134
|
+
warnings.push(`Requested leverage ${leverage}x exceeds max ${market.maxLeverage}x`);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
valid,
|
|
138
|
+
checks,
|
|
139
|
+
warnings,
|
|
140
|
+
estimatedCost: {
|
|
141
|
+
margin: marginRequired,
|
|
142
|
+
fee,
|
|
143
|
+
slippage: estimatedSlippage,
|
|
144
|
+
total: marginRequired + fee + estimatedSlippage,
|
|
145
|
+
},
|
|
146
|
+
marketInfo: {
|
|
147
|
+
symbol: sym,
|
|
148
|
+
markPrice,
|
|
149
|
+
fundingRate: Number(market.fundingRate),
|
|
150
|
+
maxLeverage: market.maxLeverage,
|
|
151
|
+
},
|
|
152
|
+
timestamp: new Date().toISOString(),
|
|
153
|
+
};
|
|
154
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare function symbolMatch(candidate: string, target: string): boolean;
|
|
2
|
+
export declare function formatUsd(value: string | number): string;
|
|
3
|
+
export declare function formatPnl(value: string | number): string;
|
|
4
|
+
export declare function formatPercent(value: string | number): string;
|
|
5
|
+
export declare function makeTable(head: string[], rows: string[][]): string;
|
|
6
|
+
export declare function printJson(data: unknown): void;
|
|
7
|
+
export declare function errorAndExit(msg: string): never;
|
|
8
|
+
/** Standard JSON response envelope */
|
|
9
|
+
export interface ApiResponse<T = unknown> {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
data?: T;
|
|
12
|
+
error?: {
|
|
13
|
+
code: string;
|
|
14
|
+
message: string;
|
|
15
|
+
status?: number;
|
|
16
|
+
retryable?: boolean;
|
|
17
|
+
retryAfterMs?: number;
|
|
18
|
+
details?: Record<string, unknown>;
|
|
19
|
+
};
|
|
20
|
+
meta?: {
|
|
21
|
+
exchange?: string;
|
|
22
|
+
timestamp: string;
|
|
23
|
+
duration_ms?: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Wrap successful result in standard envelope */
|
|
27
|
+
export declare function jsonOk<T>(data: T, meta?: Partial<ApiResponse['meta']>): ApiResponse<T>;
|
|
28
|
+
/** Wrap error in standard envelope */
|
|
29
|
+
export declare function jsonError(code: string, message: string, meta?: Partial<ApiResponse['meta']> & {
|
|
30
|
+
status?: number;
|
|
31
|
+
retryable?: boolean;
|
|
32
|
+
retryAfterMs?: number;
|
|
33
|
+
details?: Record<string, unknown>;
|
|
34
|
+
}): ApiResponse<never>;
|
|
35
|
+
/** Execute a command action with structured error handling.
|
|
36
|
+
* In JSON mode, errors are returned as JSON instead of crashing.
|
|
37
|
+
*/
|
|
38
|
+
export declare function withJsonErrors<T>(isJson: boolean, fn: () => Promise<T>, exchange?: string): Promise<T | undefined>;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import Table from "cli-table3";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { classifyError } from "./errors.js";
|
|
4
|
+
export function symbolMatch(candidate, target) {
|
|
5
|
+
const c = candidate.toUpperCase();
|
|
6
|
+
const t = target.toUpperCase();
|
|
7
|
+
return c === t || c === `${t}-PERP` || c.replace(/-PERP$/, "") === t;
|
|
8
|
+
}
|
|
9
|
+
export function formatUsd(value) {
|
|
10
|
+
const num = Number(value);
|
|
11
|
+
if (isNaN(num))
|
|
12
|
+
return String(value);
|
|
13
|
+
return num.toLocaleString("en-US", {
|
|
14
|
+
minimumFractionDigits: 2,
|
|
15
|
+
maximumFractionDigits: 2,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function formatPnl(value) {
|
|
19
|
+
const num = Number(value);
|
|
20
|
+
if (isNaN(num))
|
|
21
|
+
return String(value);
|
|
22
|
+
const formatted = formatUsd(Math.abs(num));
|
|
23
|
+
if (num > 0)
|
|
24
|
+
return chalk.green(`+$${formatted}`);
|
|
25
|
+
if (num < 0)
|
|
26
|
+
return chalk.red(`-$${formatted}`);
|
|
27
|
+
return `$${formatted}`;
|
|
28
|
+
}
|
|
29
|
+
export function formatPercent(value) {
|
|
30
|
+
const num = Number(value);
|
|
31
|
+
if (isNaN(num))
|
|
32
|
+
return String(value);
|
|
33
|
+
const pct = (num * 100).toFixed(4);
|
|
34
|
+
const prefix = num > 0 ? "+" : "";
|
|
35
|
+
const color = num > 0 ? chalk.green : num < 0 ? chalk.red : chalk.white;
|
|
36
|
+
return color(`${prefix}${pct}%`);
|
|
37
|
+
}
|
|
38
|
+
export function makeTable(head, rows) {
|
|
39
|
+
const table = new Table({
|
|
40
|
+
head: head.map((h) => chalk.cyan.bold(h)),
|
|
41
|
+
style: { head: [], border: [] },
|
|
42
|
+
chars: {
|
|
43
|
+
top: "─", "top-mid": "┬", "top-left": "┌", "top-right": "┐",
|
|
44
|
+
bottom: "─", "bottom-mid": "┴", "bottom-left": "└", "bottom-right": "┘",
|
|
45
|
+
left: "│", "left-mid": "├",
|
|
46
|
+
mid: "─", "mid-mid": "┼",
|
|
47
|
+
right: "│", "right-mid": "┤",
|
|
48
|
+
middle: "│",
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
rows.forEach((r) => table.push(r));
|
|
52
|
+
return table.toString();
|
|
53
|
+
}
|
|
54
|
+
export function printJson(data) {
|
|
55
|
+
console.log(JSON.stringify(data, null, 2));
|
|
56
|
+
}
|
|
57
|
+
export function errorAndExit(msg) {
|
|
58
|
+
if (process.argv.includes("--json")) {
|
|
59
|
+
console.log(JSON.stringify(jsonError("INVALID_PARAMS", msg)));
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
console.error(chalk.red(`Error: ${msg}`));
|
|
63
|
+
}
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
/** Wrap successful result in standard envelope */
|
|
67
|
+
export function jsonOk(data, meta) {
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
data,
|
|
71
|
+
meta: { timestamp: new Date().toISOString(), ...meta },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Wrap error in standard envelope */
|
|
75
|
+
export function jsonError(code, message, meta) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
error: {
|
|
79
|
+
code,
|
|
80
|
+
message,
|
|
81
|
+
...(meta?.status !== undefined ? { status: meta.status } : {}),
|
|
82
|
+
...(meta?.retryable !== undefined ? { retryable: meta.retryable } : {}),
|
|
83
|
+
...(meta?.retryAfterMs !== undefined ? { retryAfterMs: meta.retryAfterMs } : {}),
|
|
84
|
+
...(meta?.details ? { details: meta.details } : {}),
|
|
85
|
+
},
|
|
86
|
+
meta: { timestamp: new Date().toISOString(), ...meta },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Execute a command action with structured error handling.
|
|
90
|
+
* In JSON mode, errors are returned as JSON instead of crashing.
|
|
91
|
+
*/
|
|
92
|
+
export async function withJsonErrors(isJson, fn, exchange) {
|
|
93
|
+
try {
|
|
94
|
+
return await fn();
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
const classified = classifyError(err, exchange);
|
|
98
|
+
if (isJson) {
|
|
99
|
+
console.log(JSON.stringify(jsonError(classified.code, classified.message, {
|
|
100
|
+
status: classified.status,
|
|
101
|
+
retryable: classified.retryable,
|
|
102
|
+
retryAfterMs: classified.retryAfterMs,
|
|
103
|
+
})));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.error(chalk.red(`Error: ${classified.message}`));
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "perp-cli",
|
|
3
|
+
"version": "0.3.3",
|
|
4
|
+
"description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter",
|
|
5
|
+
"bin": {
|
|
6
|
+
"perp": "./dist/index.js",
|
|
7
|
+
"perp-mcp": "./dist/mcp-server.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"skills"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsc --watch",
|
|
17
|
+
"test": "vitest run --exclude '**/integration/**'",
|
|
18
|
+
"test:integration": "vitest run '**/*.integration.test.ts'",
|
|
19
|
+
"test:all": "vitest run",
|
|
20
|
+
"prepublishOnly": "pnpm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"pacifica",
|
|
24
|
+
"hyperliquid",
|
|
25
|
+
"lighter",
|
|
26
|
+
"solana",
|
|
27
|
+
"perps",
|
|
28
|
+
"dex",
|
|
29
|
+
"cli",
|
|
30
|
+
"trading",
|
|
31
|
+
"funding-rate",
|
|
32
|
+
"arbitrage"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": "^20.0.0 || ^22.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
40
|
+
"@msgpack/msgpack": "^3.1.3",
|
|
41
|
+
"@solana/web3.js": "^1.98.0",
|
|
42
|
+
"bs58": "^6.0.0",
|
|
43
|
+
"chalk": "^5.4.1",
|
|
44
|
+
"cli-table3": "^0.6.5",
|
|
45
|
+
"commander": "^13.1.0",
|
|
46
|
+
"dotenv": "^16.5.0",
|
|
47
|
+
"ethers": "^6.13.2",
|
|
48
|
+
"hyperliquid": "^1.7.7",
|
|
49
|
+
"lighter-sdk": "^0.0.17",
|
|
50
|
+
"tweetnacl": "^1.0.3",
|
|
51
|
+
"ws": "^8.19.0",
|
|
52
|
+
"yaml": "^2.8.2",
|
|
53
|
+
"zod": "^4.3.6"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^25.3.3",
|
|
57
|
+
"@types/ws": "^8.18.1",
|
|
58
|
+
"tsup": "^8.4.0",
|
|
59
|
+
"tsx": "^4.21.0",
|
|
60
|
+
"typescript": "^5.7.0",
|
|
61
|
+
"vitest": "^4.0.18"
|
|
62
|
+
}
|
|
63
|
+
}
|