@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,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arb History Command
|
|
3
|
+
* View historical funding rate spreads
|
|
4
|
+
*/
|
|
5
|
+
import { getOutputOptions } from "../../cli/program.js";
|
|
6
|
+
import { output, outputError } from "../../cli/output.js";
|
|
7
|
+
import { getFundingHistory, getLatestFundingRates, getSpreadStats, getFundingSpreads, getRecordCount, } from "../../lib/db/funding-history.js";
|
|
8
|
+
import { DEFAULT_HISTORY_HOURS } from "../../lib/constants.js";
|
|
9
|
+
import { validateAsset, parsePositiveInt } from "../../lib/validate.js";
|
|
10
|
+
export function registerArbHistoryCommand(arb) {
|
|
11
|
+
arb
|
|
12
|
+
.command("history [asset]")
|
|
13
|
+
.description("View historical funding rate data")
|
|
14
|
+
.option("-h, --hours <hours>", "Hours of history to show", String(DEFAULT_HISTORY_HOURS))
|
|
15
|
+
.option("--stats", "Show statistics summary")
|
|
16
|
+
.option("--spreads", "Show hourly spread history")
|
|
17
|
+
.action(async function (asset) {
|
|
18
|
+
const outputOpts = getOutputOptions(this);
|
|
19
|
+
const opts = this.opts();
|
|
20
|
+
const hours = parsePositiveInt(opts.hours, "hours", DEFAULT_HISTORY_HOURS);
|
|
21
|
+
const market = `${validateAsset(asset)}-PERP`;
|
|
22
|
+
try {
|
|
23
|
+
const recordCount = getRecordCount();
|
|
24
|
+
if (recordCount === 0) {
|
|
25
|
+
if (outputOpts.json) {
|
|
26
|
+
output({
|
|
27
|
+
status: "no_data",
|
|
28
|
+
market,
|
|
29
|
+
hours,
|
|
30
|
+
history: [],
|
|
31
|
+
latestRates: [],
|
|
32
|
+
message: "No funding history recorded yet. Run `perps arb track` first.",
|
|
33
|
+
}, outputOpts);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
console.log("\n No funding history recorded yet.");
|
|
37
|
+
console.log(" Run \x1b[36mperps arb track\x1b[0m to start collecting data.\n");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (opts.stats) {
|
|
41
|
+
showStats(market, hours, outputOpts.json);
|
|
42
|
+
}
|
|
43
|
+
else if (opts.spreads) {
|
|
44
|
+
showSpreads(market, hours, outputOpts.json);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
showHistory(market, hours, outputOpts.json);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
outputError(err instanceof Error ? err.message : String(err));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function showHistory(market, hours, json) {
|
|
57
|
+
const rates = getLatestFundingRates(market);
|
|
58
|
+
const history = getFundingHistory(market, { hours });
|
|
59
|
+
if (json) {
|
|
60
|
+
output({ market, hours, latestRates: rates, history }, { json: true });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
console.log(`\n ${market} Funding History (last ${hours}h)\n`);
|
|
64
|
+
if (rates.length === 0) {
|
|
65
|
+
console.log(` No data for ${market}`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Show latest rates
|
|
69
|
+
console.log(" \x1b[1mLatest Rates:\x1b[0m");
|
|
70
|
+
console.log(" " + "Exchange".padEnd(18) + "Rate (1h)".padEnd(14) + "Annualized".padEnd(14) + "Last Updated");
|
|
71
|
+
console.log(" " + "─".repeat(70));
|
|
72
|
+
for (const r of rates) {
|
|
73
|
+
const rateColor = r.rate >= 0 ? "\x1b[32m" : "\x1b[31m";
|
|
74
|
+
const sign = r.rate >= 0 ? "+" : "";
|
|
75
|
+
const annPercent = r.annualized;
|
|
76
|
+
const timeAgo = formatTimeAgo(r.recordedAt);
|
|
77
|
+
console.log(` ${r.exchange.padEnd(16)}` +
|
|
78
|
+
`${rateColor}${sign}${(r.rate * 100).toFixed(4)}%\x1b[0m`.padEnd(23) +
|
|
79
|
+
`${rateColor}${sign}${annPercent.toFixed(2)}%\x1b[0m`.padEnd(23) +
|
|
80
|
+
timeAgo);
|
|
81
|
+
}
|
|
82
|
+
// Show sample count
|
|
83
|
+
console.log(`\n \x1b[90m${history.length} samples in last ${hours}h\x1b[0m\n`);
|
|
84
|
+
}
|
|
85
|
+
function showStats(market, hours, json) {
|
|
86
|
+
const stats = getSpreadStats(market, hours);
|
|
87
|
+
if (json) {
|
|
88
|
+
output({ market, hours, stats }, { json: true });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
console.log(`\n ${market} Spread Statistics (last ${hours}h)\n`);
|
|
92
|
+
if (stats.samples === 0) {
|
|
93
|
+
console.log(` No spread data for ${market}`);
|
|
94
|
+
console.log(` Run \x1b[36mperps arb track\x1b[0m to start collecting data.\n`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
console.log(" " + "─".repeat(50));
|
|
98
|
+
const annualizedPct = stats.avgAnnualized * 100; // Convert decimal to percentage
|
|
99
|
+
const spreadColor = annualizedPct > 10 ? "\x1b[32m" : "\x1b[33m";
|
|
100
|
+
const consistColor = stats.consistency > 80 ? "\x1b[32m" : stats.consistency > 50 ? "\x1b[33m" : "\x1b[31m";
|
|
101
|
+
console.log(` Avg Spread: ${spreadColor}${(stats.avgSpread * 100).toFixed(4)}%\x1b[0m per hour`);
|
|
102
|
+
console.log(` Avg Annualized: ${spreadColor}${annualizedPct.toFixed(2)}%\x1b[0m`);
|
|
103
|
+
console.log(` Min Spread: ${(stats.minSpread * 100).toFixed(4)}%`);
|
|
104
|
+
console.log(` Max Spread: ${(stats.maxSpread * 100).toFixed(4)}%`);
|
|
105
|
+
console.log(` Consistency: ${consistColor}${stats.consistency.toFixed(1)}%\x1b[0m of samples have positive spread`);
|
|
106
|
+
console.log(` Samples: ${stats.samples}`);
|
|
107
|
+
if (stats.bestPair) {
|
|
108
|
+
console.log(`\n Best Pair: Short ${stats.bestPair.high} / Long ${stats.bestPair.low}`);
|
|
109
|
+
}
|
|
110
|
+
// Profitability estimate
|
|
111
|
+
if (stats.avgAnnualized > 0) {
|
|
112
|
+
// avgSpread is decimal (e.g., 0.0001 = 0.01%), so multiply by position size
|
|
113
|
+
const dailyOnTenK = stats.avgSpread * 24 * 10000;
|
|
114
|
+
console.log(`\n \x1b[36mEstimated Daily Profit:\x1b[0m $${dailyOnTenK.toFixed(2)} on $10k delta-neutral`);
|
|
115
|
+
console.log(` \x1b[90m(Based on average spread, actual results vary)\x1b[0m`);
|
|
116
|
+
}
|
|
117
|
+
console.log("\n " + "─".repeat(50) + "\n");
|
|
118
|
+
}
|
|
119
|
+
function showSpreads(market, hours, json) {
|
|
120
|
+
const spreads = getFundingSpreads(market, hours);
|
|
121
|
+
if (json) {
|
|
122
|
+
output({ market, hours, spreads }, { json: true });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
console.log(`\n ${market} Hourly Spreads (last ${hours}h)\n`);
|
|
126
|
+
if (spreads.length === 0) {
|
|
127
|
+
console.log(` No spread data for ${market}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
console.log(" " + "Time".padEnd(14) + "High".padEnd(20) + "Low".padEnd(20) + "Spread".padEnd(14) + "Ann.");
|
|
131
|
+
console.log(" " + "─".repeat(75));
|
|
132
|
+
for (const s of spreads.slice(0, 24)) { // Last 24 entries
|
|
133
|
+
const time = new Date(s.recordedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
134
|
+
const annPct = s.annualizedSpread * 100; // Convert decimal to percentage
|
|
135
|
+
const spreadColor = annPct > 10 ? "\x1b[32m" : "\x1b[33m";
|
|
136
|
+
console.log(` ${time.padEnd(12)}` +
|
|
137
|
+
`${s.highExchange.padEnd(10)} ${(s.highRate * 100).toFixed(4)}%`.padEnd(20) +
|
|
138
|
+
`${s.lowExchange.padEnd(10)} ${(s.lowRate * 100).toFixed(4)}%`.padEnd(20) +
|
|
139
|
+
`${(s.spread * 100).toFixed(4)}%`.padEnd(14) +
|
|
140
|
+
`${spreadColor}${annPct.toFixed(1)}%\x1b[0m`);
|
|
141
|
+
}
|
|
142
|
+
console.log("");
|
|
143
|
+
}
|
|
144
|
+
function formatTimeAgo(timestamp) {
|
|
145
|
+
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
146
|
+
if (seconds < 60)
|
|
147
|
+
return `${seconds}s ago`;
|
|
148
|
+
if (seconds < 3600)
|
|
149
|
+
return `${Math.floor(seconds / 60)}m ago`;
|
|
150
|
+
if (seconds < 86400)
|
|
151
|
+
return `${Math.floor(seconds / 3600)}h ago`;
|
|
152
|
+
return `${Math.floor(seconds / 86400)}d ago`;
|
|
153
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arbitrage Commands
|
|
3
|
+
* Cross-exchange price and funding comparisons
|
|
4
|
+
*/
|
|
5
|
+
import { registerArbSpreadCommand } from "./spread.js";
|
|
6
|
+
import { registerArbFundingCommand } from "./funding.js";
|
|
7
|
+
import { registerArbHistoryCommand } from "./history.js";
|
|
8
|
+
import { registerArbTrackCommand } from "./track.js";
|
|
9
|
+
import { registerArbExecuteCommand } from "./execute.js";
|
|
10
|
+
import { registerArbAlertCommand } from "./alert.js";
|
|
11
|
+
import { registerArbPositionsCommand } from "./positions.js";
|
|
12
|
+
import { registerArbCompareCommand } from "./compare.js";
|
|
13
|
+
import { registerArbBasisCommand } from "./basis.js";
|
|
14
|
+
import { registerArbBasisExecuteCommand } from "./basis-execute.js";
|
|
15
|
+
export function registerArbCommands(program) {
|
|
16
|
+
const arb = program
|
|
17
|
+
.command("arb")
|
|
18
|
+
.description("Cross-exchange arbitrage tools");
|
|
19
|
+
registerArbSpreadCommand(arb);
|
|
20
|
+
registerArbFundingCommand(arb);
|
|
21
|
+
registerArbHistoryCommand(arb);
|
|
22
|
+
registerArbTrackCommand(arb);
|
|
23
|
+
registerArbExecuteCommand(arb);
|
|
24
|
+
registerArbAlertCommand(arb);
|
|
25
|
+
registerArbPositionsCommand(arb);
|
|
26
|
+
registerArbCompareCommand(arb);
|
|
27
|
+
registerArbBasisCommand(arb);
|
|
28
|
+
registerArbBasisExecuteCommand(arb);
|
|
29
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arb Positions Command
|
|
3
|
+
* View all open positions across exchanges
|
|
4
|
+
*/
|
|
5
|
+
import { getContext, getOutputOptions } from "../../cli/program.js";
|
|
6
|
+
import { output, outputError } from "../../cli/output.js";
|
|
7
|
+
import { getExchangeAdapterById, getAvailableExchanges } from "../../lib/exchange.js";
|
|
8
|
+
export function registerArbPositionsCommand(arb) {
|
|
9
|
+
arb
|
|
10
|
+
.command("positions")
|
|
11
|
+
.description("View all open positions across exchanges")
|
|
12
|
+
.option("--exchange <exchanges>", "Comma-separated exchanges to check (default: all)")
|
|
13
|
+
.action(async function () {
|
|
14
|
+
const ctx = getContext(this);
|
|
15
|
+
const outputOpts = getOutputOptions(this);
|
|
16
|
+
const opts = this.opts();
|
|
17
|
+
const isTestnet = ctx.config.testnet;
|
|
18
|
+
const exchanges = opts.exchange
|
|
19
|
+
? opts.exchange.split(",").map(e => e.trim().toLowerCase())
|
|
20
|
+
: getAvailableExchanges();
|
|
21
|
+
try {
|
|
22
|
+
if (!outputOpts.json) {
|
|
23
|
+
console.log("\n Fetching positions across exchanges...\n");
|
|
24
|
+
}
|
|
25
|
+
const allPositions = [];
|
|
26
|
+
const errors = [];
|
|
27
|
+
for (const exchangeId of exchanges) {
|
|
28
|
+
let adapter = null;
|
|
29
|
+
let connected = false;
|
|
30
|
+
try {
|
|
31
|
+
adapter = getExchangeAdapterById(exchangeId);
|
|
32
|
+
await adapter.connect({ testnet: isTestnet });
|
|
33
|
+
connected = true;
|
|
34
|
+
const positions = await adapter.getPositions();
|
|
35
|
+
for (const pos of positions) {
|
|
36
|
+
if (parseFloat(pos.size) === 0)
|
|
37
|
+
continue;
|
|
38
|
+
allPositions.push({
|
|
39
|
+
exchange: adapter.info.name,
|
|
40
|
+
market: pos.market,
|
|
41
|
+
side: pos.side,
|
|
42
|
+
size: pos.size,
|
|
43
|
+
entryPrice: pos.entryPrice,
|
|
44
|
+
markPrice: pos.markPrice,
|
|
45
|
+
unrealizedPnl: pos.unrealizedPnl,
|
|
46
|
+
leverage: pos.leverage,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
errors.push({
|
|
52
|
+
exchange: adapter?.info.name ?? exchangeId,
|
|
53
|
+
error: err instanceof Error ? err.message : "Unknown error",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
if (connected && adapter) {
|
|
58
|
+
await adapter.disconnect().catch(() => undefined);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (outputOpts.json) {
|
|
63
|
+
output({ positions: allPositions, errors }, { json: true });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (allPositions.length === 0) {
|
|
67
|
+
console.log(" No open positions found.\n");
|
|
68
|
+
if (errors.length > 0) {
|
|
69
|
+
console.log(" \x1b[33mNote:\x1b[0m Some exchanges require authentication:");
|
|
70
|
+
for (const e of errors) {
|
|
71
|
+
console.log(` ${e.exchange}: ${e.error}`);
|
|
72
|
+
}
|
|
73
|
+
console.log("");
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Display positions
|
|
78
|
+
console.log(" \x1b[1mOpen Positions\x1b[0m\n");
|
|
79
|
+
console.log(" " +
|
|
80
|
+
"Exchange".padEnd(14) +
|
|
81
|
+
"Market".padEnd(12) +
|
|
82
|
+
"Side".padEnd(8) +
|
|
83
|
+
"Size".padEnd(14) +
|
|
84
|
+
"Entry".padEnd(12) +
|
|
85
|
+
"Mark".padEnd(12) +
|
|
86
|
+
"PnL");
|
|
87
|
+
console.log(" " + "─".repeat(85));
|
|
88
|
+
let totalPnl = 0;
|
|
89
|
+
for (const pos of allPositions) {
|
|
90
|
+
const sideColor = pos.side === "long" ? "\x1b[32m" : "\x1b[31m";
|
|
91
|
+
const pnl = parseFloat(pos.unrealizedPnl);
|
|
92
|
+
const pnlColor = pnl >= 0 ? "\x1b[32m" : "\x1b[31m";
|
|
93
|
+
const pnlSign = pnl >= 0 ? "+" : "";
|
|
94
|
+
totalPnl += pnl;
|
|
95
|
+
console.log(` ${pos.exchange.padEnd(12)}` +
|
|
96
|
+
`${pos.market.padEnd(12)}` +
|
|
97
|
+
`${sideColor}${pos.side.toUpperCase().padEnd(8)}\x1b[0m` +
|
|
98
|
+
`${pos.size.padEnd(14)}` +
|
|
99
|
+
`$${parseFloat(pos.entryPrice).toFixed(2).padEnd(11)}` +
|
|
100
|
+
`$${parseFloat(pos.markPrice).toFixed(2).padEnd(11)}` +
|
|
101
|
+
`${pnlColor}${pnlSign}$${pnl.toFixed(2)}\x1b[0m`);
|
|
102
|
+
}
|
|
103
|
+
console.log(" " + "─".repeat(85));
|
|
104
|
+
const totalPnlColor = totalPnl >= 0 ? "\x1b[32m" : "\x1b[31m";
|
|
105
|
+
const totalPnlSign = totalPnl >= 0 ? "+" : "";
|
|
106
|
+
console.log(` ${"Total PnL:".padEnd(66)}${totalPnlColor}${totalPnlSign}$${totalPnl.toFixed(2)}\x1b[0m`);
|
|
107
|
+
// Calculate net exposure per market
|
|
108
|
+
console.log("\n \x1b[1mNet Exposure\x1b[0m\n");
|
|
109
|
+
const exposureByMarket = new Map();
|
|
110
|
+
for (const pos of allPositions) {
|
|
111
|
+
const existing = exposureByMarket.get(pos.market) || {
|
|
112
|
+
market: pos.market,
|
|
113
|
+
long: 0,
|
|
114
|
+
short: 0,
|
|
115
|
+
net: 0,
|
|
116
|
+
value: 0,
|
|
117
|
+
};
|
|
118
|
+
const size = parseFloat(pos.size);
|
|
119
|
+
const price = parseFloat(pos.markPrice);
|
|
120
|
+
if (pos.side === "long") {
|
|
121
|
+
existing.long += size;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
existing.short += size;
|
|
125
|
+
}
|
|
126
|
+
existing.net = existing.long - existing.short;
|
|
127
|
+
existing.value = existing.net * price;
|
|
128
|
+
exposureByMarket.set(pos.market, existing);
|
|
129
|
+
}
|
|
130
|
+
console.log(" " + "Market".padEnd(12) + "Long".padEnd(14) + "Short".padEnd(14) + "Net".padEnd(14) + "Value");
|
|
131
|
+
console.log(" " + "─".repeat(60));
|
|
132
|
+
for (const exposure of exposureByMarket.values()) {
|
|
133
|
+
const netColor = Math.abs(exposure.net) < 0.0001
|
|
134
|
+
? "\x1b[32m"
|
|
135
|
+
: exposure.net > 0
|
|
136
|
+
? "\x1b[33m"
|
|
137
|
+
: "\x1b[31m";
|
|
138
|
+
const netStr = Math.abs(exposure.net) < 0.0001
|
|
139
|
+
? "HEDGED"
|
|
140
|
+
: exposure.net.toFixed(6);
|
|
141
|
+
console.log(` ${exposure.market.padEnd(12)}` +
|
|
142
|
+
`${exposure.long.toFixed(6).padEnd(14)}` +
|
|
143
|
+
`${exposure.short.toFixed(6).padEnd(14)}` +
|
|
144
|
+
`${netColor}${netStr.padEnd(14)}\x1b[0m` +
|
|
145
|
+
`$${exposure.value.toFixed(2)}`);
|
|
146
|
+
}
|
|
147
|
+
console.log("");
|
|
148
|
+
if (errors.length > 0) {
|
|
149
|
+
console.log(" \x1b[33mCould not check:\x1b[0m " + errors.map(e => e.exchange).join(", "));
|
|
150
|
+
console.log("");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
outputError(err instanceof Error ? err.message : String(err));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arb Spread Command
|
|
3
|
+
* Compare prices across all exchanges for arbitrage opportunities
|
|
4
|
+
*/
|
|
5
|
+
import { getContext, getOutputOptions } from "../../cli/program.js";
|
|
6
|
+
import { output, outputError } from "../../cli/output.js";
|
|
7
|
+
import { getExchangeAdapterById, getAvailableExchanges } from "../../lib/exchange.js";
|
|
8
|
+
export function registerArbSpreadCommand(arb) {
|
|
9
|
+
arb
|
|
10
|
+
.command("spread [asset]")
|
|
11
|
+
.description("Compare prices across exchanges")
|
|
12
|
+
.option("-w, --watch", "Watch mode - continuously update")
|
|
13
|
+
.option("--all", "Show all common markets")
|
|
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 (outputOpts.json && opts.watch) {
|
|
22
|
+
throw new Error("Watch mode is not supported with --json");
|
|
23
|
+
}
|
|
24
|
+
if (opts.all) {
|
|
25
|
+
await showAllSpreads(exchanges, outputOpts.json, isTestnet);
|
|
26
|
+
}
|
|
27
|
+
else if (asset) {
|
|
28
|
+
if (opts.watch) {
|
|
29
|
+
await watchSpread(asset.toUpperCase(), exchanges, isTestnet);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
await showSpread(asset.toUpperCase(), exchanges, outputOpts.json, isTestnet);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// Default: show BTC spread
|
|
37
|
+
await showSpread("BTC", exchanges, outputOpts.json, isTestnet);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
outputError(err instanceof Error ? err.message : String(err));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function fetchPrices(asset, exchanges, isTestnet) {
|
|
47
|
+
const results = [];
|
|
48
|
+
const promises = exchanges.map(async (exchangeId) => {
|
|
49
|
+
const adapter = getExchangeAdapterById(exchangeId);
|
|
50
|
+
let connected = false;
|
|
51
|
+
try {
|
|
52
|
+
await adapter.connect({ testnet: isTestnet });
|
|
53
|
+
connected = true;
|
|
54
|
+
// Try to get ticker for this asset
|
|
55
|
+
const symbol = `${asset}-PERP`;
|
|
56
|
+
const ticker = await adapter.getTicker(symbol);
|
|
57
|
+
return {
|
|
58
|
+
exchange: adapter.info.name,
|
|
59
|
+
symbol: ticker.market,
|
|
60
|
+
markPrice: parseFloat(ticker.markPrice),
|
|
61
|
+
indexPrice: parseFloat(ticker.indexPrice),
|
|
62
|
+
bid: parseFloat(ticker.bid),
|
|
63
|
+
ask: parseFloat(ticker.ask),
|
|
64
|
+
fundingRate: parseFloat(ticker.fundingRate) * 100, // Convert to percentage
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
return {
|
|
69
|
+
exchange: adapter.info.name,
|
|
70
|
+
symbol: `${asset}-PERP`,
|
|
71
|
+
markPrice: 0,
|
|
72
|
+
indexPrice: 0,
|
|
73
|
+
bid: 0,
|
|
74
|
+
ask: 0,
|
|
75
|
+
fundingRate: 0,
|
|
76
|
+
error: err instanceof Error ? err.message : "Unknown error",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
if (connected) {
|
|
81
|
+
await adapter.disconnect().catch(() => undefined);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const settled = await Promise.allSettled(promises);
|
|
86
|
+
for (const result of settled) {
|
|
87
|
+
if (result.status === "fulfilled") {
|
|
88
|
+
results.push(result.value);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
async function showSpread(asset, exchanges, json, isTestnet) {
|
|
94
|
+
if (!json) {
|
|
95
|
+
console.log(`\nFetching ${asset} prices across ${exchanges.length} exchanges...\n`);
|
|
96
|
+
}
|
|
97
|
+
const prices = await fetchPrices(asset, exchanges, isTestnet);
|
|
98
|
+
const validPrices = prices.filter(p => p.markPrice > 0);
|
|
99
|
+
if (validPrices.length === 0) {
|
|
100
|
+
if (json) {
|
|
101
|
+
output({
|
|
102
|
+
status: "no_data",
|
|
103
|
+
asset,
|
|
104
|
+
prices: [],
|
|
105
|
+
exchangesScanned: exchanges.length,
|
|
106
|
+
}, { json: true });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
console.log(`No prices found for ${asset} on any exchange`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Find best bid (highest) and best ask (lowest)
|
|
113
|
+
const bestBid = validPrices.reduce((best, p) => p.bid > best.bid ? p : best, validPrices[0]);
|
|
114
|
+
const bestAsk = validPrices.reduce((best, p) => p.ask < best.ask || best.ask === 0 ? p : best, validPrices[0]);
|
|
115
|
+
// Calculate spread
|
|
116
|
+
const spreadPct = ((bestBid.bid - bestAsk.ask) / bestAsk.ask) * 100;
|
|
117
|
+
const data = {
|
|
118
|
+
asset,
|
|
119
|
+
prices: validPrices,
|
|
120
|
+
bestBid: { exchange: bestBid.exchange, price: bestBid.bid },
|
|
121
|
+
bestAsk: { exchange: bestAsk.exchange, price: bestAsk.ask },
|
|
122
|
+
spreadPct,
|
|
123
|
+
timestamp: Date.now(),
|
|
124
|
+
};
|
|
125
|
+
if (json) {
|
|
126
|
+
output(data, { json: true });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Table output
|
|
130
|
+
console.log(` ${asset}-PERP Price Comparison\n`);
|
|
131
|
+
console.log(" Exchange".padEnd(18) + "Mark Price".padEnd(14) + "Bid".padEnd(14) + "Ask".padEnd(14) + "Funding");
|
|
132
|
+
console.log(" " + "─".repeat(70));
|
|
133
|
+
// Sort by mark price descending
|
|
134
|
+
validPrices.sort((a, b) => b.markPrice - a.markPrice);
|
|
135
|
+
for (const p of validPrices) {
|
|
136
|
+
const isBestBid = p.exchange === bestBid.exchange;
|
|
137
|
+
const isBestAsk = p.exchange === bestAsk.exchange;
|
|
138
|
+
const bidStr = isBestBid ? `\x1b[32m${p.bid.toFixed(2)}\x1b[0m` : p.bid.toFixed(2);
|
|
139
|
+
const askStr = isBestAsk ? `\x1b[31m${p.ask.toFixed(2)}\x1b[0m` : p.ask.toFixed(2);
|
|
140
|
+
const fundingStr = p.fundingRate >= 0
|
|
141
|
+
? `\x1b[32m+${p.fundingRate.toFixed(4)}%\x1b[0m`
|
|
142
|
+
: `\x1b[31m${p.fundingRate.toFixed(4)}%\x1b[0m`;
|
|
143
|
+
console.log(` ${p.exchange.padEnd(16)}` +
|
|
144
|
+
`$${p.markPrice.toFixed(2).padEnd(13)}` +
|
|
145
|
+
`${bidStr.padEnd(14 + (isBestBid ? 9 : 0))}` + // Account for ANSI codes
|
|
146
|
+
`${askStr.padEnd(14 + (isBestAsk ? 9 : 0))}` +
|
|
147
|
+
fundingStr);
|
|
148
|
+
}
|
|
149
|
+
// Show errors
|
|
150
|
+
const errors = prices.filter(p => p.error);
|
|
151
|
+
if (errors.length > 0) {
|
|
152
|
+
console.log("\n \x1b[33mNot available on:\x1b[0m " + errors.map(e => e.exchange).join(", "));
|
|
153
|
+
}
|
|
154
|
+
// Show spread summary
|
|
155
|
+
console.log("\n " + "─".repeat(70));
|
|
156
|
+
if (spreadPct > 0) {
|
|
157
|
+
console.log(`\n \x1b[32mArbitrage Opportunity: ${spreadPct.toFixed(4)}%\x1b[0m`);
|
|
158
|
+
console.log(` Buy on ${bestAsk.exchange} @ $${bestAsk.ask.toFixed(2)}`);
|
|
159
|
+
console.log(` Sell on ${bestBid.exchange} @ $${bestBid.bid.toFixed(2)}`);
|
|
160
|
+
console.log(` Profit: $${(bestBid.bid - bestAsk.ask).toFixed(2)} per unit`);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
console.log(`\n No arbitrage opportunity (spread: ${spreadPct.toFixed(4)}%)`);
|
|
164
|
+
}
|
|
165
|
+
console.log("");
|
|
166
|
+
}
|
|
167
|
+
async function showAllSpreads(exchanges, json, isTestnet) {
|
|
168
|
+
// Common assets across exchanges
|
|
169
|
+
const assets = ["BTC", "ETH", "SOL", "DOGE", "ARB", "OP", "WIF", "TIA"];
|
|
170
|
+
const rows = [];
|
|
171
|
+
if (!json) {
|
|
172
|
+
console.log("\nFetching prices for common assets...\n");
|
|
173
|
+
console.log(" Asset".padEnd(10) + "Best Bid".padEnd(24) + "Best Ask".padEnd(24) + "Spread");
|
|
174
|
+
console.log(" " + "─".repeat(70));
|
|
175
|
+
}
|
|
176
|
+
for (const asset of assets) {
|
|
177
|
+
const prices = await fetchPrices(asset, exchanges, isTestnet);
|
|
178
|
+
const validPrices = prices.filter(p => p.markPrice > 0);
|
|
179
|
+
if (validPrices.length < 2) {
|
|
180
|
+
if (!json) {
|
|
181
|
+
console.log(` ${asset.padEnd(8)} Available on <2 exchanges`);
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const bestBid = validPrices.reduce((best, p) => p.bid > best.bid ? p : best, validPrices[0]);
|
|
186
|
+
const bestAsk = validPrices.reduce((best, p) => p.ask < best.ask || best.ask === 0 ? p : best, validPrices[0]);
|
|
187
|
+
const spreadPct = ((bestBid.bid - bestAsk.ask) / bestAsk.ask) * 100;
|
|
188
|
+
rows.push({
|
|
189
|
+
asset,
|
|
190
|
+
bestBid: { exchange: bestBid.exchange, price: bestBid.bid },
|
|
191
|
+
bestAsk: { exchange: bestAsk.exchange, price: bestAsk.ask },
|
|
192
|
+
spreadPct,
|
|
193
|
+
timestamp: Date.now(),
|
|
194
|
+
});
|
|
195
|
+
if (!json) {
|
|
196
|
+
const spreadColor = spreadPct > 0 ? "\x1b[32m" : "\x1b[90m";
|
|
197
|
+
console.log(` ${asset.padEnd(8)}` +
|
|
198
|
+
`${bestBid.exchange.padEnd(12)} $${bestBid.bid.toFixed(2).padEnd(10)}` +
|
|
199
|
+
`${bestAsk.exchange.padEnd(12)} $${bestAsk.ask.toFixed(2).padEnd(10)}` +
|
|
200
|
+
`${spreadColor}${spreadPct.toFixed(4)}%\x1b[0m`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (json) {
|
|
204
|
+
output(rows, { json: true });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
console.log("");
|
|
208
|
+
}
|
|
209
|
+
async function watchSpread(asset, exchanges, isTestnet) {
|
|
210
|
+
console.log(`\nWatching ${asset} spread (Ctrl+C to exit)...\n`);
|
|
211
|
+
const refresh = async () => {
|
|
212
|
+
// Clear screen
|
|
213
|
+
process.stdout.write("\x1B[2J\x1B[0f");
|
|
214
|
+
console.log(`\n ${asset}-PERP Spread Monitor (${new Date().toLocaleTimeString()})\n`);
|
|
215
|
+
const prices = await fetchPrices(asset, exchanges, isTestnet);
|
|
216
|
+
const validPrices = prices.filter(p => p.markPrice > 0);
|
|
217
|
+
if (validPrices.length === 0) {
|
|
218
|
+
console.log(` No prices available`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
console.log(" Exchange".padEnd(18) + "Mark".padEnd(14) + "Bid".padEnd(14) + "Ask".padEnd(14) + "Funding");
|
|
222
|
+
console.log(" " + "─".repeat(65));
|
|
223
|
+
validPrices.sort((a, b) => b.markPrice - a.markPrice);
|
|
224
|
+
for (const p of validPrices) {
|
|
225
|
+
const fundingColor = p.fundingRate >= 0 ? "\x1b[32m" : "\x1b[31m";
|
|
226
|
+
console.log(` ${p.exchange.padEnd(16)}` +
|
|
227
|
+
`$${p.markPrice.toFixed(2).padEnd(13)}` +
|
|
228
|
+
`$${p.bid.toFixed(2).padEnd(13)}` +
|
|
229
|
+
`$${p.ask.toFixed(2).padEnd(13)}` +
|
|
230
|
+
`${fundingColor}${p.fundingRate.toFixed(4)}%\x1b[0m`);
|
|
231
|
+
}
|
|
232
|
+
// Calculate spread
|
|
233
|
+
const bestBid = validPrices.reduce((best, p) => p.bid > best.bid ? p : best, validPrices[0]);
|
|
234
|
+
const bestAsk = validPrices.reduce((best, p) => p.ask < best.ask ? p : best, validPrices[0]);
|
|
235
|
+
const spread = ((bestBid.bid - bestAsk.ask) / bestAsk.ask) * 100;
|
|
236
|
+
console.log("\n " + "─".repeat(65));
|
|
237
|
+
console.log(`\n Spread: ${spread >= 0 ? "\x1b[32m" : "\x1b[90m"}${spread.toFixed(4)}%\x1b[0m`);
|
|
238
|
+
console.log(` Best bid: ${bestBid.exchange} @ $${bestBid.bid.toFixed(2)}`);
|
|
239
|
+
console.log(` Best ask: ${bestAsk.exchange} @ $${bestAsk.ask.toFixed(2)}`);
|
|
240
|
+
console.log("\n Press Ctrl+C to exit");
|
|
241
|
+
};
|
|
242
|
+
// Initial refresh
|
|
243
|
+
await refresh();
|
|
244
|
+
// Refresh every 5 seconds
|
|
245
|
+
const interval = setInterval(refresh, 5000);
|
|
246
|
+
process.once("SIGINT", () => {
|
|
247
|
+
clearInterval(interval);
|
|
248
|
+
console.log("\n");
|
|
249
|
+
process.exit(0);
|
|
250
|
+
});
|
|
251
|
+
// Keep alive
|
|
252
|
+
await new Promise(() => { });
|
|
253
|
+
}
|