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,142 @@
|
|
|
1
|
+
import { EXCHANGE_TO_CHAIN } from "./bridge-engine.js";
|
|
2
|
+
// Re-export for backward compat
|
|
3
|
+
const EXCHANGE_CHAINS = Object.fromEntries(Object.entries(EXCHANGE_TO_CHAIN).map(([ex, chain]) => [ex, { chain }]));
|
|
4
|
+
/**
|
|
5
|
+
* Fetch balances from all exchanges in parallel.
|
|
6
|
+
*/
|
|
7
|
+
export async function fetchAllBalances(adapters) {
|
|
8
|
+
const entries = [...adapters.entries()];
|
|
9
|
+
const results = await Promise.allSettled(entries.map(async ([name, adapter]) => {
|
|
10
|
+
const bal = await adapter.getBalance();
|
|
11
|
+
return {
|
|
12
|
+
exchange: name,
|
|
13
|
+
equity: Number(bal.equity),
|
|
14
|
+
available: Number(bal.available),
|
|
15
|
+
marginUsed: Number(bal.marginUsed),
|
|
16
|
+
unrealizedPnl: Number(bal.unrealizedPnl),
|
|
17
|
+
};
|
|
18
|
+
}));
|
|
19
|
+
return results
|
|
20
|
+
.filter((r) => r.status === "fulfilled")
|
|
21
|
+
.map((r) => r.value);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Compute a rebalancing plan to equalize available balance across exchanges.
|
|
25
|
+
*
|
|
26
|
+
* Strategy: equal-weight — target = totalAvailable / numExchanges
|
|
27
|
+
* Only moves from exchanges with surplus to those with deficit.
|
|
28
|
+
* Respects a minimum move threshold to avoid tiny transfers.
|
|
29
|
+
*/
|
|
30
|
+
export function computeRebalancePlan(snapshots, opts = {}) {
|
|
31
|
+
const minMove = opts.minMove ?? 50;
|
|
32
|
+
const reserve = opts.reserve ?? 20;
|
|
33
|
+
const totalEquity = snapshots.reduce((s, e) => s + e.equity, 0);
|
|
34
|
+
const totalAvailable = snapshots.reduce((s, e) => s + e.available, 0);
|
|
35
|
+
// Calculate targets
|
|
36
|
+
const weights = opts.weights ?? Object.fromEntries(snapshots.map((s) => [s.exchange, 1 / snapshots.length]));
|
|
37
|
+
const targets = new Map();
|
|
38
|
+
for (const snap of snapshots) {
|
|
39
|
+
const w = weights[snap.exchange] ?? 1 / snapshots.length;
|
|
40
|
+
targets.set(snap.exchange, totalAvailable * w);
|
|
41
|
+
}
|
|
42
|
+
// Calculate deltas (positive = surplus, negative = deficit)
|
|
43
|
+
const deltas = new Map();
|
|
44
|
+
for (const snap of snapshots) {
|
|
45
|
+
const target = targets.get(snap.exchange) ?? 0;
|
|
46
|
+
const movable = Math.max(0, snap.available - reserve);
|
|
47
|
+
const delta = movable - Math.max(0, target - reserve);
|
|
48
|
+
deltas.set(snap.exchange, delta);
|
|
49
|
+
}
|
|
50
|
+
// Match surplus → deficit
|
|
51
|
+
const moves = [];
|
|
52
|
+
const surpluses = [...deltas.entries()].filter(([, d]) => d > minMove).sort((a, b) => b[1] - a[1]);
|
|
53
|
+
const deficits = [...deltas.entries()].filter(([, d]) => d < -minMove).sort((a, b) => a[1] - b[1]);
|
|
54
|
+
for (const [fromEx, surplus] of surpluses) {
|
|
55
|
+
let remaining = surplus;
|
|
56
|
+
for (const deficit of deficits) {
|
|
57
|
+
if (remaining < minMove)
|
|
58
|
+
break;
|
|
59
|
+
const [toEx, deficitAmt] = deficit;
|
|
60
|
+
if (deficitAmt >= -minMove)
|
|
61
|
+
continue;
|
|
62
|
+
const moveAmt = Math.min(remaining, Math.abs(deficitAmt));
|
|
63
|
+
if (moveAmt < minMove)
|
|
64
|
+
continue;
|
|
65
|
+
moves.push({
|
|
66
|
+
from: fromEx,
|
|
67
|
+
to: toEx,
|
|
68
|
+
amount: Math.floor(moveAmt),
|
|
69
|
+
reason: `Rebalance: ${fromEx} has $${Math.floor(surplus)} surplus, ${toEx} needs $${Math.floor(Math.abs(deficitAmt))}`,
|
|
70
|
+
});
|
|
71
|
+
remaining -= moveAmt;
|
|
72
|
+
deficit[1] += moveAmt; // reduce deficit
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const summary = moves.length === 0
|
|
76
|
+
? "Balanced — no moves needed"
|
|
77
|
+
: `${moves.length} move(s): ${moves.map((m) => `$${m.amount} ${m.from}→${m.to}`).join(", ")}`;
|
|
78
|
+
return {
|
|
79
|
+
snapshots,
|
|
80
|
+
totalEquity,
|
|
81
|
+
targetPerExchange: totalAvailable / snapshots.length,
|
|
82
|
+
moves,
|
|
83
|
+
summary,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Check if an exchange has enough available balance for a given trade size.
|
|
88
|
+
*/
|
|
89
|
+
export function hasEnoughBalance(snapshots, exchange, requiredUsd, marginBuffer = 1.5) {
|
|
90
|
+
const snap = snapshots.find((s) => s.exchange === exchange);
|
|
91
|
+
if (!snap)
|
|
92
|
+
return false;
|
|
93
|
+
return snap.available >= requiredUsd * marginBuffer;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get the chain/bridge info for an exchange.
|
|
97
|
+
*/
|
|
98
|
+
export function getExchangeChain(exchange) {
|
|
99
|
+
return EXCHANGE_CHAINS[exchange];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Describe the bridge route for a rebalance move.
|
|
103
|
+
*/
|
|
104
|
+
export function describeBridgeRoute(move) {
|
|
105
|
+
const from = EXCHANGE_CHAINS[move.from];
|
|
106
|
+
const to = EXCHANGE_CHAINS[move.to];
|
|
107
|
+
if (!from || !to)
|
|
108
|
+
return `${move.from} → ${move.to} (unknown route)`;
|
|
109
|
+
if (from.chain === to.chain)
|
|
110
|
+
return `Internal transfer on ${from.chain}`;
|
|
111
|
+
// All cross-chain moves go through USDC bridging
|
|
112
|
+
const steps = [];
|
|
113
|
+
steps.push(`1. Withdraw $${move.amount} USDC from ${move.from} (${from.chain})`);
|
|
114
|
+
// Determine bridge method
|
|
115
|
+
if (from.chain === "solana" || to.chain === "solana") {
|
|
116
|
+
steps.push(`2. Bridge via CCTP/Wormhole (${from.chain} → ${to.chain})`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
steps.push(`2. Bridge via CCTP (${from.chain} → ${to.chain})`);
|
|
120
|
+
}
|
|
121
|
+
steps.push(`3. Deposit into ${move.to} (${to.chain})`);
|
|
122
|
+
return steps.join("\n ");
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Estimate total time for a rebalance move.
|
|
126
|
+
*/
|
|
127
|
+
export function estimateMoveTime(move) {
|
|
128
|
+
const from = EXCHANGE_CHAINS[move.from]?.chain;
|
|
129
|
+
const to = EXCHANGE_CHAINS[move.to]?.chain;
|
|
130
|
+
// Withdrawal times
|
|
131
|
+
const withdrawTime = {
|
|
132
|
+
pacifica: "~10s",
|
|
133
|
+
hyperliquid: "~5min",
|
|
134
|
+
lighter: "~12h (standard)",
|
|
135
|
+
};
|
|
136
|
+
// Bridge times
|
|
137
|
+
let bridgeTime = "~1-3min (CCTP)";
|
|
138
|
+
if (from === "solana" || to === "solana") {
|
|
139
|
+
bridgeTime = "~15min (Wormhole/CCTP)";
|
|
140
|
+
}
|
|
141
|
+
return `Withdraw: ${withdrawTime[move.from] ?? "?"} → Bridge: ${bridgeTime} → Deposit: ~1min`;
|
|
142
|
+
}
|
package/dist/retry.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-retry middleware for exchange operations.
|
|
3
|
+
* Retries operations that fail with retryable error codes.
|
|
4
|
+
*/
|
|
5
|
+
import { type StructuredError } from "./errors.js";
|
|
6
|
+
import type { ExchangeAdapter } from "./exchanges/interface.js";
|
|
7
|
+
export interface RetryOptions {
|
|
8
|
+
maxRetries?: number;
|
|
9
|
+
baseDelayMs?: number;
|
|
10
|
+
maxDelayMs?: number;
|
|
11
|
+
backoffMultiplier?: number;
|
|
12
|
+
onRetry?: (attempt: number, error: StructuredError, delayMs: number) => void;
|
|
13
|
+
}
|
|
14
|
+
export interface RetryResult<T> {
|
|
15
|
+
data: T;
|
|
16
|
+
attempts: number;
|
|
17
|
+
totalDelayMs: number;
|
|
18
|
+
retries: {
|
|
19
|
+
attempt: number;
|
|
20
|
+
error: StructuredError;
|
|
21
|
+
delayMs: number;
|
|
22
|
+
}[];
|
|
23
|
+
}
|
|
24
|
+
/** Error thrown when all retries are exhausted */
|
|
25
|
+
export declare class RetriesExhaustedError extends Error {
|
|
26
|
+
readonly lastError: StructuredError;
|
|
27
|
+
readonly attempts: number;
|
|
28
|
+
readonly retries: {
|
|
29
|
+
attempt: number;
|
|
30
|
+
error: StructuredError;
|
|
31
|
+
delayMs: number;
|
|
32
|
+
}[];
|
|
33
|
+
constructor(lastError: StructuredError, attempts: number, retries: {
|
|
34
|
+
attempt: number;
|
|
35
|
+
error: StructuredError;
|
|
36
|
+
delayMs: number;
|
|
37
|
+
}[]);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Apply jitter of +/-20% to a delay value.
|
|
41
|
+
* Returns a value in [delay * 0.8, delay * 1.2].
|
|
42
|
+
*/
|
|
43
|
+
export declare function applyJitter(delayMs: number): number;
|
|
44
|
+
/**
|
|
45
|
+
* Compute the delay for a given retry attempt.
|
|
46
|
+
* Uses exponential backoff, respects retryAfterMs from the error code,
|
|
47
|
+
* applies jitter, and caps at maxDelayMs.
|
|
48
|
+
*/
|
|
49
|
+
export declare function computeDelay(attempt: number, error: StructuredError, opts: Required<Omit<RetryOptions, "onRetry">>): number;
|
|
50
|
+
/**
|
|
51
|
+
* Execute `fn` with automatic retry on retryable errors.
|
|
52
|
+
*
|
|
53
|
+
* - On failure, classifies the error via `classifyError`.
|
|
54
|
+
* - If `retryable: true`, waits with exponential backoff and retries.
|
|
55
|
+
* - If `retryable: false`, throws immediately.
|
|
56
|
+
* - Uses `retryAfterMs` from the error code if available (e.g., rate limit = 1000ms).
|
|
57
|
+
* - Caps delay at `maxDelayMs`.
|
|
58
|
+
* - Adds jitter (+/-20%) to prevent thundering herd.
|
|
59
|
+
* - Returns the result with retry metadata.
|
|
60
|
+
*/
|
|
61
|
+
export declare function withRetry<T>(fn: () => Promise<T>, opts?: RetryOptions): Promise<RetryResult<T>>;
|
|
62
|
+
/**
|
|
63
|
+
* Simpler version of withRetry that just returns T (no metadata).
|
|
64
|
+
* For use in places where you just want automatic retry without caring about details.
|
|
65
|
+
*/
|
|
66
|
+
export declare function withRetrySimple<T>(fn: () => Promise<T>, maxRetries?: number): Promise<T>;
|
|
67
|
+
/**
|
|
68
|
+
* Wrap an ExchangeAdapter so every async method is automatically retried.
|
|
69
|
+
*
|
|
70
|
+
* Returns a Proxy that intercepts property access:
|
|
71
|
+
* - For non-function properties (like `name`), returns the value directly.
|
|
72
|
+
* - For function properties, returns a wrapper that calls `withRetry` around the original method.
|
|
73
|
+
*/
|
|
74
|
+
export declare function wrapAdapterWithRetry(adapter: ExchangeAdapter, opts?: RetryOptions): ExchangeAdapter;
|
package/dist/retry.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-retry middleware for exchange operations.
|
|
3
|
+
* Retries operations that fail with retryable error codes.
|
|
4
|
+
*/
|
|
5
|
+
import { classifyError } from "./errors.js";
|
|
6
|
+
/** Error thrown when all retries are exhausted */
|
|
7
|
+
export class RetriesExhaustedError extends Error {
|
|
8
|
+
lastError;
|
|
9
|
+
attempts;
|
|
10
|
+
retries;
|
|
11
|
+
constructor(lastError, attempts, retries) {
|
|
12
|
+
super(`All ${attempts} attempts failed. Last error: [${lastError.code}] ${lastError.message}`);
|
|
13
|
+
this.name = "RetriesExhaustedError";
|
|
14
|
+
this.lastError = lastError;
|
|
15
|
+
this.attempts = attempts;
|
|
16
|
+
this.retries = retries;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const DEFAULT_OPTIONS = {
|
|
20
|
+
maxRetries: 3,
|
|
21
|
+
baseDelayMs: 1000,
|
|
22
|
+
maxDelayMs: 30000,
|
|
23
|
+
backoffMultiplier: 2,
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Apply jitter of +/-20% to a delay value.
|
|
27
|
+
* Returns a value in [delay * 0.8, delay * 1.2].
|
|
28
|
+
*/
|
|
29
|
+
export function applyJitter(delayMs) {
|
|
30
|
+
const jitterFactor = 0.8 + Math.random() * 0.4; // [0.8, 1.2]
|
|
31
|
+
return Math.round(delayMs * jitterFactor);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Compute the delay for a given retry attempt.
|
|
35
|
+
* Uses exponential backoff, respects retryAfterMs from the error code,
|
|
36
|
+
* applies jitter, and caps at maxDelayMs.
|
|
37
|
+
*/
|
|
38
|
+
export function computeDelay(attempt, error, opts) {
|
|
39
|
+
// Start with exponential backoff: baseDelay * multiplier^(attempt-1)
|
|
40
|
+
let delay = opts.baseDelayMs * Math.pow(opts.backoffMultiplier, attempt - 1);
|
|
41
|
+
// If the error specifies a retryAfterMs, use the larger of the two
|
|
42
|
+
if (error.retryAfterMs !== undefined) {
|
|
43
|
+
delay = Math.max(delay, error.retryAfterMs);
|
|
44
|
+
}
|
|
45
|
+
// Cap at maxDelayMs
|
|
46
|
+
delay = Math.min(delay, opts.maxDelayMs);
|
|
47
|
+
// Apply jitter
|
|
48
|
+
return applyJitter(delay);
|
|
49
|
+
}
|
|
50
|
+
/** Internal sleep helper */
|
|
51
|
+
function sleep(ms) {
|
|
52
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Execute `fn` with automatic retry on retryable errors.
|
|
56
|
+
*
|
|
57
|
+
* - On failure, classifies the error via `classifyError`.
|
|
58
|
+
* - If `retryable: true`, waits with exponential backoff and retries.
|
|
59
|
+
* - If `retryable: false`, throws immediately.
|
|
60
|
+
* - Uses `retryAfterMs` from the error code if available (e.g., rate limit = 1000ms).
|
|
61
|
+
* - Caps delay at `maxDelayMs`.
|
|
62
|
+
* - Adds jitter (+/-20%) to prevent thundering herd.
|
|
63
|
+
* - Returns the result with retry metadata.
|
|
64
|
+
*/
|
|
65
|
+
export async function withRetry(fn, opts) {
|
|
66
|
+
const { maxRetries, baseDelayMs, maxDelayMs, backoffMultiplier } = {
|
|
67
|
+
...DEFAULT_OPTIONS,
|
|
68
|
+
...opts,
|
|
69
|
+
};
|
|
70
|
+
const resolvedOpts = { maxRetries, baseDelayMs, maxDelayMs, backoffMultiplier };
|
|
71
|
+
const retries = [];
|
|
72
|
+
let totalDelayMs = 0;
|
|
73
|
+
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
|
|
74
|
+
try {
|
|
75
|
+
const data = await fn();
|
|
76
|
+
return { data, attempts: attempt, totalDelayMs, retries };
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const classified = classifyError(err);
|
|
80
|
+
// Non-retryable error: throw immediately
|
|
81
|
+
if (!classified.retryable) {
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
// Out of retries: throw with exhaustion info
|
|
85
|
+
if (attempt > maxRetries) {
|
|
86
|
+
throw new RetriesExhaustedError(classified, attempt, retries);
|
|
87
|
+
}
|
|
88
|
+
const delayMs = computeDelay(attempt, classified, resolvedOpts);
|
|
89
|
+
// Call onRetry callback if provided
|
|
90
|
+
opts?.onRetry?.(attempt, classified, delayMs);
|
|
91
|
+
retries.push({ attempt, error: classified, delayMs });
|
|
92
|
+
totalDelayMs += delayMs;
|
|
93
|
+
await sleep(delayMs);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Unreachable, but TypeScript needs it
|
|
97
|
+
throw new Error("withRetry: unexpected exit from retry loop");
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Simpler version of withRetry that just returns T (no metadata).
|
|
101
|
+
* For use in places where you just want automatic retry without caring about details.
|
|
102
|
+
*/
|
|
103
|
+
export async function withRetrySimple(fn, maxRetries) {
|
|
104
|
+
const result = await withRetry(fn, maxRetries !== undefined ? { maxRetries } : undefined);
|
|
105
|
+
return result.data;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Wrap an ExchangeAdapter so every async method is automatically retried.
|
|
109
|
+
*
|
|
110
|
+
* Returns a Proxy that intercepts property access:
|
|
111
|
+
* - For non-function properties (like `name`), returns the value directly.
|
|
112
|
+
* - For function properties, returns a wrapper that calls `withRetry` around the original method.
|
|
113
|
+
*/
|
|
114
|
+
export function wrapAdapterWithRetry(adapter, opts) {
|
|
115
|
+
return new Proxy(adapter, {
|
|
116
|
+
get(target, prop, receiver) {
|
|
117
|
+
const value = Reflect.get(target, prop, receiver);
|
|
118
|
+
// Pass through non-function properties (e.g., `name` getter)
|
|
119
|
+
if (typeof value !== "function") {
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
// Wrap function in retry logic
|
|
123
|
+
return async function (...args) {
|
|
124
|
+
const result = await withRetry(() => value.apply(target, args), opts);
|
|
125
|
+
return result.data;
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
package/dist/risk.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ExchangeBalance, ExchangePosition } from "./exchanges/interface.js";
|
|
2
|
+
export interface RiskLimits {
|
|
3
|
+
maxDrawdownUsd: number;
|
|
4
|
+
maxPositionUsd: number;
|
|
5
|
+
maxTotalExposureUsd: number;
|
|
6
|
+
dailyLossLimitUsd: number;
|
|
7
|
+
maxPositions: number;
|
|
8
|
+
maxLeverage: number;
|
|
9
|
+
maxMarginUtilization: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function loadRiskLimits(): RiskLimits;
|
|
12
|
+
export declare function saveRiskLimits(limits: RiskLimits): void;
|
|
13
|
+
export type RiskLevel = "low" | "medium" | "high" | "critical";
|
|
14
|
+
export interface RiskViolation {
|
|
15
|
+
rule: string;
|
|
16
|
+
severity: RiskLevel;
|
|
17
|
+
message: string;
|
|
18
|
+
current: number;
|
|
19
|
+
limit: number;
|
|
20
|
+
}
|
|
21
|
+
export interface RiskAssessment {
|
|
22
|
+
level: RiskLevel;
|
|
23
|
+
violations: RiskViolation[];
|
|
24
|
+
metrics: {
|
|
25
|
+
totalEquity: number;
|
|
26
|
+
totalUnrealizedPnl: number;
|
|
27
|
+
totalMarginUsed: number;
|
|
28
|
+
totalExposure: number;
|
|
29
|
+
positionCount: number;
|
|
30
|
+
marginUtilization: number;
|
|
31
|
+
largestPositionUsd: number;
|
|
32
|
+
maxLeverageUsed: number;
|
|
33
|
+
};
|
|
34
|
+
limits: RiskLimits;
|
|
35
|
+
canTrade: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare function assessRisk(balances: {
|
|
38
|
+
exchange: string;
|
|
39
|
+
balance: ExchangeBalance;
|
|
40
|
+
}[], positions: {
|
|
41
|
+
exchange: string;
|
|
42
|
+
position: ExchangePosition;
|
|
43
|
+
}[], limits?: RiskLimits): RiskAssessment;
|
|
44
|
+
/** Pre-trade check: would this new order violate risk limits? */
|
|
45
|
+
export declare function preTradeCheck(assessment: RiskAssessment, newOrderNotional: number, newOrderLeverage: number): {
|
|
46
|
+
allowed: boolean;
|
|
47
|
+
reason?: string;
|
|
48
|
+
};
|
package/dist/risk.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
const DEFAULT_LIMITS = {
|
|
4
|
+
maxDrawdownUsd: 500,
|
|
5
|
+
maxPositionUsd: 5000,
|
|
6
|
+
maxTotalExposureUsd: 20000,
|
|
7
|
+
dailyLossLimitUsd: 200,
|
|
8
|
+
maxPositions: 10,
|
|
9
|
+
maxLeverage: 20,
|
|
10
|
+
maxMarginUtilization: 80,
|
|
11
|
+
};
|
|
12
|
+
const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
|
|
13
|
+
const RISK_FILE = resolve(PERP_DIR, "risk.json");
|
|
14
|
+
export function loadRiskLimits() {
|
|
15
|
+
if (!existsSync(RISK_FILE))
|
|
16
|
+
return { ...DEFAULT_LIMITS };
|
|
17
|
+
try {
|
|
18
|
+
const stored = JSON.parse(readFileSync(RISK_FILE, "utf-8"));
|
|
19
|
+
return { ...DEFAULT_LIMITS, ...stored };
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return { ...DEFAULT_LIMITS };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function saveRiskLimits(limits) {
|
|
26
|
+
if (!existsSync(PERP_DIR))
|
|
27
|
+
mkdirSync(PERP_DIR, { recursive: true, mode: 0o700 });
|
|
28
|
+
writeFileSync(RISK_FILE, JSON.stringify(limits, null, 2), { mode: 0o600 });
|
|
29
|
+
}
|
|
30
|
+
export function assessRisk(balances, positions, limits) {
|
|
31
|
+
const lim = limits ?? loadRiskLimits();
|
|
32
|
+
const violations = [];
|
|
33
|
+
// Compute metrics
|
|
34
|
+
let totalEquity = 0;
|
|
35
|
+
let totalUnrealizedPnl = 0;
|
|
36
|
+
let totalMarginUsed = 0;
|
|
37
|
+
for (const { balance } of balances) {
|
|
38
|
+
totalEquity += Number(balance.equity);
|
|
39
|
+
totalUnrealizedPnl += Number(balance.unrealizedPnl);
|
|
40
|
+
totalMarginUsed += Number(balance.marginUsed);
|
|
41
|
+
}
|
|
42
|
+
let totalExposure = 0;
|
|
43
|
+
let largestPositionUsd = 0;
|
|
44
|
+
let maxLeverageUsed = 0;
|
|
45
|
+
for (const { position: p } of positions) {
|
|
46
|
+
const notional = Math.abs(Number(p.size) * Number(p.markPrice));
|
|
47
|
+
totalExposure += notional;
|
|
48
|
+
if (notional > largestPositionUsd)
|
|
49
|
+
largestPositionUsd = notional;
|
|
50
|
+
const lev = Number(p.leverage) || 0;
|
|
51
|
+
if (lev > maxLeverageUsed)
|
|
52
|
+
maxLeverageUsed = lev;
|
|
53
|
+
}
|
|
54
|
+
const marginUtilization = totalEquity > 0 ? (totalMarginUsed / totalEquity) * 100 : 0;
|
|
55
|
+
// Check violations
|
|
56
|
+
if (totalUnrealizedPnl < -lim.maxDrawdownUsd) {
|
|
57
|
+
violations.push({
|
|
58
|
+
rule: "max_drawdown",
|
|
59
|
+
severity: "critical",
|
|
60
|
+
message: `Unrealized loss $${Math.abs(totalUnrealizedPnl).toFixed(2)} exceeds max drawdown $${lim.maxDrawdownUsd}`,
|
|
61
|
+
current: Math.abs(totalUnrealizedPnl),
|
|
62
|
+
limit: lim.maxDrawdownUsd,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (largestPositionUsd > lim.maxPositionUsd) {
|
|
66
|
+
violations.push({
|
|
67
|
+
rule: "max_position_size",
|
|
68
|
+
severity: "high",
|
|
69
|
+
message: `Largest position $${largestPositionUsd.toFixed(2)} exceeds limit $${lim.maxPositionUsd}`,
|
|
70
|
+
current: largestPositionUsd,
|
|
71
|
+
limit: lim.maxPositionUsd,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (totalExposure > lim.maxTotalExposureUsd) {
|
|
75
|
+
violations.push({
|
|
76
|
+
rule: "max_total_exposure",
|
|
77
|
+
severity: "high",
|
|
78
|
+
message: `Total exposure $${totalExposure.toFixed(2)} exceeds limit $${lim.maxTotalExposureUsd}`,
|
|
79
|
+
current: totalExposure,
|
|
80
|
+
limit: lim.maxTotalExposureUsd,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (positions.length > lim.maxPositions) {
|
|
84
|
+
violations.push({
|
|
85
|
+
rule: "max_positions",
|
|
86
|
+
severity: "medium",
|
|
87
|
+
message: `${positions.length} positions exceeds limit of ${lim.maxPositions}`,
|
|
88
|
+
current: positions.length,
|
|
89
|
+
limit: lim.maxPositions,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (maxLeverageUsed > lim.maxLeverage) {
|
|
93
|
+
violations.push({
|
|
94
|
+
rule: "max_leverage",
|
|
95
|
+
severity: "high",
|
|
96
|
+
message: `Leverage ${maxLeverageUsed}x exceeds limit ${lim.maxLeverage}x`,
|
|
97
|
+
current: maxLeverageUsed,
|
|
98
|
+
limit: lim.maxLeverage,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (marginUtilization > lim.maxMarginUtilization) {
|
|
102
|
+
violations.push({
|
|
103
|
+
rule: "max_margin_utilization",
|
|
104
|
+
severity: marginUtilization > 90 ? "critical" : "high",
|
|
105
|
+
message: `Margin utilization ${marginUtilization.toFixed(1)}% exceeds limit ${lim.maxMarginUtilization}%`,
|
|
106
|
+
current: marginUtilization,
|
|
107
|
+
limit: lim.maxMarginUtilization,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// Determine overall risk level
|
|
111
|
+
let level = "low";
|
|
112
|
+
if (violations.some(v => v.severity === "critical"))
|
|
113
|
+
level = "critical";
|
|
114
|
+
else if (violations.some(v => v.severity === "high"))
|
|
115
|
+
level = "high";
|
|
116
|
+
else if (violations.some(v => v.severity === "medium"))
|
|
117
|
+
level = "medium";
|
|
118
|
+
// Can trade only if no critical violations
|
|
119
|
+
const canTrade = !violations.some(v => v.severity === "critical");
|
|
120
|
+
return {
|
|
121
|
+
level,
|
|
122
|
+
violations,
|
|
123
|
+
metrics: {
|
|
124
|
+
totalEquity,
|
|
125
|
+
totalUnrealizedPnl,
|
|
126
|
+
totalMarginUsed,
|
|
127
|
+
totalExposure,
|
|
128
|
+
positionCount: positions.length,
|
|
129
|
+
marginUtilization,
|
|
130
|
+
largestPositionUsd,
|
|
131
|
+
maxLeverageUsed,
|
|
132
|
+
},
|
|
133
|
+
limits: lim,
|
|
134
|
+
canTrade,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Pre-trade check: would this new order violate risk limits? */
|
|
138
|
+
export function preTradeCheck(assessment, newOrderNotional, newOrderLeverage) {
|
|
139
|
+
if (!assessment.canTrade) {
|
|
140
|
+
return { allowed: false, reason: "Trading suspended: critical risk violation active" };
|
|
141
|
+
}
|
|
142
|
+
const lim = assessment.limits;
|
|
143
|
+
if (newOrderNotional > lim.maxPositionUsd) {
|
|
144
|
+
return { allowed: false, reason: `Order notional $${newOrderNotional.toFixed(0)} exceeds max position size $${lim.maxPositionUsd}` };
|
|
145
|
+
}
|
|
146
|
+
if (assessment.metrics.totalExposure + newOrderNotional > lim.maxTotalExposureUsd) {
|
|
147
|
+
return { allowed: false, reason: `Would exceed total exposure limit ($${(assessment.metrics.totalExposure + newOrderNotional).toFixed(0)} > $${lim.maxTotalExposureUsd})` };
|
|
148
|
+
}
|
|
149
|
+
if (assessment.metrics.positionCount + 1 > lim.maxPositions) {
|
|
150
|
+
return { allowed: false, reason: `Would exceed max positions (${assessment.metrics.positionCount + 1} > ${lim.maxPositions})` };
|
|
151
|
+
}
|
|
152
|
+
if (newOrderLeverage > lim.maxLeverage) {
|
|
153
|
+
return { allowed: false, reason: `Leverage ${newOrderLeverage}x exceeds max ${lim.maxLeverage}x` };
|
|
154
|
+
}
|
|
155
|
+
return { allowed: true };
|
|
156
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface Settings {
|
|
2
|
+
/** Default exchange when -e flag is omitted */
|
|
3
|
+
defaultExchange: string;
|
|
4
|
+
/** Enable referral/builder codes (default: false — opt-in only) */
|
|
5
|
+
referrals: boolean;
|
|
6
|
+
/** Per-exchange referral codes (used when referrals=true) */
|
|
7
|
+
referralCodes: {
|
|
8
|
+
pacifica: string;
|
|
9
|
+
hyperliquid: string;
|
|
10
|
+
lighter: string;
|
|
11
|
+
};
|
|
12
|
+
/** Track which exchanges have had referral codes applied (one-time) */
|
|
13
|
+
referralApplied: {
|
|
14
|
+
hyperliquid: boolean;
|
|
15
|
+
lighter: boolean;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export declare function loadSettings(): Settings;
|
|
19
|
+
export declare function saveSettings(settings: Settings): void;
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
|
|
4
|
+
const SETTINGS_FILE = resolve(PERP_DIR, "settings.json");
|
|
5
|
+
const DEFAULTS = {
|
|
6
|
+
defaultExchange: "",
|
|
7
|
+
referrals: false,
|
|
8
|
+
referralCodes: {
|
|
9
|
+
pacifica: "",
|
|
10
|
+
hyperliquid: "HYPERCASH",
|
|
11
|
+
lighter: "718585MY",
|
|
12
|
+
},
|
|
13
|
+
referralApplied: {
|
|
14
|
+
hyperliquid: false,
|
|
15
|
+
lighter: false,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
export function loadSettings() {
|
|
19
|
+
if (!existsSync(SETTINGS_FILE))
|
|
20
|
+
return { ...DEFAULTS, referralCodes: { ...DEFAULTS.referralCodes } };
|
|
21
|
+
try {
|
|
22
|
+
const stored = JSON.parse(readFileSync(SETTINGS_FILE, "utf-8"));
|
|
23
|
+
return {
|
|
24
|
+
defaultExchange: stored.defaultExchange ?? DEFAULTS.defaultExchange,
|
|
25
|
+
referrals: stored.referrals ?? DEFAULTS.referrals,
|
|
26
|
+
referralCodes: {
|
|
27
|
+
pacifica: stored.referralCodes?.pacifica ?? DEFAULTS.referralCodes.pacifica,
|
|
28
|
+
hyperliquid: stored.referralCodes?.hyperliquid ?? DEFAULTS.referralCodes.hyperliquid,
|
|
29
|
+
lighter: stored.referralCodes?.lighter ?? DEFAULTS.referralCodes.lighter,
|
|
30
|
+
},
|
|
31
|
+
referralApplied: {
|
|
32
|
+
hyperliquid: stored.referralApplied?.hyperliquid ?? DEFAULTS.referralApplied.hyperliquid,
|
|
33
|
+
lighter: stored.referralApplied?.lighter ?? DEFAULTS.referralApplied.lighter,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return { ...DEFAULTS, referralCodes: { ...DEFAULTS.referralCodes } };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function saveSettings(settings) {
|
|
42
|
+
if (!existsSync(PERP_DIR))
|
|
43
|
+
mkdirSync(PERP_DIR, { recursive: true, mode: 0o700 });
|
|
44
|
+
writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
45
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ExchangeAdapter } from "../exchanges/interface.js";
|
|
2
|
+
export interface DCAParams {
|
|
3
|
+
symbol: string;
|
|
4
|
+
side: "buy" | "sell";
|
|
5
|
+
amountPerOrder: number;
|
|
6
|
+
intervalSec: number;
|
|
7
|
+
totalOrders: number;
|
|
8
|
+
priceLimit?: number;
|
|
9
|
+
maxRuntime?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface DCAState {
|
|
12
|
+
ordersPlaced: number;
|
|
13
|
+
totalFilled: number;
|
|
14
|
+
totalCost: number;
|
|
15
|
+
avgPrice: number;
|
|
16
|
+
errors: number;
|
|
17
|
+
startedAt: number;
|
|
18
|
+
running: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function runDCA(adapter: ExchangeAdapter, params: DCAParams, jobId?: string, log?: (msg: string) => void): Promise<{
|
|
21
|
+
ordersPlaced: number;
|
|
22
|
+
totalFilled: number;
|
|
23
|
+
avgPrice: number;
|
|
24
|
+
runtime: number;
|
|
25
|
+
}>;
|