@raintree-technology/perps 0.1.0
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/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/dist/adapters/aevo.d.ts +64 -0
- package/dist/adapters/aevo.js +899 -0
- package/dist/adapters/certification.d.ts +33 -0
- package/dist/adapters/certification.js +99 -0
- package/dist/adapters/decibel/order-manager.d.ts +45 -0
- package/dist/adapters/decibel/order-manager.js +140 -0
- package/dist/adapters/decibel/rest-client.d.ts +176 -0
- package/dist/adapters/decibel/rest-client.js +155 -0
- package/dist/adapters/decibel/ws-feed.d.ts +28 -0
- package/dist/adapters/decibel/ws-feed.js +166 -0
- package/dist/adapters/decibel.d.ts +108 -0
- package/dist/adapters/decibel.js +1377 -0
- package/dist/adapters/hyperliquid.d.ts +63 -0
- package/dist/adapters/hyperliquid.js +797 -0
- package/dist/adapters/index.d.ts +11 -0
- package/dist/adapters/index.js +12 -0
- package/dist/adapters/interface.d.ts +310 -0
- package/dist/adapters/interface.js +15 -0
- package/dist/adapters/orderly.d.ts +70 -0
- package/dist/adapters/orderly.js +936 -0
- package/dist/adapters/paradex.d.ts +69 -0
- package/dist/adapters/paradex.js +862 -0
- package/dist/adapters/utils.d.ts +17 -0
- package/dist/adapters/utils.js +122 -0
- package/dist/cli/command-metadata.d.ts +2 -0
- package/dist/cli/command-metadata.js +44 -0
- package/dist/cli/context.d.ts +14 -0
- package/dist/cli/context.js +59 -0
- package/dist/cli/experience.d.ts +48 -0
- package/dist/cli/experience.js +243 -0
- package/dist/cli/ink/app/AppShell.d.ts +12 -0
- package/dist/cli/ink/app/AppShell.js +32 -0
- package/dist/cli/ink/app/MetricStrip.d.ts +6 -0
- package/dist/cli/ink/app/MetricStrip.js +14 -0
- package/dist/cli/ink/app/Panel.d.ts +9 -0
- package/dist/cli/ink/app/Panel.js +7 -0
- package/dist/cli/ink/app/ascii.d.ts +2 -0
- package/dist/cli/ink/app/ascii.js +46 -0
- package/dist/cli/ink/app/index.d.ts +5 -0
- package/dist/cli/ink/app/index.js +4 -0
- package/dist/cli/ink/app/types.d.ts +15 -0
- package/dist/cli/ink/app/types.js +1 -0
- package/dist/cli/ink/components/PnL.d.ts +12 -0
- package/dist/cli/ink/components/PnL.js +23 -0
- package/dist/cli/ink/components/Spinner.d.ts +13 -0
- package/dist/cli/ink/components/Spinner.js +13 -0
- package/dist/cli/ink/components/Table.d.ts +14 -0
- package/dist/cli/ink/components/Table.js +42 -0
- package/dist/cli/ink/components/WatchHeader.d.ts +10 -0
- package/dist/cli/ink/components/WatchHeader.js +18 -0
- package/dist/cli/ink/components/index.d.ts +4 -0
- package/dist/cli/ink/components/index.js +4 -0
- package/dist/cli/ink/index.d.ts +4 -0
- package/dist/cli/ink/index.js +4 -0
- package/dist/cli/ink/render.d.ts +12 -0
- package/dist/cli/ink/render.js +21 -0
- package/dist/cli/ink/theme.d.ts +29 -0
- package/dist/cli/ink/theme.js +40 -0
- package/dist/cli/network-defaults.d.ts +10 -0
- package/dist/cli/network-defaults.js +35 -0
- package/dist/cli/output.d.ts +11 -0
- package/dist/cli/output.js +115 -0
- package/dist/cli/program.d.ts +18 -0
- package/dist/cli/program.js +164 -0
- package/dist/cli/watch.d.ts +19 -0
- package/dist/cli/watch.js +35 -0
- package/dist/client/index.d.ts +55 -0
- package/dist/client/index.js +157 -0
- package/dist/commands/account/add.d.ts +2 -0
- package/dist/commands/account/add.js +510 -0
- package/dist/commands/account/balances-simple.d.ts +5 -0
- package/dist/commands/account/balances-simple.js +63 -0
- package/dist/commands/account/index.d.ts +2 -0
- package/dist/commands/account/index.js +17 -0
- package/dist/commands/account/ls.d.ts +2 -0
- package/dist/commands/account/ls.js +95 -0
- package/dist/commands/account/positions-simple.d.ts +5 -0
- package/dist/commands/account/positions-simple.js +77 -0
- package/dist/commands/account/remove.d.ts +2 -0
- package/dist/commands/account/remove.js +47 -0
- package/dist/commands/account/set-default.d.ts +2 -0
- package/dist/commands/account/set-default.js +47 -0
- package/dist/commands/agent/index.d.ts +2 -0
- package/dist/commands/agent/index.js +126 -0
- package/dist/commands/arb/alert.d.ts +6 -0
- package/dist/commands/arb/alert.js +88 -0
- package/dist/commands/arb/basis-execute.d.ts +6 -0
- package/dist/commands/arb/basis-execute.js +332 -0
- package/dist/commands/arb/basis.d.ts +6 -0
- package/dist/commands/arb/basis.js +181 -0
- package/dist/commands/arb/compare.d.ts +6 -0
- package/dist/commands/arb/compare.js +216 -0
- package/dist/commands/arb/execute.d.ts +6 -0
- package/dist/commands/arb/execute.js +467 -0
- package/dist/commands/arb/funding.d.ts +6 -0
- package/dist/commands/arb/funding.js +201 -0
- package/dist/commands/arb/history.d.ts +6 -0
- package/dist/commands/arb/history.js +153 -0
- package/dist/commands/arb/index.d.ts +6 -0
- package/dist/commands/arb/index.js +29 -0
- package/dist/commands/arb/positions.d.ts +6 -0
- package/dist/commands/arb/positions.js +158 -0
- package/dist/commands/arb/spread.d.ts +6 -0
- package/dist/commands/arb/spread.js +253 -0
- package/dist/commands/arb/track.d.ts +6 -0
- package/dist/commands/arb/track.js +259 -0
- package/dist/commands/asset/book-simple.d.ts +5 -0
- package/dist/commands/asset/book-simple.js +77 -0
- package/dist/commands/asset/index.d.ts +2 -0
- package/dist/commands/asset/index.js +5 -0
- package/dist/commands/completion.d.ts +2 -0
- package/dist/commands/completion.js +161 -0
- package/dist/commands/config/index.d.ts +5 -0
- package/dist/commands/config/index.js +109 -0
- package/dist/commands/data/index.d.ts +31 -0
- package/dist/commands/data/index.js +1466 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +201 -0
- package/dist/commands/exchange/index.d.ts +2 -0
- package/dist/commands/exchange/index.js +107 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +48 -0
- package/dist/commands/markets/index.d.ts +2 -0
- package/dist/commands/markets/index.js +5 -0
- package/dist/commands/markets/ls-simple.d.ts +7 -0
- package/dist/commands/markets/ls-simple.js +277 -0
- package/dist/commands/operator/index.d.ts +2 -0
- package/dist/commands/operator/index.js +146 -0
- package/dist/commands/order/cancel-simple.d.ts +5 -0
- package/dist/commands/order/cancel-simple.js +104 -0
- package/dist/commands/order/index.d.ts +2 -0
- package/dist/commands/order/index.js +13 -0
- package/dist/commands/order/limit-simple.d.ts +5 -0
- package/dist/commands/order/limit-simple.js +195 -0
- package/dist/commands/order/market-simple.d.ts +5 -0
- package/dist/commands/order/market-simple.js +190 -0
- package/dist/commands/order/shared.d.ts +17 -0
- package/dist/commands/order/shared.js +51 -0
- package/dist/commands/order/trigger-simple.d.ts +5 -0
- package/dist/commands/order/trigger-simple.js +246 -0
- package/dist/commands/referral/index.d.ts +2 -0
- package/dist/commands/referral/index.js +7 -0
- package/dist/commands/referral/set.d.ts +2 -0
- package/dist/commands/referral/set.js +26 -0
- package/dist/commands/referral/status.d.ts +2 -0
- package/dist/commands/referral/status.js +31 -0
- package/dist/commands/replay/index.d.ts +2 -0
- package/dist/commands/replay/index.js +152 -0
- package/dist/commands/risk/analytics.d.ts +2 -0
- package/dist/commands/risk/analytics.js +64 -0
- package/dist/commands/risk/audit.d.ts +2 -0
- package/dist/commands/risk/audit.js +52 -0
- package/dist/commands/risk/index.d.ts +2 -0
- package/dist/commands/risk/index.js +9 -0
- package/dist/commands/risk/rules.d.ts +2 -0
- package/dist/commands/risk/rules.js +102 -0
- package/dist/commands/server.d.ts +2 -0
- package/dist/commands/server.js +208 -0
- package/dist/commands/setup/index.d.ts +2 -0
- package/dist/commands/setup/index.js +478 -0
- package/dist/commands/signal/index.d.ts +2 -0
- package/dist/commands/signal/index.js +129 -0
- package/dist/commands/state/index.d.ts +2 -0
- package/dist/commands/state/index.js +5 -0
- package/dist/commands/state/show.d.ts +2 -0
- package/dist/commands/state/show.js +105 -0
- package/dist/commands/strategy/index.d.ts +4 -0
- package/dist/commands/strategy/index.js +73 -0
- package/dist/commands/traces/index.d.ts +2 -0
- package/dist/commands/traces/index.js +76 -0
- package/dist/commands/ui/demo.d.ts +9 -0
- package/dist/commands/ui/demo.js +195 -0
- package/dist/commands/ui/index.d.ts +2 -0
- package/dist/commands/ui/index.js +7 -0
- package/dist/commands/ui/terminal.d.ts +2 -0
- package/dist/commands/ui/terminal.js +255 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +98 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/lib/agent/audit.d.ts +12 -0
- package/dist/lib/agent/audit.js +13 -0
- package/dist/lib/agent/gateway.d.ts +13 -0
- package/dist/lib/agent/gateway.js +598 -0
- package/dist/lib/agent/metrics.d.ts +33 -0
- package/dist/lib/agent/metrics.js +175 -0
- package/dist/lib/agent/signature.d.ts +8 -0
- package/dist/lib/agent/signature.js +28 -0
- package/dist/lib/agent/tools.d.ts +28 -0
- package/dist/lib/agent/tools.js +453 -0
- package/dist/lib/agent/x402.d.ts +23 -0
- package/dist/lib/agent/x402.js +62 -0
- package/dist/lib/api-wallet.d.ts +69 -0
- package/dist/lib/api-wallet.js +101 -0
- package/dist/lib/balance-watcher.d.ts +25 -0
- package/dist/lib/balance-watcher.js +83 -0
- package/dist/lib/book-watcher.d.ts +25 -0
- package/dist/lib/book-watcher.js +48 -0
- package/dist/lib/config.d.ts +88 -0
- package/dist/lib/config.js +427 -0
- package/dist/lib/constants.d.ts +50 -0
- package/dist/lib/constants.js +84 -0
- package/dist/lib/contracts.d.ts +7 -0
- package/dist/lib/contracts.js +8 -0
- package/dist/lib/credential-vault.d.ts +22 -0
- package/dist/lib/credential-vault.js +109 -0
- package/dist/lib/db/accounts.d.ts +83 -0
- package/dist/lib/db/accounts.js +203 -0
- package/dist/lib/db/funding-history.d.ts +69 -0
- package/dist/lib/db/funding-history.js +183 -0
- package/dist/lib/db/index.d.ts +11 -0
- package/dist/lib/db/index.js +272 -0
- package/dist/lib/events/bus.d.ts +10 -0
- package/dist/lib/events/bus.js +17 -0
- package/dist/lib/events/types.d.ts +51 -0
- package/dist/lib/events/types.js +1 -0
- package/dist/lib/exchange.d.ts +30 -0
- package/dist/lib/exchange.js +84 -0
- package/dist/lib/execution/journal.d.ts +25 -0
- package/dist/lib/execution/journal.js +158 -0
- package/dist/lib/execution/safety.d.ts +34 -0
- package/dist/lib/execution/safety.js +197 -0
- package/dist/lib/exit-codes.d.ts +18 -0
- package/dist/lib/exit-codes.js +60 -0
- package/dist/lib/fetch.d.ts +18 -0
- package/dist/lib/fetch.js +66 -0
- package/dist/lib/fs-security.d.ts +10 -0
- package/dist/lib/fs-security.js +26 -0
- package/dist/lib/funding-tracker.d.ts +40 -0
- package/dist/lib/funding-tracker.js +118 -0
- package/dist/lib/logger.d.ts +27 -0
- package/dist/lib/logger.js +82 -0
- package/dist/lib/network-model.d.ts +13 -0
- package/dist/lib/network-model.js +30 -0
- package/dist/lib/onboarding.d.ts +133 -0
- package/dist/lib/onboarding.js +1459 -0
- package/dist/lib/operator-state.d.ts +25 -0
- package/dist/lib/operator-state.js +82 -0
- package/dist/lib/orders-watcher.d.ts +24 -0
- package/dist/lib/orders-watcher.js +74 -0
- package/dist/lib/paths.d.ts +20 -0
- package/dist/lib/paths.js +23 -0
- package/dist/lib/portfolio-watcher.d.ts +33 -0
- package/dist/lib/portfolio-watcher.js +95 -0
- package/dist/lib/position-watcher.d.ts +16 -0
- package/dist/lib/position-watcher.js +44 -0
- package/dist/lib/price-watcher.d.ts +15 -0
- package/dist/lib/price-watcher.js +84 -0
- package/dist/lib/prompts.d.ts +32 -0
- package/dist/lib/prompts.js +105 -0
- package/dist/lib/rate-limit.d.ts +32 -0
- package/dist/lib/rate-limit.js +88 -0
- package/dist/lib/risk/analytics.d.ts +39 -0
- package/dist/lib/risk/analytics.js +98 -0
- package/dist/lib/risk/drawdown.d.ts +18 -0
- package/dist/lib/risk/drawdown.js +49 -0
- package/dist/lib/risk/evaluation-log.d.ts +29 -0
- package/dist/lib/risk/evaluation-log.js +61 -0
- package/dist/lib/risk/index.d.ts +4 -0
- package/dist/lib/risk/index.js +4 -0
- package/dist/lib/risk/limits.d.ts +23 -0
- package/dist/lib/risk/limits.js +27 -0
- package/dist/lib/risk/manager.d.ts +32 -0
- package/dist/lib/risk/manager.js +85 -0
- package/dist/lib/risk/policy-middleware.d.ts +33 -0
- package/dist/lib/risk/policy-middleware.js +267 -0
- package/dist/lib/risk/position-sizer.d.ts +9 -0
- package/dist/lib/risk/position-sizer.js +14 -0
- package/dist/lib/risk/rules-store.d.ts +16 -0
- package/dist/lib/risk/rules-store.js +47 -0
- package/dist/lib/schema.d.ts +254 -0
- package/dist/lib/schema.js +199 -0
- package/dist/lib/secrets.d.ts +3 -0
- package/dist/lib/secrets.js +62 -0
- package/dist/lib/settings.d.ts +24 -0
- package/dist/lib/settings.js +86 -0
- package/dist/lib/signals.d.ts +73 -0
- package/dist/lib/signals.js +136 -0
- package/dist/lib/stable-stringify.d.ts +6 -0
- package/dist/lib/stable-stringify.js +17 -0
- package/dist/lib/state-context.d.ts +44 -0
- package/dist/lib/state-context.js +133 -0
- package/dist/lib/strategy/basis-trade.d.ts +2 -0
- package/dist/lib/strategy/basis-trade.js +24 -0
- package/dist/lib/strategy/funding-arb.d.ts +2 -0
- package/dist/lib/strategy/funding-arb.js +23 -0
- package/dist/lib/strategy/interface.d.ts +23 -0
- package/dist/lib/strategy/interface.js +1 -0
- package/dist/lib/strategy/registry.d.ts +4 -0
- package/dist/lib/strategy/registry.js +10 -0
- package/dist/lib/telemetry.d.ts +25 -0
- package/dist/lib/telemetry.js +101 -0
- package/dist/lib/trace-queries.d.ts +20 -0
- package/dist/lib/trace-queries.js +133 -0
- package/dist/lib/trace.d.ts +1 -0
- package/dist/lib/trace.js +4 -0
- package/dist/lib/trade-reputation.d.ts +6 -0
- package/dist/lib/trade-reputation.js +99 -0
- package/dist/lib/ui-tokens.d.ts +21 -0
- package/dist/lib/ui-tokens.js +26 -0
- package/dist/lib/validate.d.ts +39 -0
- package/dist/lib/validate.js +108 -0
- package/dist/lib/validation.d.ts +9 -0
- package/dist/lib/validation.js +64 -0
- package/dist/server/cache.d.ts +38 -0
- package/dist/server/cache.js +56 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +89 -0
- package/dist/server/ipc.d.ts +18 -0
- package/dist/server/ipc.js +159 -0
- package/dist/server/subscriptions.d.ts +18 -0
- package/dist/server/subscriptions.js +114 -0
- package/package.json +124 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arb Execute Command
|
|
3
|
+
* Execute delta-neutral funding arbitrage across exchanges
|
|
4
|
+
*/
|
|
5
|
+
import { confirm } from "@inquirer/prompts";
|
|
6
|
+
import { getContext, getOutputOptions } from "../../cli/program.js";
|
|
7
|
+
import { output, outputError } from "../../cli/output.js";
|
|
8
|
+
import { getExchangeAdapterById } from "../../lib/exchange.js";
|
|
9
|
+
import { getLatestFundingRates } from "../../lib/db/funding-history.js";
|
|
10
|
+
import { getExchangeIdByName, DEFAULT_ARB_SIZE_USD } from "../../lib/constants.js";
|
|
11
|
+
import { validateAsset, validateSize } from "../../lib/validate.js";
|
|
12
|
+
import { getExchangeCredentials } from "../../lib/config.js";
|
|
13
|
+
import { RiskPolicyMiddleware } from "../../lib/risk/policy-middleware.js";
|
|
14
|
+
import { executeOrderWithSafety } from "../../lib/execution/safety.js";
|
|
15
|
+
import { runWithExecutionJournal } from "../../lib/execution/journal.js";
|
|
16
|
+
import { withJsonContract } from "../../lib/contracts.js";
|
|
17
|
+
import { CLIError, EXIT_CODES, inferExitCode } from "../../lib/exit-codes.js";
|
|
18
|
+
export function registerArbExecuteCommand(arb) {
|
|
19
|
+
arb
|
|
20
|
+
.command("execute [asset]")
|
|
21
|
+
.description("Execute delta-neutral funding arbitrage")
|
|
22
|
+
.option("-s, --size <usd>", "Position size in USD", String(DEFAULT_ARB_SIZE_USD))
|
|
23
|
+
.option("--confidence <0-1>", "Signal confidence for risk policy (default: 1.0)")
|
|
24
|
+
.option("--tp <pct>", "Attach take-profit (% from entry) to each leg")
|
|
25
|
+
.option("--sl <pct>", "Attach stop-loss (% from entry) to each leg")
|
|
26
|
+
.option("--no-close-then-flip", "Disable auto close-then-flip behavior")
|
|
27
|
+
.option("--no-spread-aware", "Disable spread-aware execution pricing")
|
|
28
|
+
.option("--idempotency-key <key>", "Idempotency key prefix for safe retries (leg keys use :short/:long suffixes)")
|
|
29
|
+
.option("--dry-run", "Show execution plan without trading")
|
|
30
|
+
.option("-y, --yes", "Skip confirmation prompts")
|
|
31
|
+
.action(async function (asset) {
|
|
32
|
+
const ctx = getContext(this);
|
|
33
|
+
const outputOpts = getOutputOptions(this);
|
|
34
|
+
const opts = this.opts();
|
|
35
|
+
const emitJson = (data) => {
|
|
36
|
+
output(withJsonContract("arb.execute.result", data), outputOpts);
|
|
37
|
+
};
|
|
38
|
+
const isJson = outputOpts.json;
|
|
39
|
+
try {
|
|
40
|
+
if (isJson && !opts.yes && !opts.dryRun) {
|
|
41
|
+
throw new CLIError("Use --yes with --json for non-interactive arb execution", EXIT_CODES.VALIDATION_ERROR);
|
|
42
|
+
}
|
|
43
|
+
const assetName = validateAsset(asset);
|
|
44
|
+
const market = `${assetName}-PERP`;
|
|
45
|
+
const requestedSizeUsd = validateSize(opts.size, DEFAULT_ARB_SIZE_USD);
|
|
46
|
+
const confidence = opts.confidence ? parseFloat(opts.confidence) : 1;
|
|
47
|
+
if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
|
|
48
|
+
throw new CLIError("Confidence must be a number between 0 and 1", EXIT_CODES.VALIDATION_ERROR);
|
|
49
|
+
}
|
|
50
|
+
const rates = getLatestFundingRates(market);
|
|
51
|
+
if (rates.length < 2) {
|
|
52
|
+
if (isJson) {
|
|
53
|
+
emitJson({
|
|
54
|
+
status: "no_data",
|
|
55
|
+
reason: "not_enough_exchange_data",
|
|
56
|
+
market,
|
|
57
|
+
requiredExchanges: 2,
|
|
58
|
+
availableExchanges: rates.length,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
console.log("\n Not enough exchange data. Run \x1b[36mperps arb track --once\x1b[0m first.\n");
|
|
63
|
+
}
|
|
64
|
+
process.exitCode = EXIT_CODES.DATA_UNAVAILABLE;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const sorted = [...rates].sort((a, b) => b.rate - a.rate);
|
|
68
|
+
const highest = sorted[0];
|
|
69
|
+
const lowest = sorted[sorted.length - 1];
|
|
70
|
+
const spread = highest.rate - lowest.rate;
|
|
71
|
+
if (spread <= 0) {
|
|
72
|
+
if (isJson) {
|
|
73
|
+
emitJson({
|
|
74
|
+
status: "no_opportunity",
|
|
75
|
+
reason: "non_positive_spread",
|
|
76
|
+
market,
|
|
77
|
+
spread,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
console.log("\n No positive spread found. Arbitrage not profitable right now.\n");
|
|
82
|
+
}
|
|
83
|
+
process.exitCode = EXIT_CODES.NO_OPPORTUNITY;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const shortExchangeId = getExchangeIdByName(highest.exchange);
|
|
87
|
+
const longExchangeId = getExchangeIdByName(lowest.exchange);
|
|
88
|
+
if (shortExchangeId === longExchangeId) {
|
|
89
|
+
if (isJson) {
|
|
90
|
+
emitJson({
|
|
91
|
+
status: "no_opportunity",
|
|
92
|
+
reason: "same_exchange_legs",
|
|
93
|
+
market,
|
|
94
|
+
exchangeId: shortExchangeId,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.log("\n Best long/short legs resolved to the same exchange. Skipping.\n");
|
|
99
|
+
}
|
|
100
|
+
process.exitCode = EXIT_CODES.NO_OPPORTUNITY;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const shortAdapter = getExchangeAdapterById(shortExchangeId);
|
|
104
|
+
const longAdapter = getExchangeAdapterById(longExchangeId);
|
|
105
|
+
const shortCredentials = getExchangeCredentials(ctx.config, shortExchangeId, {
|
|
106
|
+
requireTrading: true,
|
|
107
|
+
});
|
|
108
|
+
const longCredentials = getExchangeCredentials(ctx.config, longExchangeId, {
|
|
109
|
+
requireTrading: true,
|
|
110
|
+
});
|
|
111
|
+
await shortAdapter.connect({
|
|
112
|
+
testnet: ctx.config.testnet,
|
|
113
|
+
rpcUrl: shortCredentials.fullnodeUrl,
|
|
114
|
+
wsUrl: shortCredentials.wsUrl,
|
|
115
|
+
credentials: shortCredentials,
|
|
116
|
+
});
|
|
117
|
+
await longAdapter.connect({
|
|
118
|
+
testnet: ctx.config.testnet,
|
|
119
|
+
rpcUrl: longCredentials.fullnodeUrl,
|
|
120
|
+
wsUrl: longCredentials.wsUrl,
|
|
121
|
+
credentials: longCredentials,
|
|
122
|
+
});
|
|
123
|
+
try {
|
|
124
|
+
const shortTicker = await shortAdapter.getTicker(market);
|
|
125
|
+
const longTicker = await longAdapter.getTicker(market);
|
|
126
|
+
const shortPrice = parseFloat(shortTicker.lastPrice);
|
|
127
|
+
const longPrice = parseFloat(longTicker.lastPrice);
|
|
128
|
+
if (!Number.isFinite(shortPrice) || !Number.isFinite(longPrice)) {
|
|
129
|
+
throw new CLIError("Unable to fetch reliable ticker prices for arb sizing", EXIT_CODES.DATA_UNAVAILABLE);
|
|
130
|
+
}
|
|
131
|
+
// Run risk policy independently on both legs and size to the stricter leg.
|
|
132
|
+
const shortRisk = new RiskPolicyMiddleware(ctx.config, shortExchangeId);
|
|
133
|
+
const longRisk = new RiskPolicyMiddleware(ctx.config, longExchangeId);
|
|
134
|
+
const shortEval = await shortRisk.evaluateUsdSignal(shortAdapter, { market, side: "short", confidence, reason: "arb:short", timestamp: Date.now() }, requestedSizeUsd);
|
|
135
|
+
const longEval = await longRisk.evaluateUsdSignal(longAdapter, { market, side: "long", confidence, reason: "arb:long", timestamp: Date.now() }, requestedSizeUsd);
|
|
136
|
+
if (!shortEval.allowed) {
|
|
137
|
+
throw new CLIError(`Short leg blocked by risk policy: ${shortEval.reason ?? "unknown"}`, EXIT_CODES.EXECUTION_ERROR);
|
|
138
|
+
}
|
|
139
|
+
if (!longEval.allowed) {
|
|
140
|
+
throw new CLIError(`Long leg blocked by risk policy: ${longEval.reason ?? "unknown"}`, EXIT_CODES.EXECUTION_ERROR);
|
|
141
|
+
}
|
|
142
|
+
const sizeUsd = Math.min(requestedSizeUsd, shortEval.sizeUsd, longEval.sizeUsd);
|
|
143
|
+
if (!Number.isFinite(sizeUsd) || sizeUsd <= 0) {
|
|
144
|
+
throw new CLIError("Risk policy reduced arb size to zero", EXIT_CODES.EXECUTION_ERROR);
|
|
145
|
+
}
|
|
146
|
+
const annualized = spread * 24 * 365 * 100;
|
|
147
|
+
const dailyProfit = spread * 24 * sizeUsd;
|
|
148
|
+
const plan = {
|
|
149
|
+
asset: assetName,
|
|
150
|
+
size: sizeUsd,
|
|
151
|
+
shortExchange: highest.exchange,
|
|
152
|
+
shortPrice,
|
|
153
|
+
longExchange: lowest.exchange,
|
|
154
|
+
longPrice,
|
|
155
|
+
spread: spread * 100,
|
|
156
|
+
annualized,
|
|
157
|
+
dailyProfit,
|
|
158
|
+
};
|
|
159
|
+
if (!isJson) {
|
|
160
|
+
console.log("\n \x1b[1m━━━ Delta-Neutral Execution Plan ━━━\x1b[0m\n");
|
|
161
|
+
console.log(` Asset: ${assetName}`);
|
|
162
|
+
console.log(` Position Size: $${sizeUsd.toLocaleString()}`);
|
|
163
|
+
if (Math.abs(sizeUsd - requestedSizeUsd) > 1e-9) {
|
|
164
|
+
console.log(` Risk Adjusted: from $${requestedSizeUsd.toLocaleString()}`);
|
|
165
|
+
}
|
|
166
|
+
console.log("");
|
|
167
|
+
console.log(` \x1b[31m▼ SHORT\x1b[0m ${highest.exchange}`);
|
|
168
|
+
console.log(` Price: $${shortPrice.toLocaleString()}`);
|
|
169
|
+
console.log(` Funding: ${(highest.rate * 100).toFixed(4)}%/hr (you receive)`);
|
|
170
|
+
console.log("");
|
|
171
|
+
console.log(` \x1b[32m▲ LONG\x1b[0m ${lowest.exchange}`);
|
|
172
|
+
console.log(` Price: $${longPrice.toLocaleString()}`);
|
|
173
|
+
console.log(` Funding: ${(lowest.rate * 100).toFixed(4)}%/hr (you pay)`);
|
|
174
|
+
console.log("");
|
|
175
|
+
console.log(" " + "─".repeat(45));
|
|
176
|
+
console.log(` Net Spread: \x1b[32m${plan.spread.toFixed(4)}%/hr\x1b[0m`);
|
|
177
|
+
console.log(` Annualized: \x1b[32m${annualized.toFixed(2)}%\x1b[0m`);
|
|
178
|
+
console.log(` Est. Daily: \x1b[32m$${dailyProfit.toFixed(2)}\x1b[0m`);
|
|
179
|
+
console.log(" " + "─".repeat(45));
|
|
180
|
+
console.log("");
|
|
181
|
+
}
|
|
182
|
+
if (opts.dryRun) {
|
|
183
|
+
if (isJson) {
|
|
184
|
+
emitJson({
|
|
185
|
+
status: "dry_run",
|
|
186
|
+
market,
|
|
187
|
+
asset: assetName,
|
|
188
|
+
testnet: ctx.config.testnet,
|
|
189
|
+
requestedSizeUsd,
|
|
190
|
+
plan,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.log(" \x1b[33m[DRY RUN]\x1b[0m Execution skipped.\n");
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (!opts.yes) {
|
|
199
|
+
const confirmed = await confirm({
|
|
200
|
+
message: "Execute this arbitrage trade?",
|
|
201
|
+
default: false,
|
|
202
|
+
});
|
|
203
|
+
if (!confirmed) {
|
|
204
|
+
if (isJson) {
|
|
205
|
+
emitJson({
|
|
206
|
+
status: "cancelled",
|
|
207
|
+
reason: "user_declined_confirmation",
|
|
208
|
+
market,
|
|
209
|
+
asset: assetName,
|
|
210
|
+
plan,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
console.log("\n Execution cancelled.\n");
|
|
215
|
+
}
|
|
216
|
+
process.exitCode = EXIT_CODES.CANCELLED;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const shortSize = (sizeUsd / shortPrice).toFixed(8);
|
|
221
|
+
const longSize = (sizeUsd / longPrice).toFixed(8);
|
|
222
|
+
const shouldAttachTpSl = opts.tp !== undefined || opts.sl !== undefined;
|
|
223
|
+
const tp = opts.tp !== undefined ? parseFloat(opts.tp) : ctx.config.executionSafety.takeProfitPct;
|
|
224
|
+
const sl = opts.sl !== undefined ? parseFloat(opts.sl) : ctx.config.executionSafety.stopLossPct;
|
|
225
|
+
const closeThenFlip = opts.closeThenFlip ?? true;
|
|
226
|
+
const spreadAware = opts.spreadAware ?? true;
|
|
227
|
+
const idempotencyPrefix = opts.idempotencyKey?.trim() || undefined;
|
|
228
|
+
if (shouldAttachTpSl) {
|
|
229
|
+
if (!Number.isFinite(tp) || tp <= 0) {
|
|
230
|
+
throw new CLIError("TP must be a positive number", EXIT_CODES.VALIDATION_ERROR);
|
|
231
|
+
}
|
|
232
|
+
if (!Number.isFinite(sl) || sl <= 0) {
|
|
233
|
+
throw new CLIError("SL must be a positive number", EXIT_CODES.VALIDATION_ERROR);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (!isJson) {
|
|
237
|
+
console.log("\n \x1b[36mExecuting...\x1b[0m\n");
|
|
238
|
+
}
|
|
239
|
+
if (!isJson) {
|
|
240
|
+
console.log(` Opening SHORT on ${highest.exchange}...`);
|
|
241
|
+
}
|
|
242
|
+
const shortJournal = await runWithExecutionJournal({
|
|
243
|
+
idempotencyKey: idempotencyPrefix ? `${idempotencyPrefix}:short` : undefined,
|
|
244
|
+
metadata: {
|
|
245
|
+
command: "arb.execute.short",
|
|
246
|
+
exchange: shortExchangeId,
|
|
247
|
+
testnet: ctx.config.testnet,
|
|
248
|
+
market,
|
|
249
|
+
side: "short",
|
|
250
|
+
orderType: "market",
|
|
251
|
+
},
|
|
252
|
+
request: {
|
|
253
|
+
market,
|
|
254
|
+
side: "short",
|
|
255
|
+
size: shortSize,
|
|
256
|
+
closeThenFlip,
|
|
257
|
+
spreadAware,
|
|
258
|
+
spreadOffset: ctx.config.executionSafety.spreadOffset,
|
|
259
|
+
tp: shouldAttachTpSl ? tp : null,
|
|
260
|
+
sl: shouldAttachTpSl ? sl : null,
|
|
261
|
+
requestedSizeUsd,
|
|
262
|
+
adjustedSizeUsd: sizeUsd,
|
|
263
|
+
confidence,
|
|
264
|
+
},
|
|
265
|
+
execute: async () => {
|
|
266
|
+
const result = await executeOrderWithSafety(shortAdapter, {
|
|
267
|
+
market,
|
|
268
|
+
side: "short",
|
|
269
|
+
type: "market",
|
|
270
|
+
size: shortSize,
|
|
271
|
+
}, {
|
|
272
|
+
closeThenFlip,
|
|
273
|
+
spreadAwarePricing: spreadAware,
|
|
274
|
+
spreadOffset: ctx.config.executionSafety.spreadOffset,
|
|
275
|
+
attachTpSl: shouldAttachTpSl,
|
|
276
|
+
tpSlConfig: shouldAttachTpSl
|
|
277
|
+
? { stopLossPct: sl, takeProfitPct: tp }
|
|
278
|
+
: undefined,
|
|
279
|
+
});
|
|
280
|
+
return {
|
|
281
|
+
order: {
|
|
282
|
+
id: result.order.id,
|
|
283
|
+
market: result.order.market,
|
|
284
|
+
side: result.order.side,
|
|
285
|
+
size: result.order.size,
|
|
286
|
+
status: result.order.status,
|
|
287
|
+
price: result.order.price,
|
|
288
|
+
},
|
|
289
|
+
safety: {
|
|
290
|
+
closedOppositePosition: result.closedOppositePosition,
|
|
291
|
+
tpSlOrderIds: result.tpSlOrderIds,
|
|
292
|
+
pricing: result.pricing,
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
if (!isJson) {
|
|
298
|
+
console.log(` ✓ Short ${shortJournal.replayed ? "replayed" : "opened"}: ${shortJournal.result.order.id}`);
|
|
299
|
+
}
|
|
300
|
+
if (!isJson) {
|
|
301
|
+
console.log(` Opening LONG on ${lowest.exchange}...`);
|
|
302
|
+
}
|
|
303
|
+
let longJournal = null;
|
|
304
|
+
try {
|
|
305
|
+
longJournal = await runWithExecutionJournal({
|
|
306
|
+
idempotencyKey: idempotencyPrefix ? `${idempotencyPrefix}:long` : undefined,
|
|
307
|
+
metadata: {
|
|
308
|
+
command: "arb.execute.long",
|
|
309
|
+
exchange: longExchangeId,
|
|
310
|
+
testnet: ctx.config.testnet,
|
|
311
|
+
market,
|
|
312
|
+
side: "long",
|
|
313
|
+
orderType: "market",
|
|
314
|
+
},
|
|
315
|
+
request: {
|
|
316
|
+
market,
|
|
317
|
+
side: "long",
|
|
318
|
+
size: longSize,
|
|
319
|
+
closeThenFlip,
|
|
320
|
+
spreadAware,
|
|
321
|
+
spreadOffset: ctx.config.executionSafety.spreadOffset,
|
|
322
|
+
tp: shouldAttachTpSl ? tp : null,
|
|
323
|
+
sl: shouldAttachTpSl ? sl : null,
|
|
324
|
+
requestedSizeUsd,
|
|
325
|
+
adjustedSizeUsd: sizeUsd,
|
|
326
|
+
confidence,
|
|
327
|
+
},
|
|
328
|
+
execute: async () => {
|
|
329
|
+
const result = await executeOrderWithSafety(longAdapter, {
|
|
330
|
+
market,
|
|
331
|
+
side: "long",
|
|
332
|
+
type: "market",
|
|
333
|
+
size: longSize,
|
|
334
|
+
}, {
|
|
335
|
+
closeThenFlip,
|
|
336
|
+
spreadAwarePricing: spreadAware,
|
|
337
|
+
spreadOffset: ctx.config.executionSafety.spreadOffset,
|
|
338
|
+
attachTpSl: shouldAttachTpSl,
|
|
339
|
+
tpSlConfig: shouldAttachTpSl
|
|
340
|
+
? { stopLossPct: sl, takeProfitPct: tp }
|
|
341
|
+
: undefined,
|
|
342
|
+
});
|
|
343
|
+
return {
|
|
344
|
+
order: {
|
|
345
|
+
id: result.order.id,
|
|
346
|
+
market: result.order.market,
|
|
347
|
+
side: result.order.side,
|
|
348
|
+
size: result.order.size,
|
|
349
|
+
status: result.order.status,
|
|
350
|
+
price: result.order.price,
|
|
351
|
+
},
|
|
352
|
+
safety: {
|
|
353
|
+
closedOppositePosition: result.closedOppositePosition,
|
|
354
|
+
tpSlOrderIds: result.tpSlOrderIds,
|
|
355
|
+
pricing: result.pricing,
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
363
|
+
if (isJson) {
|
|
364
|
+
emitJson({
|
|
365
|
+
status: "partial_failure",
|
|
366
|
+
market,
|
|
367
|
+
asset: assetName,
|
|
368
|
+
testnet: ctx.config.testnet,
|
|
369
|
+
plan,
|
|
370
|
+
error: {
|
|
371
|
+
message,
|
|
372
|
+
exitCode: EXIT_CODES.PARTIAL_EXECUTION,
|
|
373
|
+
},
|
|
374
|
+
execution: {
|
|
375
|
+
short: {
|
|
376
|
+
exchange: highest.exchange,
|
|
377
|
+
exchangeId: shortExchangeId,
|
|
378
|
+
...shortJournal.result,
|
|
379
|
+
idempotency: {
|
|
380
|
+
key: shortJournal.idempotencyKey,
|
|
381
|
+
replayed: shortJournal.replayed,
|
|
382
|
+
autoGenerated: shortJournal.autoGeneratedKey,
|
|
383
|
+
journalId: shortJournal.journalId,
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
console.error("\n \x1b[31mExecution failed on long leg:\x1b[0m", message);
|
|
391
|
+
console.log(" \x1b[33mWarning:\x1b[0m Short leg may be open. Check positions manually.\n");
|
|
392
|
+
}
|
|
393
|
+
process.exitCode = EXIT_CODES.PARTIAL_EXECUTION;
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (!longJournal) {
|
|
397
|
+
throw new CLIError("Long-leg journal result missing", EXIT_CODES.EXECUTION_ERROR);
|
|
398
|
+
}
|
|
399
|
+
if (!isJson) {
|
|
400
|
+
console.log(` ✓ Long ${longJournal.replayed ? "replayed" : "opened"}: ${longJournal.result.order.id}`);
|
|
401
|
+
console.log("\n \x1b[32m━━━ Arbitrage Executed ━━━\x1b[0m");
|
|
402
|
+
if (shortJournal.result.safety.tpSlOrderIds.length > 0 ||
|
|
403
|
+
longJournal.result.safety.tpSlOrderIds.length > 0) {
|
|
404
|
+
console.log(" TP/SL attached on opened legs");
|
|
405
|
+
}
|
|
406
|
+
console.log(` Monitor positions with: perps -e ${shortExchangeId} account positions`);
|
|
407
|
+
console.log(` and: perps -e ${longExchangeId} account positions`);
|
|
408
|
+
console.log("");
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
emitJson({
|
|
412
|
+
status: "executed",
|
|
413
|
+
market,
|
|
414
|
+
asset: assetName,
|
|
415
|
+
testnet: ctx.config.testnet,
|
|
416
|
+
requestedSizeUsd,
|
|
417
|
+
plan,
|
|
418
|
+
execution: {
|
|
419
|
+
short: {
|
|
420
|
+
exchange: highest.exchange,
|
|
421
|
+
exchangeId: shortExchangeId,
|
|
422
|
+
...shortJournal.result,
|
|
423
|
+
idempotency: {
|
|
424
|
+
key: shortJournal.idempotencyKey,
|
|
425
|
+
replayed: shortJournal.replayed,
|
|
426
|
+
autoGenerated: shortJournal.autoGeneratedKey,
|
|
427
|
+
journalId: shortJournal.journalId,
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
long: {
|
|
431
|
+
exchange: lowest.exchange,
|
|
432
|
+
exchangeId: longExchangeId,
|
|
433
|
+
...longJournal.result,
|
|
434
|
+
idempotency: {
|
|
435
|
+
key: longJournal.idempotencyKey,
|
|
436
|
+
replayed: longJournal.replayed,
|
|
437
|
+
autoGenerated: longJournal.autoGeneratedKey,
|
|
438
|
+
journalId: longJournal.journalId,
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
await Promise.allSettled([shortAdapter.disconnect(), longAdapter.disconnect()]);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
const code = inferExitCode(err);
|
|
451
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
452
|
+
if (isJson) {
|
|
453
|
+
emitJson({
|
|
454
|
+
status: "error",
|
|
455
|
+
error: {
|
|
456
|
+
message,
|
|
457
|
+
exitCode: code,
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
outputError(message);
|
|
463
|
+
}
|
|
464
|
+
process.exit(code);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arb Funding Command
|
|
3
|
+
* Compare funding rates across exchanges
|
|
4
|
+
*/
|
|
5
|
+
import { highlighter } from "../../cli/experience.js";
|
|
6
|
+
import { output, outputError } from "../../cli/output.js";
|
|
7
|
+
import { getContext, getOutputOptions } from "../../cli/program.js";
|
|
8
|
+
import { getAvailableExchanges, getExchangeAdapterById } from "../../lib/exchange.js";
|
|
9
|
+
export function registerArbFundingCommand(arb) {
|
|
10
|
+
arb
|
|
11
|
+
.command("funding [asset]")
|
|
12
|
+
.description("Compare funding rates across exchanges")
|
|
13
|
+
.option("--all", "Show funding for common assets")
|
|
14
|
+
.action(async function (asset) {
|
|
15
|
+
const ctx = getContext(this);
|
|
16
|
+
const outputOpts = getOutputOptions(this);
|
|
17
|
+
const opts = this.opts();
|
|
18
|
+
const isTestnet = ctx.config.testnet;
|
|
19
|
+
try {
|
|
20
|
+
const exchanges = getAvailableExchanges();
|
|
21
|
+
if (opts.all) {
|
|
22
|
+
await showAllFunding(exchanges, outputOpts.json, isTestnet);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
await showFunding(asset?.toUpperCase() || "BTC", exchanges, outputOpts.json, isTestnet);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
outputError(err instanceof Error ? err.message : String(err));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function fetchFunding(asset, exchanges, isTestnet) {
|
|
35
|
+
const results = [];
|
|
36
|
+
const promises = exchanges.map(async (exchangeId) => {
|
|
37
|
+
const adapter = getExchangeAdapterById(exchangeId);
|
|
38
|
+
let connected = false;
|
|
39
|
+
try {
|
|
40
|
+
await adapter.connect({ testnet: isTestnet });
|
|
41
|
+
connected = true;
|
|
42
|
+
const symbol = `${asset}-PERP`;
|
|
43
|
+
const funding = await adapter.getFundingRate(symbol);
|
|
44
|
+
const rate = parseFloat(funding.rate) * 100; // Convert to percentage
|
|
45
|
+
return {
|
|
46
|
+
exchange: adapter.info.name,
|
|
47
|
+
symbol: funding.market,
|
|
48
|
+
rate,
|
|
49
|
+
annualized: rate * 24 * 365, // Hourly rate * 24 hours * 365 days
|
|
50
|
+
nextFunding: funding.nextFundingTime,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
return {
|
|
55
|
+
exchange: adapter.info.name,
|
|
56
|
+
symbol: `${asset}-PERP`,
|
|
57
|
+
rate: 0,
|
|
58
|
+
annualized: 0,
|
|
59
|
+
nextFunding: 0,
|
|
60
|
+
error: err instanceof Error ? err.message : "Unknown error",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
if (connected) {
|
|
65
|
+
await adapter.disconnect().catch(() => undefined);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
const settled = await Promise.allSettled(promises);
|
|
70
|
+
for (const result of settled) {
|
|
71
|
+
if (result.status === "fulfilled") {
|
|
72
|
+
results.push(result.value);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return results;
|
|
76
|
+
}
|
|
77
|
+
async function showFunding(asset, exchanges, json, isTestnet) {
|
|
78
|
+
if (!json) {
|
|
79
|
+
console.log(`\nFetching ${asset} funding rates...\n`);
|
|
80
|
+
}
|
|
81
|
+
const rates = await fetchFunding(asset, exchanges, isTestnet);
|
|
82
|
+
const validRates = rates.filter((r) => !r.error);
|
|
83
|
+
if (validRates.length === 0) {
|
|
84
|
+
if (json) {
|
|
85
|
+
output({
|
|
86
|
+
status: "no_data",
|
|
87
|
+
asset,
|
|
88
|
+
rates: [],
|
|
89
|
+
exchangesScanned: exchanges.length,
|
|
90
|
+
}, { json: true });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
console.log(`No funding data found for ${asset}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Find highest and lowest
|
|
97
|
+
const sorted = [...validRates].sort((a, b) => b.rate - a.rate);
|
|
98
|
+
const highest = sorted[0];
|
|
99
|
+
const lowest = sorted[sorted.length - 1];
|
|
100
|
+
const delta = highest.rate - lowest.rate;
|
|
101
|
+
const data = {
|
|
102
|
+
asset,
|
|
103
|
+
rates: validRates,
|
|
104
|
+
spread: {
|
|
105
|
+
high: { exchange: highest.exchange, rate: highest.rate },
|
|
106
|
+
low: { exchange: lowest.exchange, rate: lowest.rate },
|
|
107
|
+
delta,
|
|
108
|
+
annualizedDelta: delta * 24 * 365,
|
|
109
|
+
},
|
|
110
|
+
timestamp: Date.now(),
|
|
111
|
+
};
|
|
112
|
+
if (json) {
|
|
113
|
+
output(data, { json: true });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Table output
|
|
117
|
+
console.log(` ${asset}-PERP Funding Rate Comparison\n`);
|
|
118
|
+
console.log(" Exchange".padEnd(18) + "Rate (1h)".padEnd(16) + "Annualized".padEnd(16) + "Next Funding");
|
|
119
|
+
console.log(" " + "─".repeat(65));
|
|
120
|
+
const colorRate = (value, text) => value >= 0 ? highlighter.success(text) : highlighter.error(text);
|
|
121
|
+
for (const r of sorted) {
|
|
122
|
+
const sign = r.rate >= 0 ? "+" : "";
|
|
123
|
+
const rateStr = `${sign}${r.rate.toFixed(4)}%`;
|
|
124
|
+
const annStr = `${sign}${r.annualized.toFixed(2)}%`;
|
|
125
|
+
const nextFundingStr = r.nextFunding > 0 ? new Date(r.nextFunding).toLocaleTimeString() : "N/A";
|
|
126
|
+
// Use fixed-width fields before coloring to avoid ANSI padding issues
|
|
127
|
+
console.log(` ${r.exchange.padEnd(16)}` +
|
|
128
|
+
`${colorRate(r.rate, rateStr.padEnd(16))}` +
|
|
129
|
+
`${colorRate(r.annualized, annStr.padEnd(16))}` +
|
|
130
|
+
nextFundingStr);
|
|
131
|
+
}
|
|
132
|
+
// Show errors
|
|
133
|
+
const errors = rates.filter((r) => r.error);
|
|
134
|
+
if (errors.length > 0) {
|
|
135
|
+
console.log(`\n ${highlighter.warn("Not available on:")} ${errors.map((e) => e.exchange).join(", ")}`);
|
|
136
|
+
}
|
|
137
|
+
// Show arbitrage opportunity
|
|
138
|
+
console.log("\n " + "─".repeat(65));
|
|
139
|
+
if (delta > 0.001) {
|
|
140
|
+
// Only show if meaningful spread
|
|
141
|
+
console.log(`\n ${highlighter.success("Funding Rate Arbitrage Opportunity")}`);
|
|
142
|
+
console.log(` Short on ${highest.exchange} (paying ${highest.rate.toFixed(4)}%)`);
|
|
143
|
+
console.log(` Long on ${lowest.exchange} (paying ${lowest.rate.toFixed(4)}%)`);
|
|
144
|
+
console.log(` Delta: ${delta.toFixed(4)}% per hour (${(delta * 24 * 365).toFixed(2)}% annualized)`);
|
|
145
|
+
// Calculate daily profit on $10k position
|
|
146
|
+
const dailyProfit = (delta / 100) * 24 * 10000;
|
|
147
|
+
console.log(` ~$${dailyProfit.toFixed(2)}/day on $10k delta-neutral position`);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
console.log(`\n No significant funding rate spread (${delta.toFixed(4)}%)`);
|
|
151
|
+
}
|
|
152
|
+
console.log("");
|
|
153
|
+
}
|
|
154
|
+
async function showAllFunding(exchanges, json, isTestnet) {
|
|
155
|
+
const assets = ["BTC", "ETH", "SOL", "DOGE", "ARB", "OP"];
|
|
156
|
+
const rows = [];
|
|
157
|
+
if (!json) {
|
|
158
|
+
console.log("\nFetching funding rates for common assets...\n");
|
|
159
|
+
console.log(" Asset".padEnd(10) + "Highest".padEnd(28) + "Lowest".padEnd(28) + "Spread (Ann.)");
|
|
160
|
+
console.log(" " + "─".repeat(80));
|
|
161
|
+
}
|
|
162
|
+
for (const asset of assets) {
|
|
163
|
+
const rates = await fetchFunding(asset, exchanges, isTestnet);
|
|
164
|
+
const validRates = rates.filter((r) => !r.error);
|
|
165
|
+
if (validRates.length < 2) {
|
|
166
|
+
if (!json) {
|
|
167
|
+
console.log(` ${asset.padEnd(8)} Available on <2 exchanges`);
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const sorted = [...validRates].sort((a, b) => b.rate - a.rate);
|
|
172
|
+
const highest = sorted[0];
|
|
173
|
+
const lowest = sorted[sorted.length - 1];
|
|
174
|
+
const delta = highest.rate - lowest.rate;
|
|
175
|
+
const annDelta = delta * 24 * 365;
|
|
176
|
+
rows.push({
|
|
177
|
+
asset,
|
|
178
|
+
highest: { exchange: highest.exchange, rate: highest.rate },
|
|
179
|
+
lowest: { exchange: lowest.exchange, rate: lowest.rate },
|
|
180
|
+
spreadAnnualized: annDelta,
|
|
181
|
+
timestamp: Date.now(),
|
|
182
|
+
});
|
|
183
|
+
if (!json) {
|
|
184
|
+
const colorRate = (value, text) => value >= 0 ? highlighter.success(text) : highlighter.error(text);
|
|
185
|
+
const highSign = highest.rate >= 0 ? "+" : "";
|
|
186
|
+
const lowSign = lowest.rate >= 0 ? "+" : "";
|
|
187
|
+
const highStr = `${highSign}${highest.rate.toFixed(4)}%`;
|
|
188
|
+
const lowStr = `${lowSign}${lowest.rate.toFixed(4)}%`;
|
|
189
|
+
const deltaStr = `${annDelta.toFixed(2)}%`;
|
|
190
|
+
console.log(` ${asset.padEnd(8)}` +
|
|
191
|
+
`${highest.exchange.padEnd(12)} ${colorRate(highest.rate, highStr.padEnd(12))} ` +
|
|
192
|
+
`${lowest.exchange.padEnd(12)} ${colorRate(lowest.rate, lowStr.padEnd(12))} ` +
|
|
193
|
+
(annDelta > 10 ? highlighter.success(deltaStr) : highlighter.dim(deltaStr)));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (json) {
|
|
197
|
+
output(rows, { json: true });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
console.log("");
|
|
201
|
+
}
|