perp-cli 0.12.17 → 0.14.1
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/README.md +247 -27
- package/dist/_stderr-filter.js +0 -1
- package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
- package/dist/agent-wallet/lighter-keystore.js +92 -10
- package/dist/agent-wallet/passphrase.d.ts +1 -1
- package/dist/agent-wallet/passphrase.js +16 -3
- package/dist/agent-wallet/store.d.ts +37 -4
- package/dist/agent-wallet/store.js +132 -0
- package/dist/agent-wallet/venue-keystore.d.ts +14 -0
- package/dist/agent-wallet/venue-keystore.js +121 -0
- package/dist/api/public/hyperliquid.js +4 -4
- package/dist/api/public/lighter.js +5 -5
- package/dist/api/public/pacifica.d.ts +0 -1
- package/dist/api/public/pacifica.js +4 -13
- package/dist/api/public/urls.d.ts +9 -1
- package/dist/api/public/urls.js +35 -13
- package/dist/arb/file-lock.d.ts +11 -0
- package/dist/arb/file-lock.js +74 -0
- package/dist/arb/index.d.ts +2 -2
- package/dist/arb/index.js +2 -2
- package/dist/arb/pending-entry.d.ts +32 -0
- package/dist/arb/pending-entry.js +77 -0
- package/dist/arb/sizing.d.ts +6 -13
- package/dist/arb/sizing.js +12 -51
- package/dist/arb/state.d.ts +45 -12
- package/dist/arb/state.js +214 -42
- package/dist/arb/utils.d.ts +20 -6
- package/dist/arb/utils.js +43 -39
- package/dist/bot/conditions.d.ts +4 -4
- package/dist/bot/conditions.js +11 -6
- package/dist/bot/config.d.ts +17 -1
- package/dist/bot/config.js +48 -12
- package/dist/bot/engine.d.ts +61 -2
- package/dist/bot/engine.js +610 -144
- package/dist/bot/fill-size.d.ts +7 -0
- package/dist/bot/fill-size.js +18 -0
- package/dist/bot/index.d.ts +1 -1
- package/dist/bot/index.js +1 -1
- package/dist/bot/perp-order.d.ts +99 -0
- package/dist/bot/perp-order.js +299 -0
- package/dist/bot/presets.d.ts +7 -0
- package/dist/bot/presets.js +17 -0
- package/dist/bot/quarantine.d.ts +75 -0
- package/dist/bot/quarantine.js +190 -0
- package/dist/bot/quote-rules.d.ts +23 -0
- package/dist/bot/quote-rules.js +69 -0
- package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
- package/dist/bot/strategies/avellaneda-mm.js +72 -70
- package/dist/bot/strategies/basis-arb.js +2 -0
- package/dist/bot/strategies/dca-strategy.d.ts +2 -0
- package/dist/bot/strategies/dca-strategy.js +5 -0
- package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
- package/dist/bot/strategies/funding-arb-strategy.js +571 -380
- package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
- package/dist/bot/strategies/funding-arb-utils.js +159 -195
- package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
- package/dist/bot/strategies/funding-arb-v2.js +258 -47
- package/dist/bot/strategies/funding-auto.js +215 -194
- package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
- package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
- package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
- package/dist/bot/strategies/spot-perp-executor.js +319 -120
- package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
- package/dist/bot/strategies/spot-perp-state.js +70 -0
- package/dist/bot/strategy-types.d.ts +6 -0
- package/dist/bot/tui/BotDashboard.d.ts +7 -2
- package/dist/bot/tui/BotDashboard.js +6 -3
- package/dist/bridge-engine.d.ts +120 -19
- package/dist/bridge-engine.js +710 -422
- package/dist/bridge-signers.d.ts +54 -0
- package/dist/bridge-signers.js +161 -0
- package/dist/cache.d.ts +4 -1
- package/dist/cache.js +11 -0
- package/dist/cli-flags.d.ts +50 -0
- package/dist/cli-flags.js +104 -0
- package/dist/cli-program.d.ts +35 -0
- package/dist/cli-program.js +303 -0
- package/dist/cli-spec.js +1 -1
- package/dist/client-id-tracker.js +11 -2
- package/dist/commands/account.d.ts +7 -1
- package/dist/commands/account.js +70 -92
- package/dist/commands/agent-import.d.ts +114 -0
- package/dist/commands/agent-import.js +794 -0
- package/dist/commands/agent.d.ts +232 -2
- package/dist/commands/agent.js +1858 -955
- package/dist/commands/alerts.d.ts +1 -0
- package/dist/commands/alerts.js +31 -28
- package/dist/commands/arb/index.d.ts +60 -1
- package/dist/commands/arb/index.js +381 -384
- package/dist/commands/arb-auto.d.ts +132 -4
- package/dist/commands/arb-auto.js +1474 -967
- package/dist/commands/arb.d.ts +3 -1
- package/dist/commands/arb.js +62 -30
- package/dist/commands/bot.js +127 -47
- package/dist/commands/bridge.d.ts +2 -1
- package/dist/commands/bridge.js +339 -192
- package/dist/commands/builder.d.ts +55 -0
- package/dist/commands/builder.js +387 -0
- package/dist/commands/funds-onchain.d.ts +32 -0
- package/dist/commands/funds-onchain.js +248 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +925 -245
- package/dist/commands/history.js +102 -54
- package/dist/commands/init.d.ts +107 -15
- package/dist/commands/init.js +1116 -249
- package/dist/commands/jobs.d.ts +2 -1
- package/dist/commands/jobs.js +77 -15
- package/dist/commands/manage.js +136 -91
- package/dist/commands/market.d.ts +4 -1
- package/dist/commands/market.js +35 -32
- package/dist/commands/outcome.d.ts +10 -0
- package/dist/commands/outcome.js +396 -0
- package/dist/commands/plan.js +52 -12
- package/dist/commands/portfolio.d.ts +1 -0
- package/dist/commands/portfolio.js +60 -72
- package/dist/commands/rebalance.d.ts +2 -1
- package/dist/commands/rebalance.js +33 -43
- package/dist/commands/risk.js +46 -19
- package/dist/commands/settings.d.ts +1 -0
- package/dist/commands/settings.js +246 -51
- package/dist/commands/trade.d.ts +50 -0
- package/dist/commands/trade.js +1943 -695
- package/dist/commands/wallet.d.ts +30 -1
- package/dist/commands/wallet.js +357 -459
- package/dist/config.d.ts +60 -9
- package/dist/config.js +252 -82
- package/dist/constants.d.ts +29 -4
- package/dist/constants.js +62 -20
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +9 -6
- package/dist/dashboard/ui.js +6 -4
- package/dist/dashboard/ws-feeds.js +20 -8
- package/dist/dex-asset-map.js +5 -3
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +194 -0
- package/dist/event-stream.js +75 -19
- package/dist/evm-master-tx.d.ts +59 -0
- package/dist/evm-master-tx.js +154 -0
- package/dist/exchanges/adapter-network.d.ts +10 -0
- package/dist/exchanges/adapter-network.js +10 -0
- package/dist/exchanges/arcus/adapter.d.ts +196 -0
- package/dist/exchanges/arcus/adapter.js +1092 -0
- package/dist/exchanges/arcus/agent.d.ts +55 -0
- package/dist/exchanges/arcus/agent.js +188 -0
- package/dist/exchanges/arcus/client.d.ts +63 -0
- package/dist/exchanges/arcus/client.js +285 -0
- package/dist/exchanges/arcus/public.d.ts +65 -0
- package/dist/exchanges/arcus/public.js +138 -0
- package/dist/exchanges/arcus/signing.d.ts +176 -0
- package/dist/exchanges/arcus/signing.js +288 -0
- package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
- package/dist/exchanges/arcus/spot-chain.js +241 -0
- package/dist/exchanges/arcus/spot-router.d.ts +177 -0
- package/dist/exchanges/arcus/spot-router.js +183 -0
- package/dist/exchanges/arcus/spot.d.ts +109 -0
- package/dist/exchanges/arcus/spot.js +645 -0
- package/dist/exchanges/aster-funds.d.ts +127 -0
- package/dist/exchanges/aster-funds.js +252 -0
- package/dist/exchanges/aster-nonce.d.ts +2 -0
- package/dist/exchanges/aster-nonce.js +107 -0
- package/dist/exchanges/aster-spot.d.ts +117 -0
- package/dist/exchanges/aster-spot.js +686 -0
- package/dist/exchanges/aster-typed-data.d.ts +133 -7
- package/dist/exchanges/aster-typed-data.js +121 -7
- package/dist/exchanges/aster.d.ts +240 -6
- package/dist/exchanges/aster.js +1139 -195
- package/dist/exchanges/builder-fee.d.ts +254 -0
- package/dist/exchanges/builder-fee.js +421 -0
- package/dist/exchanges/canonical-symbols.d.ts +32 -0
- package/dist/exchanges/canonical-symbols.js +96 -0
- package/dist/exchanges/capabilities.d.ts +7 -1
- package/dist/exchanges/capabilities.js +7 -0
- package/dist/exchanges/hl-signing.d.ts +165 -0
- package/dist/exchanges/hl-signing.js +158 -0
- package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
- package/dist/exchanges/hyperliquid-outcome.js +646 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
- package/dist/exchanges/hyperliquid-spot.js +308 -117
- package/dist/exchanges/hyperliquid.d.ts +462 -57
- package/dist/exchanges/hyperliquid.js +1575 -449
- package/dist/exchanges/index.d.ts +6 -0
- package/dist/exchanges/index.js +4 -0
- package/dist/exchanges/interface.d.ts +63 -10
- package/dist/exchanges/interface.js +18 -3
- package/dist/exchanges/ioc-outcome.d.ts +44 -0
- package/dist/exchanges/ioc-outcome.js +57 -0
- package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
- package/dist/exchanges/legacy-trading-v2.js +940 -0
- package/dist/exchanges/lighter-public.d.ts +43 -0
- package/dist/exchanges/lighter-public.js +276 -0
- package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
- package/dist/exchanges/lighter-rh-deposit.js +185 -0
- package/dist/exchanges/lighter-signer.d.ts +123 -0
- package/dist/exchanges/lighter-signer.js +215 -0
- package/dist/exchanges/lighter-spot.d.ts +91 -13
- package/dist/exchanges/lighter-spot.js +595 -233
- package/dist/exchanges/lighter.d.ts +611 -39
- package/dist/exchanges/lighter.js +1890 -649
- package/dist/exchanges/ondo/adapter.d.ts +152 -0
- package/dist/exchanges/ondo/adapter.js +811 -0
- package/dist/exchanges/ondo/agent.d.ts +31 -0
- package/dist/exchanges/ondo/agent.js +101 -0
- package/dist/exchanges/ondo/client.d.ts +60 -0
- package/dist/exchanges/ondo/client.js +297 -0
- package/dist/exchanges/ondo/funds.d.ts +95 -0
- package/dist/exchanges/ondo/funds.js +198 -0
- package/dist/exchanges/ondo/public.d.ts +95 -0
- package/dist/exchanges/ondo/public.js +214 -0
- package/dist/exchanges/ondo/spot.d.ts +127 -0
- package/dist/exchanges/ondo/spot.js +392 -0
- package/dist/exchanges/outcome-interface.d.ts +140 -0
- package/dist/exchanges/outcome-interface.js +16 -0
- package/dist/exchanges/pacifica-spot.d.ts +83 -0
- package/dist/exchanges/pacifica-spot.js +324 -0
- package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
- package/dist/exchanges/pacifica-typed-data.js +57 -9
- package/dist/exchanges/pacifica.d.ts +293 -4
- package/dist/exchanges/pacifica.js +1044 -90
- package/dist/exchanges/precision.d.ts +47 -0
- package/dist/exchanges/precision.js +209 -0
- package/dist/exchanges/qfex/adapter.d.ts +341 -0
- package/dist/exchanges/qfex/adapter.js +1386 -0
- package/dist/exchanges/qfex/agent.d.ts +58 -0
- package/dist/exchanges/qfex/agent.js +188 -0
- package/dist/exchanges/qfex/client.d.ts +54 -0
- package/dist/exchanges/qfex/client.js +138 -0
- package/dist/exchanges/qfex/errors.d.ts +70 -0
- package/dist/exchanges/qfex/errors.js +236 -0
- package/dist/exchanges/qfex/public.d.ts +122 -0
- package/dist/exchanges/qfex/public.js +268 -0
- package/dist/exchanges/qfex/signing.d.ts +160 -0
- package/dist/exchanges/qfex/signing.js +283 -0
- package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
- package/dist/exchanges/qfex/ws-transport.js +798 -0
- package/dist/exchanges/rate-limiter.d.ts +12 -0
- package/dist/exchanges/rate-limiter.js +64 -0
- package/dist/exchanges/registry.d.ts +14 -2
- package/dist/exchanges/registry.js +20 -32
- package/dist/exchanges/slippage.d.ts +37 -0
- package/dist/exchanges/slippage.js +70 -0
- package/dist/exchanges/spot-factory.d.ts +166 -0
- package/dist/exchanges/spot-factory.js +431 -0
- package/dist/exchanges/spot-interface.d.ts +98 -4
- package/dist/exchanges/spot-interface.js +27 -1
- package/dist/exchanges/spot-result.d.ts +79 -0
- package/dist/exchanges/spot-result.js +273 -0
- package/dist/exchanges/trading-v2.d.ts +117 -0
- package/dist/exchanges/trading-v2.js +301 -0
- package/dist/exchanges/venue-types.d.ts +231 -0
- package/dist/exchanges/venue-types.js +87 -0
- package/dist/exchanges/venue.d.ts +211 -0
- package/dist/exchanges/venue.js +132 -0
- package/dist/exchanges/venues/arcus.d.ts +8 -0
- package/dist/exchanges/venues/arcus.js +109 -0
- package/dist/exchanges/venues/aster.d.ts +49 -0
- package/dist/exchanges/venues/aster.js +176 -0
- package/dist/exchanges/venues/create-helpers.d.ts +18 -0
- package/dist/exchanges/venues/create-helpers.js +199 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
- package/dist/exchanges/venues/hyperliquid.js +126 -0
- package/dist/exchanges/venues/index.d.ts +16 -0
- package/dist/exchanges/venues/index.js +26 -0
- package/dist/exchanges/venues/lighter-family.d.ts +92 -0
- package/dist/exchanges/venues/lighter-family.js +166 -0
- package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
- package/dist/exchanges/venues/lighter-rh.js +58 -0
- package/dist/exchanges/venues/lighter.d.ts +6 -0
- package/dist/exchanges/venues/lighter.js +55 -0
- package/dist/exchanges/venues/ondo.d.ts +29 -0
- package/dist/exchanges/venues/ondo.js +136 -0
- package/dist/exchanges/venues/pacifica.d.ts +8 -0
- package/dist/exchanges/venues/pacifica.js +95 -0
- package/dist/exchanges/venues/qfex.d.ts +39 -0
- package/dist/exchanges/venues/qfex.js +140 -0
- package/dist/execution-log.js +10 -2
- package/dist/funding/legacy-public.d.ts +31 -0
- package/dist/funding/legacy-public.js +369 -0
- package/dist/funding/normalize.d.ts +28 -16
- package/dist/funding/normalize.js +61 -29
- package/dist/funding/rates.d.ts +46 -11
- package/dist/funding/rates.js +228 -277
- package/dist/guardrail/perp-guardrail.js +0 -0
- package/dist/index.js +156 -684
- package/dist/jobs.d.ts +84 -6
- package/dist/jobs.js +239 -41
- package/dist/landing.d.ts +7 -2
- package/dist/landing.js +19 -3
- package/dist/mcp-server.d.ts +14 -4
- package/dist/mcp-server.js +821 -403
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -0
- package/dist/onchain-balances.d.ts +40 -0
- package/dist/onchain-balances.js +173 -0
- package/dist/pacifica/client.d.ts +41 -10
- package/dist/pacifica/client.js +167 -63
- package/dist/pacifica/constants.d.ts +0 -1
- package/dist/pacifica/constants.js +0 -5
- package/dist/pacifica/deposit.d.ts +30 -3
- package/dist/pacifica/deposit.js +119 -2
- package/dist/pacifica/faucet.d.ts +29 -0
- package/dist/pacifica/faucet.js +100 -0
- package/dist/pacifica/sign-tx.d.ts +15 -0
- package/dist/pacifica/sign-tx.js +41 -0
- package/dist/pacifica/types/account.d.ts +28 -4
- package/dist/pacifica/types/lake.d.ts +20 -5
- package/dist/pacifica/types/lake.js +4 -0
- package/dist/pacifica/types/market.d.ts +5 -0
- package/dist/pacifica/types/order.d.ts +4 -1
- package/dist/pacifica/types/position.d.ts +9 -1
- package/dist/plan-executor.d.ts +10 -5
- package/dist/plan-executor.js +165 -54
- package/dist/position-history.js +13 -0
- package/dist/rebalance.d.ts +26 -2
- package/dist/rebalance.js +86 -7
- package/dist/relay-swap.d.ts +232 -0
- package/dist/relay-swap.js +630 -0
- package/dist/risk.js +5 -1
- package/dist/secret-env.d.ts +15 -0
- package/dist/secret-env.js +36 -0
- package/dist/settings.d.ts +169 -6
- package/dist/settings.js +219 -38
- package/dist/signer/ows-evm.d.ts +13 -0
- package/dist/signer/ows-evm.js +22 -0
- package/dist/smart-order.d.ts +9 -0
- package/dist/smart-order.js +53 -8
- package/dist/strategies/dca.d.ts +6 -2
- package/dist/strategies/dca.js +66 -26
- package/dist/strategies/funding-arb.d.ts +11 -1
- package/dist/strategies/funding-arb.js +202 -89
- package/dist/strategies/grid.d.ts +31 -0
- package/dist/strategies/grid.js +89 -11
- package/dist/strategies/pending-stop.d.ts +8 -0
- package/dist/strategies/pending-stop.js +12 -0
- package/dist/strategies/split-order.d.ts +45 -17
- package/dist/strategies/split-order.js +157 -91
- package/dist/strategies/trailing-stop.d.ts +2 -1
- package/dist/strategies/trailing-stop.js +52 -6
- package/dist/strategies/twap.d.ts +6 -1
- package/dist/strategies/twap.js +39 -11
- package/dist/trade-validator.d.ts +22 -0
- package/dist/trade-validator.js +119 -15
- package/dist/utils/numeric.d.ts +46 -0
- package/dist/utils/numeric.js +79 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +96 -19
- package/dist/withdraw-gate.d.ts +49 -0
- package/dist/withdraw-gate.js +131 -0
- package/package.json +15 -6
- package/skills/perp-cli/SKILL.md +159 -55
- package/skills/perp-cli/references/agent-operations.md +112 -58
- package/skills/perp-cli/references/commands.md +269 -73
- package/skills/perp-cli/references/strategies.md +34 -31
- package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
- package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
- package/skills/perp-cli/scripts/preflight.sh +21 -14
- package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
- package/skills/perp-cli/scripts/validate-arb.sh +31 -15
- package/vendor/lighter-signer/SHA256SUMS +2 -0
- package/vendor/lighter-signer/VERSION +8 -0
- package/vendor/lighter-signer/lighter-signer.wasm +0 -0
- package/vendor/lighter-signer/wasm_exec.js +575 -0
- package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
- package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ondo Perps adapter — native TradingV2 + legacy ExchangeAdapter shim
|
|
3
|
+
* (design §10.7, §10.10).
|
|
4
|
+
*
|
|
5
|
+
* Signer: HMAC API key only (OndoAgentMeta → ~/.perp/ondo-agents/<keyId>.json).
|
|
6
|
+
* There is no master-key signing path for trading; read-only adapters serve
|
|
7
|
+
* public endpoints only.
|
|
8
|
+
*
|
|
9
|
+
* Order entry:
|
|
10
|
+
* - market orders: IOC LIMIT at the protection price
|
|
11
|
+
* alignProtectionPrice(slippagePrice(mark, side, bps), quoteIncrement, side),
|
|
12
|
+
* bps = the caller's resolveSlippage() value (design §13.2);
|
|
13
|
+
* - TIF (design §13.3): GTC | IOC | ALO, where ALO = GTC + postOnly; FOK is
|
|
14
|
+
* not offered (uniform CAPABILITY_UNSUPPORTED, never emulated); a limit order
|
|
15
|
+
* without a TIF is GTC;
|
|
16
|
+
* - limit orders without a caller clientOrderId get a generated one that
|
|
17
|
+
* encodes post-only (`pcli-p-<hex>` / `pcli-n-<hex>`, see ondoClientOrderId)
|
|
18
|
+
* because the API never reports postOnly back;
|
|
19
|
+
* - TP/SL attached to the entry via takeProfit/stopLoss;
|
|
20
|
+
* - stopMarket = position-level stop (POST /v1/perps/stop_order), reduce-only,
|
|
21
|
+
* mark-triggered; stopLimit is not offered by the venue;
|
|
22
|
+
* - amend = cancel, then place (design §13.10 #10, #11). `size` is the NEW OPEN
|
|
23
|
+
* size. The replacement keeps side / reduceOnly / TIF; post-only comes from
|
|
24
|
+
* the original's generated clientOrderId, or the caller's explicit TIF — an
|
|
25
|
+
* order whose post-only flag is not decodable (placed elsewhere / caller id)
|
|
26
|
+
* and no TIF given is refused before anything is cancelled. Fills that race
|
|
27
|
+
* the cancel reduce the replacement: replace = size − (filledAtCancel −
|
|
28
|
+
* filledAtRead); ≤ 0 (or below the step) → no replacement (reported). Cancel
|
|
29
|
+
* outcome unknown → ORDER_PENDING (nothing placed); cancel ok + replacement
|
|
30
|
+
* refused → ORDER_PARTIAL { cancelled, placed: null }.
|
|
31
|
+
* - cancel-all cancels resting ("open") orders and untriggered stops (position-level
|
|
32
|
+
* and entry-attached TP/SL) — see listWorkingOrders.
|
|
33
|
+
* - a reduce-only limit order must be IOC (capabilities.reduceOnlyTif); a resting one is
|
|
34
|
+
* refused before sending.
|
|
35
|
+
* Limit prices and TP/SL triggers must be on tick (INVALID_TICK); sizes are
|
|
36
|
+
* floored to the step (SIZE_TOO_SMALL when that reaches zero).
|
|
37
|
+
*/
|
|
38
|
+
import { randomUUID } from "node:crypto";
|
|
39
|
+
import { PerpError, orderNotSent, orderOutcomeUnknown } from "../../errors.js";
|
|
40
|
+
import { alignProtectionPrice, floorSize, formatComputedPrice, requireOnTick, slippagePrice, subDecimal } from "../precision.js";
|
|
41
|
+
import { unsupported, validateLegacyTif, validateOrderRequest } from "../trading-v2.js";
|
|
42
|
+
import { networkConfig, requireCapabilities } from "../venue.js";
|
|
43
|
+
import { parseOrderRef, toLegacyMarketInfo } from "../venue-types.js";
|
|
44
|
+
import { builderGateOf, builderRejectionOf, NO_BUILDER, requireFeeBps } from "../builder-fee.js";
|
|
45
|
+
import { loadOndoCredentials, ONDO_APPROVE_REMEDIATION } from "./agent.js";
|
|
46
|
+
import { OndoClient } from "./client.js";
|
|
47
|
+
import { fetchOndoTradingPairs, ondoPlainSymbol, ondoPublicData, ondoVenueSymbol, parseOndoTime, strOrNull, } from "./public.js";
|
|
48
|
+
const ORDERS_PAGE_LIMIT = 100;
|
|
49
|
+
const DEPTH_LEVELS = 50;
|
|
50
|
+
const PAIRS_TTL_MS = 5 * 60_000;
|
|
51
|
+
const ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
52
|
+
/** Ondo (Go) encodes an empty list as JSON null; that is "no rows", not an unknown value. */
|
|
53
|
+
function listOf(v) {
|
|
54
|
+
return v === null ? [] : v;
|
|
55
|
+
}
|
|
56
|
+
function isPositive(s) {
|
|
57
|
+
return Number(s) > 0;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Generated clientOrderId carrying the post-only flag (design §13.10 #11): the
|
|
61
|
+
* Ondo API never returns postOnly, so amend recovers it from the id instead of
|
|
62
|
+
* a local file. `pcli-p-` = post-only, `pcli-n-` = not post-only; 39 chars of
|
|
63
|
+
* [A-Za-z0-9_-] (venue rule: 1–64).
|
|
64
|
+
*/
|
|
65
|
+
const GENERATED_CLOID_RE = /^pcli-([pn])-[0-9a-f]{32}$/;
|
|
66
|
+
function ondoClientOrderId(postOnly) {
|
|
67
|
+
return `pcli-${postOnly ? "p" : "n"}-${randomUUID().replace(/-/g, "")}`;
|
|
68
|
+
}
|
|
69
|
+
/** Post-only flag of a generated clientOrderId; null = not generated here (attributes unknown). */
|
|
70
|
+
function postOnlyOf(clientOrderId) {
|
|
71
|
+
if (clientOrderId === undefined)
|
|
72
|
+
return null;
|
|
73
|
+
const m = GENERATED_CLOID_RE.exec(clientOrderId);
|
|
74
|
+
return m === null ? null : m[1] === "p";
|
|
75
|
+
}
|
|
76
|
+
/** Ondo wire TIF for a resolved common TIF: ALO = GTC + postOnly. FOK is never sent. */
|
|
77
|
+
function wireTif(tif) {
|
|
78
|
+
if (tif === "ALO")
|
|
79
|
+
return { timeInForce: "GTC", postOnly: true, label: "GTC+postOnly" };
|
|
80
|
+
if (tif === "GTC" || tif === "IOC")
|
|
81
|
+
return { timeInForce: tif, postOnly: false, label: tif };
|
|
82
|
+
throw new PerpError("INVALID_PARAMS", `unreachable: Ondo TIF ${tif} passed validation`);
|
|
83
|
+
}
|
|
84
|
+
/** All-null execution record with the given method (fields set by the caller). */
|
|
85
|
+
function execution(method, partial) {
|
|
86
|
+
return {
|
|
87
|
+
method,
|
|
88
|
+
slippageBps: partial.slippageBps ?? null,
|
|
89
|
+
slippageSource: partial.slippageSource ?? null,
|
|
90
|
+
protectionPrice: partial.protectionPrice ?? null,
|
|
91
|
+
referencePrice: partial.referencePrice ?? null,
|
|
92
|
+
tifRequested: partial.tifRequested ?? null,
|
|
93
|
+
tifSent: partial.tifSent ?? null,
|
|
94
|
+
expiresAt: partial.expiresAt ?? null,
|
|
95
|
+
builderFee: partial.builderFee ?? null,
|
|
96
|
+
notes: partial.notes ?? [],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Execution record for an order read back from the venue (status / amend paths). */
|
|
100
|
+
function readExecution(o) {
|
|
101
|
+
const postOnly = postOnlyOf(o.clientOrderId);
|
|
102
|
+
const method = o.type === "limit" ? "limit" : o.type === "market" ? "market" : "trigger";
|
|
103
|
+
const tifSent = o.timeInForce === undefined ? null : postOnly === true ? "GTC+postOnly" : o.timeInForce;
|
|
104
|
+
return execution(method, {
|
|
105
|
+
tifSent,
|
|
106
|
+
notes: postOnly === null && o.type === "limit" ? ["post-only flag not reported by the venue (order not placed with a perp-cli generated client id)"] : [],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** Thrown by an amend's DELETE: a venue answer (HTTP < 500 / rate limit) means the cancel was refused, anything else is unknown. */
|
|
110
|
+
function cancelDefinitelyRefused(e) {
|
|
111
|
+
if (!(e instanceof PerpError))
|
|
112
|
+
return false;
|
|
113
|
+
if (e.structured.code === "RATE_LIMITED")
|
|
114
|
+
return true;
|
|
115
|
+
const http = e.structured.details?.httpStatus;
|
|
116
|
+
return typeof http === "number" && http < 500;
|
|
117
|
+
}
|
|
118
|
+
/** Thrown by a POST: definitely not placed (venue refusal with HTTP < 500 or a pre-send failure). */
|
|
119
|
+
function placementDefinitelyRefused(e) {
|
|
120
|
+
if (!(e instanceof PerpError) || e.structured.code === "ORDER_PENDING")
|
|
121
|
+
return false;
|
|
122
|
+
const http = e.structured.details?.httpStatus;
|
|
123
|
+
return typeof http === "number" ? http < 500 : e.structured.code !== "EXCHANGE_ERROR";
|
|
124
|
+
}
|
|
125
|
+
function mapStatus(o) {
|
|
126
|
+
switch (o.status) {
|
|
127
|
+
case "pending": return "accepted";
|
|
128
|
+
case "untriggered": return "open";
|
|
129
|
+
case "open": return isPositive(o.filledSize) ? "partiallyFilled" : "open";
|
|
130
|
+
case "fullyfilled": return "filled";
|
|
131
|
+
// A cancelled order that filled part of its size is a partial fill (its remainder cancelled).
|
|
132
|
+
case "canceled": return isPositive(o.filledSize) ? "partiallyFilled" : "cancelled";
|
|
133
|
+
}
|
|
134
|
+
throw new PerpError("EXCHANGE_ERROR", `Ondo Perps: unknown order status "${String(o.status)}"`);
|
|
135
|
+
}
|
|
136
|
+
function refPath(ref) {
|
|
137
|
+
if (!ID_RE.test(ref.value)) {
|
|
138
|
+
throw new PerpError("INVALID_PARAMS", `invalid order reference "${ref.value}" (allowed: A-Z a-z 0-9 _ -, max 64)`);
|
|
139
|
+
}
|
|
140
|
+
return ref.kind === "orderId" ? `/v1/perps/orders/${ref.value}` : `/v1/perps/orders/client:${ref.value}`;
|
|
141
|
+
}
|
|
142
|
+
/** CLI kline interval → Ondo resolution ("15m" → "15", "4h" → "240", "1d" → "1D", "1w" → "1W", "1M" → "1M"). */
|
|
143
|
+
function toResolution(interval) {
|
|
144
|
+
const m = /^(\d+)([mhdwM])$/.exec(interval.trim());
|
|
145
|
+
if (!m) {
|
|
146
|
+
throw new PerpError("INVALID_PARAMS", `Unsupported kline interval "${interval}"`, {
|
|
147
|
+
remediation: "Use e.g. 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w, 1M",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const n = Number(m[1]);
|
|
151
|
+
switch (m[2]) {
|
|
152
|
+
case "m": return String(n);
|
|
153
|
+
case "h": return String(n * 60);
|
|
154
|
+
case "d": return `${n}D`;
|
|
155
|
+
case "w": return `${n}W`;
|
|
156
|
+
default: return `${n}M`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
export class OndoAdapter {
|
|
160
|
+
name = "ondo";
|
|
161
|
+
aliases = ["ondo-perps"];
|
|
162
|
+
venue;
|
|
163
|
+
network;
|
|
164
|
+
caps;
|
|
165
|
+
client;
|
|
166
|
+
pairsCache = null;
|
|
167
|
+
/**
|
|
168
|
+
* Builder-fee gate (settings builder.ondo; Ondo has no approval step); null = not
|
|
169
|
+
* configured. Set by the venue definition's create — the adapter never reads settings.
|
|
170
|
+
*/
|
|
171
|
+
builderFee = null;
|
|
172
|
+
/**
|
|
173
|
+
* GET /v1/perps/candles needs the account's API key (docs.ondoperps.xyz rest-spec.json:
|
|
174
|
+
* `security: [{"BearerAuth": []}, {"ApiKeyAuth": []}]` — no anonymous `{}` entry, unlike
|
|
175
|
+
* /v1/perps/depth). `market kline` therefore uses the credentialed adapter (klinesNeedAuth).
|
|
176
|
+
*/
|
|
177
|
+
klinesRequireAuth = true;
|
|
178
|
+
constructor(def, ctx) {
|
|
179
|
+
this.venue = def;
|
|
180
|
+
this.network = ctx.network;
|
|
181
|
+
this.caps = requireCapabilities(def);
|
|
182
|
+
let credentials = null;
|
|
183
|
+
if (!ctx.readOnly) {
|
|
184
|
+
if (ctx.noAgent) {
|
|
185
|
+
throw new PerpError("NO_SIGNER_AVAILABLE", "Ondo Perps signs only with a registered API key; --no-agent leaves no signer", {
|
|
186
|
+
remediation: `Drop --no-agent, or register a key: ${ONDO_APPROVE_REMEDIATION}`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const agent = ctx.credentials.agent;
|
|
190
|
+
if (agent === null || !("kind" in agent) || agent.kind !== "ondo") {
|
|
191
|
+
throw new PerpError("NO_SIGNER_AVAILABLE", "No Ondo Perps API key registered", {
|
|
192
|
+
remediation: `Register one: ${ONDO_APPROVE_REMEDIATION}`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const meta = agent;
|
|
196
|
+
if (meta.network !== ctx.network) {
|
|
197
|
+
throw new PerpError("AGENT_NOT_REGISTERED", `Ondo Perps API key ${meta.keyId} was approved for ${String(meta.network)}, not ${ctx.network}`, {
|
|
198
|
+
remediation: `Register a ${ctx.network} key: ${ONDO_APPROVE_REMEDIATION}${ctx.network === "testnet" ? " -n testnet" : ""}`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
credentials = loadOndoCredentials(meta.keyId);
|
|
202
|
+
}
|
|
203
|
+
this.client = new OndoClient({
|
|
204
|
+
restUrl: networkConfig(def, ctx.network).restUrl,
|
|
205
|
+
network: ctx.network,
|
|
206
|
+
rateLimit: def.rateLimit,
|
|
207
|
+
credentials,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/** Raw REST client (same host + HMAC key) — shared with OndoSpotAdapter. */
|
|
211
|
+
get restClient() {
|
|
212
|
+
return this.client;
|
|
213
|
+
}
|
|
214
|
+
/** Account id from GET /v1/account — the `from.id`/`to.id` of withdrawals and spot↔perp transfers. */
|
|
215
|
+
async accountId() {
|
|
216
|
+
const account = await this.client.get("/v1/account", null);
|
|
217
|
+
return account.accountID;
|
|
218
|
+
}
|
|
219
|
+
// ── Helpers ──
|
|
220
|
+
async pairOf(symbol) {
|
|
221
|
+
const market = ondoVenueSymbol(symbol);
|
|
222
|
+
if (this.pairsCache === null || Date.now() - this.pairsCache.at > PAIRS_TTL_MS) {
|
|
223
|
+
const pairs = await fetchOndoTradingPairs(this.client);
|
|
224
|
+
this.pairsCache = { at: Date.now(), byMarket: new Map(pairs.map(p => [p.market, p])) };
|
|
225
|
+
}
|
|
226
|
+
const pair = this.pairsCache.byMarket.get(market);
|
|
227
|
+
if (pair === undefined) {
|
|
228
|
+
throw new PerpError("SYMBOL_NOT_FOUND", `Ondo Perps has no market ${market}`, {
|
|
229
|
+
remediation: "List markets with: perp market list -e ondo",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return pair;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* An order the venue returned as "pending" (not yet open / filled) is read back by id
|
|
236
|
+
* (GET /v1/perps/orders/{id}, bounded 5 × 500 ms) until the venue reports a final status;
|
|
237
|
+
* still pending → the result stays pending (never assumed open).
|
|
238
|
+
*/
|
|
239
|
+
async settlePending(r) {
|
|
240
|
+
if (r.finality === "final" || r.orderId === null)
|
|
241
|
+
return r;
|
|
242
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
243
|
+
await new Promise(res => setTimeout(res, 500));
|
|
244
|
+
let order;
|
|
245
|
+
try {
|
|
246
|
+
order = await this.client.get(refPath({ kind: "orderId", value: r.orderId }), null);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
continue; // read retried; still unknown after the loop → pending
|
|
250
|
+
}
|
|
251
|
+
const read = this.toOrderResult(order, order, r.execution);
|
|
252
|
+
if (read.finality === "final")
|
|
253
|
+
return { ...read, venueRaw: { ack: r.venueRaw, order } };
|
|
254
|
+
}
|
|
255
|
+
r.execution.notes.push(`order ${r.orderId} still "pending" at the venue after the read-back — verify: perp account orders -e ondo`);
|
|
256
|
+
return r;
|
|
257
|
+
}
|
|
258
|
+
toOrderResult(o, venueRaw, exec) {
|
|
259
|
+
const status = mapStatus(o);
|
|
260
|
+
const filled = isPositive(o.filledSize);
|
|
261
|
+
return {
|
|
262
|
+
venue: "ondo",
|
|
263
|
+
symbol: ondoPlainSymbol(o.market),
|
|
264
|
+
status,
|
|
265
|
+
finality: status === "accepted" ? "pending" : "final",
|
|
266
|
+
orderId: o.orderId,
|
|
267
|
+
clientOrderId: strOrNull(o.clientOrderId),
|
|
268
|
+
filledSize: o.filledSize,
|
|
269
|
+
avgPrice: filled ? formatComputedPrice(Number(o.filledCost) / Number(o.filledSize)) : null,
|
|
270
|
+
execution: exec,
|
|
271
|
+
venueRaw,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
toExchangeOrder(o) {
|
|
275
|
+
return {
|
|
276
|
+
orderId: o.orderId,
|
|
277
|
+
symbol: ondoPlainSymbol(o.market),
|
|
278
|
+
side: o.side,
|
|
279
|
+
price: o.price,
|
|
280
|
+
size: o.size,
|
|
281
|
+
filled: o.filledSize,
|
|
282
|
+
status: o.status,
|
|
283
|
+
type: o.type,
|
|
284
|
+
triggerPrice: o.triggerPrice !== undefined && Number(o.triggerPrice) > 0 ? o.triggerPrice : null,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
async listOrders(query) {
|
|
288
|
+
const out = [];
|
|
289
|
+
let cursor = null;
|
|
290
|
+
do {
|
|
291
|
+
const page = await this.client.getPage("/v1/perps/orders", { ...query, limit: ORDERS_PAGE_LIMIT, cursor });
|
|
292
|
+
out.push(...listOf(page.result));
|
|
293
|
+
cursor = page.nextCursor;
|
|
294
|
+
} while (cursor !== null);
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Resting orders plus untriggered stop orders. The documented `status` filter enum is open /
|
|
299
|
+
* canceled / fullyfilled; `untriggered` is a documented order status and the filter accepts it
|
|
300
|
+
* (live mainnet 2026-10-05) — every returned row's own status is still checked.
|
|
301
|
+
*/
|
|
302
|
+
async listWorkingOrders(market) {
|
|
303
|
+
const open = await this.listOrders({ status: "open", market });
|
|
304
|
+
const untriggered = (await this.listOrders({ status: "untriggered", market })).filter(o => o.status === "untriggered");
|
|
305
|
+
return [...open, ...untriggered];
|
|
306
|
+
}
|
|
307
|
+
async markPriceOf(market) {
|
|
308
|
+
const marks = await this.client.publicGet("/v1/perps/mark_prices", null);
|
|
309
|
+
const m = marks[market];
|
|
310
|
+
if (m === undefined)
|
|
311
|
+
throw new PerpError("PRICE_STALE", `Ondo Perps: no mark price for ${market}`);
|
|
312
|
+
return m.markPrice;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* `builderCode: { code, feeRateBpsFractional }` for the next order, recorded on `exec`
|
|
316
|
+
* (builderFee + the reason a configured fee was not attached). Every order body carries it
|
|
317
|
+
* explicitly — position stops do not inherit it (Ondo integration guide).
|
|
318
|
+
*/
|
|
319
|
+
async attachBuilderCode(body, exec) {
|
|
320
|
+
const gate = builderGateOf(this);
|
|
321
|
+
const d = gate === null ? NO_BUILDER : await gate.decide({ hip3: false });
|
|
322
|
+
exec.builderFee = d.applied;
|
|
323
|
+
if (d.note !== null)
|
|
324
|
+
exec.notes.push(d.note);
|
|
325
|
+
if (d.applied !== null)
|
|
326
|
+
body.builderCode = { code: d.applied.identity, feeRateBpsFractional: requireFeeBps(d.applied) };
|
|
327
|
+
}
|
|
328
|
+
/** stopMarket → position-level stop (POST /v1/perps/stop_order). */
|
|
329
|
+
async placePositionStop(req, triggerPrice) {
|
|
330
|
+
if (!req.reduceOnly) {
|
|
331
|
+
throw unsupported(this.venue.displayName, "openingStop", "stop orders are position-level and always reduce-only", "Pass --reduce-only for stop orders on ondo");
|
|
332
|
+
}
|
|
333
|
+
if (req.clientOrderId !== null) {
|
|
334
|
+
throw unsupported(this.venue.displayName, "stopClientOrderId", "position stops do not take a client order id", "Omit the client order id for stop orders");
|
|
335
|
+
}
|
|
336
|
+
if (req.tpsl !== null)
|
|
337
|
+
throw new PerpError("INVALID_PARAMS", "tpsl cannot be attached to a stop order");
|
|
338
|
+
if (req.triggerSource !== null && req.triggerSource !== "mark") {
|
|
339
|
+
throw unsupported(this.venue.displayName, "triggerSource", `stops trigger on the mark price only (got ${req.triggerSource})`, "Use --trigger-type mark or omit it");
|
|
340
|
+
}
|
|
341
|
+
const pair = await this.pairOf(req.symbol);
|
|
342
|
+
const trigger = requireOnTick(triggerPrice, pair.quoteIncrement);
|
|
343
|
+
const quantity = floorSize(req.size, pair.baseIncrement);
|
|
344
|
+
// A sell stop closes a long; a buy stop closes a short.
|
|
345
|
+
const positionDirection = req.side === "sell" ? "long" : "short";
|
|
346
|
+
const mark = Number(await this.markPriceOf(pair.market));
|
|
347
|
+
const t = Number(trigger);
|
|
348
|
+
if (t === mark) {
|
|
349
|
+
throw new PerpError("INVALID_PARAMS", `Trigger ${trigger} equals the current mark price; cannot tell stop-loss from take-profit`);
|
|
350
|
+
}
|
|
351
|
+
const lossSide = positionDirection === "long" ? t < mark : t > mark;
|
|
352
|
+
const type = lossSide ? "stopLoss" : "takeProfit";
|
|
353
|
+
const exec = execution("trigger", {
|
|
354
|
+
referencePrice: trigger,
|
|
355
|
+
notes: [`position-level ${type} on the mark price; executes as a venue market order (no protection price)`],
|
|
356
|
+
});
|
|
357
|
+
const stopBody = { market: pair.market, positionDirection, type, triggerPrice: trigger, quantity };
|
|
358
|
+
await this.attachBuilderCode(stopBody, exec);
|
|
359
|
+
// Order type per kind, live mainnet 2026-10-05: stopLoss → "stopMarket", takeProfit → "takeProfitMarket".
|
|
360
|
+
const orderType = type === "stopLoss" ? "stopMarket" : "takeProfitMarket";
|
|
361
|
+
const sameStop = (o) => o.status === "untriggered" && o.type === orderType && o.stopOrderType === type
|
|
362
|
+
&& o.triggerPrice !== undefined && Number(o.triggerPrice) === t && subDecimal(o.size, quantity) === "0";
|
|
363
|
+
// The stop endpoint returns no order id ({market, positionDirection, stopLoss?, takeProfit?}),
|
|
364
|
+
// so the order ids already listed BEFORE the POST are excluded afterwards — an older identical
|
|
365
|
+
// stop is never reported as the new one. Pre-send: a failed read sends nothing.
|
|
366
|
+
let before;
|
|
367
|
+
try {
|
|
368
|
+
const prior = await this.client.getPage("/v1/perps/orders", { market: pair.market, limit: ORDERS_PAGE_LIMIT });
|
|
369
|
+
before = new Set(listOf(prior.result).map(o => o.orderId));
|
|
370
|
+
}
|
|
371
|
+
catch (e) {
|
|
372
|
+
throw orderNotSent("ondo", `${type} ${req.side} ${quantity} ${pair.market} @ ${trigger}`, e);
|
|
373
|
+
}
|
|
374
|
+
const stopRaw = await this.client.post("/v1/perps/stop_order", stopBody);
|
|
375
|
+
// Locate the resulting untriggered order. A stop set with `quantity` is a PARTIAL stop
|
|
376
|
+
// (set-stop-order: "Sets a partial stop order for this quantity of the position instead of the
|
|
377
|
+
// full position"), so it carries `size`, not closePosition (live mainnet 2026-10-05:
|
|
378
|
+
// {type:"stopMarket", status:"untriggered", size:"0.005"}). The POST succeeded: a failed read
|
|
379
|
+
// or no / several new candidates leave the stop's state unknown — ORDER_PENDING, never resubmit.
|
|
380
|
+
const pendingRemediation = "Do NOT resubmit — the stop may be set. Check with: perp account orders -e ondo";
|
|
381
|
+
let recent;
|
|
382
|
+
try {
|
|
383
|
+
recent = await this.client.getPage("/v1/perps/orders", { market: pair.market, limit: ORDERS_PAGE_LIMIT });
|
|
384
|
+
}
|
|
385
|
+
catch (e) {
|
|
386
|
+
throw orderOutcomeUnknown("ondo", `${type} at ${trigger}: accepted, but the order list could not be read`, e, pendingRemediation);
|
|
387
|
+
}
|
|
388
|
+
const fresh = listOf(recent.result).filter(o => !before.has(o.orderId) && sameStop(o));
|
|
389
|
+
if (fresh.length !== 1) {
|
|
390
|
+
throw new PerpError("ORDER_PENDING", `Ondo Perps accepted the ${type} at ${trigger} but ${fresh.length === 0 ? "the new order is not visible in the order list" : `${fresh.length} new matching orders are listed (${fresh.map(o => o.orderId).join(", ")})`}`, {
|
|
391
|
+
exchange: "ondo", stopOrders: stopRaw, remediation: pendingRemediation,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const order = fresh[0];
|
|
395
|
+
return this.toOrderResult(order, subDecimal(req.size, quantity) === "0"
|
|
396
|
+
? { stopOrder: stopRaw, order }
|
|
397
|
+
: { stopOrder: stopRaw, order, requestedSize: req.size, placedSize: quantity }, exec);
|
|
398
|
+
}
|
|
399
|
+
// ── TradingV2 ──
|
|
400
|
+
async getVenueMarkets() {
|
|
401
|
+
return ondoPublicData.fetchMarkets(this.network);
|
|
402
|
+
}
|
|
403
|
+
async placeOrder(input) {
|
|
404
|
+
try {
|
|
405
|
+
return await this.placeOrderOnVenue(input);
|
|
406
|
+
}
|
|
407
|
+
catch (e) {
|
|
408
|
+
// A builder-related rejection drops the cached approval and says how to proceed; never retried.
|
|
409
|
+
throw builderRejectionOf(this, e);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async placeOrderOnVenue(input) {
|
|
413
|
+
// §13.3: GTC is the uniform default for a limit order without a TIF.
|
|
414
|
+
const tifRequested = input.tif;
|
|
415
|
+
const req = input.type === "limit" && input.tif === null ? { ...input, tif: "GTC" } : input;
|
|
416
|
+
if (req.tif === "FOK") {
|
|
417
|
+
throw unsupported(this.venue.displayName, "fok", "fill-or-kill is not offered and is never emulated (a partial IOC fill cannot be undone)", "Use --tif IOC (partial fills possible) or GTC / ALO");
|
|
418
|
+
}
|
|
419
|
+
validateOrderRequest(this.venue, this.caps, req);
|
|
420
|
+
if (req.type === "stopLimit") {
|
|
421
|
+
throw unsupported(this.venue.displayName, "stopLimit", "the venue offers position stops (market) only", "Use a stop-market (position stop) instead");
|
|
422
|
+
}
|
|
423
|
+
if (req.type === "stopMarket") {
|
|
424
|
+
if (req.triggerPrice === null)
|
|
425
|
+
throw new PerpError("INVALID_PARAMS", "stopMarket order requires a trigger price");
|
|
426
|
+
return this.placePositionStop(req, req.triggerPrice);
|
|
427
|
+
}
|
|
428
|
+
const pair = await this.pairOf(req.symbol);
|
|
429
|
+
const size = floorSize(req.size, pair.baseIncrement);
|
|
430
|
+
const body = {
|
|
431
|
+
market: pair.market,
|
|
432
|
+
side: req.side,
|
|
433
|
+
size,
|
|
434
|
+
reduceOnly: req.reduceOnly,
|
|
435
|
+
};
|
|
436
|
+
let exec;
|
|
437
|
+
if (req.type === "limit") {
|
|
438
|
+
if (req.price === null || req.tif === null)
|
|
439
|
+
throw new PerpError("INVALID_PARAMS", "limit order requires price and tif");
|
|
440
|
+
const w = wireTif(req.tif);
|
|
441
|
+
body.type = "limit";
|
|
442
|
+
body.price = requireOnTick(req.price, pair.quoteIncrement);
|
|
443
|
+
body.timeInForce = w.timeInForce;
|
|
444
|
+
if (w.postOnly)
|
|
445
|
+
body.postOnly = true;
|
|
446
|
+
// The API never returns postOnly: encode it in a generated client id so amend can recover it.
|
|
447
|
+
if (req.clientOrderId === null)
|
|
448
|
+
body.clientOrderId = ondoClientOrderId(w.postOnly);
|
|
449
|
+
exec = execution("limit", {
|
|
450
|
+
tifRequested, tifSent: w.label,
|
|
451
|
+
notes: tifRequested === null ? ["no time-in-force given; GTC (uniform default)"] : [],
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
// validateOrderRequest guarantees the resolved bound for market orders.
|
|
456
|
+
const slippage = req.slippage;
|
|
457
|
+
// Slippage-bounded market: IOC limit at the protection price (see header).
|
|
458
|
+
const mark = await this.markPriceOf(pair.market);
|
|
459
|
+
const protectionPrice = alignProtectionPrice(slippagePrice(mark, req.side, slippage.bps), pair.quoteIncrement, req.side);
|
|
460
|
+
body.type = "limit";
|
|
461
|
+
body.price = protectionPrice;
|
|
462
|
+
body.timeInForce = "IOC";
|
|
463
|
+
const appliedBps = Math.abs(Number(protectionPrice) - Number(mark)) / Number(mark) * 10_000;
|
|
464
|
+
exec = execution("protectedIoc", {
|
|
465
|
+
slippageBps: Number(appliedBps.toFixed(4)), slippageSource: slippage.source,
|
|
466
|
+
protectionPrice, referencePrice: mark, tifSent: "IOC",
|
|
467
|
+
notes: [`resolved bound ${slippage.bps} bps (${slippage.source}); protection price aligned to tick ${pair.quoteIncrement} on the mark price`],
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (req.clientOrderId !== null) {
|
|
471
|
+
if (!ID_RE.test(req.clientOrderId)) {
|
|
472
|
+
throw new PerpError("INVALID_PARAMS", `clientOrderId "${req.clientOrderId}" must be 1-64 chars of A-Z a-z 0-9 _ -`);
|
|
473
|
+
}
|
|
474
|
+
// A caller id in the generated format must not misstate post-only (amend trusts it).
|
|
475
|
+
const flag = postOnlyOf(req.clientOrderId);
|
|
476
|
+
if (flag !== null && flag !== (body.postOnly === true)) {
|
|
477
|
+
throw new PerpError("INVALID_PARAMS", `clientOrderId "${req.clientOrderId}" uses the reserved pcli-${flag ? "p" : "n"}- prefix but the order is ${body.postOnly === true ? "" : "not "}post-only`);
|
|
478
|
+
}
|
|
479
|
+
body.clientOrderId = req.clientOrderId;
|
|
480
|
+
}
|
|
481
|
+
if (req.tpsl !== null) {
|
|
482
|
+
if (req.tpsl.tp !== null)
|
|
483
|
+
body.takeProfit = { triggerPrice: requireOnTick(req.tpsl.tp, pair.quoteIncrement) };
|
|
484
|
+
if (req.tpsl.sl !== null)
|
|
485
|
+
body.stopLoss = { triggerPrice: requireOnTick(req.tpsl.sl, pair.quoteIncrement) };
|
|
486
|
+
exec.notes.push("TP/SL attached to the entry (venue takeProfit/stopLoss)");
|
|
487
|
+
}
|
|
488
|
+
await this.attachBuilderCode(body, exec);
|
|
489
|
+
const order = await this.client.post("/v1/perps/orders", body);
|
|
490
|
+
// Size flooring is surfaced, never silent (L2).
|
|
491
|
+
return this.settlePending(this.toOrderResult(order, subDecimal(req.size, size) === "0" ? order : { requestedSize: req.size, placedSize: size, response: order }, exec));
|
|
492
|
+
}
|
|
493
|
+
async amendOrder(req) {
|
|
494
|
+
const pair = await this.pairOf(req.symbol);
|
|
495
|
+
const original = await this.client.get(refPath(req.ref), null);
|
|
496
|
+
if (original.market !== pair.market) {
|
|
497
|
+
throw new PerpError("INVALID_PARAMS", `Order ${req.ref.value} is on ${original.market}, not ${pair.market}`);
|
|
498
|
+
}
|
|
499
|
+
if (original.type !== "limit" || original.status !== "open") {
|
|
500
|
+
throw unsupported(this.venue.displayName, "amend", `only open limit orders can be amended (order is ${original.type}/${original.status})`, `Cancel it (perp trade cancel ${req.symbol} ${original.orderId} -e ondo) and place a new order`);
|
|
501
|
+
}
|
|
502
|
+
if (original.timeInForce === undefined) {
|
|
503
|
+
throw new PerpError("EXCHANGE_ERROR", `Ondo Perps returned no timeInForce for limit order ${original.orderId}`);
|
|
504
|
+
}
|
|
505
|
+
// Replacement TIF: explicit caller TIF, else the original's (post-only from the generated client id).
|
|
506
|
+
const postOnly = postOnlyOf(original.clientOrderId);
|
|
507
|
+
let tif;
|
|
508
|
+
if (req.tif !== null) {
|
|
509
|
+
if (req.tif === "FOK") {
|
|
510
|
+
throw unsupported(this.venue.displayName, "fok", "fill-or-kill is not offered and is never emulated", "Use --tif GTC, IOC or ALO");
|
|
511
|
+
}
|
|
512
|
+
tif = validateLegacyTif(this.venue, this.caps, req.tif);
|
|
513
|
+
if (postOnly !== null && (tif === "ALO") !== postOnly && req.forceTif !== true) {
|
|
514
|
+
throw new PerpError("INVALID_PARAMS", `--tif ${tif} would make order ${original.orderId} ${postOnly ? "no longer post-only" : "post-only"} (its client id encodes ${postOnly ? "" : "not "}post-only)`, {
|
|
515
|
+
remediation: `Omit --tif to keep the order's own TIF, or pass --force-tif to change it`,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
else if (postOnly === null) {
|
|
520
|
+
throw unsupported(this.venue.displayName, "amend", "original order attributes unknown", `pass --tif: perp trade edit ${req.symbol} ${original.orderId} <price> <size> --tif <GTC|IOC|ALO> -e ondo (ALO keeps post-only)`);
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
tif = postOnly ? "ALO" : original.timeInForce;
|
|
524
|
+
}
|
|
525
|
+
const w = wireTif(tif);
|
|
526
|
+
// The replacement keeps reduceOnly: a reduce-only limit must be one of caps.reduceOnlyTif (IOC
|
|
527
|
+
// — see header), so a reduce-only replacement at another TIF would be refused only AFTER the
|
|
528
|
+
// original is cancelled. Refuse up front; nothing is cancelled.
|
|
529
|
+
if (original.reduceOnly === true && this.caps.reduceOnlyTif !== null && !this.caps.reduceOnlyTif.includes(tif)) {
|
|
530
|
+
throw unsupported(this.venue.displayName, "amend", `order ${original.orderId} is reduce-only and a reduce-only limit can only be ${this.caps.reduceOnlyTif.join(" / ")} (replacement TIF ${tif}); nothing was cancelled`, `Cancel it (perp trade cancel ${req.symbol} ${original.orderId} -e ondo) and place a new order`);
|
|
531
|
+
}
|
|
532
|
+
// Validate the replacement BEFORE cancelling so a bad price/size never loses the original.
|
|
533
|
+
const price = requireOnTick(req.price, pair.quoteIncrement);
|
|
534
|
+
floorSize(req.size, pair.baseIncrement);
|
|
535
|
+
const filledAtRead = original.filledSize;
|
|
536
|
+
let cancelled;
|
|
537
|
+
try {
|
|
538
|
+
cancelled = await this.client.delete(`/v1/perps/orders/${original.orderId}`, null);
|
|
539
|
+
}
|
|
540
|
+
catch (e) {
|
|
541
|
+
// A venue answer means the cancel was refused (original untouched); anything else is unknown.
|
|
542
|
+
if (cancelDefinitelyRefused(e))
|
|
543
|
+
throw e;
|
|
544
|
+
throw orderOutcomeUnknown("ondo", `amend: cancel of ${original.orderId}`, e, `No replacement was placed. Check the original: perp trade status ${original.orderId} --symbol ${req.symbol} -e ondo`);
|
|
545
|
+
}
|
|
546
|
+
const notes = [
|
|
547
|
+
req.tif !== null ? `TIF ${tif} (from --tif)` : `TIF ${tif} kept (post-only flag from the order's generated client id)`,
|
|
548
|
+
`cancel→replace: filledAtRead ${filledAtRead}, filledAtCancel ${cancelled.filledSize}`,
|
|
549
|
+
];
|
|
550
|
+
const amendExec = (extra, sent) => execution("cancelReplace", { tifRequested: req.tif, tifSent: sent, notes: [...notes, ...extra] });
|
|
551
|
+
if (cancelled.status === "fullyfilled") {
|
|
552
|
+
return this.toOrderResult(cancelled, { cancelled, placed: null }, amendExec([`order ${original.orderId} filled completely before the cancel — no replacement placed`], null));
|
|
553
|
+
}
|
|
554
|
+
if (cancelled.status !== "canceled") {
|
|
555
|
+
throw new PerpError("ORDER_PENDING", `Ondo Perps amend: cancel of ${original.orderId} returned status "${cancelled.status}", not "canceled"; no replacement was placed`, {
|
|
556
|
+
exchange: "ondo",
|
|
557
|
+
cancelled,
|
|
558
|
+
remediation: `Do NOT resubmit. Check the order: perp trade status ${original.orderId} --symbol ${req.symbol} -e ondo`,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
// req.size = new OPEN size; fills that raced the cancel reduce the replacement (§13.10 #10).
|
|
562
|
+
const racedFill = subDecimal(cancelled.filledSize, filledAtRead);
|
|
563
|
+
const replace = subDecimal(req.size, racedFill.startsWith("-") ? "0" : racedFill);
|
|
564
|
+
let size = null;
|
|
565
|
+
if (!replace.startsWith("-") && isPositive(replace)) {
|
|
566
|
+
try {
|
|
567
|
+
size = floorSize(replace, pair.baseIncrement);
|
|
568
|
+
}
|
|
569
|
+
catch (e) {
|
|
570
|
+
if (!(e instanceof PerpError) || e.structured.code !== "SIZE_TOO_SMALL")
|
|
571
|
+
throw e;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (size === null) {
|
|
575
|
+
return this.toOrderResult(cancelled, { cancelled, placed: null }, amendExec([`${racedFill} filled between read and cancel; replacement ${replace} is not above the step ${pair.baseIncrement} — no replacement placed`], null));
|
|
576
|
+
}
|
|
577
|
+
const body = {
|
|
578
|
+
market: pair.market,
|
|
579
|
+
side: original.side,
|
|
580
|
+
type: "limit",
|
|
581
|
+
price,
|
|
582
|
+
size,
|
|
583
|
+
reduceOnly: original.reduceOnly === true,
|
|
584
|
+
timeInForce: w.timeInForce,
|
|
585
|
+
clientOrderId: ondoClientOrderId(w.postOnly),
|
|
586
|
+
};
|
|
587
|
+
if (w.postOnly)
|
|
588
|
+
body.postOnly = true;
|
|
589
|
+
const replaceExec = amendExec([], w.label);
|
|
590
|
+
await this.attachBuilderCode(body, replaceExec);
|
|
591
|
+
let placed;
|
|
592
|
+
try {
|
|
593
|
+
placed = await this.client.post("/v1/perps/orders", body);
|
|
594
|
+
}
|
|
595
|
+
catch (e) {
|
|
596
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
597
|
+
if (placementDefinitelyRefused(e)) {
|
|
598
|
+
throw new PerpError("ORDER_PARTIAL", `Ondo Perps amend: original order ${original.orderId} was CANCELLED but the replacement was refused: ${msg}`, {
|
|
599
|
+
exchange: "ondo",
|
|
600
|
+
cancelled,
|
|
601
|
+
placed: null,
|
|
602
|
+
remediation: "The original order is gone; place a new order manually",
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
throw new PerpError("ORDER_PENDING", `Ondo Perps amend: original order ${original.orderId} was CANCELLED; the replacement outcome is unknown: ${msg}`, {
|
|
606
|
+
exchange: "ondo",
|
|
607
|
+
cancelled,
|
|
608
|
+
replacementClientOrderId: body.clientOrderId,
|
|
609
|
+
remediation: `Do NOT resubmit. Check: perp trade status c:${String(body.clientOrderId)} --symbol ${req.symbol} -e ondo`,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
const extra = [
|
|
613
|
+
`filledBeforeCancel ${cancelled.filledSize}, replacedSize ${size}`,
|
|
614
|
+
...(subDecimal(req.size, size) === "0" ? [] : [`replacement size ${size} (requested open size ${req.size})`]),
|
|
615
|
+
];
|
|
616
|
+
replaceExec.notes.push(...extra);
|
|
617
|
+
return this.settlePending(this.toOrderResult(placed, { cancelled, placed, requestedSize: req.size, placedSize: size }, replaceExec));
|
|
618
|
+
}
|
|
619
|
+
async cancelByRef(_symbol, ref) {
|
|
620
|
+
const order = await this.client.delete(refPath(ref), null);
|
|
621
|
+
return { venue: "ondo", cancelled: [ref], failed: [], venueRaw: order };
|
|
622
|
+
}
|
|
623
|
+
/** Lists resting orders and untriggered stops (listWorkingOrders), then cancels them via the batch endpoint so every result is exact. */
|
|
624
|
+
async cancelAllV2(symbol) {
|
|
625
|
+
const open = await this.listWorkingOrders(symbol === null ? null : ondoVenueSymbol(symbol));
|
|
626
|
+
const result = { venue: "ondo", cancelled: [], failed: [], venueRaw: [] };
|
|
627
|
+
if (this.caps.batchPlaceMax === null)
|
|
628
|
+
throw new Error("ondo capabilities.batchPlaceMax must be set");
|
|
629
|
+
const chunk = this.caps.batchPlaceMax;
|
|
630
|
+
const raws = [];
|
|
631
|
+
for (let i = 0; i < open.length; i += chunk) {
|
|
632
|
+
const ids = open.slice(i, i + chunk).map(o => o.orderId);
|
|
633
|
+
const r = await this.client.delete("/v1/perps/orders/batch", { orderIDs: ids.join(",") });
|
|
634
|
+
raws.push(r);
|
|
635
|
+
for (const o of listOf(r.successfulCancels))
|
|
636
|
+
result.cancelled.push({ kind: "orderId", value: o.orderId });
|
|
637
|
+
for (const f of listOf(r.failedCancels)) {
|
|
638
|
+
result.failed.push({ ref: { kind: "orderId", value: f.orderId }, error: JSON.stringify(f) });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
result.venueRaw = { batches: raws };
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
644
|
+
async applyLeverage(symbol, leverage, marginMode) {
|
|
645
|
+
if (marginMode === "isolated") {
|
|
646
|
+
throw unsupported(this.venue.displayName, "isolatedMargin", "accounts are cross-margin only", "Omit --isolated (ondo accounts are cross-margin)");
|
|
647
|
+
}
|
|
648
|
+
if (!Number.isInteger(leverage) || leverage < 1) {
|
|
649
|
+
throw new PerpError("INVALID_PARAMS", `Ondo Perps leverage must be a positive integer (got ${leverage})`);
|
|
650
|
+
}
|
|
651
|
+
const market = ondoVenueSymbol(symbol);
|
|
652
|
+
await this.client.postNoResult("/v1/perps/leverage", { market, leverage: String(leverage) });
|
|
653
|
+
return { venue: "ondo", symbol: ondoPlainSymbol(market), leverage, marginMode: "cross", venueRaw: null };
|
|
654
|
+
}
|
|
655
|
+
async getOrderStatus(_symbol, ref) {
|
|
656
|
+
const order = await this.client.get(refPath(ref), null);
|
|
657
|
+
return this.toOrderResult(order, order, readExecution(order));
|
|
658
|
+
}
|
|
659
|
+
// ── Legacy ExchangeAdapter: market data ──
|
|
660
|
+
async getMarkets() {
|
|
661
|
+
return (await this.getVenueMarkets()).map(toLegacyMarketInfo);
|
|
662
|
+
}
|
|
663
|
+
async getOrderbook(symbol) {
|
|
664
|
+
const book = await this.client.publicGet("/v1/perps/depth", { market: ondoVenueSymbol(symbol), depth: DEPTH_LEVELS });
|
|
665
|
+
const lvl = (l) => [l[0], l[1]];
|
|
666
|
+
return { bids: book.bids.map(lvl), asks: book.asks.map(lvl) };
|
|
667
|
+
}
|
|
668
|
+
async getRecentTrades(symbol, limit) {
|
|
669
|
+
const trades = await this.client.publicGet("/v1/perps/trades", {
|
|
670
|
+
market: ondoVenueSymbol(symbol), limit: limit === undefined ? null : limit,
|
|
671
|
+
});
|
|
672
|
+
return listOf(trades).map(t => ({
|
|
673
|
+
time: parseOndoTime(t.time, "trade"),
|
|
674
|
+
symbol: ondoPlainSymbol(t.market),
|
|
675
|
+
side: t.aggressor_side,
|
|
676
|
+
price: t.price,
|
|
677
|
+
size: t.size,
|
|
678
|
+
fee: "0", // public tape: no fee applies to the viewer
|
|
679
|
+
}));
|
|
680
|
+
}
|
|
681
|
+
async getFundingHistory(symbol, limit) {
|
|
682
|
+
const rows = await this.client.publicGet("/v1/perps/funding_rate_history", {
|
|
683
|
+
market: ondoVenueSymbol(symbol), limit: limit === undefined ? null : limit,
|
|
684
|
+
});
|
|
685
|
+
return listOf(rows).map(r => ({ time: parseOndoTime(r.time, "funding"), rate: r.fundingRate, price: null }));
|
|
686
|
+
}
|
|
687
|
+
/** GET /v1/perps/candles — authenticated (see klinesRequireAuth). */
|
|
688
|
+
async getKlines(symbol, interval, startTime, endTime) {
|
|
689
|
+
const candles = await this.client.get("/v1/perps/candles", {
|
|
690
|
+
market: ondoVenueSymbol(symbol),
|
|
691
|
+
resolution: toResolution(interval),
|
|
692
|
+
from: Math.floor(startTime / 1000),
|
|
693
|
+
to: Math.floor(endTime / 1000),
|
|
694
|
+
});
|
|
695
|
+
return listOf(candles).map(c => ({
|
|
696
|
+
time: parseOndoTime(c.startTime, "candle"),
|
|
697
|
+
open: c.open,
|
|
698
|
+
high: c.high,
|
|
699
|
+
low: c.low,
|
|
700
|
+
close: c.close,
|
|
701
|
+
volume: c.volume,
|
|
702
|
+
trades: null, // Ondo publishes no per-candle trade count
|
|
703
|
+
}));
|
|
704
|
+
}
|
|
705
|
+
// ── Legacy ExchangeAdapter: account ──
|
|
706
|
+
async getBalance() {
|
|
707
|
+
const b = await this.client.get("/v1/perps/balance", null);
|
|
708
|
+
return { equity: b.marginBalance, available: b.availableMargin, marginUsed: b.usedMargin, unrealizedPnl: b.unrealizedPnl };
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* netQuantity is documented as the unsigned position size ("Size of the
|
|
712
|
+
* position in base currency", example "1.5489" on a short) with the side in
|
|
713
|
+
* `direction`. A signed value is still accepted: the magnitude is used and
|
|
714
|
+
* the sign must agree with `direction` (never dropped, never guessed).
|
|
715
|
+
*/
|
|
716
|
+
async getPositions() {
|
|
717
|
+
const rows = await this.client.get("/v1/perps/positions", null);
|
|
718
|
+
const out = [];
|
|
719
|
+
for (const p of listOf(rows)) {
|
|
720
|
+
const negative = p.netQuantity.startsWith("-");
|
|
721
|
+
const size = negative ? p.netQuantity.slice(1) : p.netQuantity;
|
|
722
|
+
if (p.direction === "neutral" || !isPositive(size))
|
|
723
|
+
continue;
|
|
724
|
+
if (negative && p.direction !== "short") {
|
|
725
|
+
throw new PerpError("EXCHANGE_ERROR", `Ondo Perps position ${p.market}: netQuantity ${p.netQuantity} contradicts direction ${p.direction}`);
|
|
726
|
+
}
|
|
727
|
+
out.push({
|
|
728
|
+
symbol: ondoPlainSymbol(p.market),
|
|
729
|
+
side: p.direction === "long" ? "long" : "short",
|
|
730
|
+
size,
|
|
731
|
+
entryPrice: p.averageEntryPrice,
|
|
732
|
+
markPrice: p.markPrice,
|
|
733
|
+
liquidationPrice: strOrNull(p.liquidationPrice),
|
|
734
|
+
unrealizedPnl: p.unrealizedPnl,
|
|
735
|
+
leverage: Number(p.leverage),
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
return out;
|
|
739
|
+
}
|
|
740
|
+
async getOpenOrders() {
|
|
741
|
+
return (await this.listWorkingOrders(null)).map(o => this.toExchangeOrder(o));
|
|
742
|
+
}
|
|
743
|
+
async getOrderHistory(limit) {
|
|
744
|
+
const rows = await this.client.get("/v1/perps/orders", { limit: limit === undefined ? null : limit });
|
|
745
|
+
return listOf(rows).map(o => this.toExchangeOrder(o));
|
|
746
|
+
}
|
|
747
|
+
async getTradeHistory(limit) {
|
|
748
|
+
const fills = await this.client.get("/v1/perps/fills", { limit: limit === undefined ? null : limit });
|
|
749
|
+
return listOf(fills).map(f => ({
|
|
750
|
+
time: parseOndoTime(f.time, "fill"),
|
|
751
|
+
symbol: ondoPlainSymbol(f.market),
|
|
752
|
+
side: f.side,
|
|
753
|
+
price: f.price,
|
|
754
|
+
size: f.size,
|
|
755
|
+
fee: f.fee,
|
|
756
|
+
}));
|
|
757
|
+
}
|
|
758
|
+
/** `amount` is positive when the account received funding. */
|
|
759
|
+
async getFundingPayments(limit) {
|
|
760
|
+
const rows = await this.client.get("/v1/perps/funding_fees", { limit: limit === undefined ? null : limit });
|
|
761
|
+
return listOf(rows).map(r => ({ time: parseOndoTime(r.time, "funding fee"), symbol: ondoPlainSymbol(r.market), payment: r.amount }));
|
|
762
|
+
}
|
|
763
|
+
// ── Legacy ExchangeAdapter: trading (shim → TradingV2, design §10.7) ──
|
|
764
|
+
async marketOrder(symbol, side, size, opts) {
|
|
765
|
+
return this.placeOrder({
|
|
766
|
+
symbol, side, type: "market", size, price: null, triggerPrice: null, triggerSource: null, tif: null,
|
|
767
|
+
reduceOnly: opts.reduceOnly, clientOrderId: null, slippage: opts.slippage, tpsl: null,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
async limitOrder(symbol, side, price, size, opts) {
|
|
771
|
+
// §13.3: no TIF → GTC (uniform default); FOK → uniform CAPABILITY_UNSUPPORTED.
|
|
772
|
+
const tifInput = opts?.tif;
|
|
773
|
+
const tif = tifInput === undefined || tifInput === "" ? null : validateLegacyTif(this.venue, this.caps, tifInput);
|
|
774
|
+
return this.placeOrder({
|
|
775
|
+
symbol, side, type: "limit", size, price, triggerPrice: null, triggerSource: null, tif,
|
|
776
|
+
reduceOnly: opts?.reduceOnly === true, clientOrderId: null, slippage: null, tpsl: null,
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
async editOrder(symbol, orderId, price, size) {
|
|
780
|
+
// tif null → keep the original's TIF (post-only decoded from the generated client id; refused when unknown).
|
|
781
|
+
return this.amendOrder({ symbol, ref: parseOrderRef(orderId), price, size, tif: null });
|
|
782
|
+
}
|
|
783
|
+
async cancelOrder(symbol, orderId) {
|
|
784
|
+
return this.cancelByRef(symbol, parseOrderRef(orderId));
|
|
785
|
+
}
|
|
786
|
+
async cancelAllOrders(symbol) {
|
|
787
|
+
return this.cancelAllV2(symbol === undefined ? null : symbol);
|
|
788
|
+
}
|
|
789
|
+
async setLeverage(symbol, leverage, marginMode) {
|
|
790
|
+
return this.applyLeverage(symbol, leverage, marginMode === undefined ? null : marginMode);
|
|
791
|
+
}
|
|
792
|
+
async stopOrder(symbol, side, size, triggerPrice, opts) {
|
|
793
|
+
if (opts?.limitPrice !== undefined) {
|
|
794
|
+
throw unsupported(this.venue.displayName, "stopLimit", "the venue offers position stops (market) only", "Omit --limit-price; ondo stops execute as market orders");
|
|
795
|
+
}
|
|
796
|
+
return this.placeOrder({
|
|
797
|
+
symbol, side, type: "stopMarket", size, price: null, triggerPrice,
|
|
798
|
+
triggerSource: opts?.triggerType === undefined ? null : opts.triggerType,
|
|
799
|
+
tif: null, reduceOnly: opts?.reduceOnly === true, clientOrderId: null, slippage: null, tpsl: null,
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
// ── Withdraw (capabilities.WithdrawCapable) ──
|
|
803
|
+
/**
|
|
804
|
+
* Withdrawals are not available over the API (account owner, 2026-10-13): Ondo pays only
|
|
805
|
+
* address-book destinations and refuses API-key auth for adding one
|
|
806
|
+
* (`key_not_allowed_for_action`). Refused before any request — withdraw in the web app.
|
|
807
|
+
*/
|
|
808
|
+
async withdraw(_amount, _destination) {
|
|
809
|
+
throw unsupported("Ondo Perps", "withdraw", "the Ondo Perps API does not allow withdrawals", "Withdraw from the Ondo Perps web app");
|
|
810
|
+
}
|
|
811
|
+
}
|