perp-cli 0.12.15 → 0.14.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/README.md +278 -31
- 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 +286 -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 +74 -84
- 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 +286 -0
- package/dist/commands/funds.d.ts +25 -0
- package/dist/commands/funds.js +939 -245
- package/dist/commands/history.js +72 -45
- 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 +316 -337
- 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 +219 -6
- package/dist/exchanges/aster.js +1023 -153
- 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 +166 -0
- package/dist/exchanges/ondo/adapter.js +835 -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 +122 -0
- package/dist/exchanges/ondo/funds.js +290 -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 +288 -4
- package/dist/exchanges/pacifica.js +1013 -76
- 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 +1383 -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 +34 -0
- package/dist/exchanges/venues/ondo.js +140 -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 +28 -0
- package/dist/exchanges/venues/qfex.js +132 -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 +157 -682
- package/dist/jobs.d.ts +84 -6
- package/dist/jobs.js +239 -41
- package/dist/landing.d.ts +14 -0
- package/dist/landing.js +36 -0
- package/dist/mcp-server.d.ts +14 -4
- package/dist/mcp-server.js +820 -402
- package/dist/mcp-trade-guard.d.ts +16 -0
- package/dist/mcp-trade-guard.js +59 -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 +338 -134
- package/skills/perp-cli/references/agent-operations.md +181 -92
- package/skills/perp-cli/references/commands.md +475 -132
- package/skills/perp-cli/references/strategies.md +44 -38
- package/skills/perp-cli/scripts/arb-monitor.sh +22 -17
- package/skills/perp-cli/scripts/funding-analysis.sh +20 -10
- 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 +33 -16
- 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,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QFEX venue status / error → PerpError (design §14.3 error table, §14.12 #3, #8, #11).
|
|
3
|
+
*
|
|
4
|
+
* Three sources:
|
|
5
|
+
* - order_response.status engine verdict on one order (definite once correlated);
|
|
6
|
+
* - error frames `{err:{…}}`, `{type:"Err",…}`, `{type:"error",…}` (all documented);
|
|
7
|
+
* - REST problem+json api.qfex.* and banker.qfex.*.
|
|
8
|
+
* Nothing here retries. A RATE_LIMITED on a cancel is surfaced, never re-sent.
|
|
9
|
+
*/
|
|
10
|
+
import { PerpError } from "../../errors.js";
|
|
11
|
+
/** order_response.status values (AsyncAPI OrderResponse.status enum). */
|
|
12
|
+
export const QFEX_ORDER_STATUSES = [
|
|
13
|
+
"ACK", "EXPIRED", "IOC_CANCELLED", "INVALID_PRICE", "INVALID_QUANTITY", "NOT_FOUND", "IOC_PARTIALLY_FILLED",
|
|
14
|
+
"USER_NOT_FOUND", "PERMISSION_DENIED", "FILLED", "MODIFIED", "CANCELLED", "CANCELLED_STP", "REJECTED",
|
|
15
|
+
"NO_SUCH_ORDER", "INVALID_ORDER_TYPE", "BAD_SYMBOL", "PRICE_LESS_THAN_MIN_PRICE", "PRICE_GREATER_THAN_MAX_PRICE",
|
|
16
|
+
"CANNOT_MODIFY_PARTIAL_FILL", "CANNOT_MODIFY_ALO_WOULD_CROSS", "FAILED_MARGIN_CHECK",
|
|
17
|
+
"REJECTED_MUST_CHANGE_VALUE_MODIFY", "NO_CHANGE_NO_UPDATE", "INVALID_TICK_SIZE_PRECISION_PRICE",
|
|
18
|
+
"INVALID_TICK_SIZE_PRECISION_QUANTITY", "QUANTITY_LESS_THAN_MIN_QUANTITY", "QUANTITY_GREATER_THAN_MAX_QUANTITY",
|
|
19
|
+
"REJECTED_GREATER_THAN_MAX_PRICE_BAND", "REJECTED_LESS_THAN_MIN_PRICE_BAND", "INVALID_TIME_IN_FORCE",
|
|
20
|
+
"REJECTED_WOULD_BREACH_MAX_POSITION", "REJECTED_WOULD_BREACH_MAX_NOTIONAL", "CANNOT_MODIFY_NO_SUCH_ORDER",
|
|
21
|
+
"REJECTED_MARKET_CLOSED", "USER_IN_LIQUIDATION", "REJECTED_FAILED_TO_PROCESS", "INVALID_TAKE_PROFIT_PRICE",
|
|
22
|
+
"INVALID_STOP_LOSS_PRICE", "INVALID_CLIENT_ORDER_ID", "RATE_LIMITED", "REJECTED_TOO_MANY_OPEN_ORDERS",
|
|
23
|
+
"REJECTED_OPEN_INTEREST_LIMIT", "REJECTED_TOTAL_OPEN_INTEREST_LIMIT", "REJECTED_LESS_THAN_MIN_NOTIONAL",
|
|
24
|
+
];
|
|
25
|
+
const TERMINAL = new Set(["IOC_PARTIALLY_FILLED", "IOC_CANCELLED", "CANCELLED", "CANCELLED_STP", "EXPIRED"]);
|
|
26
|
+
const KNOWN = new Set(QFEX_ORDER_STATUSES);
|
|
27
|
+
export function qfexStatusClass(status) {
|
|
28
|
+
if (status === "ACK")
|
|
29
|
+
return "ack";
|
|
30
|
+
if (status === "FILLED")
|
|
31
|
+
return "fill";
|
|
32
|
+
if (status === "MODIFIED")
|
|
33
|
+
return "modified";
|
|
34
|
+
if (TERMINAL.has(status))
|
|
35
|
+
return "terminal";
|
|
36
|
+
return KNOWN.has(status) ? "reject" : "unknown";
|
|
37
|
+
}
|
|
38
|
+
const STATUS_CODE = {
|
|
39
|
+
INVALID_PRICE: "INVALID_PARAMS",
|
|
40
|
+
INVALID_QUANTITY: "INVALID_PARAMS",
|
|
41
|
+
INVALID_ORDER_TYPE: "INVALID_PARAMS",
|
|
42
|
+
INVALID_TIME_IN_FORCE: "INVALID_PARAMS",
|
|
43
|
+
INVALID_TAKE_PROFIT_PRICE: "INVALID_PARAMS",
|
|
44
|
+
INVALID_STOP_LOSS_PRICE: "INVALID_PARAMS",
|
|
45
|
+
INVALID_CLIENT_ORDER_ID: "INVALID_PARAMS",
|
|
46
|
+
INVALID_TICK_SIZE_PRECISION_QUANTITY: "INVALID_PARAMS",
|
|
47
|
+
REJECTED_MUST_CHANGE_VALUE_MODIFY: "INVALID_PARAMS",
|
|
48
|
+
NO_CHANGE_NO_UPDATE: "INVALID_PARAMS",
|
|
49
|
+
INVALID_TICK_SIZE_PRECISION_PRICE: "INVALID_TICK",
|
|
50
|
+
PRICE_LESS_THAN_MIN_PRICE: "INVALID_PARAMS",
|
|
51
|
+
PRICE_GREATER_THAN_MAX_PRICE: "INVALID_PARAMS",
|
|
52
|
+
REJECTED_GREATER_THAN_MAX_PRICE_BAND: "INVALID_PARAMS",
|
|
53
|
+
REJECTED_LESS_THAN_MIN_PRICE_BAND: "INVALID_PARAMS",
|
|
54
|
+
QUANTITY_LESS_THAN_MIN_QUANTITY: "SIZE_TOO_SMALL",
|
|
55
|
+
REJECTED_LESS_THAN_MIN_NOTIONAL: "SIZE_TOO_SMALL",
|
|
56
|
+
QUANTITY_GREATER_THAN_MAX_QUANTITY: "SIZE_TOO_LARGE",
|
|
57
|
+
BAD_SYMBOL: "SYMBOL_NOT_FOUND",
|
|
58
|
+
NOT_FOUND: "ORDER_NOT_FOUND",
|
|
59
|
+
NO_SUCH_ORDER: "ORDER_NOT_FOUND",
|
|
60
|
+
CANNOT_MODIFY_NO_SUCH_ORDER: "ORDER_NOT_FOUND",
|
|
61
|
+
CANNOT_MODIFY_PARTIAL_FILL: "ORDER_REJECTED",
|
|
62
|
+
CANNOT_MODIFY_ALO_WOULD_CROSS: "ORDER_REJECTED",
|
|
63
|
+
REJECTED: "ORDER_REJECTED",
|
|
64
|
+
REJECTED_FAILED_TO_PROCESS: "ORDER_REJECTED",
|
|
65
|
+
REJECTED_MARKET_CLOSED: "ORDER_REJECTED",
|
|
66
|
+
FAILED_MARGIN_CHECK: "MARGIN_INSUFFICIENT",
|
|
67
|
+
REJECTED_WOULD_BREACH_MAX_POSITION: "RISK_VIOLATION",
|
|
68
|
+
REJECTED_WOULD_BREACH_MAX_NOTIONAL: "RISK_VIOLATION",
|
|
69
|
+
REJECTED_OPEN_INTEREST_LIMIT: "RISK_VIOLATION",
|
|
70
|
+
REJECTED_TOTAL_OPEN_INTEREST_LIMIT: "RISK_VIOLATION",
|
|
71
|
+
REJECTED_TOO_MANY_OPEN_ORDERS: "RISK_VIOLATION",
|
|
72
|
+
USER_IN_LIQUIDATION: "RISK_VIOLATION",
|
|
73
|
+
PERMISSION_DENIED: "POLICY_DENIED",
|
|
74
|
+
USER_NOT_FOUND: "AGENT_NOT_REGISTERED",
|
|
75
|
+
RATE_LIMITED: "RATE_LIMITED",
|
|
76
|
+
};
|
|
77
|
+
const KEY_PERMISSION_REMEDIATION = "Check the API key's permissions and account scope in QFEX Developer Settings (website, 2FA). The denial can also be KYC, terms or region — perp-cli does not cache it";
|
|
78
|
+
const CODE_REMEDIATION = {
|
|
79
|
+
INVALID_TICK: "Align the price with the market tick: perp market info <symbol> -e qfex",
|
|
80
|
+
SIZE_TOO_SMALL: "Increase the size above the market minimum: perp market info <symbol> -e qfex",
|
|
81
|
+
SIZE_TOO_LARGE: "Reduce the size below the market maximum: perp market info <symbol> -e qfex",
|
|
82
|
+
SYMBOL_NOT_FOUND: "List markets with: perp market list -e qfex",
|
|
83
|
+
MARGIN_INSUFFICIENT: "Reduce size / leverage or deposit more USDC (perp funds deposit qfex)",
|
|
84
|
+
RISK_VIOLATION: "The venue's position / notional / open-interest / open-order limit was hit — reduce the size or close positions",
|
|
85
|
+
POLICY_DENIED: KEY_PERMISSION_REMEDIATION,
|
|
86
|
+
AGENT_NOT_REGISTERED: "The venue does not know this user / account — re-import the key: perp wallet agent import qfex",
|
|
87
|
+
RATE_LIMITED: "Wait for the retry-after hint, then run the command again yourself (perp-cli never re-sends an order or a cancel)",
|
|
88
|
+
};
|
|
89
|
+
const STATUS_REMEDIATION = {
|
|
90
|
+
CANNOT_MODIFY_PARTIAL_FILL: "A partially filled order cannot be modified — cancel it and place a new order",
|
|
91
|
+
CANNOT_MODIFY_ALO_WOULD_CROSS: "The new price would cross the book — choose a passive price, or cancel and place a LIMIT order",
|
|
92
|
+
REJECTED_MARKET_CLOSED: "The market is closed for this order right now — check the session: perp market info <symbol> -e qfex",
|
|
93
|
+
INVALID_CLIENT_ORDER_ID: "The venue refused the generated client order id — report it (docs/TEST-PLAN.md QFEX Q18)",
|
|
94
|
+
};
|
|
95
|
+
/** "… retry after 200ms" / "… retry after 2 seconds" → ms; null when the message carries no hint. */
|
|
96
|
+
export function parseRetryAfterMs(message) {
|
|
97
|
+
if (message === null)
|
|
98
|
+
return null;
|
|
99
|
+
const m = /retry after\s+(\d+(?:\.\d+)?)\s*(ms|milliseconds?|s|sec|seconds?)\b/i.exec(message);
|
|
100
|
+
if (m === null)
|
|
101
|
+
return null;
|
|
102
|
+
const n = Number(m[1]);
|
|
103
|
+
return /^ms|^milli/i.test(m[2]) ? n : n * 1000;
|
|
104
|
+
}
|
|
105
|
+
/** Error for a correlated order_response whose status is an engine rejection. */
|
|
106
|
+
export function qfexOrderStatusError(status, ctx, frame) {
|
|
107
|
+
const code = STATUS_CODE[status] ?? "ORDER_REJECTED";
|
|
108
|
+
const remediation = STATUS_REMEDIATION[status] ?? CODE_REMEDIATION[code];
|
|
109
|
+
const details = { exchange: "qfex", venueStatus: status, venueRaw: frame };
|
|
110
|
+
if (status === "REJECTED_MARKET_CLOSED")
|
|
111
|
+
details.reason = "marketClosed";
|
|
112
|
+
if (ctx.band !== null && /PRICE|BAND/.test(status))
|
|
113
|
+
details.band = ctx.band;
|
|
114
|
+
if (remediation !== undefined)
|
|
115
|
+
details.remediation = remediation;
|
|
116
|
+
return new PerpError(code, `QFEX rejected ${ctx.what}: ${status}`, details);
|
|
117
|
+
}
|
|
118
|
+
// ── Error frames ──
|
|
119
|
+
export const QFEX_ERR_CODES = [
|
|
120
|
+
"RateLimited", "InvalidJSONFormat", "AlreadyAuthenticated", "InvalidParameter", "PermissionDenied",
|
|
121
|
+
"ServerError", "InvalidOrder", "InvalidOrderId", "KycRequired", "TncRequired",
|
|
122
|
+
];
|
|
123
|
+
const ERR_CODE = {
|
|
124
|
+
RateLimited: "RATE_LIMITED",
|
|
125
|
+
InvalidJSONFormat: "INVALID_PARAMS",
|
|
126
|
+
InvalidParameter: "INVALID_PARAMS",
|
|
127
|
+
InvalidOrder: "INVALID_PARAMS",
|
|
128
|
+
InvalidOrderId: "INVALID_PARAMS",
|
|
129
|
+
PermissionDenied: "POLICY_DENIED",
|
|
130
|
+
KycRequired: "POLICY_DENIED",
|
|
131
|
+
TncRequired: "POLICY_DENIED",
|
|
132
|
+
AlreadyAuthenticated: "EXCHANGE_ERROR",
|
|
133
|
+
ServerError: "EXCHANGE_ERROR",
|
|
134
|
+
};
|
|
135
|
+
const ERR_REMEDIATION = {
|
|
136
|
+
KycRequired: "Complete KYC on the QFEX website, then retry",
|
|
137
|
+
TncRequired: "Accept the latest QFEX Terms and Conditions on the website, then retry",
|
|
138
|
+
PermissionDenied: KEY_PERMISSION_REMEDIATION,
|
|
139
|
+
RateLimited: CODE_REMEDIATION.RATE_LIMITED,
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Is an error frame, received after a MUTATING request was sent, proof that the request was
|
|
143
|
+
* NOT applied? Only when the venue echoed that very request AND the code is a refusal.
|
|
144
|
+
* `ServerError` ("safe to retry" in the docs) says nothing about whether the engine saw the
|
|
145
|
+
* order, and an error without an echo — including the documented `RateLimited` with
|
|
146
|
+
* `incoming_message: {}` — cannot be attributed to this request: both are unknown outcomes.
|
|
147
|
+
*/
|
|
148
|
+
export function qfexErrIsDefiniteRefusal(err, echoMatches) {
|
|
149
|
+
if (!echoMatches)
|
|
150
|
+
return false;
|
|
151
|
+
return err.errorCode !== "ServerError" && err.errorCode in ERR_CODE;
|
|
152
|
+
}
|
|
153
|
+
/** PerpError for an error frame that is a definite answer (reads, or a refusal per qfexErrIsDefiniteRefusal). */
|
|
154
|
+
export function qfexErrFrameError(err, what) {
|
|
155
|
+
const code = ERR_CODE[err.errorCode] ?? "EXCHANGE_ERROR";
|
|
156
|
+
const details = { exchange: "qfex", venueCode: err.errorCode };
|
|
157
|
+
const retryAfterMs = err.errorCode === "RateLimited" ? parseRetryAfterMs(err.message) : null;
|
|
158
|
+
if (retryAfterMs !== null)
|
|
159
|
+
details.retryAfterMs = retryAfterMs;
|
|
160
|
+
const remediation = ERR_REMEDIATION[err.errorCode] ?? CODE_REMEDIATION[code];
|
|
161
|
+
if (remediation !== undefined)
|
|
162
|
+
details.remediation = remediation;
|
|
163
|
+
return new PerpError(code, `QFEX ${what}: ${err.errorCode}${err.message !== null ? ` — ${err.message}` : ""}`, details);
|
|
164
|
+
}
|
|
165
|
+
function problemText(body) {
|
|
166
|
+
try {
|
|
167
|
+
const p = JSON.parse(body);
|
|
168
|
+
if (p !== null && typeof p === "object") {
|
|
169
|
+
const parts = [p.title, p.detail].filter((x) => typeof x === "string" && x !== "");
|
|
170
|
+
if (parts.length > 0)
|
|
171
|
+
return parts.join(": ");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch { /* not JSON — the raw text is used */ }
|
|
175
|
+
return body.slice(0, 300);
|
|
176
|
+
}
|
|
177
|
+
/** api.qfex.* (trading / account REST): 401 → SIGNATURE_FAILED, 403 → POLICY_DENIED, 429 → RATE_LIMITED, 5xx → EXCHANGE_ERROR. */
|
|
178
|
+
export function qfexRestError(method, path, httpStatus, body) {
|
|
179
|
+
const text = problemText(body);
|
|
180
|
+
const at = `[${method} ${path}, HTTP ${httpStatus}]`;
|
|
181
|
+
const base = { exchange: "qfex", httpStatus };
|
|
182
|
+
if (httpStatus === 401) {
|
|
183
|
+
return new PerpError("SIGNATURE_FAILED", `QFEX: ${text} ${at}`, {
|
|
184
|
+
...base, remediation: "Check the system clock (QFEX accepts ±5 minutes) and that the key was not revoked; re-import: perp wallet agent import qfex --replace",
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (httpStatus === 403)
|
|
188
|
+
return new PerpError("POLICY_DENIED", `QFEX: ${text} ${at}`, { ...base, remediation: KEY_PERMISSION_REMEDIATION });
|
|
189
|
+
if (httpStatus === 404)
|
|
190
|
+
return new PerpError("EXCHANGE_ERROR", `QFEX: ${text} ${at}`, base);
|
|
191
|
+
if (httpStatus === 429)
|
|
192
|
+
return new PerpError("RATE_LIMITED", `QFEX: ${text} ${at}`, { ...base, remediation: CODE_REMEDIATION.RATE_LIMITED });
|
|
193
|
+
if (httpStatus >= 400 && httpStatus < 500)
|
|
194
|
+
return new PerpError("INVALID_PARAMS", `QFEX: ${text} ${at}`, base);
|
|
195
|
+
return new PerpError("EXCHANGE_ERROR", `QFEX: ${text} ${at}`, base);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* banker.qfex.* (deposit address / withdraw), docs programmatic-funding "Errors":
|
|
199
|
+
* 400 invalid amount / address / network, or insufficient withdrawable balance → definite refusal;
|
|
200
|
+
* 401 missing / invalid authentication OR terms not accepted → by body;
|
|
201
|
+
* 403 key lacks "Deposit and withdraw", or the account cannot use funding;
|
|
202
|
+
* The official CLI README adds that withdrawals need 2FA and cannot be started from a CLI, so a
|
|
203
|
+
* refusal here is expected for many accounts.
|
|
204
|
+
*/
|
|
205
|
+
export function qfexBankerError(method, path, httpStatus, body) {
|
|
206
|
+
const text = problemText(body);
|
|
207
|
+
const at = `[${method} ${path}, HTTP ${httpStatus}]`;
|
|
208
|
+
const base = { exchange: "qfex", httpStatus };
|
|
209
|
+
const website = "QFEX documents that withdrawals need two-factor authentication on the web app; if the API refuses, withdraw there";
|
|
210
|
+
if (httpStatus === 400) {
|
|
211
|
+
const insufficient = /insufficient|withdrawable|balance/i.test(text);
|
|
212
|
+
return new PerpError(insufficient ? "INSUFFICIENT_BALANCE" : "ACTION_REJECTED", `QFEX funding refused: ${text} ${at}`, {
|
|
213
|
+
...base, remediation: insufficient
|
|
214
|
+
? "The amount exceeds the Withdrawable Balance (cash + realised PnL + min(unrealised PnL, 0) − 1.05 × margin) — lower it or free margin"
|
|
215
|
+
: `Nothing was executed. Check the amount, address and network (ARBITRUM_ONE). ${website}`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
if (httpStatus === 401) {
|
|
219
|
+
const terms = /terms|t&c|tnc|agreement|conditions/i.test(text);
|
|
220
|
+
return new PerpError(terms ? "POLICY_DENIED" : "SIGNATURE_FAILED", `QFEX funding: ${text} ${at}`, {
|
|
221
|
+
...base, remediation: terms
|
|
222
|
+
? "Accept the latest QFEX terms on the website, then retry"
|
|
223
|
+
: "Authentication was refused: check the system clock and the key (perp wallet agent import qfex --replace). Nothing was executed",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (httpStatus === 403) {
|
|
227
|
+
return new PerpError("POLICY_DENIED", `QFEX funding: ${text} ${at}`, {
|
|
228
|
+
...base, remediation: `The key needs the "Deposit and withdraw" permission (only "All accounts" keys can have it), or the account cannot use funding. ${website}`,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (httpStatus === 429)
|
|
232
|
+
return new PerpError("RATE_LIMITED", `QFEX funding: ${text} ${at}`, { ...base, remediation: CODE_REMEDIATION.RATE_LIMITED });
|
|
233
|
+
if (httpStatus >= 400 && httpStatus < 500)
|
|
234
|
+
return new PerpError("ACTION_REJECTED", `QFEX funding refused: ${text} ${at}`, base);
|
|
235
|
+
return new PerpError("EXCHANGE_ERROR", `QFEX funding: ${text} ${at}`, base);
|
|
236
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QFEX public market data (design §14.8 as amended by §14.12 #6, #7, #10).
|
|
3
|
+
*
|
|
4
|
+
* Sources (all public):
|
|
5
|
+
* GET /refdata instruments: tick, lot, min/max quantity, price band, status,
|
|
6
|
+
* quote / margin asset, allowed order types + TIFs, market hours
|
|
7
|
+
* MDS market_stats snapshot mark price (each entry with its own `time`), open interest,
|
|
8
|
+
* 24h notional volume
|
|
9
|
+
* GET /md/contracts funding_rate / next_funding_rate_timestamp (funding reads only)
|
|
10
|
+
*
|
|
11
|
+
* Coupling (§14.12 #10): order-entry precision comes from /refdata ALONE (fetchQfexInstruments).
|
|
12
|
+
* Markets need /refdata + the MDS snapshot. Only funding reads need /md/contracts — its failure
|
|
13
|
+
* fails fetchFundingRates, never market listing or order entry.
|
|
14
|
+
*
|
|
15
|
+
* Funding is UNVERIFIED (venue flag fundingVerified: false, Q4): the rate's unit is not
|
|
16
|
+
* confirmed against settled payments and `funding_rate` vs `next_funding_rate` is undecided.
|
|
17
|
+
* Markets therefore carry fundingRate null; fetchFundingRates returns the raw `funding_rate`
|
|
18
|
+
* for the explicit, flagged reads only (`perp funding`, `arb scan --include-unverified`).
|
|
19
|
+
*
|
|
20
|
+
* Symbols: "AAPL-USD" → CLI symbol "AAPL". Non-USD quotes keep the suffix ("SKHYNIX-KRW"),
|
|
21
|
+
* have no canonical symbol and are NOT tradable through perp-cli (USD-assuming sizing / risk).
|
|
22
|
+
*/
|
|
23
|
+
import type { VenuePublicData } from "../venue.js";
|
|
24
|
+
import type { AssetClass, FundingRateBasis, Network, VenueFundingRate, VenueMarket } from "../venue-types.js";
|
|
25
|
+
import { QfexClient } from "./client.js";
|
|
26
|
+
export declare const QFEX_FUNDING_BASIS: FundingRateBasis;
|
|
27
|
+
export declare const QFEX_SETTLEMENT_HOURS = 1;
|
|
28
|
+
export declare const QFEX_USD_SUFFIX = "-USD";
|
|
29
|
+
/**
|
|
30
|
+
* Pre-IPO perps (docs /qfex/pre-ipo-stocks). /refdata does not mark them (they are listed as
|
|
31
|
+
* EQUITY), so the list is explicit; its completeness is Q12. Their price convention (implied
|
|
32
|
+
* valuation per notional share count) is venue-specific → never matched across venues.
|
|
33
|
+
*/
|
|
34
|
+
export declare const QFEX_PRE_IPO: ReadonlySet<string>;
|
|
35
|
+
export interface QfexSessionWindow {
|
|
36
|
+
session: string;
|
|
37
|
+
open: string;
|
|
38
|
+
close: string;
|
|
39
|
+
}
|
|
40
|
+
export interface QfexRefdataRow {
|
|
41
|
+
symbol: string;
|
|
42
|
+
status: string;
|
|
43
|
+
product_category: string;
|
|
44
|
+
tick_size: string;
|
|
45
|
+
lot_size: string;
|
|
46
|
+
min_price: string;
|
|
47
|
+
max_price: string;
|
|
48
|
+
min_quantity: string;
|
|
49
|
+
max_quantity: string;
|
|
50
|
+
default_max_leverage: number;
|
|
51
|
+
base_asset: string;
|
|
52
|
+
quote_asset: string;
|
|
53
|
+
margin_asset: string;
|
|
54
|
+
order_types: string[];
|
|
55
|
+
order_time_in_force: string[];
|
|
56
|
+
market_hours: Record<string, unknown> | null;
|
|
57
|
+
}
|
|
58
|
+
export interface QfexContractRow {
|
|
59
|
+
ticker_id: string;
|
|
60
|
+
funding_rate: string;
|
|
61
|
+
next_funding_rate: string;
|
|
62
|
+
next_funding_rate_timestamp: number;
|
|
63
|
+
}
|
|
64
|
+
/** One instrument, as order entry needs it — from /refdata only. */
|
|
65
|
+
export interface QfexInstrument {
|
|
66
|
+
venueSymbol: string;
|
|
67
|
+
symbol: string;
|
|
68
|
+
status: string;
|
|
69
|
+
assetClass: AssetClass;
|
|
70
|
+
tickSize: string;
|
|
71
|
+
lotSize: string;
|
|
72
|
+
minQuantity: string;
|
|
73
|
+
maxQuantity: string;
|
|
74
|
+
/** Current price band (orders outside it are rejected before the engine). */
|
|
75
|
+
band: {
|
|
76
|
+
lower: string;
|
|
77
|
+
upper: string;
|
|
78
|
+
};
|
|
79
|
+
maxLeverage: number;
|
|
80
|
+
quoteAsset: string;
|
|
81
|
+
marginAsset: string;
|
|
82
|
+
orderTypes: readonly string[];
|
|
83
|
+
timeInForce: readonly string[];
|
|
84
|
+
marketHours: Record<string, unknown> | null;
|
|
85
|
+
}
|
|
86
|
+
/** "AAPL-USD" → "AAPL"; a non-USD quote keeps its suffix ("SKHYNIX-KRW"). */
|
|
87
|
+
export declare function qfexPlainSymbol(venueSymbol: string): string;
|
|
88
|
+
/** CLI symbol ("aapl", "AAPL", "AAPL-USD", "SKHYNIX-KRW") → venue symbol. A bare ticker is the USD market. */
|
|
89
|
+
export declare function qfexVenueSymbol(symbol: string): string;
|
|
90
|
+
export declare function qfexAssetClass(row: Pick<QfexRefdataRow, "symbol" | "product_category" | "base_asset">): AssetClass;
|
|
91
|
+
export declare function toQfexInstrument(raw: unknown): QfexInstrument;
|
|
92
|
+
/** Is the market one perp-cli may send orders to? USD-quoted and ACTIVE (§14.12 #7). */
|
|
93
|
+
export declare function qfexTradable(i: Pick<QfexInstrument, "status" | "quoteAsset">): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Whether `nowMs` falls inside a `session: "open"` window of the instrument's market_hours
|
|
96
|
+
* (weekday + local time in its `tz`). null = the venue published no usable schedule.
|
|
97
|
+
* This is the underlying's regular session only — it is NOT used to infer whether funding
|
|
98
|
+
* accrues (the schedule and the documented funding hours do not map 1:1, Q9).
|
|
99
|
+
*/
|
|
100
|
+
export declare function qfexUnderlyingOpen(marketHours: Record<string, unknown> | null, nowMs: number): boolean | null;
|
|
101
|
+
export declare function qfexPublicClient(net: Network): QfexClient;
|
|
102
|
+
/** Every instrument of /refdata (all statuses). The only input of order-entry validation. */
|
|
103
|
+
export declare function fetchQfexInstruments(client: QfexClient): Promise<QfexInstrument[]>;
|
|
104
|
+
export declare function buildQfexMarkets(instruments: readonly QfexInstrument[], snap: {
|
|
105
|
+
marks: ReadonlyMap<string, {
|
|
106
|
+
price: string;
|
|
107
|
+
time: number;
|
|
108
|
+
}>;
|
|
109
|
+
openInterest: ReadonlyMap<string, string>;
|
|
110
|
+
volumeNotional: ReadonlyMap<string, string>;
|
|
111
|
+
}, nowMs: number): {
|
|
112
|
+
markets: VenueMarket[];
|
|
113
|
+
activeWithoutMark: string[];
|
|
114
|
+
};
|
|
115
|
+
export declare function buildQfexFundingRates(instruments: readonly QfexInstrument[], contracts: readonly QfexContractRow[], marks: ReadonlyMap<string, {
|
|
116
|
+
price: string;
|
|
117
|
+
time: number;
|
|
118
|
+
}>, nowMs: number): {
|
|
119
|
+
rates: VenueFundingRate[];
|
|
120
|
+
unlisted: string[];
|
|
121
|
+
};
|
|
122
|
+
export declare const qfexPublicData: VenuePublicData;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QFEX public market data (design §14.8 as amended by §14.12 #6, #7, #10).
|
|
3
|
+
*
|
|
4
|
+
* Sources (all public):
|
|
5
|
+
* GET /refdata instruments: tick, lot, min/max quantity, price band, status,
|
|
6
|
+
* quote / margin asset, allowed order types + TIFs, market hours
|
|
7
|
+
* MDS market_stats snapshot mark price (each entry with its own `time`), open interest,
|
|
8
|
+
* 24h notional volume
|
|
9
|
+
* GET /md/contracts funding_rate / next_funding_rate_timestamp (funding reads only)
|
|
10
|
+
*
|
|
11
|
+
* Coupling (§14.12 #10): order-entry precision comes from /refdata ALONE (fetchQfexInstruments).
|
|
12
|
+
* Markets need /refdata + the MDS snapshot. Only funding reads need /md/contracts — its failure
|
|
13
|
+
* fails fetchFundingRates, never market listing or order entry.
|
|
14
|
+
*
|
|
15
|
+
* Funding is UNVERIFIED (venue flag fundingVerified: false, Q4): the rate's unit is not
|
|
16
|
+
* confirmed against settled payments and `funding_rate` vs `next_funding_rate` is undecided.
|
|
17
|
+
* Markets therefore carry fundingRate null; fetchFundingRates returns the raw `funding_rate`
|
|
18
|
+
* for the explicit, flagged reads only (`perp funding`, `arb scan --include-unverified`).
|
|
19
|
+
*
|
|
20
|
+
* Symbols: "AAPL-USD" → CLI symbol "AAPL". Non-USD quotes keep the suffix ("SKHYNIX-KRW"),
|
|
21
|
+
* have no canonical symbol and are NOT tradable through perp-cli (USD-assuming sizing / risk).
|
|
22
|
+
*/
|
|
23
|
+
import { PerpError } from "../../errors.js";
|
|
24
|
+
import { canonicalFor } from "../canonical-symbols.js";
|
|
25
|
+
import { QFEX_ENDPOINTS } from "../venues/qfex.js";
|
|
26
|
+
import { QfexClient } from "./client.js";
|
|
27
|
+
import { fetchQfexMarketStats, QFEX_MDS_TIMEOUT_MS } from "./ws-transport.js";
|
|
28
|
+
export const QFEX_FUNDING_BASIS = { kind: "perHours", hours: 1 };
|
|
29
|
+
export const QFEX_SETTLEMENT_HOURS = 1;
|
|
30
|
+
export const QFEX_USD_SUFFIX = "-USD";
|
|
31
|
+
/**
|
|
32
|
+
* Pre-IPO perps (docs /qfex/pre-ipo-stocks). /refdata does not mark them (they are listed as
|
|
33
|
+
* EQUITY), so the list is explicit; its completeness is Q12. Their price convention (implied
|
|
34
|
+
* valuation per notional share count) is venue-specific → never matched across venues.
|
|
35
|
+
*/
|
|
36
|
+
export const QFEX_PRE_IPO = new Set(["ANTHROPIC", "OPENAI"]);
|
|
37
|
+
// ── Symbols ──
|
|
38
|
+
/** "AAPL-USD" → "AAPL"; a non-USD quote keeps its suffix ("SKHYNIX-KRW"). */
|
|
39
|
+
export function qfexPlainSymbol(venueSymbol) {
|
|
40
|
+
return venueSymbol.endsWith(QFEX_USD_SUFFIX) ? venueSymbol.slice(0, -QFEX_USD_SUFFIX.length) : venueSymbol;
|
|
41
|
+
}
|
|
42
|
+
/** CLI symbol ("aapl", "AAPL", "AAPL-USD", "SKHYNIX-KRW") → venue symbol. A bare ticker is the USD market. */
|
|
43
|
+
export function qfexVenueSymbol(symbol) {
|
|
44
|
+
const up = symbol.trim().toUpperCase().replace(/-PERP$/, "");
|
|
45
|
+
if (up.length === 0)
|
|
46
|
+
throw new PerpError("INVALID_PARAMS", "symbol is empty");
|
|
47
|
+
return up.includes("-") ? up : `${up}${QFEX_USD_SUFFIX}`;
|
|
48
|
+
}
|
|
49
|
+
const CATEGORY_TO_ASSET_CLASS = {
|
|
50
|
+
EQUITY: "equity",
|
|
51
|
+
INDEX: "index",
|
|
52
|
+
COMMODITY: "commodity",
|
|
53
|
+
FX: "fx",
|
|
54
|
+
};
|
|
55
|
+
export function qfexAssetClass(row) {
|
|
56
|
+
const c = CATEGORY_TO_ASSET_CLASS[row.product_category];
|
|
57
|
+
if (c === undefined) {
|
|
58
|
+
throw new PerpError("EXCHANGE_ERROR", `QFEX: cannot classify ${row.symbol} (product_category ${JSON.stringify(row.product_category)})`, {
|
|
59
|
+
remediation: "Update CATEGORY_TO_ASSET_CLASS in src/exchanges/qfex/public.ts for the new venue category",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return c === "equity" && QFEX_PRE_IPO.has(row.base_asset) ? "preIpo" : c;
|
|
63
|
+
}
|
|
64
|
+
function requireString(row, key) {
|
|
65
|
+
const v = row[key];
|
|
66
|
+
if (typeof v !== "string" || v === "")
|
|
67
|
+
throw new PerpError("EXCHANGE_ERROR", `QFEX /refdata: ${String(row.symbol)} has no ${key}`);
|
|
68
|
+
return v;
|
|
69
|
+
}
|
|
70
|
+
function stringList(row, key) {
|
|
71
|
+
const v = row[key];
|
|
72
|
+
if (!Array.isArray(v) || v.some(x => typeof x !== "string"))
|
|
73
|
+
throw new PerpError("EXCHANGE_ERROR", `QFEX /refdata: ${String(row.symbol)} has no ${key} list`);
|
|
74
|
+
return v;
|
|
75
|
+
}
|
|
76
|
+
export function toQfexInstrument(raw) {
|
|
77
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
78
|
+
throw new PerpError("EXCHANGE_ERROR", "QFEX /refdata: instrument row is not an object");
|
|
79
|
+
const row = raw;
|
|
80
|
+
const venueSymbol = requireString(row, "symbol");
|
|
81
|
+
const lev = row.default_max_leverage;
|
|
82
|
+
if (typeof lev !== "number" || !Number.isFinite(lev) || lev <= 0) {
|
|
83
|
+
throw new PerpError("EXCHANGE_ERROR", `QFEX /refdata: ${venueSymbol} has no default_max_leverage`);
|
|
84
|
+
}
|
|
85
|
+
const hours = row.market_hours;
|
|
86
|
+
const base = requireString(row, "base_asset");
|
|
87
|
+
const category = requireString(row, "product_category");
|
|
88
|
+
return {
|
|
89
|
+
venueSymbol,
|
|
90
|
+
symbol: qfexPlainSymbol(venueSymbol),
|
|
91
|
+
status: requireString(row, "status"),
|
|
92
|
+
assetClass: qfexAssetClass({ symbol: venueSymbol, product_category: category, base_asset: base }),
|
|
93
|
+
tickSize: requireString(row, "tick_size"),
|
|
94
|
+
lotSize: requireString(row, "lot_size"),
|
|
95
|
+
minQuantity: requireString(row, "min_quantity"),
|
|
96
|
+
maxQuantity: requireString(row, "max_quantity"),
|
|
97
|
+
band: { lower: requireString(row, "min_price"), upper: requireString(row, "max_price") },
|
|
98
|
+
maxLeverage: lev,
|
|
99
|
+
quoteAsset: requireString(row, "quote_asset"),
|
|
100
|
+
marginAsset: requireString(row, "margin_asset"),
|
|
101
|
+
orderTypes: stringList(row, "order_types"),
|
|
102
|
+
timeInForce: stringList(row, "order_time_in_force"),
|
|
103
|
+
marketHours: hours !== null && typeof hours === "object" && !Array.isArray(hours) ? hours : null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Is the market one perp-cli may send orders to? USD-quoted and ACTIVE (§14.12 #7). */
|
|
107
|
+
export function qfexTradable(i) {
|
|
108
|
+
return i.status === "ACTIVE" && i.quoteAsset === "USD";
|
|
109
|
+
}
|
|
110
|
+
// ── Market hours → session ──
|
|
111
|
+
const WEEKDAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
112
|
+
/**
|
|
113
|
+
* Whether `nowMs` falls inside a `session: "open"` window of the instrument's market_hours
|
|
114
|
+
* (weekday + local time in its `tz`). null = the venue published no usable schedule.
|
|
115
|
+
* This is the underlying's regular session only — it is NOT used to infer whether funding
|
|
116
|
+
* accrues (the schedule and the documented funding hours do not map 1:1, Q9).
|
|
117
|
+
*/
|
|
118
|
+
export function qfexUnderlyingOpen(marketHours, nowMs) {
|
|
119
|
+
if (marketHours === null || typeof marketHours.tz !== "string")
|
|
120
|
+
return null;
|
|
121
|
+
let parts;
|
|
122
|
+
try {
|
|
123
|
+
parts = new Intl.DateTimeFormat("en-US", {
|
|
124
|
+
timeZone: marketHours.tz, weekday: "long", hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23",
|
|
125
|
+
}).formatToParts(new Date(nowMs));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return null; // an unknown IANA zone name is "no usable schedule", not a closed market
|
|
129
|
+
}
|
|
130
|
+
const get = (t) => parts.find(p => p.type === t)?.value;
|
|
131
|
+
const weekday = get("weekday")?.toLowerCase();
|
|
132
|
+
const h = get("hour"), m = get("minute"), s = get("second");
|
|
133
|
+
if (weekday === undefined || !WEEKDAYS.includes(weekday) || h === undefined || m === undefined || s === undefined)
|
|
134
|
+
return null;
|
|
135
|
+
const now = `${h}:${m}:${s}`;
|
|
136
|
+
const day = marketHours[weekday];
|
|
137
|
+
if (day === undefined)
|
|
138
|
+
return false; // no window listed for this weekday
|
|
139
|
+
if (!Array.isArray(day))
|
|
140
|
+
return null;
|
|
141
|
+
for (const w of day) {
|
|
142
|
+
if (w === null || typeof w !== "object")
|
|
143
|
+
return null;
|
|
144
|
+
const win = w;
|
|
145
|
+
if (typeof win.session !== "string" || typeof win.open !== "string" || typeof win.close !== "string")
|
|
146
|
+
return null;
|
|
147
|
+
if (win.session === "open" && now >= win.open && now <= win.close)
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
function sessionOf(i, nowMs) {
|
|
153
|
+
const open = qfexUnderlyingOpen(i.marketHours, nowMs);
|
|
154
|
+
if (open === null)
|
|
155
|
+
return null;
|
|
156
|
+
return { underlyingOpen: open, priceBand: i.band, marginMultiplier: null };
|
|
157
|
+
}
|
|
158
|
+
// ── Fetchers ──
|
|
159
|
+
export function qfexPublicClient(net) {
|
|
160
|
+
const ep = QFEX_ENDPOINTS[net];
|
|
161
|
+
return new QfexClient({ restUrl: ep.rest, bankerUrl: ep.banker, network: net, credentials: null, accountId: null });
|
|
162
|
+
}
|
|
163
|
+
/** Every instrument of /refdata (all statuses). The only input of order-entry validation. */
|
|
164
|
+
export async function fetchQfexInstruments(client) {
|
|
165
|
+
const body = await client.publicGet("/refdata", null);
|
|
166
|
+
if (!Array.isArray(body.data))
|
|
167
|
+
throw new PerpError("EXCHANGE_ERROR", "QFEX /refdata: response has no data list");
|
|
168
|
+
return body.data.map(toQfexInstrument);
|
|
169
|
+
}
|
|
170
|
+
function canonicalOf(i) {
|
|
171
|
+
// A local-currency quote is a different contract from the USD one (ZHIPU-HKD vs ZHIPU-USD).
|
|
172
|
+
if (i.quoteAsset !== "USD")
|
|
173
|
+
return null;
|
|
174
|
+
return canonicalFor("qfex", i.symbol, i.assetClass);
|
|
175
|
+
}
|
|
176
|
+
export function buildQfexMarkets(instruments, snap, nowMs) {
|
|
177
|
+
const markets = [];
|
|
178
|
+
const activeWithoutMark = [];
|
|
179
|
+
for (const i of instruments) {
|
|
180
|
+
const mark = snap.marks.get(i.venueSymbol);
|
|
181
|
+
if (mark === undefined) {
|
|
182
|
+
if (i.status === "ACTIVE")
|
|
183
|
+
activeWithoutMark.push(i.venueSymbol);
|
|
184
|
+
continue; // no price feed → cannot be priced; never given a made-up mark
|
|
185
|
+
}
|
|
186
|
+
const oi = snap.openInterest.get(i.venueSymbol);
|
|
187
|
+
const vol = snap.volumeNotional.get(i.venueSymbol);
|
|
188
|
+
markets.push({
|
|
189
|
+
venue: "qfex",
|
|
190
|
+
venueSymbol: i.venueSymbol,
|
|
191
|
+
symbol: i.symbol,
|
|
192
|
+
canonical: canonicalOf(i),
|
|
193
|
+
assetClass: i.assetClass,
|
|
194
|
+
settle: i.marginAsset,
|
|
195
|
+
quoteAsset: i.quoteAsset,
|
|
196
|
+
markPrice: mark.price,
|
|
197
|
+
indexPrice: null,
|
|
198
|
+
fundingRate: null, // unverified (Q4) — see module header
|
|
199
|
+
fundingRateBasis: QFEX_FUNDING_BASIS,
|
|
200
|
+
settlementIntervalHours: QFEX_SETTLEMENT_HOURS,
|
|
201
|
+
openInterest: oi === undefined ? null : oi,
|
|
202
|
+
volume24h: vol === undefined ? null : vol,
|
|
203
|
+
tickSize: i.tickSize,
|
|
204
|
+
stepSize: i.lotSize,
|
|
205
|
+
minSize: i.minQuantity,
|
|
206
|
+
minNotional: null, // REJECTED_LESS_THAN_MIN_NOTIONAL exists but the threshold is unpublished
|
|
207
|
+
maxLeverage: i.maxLeverage,
|
|
208
|
+
marginTiers: null,
|
|
209
|
+
session: sessionOf(i, nowMs),
|
|
210
|
+
tradable: qfexTradable(i),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return { markets, activeWithoutMark };
|
|
214
|
+
}
|
|
215
|
+
async function fetchMarkets(net) {
|
|
216
|
+
const instruments = await fetchQfexInstruments(qfexPublicClient(net));
|
|
217
|
+
const snap = await fetchQfexMarketStats(QFEX_ENDPOINTS[net].mdsWs, "*", QFEX_MDS_TIMEOUT_MS);
|
|
218
|
+
const { markets, activeWithoutMark } = buildQfexMarkets(instruments, snap, Date.now());
|
|
219
|
+
if (activeWithoutMark.length > 0) {
|
|
220
|
+
console.warn(`[qfex] excluded ${activeWithoutMark.length} active market(s) without a mark price: ${activeWithoutMark.join(", ")}`);
|
|
221
|
+
}
|
|
222
|
+
return markets;
|
|
223
|
+
}
|
|
224
|
+
export function buildQfexFundingRates(instruments, contracts, marks, nowMs) {
|
|
225
|
+
const byVenueSymbol = new Map(instruments.map(i => [i.venueSymbol, i]));
|
|
226
|
+
const rates = [];
|
|
227
|
+
const unlisted = [];
|
|
228
|
+
for (const c of contracts) {
|
|
229
|
+
const i = byVenueSymbol.get(c.ticker_id);
|
|
230
|
+
if (i === undefined) {
|
|
231
|
+
unlisted.push(c.ticker_id);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (i.status !== "ACTIVE")
|
|
235
|
+
continue;
|
|
236
|
+
if (typeof c.funding_rate !== "string" || c.funding_rate === "") {
|
|
237
|
+
throw new PerpError("EXCHANGE_ERROR", `QFEX /md/contracts: ${c.ticker_id} has no funding_rate`);
|
|
238
|
+
}
|
|
239
|
+
const mark = marks.get(c.ticker_id);
|
|
240
|
+
const nextMs = typeof c.next_funding_rate_timestamp === "number" ? c.next_funding_rate_timestamp * 1000 : null;
|
|
241
|
+
rates.push({
|
|
242
|
+
venue: "qfex",
|
|
243
|
+
venueSymbol: c.ticker_id,
|
|
244
|
+
symbol: i.symbol,
|
|
245
|
+
canonical: canonicalOf(i),
|
|
246
|
+
rate: c.funding_rate,
|
|
247
|
+
basis: QFEX_FUNDING_BASIS,
|
|
248
|
+
markPrice: mark === undefined ? null : mark.price,
|
|
249
|
+
// A timestamp in the past is not a "next" funding time (seen on closed markets).
|
|
250
|
+
nextFundingTime: nextMs !== null && nextMs > nowMs ? nextMs : null,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return { rates, unlisted };
|
|
254
|
+
}
|
|
255
|
+
async function fetchFundingRates(net) {
|
|
256
|
+
const client = qfexPublicClient(net);
|
|
257
|
+
const instruments = await fetchQfexInstruments(client);
|
|
258
|
+
const body = await client.publicGet("/md/contracts", null);
|
|
259
|
+
if (!Array.isArray(body.data))
|
|
260
|
+
throw new PerpError("EXCHANGE_ERROR", "QFEX /md/contracts: response has no data list");
|
|
261
|
+
const snap = await fetchQfexMarketStats(QFEX_ENDPOINTS[net].mdsWs, "*", QFEX_MDS_TIMEOUT_MS);
|
|
262
|
+
const { rates, unlisted } = buildQfexFundingRates(instruments, body.data, snap.marks, Date.now());
|
|
263
|
+
if (unlisted.length > 0) {
|
|
264
|
+
console.warn(`[qfex] excluded ${unlisted.length} contract(s) missing from /refdata: ${unlisted.join(", ")}`);
|
|
265
|
+
}
|
|
266
|
+
return rates;
|
|
267
|
+
}
|
|
268
|
+
export const qfexPublicData = { fetchMarkets, fetchFundingRates };
|