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,327 @@
|
|
|
1
|
+
import { printJson, jsonOk, jsonError, makeTable, formatUsd } from "../utils.js";
|
|
2
|
+
import { getHistoricalRates } from "../funding-history.js";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
export function registerBacktestCommands(program, isJson) {
|
|
5
|
+
const backtest = program.command("backtest").description("Backtest trading strategies on historical data");
|
|
6
|
+
// ── Funding Arbitrage Backtest ──
|
|
7
|
+
backtest
|
|
8
|
+
.command("funding-arb")
|
|
9
|
+
.description("Backtest funding rate arbitrage strategy")
|
|
10
|
+
.requiredOption("--symbol <sym>", "Symbol to backtest (e.g., BTC)")
|
|
11
|
+
.option("--days <n>", "Number of days to backtest", "30")
|
|
12
|
+
.option("--spread-entry <pct>", "Annual spread % to enter (default: 10)", "10")
|
|
13
|
+
.option("--spread-close <pct>", "Annual spread % to close (default: 5)", "5")
|
|
14
|
+
.option("--exchanges <list>", "Comma-separated exchange pair (e.g., hyperliquid,pacifica)", "hyperliquid,pacifica")
|
|
15
|
+
.option("--size-usd <usd>", "Position size in USD per leg", "1000")
|
|
16
|
+
.action(async (opts) => {
|
|
17
|
+
const sym = opts.symbol.toUpperCase();
|
|
18
|
+
const days = parseInt(opts.days);
|
|
19
|
+
const spreadEntry = parseFloat(opts.spreadEntry);
|
|
20
|
+
const spreadClose = parseFloat(opts.spreadClose);
|
|
21
|
+
const sizeUsd = parseFloat(opts.sizeUsd);
|
|
22
|
+
const [exchA, exchB] = opts.exchanges.split(",").map(e => e.trim().toLowerCase());
|
|
23
|
+
if (!exchA || !exchB) {
|
|
24
|
+
if (isJson())
|
|
25
|
+
return printJson(jsonError("INVALID_PARAMS", "Need exactly 2 exchanges (e.g., --exchanges hyperliquid,pacifica)"));
|
|
26
|
+
console.error(chalk.red("Error: Need exactly 2 exchanges (e.g., --exchanges hyperliquid,pacifica)"));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const endTime = new Date();
|
|
30
|
+
const startTime = new Date(endTime.getTime() - days * 24 * 60 * 60 * 1000);
|
|
31
|
+
// Get historical funding data
|
|
32
|
+
const ratesA = getHistoricalRates(sym, exchA, startTime, endTime);
|
|
33
|
+
const ratesB = getHistoricalRates(sym, exchB, startTime, endTime);
|
|
34
|
+
if (ratesA.length === 0 && ratesB.length === 0) {
|
|
35
|
+
if (isJson())
|
|
36
|
+
return printJson(jsonError("NO_DATA", `No historical funding data for ${sym}. Run 'perp funding snapshot' first to collect data.`));
|
|
37
|
+
console.log(chalk.yellow(`\n No historical funding data for ${sym} on ${exchA}/${exchB}.`));
|
|
38
|
+
console.log(chalk.yellow(` Run 'perp funding snapshot' periodically to collect data first.\n`));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Build time-aligned rate pairs
|
|
42
|
+
const rateMap = new Map();
|
|
43
|
+
for (const r of ratesA) {
|
|
44
|
+
// Round timestamp to nearest hour for alignment
|
|
45
|
+
const hourKey = new Date(r.ts).toISOString().slice(0, 13);
|
|
46
|
+
if (!rateMap.has(hourKey))
|
|
47
|
+
rateMap.set(hourKey, {});
|
|
48
|
+
rateMap.get(hourKey).a = r.hourlyRate;
|
|
49
|
+
}
|
|
50
|
+
for (const r of ratesB) {
|
|
51
|
+
const hourKey = new Date(r.ts).toISOString().slice(0, 13);
|
|
52
|
+
if (!rateMap.has(hourKey))
|
|
53
|
+
rateMap.set(hourKey, {});
|
|
54
|
+
rateMap.get(hourKey).b = r.hourlyRate;
|
|
55
|
+
}
|
|
56
|
+
// Sort by time
|
|
57
|
+
const sortedKeys = [...rateMap.keys()].sort();
|
|
58
|
+
// Simulate
|
|
59
|
+
let inPosition = false;
|
|
60
|
+
let entrySpread = 0;
|
|
61
|
+
let entryTime = "";
|
|
62
|
+
let totalTrades = 0;
|
|
63
|
+
let totalFundingCollected = 0;
|
|
64
|
+
let totalHoldingHours = 0;
|
|
65
|
+
const trades = [];
|
|
66
|
+
for (const key of sortedKeys) {
|
|
67
|
+
const pair = rateMap.get(key);
|
|
68
|
+
if (pair.a === undefined || pair.b === undefined)
|
|
69
|
+
continue;
|
|
70
|
+
// Annual spread = |rateA - rateB| * 8760 * 100
|
|
71
|
+
const hourlySpread = Math.abs(pair.a - pair.b);
|
|
72
|
+
const annualSpreadPct = hourlySpread * 8760 * 100;
|
|
73
|
+
if (!inPosition && annualSpreadPct >= spreadEntry) {
|
|
74
|
+
inPosition = true;
|
|
75
|
+
entrySpread = annualSpreadPct;
|
|
76
|
+
entryTime = key;
|
|
77
|
+
}
|
|
78
|
+
else if (inPosition && annualSpreadPct < spreadClose) {
|
|
79
|
+
// Close position
|
|
80
|
+
const holdingHours = (new Date(key).getTime() - new Date(entryTime).getTime()) / (1000 * 60 * 60);
|
|
81
|
+
// Funding collected = sum of hourly spreads during holding period
|
|
82
|
+
let fundingCollected = 0;
|
|
83
|
+
for (const hk of sortedKeys) {
|
|
84
|
+
if (hk >= entryTime && hk <= key) {
|
|
85
|
+
const p = rateMap.get(hk);
|
|
86
|
+
if (p.a !== undefined && p.b !== undefined) {
|
|
87
|
+
fundingCollected += Math.abs(p.a - p.b) * sizeUsd;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
trades.push({
|
|
92
|
+
entryTime,
|
|
93
|
+
exitTime: key,
|
|
94
|
+
holdingHours,
|
|
95
|
+
fundingCollected,
|
|
96
|
+
spread: entrySpread,
|
|
97
|
+
});
|
|
98
|
+
totalTrades++;
|
|
99
|
+
totalFundingCollected += fundingCollected;
|
|
100
|
+
totalHoldingHours += holdingHours;
|
|
101
|
+
inPosition = false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Summary
|
|
105
|
+
const avgHoldingHours = totalTrades > 0 ? totalHoldingHours / totalTrades : 0;
|
|
106
|
+
// Rough PnL estimate: funding collected minus estimated trading costs (0.1% per trade * 2 legs * 2 trades)
|
|
107
|
+
const tradingCosts = totalTrades * 2 * 2 * sizeUsd * 0.001;
|
|
108
|
+
const netPnl = totalFundingCollected - tradingCosts;
|
|
109
|
+
const summary = {
|
|
110
|
+
symbol: sym,
|
|
111
|
+
exchanges: `${exchA} vs ${exchB}`,
|
|
112
|
+
period: `${days} days`,
|
|
113
|
+
dataPoints: sortedKeys.length,
|
|
114
|
+
spreadEntryThreshold: `${spreadEntry}%`,
|
|
115
|
+
spreadCloseThreshold: `${spreadClose}%`,
|
|
116
|
+
sizeUsd,
|
|
117
|
+
totalTrades,
|
|
118
|
+
avgHoldingPeriod: `${avgHoldingHours.toFixed(1)}h`,
|
|
119
|
+
totalFundingCollected: `$${totalFundingCollected.toFixed(2)}`,
|
|
120
|
+
tradingCosts: `$${tradingCosts.toFixed(2)}`,
|
|
121
|
+
netPnl: `$${netPnl.toFixed(2)}`,
|
|
122
|
+
trades,
|
|
123
|
+
};
|
|
124
|
+
if (isJson())
|
|
125
|
+
return printJson(jsonOk(summary));
|
|
126
|
+
console.log(chalk.cyan.bold(`\n Funding Arb Backtest — ${sym}\n`));
|
|
127
|
+
console.log(` Exchanges: ${exchA} vs ${exchB}`);
|
|
128
|
+
console.log(` Period: ${days} days (${sortedKeys.length} data points)`);
|
|
129
|
+
console.log(` Entry spread: >= ${spreadEntry}% annualized`);
|
|
130
|
+
console.log(` Close spread: < ${spreadClose}% annualized`);
|
|
131
|
+
console.log(` Size per leg: $${formatUsd(String(sizeUsd))}`);
|
|
132
|
+
console.log();
|
|
133
|
+
console.log(chalk.white.bold(` Results:`));
|
|
134
|
+
console.log(` Total trades: ${totalTrades}`);
|
|
135
|
+
console.log(` Avg holding period: ${avgHoldingHours.toFixed(1)}h`);
|
|
136
|
+
console.log(` Funding collected: ${chalk.green(`$${totalFundingCollected.toFixed(2)}`)}`);
|
|
137
|
+
console.log(` Trading costs: ${chalk.red(`$${tradingCosts.toFixed(2)}`)}`);
|
|
138
|
+
const pnlColor = netPnl >= 0 ? chalk.green : chalk.red;
|
|
139
|
+
console.log(` Net PnL: ${pnlColor(`$${netPnl.toFixed(2)}`)}`);
|
|
140
|
+
if (trades.length > 0) {
|
|
141
|
+
console.log(chalk.white.bold(`\n Trade History:`));
|
|
142
|
+
const rows = trades.map((t, i) => [
|
|
143
|
+
String(i + 1),
|
|
144
|
+
t.entryTime.replace("T", " "),
|
|
145
|
+
t.exitTime.replace("T", " "),
|
|
146
|
+
`${t.holdingHours.toFixed(1)}h`,
|
|
147
|
+
`${t.spread.toFixed(1)}%`,
|
|
148
|
+
`$${t.fundingCollected.toFixed(2)}`,
|
|
149
|
+
]);
|
|
150
|
+
console.log(makeTable(["#", "Entry", "Exit", "Duration", "Spread", "Funding"], rows));
|
|
151
|
+
}
|
|
152
|
+
console.log();
|
|
153
|
+
});
|
|
154
|
+
// ── Grid Backtest ──
|
|
155
|
+
backtest
|
|
156
|
+
.command("grid")
|
|
157
|
+
.description("Backtest grid trading strategy on historical klines")
|
|
158
|
+
.requiredOption("--symbol <sym>", "Symbol to backtest (e.g., ETH)")
|
|
159
|
+
.requiredOption("--upper <price>", "Upper price bound")
|
|
160
|
+
.requiredOption("--lower <price>", "Lower price bound")
|
|
161
|
+
.option("--grids <n>", "Number of grid lines", "10")
|
|
162
|
+
.option("--days <n>", "Number of days to backtest", "7")
|
|
163
|
+
.option("--size <base>", "Size per grid in base currency", "0.1")
|
|
164
|
+
.action(async (opts) => {
|
|
165
|
+
const sym = opts.symbol.toUpperCase();
|
|
166
|
+
const upperPrice = parseFloat(opts.upper);
|
|
167
|
+
const lowerPrice = parseFloat(opts.lower);
|
|
168
|
+
const grids = parseInt(opts.grids);
|
|
169
|
+
const days = parseInt(opts.days);
|
|
170
|
+
const sizePerGrid = parseFloat(opts.size);
|
|
171
|
+
if (upperPrice <= lowerPrice) {
|
|
172
|
+
if (isJson())
|
|
173
|
+
return printJson(jsonError("INVALID_PARAMS", "Upper price must be greater than lower price"));
|
|
174
|
+
console.error(chalk.red("Error: Upper price must be greater than lower price"));
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
if (grids < 2) {
|
|
178
|
+
if (isJson())
|
|
179
|
+
return printJson(jsonError("INVALID_PARAMS", "Need at least 2 grid lines"));
|
|
180
|
+
console.error(chalk.red("Error: Need at least 2 grid lines"));
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
const endTime = Date.now();
|
|
184
|
+
const startTime = endTime - days * 24 * 60 * 60 * 1000;
|
|
185
|
+
// Fetch historical klines from Hyperliquid
|
|
186
|
+
if (!isJson()) {
|
|
187
|
+
console.log(chalk.gray(`\n Fetching ${days}d of 1h klines for ${sym}...`));
|
|
188
|
+
}
|
|
189
|
+
let klines;
|
|
190
|
+
try {
|
|
191
|
+
const resp = await fetch("https://api.hyperliquid.xyz/info", {
|
|
192
|
+
method: "POST",
|
|
193
|
+
headers: { "Content-Type": "application/json" },
|
|
194
|
+
body: JSON.stringify({
|
|
195
|
+
type: "candleSnapshot",
|
|
196
|
+
req: {
|
|
197
|
+
coin: sym,
|
|
198
|
+
interval: "1h",
|
|
199
|
+
startTime,
|
|
200
|
+
endTime,
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
if (!resp.ok)
|
|
205
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
206
|
+
const data = await resp.json();
|
|
207
|
+
klines = data.map(k => ({ t: k.t, o: k.o, h: k.h, l: k.l, c: k.c }));
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
211
|
+
if (isJson())
|
|
212
|
+
return printJson(jsonError("FETCH_ERROR", `Failed to fetch klines: ${msg}`));
|
|
213
|
+
console.error(chalk.red(`Error fetching klines: ${msg}`));
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
if (klines.length === 0) {
|
|
217
|
+
if (isJson())
|
|
218
|
+
return printJson(jsonError("NO_DATA", `No kline data for ${sym}`));
|
|
219
|
+
console.log(chalk.yellow(`\n No kline data available for ${sym}.\n`));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// Build grid levels
|
|
223
|
+
const step = (upperPrice - lowerPrice) / (grids - 1);
|
|
224
|
+
const gridLevels = [];
|
|
225
|
+
for (let i = 0; i < grids; i++) {
|
|
226
|
+
gridLevels.push(lowerPrice + step * i);
|
|
227
|
+
}
|
|
228
|
+
// Simulate grid fills
|
|
229
|
+
// Track which grid levels have pending orders (buy below current, sell above)
|
|
230
|
+
let totalTrades = 0;
|
|
231
|
+
let totalProfit = 0;
|
|
232
|
+
let maxDrawdown = 0;
|
|
233
|
+
let currentDrawdown = 0;
|
|
234
|
+
let peakProfit = 0;
|
|
235
|
+
const filledBuys = new Set(); // grid indices that have been bought
|
|
236
|
+
// Initialize: determine initial grid state based on first kline
|
|
237
|
+
const firstPrice = parseFloat(klines[0].c);
|
|
238
|
+
for (let i = 0; i < gridLevels.length; i++) {
|
|
239
|
+
if (gridLevels[i] < firstPrice) {
|
|
240
|
+
// Below current price: place buy orders (unfilled)
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
// Above current price: assume we've "bought" these to sell
|
|
244
|
+
filledBuys.add(i);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const kline of klines) {
|
|
248
|
+
const low = parseFloat(kline.l);
|
|
249
|
+
const high = parseFloat(kline.h);
|
|
250
|
+
// Check buy fills (price dipped to grid level)
|
|
251
|
+
for (let i = 0; i < gridLevels.length; i++) {
|
|
252
|
+
if (!filledBuys.has(i) && low <= gridLevels[i]) {
|
|
253
|
+
filledBuys.add(i);
|
|
254
|
+
totalTrades++;
|
|
255
|
+
// Bought at grid level
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Check sell fills (price rose to grid level)
|
|
259
|
+
for (let i = 0; i < gridLevels.length; i++) {
|
|
260
|
+
if (filledBuys.has(i) && high >= gridLevels[i] && i > 0) {
|
|
261
|
+
// Check if there's a higher grid to sell at
|
|
262
|
+
const sellIdx = i;
|
|
263
|
+
// Find next unfilled buy below to pair with
|
|
264
|
+
for (let j = sellIdx - 1; j >= 0; j--) {
|
|
265
|
+
if (filledBuys.has(j))
|
|
266
|
+
continue;
|
|
267
|
+
// Grid profit = sell level - buy level
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
// Simple model: profit is one step worth
|
|
271
|
+
if (filledBuys.has(i)) {
|
|
272
|
+
filledBuys.delete(i);
|
|
273
|
+
totalTrades++;
|
|
274
|
+
totalProfit += step * sizePerGrid;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Track drawdown
|
|
279
|
+
if (totalProfit > peakProfit)
|
|
280
|
+
peakProfit = totalProfit;
|
|
281
|
+
currentDrawdown = peakProfit - totalProfit;
|
|
282
|
+
if (currentDrawdown > maxDrawdown)
|
|
283
|
+
maxDrawdown = currentDrawdown;
|
|
284
|
+
}
|
|
285
|
+
// Calculate some stats
|
|
286
|
+
const lastPrice = parseFloat(klines[klines.length - 1].c);
|
|
287
|
+
const priceRange = `$${formatUsd(String(Math.min(...klines.map(k => parseFloat(k.l)))))} - $${formatUsd(String(Math.max(...klines.map(k => parseFloat(k.h)))))}`;
|
|
288
|
+
const tradingFees = totalTrades * sizePerGrid * lastPrice * 0.00035; // ~0.035% per trade
|
|
289
|
+
const netProfit = totalProfit - tradingFees;
|
|
290
|
+
const summary = {
|
|
291
|
+
symbol: sym,
|
|
292
|
+
period: `${days} days`,
|
|
293
|
+
klines: klines.length,
|
|
294
|
+
priceRange,
|
|
295
|
+
gridRange: `$${formatUsd(String(lowerPrice))} - $${formatUsd(String(upperPrice))}`,
|
|
296
|
+
grids,
|
|
297
|
+
step: `$${step.toFixed(2)}`,
|
|
298
|
+
sizePerGrid,
|
|
299
|
+
totalTrades,
|
|
300
|
+
grossProfit: `$${totalProfit.toFixed(2)}`,
|
|
301
|
+
tradingFees: `$${tradingFees.toFixed(2)}`,
|
|
302
|
+
netProfit: `$${netProfit.toFixed(2)}`,
|
|
303
|
+
maxDrawdown: `$${maxDrawdown.toFixed(2)}`,
|
|
304
|
+
profitPerTrade: totalTrades > 0 ? `$${(netProfit / totalTrades).toFixed(2)}` : "$0.00",
|
|
305
|
+
};
|
|
306
|
+
if (isJson())
|
|
307
|
+
return printJson(jsonOk(summary));
|
|
308
|
+
console.log(chalk.cyan.bold(`\n Grid Backtest — ${sym}\n`));
|
|
309
|
+
console.log(` Period: ${days} days (${klines.length} candles)`);
|
|
310
|
+
console.log(` Price range: ${priceRange}`);
|
|
311
|
+
console.log(` Grid range: $${formatUsd(String(lowerPrice))} - $${formatUsd(String(upperPrice))}`);
|
|
312
|
+
console.log(` Grid lines: ${grids} (step: $${step.toFixed(2)})`);
|
|
313
|
+
console.log(` Size per grid: ${sizePerGrid}`);
|
|
314
|
+
console.log();
|
|
315
|
+
console.log(chalk.white.bold(` Results:`));
|
|
316
|
+
console.log(` Total trades: ${totalTrades}`);
|
|
317
|
+
console.log(` Gross profit: ${chalk.green(`$${totalProfit.toFixed(2)}`)}`);
|
|
318
|
+
console.log(` Trading fees: ${chalk.red(`$${tradingFees.toFixed(2)}`)}`);
|
|
319
|
+
const pnlColor = netProfit >= 0 ? chalk.green : chalk.red;
|
|
320
|
+
console.log(` Net profit: ${pnlColor(`$${netProfit.toFixed(2)}`)}`);
|
|
321
|
+
console.log(` Max drawdown: ${chalk.red(`$${maxDrawdown.toFixed(2)}`)}`);
|
|
322
|
+
if (totalTrades > 0) {
|
|
323
|
+
console.log(` Avg profit/trade: $${(netProfit / totalTrades).toFixed(2)}`);
|
|
324
|
+
}
|
|
325
|
+
console.log();
|
|
326
|
+
});
|
|
327
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import type { ExchangeAdapter } from "../exchanges/interface.js";
|
|
3
|
+
export declare function registerBotCommands(program: Command, getAdapter: () => Promise<ExchangeAdapter>, getAdapterFor: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean): void;
|