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,114 @@
|
|
|
1
|
+
import { updateJobState } from "../jobs.js";
|
|
2
|
+
export async function runDCA(adapter, params, jobId, log = console.log) {
|
|
3
|
+
const { symbol, side, amountPerOrder, intervalSec, totalOrders } = params;
|
|
4
|
+
const maxRuntime = (params.maxRuntime ?? 0) * 1000;
|
|
5
|
+
const state = {
|
|
6
|
+
ordersPlaced: 0,
|
|
7
|
+
totalFilled: 0,
|
|
8
|
+
totalCost: 0,
|
|
9
|
+
avgPrice: 0,
|
|
10
|
+
errors: 0,
|
|
11
|
+
startedAt: Date.now(),
|
|
12
|
+
running: true,
|
|
13
|
+
};
|
|
14
|
+
const target = totalOrders > 0 ? `${totalOrders} orders` : "unlimited (Ctrl+C to stop)";
|
|
15
|
+
log(`[DCA] ${side.toUpperCase()} ${amountPerOrder} ${symbol} every ${intervalSec}s | ${target}`);
|
|
16
|
+
if (params.priceLimit) {
|
|
17
|
+
log(`[DCA] Price limit: $${params.priceLimit} (${side === "buy" ? "won't buy above" : "won't sell below"})`);
|
|
18
|
+
}
|
|
19
|
+
// Graceful shutdown
|
|
20
|
+
const shutdown = () => { state.running = false; };
|
|
21
|
+
process.on("SIGINT", shutdown);
|
|
22
|
+
process.on("SIGTERM", shutdown);
|
|
23
|
+
try {
|
|
24
|
+
while (state.running) {
|
|
25
|
+
// Check if we've reached order limit
|
|
26
|
+
if (totalOrders > 0 && state.ordersPlaced >= totalOrders) {
|
|
27
|
+
log(`[DCA] Reached target of ${totalOrders} orders. Done.`);
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
// Check max runtime
|
|
31
|
+
if (maxRuntime > 0 && Date.now() - state.startedAt > maxRuntime) {
|
|
32
|
+
log(`[DCA] Max runtime reached. Stopping.`);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
// Check price limit
|
|
36
|
+
if (params.priceLimit) {
|
|
37
|
+
try {
|
|
38
|
+
const markets = await adapter.getMarkets();
|
|
39
|
+
const market = markets.find(m => m.symbol.toUpperCase() === symbol.toUpperCase());
|
|
40
|
+
if (market) {
|
|
41
|
+
const price = parseFloat(market.markPrice);
|
|
42
|
+
if (side === "buy" && price > params.priceLimit) {
|
|
43
|
+
log(`[DCA] Price $${price.toFixed(2)} > limit $${params.priceLimit}. Skipping.`);
|
|
44
|
+
await sleep(intervalSec * 1000);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (side === "sell" && price < params.priceLimit) {
|
|
48
|
+
log(`[DCA] Price $${price.toFixed(2)} < limit $${params.priceLimit}. Skipping.`);
|
|
49
|
+
await sleep(intervalSec * 1000);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// non-critical, proceed with order
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Place market order
|
|
59
|
+
try {
|
|
60
|
+
const result = await adapter.marketOrder(symbol, side, String(amountPerOrder));
|
|
61
|
+
state.ordersPlaced++;
|
|
62
|
+
state.totalFilled += amountPerOrder;
|
|
63
|
+
const fillPrice = Number(result?.price ?? result?.avg_price ?? result?.fill_price ?? 0);
|
|
64
|
+
if (fillPrice > 0) {
|
|
65
|
+
state.totalCost += amountPerOrder * fillPrice;
|
|
66
|
+
state.avgPrice = state.totalCost / state.totalFilled;
|
|
67
|
+
}
|
|
68
|
+
const progress = totalOrders > 0 ? ` (${state.ordersPlaced}/${totalOrders})` : "";
|
|
69
|
+
log(`[DCA] Order #${state.ordersPlaced}${progress}: ${side} ${amountPerOrder} ${symbol}${state.avgPrice > 0 ? ` @ $${state.avgPrice.toFixed(2)} avg` : ""}`);
|
|
70
|
+
// Update job state
|
|
71
|
+
if (jobId) {
|
|
72
|
+
updateJobState(jobId, {
|
|
73
|
+
result: {
|
|
74
|
+
ordersPlaced: state.ordersPlaced,
|
|
75
|
+
totalFilled: state.totalFilled,
|
|
76
|
+
avgPrice: state.avgPrice,
|
|
77
|
+
errors: state.errors,
|
|
78
|
+
runtime: Math.floor((Date.now() - state.startedAt) / 1000),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
state.errors++;
|
|
85
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
86
|
+
log(`[DCA] Order error: ${msg}`);
|
|
87
|
+
if (state.errors > 10 && state.errors > state.ordersPlaced) {
|
|
88
|
+
log(`[DCA] Too many errors. Stopping.`);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Wait for next interval
|
|
93
|
+
if (state.running && (totalOrders === 0 || state.ordersPlaced < totalOrders)) {
|
|
94
|
+
await sleep(intervalSec * 1000);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
process.removeListener("SIGINT", shutdown);
|
|
100
|
+
process.removeListener("SIGTERM", shutdown);
|
|
101
|
+
}
|
|
102
|
+
const runtime = Math.floor((Date.now() - state.startedAt) / 1000);
|
|
103
|
+
log(`[DCA] Done. ${state.ordersPlaced} orders, ${state.totalFilled} filled, avg $${state.avgPrice.toFixed(2)}, ${state.errors} errors, ${runtime}s`);
|
|
104
|
+
if (jobId) {
|
|
105
|
+
updateJobState(jobId, {
|
|
106
|
+
status: "done",
|
|
107
|
+
result: { ordersPlaced: state.ordersPlaced, totalFilled: state.totalFilled, avgPrice: state.avgPrice, runtime },
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return { ordersPlaced: state.ordersPlaced, totalFilled: state.totalFilled, avgPrice: state.avgPrice, runtime };
|
|
111
|
+
}
|
|
112
|
+
function sleep(ms) {
|
|
113
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
114
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ExchangeAdapter } from "../exchanges/interface.js";
|
|
2
|
+
export interface FundingArbParams {
|
|
3
|
+
minSpread: number;
|
|
4
|
+
closeSpread: number;
|
|
5
|
+
size: string;
|
|
6
|
+
sizeUsd?: number;
|
|
7
|
+
symbols?: string[];
|
|
8
|
+
intervalSec: number;
|
|
9
|
+
autoExecute: boolean;
|
|
10
|
+
maxPositions?: number;
|
|
11
|
+
autoRebalance?: boolean;
|
|
12
|
+
rebalanceThreshold?: number;
|
|
13
|
+
maxDrawdown?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function runFundingArb(adapters: Map<string, ExchangeAdapter>, params: FundingArbParams, jobId?: string, log?: (msg: string) => void): Promise<void>;
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { symbolMatch } from "../utils.js";
|
|
2
|
+
import { updateJobState } from "../jobs.js";
|
|
3
|
+
import { fetchAllBalances, computeRebalancePlan, hasEnoughBalance } from "../rebalance.js";
|
|
4
|
+
import { checkArbLiquidity } from "../liquidity.js";
|
|
5
|
+
import { computeAnnualSpread } from "../funding.js";
|
|
6
|
+
import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates, } from "../shared-api.js";
|
|
7
|
+
import { computeMatchedSize, reconcileArbFills } from "../arb-sizing.js";
|
|
8
|
+
async function fetchAllRates() {
|
|
9
|
+
const [pacRates, hlRates, ltRates] = await Promise.allSettled([
|
|
10
|
+
fetchPacificaPrices().then(assets => assets.map(p => ({
|
|
11
|
+
exchange: "pacifica", symbol: p.symbol, fundingRate: p.funding, markPrice: p.mark,
|
|
12
|
+
}))),
|
|
13
|
+
fetchHyperliquidMeta().then(assets => assets.map(a => ({
|
|
14
|
+
exchange: "hyperliquid", symbol: a.symbol, fundingRate: a.funding, markPrice: a.markPx,
|
|
15
|
+
}))),
|
|
16
|
+
(async () => {
|
|
17
|
+
const [details, funding] = await Promise.all([
|
|
18
|
+
fetchLighterOrderBookDetails(),
|
|
19
|
+
fetchLighterFundingRates(),
|
|
20
|
+
]);
|
|
21
|
+
const priceMap = new Map(details.map(d => [d.marketId, d.lastTradePrice]));
|
|
22
|
+
const symMap = new Map(details.map(d => [d.marketId, d.symbol]));
|
|
23
|
+
return funding.map(fr => ({
|
|
24
|
+
exchange: "lighter",
|
|
25
|
+
symbol: fr.symbol || symMap.get(fr.marketId) || "",
|
|
26
|
+
fundingRate: fr.rate,
|
|
27
|
+
markPrice: fr.markPrice || priceMap.get(fr.marketId) || 0,
|
|
28
|
+
}));
|
|
29
|
+
})(),
|
|
30
|
+
]);
|
|
31
|
+
return [
|
|
32
|
+
...(pacRates.status === "fulfilled" ? pacRates.value : []),
|
|
33
|
+
...(hlRates.status === "fulfilled" ? hlRates.value : []),
|
|
34
|
+
...(ltRates.status === "fulfilled" ? ltRates.value : []),
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
// annualize: use computeAnnualSpread from funding.ts for cross-exchange comparison
|
|
38
|
+
export async function runFundingArb(adapters, params, jobId, log = console.log) {
|
|
39
|
+
const maxPos = params.maxPositions ?? 3;
|
|
40
|
+
const closeSpread = params.closeSpread ?? 5;
|
|
41
|
+
const positions = [];
|
|
42
|
+
let cycleCount = 0;
|
|
43
|
+
let lastRebalanceCheck = 0;
|
|
44
|
+
log(`[ARB] Funding rate arbitrage started`);
|
|
45
|
+
log(`[ARB] Entry spread: >= ${params.minSpread}% | Close spread: <= ${closeSpread}%`);
|
|
46
|
+
log(`[ARB] Size: ${params.size} | Interval: ${params.intervalSec}s`);
|
|
47
|
+
log(`[ARB] Auto-execute: ${params.autoExecute} | Max positions: ${maxPos}`);
|
|
48
|
+
log(`[ARB] Exchanges: ${[...adapters.keys()].join(", ")}`);
|
|
49
|
+
if (params.autoRebalance)
|
|
50
|
+
log(`[ARB] Auto-rebalance: ON (threshold: $${params.rebalanceThreshold ?? 100})`);
|
|
51
|
+
if (params.maxDrawdown)
|
|
52
|
+
log(`[ARB] Max drawdown: $${params.maxDrawdown}`);
|
|
53
|
+
if (params.symbols?.length)
|
|
54
|
+
log(`[ARB] Symbols: ${params.symbols.join(", ")}`);
|
|
55
|
+
while (true) {
|
|
56
|
+
cycleCount++;
|
|
57
|
+
try {
|
|
58
|
+
const rates = await fetchAllRates();
|
|
59
|
+
// Group by symbol
|
|
60
|
+
const rateMap = new Map();
|
|
61
|
+
for (const r of rates) {
|
|
62
|
+
if (params.symbols?.length && !params.symbols.includes(r.symbol))
|
|
63
|
+
continue;
|
|
64
|
+
if (!rateMap.has(r.symbol))
|
|
65
|
+
rateMap.set(r.symbol, []);
|
|
66
|
+
rateMap.get(r.symbol).push(r);
|
|
67
|
+
}
|
|
68
|
+
// ── Phase 1: Check positions for close conditions ──
|
|
69
|
+
for (let i = positions.length - 1; i >= 0; i--) {
|
|
70
|
+
const pos = positions[i];
|
|
71
|
+
const symbolRates = rateMap.get(pos.symbol);
|
|
72
|
+
if (!symbolRates)
|
|
73
|
+
continue;
|
|
74
|
+
const longRate = symbolRates.find((r) => r.exchange === pos.longExchange);
|
|
75
|
+
const shortRate = symbolRates.find((r) => r.exchange === pos.shortExchange);
|
|
76
|
+
if (!longRate || !shortRate)
|
|
77
|
+
continue;
|
|
78
|
+
const currentSpread = computeAnnualSpread(shortRate.fundingRate, shortRate.exchange, longRate.fundingRate, longRate.exchange);
|
|
79
|
+
if (currentSpread <= closeSpread) {
|
|
80
|
+
log(`[ARB] CLOSE signal: ${pos.symbol} spread ${currentSpread.toFixed(1)}% <= ${closeSpread}%`);
|
|
81
|
+
if (params.autoExecute) {
|
|
82
|
+
const longAdapter = adapters.get(pos.longExchange);
|
|
83
|
+
const shortAdapter = adapters.get(pos.shortExchange);
|
|
84
|
+
if (longAdapter && shortAdapter) {
|
|
85
|
+
try {
|
|
86
|
+
await Promise.all([
|
|
87
|
+
longAdapter.marketOrder(pos.symbol, "sell", pos.size),
|
|
88
|
+
shortAdapter.marketOrder(pos.symbol, "buy", pos.size),
|
|
89
|
+
]);
|
|
90
|
+
log(`[ARB] CLOSED ${pos.symbol} — both legs unwound`);
|
|
91
|
+
// Estimate P&L from funding collected
|
|
92
|
+
const hoursOpen = (Date.now() - pos.openedAt) / (1000 * 60 * 60);
|
|
93
|
+
// entrySpread is annualized %; convert to per-hour rate
|
|
94
|
+
const hourlySpreadRate = pos.entrySpread / 100 / (24 * 365);
|
|
95
|
+
const estimatedFunding = hourlySpreadRate * hoursOpen * Number(pos.size) * pos.entryPrices.long;
|
|
96
|
+
log(`[ARB] Est. funding P&L: ~$${estimatedFunding.toFixed(2)} (${hoursOpen.toFixed(1)}h open)`);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
100
|
+
log(`[ARB] Close error: ${msg}`);
|
|
101
|
+
continue; // don't remove position if close failed
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
positions.splice(i, 1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// ── Phase 2: Max drawdown check ──
|
|
109
|
+
if (params.maxDrawdown && params.autoExecute && positions.length > 0) {
|
|
110
|
+
try {
|
|
111
|
+
const snapshots = await fetchAllBalances(adapters);
|
|
112
|
+
const totalPnl = snapshots.reduce((s, e) => s + e.unrealizedPnl, 0);
|
|
113
|
+
if (totalPnl < -params.maxDrawdown) {
|
|
114
|
+
log(`[ARB] MAX DRAWDOWN hit: uPnL $${totalPnl.toFixed(2)} < -$${params.maxDrawdown}`);
|
|
115
|
+
log(`[ARB] Closing all ${positions.length} positions...`);
|
|
116
|
+
await closeAllPositions(positions, adapters, log);
|
|
117
|
+
positions.length = 0;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch { /* non-critical */ }
|
|
121
|
+
}
|
|
122
|
+
// ── Phase 3: Balance check + rebalance trigger ──
|
|
123
|
+
let balanceSnapshots = null;
|
|
124
|
+
const shouldCheckBalance = params.autoRebalance && (Date.now() - lastRebalanceCheck > 300_000); // every 5 min
|
|
125
|
+
if (shouldCheckBalance || (params.autoExecute && positions.length < maxPos)) {
|
|
126
|
+
try {
|
|
127
|
+
balanceSnapshots = await fetchAllBalances(adapters);
|
|
128
|
+
lastRebalanceCheck = Date.now();
|
|
129
|
+
if (params.autoRebalance) {
|
|
130
|
+
const threshold = params.rebalanceThreshold ?? 100;
|
|
131
|
+
const lowExchanges = balanceSnapshots.filter((s) => s.available < threshold);
|
|
132
|
+
if (lowExchanges.length > 0) {
|
|
133
|
+
const plan = computeRebalancePlan(balanceSnapshots, { minMove: 50, reserve: 20 });
|
|
134
|
+
if (plan.moves.length > 0) {
|
|
135
|
+
log(`[ARB] Rebalance needed: ${plan.summary}`);
|
|
136
|
+
for (const move of plan.moves) {
|
|
137
|
+
log(`[ARB] $${move.amount} ${move.from} → ${move.to}`);
|
|
138
|
+
}
|
|
139
|
+
log(`[ARB] Run 'perp rebalance execute' to rebalance.`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch { /* non-critical */ }
|
|
145
|
+
}
|
|
146
|
+
// ── Phase 4: Find new entry opportunities ──
|
|
147
|
+
if (positions.length < maxPos) {
|
|
148
|
+
for (const [symbol, symbolRates] of rateMap) {
|
|
149
|
+
if (symbolRates.length < 2)
|
|
150
|
+
continue;
|
|
151
|
+
if (positions.find((p) => symbolMatch(p.symbol, symbol)))
|
|
152
|
+
continue;
|
|
153
|
+
if (positions.length >= maxPos)
|
|
154
|
+
break;
|
|
155
|
+
// Only consider exchanges we have adapters for
|
|
156
|
+
const available = symbolRates.filter((r) => adapters.has(r.exchange));
|
|
157
|
+
if (available.length < 2)
|
|
158
|
+
continue;
|
|
159
|
+
available.sort((a, b) => a.fundingRate - b.fundingRate);
|
|
160
|
+
const lowest = available[0];
|
|
161
|
+
const highest = available[available.length - 1];
|
|
162
|
+
if (lowest.exchange === highest.exchange)
|
|
163
|
+
continue;
|
|
164
|
+
const annualSpread = computeAnnualSpread(highest.fundingRate, highest.exchange, lowest.fundingRate, lowest.exchange);
|
|
165
|
+
if (annualSpread >= params.minSpread) {
|
|
166
|
+
const longEx = lowest.exchange;
|
|
167
|
+
const shortEx = highest.exchange;
|
|
168
|
+
log(`[ARB] ENTRY signal: ${symbol} spread ${annualSpread.toFixed(1)}% — long ${longEx} (${(lowest.fundingRate * 100).toFixed(4)}%) / short ${shortEx} (${(highest.fundingRate * 100).toFixed(4)}%)`);
|
|
169
|
+
if (params.autoExecute) {
|
|
170
|
+
const longAdapter = adapters.get(longEx);
|
|
171
|
+
const shortAdapter = adapters.get(shortEx);
|
|
172
|
+
if (!longAdapter || !shortAdapter) {
|
|
173
|
+
log(`[ARB] Skip: adapter not available for ${longEx} or ${shortEx}`);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// Check orderbook liquidity & adjust size
|
|
177
|
+
const requestedUsd = params.sizeUsd ?? Number(params.size) * highest.markPrice;
|
|
178
|
+
const liq = await checkArbLiquidity(longAdapter, shortAdapter, symbol, requestedUsd, 0.5, log);
|
|
179
|
+
if (!liq.viable)
|
|
180
|
+
continue;
|
|
181
|
+
// Compute matched size (same for both legs)
|
|
182
|
+
const matched = computeMatchedSize(liq.adjustedSizeUsd, highest.markPrice, longEx, shortEx);
|
|
183
|
+
if (!matched) {
|
|
184
|
+
log(`[ARB] Skip ${symbol}: can't compute matched size (min notional or precision issue)`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
// Check balances before trading
|
|
188
|
+
if (balanceSnapshots) {
|
|
189
|
+
if (!hasEnoughBalance(balanceSnapshots, longEx, matched.notional) ||
|
|
190
|
+
!hasEnoughBalance(balanceSnapshots, shortEx, matched.notional)) {
|
|
191
|
+
log(`[ARB] Skip ${symbol}: insufficient balance on ${longEx} or ${shortEx} (need ~$${matched.notional.toFixed(0)} per leg)`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
log(`[ARB] Opening: ${matched.size} ${symbol} on both legs ($${matched.notional.toFixed(0)}/leg, slippage ~${liq.longSlippage.toFixed(2)}%/${liq.shortSlippage.toFixed(2)}%)...`);
|
|
197
|
+
await Promise.all([
|
|
198
|
+
longAdapter.marketOrder(symbol, "buy", matched.size),
|
|
199
|
+
shortAdapter.marketOrder(symbol, "sell", matched.size),
|
|
200
|
+
]);
|
|
201
|
+
// Verify fills match, correct if needed
|
|
202
|
+
try {
|
|
203
|
+
const recon = await reconcileArbFills(longAdapter, shortAdapter, symbol, log);
|
|
204
|
+
if (!recon.matched) {
|
|
205
|
+
log(`[ARB] WARNING: fills not matched after correction attempt`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch { /* non-critical */ }
|
|
209
|
+
positions.push({
|
|
210
|
+
symbol,
|
|
211
|
+
longExchange: longEx,
|
|
212
|
+
shortExchange: shortEx,
|
|
213
|
+
size: matched.size,
|
|
214
|
+
entrySpread: annualSpread,
|
|
215
|
+
openedAt: Date.now(),
|
|
216
|
+
entryPrices: { long: lowest.markPrice, short: highest.markPrice },
|
|
217
|
+
});
|
|
218
|
+
log(`[ARB] OPENED ${symbol} delta-neutral (${positions.length}/${maxPos})`);
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
222
|
+
log(`[ARB] Entry error: ${msg}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// ── Update job state ──
|
|
229
|
+
if (jobId) {
|
|
230
|
+
updateJobState(jobId, {
|
|
231
|
+
result: {
|
|
232
|
+
cycle: cycleCount,
|
|
233
|
+
activePositions: positions.length,
|
|
234
|
+
positions: positions.map((p) => ({
|
|
235
|
+
symbol: p.symbol,
|
|
236
|
+
long: p.longExchange,
|
|
237
|
+
short: p.shortExchange,
|
|
238
|
+
size: p.size,
|
|
239
|
+
entrySpread: p.entrySpread,
|
|
240
|
+
hoursOpen: ((Date.now() - p.openedAt) / 3_600_000).toFixed(1),
|
|
241
|
+
})),
|
|
242
|
+
lastCheck: new Date().toISOString(),
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
// Periodic status log
|
|
247
|
+
if (cycleCount % 10 === 0) {
|
|
248
|
+
const posInfo = positions.length > 0
|
|
249
|
+
? positions.map((p) => `${p.symbol}(${p.entrySpread.toFixed(0)}%)`).join(", ")
|
|
250
|
+
: "none";
|
|
251
|
+
log(`[ARB] Cycle ${cycleCount} | ${positions.length} positions: ${posInfo} | ${rateMap.size} symbols`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
256
|
+
log(`[ARB] Cycle error: ${msg}`);
|
|
257
|
+
}
|
|
258
|
+
await sleep(params.intervalSec * 1000);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function closeAllPositions(positions, adapters, log) {
|
|
262
|
+
for (const pos of positions) {
|
|
263
|
+
const longAdapter = adapters.get(pos.longExchange);
|
|
264
|
+
const shortAdapter = adapters.get(pos.shortExchange);
|
|
265
|
+
if (!longAdapter || !shortAdapter)
|
|
266
|
+
continue;
|
|
267
|
+
try {
|
|
268
|
+
await Promise.all([
|
|
269
|
+
longAdapter.marketOrder(pos.symbol, "sell", pos.size),
|
|
270
|
+
shortAdapter.marketOrder(pos.symbol, "buy", pos.size),
|
|
271
|
+
]);
|
|
272
|
+
log(`[ARB] Emergency closed ${pos.symbol}`);
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
log(`[ARB] Failed to close ${pos.symbol}: ${err instanceof Error ? err.message : err}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function sleep(ms) {
|
|
280
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
281
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ExchangeAdapter } from "../exchanges/interface.js";
|
|
2
|
+
export interface GridParams {
|
|
3
|
+
symbol: string;
|
|
4
|
+
side: "long" | "short" | "neutral";
|
|
5
|
+
upperPrice: number;
|
|
6
|
+
lowerPrice: number;
|
|
7
|
+
grids: number;
|
|
8
|
+
totalSize: number;
|
|
9
|
+
leverage?: number;
|
|
10
|
+
intervalSec?: number;
|
|
11
|
+
maxRuntime?: number;
|
|
12
|
+
trailingStop?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface GridState {
|
|
15
|
+
gridLines: GridLine[];
|
|
16
|
+
activeOrders: Map<string, string>;
|
|
17
|
+
fills: number;
|
|
18
|
+
totalPnl: number;
|
|
19
|
+
peakEquity: number;
|
|
20
|
+
startedAt: number;
|
|
21
|
+
running: boolean;
|
|
22
|
+
}
|
|
23
|
+
interface GridLine {
|
|
24
|
+
price: number;
|
|
25
|
+
side: "buy" | "sell";
|
|
26
|
+
size: number;
|
|
27
|
+
filled: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function runGrid(adapter: ExchangeAdapter, params: GridParams, jobId?: string, log?: (msg: string) => void): Promise<{
|
|
30
|
+
fills: number;
|
|
31
|
+
totalPnl: number;
|
|
32
|
+
runtime: number;
|
|
33
|
+
}>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { updateJobState } from "../jobs.js";
|
|
2
|
+
export async function runGrid(adapter, params, jobId, log = console.log) {
|
|
3
|
+
const { symbol, upperPrice, lowerPrice, grids, totalSize } = params;
|
|
4
|
+
const intervalMs = (params.intervalSec ?? 10) * 1000;
|
|
5
|
+
const maxRuntime = (params.maxRuntime ?? 0) * 1000;
|
|
6
|
+
const sizePerGrid = totalSize / grids;
|
|
7
|
+
if (upperPrice <= lowerPrice)
|
|
8
|
+
throw new Error("upperPrice must be > lowerPrice");
|
|
9
|
+
if (grids < 2)
|
|
10
|
+
throw new Error("Need at least 2 grid lines");
|
|
11
|
+
// Set leverage if specified
|
|
12
|
+
if (params.leverage) {
|
|
13
|
+
try {
|
|
14
|
+
await adapter.setLeverage(symbol, params.leverage);
|
|
15
|
+
log(`[GRID] Leverage set to ${params.leverage}x`);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
log(`[GRID] Could not set leverage (may not be supported)`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Build grid lines
|
|
22
|
+
const step = (upperPrice - lowerPrice) / (grids - 1);
|
|
23
|
+
const gridLines = [];
|
|
24
|
+
for (let i = 0; i < grids; i++) {
|
|
25
|
+
gridLines.push({
|
|
26
|
+
price: lowerPrice + step * i,
|
|
27
|
+
side: "buy", // will be set based on current price
|
|
28
|
+
size: sizePerGrid,
|
|
29
|
+
filled: false,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
log(`[GRID] ${symbol} ${params.side} | ${grids} grids | $${lowerPrice} - $${upperPrice} | step $${step.toFixed(2)}`);
|
|
33
|
+
log(`[GRID] Size per grid: ${sizePerGrid.toFixed(6)} | Total: ${totalSize}`);
|
|
34
|
+
const state = {
|
|
35
|
+
gridLines,
|
|
36
|
+
activeOrders: new Map(),
|
|
37
|
+
fills: 0,
|
|
38
|
+
totalPnl: 0,
|
|
39
|
+
peakEquity: 0,
|
|
40
|
+
startedAt: Date.now(),
|
|
41
|
+
running: true,
|
|
42
|
+
};
|
|
43
|
+
// Graceful shutdown
|
|
44
|
+
const shutdown = () => { state.running = false; };
|
|
45
|
+
process.on("SIGINT", shutdown);
|
|
46
|
+
process.on("SIGTERM", shutdown);
|
|
47
|
+
try {
|
|
48
|
+
// Get current price to determine buy/sell sides
|
|
49
|
+
const markets = await adapter.getMarkets();
|
|
50
|
+
const market = markets.find(m => m.symbol.toUpperCase() === symbol.toUpperCase());
|
|
51
|
+
const currentPrice = market ? parseFloat(market.markPrice) : (upperPrice + lowerPrice) / 2;
|
|
52
|
+
// Assign sides: buy below current, sell above current
|
|
53
|
+
for (const line of gridLines) {
|
|
54
|
+
if (params.side === "long") {
|
|
55
|
+
line.side = "buy";
|
|
56
|
+
}
|
|
57
|
+
else if (params.side === "short") {
|
|
58
|
+
line.side = "sell";
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
line.side = line.price < currentPrice ? "buy" : "sell";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
log(`[GRID] Current price: $${currentPrice.toFixed(2)} | Placing ${grids} limit orders...`);
|
|
65
|
+
// Place initial grid orders
|
|
66
|
+
await placeGridOrders(adapter, symbol, gridLines, state, log);
|
|
67
|
+
// Main loop: monitor fills and replace orders
|
|
68
|
+
while (state.running) {
|
|
69
|
+
await sleep(intervalMs);
|
|
70
|
+
if (maxRuntime > 0 && Date.now() - state.startedAt > maxRuntime) {
|
|
71
|
+
log(`[GRID] Max runtime reached. Stopping.`);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
// Check open orders
|
|
76
|
+
const openOrders = await adapter.getOpenOrders();
|
|
77
|
+
const openIds = new Set(openOrders.filter(o => o.symbol.toUpperCase() === symbol.toUpperCase()).map(o => o.orderId));
|
|
78
|
+
// Find filled grid orders
|
|
79
|
+
let newFills = 0;
|
|
80
|
+
for (let i = 0; i < gridLines.length; i++) {
|
|
81
|
+
const orderId = state.activeOrders.get(String(i));
|
|
82
|
+
if (orderId && !openIds.has(orderId)) {
|
|
83
|
+
// Order filled — flip and replace
|
|
84
|
+
const line = gridLines[i];
|
|
85
|
+
state.fills++;
|
|
86
|
+
newFills++;
|
|
87
|
+
line.filled = true;
|
|
88
|
+
// Flip side: buy → sell, sell → buy (take profit at next grid)
|
|
89
|
+
const newSide = line.side === "buy" ? "sell" : "buy";
|
|
90
|
+
const newPrice = line.side === "buy"
|
|
91
|
+
? line.price + step // sell one grid above
|
|
92
|
+
: line.price - step; // buy one grid below
|
|
93
|
+
if (newPrice >= lowerPrice && newPrice <= upperPrice) {
|
|
94
|
+
try {
|
|
95
|
+
const result = await adapter.limitOrder(symbol, newSide, String(newPrice.toFixed(2)), String(line.size));
|
|
96
|
+
const newOrderId = String(result?.orderId ?? result?.oid ?? result?.id ?? "");
|
|
97
|
+
state.activeOrders.set(String(i), newOrderId);
|
|
98
|
+
line.side = newSide;
|
|
99
|
+
line.price = newPrice;
|
|
100
|
+
line.filled = false;
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
104
|
+
log(`[GRID] Replace order error: ${msg}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
state.activeOrders.delete(String(i));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (newFills > 0) {
|
|
113
|
+
// Estimate PnL from grid spacing
|
|
114
|
+
state.totalPnl += newFills * step * sizePerGrid;
|
|
115
|
+
log(`[GRID] ${newFills} fill(s) | Total fills: ${state.fills} | Est. PnL: $${state.totalPnl.toFixed(2)}`);
|
|
116
|
+
}
|
|
117
|
+
// Update job state
|
|
118
|
+
if (jobId) {
|
|
119
|
+
updateJobState(jobId, {
|
|
120
|
+
result: {
|
|
121
|
+
fills: state.fills,
|
|
122
|
+
totalPnl: state.totalPnl,
|
|
123
|
+
activeOrders: state.activeOrders.size,
|
|
124
|
+
runtime: Math.floor((Date.now() - state.startedAt) / 1000),
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// Trailing stop check
|
|
129
|
+
if (params.trailingStop) {
|
|
130
|
+
const bal = await adapter.getBalance();
|
|
131
|
+
const equity = parseFloat(bal.equity);
|
|
132
|
+
if (equity > state.peakEquity)
|
|
133
|
+
state.peakEquity = equity;
|
|
134
|
+
const drawdown = ((state.peakEquity - equity) / state.peakEquity) * 100;
|
|
135
|
+
if (drawdown > params.trailingStop) {
|
|
136
|
+
log(`[GRID] Trailing stop triggered (${drawdown.toFixed(1)}% drawdown). Stopping.`);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
143
|
+
log(`[GRID] Monitor error: ${msg}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
process.removeListener("SIGINT", shutdown);
|
|
149
|
+
process.removeListener("SIGTERM", shutdown);
|
|
150
|
+
// Cancel remaining grid orders
|
|
151
|
+
log(`[GRID] Cancelling remaining orders...`);
|
|
152
|
+
try {
|
|
153
|
+
await adapter.cancelAllOrders(symbol);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// best-effort
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const runtime = Math.floor((Date.now() - state.startedAt) / 1000);
|
|
160
|
+
log(`[GRID] Done. ${state.fills} fills, est. PnL $${state.totalPnl.toFixed(2)}, runtime ${runtime}s`);
|
|
161
|
+
if (jobId) {
|
|
162
|
+
updateJobState(jobId, { status: "done", result: { fills: state.fills, totalPnl: state.totalPnl, runtime } });
|
|
163
|
+
}
|
|
164
|
+
return { fills: state.fills, totalPnl: state.totalPnl, runtime };
|
|
165
|
+
}
|
|
166
|
+
async function placeGridOrders(adapter, symbol, gridLines, state, log) {
|
|
167
|
+
let placed = 0;
|
|
168
|
+
for (let i = 0; i < gridLines.length; i++) {
|
|
169
|
+
const line = gridLines[i];
|
|
170
|
+
try {
|
|
171
|
+
const result = await adapter.limitOrder(symbol, line.side, String(line.price.toFixed(2)), String(line.size));
|
|
172
|
+
const orderId = String(result?.orderId ?? result?.oid ?? result?.id ?? "");
|
|
173
|
+
state.activeOrders.set(String(i), orderId);
|
|
174
|
+
placed++;
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
178
|
+
log(`[GRID] Order at $${line.price.toFixed(2)} failed: ${msg}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
log(`[GRID] Placed ${placed}/${gridLines.length} orders`);
|
|
182
|
+
}
|
|
183
|
+
function sleep(ms) {
|
|
184
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
185
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ExchangeAdapter } from "../exchanges/interface.js";
|
|
2
|
+
export interface TrailingStopParams {
|
|
3
|
+
symbol: string;
|
|
4
|
+
trailPct: number;
|
|
5
|
+
intervalSec?: number;
|
|
6
|
+
activationPrice?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface TrailingStopResult {
|
|
9
|
+
triggered: boolean;
|
|
10
|
+
reason: "triggered" | "cancelled" | "no_position";
|
|
11
|
+
peakPrice?: number;
|
|
12
|
+
triggerPrice?: number;
|
|
13
|
+
changePct?: number;
|
|
14
|
+
positionSide?: string;
|
|
15
|
+
runtime: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function runTrailingStop(adapter: ExchangeAdapter, params: TrailingStopParams, jobId?: string, log?: (msg: string) => void): Promise<TrailingStopResult>;
|