@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,1377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decibel Adapter
|
|
3
|
+
* Implements PerpDEXAdapter interface for Decibel (Aptos)
|
|
4
|
+
*/
|
|
5
|
+
import { registerAdapter, } from "./interface.js";
|
|
6
|
+
import { DecibelRestClient, } from "./decibel/rest-client.js";
|
|
7
|
+
import { DecibelWsFeed } from "./decibel/ws-feed.js";
|
|
8
|
+
import { DecibelOrderManager, } from "./decibel/order-manager.js";
|
|
9
|
+
import { createLogger } from "../lib/logger.js";
|
|
10
|
+
import { isRecord } from "./utils.js";
|
|
11
|
+
const log = createLogger("decibel");
|
|
12
|
+
const DECIBEL_DEFAULTS = {
|
|
13
|
+
testnet: {
|
|
14
|
+
fullnodeUrl: "https://api.testnet.aptoslabs.com/v1",
|
|
15
|
+
restUrl: "https://api.testnet.aptoslabs.com/decibel",
|
|
16
|
+
wsUrl: "wss://api.testnet.aptoslabs.com/decibel/ws",
|
|
17
|
+
packageAddress: "0x95254fcb0816b9f3ec71aa4de5f5f7f8f3efeef9239f0d705a4cd3fe2f452de3",
|
|
18
|
+
},
|
|
19
|
+
mainnet: {
|
|
20
|
+
fullnodeUrl: "https://api.mainnet.aptoslabs.com/v1",
|
|
21
|
+
restUrl: "https://api.mainnet.aptoslabs.com/decibel",
|
|
22
|
+
wsUrl: "wss://api.mainnet.aptoslabs.com/decibel/ws",
|
|
23
|
+
packageAddress: "0xb8a5788314451ce4d2fbbad32e1bad88d4184b73943b7fe5166eab93cf1a5a95",
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
export class DecibelAdapter {
|
|
27
|
+
info = {
|
|
28
|
+
id: "decibel",
|
|
29
|
+
name: "Decibel",
|
|
30
|
+
type: "dex",
|
|
31
|
+
chains: ["aptos"],
|
|
32
|
+
features: {
|
|
33
|
+
spot: false,
|
|
34
|
+
perp: true,
|
|
35
|
+
margin: true,
|
|
36
|
+
crossMargin: true,
|
|
37
|
+
isolatedMargin: false,
|
|
38
|
+
stopOrders: true,
|
|
39
|
+
takeProfitOrders: true,
|
|
40
|
+
postOnly: true,
|
|
41
|
+
reduceOnly: true,
|
|
42
|
+
subaccounts: true,
|
|
43
|
+
modifyOrders: false,
|
|
44
|
+
batchOrders: false,
|
|
45
|
+
cancelAllAfter: false,
|
|
46
|
+
publicTrades: true,
|
|
47
|
+
fundingHistory: false,
|
|
48
|
+
orderHistory: true,
|
|
49
|
+
mmp: false,
|
|
50
|
+
twapOrders: false,
|
|
51
|
+
},
|
|
52
|
+
urls: {
|
|
53
|
+
app: "https://app.decibel.trade",
|
|
54
|
+
api: "https://api.mainnet.aptoslabs.com/decibel",
|
|
55
|
+
docs: "https://docs.decibel.trade",
|
|
56
|
+
testnet: "https://api.testnet.aptoslabs.com/decibel",
|
|
57
|
+
},
|
|
58
|
+
implementation: {
|
|
59
|
+
marketData: "full",
|
|
60
|
+
authenticatedReads: "full",
|
|
61
|
+
orderLifecycle: "full",
|
|
62
|
+
orderCancellation: "full",
|
|
63
|
+
subscriptions: "full",
|
|
64
|
+
advancedTrading: "partial",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
config = null;
|
|
68
|
+
connected = false;
|
|
69
|
+
restClient = null;
|
|
70
|
+
wsFeed = null;
|
|
71
|
+
orderManager = null;
|
|
72
|
+
accountAddress = null;
|
|
73
|
+
subaccountAddress = null;
|
|
74
|
+
marketsBySymbol = new Map();
|
|
75
|
+
marketsByAddr = new Map();
|
|
76
|
+
pricesByMarketAddr = new Map();
|
|
77
|
+
booksByMarketAddr = new Map();
|
|
78
|
+
assetContextsByMarketAddr = new Map();
|
|
79
|
+
assetContextsTimestamp = 0;
|
|
80
|
+
trackedMarketAddrs = new Set();
|
|
81
|
+
tickerSubscribers = new Map();
|
|
82
|
+
orderBookSubscribers = new Map();
|
|
83
|
+
globalSubscribers = new Set();
|
|
84
|
+
accountPollers = new Map();
|
|
85
|
+
backfillTimer = null;
|
|
86
|
+
async connect(config) {
|
|
87
|
+
this.config = config;
|
|
88
|
+
const network = this.resolveNetwork(config);
|
|
89
|
+
const defaults = DECIBEL_DEFAULTS[network];
|
|
90
|
+
const credentials = config.credentials;
|
|
91
|
+
const restUrl = credentials?.restUrl ?? defaults.restUrl;
|
|
92
|
+
const wsUrl = config.wsUrl ?? credentials?.wsUrl ?? defaults.wsUrl;
|
|
93
|
+
const fullnodeUrl = config.rpcUrl ?? credentials?.fullnodeUrl ?? defaults.fullnodeUrl;
|
|
94
|
+
const packageAddress = credentials?.packageAddress ?? defaults.packageAddress;
|
|
95
|
+
this.accountAddress = credentials?.accountAddress ?? credentials?.subaccountAddress ?? null;
|
|
96
|
+
this.subaccountAddress = credentials?.subaccountAddress ?? credentials?.accountAddress ?? null;
|
|
97
|
+
this.restClient = new DecibelRestClient(restUrl, credentials?.apiBearerToken);
|
|
98
|
+
await this.loadMarkets();
|
|
99
|
+
// Optional WS: if this fails we continue with REST-only fallback.
|
|
100
|
+
try {
|
|
101
|
+
this.wsFeed = new DecibelWsFeed(wsUrl, credentials?.apiBearerToken, {
|
|
102
|
+
reconnectDelayMs: 3_000,
|
|
103
|
+
pingIntervalMs: 25_000,
|
|
104
|
+
});
|
|
105
|
+
this.wsFeed.on("price", ({ marketAddr, data }) => this.handleWsPrice(marketAddr, data));
|
|
106
|
+
this.wsFeed.on("depth", ({ marketAddr, data }) => this.handleWsDepth(marketAddr, data));
|
|
107
|
+
this.wsFeed.on("trades", ({ marketAddr, data }) => this.handleWsTrades(marketAddr, data));
|
|
108
|
+
this.wsFeed.on("all_market_prices", ({ data }) => this.handleWsAllMarketPrices(data));
|
|
109
|
+
this.wsFeed.on("account_overview", ({ data }) => this.handleWsAccountOverview(data));
|
|
110
|
+
this.wsFeed.on("account_positions", ({ data }) => this.handleWsAccountPositions(data));
|
|
111
|
+
this.wsFeed.on("account_open_orders", ({ data }) => this.handleWsAccountOpenOrders(data));
|
|
112
|
+
this.wsFeed.on("user_trades", ({ data }) => this.handleWsUserTrades(data));
|
|
113
|
+
this.wsFeed.on("order_updates", ({ data }) => this.handleWsOrderUpdate(data));
|
|
114
|
+
this.wsFeed.on("notifications", ({ data }) => this.handleWsNotification(data));
|
|
115
|
+
this.wsFeed.connect();
|
|
116
|
+
// Subscribe to all_market_prices for global ticker updates
|
|
117
|
+
this.wsFeed.subscribe("all_market_prices");
|
|
118
|
+
// Subscribe to account-level channels if we have an account address
|
|
119
|
+
if (this.subaccountAddress) {
|
|
120
|
+
this.wsFeed.subscribe(`account_overview:${this.subaccountAddress}`);
|
|
121
|
+
this.wsFeed.subscribe(`account_positions:${this.subaccountAddress}`);
|
|
122
|
+
this.wsFeed.subscribe(`account_open_orders:${this.subaccountAddress}`);
|
|
123
|
+
this.wsFeed.subscribe(`order_updates:${this.subaccountAddress}`);
|
|
124
|
+
this.wsFeed.subscribe(`user_trades:${this.subaccountAddress}`);
|
|
125
|
+
this.wsFeed.subscribe(`notifications:${this.subaccountAddress}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
this.wsFeed = null;
|
|
130
|
+
log.warn("WebSocket initialization failed, REST fallback only", err);
|
|
131
|
+
}
|
|
132
|
+
// Load initial asset contexts for 24h data
|
|
133
|
+
try {
|
|
134
|
+
await this.loadAssetContexts();
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
log.warn("Failed to load initial asset contexts, 24h data will be unavailable");
|
|
138
|
+
}
|
|
139
|
+
this.startBackfillLoop();
|
|
140
|
+
if (credentials?.privateKey && this.subaccountAddress) {
|
|
141
|
+
this.orderManager = new DecibelOrderManager({
|
|
142
|
+
fullnodeUrl,
|
|
143
|
+
network,
|
|
144
|
+
packageAddress,
|
|
145
|
+
privateKey: credentials.privateKey,
|
|
146
|
+
subaccountAddress: this.subaccountAddress,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
this.connected = true;
|
|
150
|
+
}
|
|
151
|
+
async disconnect() {
|
|
152
|
+
this.stopBackfillLoop();
|
|
153
|
+
this.stopAllAccountPollers();
|
|
154
|
+
this.wsFeed?.close();
|
|
155
|
+
this.wsFeed = null;
|
|
156
|
+
this.orderManager = null;
|
|
157
|
+
this.restClient = null;
|
|
158
|
+
this.connected = false;
|
|
159
|
+
}
|
|
160
|
+
isConnected() {
|
|
161
|
+
return this.connected;
|
|
162
|
+
}
|
|
163
|
+
async getMarkets() {
|
|
164
|
+
this.ensureConnected();
|
|
165
|
+
if (this.marketsBySymbol.size === 0) {
|
|
166
|
+
await this.loadMarkets();
|
|
167
|
+
}
|
|
168
|
+
return Array.from(this.marketsBySymbol.values()).map((m) => ({
|
|
169
|
+
symbol: m.symbol,
|
|
170
|
+
baseAsset: m.baseAsset,
|
|
171
|
+
quoteAsset: m.quoteAsset,
|
|
172
|
+
type: "perp",
|
|
173
|
+
maxLeverage: m.maxLeverage,
|
|
174
|
+
minSize: bigintToDecimalString(m.minSize, m.szDecimals),
|
|
175
|
+
tickSize: bigintToDecimalString(m.tickSize, m.pxDecimals),
|
|
176
|
+
fundingInterval: 1,
|
|
177
|
+
isActive: m.marketMode !== "CloseOnly",
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
async getMarket(symbol) {
|
|
181
|
+
const normalized = this.normalizeSymbol(symbol);
|
|
182
|
+
const market = this.marketsBySymbol.get(normalized);
|
|
183
|
+
if (!market)
|
|
184
|
+
return null;
|
|
185
|
+
return {
|
|
186
|
+
symbol: market.symbol,
|
|
187
|
+
baseAsset: market.baseAsset,
|
|
188
|
+
quoteAsset: market.quoteAsset,
|
|
189
|
+
type: "perp",
|
|
190
|
+
maxLeverage: market.maxLeverage,
|
|
191
|
+
minSize: bigintToDecimalString(market.minSize, market.szDecimals),
|
|
192
|
+
tickSize: bigintToDecimalString(market.tickSize, market.pxDecimals),
|
|
193
|
+
fundingInterval: 1,
|
|
194
|
+
isActive: market.marketMode !== "CloseOnly",
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async getTicker(market) {
|
|
198
|
+
this.ensureConnected();
|
|
199
|
+
const meta = this.requireMarket(market);
|
|
200
|
+
this.trackMarket(meta.marketAddr);
|
|
201
|
+
let price = this.pricesByMarketAddr.get(meta.marketAddr);
|
|
202
|
+
if (!price || this.isPriceStale(price)) {
|
|
203
|
+
const prices = await this.restClient.getPrices(meta.marketAddr);
|
|
204
|
+
price = prices[0];
|
|
205
|
+
if (price) {
|
|
206
|
+
this.pricesByMarketAddr.set(meta.marketAddr, price);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (!price) {
|
|
210
|
+
throw new Error(`No ticker price data available for ${market}`);
|
|
211
|
+
}
|
|
212
|
+
return this.toTicker(meta, price);
|
|
213
|
+
}
|
|
214
|
+
async getTickers() {
|
|
215
|
+
this.ensureConnected();
|
|
216
|
+
// Refresh asset contexts for fresh 24h data
|
|
217
|
+
if (this.isAssetContextsStale()) {
|
|
218
|
+
try {
|
|
219
|
+
await this.loadAssetContexts();
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// Continue with stale/missing context data
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const prices = await this.restClient.getPrices();
|
|
226
|
+
for (const price of prices) {
|
|
227
|
+
this.pricesByMarketAddr.set(price.market, price);
|
|
228
|
+
}
|
|
229
|
+
const tickers = [];
|
|
230
|
+
for (const [marketAddr, price] of this.pricesByMarketAddr.entries()) {
|
|
231
|
+
const meta = this.marketsByAddr.get(marketAddr);
|
|
232
|
+
if (!meta)
|
|
233
|
+
continue;
|
|
234
|
+
tickers.push(this.toTicker(meta, price));
|
|
235
|
+
}
|
|
236
|
+
return tickers;
|
|
237
|
+
}
|
|
238
|
+
async getOrderBook(market, depth = 20) {
|
|
239
|
+
this.ensureConnected();
|
|
240
|
+
const meta = this.requireMarket(market);
|
|
241
|
+
this.trackMarket(meta.marketAddr);
|
|
242
|
+
let cached = this.booksByMarketAddr.get(meta.marketAddr);
|
|
243
|
+
if (!cached || this.isBookStale(cached.timestamp)) {
|
|
244
|
+
const depthData = await this.restClient.getDepth(meta.marketAddr, depth);
|
|
245
|
+
cached = this.normalizeDepth(depthData);
|
|
246
|
+
this.booksByMarketAddr.set(meta.marketAddr, cached);
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
market: meta.symbol,
|
|
250
|
+
bids: cached.bids.slice(0, depth).map((b) => ({
|
|
251
|
+
price: b.price.toString(),
|
|
252
|
+
size: b.size.toString(),
|
|
253
|
+
})),
|
|
254
|
+
asks: cached.asks.slice(0, depth).map((a) => ({
|
|
255
|
+
price: a.price.toString(),
|
|
256
|
+
size: a.size.toString(),
|
|
257
|
+
})),
|
|
258
|
+
timestamp: cached.timestamp,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
async getFundingRate(market) {
|
|
262
|
+
const ticker = await this.getTicker(market);
|
|
263
|
+
return {
|
|
264
|
+
market: ticker.market,
|
|
265
|
+
rate: ticker.fundingRate,
|
|
266
|
+
nextFundingTime: Date.now() + 60 * 60 * 1000,
|
|
267
|
+
timestamp: ticker.timestamp,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
async getFundingRates() {
|
|
271
|
+
const tickers = await this.getTickers();
|
|
272
|
+
return tickers.map((ticker) => ({
|
|
273
|
+
market: ticker.market,
|
|
274
|
+
rate: ticker.fundingRate,
|
|
275
|
+
nextFundingTime: Date.now() + 60 * 60 * 1000,
|
|
276
|
+
timestamp: ticker.timestamp,
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
async getPositions() {
|
|
280
|
+
this.ensureConnected();
|
|
281
|
+
this.ensureAccountReadAuth();
|
|
282
|
+
const rows = await this.restClient.getAccountPositions(this.accountAddress);
|
|
283
|
+
return rows
|
|
284
|
+
.filter((row) => parseNumber(row.size) !== 0)
|
|
285
|
+
.map((row) => this.toPosition(row));
|
|
286
|
+
}
|
|
287
|
+
async getPosition(market) {
|
|
288
|
+
const normalized = this.normalizeSymbol(market);
|
|
289
|
+
const positions = await this.getPositions();
|
|
290
|
+
return positions.find((p) => p.market === normalized) ?? null;
|
|
291
|
+
}
|
|
292
|
+
async getOrders(market) {
|
|
293
|
+
this.ensureConnected();
|
|
294
|
+
this.ensureAccountReadAuth();
|
|
295
|
+
const rows = await this.restClient.getOpenOrders(this.accountAddress);
|
|
296
|
+
const orders = rows
|
|
297
|
+
.map((row) => this.toOrder(row))
|
|
298
|
+
.filter((row) => row !== null);
|
|
299
|
+
if (!market)
|
|
300
|
+
return orders;
|
|
301
|
+
const normalized = this.normalizeSymbol(market);
|
|
302
|
+
return orders.filter((row) => row.market === normalized);
|
|
303
|
+
}
|
|
304
|
+
async getOrder(orderId) {
|
|
305
|
+
this.ensureConnected();
|
|
306
|
+
this.ensureAccountReadAuth();
|
|
307
|
+
// Try all known markets via the direct single-order endpoint first
|
|
308
|
+
for (const meta of this.marketsBySymbol.values()) {
|
|
309
|
+
try {
|
|
310
|
+
const lookup = await this.restClient.getOrderById(this.accountAddress, meta.marketAddr, orderId, undefined);
|
|
311
|
+
if (lookup.order && lookup.status !== "NotFound") {
|
|
312
|
+
return this.toOrder(lookup.order);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
// Continue checking other markets
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Fall back to scanning open orders + history for client_order_id matches
|
|
320
|
+
const openRows = await this.restClient.getOpenOrders(this.accountAddress);
|
|
321
|
+
const openMatch = openRows.find((row) => this.matchesOrderId(row, orderId));
|
|
322
|
+
if (openMatch) {
|
|
323
|
+
return this.toOrder(openMatch);
|
|
324
|
+
}
|
|
325
|
+
const historyRows = await this.restClient.getOrderHistory(this.accountAddress, 500, 0);
|
|
326
|
+
const historyMatch = historyRows.find((row) => this.matchesOrderId(row, orderId));
|
|
327
|
+
if (historyMatch) {
|
|
328
|
+
return this.toOrder(historyMatch);
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
async getBalances() {
|
|
333
|
+
this.ensureConnected();
|
|
334
|
+
this.ensureAccountReadAuth();
|
|
335
|
+
const overview = await this.restClient.getAccountOverview(this.accountAddress);
|
|
336
|
+
const equity = parseNumber(overview.equity ?? overview.perp_equity_balance);
|
|
337
|
+
const totalCollateral = parseNumber(overview.total_collateral ?? overview.total_margin);
|
|
338
|
+
const unrealizedPnl = parseNumber(overview.unrealized_pnl);
|
|
339
|
+
const marginUsed = Math.max(0, totalCollateral - equity);
|
|
340
|
+
return [
|
|
341
|
+
{
|
|
342
|
+
asset: "USD",
|
|
343
|
+
total: equity.toString(),
|
|
344
|
+
available: Math.max(0, equity - marginUsed).toString(),
|
|
345
|
+
locked: marginUsed.toString(),
|
|
346
|
+
unrealizedPnl: unrealizedPnl.toString(),
|
|
347
|
+
marginUsed: marginUsed.toString(),
|
|
348
|
+
},
|
|
349
|
+
];
|
|
350
|
+
}
|
|
351
|
+
async getTrades(market, limit = 100) {
|
|
352
|
+
this.ensureConnected();
|
|
353
|
+
this.ensureAccountReadAuth();
|
|
354
|
+
const marketAddr = market ? this.requireMarket(market).marketAddr : undefined;
|
|
355
|
+
const rows = await this.restClient.getTradeHistory(this.accountAddress, marketAddr, limit, 0);
|
|
356
|
+
return rows
|
|
357
|
+
.map((row) => this.toTrade(row))
|
|
358
|
+
.filter((row) => row !== null);
|
|
359
|
+
}
|
|
360
|
+
async placeOrder(params) {
|
|
361
|
+
this.ensureConnected();
|
|
362
|
+
this.ensureOrderManager();
|
|
363
|
+
const meta = this.requireMarket(params.market);
|
|
364
|
+
const side = params.side === "long" ? "buy" : "sell";
|
|
365
|
+
const sizeUnits = parseNumber(params.size);
|
|
366
|
+
const timeInForce = params.type === "market" ? 2 : params.postOnly ? 1 : 0;
|
|
367
|
+
let price = parseNumber(params.price);
|
|
368
|
+
let stopPrice;
|
|
369
|
+
let tpTriggerPrice;
|
|
370
|
+
let tpLimitPrice;
|
|
371
|
+
let slTriggerPrice;
|
|
372
|
+
let slLimitPrice;
|
|
373
|
+
if (params.type === "market") {
|
|
374
|
+
price = await this.resolveReferenceExecutionPrice(meta, params.side);
|
|
375
|
+
}
|
|
376
|
+
if (params.type === "stop" || params.type === "stop_limit") {
|
|
377
|
+
stopPrice = parseNumber(params.triggerPrice ?? params.price);
|
|
378
|
+
if (!price || price <= 0) {
|
|
379
|
+
price = stopPrice;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (params.type === "take_profit") {
|
|
383
|
+
tpTriggerPrice = parseNumber(params.triggerPrice);
|
|
384
|
+
tpLimitPrice = parseNumber(params.price ?? params.triggerPrice);
|
|
385
|
+
if (!price || price <= 0) {
|
|
386
|
+
price = tpLimitPrice;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (!Number.isFinite(price) || price <= 0) {
|
|
390
|
+
throw new Error("Order price could not be resolved");
|
|
391
|
+
}
|
|
392
|
+
if (params.type === "stop_limit") {
|
|
393
|
+
slTriggerPrice = stopPrice;
|
|
394
|
+
slLimitPrice = price;
|
|
395
|
+
}
|
|
396
|
+
const txHash = await this.orderManager.placeOrder({
|
|
397
|
+
market: meta,
|
|
398
|
+
side,
|
|
399
|
+
price,
|
|
400
|
+
sizeUnits,
|
|
401
|
+
timeInForce,
|
|
402
|
+
reduceOnly: params.reduceOnly ?? false,
|
|
403
|
+
clientOrderId: params.clientOrderId,
|
|
404
|
+
stopPrice,
|
|
405
|
+
tpTriggerPrice,
|
|
406
|
+
tpLimitPrice,
|
|
407
|
+
slTriggerPrice,
|
|
408
|
+
slLimitPrice,
|
|
409
|
+
});
|
|
410
|
+
return {
|
|
411
|
+
id: txHash,
|
|
412
|
+
market: meta.symbol,
|
|
413
|
+
side: params.side,
|
|
414
|
+
type: params.type,
|
|
415
|
+
size: params.size,
|
|
416
|
+
price: price.toString(),
|
|
417
|
+
filled: "0",
|
|
418
|
+
remaining: params.size,
|
|
419
|
+
status: "open",
|
|
420
|
+
reduceOnly: params.reduceOnly ?? false,
|
|
421
|
+
postOnly: params.postOnly ?? false,
|
|
422
|
+
timestamp: Date.now(),
|
|
423
|
+
triggerPrice: params.triggerPrice,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
async cancelOrder(params) {
|
|
427
|
+
this.ensureConnected();
|
|
428
|
+
this.ensureOrderManager();
|
|
429
|
+
if (!params.orderId) {
|
|
430
|
+
throw new Error("Decibel cancelOrder requires orderId");
|
|
431
|
+
}
|
|
432
|
+
let market = params.market;
|
|
433
|
+
if (!market) {
|
|
434
|
+
const existing = await this.getOrder(params.orderId);
|
|
435
|
+
market = existing?.market;
|
|
436
|
+
}
|
|
437
|
+
if (!market) {
|
|
438
|
+
throw new Error("Decibel cancelOrder requires market or resolvable orderId");
|
|
439
|
+
}
|
|
440
|
+
const meta = this.requireMarket(market);
|
|
441
|
+
await this.orderManager.cancelOrder(params.orderId, meta.marketAddr);
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
async cancelAllOrders(market) {
|
|
445
|
+
this.ensureConnected();
|
|
446
|
+
this.ensureOrderManager();
|
|
447
|
+
if (market) {
|
|
448
|
+
const meta = this.requireMarket(market);
|
|
449
|
+
await this.orderManager.cancelAllOrders(meta.marketAddr);
|
|
450
|
+
return 1;
|
|
451
|
+
}
|
|
452
|
+
let cancelled = 0;
|
|
453
|
+
for (const meta of this.marketsBySymbol.values()) {
|
|
454
|
+
try {
|
|
455
|
+
await this.orderManager.cancelAllOrders(meta.marketAddr);
|
|
456
|
+
cancelled++;
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
// Continue best-effort cancel on other markets.
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return cancelled;
|
|
463
|
+
}
|
|
464
|
+
async setLeverage(market, leverage) {
|
|
465
|
+
this.ensureConnected();
|
|
466
|
+
this.ensureOrderManager();
|
|
467
|
+
if (!Number.isFinite(leverage) || leverage <= 0) {
|
|
468
|
+
throw new Error("Leverage must be a positive number");
|
|
469
|
+
}
|
|
470
|
+
const meta = this.requireMarket(market);
|
|
471
|
+
const current = await this.getCurrentMarginSettings(meta.symbol);
|
|
472
|
+
const leverageBps = Math.max(1, Math.round(leverage * 100));
|
|
473
|
+
await this.orderManager.configureUserSettingsForMarket(meta.marketAddr, current.isCross, leverageBps);
|
|
474
|
+
}
|
|
475
|
+
async setMarginType(market, type) {
|
|
476
|
+
this.ensureConnected();
|
|
477
|
+
this.ensureOrderManager();
|
|
478
|
+
const meta = this.requireMarket(market);
|
|
479
|
+
const current = await this.getCurrentMarginSettings(meta.symbol);
|
|
480
|
+
const isCross = type === "cross";
|
|
481
|
+
await this.orderManager.configureUserSettingsForMarket(meta.marketAddr, isCross, current.userLeverageBps);
|
|
482
|
+
}
|
|
483
|
+
// --------------------------------------------------------------------------
|
|
484
|
+
// Advanced Trading
|
|
485
|
+
// --------------------------------------------------------------------------
|
|
486
|
+
async modifyOrder(_params) {
|
|
487
|
+
throw new Error("Decibel does not support order modification. Cancel and replace instead.");
|
|
488
|
+
}
|
|
489
|
+
async batchPlaceOrders(paramsList) {
|
|
490
|
+
const results = [];
|
|
491
|
+
for (const params of paramsList) {
|
|
492
|
+
results.push(await this.placeOrder(params));
|
|
493
|
+
}
|
|
494
|
+
return results;
|
|
495
|
+
}
|
|
496
|
+
async batchCancelOrders(paramsList) {
|
|
497
|
+
const results = [];
|
|
498
|
+
for (const params of paramsList) {
|
|
499
|
+
try {
|
|
500
|
+
results.push(await this.cancelOrder(params));
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
results.push(false);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return results;
|
|
507
|
+
}
|
|
508
|
+
async cancelAllAfter(_timeoutMs) {
|
|
509
|
+
throw new Error("Decibel does not support cancelAllAfter (dead man's switch)");
|
|
510
|
+
}
|
|
511
|
+
async getOrderHistory(market, limit = 100) {
|
|
512
|
+
this.ensureConnected();
|
|
513
|
+
this.ensureAccountReadAuth();
|
|
514
|
+
const rows = await this.restClient.getOrderHistory(this.accountAddress, limit, 0);
|
|
515
|
+
let orders = rows
|
|
516
|
+
.map((row) => this.toOrder(row))
|
|
517
|
+
.filter((row) => row !== null);
|
|
518
|
+
if (market) {
|
|
519
|
+
const normalized = this.normalizeSymbol(market);
|
|
520
|
+
orders = orders.filter((o) => o.market === normalized);
|
|
521
|
+
}
|
|
522
|
+
return orders;
|
|
523
|
+
}
|
|
524
|
+
async getFundingHistory(_market, _limit) {
|
|
525
|
+
// Decibel does not expose a funding payment history endpoint
|
|
526
|
+
return [];
|
|
527
|
+
}
|
|
528
|
+
async getPublicTrades(market, limit = 100) {
|
|
529
|
+
this.ensureConnected();
|
|
530
|
+
const meta = this.requireMarket(market);
|
|
531
|
+
const rows = await this.restClient.getPublicTrades(meta.marketAddr, limit);
|
|
532
|
+
return rows.map((row) => {
|
|
533
|
+
const price = parseNumber(row.price);
|
|
534
|
+
const size = parseNumber(row.size);
|
|
535
|
+
const sideValue = row.action === "sell" ? "short" : "long";
|
|
536
|
+
return {
|
|
537
|
+
id: row.trade_id ?? `${meta.symbol}-${Date.now()}`,
|
|
538
|
+
market: meta.symbol,
|
|
539
|
+
side: sideValue,
|
|
540
|
+
price: price.toString(),
|
|
541
|
+
size: size.toString(),
|
|
542
|
+
timestamp: parseNumber(row.transaction_unix_ms) || Date.now(),
|
|
543
|
+
};
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
// --------------------------------------------------------------------------
|
|
547
|
+
// Market Maker Protection
|
|
548
|
+
// --------------------------------------------------------------------------
|
|
549
|
+
async setMMP(_config) {
|
|
550
|
+
throw new Error("Decibel does not support Market Maker Protection (MMP)");
|
|
551
|
+
}
|
|
552
|
+
async getMMP(_market) {
|
|
553
|
+
throw new Error("Decibel does not support Market Maker Protection (MMP)");
|
|
554
|
+
}
|
|
555
|
+
async resetMMP(_market) {
|
|
556
|
+
throw new Error("Decibel does not support Market Maker Protection (MMP)");
|
|
557
|
+
}
|
|
558
|
+
// --------------------------------------------------------------------------
|
|
559
|
+
// TWAP Orders
|
|
560
|
+
// --------------------------------------------------------------------------
|
|
561
|
+
async placeTWAP(_params) {
|
|
562
|
+
throw new Error("Decibel does not support TWAP orders");
|
|
563
|
+
}
|
|
564
|
+
async cancelTWAP(_twapId) {
|
|
565
|
+
throw new Error("Decibel does not support TWAP orders");
|
|
566
|
+
}
|
|
567
|
+
async getTWAPStatus(_twapId) {
|
|
568
|
+
throw new Error("Decibel does not support TWAP orders");
|
|
569
|
+
}
|
|
570
|
+
// --------------------------------------------------------------------------
|
|
571
|
+
// Margin Management
|
|
572
|
+
// --------------------------------------------------------------------------
|
|
573
|
+
async updateIsolatedMargin(_market, _amount) {
|
|
574
|
+
throw new Error("Decibel does not support isolated margin adjustment");
|
|
575
|
+
}
|
|
576
|
+
subscribe(callbacks) {
|
|
577
|
+
this.globalSubscribers.add(callbacks);
|
|
578
|
+
this.startAccountPollingIfNeeded(callbacks);
|
|
579
|
+
return () => {
|
|
580
|
+
this.globalSubscribers.delete(callbacks);
|
|
581
|
+
this.stopAccountPolling(callbacks);
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
subscribeOrderBook(market, callback) {
|
|
585
|
+
const meta = this.requireMarket(market);
|
|
586
|
+
this.trackMarket(meta.marketAddr);
|
|
587
|
+
this.wsFeed?.subscribe(`depth:${meta.marketAddr}`);
|
|
588
|
+
if (!this.orderBookSubscribers.has(meta.symbol)) {
|
|
589
|
+
this.orderBookSubscribers.set(meta.symbol, new Set());
|
|
590
|
+
}
|
|
591
|
+
const subscribers = this.orderBookSubscribers.get(meta.symbol);
|
|
592
|
+
subscribers.add(callback);
|
|
593
|
+
const cached = this.booksByMarketAddr.get(meta.marketAddr);
|
|
594
|
+
if (cached) {
|
|
595
|
+
callback(this.toOrderBook(meta, cached));
|
|
596
|
+
}
|
|
597
|
+
return () => {
|
|
598
|
+
subscribers.delete(callback);
|
|
599
|
+
if (subscribers.size === 0) {
|
|
600
|
+
this.orderBookSubscribers.delete(meta.symbol);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
subscribeTicker(market, callback) {
|
|
605
|
+
const meta = this.requireMarket(market);
|
|
606
|
+
this.trackMarket(meta.marketAddr);
|
|
607
|
+
this.wsFeed?.subscribe(`market_price:${meta.marketAddr}`);
|
|
608
|
+
if (!this.tickerSubscribers.has(meta.symbol)) {
|
|
609
|
+
this.tickerSubscribers.set(meta.symbol, new Set());
|
|
610
|
+
}
|
|
611
|
+
const subscribers = this.tickerSubscribers.get(meta.symbol);
|
|
612
|
+
subscribers.add(callback);
|
|
613
|
+
const cached = this.pricesByMarketAddr.get(meta.marketAddr);
|
|
614
|
+
if (cached) {
|
|
615
|
+
callback(this.toTicker(meta, cached));
|
|
616
|
+
}
|
|
617
|
+
return () => {
|
|
618
|
+
subscribers.delete(callback);
|
|
619
|
+
if (subscribers.size === 0) {
|
|
620
|
+
this.tickerSubscribers.delete(meta.symbol);
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
// --------------------------------------------------------------------------
|
|
625
|
+
// Internal helpers
|
|
626
|
+
// --------------------------------------------------------------------------
|
|
627
|
+
async loadAssetContexts() {
|
|
628
|
+
if (!this.restClient)
|
|
629
|
+
return;
|
|
630
|
+
const contexts = await this.restClient.getAssetContexts();
|
|
631
|
+
for (const ctx of contexts) {
|
|
632
|
+
this.assetContextsByMarketAddr.set(ctx.market, ctx);
|
|
633
|
+
}
|
|
634
|
+
this.assetContextsTimestamp = Date.now();
|
|
635
|
+
}
|
|
636
|
+
isAssetContextsStale() {
|
|
637
|
+
return Date.now() - this.assetContextsTimestamp > 60_000;
|
|
638
|
+
}
|
|
639
|
+
handleWsAllMarketPrices(data) {
|
|
640
|
+
if (!isRecord(data))
|
|
641
|
+
return;
|
|
642
|
+
// all_market_prices can carry an array of price objects or a data wrapper
|
|
643
|
+
const items = Array.isArray(data.data)
|
|
644
|
+
? data.data
|
|
645
|
+
: Array.isArray(data.prices)
|
|
646
|
+
? data.prices
|
|
647
|
+
: [];
|
|
648
|
+
for (const item of items) {
|
|
649
|
+
if (!isRecord(item))
|
|
650
|
+
continue;
|
|
651
|
+
const marketAddr = typeof item.market === "string" ? item.market : undefined;
|
|
652
|
+
if (!marketAddr)
|
|
653
|
+
continue;
|
|
654
|
+
this.handleWsPrice(marketAddr, item);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
handleWsAccountOverview(data) {
|
|
658
|
+
if (!isRecord(data))
|
|
659
|
+
return;
|
|
660
|
+
for (const callbacks of this.globalSubscribers) {
|
|
661
|
+
if (!callbacks.onBalances)
|
|
662
|
+
continue;
|
|
663
|
+
try {
|
|
664
|
+
const equity = parseNumber(toNumericInput(data.equity) ?? toNumericInput(data.perp_equity_balance));
|
|
665
|
+
const totalCollateral = parseNumber(toNumericInput(data.total_collateral) ?? toNumericInput(data.total_margin));
|
|
666
|
+
const unrealizedPnl = parseNumber(toNumericInput(data.unrealized_pnl));
|
|
667
|
+
const marginUsed = Math.max(0, totalCollateral - equity);
|
|
668
|
+
callbacks.onBalances([{
|
|
669
|
+
asset: "USD",
|
|
670
|
+
total: equity.toString(),
|
|
671
|
+
available: Math.max(0, equity - marginUsed).toString(),
|
|
672
|
+
locked: marginUsed.toString(),
|
|
673
|
+
unrealizedPnl: unrealizedPnl.toString(),
|
|
674
|
+
marginUsed: marginUsed.toString(),
|
|
675
|
+
}]);
|
|
676
|
+
}
|
|
677
|
+
catch (err) {
|
|
678
|
+
callbacks.onError?.(toError(err));
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
handleWsAccountPositions(data) {
|
|
683
|
+
if (!isRecord(data))
|
|
684
|
+
return;
|
|
685
|
+
const rows = Array.isArray(data.positions)
|
|
686
|
+
? data.positions
|
|
687
|
+
: Array.isArray(data.data)
|
|
688
|
+
? data.data
|
|
689
|
+
: Array.isArray(data)
|
|
690
|
+
? data
|
|
691
|
+
: [];
|
|
692
|
+
const positions = [];
|
|
693
|
+
for (const row of rows) {
|
|
694
|
+
if (!isRecord(row))
|
|
695
|
+
continue;
|
|
696
|
+
const size = parseNumber(toNumericInput(row.size));
|
|
697
|
+
if (size === 0)
|
|
698
|
+
continue;
|
|
699
|
+
positions.push(this.toPosition(row));
|
|
700
|
+
}
|
|
701
|
+
for (const callbacks of this.globalSubscribers) {
|
|
702
|
+
callbacks.onPositions?.(positions);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
handleWsAccountOpenOrders(data) {
|
|
706
|
+
if (!isRecord(data))
|
|
707
|
+
return;
|
|
708
|
+
const rows = Array.isArray(data.orders)
|
|
709
|
+
? data.orders
|
|
710
|
+
: Array.isArray(data.data)
|
|
711
|
+
? data.data
|
|
712
|
+
: Array.isArray(data)
|
|
713
|
+
? data
|
|
714
|
+
: [];
|
|
715
|
+
const orders = [];
|
|
716
|
+
for (const row of rows) {
|
|
717
|
+
if (!isRecord(row))
|
|
718
|
+
continue;
|
|
719
|
+
const order = this.toOrder(row);
|
|
720
|
+
if (order)
|
|
721
|
+
orders.push(order);
|
|
722
|
+
}
|
|
723
|
+
for (const callbacks of this.globalSubscribers) {
|
|
724
|
+
callbacks.onOrders?.(orders);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
handleWsUserTrades(data) {
|
|
728
|
+
if (!isRecord(data))
|
|
729
|
+
return;
|
|
730
|
+
const rows = Array.isArray(data.trades)
|
|
731
|
+
? data.trades
|
|
732
|
+
: Array.isArray(data.data)
|
|
733
|
+
? data.data
|
|
734
|
+
: Array.isArray(data)
|
|
735
|
+
? data
|
|
736
|
+
: [];
|
|
737
|
+
const trades = [];
|
|
738
|
+
for (const row of rows) {
|
|
739
|
+
if (!isRecord(row))
|
|
740
|
+
continue;
|
|
741
|
+
const trade = this.toTrade(row);
|
|
742
|
+
if (trade)
|
|
743
|
+
trades.push(trade);
|
|
744
|
+
}
|
|
745
|
+
if (trades.length > 0) {
|
|
746
|
+
for (const callbacks of this.globalSubscribers) {
|
|
747
|
+
callbacks.onTrades?.(trades);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
handleWsOrderUpdate(data) {
|
|
752
|
+
if (!isRecord(data))
|
|
753
|
+
return;
|
|
754
|
+
// order_updates carries a single order status change
|
|
755
|
+
const orderData = isRecord(data.order) ? data.order : data;
|
|
756
|
+
const order = this.toOrder(orderData);
|
|
757
|
+
if (!order)
|
|
758
|
+
return;
|
|
759
|
+
for (const callbacks of this.globalSubscribers) {
|
|
760
|
+
callbacks.onOrders?.([order]);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
handleWsNotification(data) {
|
|
764
|
+
if (!isRecord(data))
|
|
765
|
+
return;
|
|
766
|
+
// Notifications include liquidation alerts - log them for observability
|
|
767
|
+
log.warn("Decibel notification received", data);
|
|
768
|
+
}
|
|
769
|
+
resolveNetwork(config) {
|
|
770
|
+
const fromCredentials = config.credentials?.network;
|
|
771
|
+
if (fromCredentials === "testnet" || fromCredentials === "mainnet") {
|
|
772
|
+
return fromCredentials;
|
|
773
|
+
}
|
|
774
|
+
return config.testnet ? "testnet" : "mainnet";
|
|
775
|
+
}
|
|
776
|
+
async loadMarkets() {
|
|
777
|
+
const rows = await this.restClient.getMarkets();
|
|
778
|
+
this.marketsBySymbol.clear();
|
|
779
|
+
this.marketsByAddr.clear();
|
|
780
|
+
for (const row of rows) {
|
|
781
|
+
const meta = this.mapMarket(row);
|
|
782
|
+
this.marketsBySymbol.set(meta.symbol, meta);
|
|
783
|
+
this.marketsByAddr.set(meta.marketAddr, meta);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
mapMarket(row) {
|
|
787
|
+
const marketName = row.market_name;
|
|
788
|
+
const baseAsset = marketName.split("/")[0]?.toUpperCase() ?? marketName.toUpperCase();
|
|
789
|
+
const quoteAsset = marketName.split("/")[1]?.toUpperCase() ?? "USD";
|
|
790
|
+
return {
|
|
791
|
+
marketName,
|
|
792
|
+
marketAddr: row.market_addr,
|
|
793
|
+
symbol: `${baseAsset}-PERP`,
|
|
794
|
+
baseAsset,
|
|
795
|
+
quoteAsset,
|
|
796
|
+
maxLeverage: parseNumber(row.max_leverage) || 20,
|
|
797
|
+
pxDecimals: parseInt(String(row.px_decimals), 10),
|
|
798
|
+
szDecimals: parseInt(String(row.sz_decimals), 10),
|
|
799
|
+
tickSize: BigInt(String(row.tick_size)),
|
|
800
|
+
lotSize: BigInt(String(row.lot_size)),
|
|
801
|
+
minSize: BigInt(String(row.min_size)),
|
|
802
|
+
marketMode: row.market_mode,
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
ensureConnected() {
|
|
806
|
+
if (!this.connected || !this.restClient) {
|
|
807
|
+
throw new Error("Not connected. Call connect() first.");
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
ensureAccountReadAuth() {
|
|
811
|
+
if (!this.accountAddress) {
|
|
812
|
+
throw new Error("Decibel account reads require account address (set DECIBEL_API_WALLET_ADDRESS or subaccount).");
|
|
813
|
+
}
|
|
814
|
+
if (!this.config?.credentials?.apiBearerToken) {
|
|
815
|
+
throw new Error("Decibel account reads require DECIBEL_API_BEARER_TOKEN.");
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
ensureOrderManager() {
|
|
819
|
+
if (!this.orderManager) {
|
|
820
|
+
throw new Error("Decibel trading requires private key + package settings + subaccount (DECIBEL_API_WALLET_PRIVATE_KEY, DECIBEL_API_WALLET_ADDRESS, DECIBEL_API_BEARER_TOKEN).");
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
requireMarket(symbol) {
|
|
824
|
+
const raw = symbol.trim();
|
|
825
|
+
const byAddress = this.marketsByAddr.get(raw);
|
|
826
|
+
if (byAddress) {
|
|
827
|
+
return byAddress;
|
|
828
|
+
}
|
|
829
|
+
const normalized = this.normalizeSymbol(symbol);
|
|
830
|
+
const market = this.marketsBySymbol.get(normalized);
|
|
831
|
+
if (!market) {
|
|
832
|
+
throw new Error(`Market not found on Decibel: ${symbol}`);
|
|
833
|
+
}
|
|
834
|
+
return market;
|
|
835
|
+
}
|
|
836
|
+
normalizeSymbol(symbol) {
|
|
837
|
+
const upper = symbol.toUpperCase().trim();
|
|
838
|
+
if (upper.endsWith("-PERP"))
|
|
839
|
+
return upper;
|
|
840
|
+
if (upper.includes("/")) {
|
|
841
|
+
return `${upper.split("/")[0]}-PERP`;
|
|
842
|
+
}
|
|
843
|
+
return `${upper}-PERP`;
|
|
844
|
+
}
|
|
845
|
+
toTicker(meta, price) {
|
|
846
|
+
const markPrice = parseNumber(price.mark_px);
|
|
847
|
+
const midPrice = parseNumber(price.mid_px) || markPrice;
|
|
848
|
+
const oraclePrice = parseNumber(price.oracle_px) || markPrice;
|
|
849
|
+
const rawFundingBps = parseNumber(price.funding_rate_bps);
|
|
850
|
+
const isFundingPositive = price.is_funding_positive;
|
|
851
|
+
const fundingRate = isFundingPositive === false
|
|
852
|
+
? -Math.abs(rawFundingBps / 10_000)
|
|
853
|
+
: rawFundingBps / 10_000;
|
|
854
|
+
const openInterest = parseNumber(price.open_interest);
|
|
855
|
+
const book = this.booksByMarketAddr.get(meta.marketAddr);
|
|
856
|
+
const bid = book?.bids[0]?.price ?? midPrice;
|
|
857
|
+
const ask = book?.asks[0]?.price ?? midPrice;
|
|
858
|
+
const timestamp = parseNumber(price.transaction_unix_ms) || Date.now();
|
|
859
|
+
// Pull 24h data from asset contexts cache
|
|
860
|
+
const ctx = this.assetContextsByMarketAddr.get(meta.marketAddr);
|
|
861
|
+
const volume24h = ctx ? parseNumber(ctx.volume_24h) : 0;
|
|
862
|
+
const change24h = ctx ? parseNumber(ctx.price_change_pct_24h) : 0;
|
|
863
|
+
return {
|
|
864
|
+
market: meta.symbol,
|
|
865
|
+
lastPrice: midPrice.toString(),
|
|
866
|
+
markPrice: markPrice.toString(),
|
|
867
|
+
indexPrice: oraclePrice.toString(),
|
|
868
|
+
bid: bid.toString(),
|
|
869
|
+
ask: ask.toString(),
|
|
870
|
+
volume24h: volume24h.toString(),
|
|
871
|
+
change24h: change24h.toString(),
|
|
872
|
+
high24h: "0",
|
|
873
|
+
low24h: "0",
|
|
874
|
+
openInterest: openInterest.toString(),
|
|
875
|
+
fundingRate: fundingRate.toString(),
|
|
876
|
+
timestamp,
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
toPosition(row) {
|
|
880
|
+
const rawSize = parseNumber(row.size);
|
|
881
|
+
const marketName = row.market_name ?? row.market ?? "UNKNOWN";
|
|
882
|
+
const side = rawSize >= 0 ? "long" : "short";
|
|
883
|
+
return {
|
|
884
|
+
market: this.normalizeSymbol(marketName),
|
|
885
|
+
side,
|
|
886
|
+
size: Math.abs(rawSize).toString(),
|
|
887
|
+
entryPrice: parseNumber(row.entry_price).toString(),
|
|
888
|
+
markPrice: parseNumber(row.mark_price ?? row.oracle_price).toString(),
|
|
889
|
+
liquidationPrice: parseNumber(row.liquidation_price ?? row.estimated_liquidation_price).toString(),
|
|
890
|
+
unrealizedPnl: parseNumber(row.unrealized_pnl ?? row.unrealized_funding).toString(),
|
|
891
|
+
realizedPnl: "0",
|
|
892
|
+
leverage: parseNumber(row.leverage ?? row.user_leverage) || 1,
|
|
893
|
+
marginType: "cross",
|
|
894
|
+
margin: "0",
|
|
895
|
+
timestamp: Date.now(),
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
toOrder(row) {
|
|
899
|
+
const marketRaw = row.market_name ?? row.market ?? "UNKNOWN";
|
|
900
|
+
const market = this.resolveMarketSymbol(marketRaw);
|
|
901
|
+
if (!market)
|
|
902
|
+
return null;
|
|
903
|
+
const totalSize = parseNumber(row.orig_size ?? row.filled_size ?? row.remaining_size);
|
|
904
|
+
const remainingSize = parseNumber(row.remaining_size);
|
|
905
|
+
const filledSize = Math.max(0, totalSize - remainingSize);
|
|
906
|
+
const priceValue = parseNumber(row.price);
|
|
907
|
+
const triggerPriceValue = parseNumber(row.trigger_price);
|
|
908
|
+
return {
|
|
909
|
+
id: row.order_id ?? row.client_order_id ?? `order-${Date.now()}`,
|
|
910
|
+
market,
|
|
911
|
+
side: row.is_buy === false || row.side === "sell" ? "short" : "long",
|
|
912
|
+
type: this.mapOrderType(row.order_type),
|
|
913
|
+
size: totalSize.toString(),
|
|
914
|
+
price: priceValue > 0 ? priceValue.toString() : null,
|
|
915
|
+
filled: filledSize.toString(),
|
|
916
|
+
remaining: Math.max(0, remainingSize).toString(),
|
|
917
|
+
status: this.mapOrderStatus(row.status),
|
|
918
|
+
reduceOnly: toBoolean(row.is_reduce_only ?? row.reduce_only),
|
|
919
|
+
postOnly: toBoolean(row.is_post_only ?? row.post_only),
|
|
920
|
+
timestamp: parseNumber(row.unix_ms) || Date.now(),
|
|
921
|
+
triggerPrice: triggerPriceValue > 0 ? triggerPriceValue.toString() : undefined,
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
toTrade(row) {
|
|
925
|
+
const marketRaw = row.market_name ?? row.market ?? "UNKNOWN";
|
|
926
|
+
const market = this.resolveMarketSymbol(marketRaw);
|
|
927
|
+
if (!market)
|
|
928
|
+
return null;
|
|
929
|
+
const orderId = row.order_id;
|
|
930
|
+
const price = parseNumber(row.price);
|
|
931
|
+
const size = parseNumber(row.size);
|
|
932
|
+
if (size <= 0)
|
|
933
|
+
return null;
|
|
934
|
+
return {
|
|
935
|
+
id: row.trade_id ?? row.fill_id ?? `${orderId ?? market}-${Date.now()}`,
|
|
936
|
+
market,
|
|
937
|
+
side: row.is_buy === false || row.side === "sell" ? "short" : "long",
|
|
938
|
+
price: price.toString(),
|
|
939
|
+
size: size.toString(),
|
|
940
|
+
fee: parseNumber(row.fee).toString(),
|
|
941
|
+
feeAsset: row.fee_asset ?? "USD",
|
|
942
|
+
timestamp: parseNumber(row.unix_ms) || Date.now(),
|
|
943
|
+
...(orderId ? { orderId } : {}),
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
resolveMarketSymbol(input) {
|
|
947
|
+
const raw = input.trim();
|
|
948
|
+
if (!raw)
|
|
949
|
+
return null;
|
|
950
|
+
const byAddress = this.marketsByAddr.get(raw);
|
|
951
|
+
if (byAddress)
|
|
952
|
+
return byAddress.symbol;
|
|
953
|
+
return this.normalizeSymbol(raw);
|
|
954
|
+
}
|
|
955
|
+
mapOrderStatus(status) {
|
|
956
|
+
const normalized = status?.toLowerCase() ?? "open";
|
|
957
|
+
if (normalized.includes("fill"))
|
|
958
|
+
return "filled";
|
|
959
|
+
if (normalized.includes("cancel"))
|
|
960
|
+
return "cancelled";
|
|
961
|
+
if (normalized.includes("expire"))
|
|
962
|
+
return "expired";
|
|
963
|
+
if (normalized.includes("partial"))
|
|
964
|
+
return "partial";
|
|
965
|
+
return "open";
|
|
966
|
+
}
|
|
967
|
+
mapOrderType(type) {
|
|
968
|
+
const normalized = type?.toLowerCase() ?? "limit";
|
|
969
|
+
if (normalized.includes("market"))
|
|
970
|
+
return "market";
|
|
971
|
+
if (normalized.includes("stop_limit"))
|
|
972
|
+
return "stop_limit";
|
|
973
|
+
if (normalized.includes("stop"))
|
|
974
|
+
return "stop";
|
|
975
|
+
if (normalized.includes("take"))
|
|
976
|
+
return "take_profit";
|
|
977
|
+
return "limit";
|
|
978
|
+
}
|
|
979
|
+
matchesOrderId(row, orderId) {
|
|
980
|
+
return row.order_id === orderId || row.client_order_id === orderId;
|
|
981
|
+
}
|
|
982
|
+
async getCurrentMarginSettings(market) {
|
|
983
|
+
const defaults = { isCross: true, userLeverageBps: 100 };
|
|
984
|
+
if (!this.accountAddress || !this.config?.credentials?.apiBearerToken) {
|
|
985
|
+
return defaults;
|
|
986
|
+
}
|
|
987
|
+
try {
|
|
988
|
+
const position = await this.getPosition(market);
|
|
989
|
+
if (!position)
|
|
990
|
+
return defaults;
|
|
991
|
+
return {
|
|
992
|
+
isCross: position.marginType === "cross",
|
|
993
|
+
userLeverageBps: Math.max(1, Math.round(position.leverage * 100)),
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
catch {
|
|
997
|
+
return defaults;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
normalizeDepth(depth) {
|
|
1001
|
+
const bids = (depth.bids ?? []).map((level) => ({
|
|
1002
|
+
price: parseNumber(level.price),
|
|
1003
|
+
size: parseNumber(level.size),
|
|
1004
|
+
}));
|
|
1005
|
+
const asks = (depth.asks ?? []).map((level) => ({
|
|
1006
|
+
price: parseNumber(level.price),
|
|
1007
|
+
size: parseNumber(level.size),
|
|
1008
|
+
}));
|
|
1009
|
+
bids.sort((a, b) => b.price - a.price);
|
|
1010
|
+
asks.sort((a, b) => a.price - b.price);
|
|
1011
|
+
return {
|
|
1012
|
+
bids,
|
|
1013
|
+
asks,
|
|
1014
|
+
timestamp: parseNumber(depth.unix_ms) || Date.now(),
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
toOrderBook(meta, cached) {
|
|
1018
|
+
return {
|
|
1019
|
+
market: meta.symbol,
|
|
1020
|
+
bids: cached.bids.map((b) => ({ price: b.price.toString(), size: b.size.toString() })),
|
|
1021
|
+
asks: cached.asks.map((a) => ({ price: a.price.toString(), size: a.size.toString() })),
|
|
1022
|
+
timestamp: cached.timestamp,
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
trackMarket(marketAddr) {
|
|
1026
|
+
if (this.trackedMarketAddrs.has(marketAddr))
|
|
1027
|
+
return;
|
|
1028
|
+
this.trackedMarketAddrs.add(marketAddr);
|
|
1029
|
+
this.wsFeed?.subscribe(`market_price:${marketAddr}`);
|
|
1030
|
+
this.wsFeed?.subscribe(`depth:${marketAddr}`);
|
|
1031
|
+
this.wsFeed?.subscribe(`trades:${marketAddr}`);
|
|
1032
|
+
}
|
|
1033
|
+
handleWsPrice(marketAddr, data) {
|
|
1034
|
+
const payload = coerceWsPricePayload(marketAddr, data);
|
|
1035
|
+
this.pricesByMarketAddr.set(marketAddr, payload);
|
|
1036
|
+
const meta = this.marketsByAddr.get(marketAddr);
|
|
1037
|
+
if (!meta)
|
|
1038
|
+
return;
|
|
1039
|
+
const ticker = this.toTicker(meta, payload);
|
|
1040
|
+
const subs = this.tickerSubscribers.get(meta.symbol);
|
|
1041
|
+
if (subs) {
|
|
1042
|
+
for (const cb of subs)
|
|
1043
|
+
cb(ticker);
|
|
1044
|
+
}
|
|
1045
|
+
for (const callbacks of this.globalSubscribers) {
|
|
1046
|
+
callbacks.onTicker?.(ticker);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
handleWsDepth(marketAddr, data) {
|
|
1050
|
+
const normalized = this.normalizeDepth({
|
|
1051
|
+
market: marketAddr,
|
|
1052
|
+
bids: coerceWsDepthLevels(data, "bids"),
|
|
1053
|
+
asks: coerceWsDepthLevels(data, "asks"),
|
|
1054
|
+
unix_ms: Date.now(),
|
|
1055
|
+
});
|
|
1056
|
+
this.booksByMarketAddr.set(marketAddr, normalized);
|
|
1057
|
+
const meta = this.marketsByAddr.get(marketAddr);
|
|
1058
|
+
if (!meta)
|
|
1059
|
+
return;
|
|
1060
|
+
const book = this.toOrderBook(meta, normalized);
|
|
1061
|
+
const subs = this.orderBookSubscribers.get(meta.symbol);
|
|
1062
|
+
if (subs) {
|
|
1063
|
+
for (const cb of subs)
|
|
1064
|
+
cb(book);
|
|
1065
|
+
}
|
|
1066
|
+
for (const callbacks of this.globalSubscribers) {
|
|
1067
|
+
callbacks.onOrderBook?.(book);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
handleWsTrades(marketAddr, data) {
|
|
1071
|
+
const meta = this.marketsByAddr.get(marketAddr);
|
|
1072
|
+
if (!meta)
|
|
1073
|
+
return;
|
|
1074
|
+
const trades = coerceWsTrades(data, meta.symbol);
|
|
1075
|
+
if (trades.length === 0)
|
|
1076
|
+
return;
|
|
1077
|
+
for (const callbacks of this.globalSubscribers) {
|
|
1078
|
+
callbacks.onTrades?.(trades);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
startBackfillLoop() {
|
|
1082
|
+
this.stopBackfillLoop();
|
|
1083
|
+
this.backfillTimer = setInterval(() => {
|
|
1084
|
+
this.backfillTrackedMarkets().catch((err) => {
|
|
1085
|
+
log.warn("Decibel REST backfill failed", err);
|
|
1086
|
+
});
|
|
1087
|
+
}, 60_000);
|
|
1088
|
+
}
|
|
1089
|
+
startAccountPollingIfNeeded(callbacks) {
|
|
1090
|
+
const needsAccountData = !!callbacks.onPositions ||
|
|
1091
|
+
!!callbacks.onOrders ||
|
|
1092
|
+
!!callbacks.onBalances ||
|
|
1093
|
+
!!callbacks.onTrades;
|
|
1094
|
+
if (!needsAccountData)
|
|
1095
|
+
return;
|
|
1096
|
+
if (!this.config?.credentials?.apiBearerToken || !this.accountAddress)
|
|
1097
|
+
return;
|
|
1098
|
+
// If WebSocket account channels are subscribed, we rely on push updates
|
|
1099
|
+
// but still do an initial REST poll to seed the state
|
|
1100
|
+
this.pollAccountCallbacks(callbacks).catch((err) => callbacks.onError?.(toError(err)));
|
|
1101
|
+
// Use a slower polling cadence as a safety net (WS push is primary)
|
|
1102
|
+
const pollInterval = this.wsFeed && this.subaccountAddress ? 30_000 : 5_000;
|
|
1103
|
+
const timer = setInterval(() => {
|
|
1104
|
+
this.pollAccountCallbacks(callbacks).catch((err) => callbacks.onError?.(toError(err)));
|
|
1105
|
+
}, pollInterval);
|
|
1106
|
+
this.accountPollers.set(callbacks, timer);
|
|
1107
|
+
}
|
|
1108
|
+
stopAccountPolling(callbacks) {
|
|
1109
|
+
const timer = this.accountPollers.get(callbacks);
|
|
1110
|
+
if (!timer)
|
|
1111
|
+
return;
|
|
1112
|
+
clearInterval(timer);
|
|
1113
|
+
this.accountPollers.delete(callbacks);
|
|
1114
|
+
}
|
|
1115
|
+
stopAllAccountPollers() {
|
|
1116
|
+
for (const timer of this.accountPollers.values()) {
|
|
1117
|
+
clearInterval(timer);
|
|
1118
|
+
}
|
|
1119
|
+
this.accountPollers.clear();
|
|
1120
|
+
}
|
|
1121
|
+
async pollAccountCallbacks(callbacks) {
|
|
1122
|
+
if (callbacks.onPositions) {
|
|
1123
|
+
const positions = await this.getPositions();
|
|
1124
|
+
callbacks.onPositions(positions);
|
|
1125
|
+
}
|
|
1126
|
+
if (callbacks.onOrders) {
|
|
1127
|
+
const orders = await this.getOrders();
|
|
1128
|
+
callbacks.onOrders(orders);
|
|
1129
|
+
}
|
|
1130
|
+
if (callbacks.onBalances) {
|
|
1131
|
+
const balances = await this.getBalances();
|
|
1132
|
+
callbacks.onBalances(balances);
|
|
1133
|
+
}
|
|
1134
|
+
if (callbacks.onTrades) {
|
|
1135
|
+
const trades = await this.getTrades(undefined, 100);
|
|
1136
|
+
callbacks.onTrades(trades);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
stopBackfillLoop() {
|
|
1140
|
+
if (this.backfillTimer) {
|
|
1141
|
+
clearInterval(this.backfillTimer);
|
|
1142
|
+
this.backfillTimer = null;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
async backfillTrackedMarkets() {
|
|
1146
|
+
if (!this.restClient || this.trackedMarketAddrs.size === 0) {
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
// Refresh asset contexts for 24h data
|
|
1150
|
+
if (this.isAssetContextsStale()) {
|
|
1151
|
+
try {
|
|
1152
|
+
await this.loadAssetContexts();
|
|
1153
|
+
}
|
|
1154
|
+
catch {
|
|
1155
|
+
log.warn("Failed to refresh asset contexts during backfill");
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
const prices = await this.restClient.getPrices();
|
|
1159
|
+
for (const price of prices) {
|
|
1160
|
+
if (!this.trackedMarketAddrs.has(price.market))
|
|
1161
|
+
continue;
|
|
1162
|
+
this.pricesByMarketAddr.set(price.market, price);
|
|
1163
|
+
const meta = this.marketsByAddr.get(price.market);
|
|
1164
|
+
if (meta) {
|
|
1165
|
+
const ticker = this.toTicker(meta, price);
|
|
1166
|
+
const subs = this.tickerSubscribers.get(meta.symbol);
|
|
1167
|
+
if (subs) {
|
|
1168
|
+
for (const cb of subs)
|
|
1169
|
+
cb(ticker);
|
|
1170
|
+
}
|
|
1171
|
+
for (const callbacks of this.globalSubscribers) {
|
|
1172
|
+
callbacks.onTicker?.(ticker);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
for (const marketAddr of this.trackedMarketAddrs) {
|
|
1177
|
+
try {
|
|
1178
|
+
const depth = await this.restClient.getDepth(marketAddr);
|
|
1179
|
+
const normalizedDepth = this.normalizeDepth(depth);
|
|
1180
|
+
this.booksByMarketAddr.set(marketAddr, normalizedDepth);
|
|
1181
|
+
const meta = this.marketsByAddr.get(marketAddr);
|
|
1182
|
+
if (meta) {
|
|
1183
|
+
const book = this.toOrderBook(meta, normalizedDepth);
|
|
1184
|
+
const subs = this.orderBookSubscribers.get(meta.symbol);
|
|
1185
|
+
if (subs) {
|
|
1186
|
+
for (const cb of subs)
|
|
1187
|
+
cb(book);
|
|
1188
|
+
}
|
|
1189
|
+
for (const callbacks of this.globalSubscribers) {
|
|
1190
|
+
callbacks.onOrderBook?.(book);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
catch {
|
|
1195
|
+
// Keep processing remaining markets.
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
isBookStale(timestamp) {
|
|
1200
|
+
return Date.now() - timestamp > 5_000;
|
|
1201
|
+
}
|
|
1202
|
+
isPriceStale(price) {
|
|
1203
|
+
const ts = parseNumber(price.transaction_unix_ms);
|
|
1204
|
+
if (!ts)
|
|
1205
|
+
return true;
|
|
1206
|
+
return Date.now() - ts > 10_000;
|
|
1207
|
+
}
|
|
1208
|
+
async resolveReferenceExecutionPrice(meta, side) {
|
|
1209
|
+
try {
|
|
1210
|
+
const book = await this.getOrderBook(meta.symbol, 1);
|
|
1211
|
+
const bestBid = parseNumber(book.bids[0]?.price);
|
|
1212
|
+
const bestAsk = parseNumber(book.asks[0]?.price);
|
|
1213
|
+
if (bestBid > 0 && bestAsk > 0) {
|
|
1214
|
+
return side === "long" ? bestAsk : bestBid;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
catch {
|
|
1218
|
+
// fallback to ticker below
|
|
1219
|
+
}
|
|
1220
|
+
const ticker = await this.getTicker(meta.symbol);
|
|
1221
|
+
const mark = parseNumber(ticker.markPrice);
|
|
1222
|
+
if (mark <= 0) {
|
|
1223
|
+
throw new Error(`Could not resolve execution price for ${meta.symbol}`);
|
|
1224
|
+
}
|
|
1225
|
+
return mark;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
function parseNumber(value) {
|
|
1229
|
+
if (value === undefined)
|
|
1230
|
+
return 0;
|
|
1231
|
+
const num = Number(value);
|
|
1232
|
+
return Number.isFinite(num) ? num : 0;
|
|
1233
|
+
}
|
|
1234
|
+
function toBoolean(value) {
|
|
1235
|
+
if (typeof value === "boolean")
|
|
1236
|
+
return value;
|
|
1237
|
+
if (typeof value === "number")
|
|
1238
|
+
return value !== 0;
|
|
1239
|
+
if (typeof value === "string")
|
|
1240
|
+
return value.toLowerCase() === "true";
|
|
1241
|
+
return false;
|
|
1242
|
+
}
|
|
1243
|
+
function toError(value) {
|
|
1244
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
1245
|
+
}
|
|
1246
|
+
function toNumericInput(value) {
|
|
1247
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
1248
|
+
return value;
|
|
1249
|
+
}
|
|
1250
|
+
return undefined;
|
|
1251
|
+
}
|
|
1252
|
+
function coerceWsPricePayload(marketAddr, data) {
|
|
1253
|
+
const payload = isRecord(data) ? data : {};
|
|
1254
|
+
const market = typeof payload.market === "string" ? payload.market : marketAddr;
|
|
1255
|
+
return {
|
|
1256
|
+
market,
|
|
1257
|
+
oracle_px: toNumericInput(payload.oracle_px) ?? 0,
|
|
1258
|
+
mark_px: toNumericInput(payload.mark_px) ?? 0,
|
|
1259
|
+
mid_px: toNumericInput(payload.mid_px) ?? 0,
|
|
1260
|
+
funding_rate_bps: toNumericInput(payload.funding_rate_bps) ?? 0,
|
|
1261
|
+
open_interest: toNumericInput(payload.open_interest) ?? 0,
|
|
1262
|
+
transaction_unix_ms: toNumericInput(payload.transaction_unix_ms),
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
function coerceWsDepthLevels(data, side) {
|
|
1266
|
+
const payload = isRecord(data) ? data : {};
|
|
1267
|
+
const rawLevels = payload[side];
|
|
1268
|
+
if (!Array.isArray(rawLevels)) {
|
|
1269
|
+
return [];
|
|
1270
|
+
}
|
|
1271
|
+
const levels = [];
|
|
1272
|
+
for (const rawLevel of rawLevels) {
|
|
1273
|
+
if (!isRecord(rawLevel))
|
|
1274
|
+
continue;
|
|
1275
|
+
const price = toNumericInput(rawLevel.price);
|
|
1276
|
+
const size = toNumericInput(rawLevel.size);
|
|
1277
|
+
if (price === undefined || size === undefined)
|
|
1278
|
+
continue;
|
|
1279
|
+
levels.push({ price, size });
|
|
1280
|
+
}
|
|
1281
|
+
return levels;
|
|
1282
|
+
}
|
|
1283
|
+
function coerceWsTrades(data, fallbackMarket) {
|
|
1284
|
+
const source = isRecord(data) ? data : {};
|
|
1285
|
+
const candidates = [
|
|
1286
|
+
source.trades,
|
|
1287
|
+
source.data,
|
|
1288
|
+
isRecord(source.data) ? source.data.trades : undefined,
|
|
1289
|
+
];
|
|
1290
|
+
let rows = [];
|
|
1291
|
+
if (Array.isArray(data)) {
|
|
1292
|
+
rows = data;
|
|
1293
|
+
}
|
|
1294
|
+
else {
|
|
1295
|
+
for (const candidate of candidates) {
|
|
1296
|
+
if (Array.isArray(candidate)) {
|
|
1297
|
+
rows = candidate;
|
|
1298
|
+
break;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
if (rows.length === 0)
|
|
1303
|
+
return [];
|
|
1304
|
+
const trades = [];
|
|
1305
|
+
for (const row of rows) {
|
|
1306
|
+
if (!isRecord(row))
|
|
1307
|
+
continue;
|
|
1308
|
+
const price = parseNumber(toNumericInput(row.price));
|
|
1309
|
+
const size = parseNumber(toNumericInput(row.size) ??
|
|
1310
|
+
toNumericInput(row.qty) ??
|
|
1311
|
+
toNumericInput(row.quantity) ??
|
|
1312
|
+
toNumericInput(row.sz));
|
|
1313
|
+
if (price <= 0 || size <= 0)
|
|
1314
|
+
continue;
|
|
1315
|
+
const market = normalizeWsMarketSymbol(typeof row.market_name === "string"
|
|
1316
|
+
? row.market_name
|
|
1317
|
+
: typeof row.market === "string"
|
|
1318
|
+
? row.market
|
|
1319
|
+
: fallbackMarket, fallbackMarket);
|
|
1320
|
+
const sideValue = typeof row.side === "string"
|
|
1321
|
+
? row.side
|
|
1322
|
+
: toBoolean(row.is_buy)
|
|
1323
|
+
? "buy"
|
|
1324
|
+
: "sell";
|
|
1325
|
+
const side = sideValue.toLowerCase().startsWith("buy") ? "long" : "short";
|
|
1326
|
+
const orderId = typeof row.order_id === "string"
|
|
1327
|
+
? row.order_id
|
|
1328
|
+
: typeof row.orderId === "string"
|
|
1329
|
+
? row.orderId
|
|
1330
|
+
: undefined;
|
|
1331
|
+
trades.push({
|
|
1332
|
+
id: (typeof row.trade_id === "string" && row.trade_id) ||
|
|
1333
|
+
(typeof row.fill_id === "string" && row.fill_id) ||
|
|
1334
|
+
(typeof row.id === "string" && row.id) ||
|
|
1335
|
+
`${market}-${Date.now()}-${trades.length}`,
|
|
1336
|
+
market,
|
|
1337
|
+
side,
|
|
1338
|
+
price: price.toString(),
|
|
1339
|
+
size: size.toString(),
|
|
1340
|
+
fee: parseNumber(toNumericInput(row.fee)).toString(),
|
|
1341
|
+
feeAsset: (typeof row.fee_asset === "string" && row.fee_asset) ||
|
|
1342
|
+
(typeof row.feeAsset === "string" && row.feeAsset) ||
|
|
1343
|
+
"USD",
|
|
1344
|
+
timestamp: parseNumber(toNumericInput(row.unix_ms) ??
|
|
1345
|
+
toNumericInput(row.timestamp) ??
|
|
1346
|
+
toNumericInput(row.ts) ??
|
|
1347
|
+
toNumericInput(row.time)) || Date.now(),
|
|
1348
|
+
...(orderId ? { orderId } : {}),
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
return trades;
|
|
1352
|
+
}
|
|
1353
|
+
function normalizeWsMarketSymbol(raw, fallbackMarket) {
|
|
1354
|
+
const value = raw.trim();
|
|
1355
|
+
if (!value)
|
|
1356
|
+
return fallbackMarket;
|
|
1357
|
+
const upper = value.toUpperCase();
|
|
1358
|
+
if (upper.endsWith("-PERP"))
|
|
1359
|
+
return upper;
|
|
1360
|
+
if (upper.includes("/")) {
|
|
1361
|
+
return `${upper.split("/")[0]}-PERP`;
|
|
1362
|
+
}
|
|
1363
|
+
// Trade feed can carry market addresses; fallback to the subscribed symbol in that case.
|
|
1364
|
+
if (upper.startsWith("0X"))
|
|
1365
|
+
return fallbackMarket;
|
|
1366
|
+
return `${upper}-PERP`;
|
|
1367
|
+
}
|
|
1368
|
+
function bigintToDecimalString(units, decimals) {
|
|
1369
|
+
if (decimals <= 0)
|
|
1370
|
+
return units.toString();
|
|
1371
|
+
const base = 10n ** BigInt(decimals);
|
|
1372
|
+
const whole = units / base;
|
|
1373
|
+
const frac = (units % base).toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
1374
|
+
return frac.length > 0 ? `${whole.toString()}.${frac}` : whole.toString();
|
|
1375
|
+
}
|
|
1376
|
+
registerAdapter("decibel", () => new DecibelAdapter());
|
|
1377
|
+
export default DecibelAdapter;
|