@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,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arb Basis Execute Command
|
|
3
|
+
* Execute basis trades (cash-and-carry arbitrage) on a single exchange
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { confirm } from "@inquirer/prompts";
|
|
7
|
+
import { getContext, getOutputOptions, getSelectedExchange } from "../../cli/program.js";
|
|
8
|
+
import { output, outputError } from "../../cli/output.js";
|
|
9
|
+
import { getExchangeAdapterById } from "../../lib/exchange.js";
|
|
10
|
+
import { 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 registerArbBasisExecuteCommand(arb) {
|
|
19
|
+
arb
|
|
20
|
+
.command("basis-execute [asset]")
|
|
21
|
+
.description("Execute basis trade (cash-and-carry arbitrage)")
|
|
22
|
+
.option("-s, --size <usd>", "Position size in USD (required)")
|
|
23
|
+
.option("--confidence <0-1>", "Signal confidence for risk evaluation (default: 0.5)")
|
|
24
|
+
.option("--exchange <id>", "Exchange to trade on (overrides default)")
|
|
25
|
+
.option("--min-basis <pct>", "Minimum basis to consider actionable (default: 0.1)")
|
|
26
|
+
.option("--tp <pct>", "Take-profit basis convergence threshold")
|
|
27
|
+
.option("--sl <pct>", "Stop-loss basis divergence threshold")
|
|
28
|
+
.option("--dry-run", "Show what would happen without executing")
|
|
29
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
30
|
+
.option("--idempotency-key <key>", "Explicit idempotency key")
|
|
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.basis-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 basis execution", EXIT_CODES.VALIDATION_ERROR);
|
|
42
|
+
}
|
|
43
|
+
const assetName = validateAsset(asset);
|
|
44
|
+
const market = `${assetName}-PERP`;
|
|
45
|
+
if (!opts.size) {
|
|
46
|
+
throw new CLIError("Position size is required. Use --size <usd>", EXIT_CODES.VALIDATION_ERROR);
|
|
47
|
+
}
|
|
48
|
+
const requestedSizeUsd = validateSize(opts.size, DEFAULT_ARB_SIZE_USD);
|
|
49
|
+
const confidence = opts.confidence ? parseFloat(opts.confidence) : 0.5;
|
|
50
|
+
if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
|
|
51
|
+
throw new CLIError("Confidence must be a number between 0 and 1", EXIT_CODES.VALIDATION_ERROR);
|
|
52
|
+
}
|
|
53
|
+
const minBasis = opts.minBasis ? parseFloat(opts.minBasis) : 0.1;
|
|
54
|
+
if (!Number.isFinite(minBasis) || minBasis < 0) {
|
|
55
|
+
throw new CLIError("Min basis must be a non-negative number", EXIT_CODES.VALIDATION_ERROR);
|
|
56
|
+
}
|
|
57
|
+
const exchangeId = opts.exchange ?? getSelectedExchange(this);
|
|
58
|
+
const adapter = getExchangeAdapterById(exchangeId);
|
|
59
|
+
const credentials = getExchangeCredentials(ctx.config, exchangeId, {
|
|
60
|
+
requireTrading: true,
|
|
61
|
+
});
|
|
62
|
+
await adapter.connect({
|
|
63
|
+
testnet: ctx.config.testnet,
|
|
64
|
+
rpcUrl: credentials.fullnodeUrl,
|
|
65
|
+
wsUrl: credentials.wsUrl,
|
|
66
|
+
credentials,
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
const ticker = await adapter.getTicker(market);
|
|
70
|
+
const markPrice = parseFloat(ticker.markPrice);
|
|
71
|
+
const indexPrice = parseFloat(ticker.indexPrice);
|
|
72
|
+
if (!Number.isFinite(markPrice) || markPrice <= 0) {
|
|
73
|
+
throw new CLIError("Unable to fetch reliable mark price for basis calculation", EXIT_CODES.DATA_UNAVAILABLE);
|
|
74
|
+
}
|
|
75
|
+
if (!Number.isFinite(indexPrice) || indexPrice <= 0) {
|
|
76
|
+
if (isJson) {
|
|
77
|
+
emitJson({
|
|
78
|
+
status: "no_data",
|
|
79
|
+
reason: "no_index_price",
|
|
80
|
+
market,
|
|
81
|
+
exchange: exchangeId,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
console.log("\n No index price available for this market on this exchange.\n");
|
|
86
|
+
}
|
|
87
|
+
process.exitCode = EXIT_CODES.DATA_UNAVAILABLE;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Compute basis: (markPrice - indexPrice) / indexPrice * 100
|
|
91
|
+
const basisPct = ((markPrice - indexPrice) / indexPrice) * 100;
|
|
92
|
+
// Guard: basis too small
|
|
93
|
+
if (Math.abs(basisPct) < minBasis) {
|
|
94
|
+
if (isJson) {
|
|
95
|
+
emitJson({
|
|
96
|
+
status: "no_opportunity",
|
|
97
|
+
reason: "basis_too_small",
|
|
98
|
+
market,
|
|
99
|
+
exchange: exchangeId,
|
|
100
|
+
basisPct,
|
|
101
|
+
minBasis,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
console.log(`\n Basis spread ${basisPct >= 0 ? "+" : ""}${basisPct.toFixed(4)}% is below minimum threshold of ${minBasis}%.`);
|
|
106
|
+
console.log(" No actionable opportunity.\n");
|
|
107
|
+
}
|
|
108
|
+
process.exitCode = EXIT_CODES.NO_OPPORTUNITY;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Determine side: basis > 0 (premium) -> short, basis < 0 (discount) -> long
|
|
112
|
+
const side = basisPct > 0 ? "short" : "long";
|
|
113
|
+
const direction = basisPct > 0 ? "premium" : "discount";
|
|
114
|
+
// Risk evaluation
|
|
115
|
+
const risk = new RiskPolicyMiddleware(ctx.config, exchangeId);
|
|
116
|
+
const evaluation = await risk.evaluateUsdSignal(adapter, { market, side, confidence, reason: "basis-trade", timestamp: Date.now() }, requestedSizeUsd);
|
|
117
|
+
if (!evaluation.allowed) {
|
|
118
|
+
throw new CLIError(`Trade blocked by risk policy: ${evaluation.reason ?? "unknown"}`, EXIT_CODES.EXECUTION_ERROR);
|
|
119
|
+
}
|
|
120
|
+
const sizeUsd = Math.min(requestedSizeUsd, evaluation.sizeUsd);
|
|
121
|
+
if (!Number.isFinite(sizeUsd) || sizeUsd <= 0) {
|
|
122
|
+
throw new CLIError("Risk policy reduced trade size to zero", EXIT_CODES.EXECUTION_ERROR);
|
|
123
|
+
}
|
|
124
|
+
// Build execution plan
|
|
125
|
+
const annualizedPct = Math.abs(basisPct) * 365;
|
|
126
|
+
const dailyProfit = (Math.abs(basisPct) / 100) * sizeUsd;
|
|
127
|
+
const plan = {
|
|
128
|
+
asset: assetName,
|
|
129
|
+
exchange: exchangeId,
|
|
130
|
+
size: sizeUsd,
|
|
131
|
+
side,
|
|
132
|
+
markPrice,
|
|
133
|
+
indexPrice,
|
|
134
|
+
basisPct,
|
|
135
|
+
direction,
|
|
136
|
+
annualizedPct,
|
|
137
|
+
dailyProfit,
|
|
138
|
+
};
|
|
139
|
+
if (!isJson) {
|
|
140
|
+
console.log("\n \x1b[1m━━━ Basis Trade Execution Plan ━━━\x1b[0m\n");
|
|
141
|
+
console.log(` Asset: ${assetName}`);
|
|
142
|
+
console.log(` Exchange: ${exchangeId}`);
|
|
143
|
+
console.log(` Position Size: $${sizeUsd.toLocaleString()}`);
|
|
144
|
+
if (Math.abs(sizeUsd - requestedSizeUsd) > 1e-9) {
|
|
145
|
+
console.log(` Risk Adjusted: from $${requestedSizeUsd.toLocaleString()}`);
|
|
146
|
+
}
|
|
147
|
+
console.log("");
|
|
148
|
+
console.log(` Mark Price: $${markPrice.toLocaleString()}`);
|
|
149
|
+
console.log(` Index Price: $${indexPrice.toLocaleString()}`);
|
|
150
|
+
console.log(` Basis: ${basisPct >= 0 ? "+" : ""}${basisPct.toFixed(4)}% (${direction})`);
|
|
151
|
+
console.log("");
|
|
152
|
+
const sideColor = side === "short" ? "\x1b[31m" : "\x1b[32m";
|
|
153
|
+
const sideArrow = side === "short" ? "▼" : "▲";
|
|
154
|
+
console.log(` ${sideColor}${sideArrow} ${side.toUpperCase()}\x1b[0m ${assetName}-PERP`);
|
|
155
|
+
console.log(` Rationale: ${direction === "premium" ? "Sell premium — basis converges to zero" : "Buy discount — basis converges to zero"}`);
|
|
156
|
+
console.log("");
|
|
157
|
+
console.log(" " + "─".repeat(45));
|
|
158
|
+
console.log(` Annualized: \x1b[32m${annualizedPct.toFixed(2)}%\x1b[0m`);
|
|
159
|
+
console.log(` Est. Daily: \x1b[32m$${dailyProfit.toFixed(2)}\x1b[0m`);
|
|
160
|
+
console.log(" " + "─".repeat(45));
|
|
161
|
+
console.log("");
|
|
162
|
+
}
|
|
163
|
+
if (opts.dryRun) {
|
|
164
|
+
if (isJson) {
|
|
165
|
+
emitJson({
|
|
166
|
+
status: "dry_run",
|
|
167
|
+
market,
|
|
168
|
+
asset: assetName,
|
|
169
|
+
exchange: exchangeId,
|
|
170
|
+
testnet: ctx.config.testnet,
|
|
171
|
+
requestedSizeUsd,
|
|
172
|
+
plan,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
console.log(" \x1b[33m[DRY RUN]\x1b[0m Execution skipped.\n");
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!opts.yes) {
|
|
181
|
+
const confirmed = await confirm({
|
|
182
|
+
message: "Execute this basis trade?",
|
|
183
|
+
default: false,
|
|
184
|
+
});
|
|
185
|
+
if (!confirmed) {
|
|
186
|
+
if (isJson) {
|
|
187
|
+
emitJson({
|
|
188
|
+
status: "cancelled",
|
|
189
|
+
reason: "user_declined_confirmation",
|
|
190
|
+
market,
|
|
191
|
+
asset: assetName,
|
|
192
|
+
plan,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
console.log("\n Execution cancelled.\n");
|
|
197
|
+
}
|
|
198
|
+
process.exitCode = EXIT_CODES.CANCELLED;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const orderSize = (sizeUsd / markPrice).toFixed(8);
|
|
203
|
+
const shouldAttachTpSl = opts.tp !== undefined || opts.sl !== undefined;
|
|
204
|
+
const tp = opts.tp !== undefined ? parseFloat(opts.tp) : ctx.config.executionSafety.takeProfitPct;
|
|
205
|
+
const sl = opts.sl !== undefined ? parseFloat(opts.sl) : ctx.config.executionSafety.stopLossPct;
|
|
206
|
+
if (shouldAttachTpSl) {
|
|
207
|
+
if (!Number.isFinite(tp) || tp <= 0) {
|
|
208
|
+
throw new CLIError("TP must be a positive number", EXIT_CODES.VALIDATION_ERROR);
|
|
209
|
+
}
|
|
210
|
+
if (!Number.isFinite(sl) || sl <= 0) {
|
|
211
|
+
throw new CLIError("SL must be a positive number", EXIT_CODES.VALIDATION_ERROR);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Build idempotency key
|
|
215
|
+
const idempotencyKey = opts.idempotencyKey?.trim() || undefined;
|
|
216
|
+
const autoKeyHash = createHash("sha256")
|
|
217
|
+
.update(`basis:${exchangeId}:${market}:${side}:${sizeUsd}`)
|
|
218
|
+
.digest("hex")
|
|
219
|
+
.slice(0, 16);
|
|
220
|
+
const timeBucket = Math.floor(Date.now() / 30_000);
|
|
221
|
+
const resolvedIdempotencyKey = idempotencyKey ?? `basis:${autoKeyHash}:${timeBucket}`;
|
|
222
|
+
if (!isJson) {
|
|
223
|
+
console.log(`\n \x1b[36mExecuting ${side.toUpperCase()} on ${exchangeId}...\x1b[0m\n`);
|
|
224
|
+
}
|
|
225
|
+
const journal = await runWithExecutionJournal({
|
|
226
|
+
idempotencyKey: resolvedIdempotencyKey,
|
|
227
|
+
metadata: {
|
|
228
|
+
command: "arb.basis-execute",
|
|
229
|
+
exchange: exchangeId,
|
|
230
|
+
testnet: ctx.config.testnet,
|
|
231
|
+
market,
|
|
232
|
+
side,
|
|
233
|
+
orderType: "market",
|
|
234
|
+
},
|
|
235
|
+
request: {
|
|
236
|
+
market,
|
|
237
|
+
side,
|
|
238
|
+
size: orderSize,
|
|
239
|
+
spreadAware: true,
|
|
240
|
+
spreadOffset: ctx.config.executionSafety.spreadOffset,
|
|
241
|
+
tp: shouldAttachTpSl ? tp : null,
|
|
242
|
+
sl: shouldAttachTpSl ? sl : null,
|
|
243
|
+
requestedSizeUsd,
|
|
244
|
+
adjustedSizeUsd: sizeUsd,
|
|
245
|
+
confidence,
|
|
246
|
+
basisPct,
|
|
247
|
+
},
|
|
248
|
+
execute: async () => {
|
|
249
|
+
const result = await executeOrderWithSafety(adapter, {
|
|
250
|
+
market,
|
|
251
|
+
side,
|
|
252
|
+
type: "market",
|
|
253
|
+
size: orderSize,
|
|
254
|
+
}, {
|
|
255
|
+
closeThenFlip: true,
|
|
256
|
+
spreadAwarePricing: true,
|
|
257
|
+
spreadOffset: ctx.config.executionSafety.spreadOffset,
|
|
258
|
+
attachTpSl: shouldAttachTpSl,
|
|
259
|
+
tpSlConfig: shouldAttachTpSl
|
|
260
|
+
? { stopLossPct: sl, takeProfitPct: tp }
|
|
261
|
+
: undefined,
|
|
262
|
+
});
|
|
263
|
+
return {
|
|
264
|
+
order: {
|
|
265
|
+
id: result.order.id,
|
|
266
|
+
market: result.order.market,
|
|
267
|
+
side: result.order.side,
|
|
268
|
+
size: result.order.size,
|
|
269
|
+
status: result.order.status,
|
|
270
|
+
price: result.order.price,
|
|
271
|
+
},
|
|
272
|
+
safety: {
|
|
273
|
+
closedOppositePosition: result.closedOppositePosition,
|
|
274
|
+
tpSlOrderIds: result.tpSlOrderIds,
|
|
275
|
+
pricing: result.pricing,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
if (!isJson) {
|
|
281
|
+
console.log(` ✓ ${side.toUpperCase()} ${journal.replayed ? "replayed" : "opened"}: ${journal.result.order.id}`);
|
|
282
|
+
console.log("\n \x1b[32m━━━ Basis Trade Executed ━━━\x1b[0m");
|
|
283
|
+
if (journal.result.safety.tpSlOrderIds.length > 0) {
|
|
284
|
+
console.log(" TP/SL attached");
|
|
285
|
+
}
|
|
286
|
+
console.log(` Monitor position with: perps -e ${exchangeId} account positions`);
|
|
287
|
+
console.log("");
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
emitJson({
|
|
291
|
+
status: "executed",
|
|
292
|
+
market,
|
|
293
|
+
asset: assetName,
|
|
294
|
+
exchange: exchangeId,
|
|
295
|
+
testnet: ctx.config.testnet,
|
|
296
|
+
requestedSizeUsd,
|
|
297
|
+
plan,
|
|
298
|
+
execution: {
|
|
299
|
+
...journal.result,
|
|
300
|
+
idempotency: {
|
|
301
|
+
key: journal.idempotencyKey,
|
|
302
|
+
replayed: journal.replayed,
|
|
303
|
+
autoGenerated: journal.autoGeneratedKey,
|
|
304
|
+
journalId: journal.journalId,
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
await adapter.disconnect().catch(() => undefined);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
const code = inferExitCode(err);
|
|
316
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
317
|
+
if (isJson) {
|
|
318
|
+
emitJson({
|
|
319
|
+
status: "error",
|
|
320
|
+
error: {
|
|
321
|
+
message,
|
|
322
|
+
exitCode: code,
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
outputError(message);
|
|
328
|
+
}
|
|
329
|
+
process.exit(code);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arb Basis Command
|
|
3
|
+
* Fetch perp and spot/index prices to show basis spread 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
|
+
import { withJsonContract } from "../../lib/contracts.js";
|
|
9
|
+
export function registerArbBasisCommand(arb) {
|
|
10
|
+
arb
|
|
11
|
+
.command("basis [asset]")
|
|
12
|
+
.description("Show basis spread between perp and index prices")
|
|
13
|
+
.option("--min-basis <pct>", "Minimum basis spread to show", "0.05")
|
|
14
|
+
.option("-w, --watch", "Watch mode - refresh every 10s")
|
|
15
|
+
.option("--json", "JSON output")
|
|
16
|
+
.action(async function (asset) {
|
|
17
|
+
const ctx = getContext(this);
|
|
18
|
+
const outputOpts = getOutputOptions(this);
|
|
19
|
+
const opts = this.opts();
|
|
20
|
+
const isTestnet = ctx.config.testnet;
|
|
21
|
+
const resolvedAsset = asset ? asset.toUpperCase() : "BTC";
|
|
22
|
+
const minBasis = parseFloat(opts.minBasis ?? "0.05");
|
|
23
|
+
try {
|
|
24
|
+
const exchanges = getAvailableExchanges();
|
|
25
|
+
if (outputOpts.json && opts.watch) {
|
|
26
|
+
throw new Error("Watch mode is not supported with --json");
|
|
27
|
+
}
|
|
28
|
+
if (opts.watch) {
|
|
29
|
+
await watchBasis(resolvedAsset, exchanges, minBasis, isTestnet);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
await showBasis(resolvedAsset, exchanges, minBasis, outputOpts.json, isTestnet);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
outputError(err instanceof Error ? err.message : String(err));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async function fetchBasis(asset, exchanges, isTestnet) {
|
|
42
|
+
const results = [];
|
|
43
|
+
const promises = exchanges.map(async (exchangeId) => {
|
|
44
|
+
const adapter = getExchangeAdapterById(exchangeId);
|
|
45
|
+
let connected = false;
|
|
46
|
+
try {
|
|
47
|
+
await adapter.connect({ testnet: isTestnet });
|
|
48
|
+
connected = true;
|
|
49
|
+
const symbol = `${asset}-PERP`;
|
|
50
|
+
const ticker = await adapter.getTicker(symbol);
|
|
51
|
+
const markPrice = parseFloat(ticker.markPrice);
|
|
52
|
+
const indexPrice = parseFloat(ticker.indexPrice);
|
|
53
|
+
if (!indexPrice || indexPrice === 0) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const basisPct = ((markPrice - indexPrice) / indexPrice) * 100;
|
|
57
|
+
const annualizedPct = Math.abs(basisPct) * 365;
|
|
58
|
+
return {
|
|
59
|
+
exchange: adapter.info.name,
|
|
60
|
+
markPrice,
|
|
61
|
+
indexPrice,
|
|
62
|
+
basisPct,
|
|
63
|
+
direction: (basisPct >= 0 ? "premium" : "discount"),
|
|
64
|
+
annualizedPct,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (connected) {
|
|
72
|
+
await adapter.disconnect().catch(() => undefined);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const settled = await Promise.allSettled(promises);
|
|
77
|
+
for (const result of settled) {
|
|
78
|
+
if (result.status === "fulfilled" && result.value !== null) {
|
|
79
|
+
results.push(result.value);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return results;
|
|
83
|
+
}
|
|
84
|
+
function formatUsd(value) {
|
|
85
|
+
return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
86
|
+
}
|
|
87
|
+
async function showBasis(asset, exchanges, minBasis, json, isTestnet) {
|
|
88
|
+
if (!json) {
|
|
89
|
+
console.log(`\nFetching ${asset} basis across ${exchanges.length} exchanges...\n`);
|
|
90
|
+
}
|
|
91
|
+
const opportunities = await fetchBasis(asset, exchanges, isTestnet);
|
|
92
|
+
// Filter by minimum basis
|
|
93
|
+
const filtered = opportunities.filter((o) => Math.abs(o.basisPct) >= minBasis);
|
|
94
|
+
// Sort by absolute basis descending
|
|
95
|
+
filtered.sort((a, b) => Math.abs(b.basisPct) - Math.abs(a.basisPct));
|
|
96
|
+
if (json) {
|
|
97
|
+
output(withJsonContract("arb.basis.result", {
|
|
98
|
+
asset,
|
|
99
|
+
opportunities: filtered,
|
|
100
|
+
timestamp: Date.now(),
|
|
101
|
+
}), { json: true });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (filtered.length === 0) {
|
|
105
|
+
console.log(` No basis opportunities found for ${asset} above ${minBasis}%`);
|
|
106
|
+
console.log("");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
console.log(` Basis Spread: ${asset}\n`);
|
|
110
|
+
console.log(" Exchange".padEnd(18) +
|
|
111
|
+
"Mark Price".padEnd(16) +
|
|
112
|
+
"Index Price".padEnd(16) +
|
|
113
|
+
"Basis (%)".padEnd(14) +
|
|
114
|
+
"Direction");
|
|
115
|
+
console.log(" " + "\u2500".repeat(76));
|
|
116
|
+
for (const o of filtered) {
|
|
117
|
+
const basisColor = o.direction === "premium" ? "\x1b[32m" : "\x1b[31m";
|
|
118
|
+
const basisSign = o.basisPct >= 0 ? "+" : "";
|
|
119
|
+
const dirLabel = o.direction === "premium"
|
|
120
|
+
? "Premium (short perp opportunity)"
|
|
121
|
+
: "Discount";
|
|
122
|
+
console.log(` ${o.exchange.padEnd(16)}` +
|
|
123
|
+
`${formatUsd(o.markPrice).padEnd(16)}` +
|
|
124
|
+
`${formatUsd(o.indexPrice).padEnd(16)}` +
|
|
125
|
+
`${basisColor}${basisSign}${o.basisPct.toFixed(2)}%\x1b[0m`.padEnd(14 + 9) +
|
|
126
|
+
dirLabel);
|
|
127
|
+
}
|
|
128
|
+
// Best opportunity
|
|
129
|
+
const best = filtered[0];
|
|
130
|
+
console.log("\n " + "\u2500".repeat(76));
|
|
131
|
+
console.log(`\n Best opportunity: ${best.exchange} at ${best.basisPct >= 0 ? "+" : ""}${best.basisPct.toFixed(2)}% basis`);
|
|
132
|
+
console.log(` Annualized (if persistent): ~${best.annualizedPct.toFixed(1)}%`);
|
|
133
|
+
console.log("");
|
|
134
|
+
}
|
|
135
|
+
async function watchBasis(asset, exchanges, minBasis, isTestnet) {
|
|
136
|
+
console.log(`\nWatching ${asset} basis (Ctrl+C to exit)...\n`);
|
|
137
|
+
const refresh = async () => {
|
|
138
|
+
process.stdout.write("\x1B[2J\x1B[0f");
|
|
139
|
+
console.log(`\n ${asset} Basis Monitor (${new Date().toLocaleTimeString()})\n`);
|
|
140
|
+
const opportunities = await fetchBasis(asset, exchanges, isTestnet);
|
|
141
|
+
const filtered = opportunities.filter((o) => Math.abs(o.basisPct) >= minBasis);
|
|
142
|
+
filtered.sort((a, b) => Math.abs(b.basisPct) - Math.abs(a.basisPct));
|
|
143
|
+
if (filtered.length === 0) {
|
|
144
|
+
console.log(` No basis opportunities above ${minBasis}%`);
|
|
145
|
+
console.log("\n Press Ctrl+C to exit");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
console.log(" Exchange".padEnd(18) +
|
|
149
|
+
"Mark Price".padEnd(16) +
|
|
150
|
+
"Index Price".padEnd(16) +
|
|
151
|
+
"Basis (%)".padEnd(14) +
|
|
152
|
+
"Direction");
|
|
153
|
+
console.log(" " + "\u2500".repeat(76));
|
|
154
|
+
for (const o of filtered) {
|
|
155
|
+
const basisColor = o.direction === "premium" ? "\x1b[32m" : "\x1b[31m";
|
|
156
|
+
const basisSign = o.basisPct >= 0 ? "+" : "";
|
|
157
|
+
const dirLabel = o.direction === "premium"
|
|
158
|
+
? "Premium (short perp opportunity)"
|
|
159
|
+
: "Discount";
|
|
160
|
+
console.log(` ${o.exchange.padEnd(16)}` +
|
|
161
|
+
`${formatUsd(o.markPrice).padEnd(16)}` +
|
|
162
|
+
`${formatUsd(o.indexPrice).padEnd(16)}` +
|
|
163
|
+
`${basisColor}${basisSign}${o.basisPct.toFixed(2)}%\x1b[0m`.padEnd(14 + 9) +
|
|
164
|
+
dirLabel);
|
|
165
|
+
}
|
|
166
|
+
if (filtered.length > 0) {
|
|
167
|
+
const best = filtered[0];
|
|
168
|
+
console.log("\n " + "\u2500".repeat(76));
|
|
169
|
+
console.log(`\n Best: ${best.exchange} at ${best.basisPct >= 0 ? "+" : ""}${best.basisPct.toFixed(2)}% (annualized ~${best.annualizedPct.toFixed(1)}%)`);
|
|
170
|
+
}
|
|
171
|
+
console.log("\n Press Ctrl+C to exit");
|
|
172
|
+
};
|
|
173
|
+
await refresh();
|
|
174
|
+
const interval = setInterval(refresh, 10000);
|
|
175
|
+
process.once("SIGINT", () => {
|
|
176
|
+
clearInterval(interval);
|
|
177
|
+
console.log("\n");
|
|
178
|
+
process.exit(0);
|
|
179
|
+
});
|
|
180
|
+
await new Promise(() => { });
|
|
181
|
+
}
|