perp-cli 0.14.2 → 0.15.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 +152 -13
- package/dist/agent-wallet/store.d.ts +3 -2
- package/dist/agent-wallet/store.js +2 -2
- package/dist/api/public/hyperliquid.js +4 -0
- package/dist/api/public/pacifica.js +83 -13
- package/dist/api/public/urls.d.ts +2 -0
- package/dist/api/public/urls.js +3 -0
- package/dist/arb/utils.d.ts +1 -2
- package/dist/arb/utils.js +1 -2
- package/dist/bot/apex/pulse.js +3 -0
- package/dist/bot/apex/radar.js +2 -1
- package/dist/bot/engine.d.ts +10 -1
- package/dist/bot/engine.js +14 -6
- package/dist/bot/indicators.d.ts +7 -0
- package/dist/bot/indicators.js +16 -0
- package/dist/bot/perp-order.d.ts +4 -4
- package/dist/bot/perp-order.js +5 -5
- package/dist/bot/quarantine.d.ts +1 -1
- package/dist/bot/quarantine.js +1 -1
- package/dist/bot/strategies/apex-strategy.js +3 -2
- package/dist/bot/strategies/engine-mm.js +6 -5
- package/dist/bot/strategies/funding-arb-strategy.js +9 -9
- package/dist/bot/strategies/funding-arb-utils.d.ts +15 -2
- package/dist/bot/strategies/funding-arb-utils.js +43 -24
- package/dist/bot/strategies/funding-auto.js +2 -2
- package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
- package/dist/bot/strategies/liquidation-mm.js +8 -2
- package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
- package/dist/bot/strategies/momentum-breakout.js +11 -3
- package/dist/bot/strategies/spot-perp-arb-strategy.js +23 -12
- package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
- package/dist/bot/strategies/spot-perp-executor.js +1 -1
- package/dist/bot/strategy-types.d.ts +2 -1
- package/dist/bot/tui/BotDashboard.d.ts +1 -1
- package/dist/bot/tui/BotDashboard.js +1 -1
- package/dist/cache.js +12 -6
- package/dist/cli-program.d.ts +15 -0
- package/dist/cli-program.js +125 -5
- package/dist/commands/agent-import.d.ts +7 -0
- package/dist/commands/agent-import.js +30 -1
- package/dist/commands/agent.d.ts +6 -3
- package/dist/commands/agent.js +160 -12
- package/dist/commands/arb/index.js +4 -4
- package/dist/commands/arb-auto.js +21 -17
- package/dist/commands/arb.d.ts +12 -0
- package/dist/commands/arb.js +61 -143
- package/dist/commands/bot.js +1 -1
- package/dist/commands/builder.js +1 -1
- package/dist/commands/funds.js +113 -3
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +27 -7
- package/dist/commands/manage.js +23 -21
- package/dist/commands/market.js +168 -36
- package/dist/commands/options.d.ts +9 -0
- package/dist/commands/options.js +134 -0
- package/dist/commands/pacifica-earn.d.ts +5 -0
- package/dist/commands/pacifica-earn.js +468 -0
- package/dist/commands/pacifica-orders.d.ts +14 -0
- package/dist/commands/pacifica-orders.js +142 -0
- package/dist/commands/portfolio.js +32 -47
- package/dist/commands/rebalance.d.ts +1 -0
- package/dist/commands/rebalance.js +15 -0
- package/dist/commands/risk.d.ts +11 -2
- package/dist/commands/risk.js +39 -44
- package/dist/commands/trade.d.ts +1 -0
- package/dist/commands/trade.js +63 -58
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +15 -4
- package/dist/constants.js +6 -0
- package/dist/dashboard/server.js +1 -1
- package/dist/dashboard/ui.js +1 -1
- package/dist/errors.d.ts +24 -1
- package/dist/errors.js +25 -3
- package/dist/exchanges/arcus/adapter.d.ts +7 -7
- package/dist/exchanges/arcus/adapter.js +12 -12
- package/dist/exchanges/arcus/agent.d.ts +1 -1
- package/dist/exchanges/arcus/agent.js +1 -1
- package/dist/exchanges/arcus/client.js +1 -1
- package/dist/exchanges/arcus/signing.d.ts +2 -3
- package/dist/exchanges/arcus/signing.js +1 -1
- package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
- package/dist/exchanges/arcus/spot-chain.js +1 -1
- package/dist/exchanges/arcus/spot-router.d.ts +1 -1
- package/dist/exchanges/arcus/spot-router.js +2 -2
- package/dist/exchanges/arcus/spot.d.ts +2 -3
- package/dist/exchanges/arcus/spot.js +2 -3
- package/dist/exchanges/aster-spot.d.ts +3 -3
- package/dist/exchanges/aster-spot.js +6 -9
- package/dist/exchanges/aster-typed-data.d.ts +1 -1
- package/dist/exchanges/aster-typed-data.js +1 -1
- package/dist/exchanges/aster.d.ts +9 -4
- package/dist/exchanges/aster.js +125 -55
- package/dist/exchanges/builder-fee.d.ts +1 -1
- package/dist/exchanges/builder-fee.js +13 -2
- package/dist/exchanges/canonical-symbols.d.ts +1 -2
- package/dist/exchanges/canonical-symbols.js +26 -2
- package/dist/exchanges/derive/client.d.ts +37 -0
- package/dist/exchanges/derive/client.js +135 -0
- package/dist/exchanges/derive/options.d.ts +68 -0
- package/dist/exchanges/derive/options.js +360 -0
- package/dist/exchanges/extended/adapter.d.ts +82 -0
- package/dist/exchanges/extended/adapter.js +179 -0
- package/dist/exchanges/extended/client.d.ts +38 -0
- package/dist/exchanges/extended/client.js +88 -0
- package/dist/exchanges/extended/public.d.ts +130 -0
- package/dist/exchanges/extended/public.js +429 -0
- package/dist/exchanges/grvt/adapter.d.ts +86 -0
- package/dist/exchanges/grvt/adapter.js +206 -0
- package/dist/exchanges/grvt/client.d.ts +25 -0
- package/dist/exchanges/grvt/client.js +86 -0
- package/dist/exchanges/grvt/public.d.ts +151 -0
- package/dist/exchanges/grvt/public.js +438 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
- package/dist/exchanges/hyperliquid-spot.js +5 -5
- package/dist/exchanges/hyperliquid.d.ts +22 -16
- package/dist/exchanges/hyperliquid.js +80 -55
- package/dist/exchanges/index.d.ts +1 -1
- package/dist/exchanges/index.js +1 -1
- package/dist/exchanges/interface.d.ts +97 -8
- package/dist/exchanges/interface.js +19 -2
- package/dist/exchanges/ioc-outcome.d.ts +1 -1
- package/dist/exchanges/ioc-outcome.js +1 -1
- package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
- package/dist/exchanges/legacy-trading-v2.js +7 -2
- package/dist/exchanges/lighter-public.d.ts +2 -2
- package/dist/exchanges/lighter-public.js +2 -2
- package/dist/exchanges/lighter-signer.d.ts +1 -1
- package/dist/exchanges/lighter-signer.js +1 -1
- package/dist/exchanges/lighter-spot.d.ts +3 -4
- package/dist/exchanges/lighter-spot.js +6 -7
- package/dist/exchanges/lighter.d.ts +11 -11
- package/dist/exchanges/lighter.js +28 -14
- package/dist/exchanges/ondo/adapter.d.ts +5 -6
- package/dist/exchanges/ondo/adapter.js +16 -14
- package/dist/exchanges/ondo/agent.d.ts +1 -1
- package/dist/exchanges/ondo/agent.js +1 -1
- package/dist/exchanges/ondo/client.d.ts +1 -1
- package/dist/exchanges/ondo/client.js +2 -2
- package/dist/exchanges/ondo/public.d.ts +8 -1
- package/dist/exchanges/ondo/public.js +13 -2
- package/dist/exchanges/ondo/spot.d.ts +2 -2
- package/dist/exchanges/ondo/spot.js +3 -3
- package/dist/exchanges/options-interface.d.ts +158 -0
- package/dist/exchanges/options-interface.js +17 -0
- package/dist/exchanges/options-venues.d.ts +12 -0
- package/dist/exchanges/options-venues.js +35 -0
- package/dist/exchanges/pacifica-atomic.d.ts +211 -0
- package/dist/exchanges/pacifica-atomic.js +440 -0
- package/dist/exchanges/pacifica-decimal.d.ts +10 -0
- package/dist/exchanges/pacifica-decimal.js +51 -0
- package/dist/exchanges/pacifica-presigned.d.ts +53 -0
- package/dist/exchanges/pacifica-presigned.js +141 -0
- package/dist/exchanges/pacifica-print.d.ts +200 -0
- package/dist/exchanges/pacifica-print.js +417 -0
- package/dist/exchanges/pacifica-spot.d.ts +2 -2
- package/dist/exchanges/pacifica-spot.js +3 -3
- package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
- package/dist/exchanges/pacifica-typed-data.js +1 -1
- package/dist/exchanges/pacifica-vaults.d.ts +121 -0
- package/dist/exchanges/pacifica-vaults.js +264 -0
- package/dist/exchanges/pacifica.d.ts +32 -5
- package/dist/exchanges/pacifica.js +114 -48
- package/dist/exchanges/precision.d.ts +3 -1
- package/dist/exchanges/precision.js +9 -2
- package/dist/exchanges/qfex/adapter.d.ts +18 -5
- package/dist/exchanges/qfex/adapter.js +32 -12
- package/dist/exchanges/qfex/agent.d.ts +1 -1
- package/dist/exchanges/qfex/agent.js +1 -1
- package/dist/exchanges/qfex/client.d.ts +1 -1
- package/dist/exchanges/qfex/client.js +1 -1
- package/dist/exchanges/qfex/errors.d.ts +1 -1
- package/dist/exchanges/qfex/errors.js +2 -2
- package/dist/exchanges/qfex/public.d.ts +3 -3
- package/dist/exchanges/qfex/public.js +3 -3
- package/dist/exchanges/qfex/signing.d.ts +2 -2
- package/dist/exchanges/qfex/signing.js +4 -5
- package/dist/exchanges/qfex/ws-transport.d.ts +2 -2
- package/dist/exchanges/qfex/ws-transport.js +3 -3
- package/dist/exchanges/rate-limiter.d.ts +1 -1
- package/dist/exchanges/rate-limiter.js +5 -2
- package/dist/exchanges/registry.d.ts +1 -1
- package/dist/exchanges/registry.js +1 -1
- package/dist/exchanges/risex/adapter.d.ts +148 -0
- package/dist/exchanges/risex/adapter.js +796 -0
- package/dist/exchanges/risex/agent.d.ts +154 -0
- package/dist/exchanges/risex/agent.js +384 -0
- package/dist/exchanges/risex/client.d.ts +47 -0
- package/dist/exchanges/risex/client.js +174 -0
- package/dist/exchanges/risex/nonce.d.ts +43 -0
- package/dist/exchanges/risex/nonce.js +212 -0
- package/dist/exchanges/risex/public.d.ts +64 -0
- package/dist/exchanges/risex/public.js +150 -0
- package/dist/exchanges/risex/session.d.ts +71 -0
- package/dist/exchanges/risex/session.js +171 -0
- package/dist/exchanges/risex/typed-data.d.ts +173 -0
- package/dist/exchanges/risex/typed-data.js +240 -0
- package/dist/exchanges/risex/units.d.ts +29 -0
- package/dist/exchanges/risex/units.js +104 -0
- package/dist/exchanges/slippage.d.ts +1 -1
- package/dist/exchanges/slippage.js +1 -1
- package/dist/exchanges/spot-factory.d.ts +3 -3
- package/dist/exchanges/spot-factory.js +7 -7
- package/dist/exchanges/spot-interface.d.ts +5 -5
- package/dist/exchanges/spot-interface.js +2 -2
- package/dist/exchanges/spot-result.d.ts +3 -3
- package/dist/exchanges/spot-result.js +3 -3
- package/dist/exchanges/trading-v2.d.ts +3 -3
- package/dist/exchanges/trading-v2.js +9 -6
- package/dist/exchanges/txflow/adapter.d.ts +54 -0
- package/dist/exchanges/txflow/adapter.js +118 -0
- package/dist/exchanges/txflow/client.d.ts +47 -0
- package/dist/exchanges/txflow/client.js +153 -0
- package/dist/exchanges/txflow/public.d.ts +76 -0
- package/dist/exchanges/txflow/public.js +284 -0
- package/dist/exchanges/unknown-symbol.d.ts +29 -0
- package/dist/exchanges/unknown-symbol.js +51 -0
- package/dist/exchanges/variational/adapter.d.ts +71 -0
- package/dist/exchanges/variational/adapter.js +137 -0
- package/dist/exchanges/variational/client.d.ts +21 -0
- package/dist/exchanges/variational/client.js +66 -0
- package/dist/exchanges/variational/public.d.ts +126 -0
- package/dist/exchanges/variational/public.js +430 -0
- package/dist/exchanges/venue-types.d.ts +6 -7
- package/dist/exchanges/venue-types.js +2 -2
- package/dist/exchanges/venue.d.ts +51 -8
- package/dist/exchanges/venue.js +54 -4
- package/dist/exchanges/venues/arcus.d.ts +1 -2
- package/dist/exchanges/venues/arcus.js +5 -4
- package/dist/exchanges/venues/aster.d.ts +2 -2
- package/dist/exchanges/venues/aster.js +6 -4
- package/dist/exchanges/venues/create-helpers.d.ts +1 -1
- package/dist/exchanges/venues/create-helpers.js +3 -3
- package/dist/exchanges/venues/extended.d.ts +21 -0
- package/dist/exchanges/venues/extended.js +96 -0
- package/dist/exchanges/venues/grvt.d.ts +30 -0
- package/dist/exchanges/venues/grvt.js +98 -0
- package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
- package/dist/exchanges/venues/hyperliquid.js +10 -8
- package/dist/exchanges/venues/index.d.ts +7 -2
- package/dist/exchanges/venues/index.js +8 -2
- package/dist/exchanges/venues/lighter-family.d.ts +7 -9
- package/dist/exchanges/venues/lighter-family.js +9 -11
- package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
- package/dist/exchanges/venues/lighter-rh.js +7 -5
- package/dist/exchanges/venues/lighter.d.ts +1 -1
- package/dist/exchanges/venues/lighter.js +5 -3
- package/dist/exchanges/venues/ondo.d.ts +1 -1
- package/dist/exchanges/venues/ondo.js +5 -3
- package/dist/exchanges/venues/pacifica.d.ts +2 -2
- package/dist/exchanges/venues/pacifica.js +6 -4
- package/dist/exchanges/venues/qfex.d.ts +1 -1
- package/dist/exchanges/venues/qfex.js +4 -2
- package/dist/exchanges/venues/risex-release.d.ts +14 -0
- package/dist/exchanges/venues/risex-release.js +14 -0
- package/dist/exchanges/venues/risex.d.ts +31 -0
- package/dist/exchanges/venues/risex.js +116 -0
- package/dist/exchanges/venues/txflow.d.ts +18 -0
- package/dist/exchanges/venues/txflow.js +77 -0
- package/dist/exchanges/venues/variational.d.ts +21 -0
- package/dist/exchanges/venues/variational.js +85 -0
- package/dist/execution-log.d.ts +1 -1
- package/dist/funding/legacy-public.d.ts +1 -1
- package/dist/funding/legacy-public.js +9 -4
- package/dist/funding/normalize.d.ts +1 -1
- package/dist/funding/normalize.js +1 -1
- package/dist/funding/rates.d.ts +44 -4
- package/dist/funding/rates.js +56 -12
- package/dist/index.js +7 -6
- package/dist/jobs.d.ts +1 -0
- package/dist/jobs.js +25 -1
- package/dist/landing.d.ts +2 -0
- package/dist/landing.js +1 -1
- package/dist/mcp-pacifica-earn.d.ts +9 -0
- package/dist/mcp-pacifica-earn.js +58 -0
- package/dist/mcp-server.js +113 -59
- package/dist/pacifica/client.d.ts +67 -4
- package/dist/pacifica/client.js +170 -8
- package/dist/pacifica/rate-limit.d.ts +144 -0
- package/dist/pacifica/rate-limit.js +267 -0
- package/dist/pacifica/types/order.d.ts +2 -1
- package/dist/plan-executor.js +7 -2
- package/dist/rebalance.js +1 -1
- package/dist/risk.d.ts +2 -1
- package/dist/risk.js +2 -4
- package/dist/settings.d.ts +32 -3
- package/dist/signer/ows-evm.d.ts +1 -1
- package/dist/signer/ows-evm.js +1 -1
- package/dist/smart-order.d.ts +1 -1
- package/dist/smart-order.js +1 -1
- package/dist/strategies/funding-arb.d.ts +1 -1
- package/dist/strategies/funding-arb.js +1 -1
- package/dist/trade-validator.d.ts +1 -1
- package/dist/trade-validator.js +9 -4
- package/dist/utils.d.ts +12 -3
- package/dist/utils.js +11 -3
- package/dist/venue-health.d.ts +40 -0
- package/dist/venue-health.js +75 -0
- package/dist/withdraw-gate.d.ts +1 -1
- package/dist/withdraw-gate.js +1 -1
- package/dist/ws/index.d.ts +959 -0
- package/dist/ws/index.js +4295 -0
- package/dist/ws/node.d.ts +975 -0
- package/dist/ws/node.js +4366 -0
- package/dist/ws/pacifica-instrument.d.ts +8 -0
- package/dist/ws/pacifica-instrument.js +8 -0
- package/dist/ws-cli/credentials.d.ts +62 -0
- package/dist/ws-cli/credentials.js +126 -0
- package/package.json +58 -10
- package/skills/perp-cli/SKILL.md +62 -9
- package/skills/perp-cli/references/agent-operations.md +7 -0
- package/skills/perp-cli/references/commands.md +76 -5
- package/skills/perp-cli/references/ws-library.md +492 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TxFlow transport — UNOFFICIAL API (see venues/txflow.ts).
|
|
3
|
+
*
|
|
4
|
+
* - REST: `POST <rest>/info` with an HL-style `{type, ...}` body. Errors come back three ways and
|
|
5
|
+
* are all checked: a non-2xx status (403 "This action is not allowed." = the read is not on the
|
|
6
|
+
* venue's allowlist), HTTP 200 with a `{message}` body, or a body of an unexpected shape.
|
|
7
|
+
* There is no fallback for a refused read (no HL-endpoint substitution, no guessing).
|
|
8
|
+
* - WS one-shot: open → subscribe `allAssetCtxs` → first data frame → close (bulk mark / funding
|
|
9
|
+
* snapshot). The socket factory is injectable for tests.
|
|
10
|
+
*/
|
|
11
|
+
import WebSocket from "ws";
|
|
12
|
+
import { PerpError, extractErrorMessage } from "../../errors.js";
|
|
13
|
+
import { acquireRate, rateLimitedError } from "../rate-limiter.js";
|
|
14
|
+
export const TXFLOW_HTTP_TIMEOUT_MS = 15_000;
|
|
15
|
+
export const TXFLOW_WS_SNAPSHOT_TIMEOUT_MS = 10_000;
|
|
16
|
+
const UNOFFICIAL_REMEDIATION = (type) => `TxFlow's unofficial API refused "${type}"; perp-cli cannot read this until it is updated (TxFlow has no official API yet)`;
|
|
17
|
+
/** Shape drift in the unofficial API: an explicit EXCHANGE_ERROR, never a guessed value. */
|
|
18
|
+
export function txflowShapeError(what) {
|
|
19
|
+
return new PerpError("EXCHANGE_ERROR", `TxFlow unofficial API shape changed: ${what}`, {
|
|
20
|
+
exchange: "txflow",
|
|
21
|
+
remediation: "TxFlow's undocumented API changed; perp-cli needs an update to read it. TxFlow data is unavailable until then",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function isMessageOnly(body) {
|
|
25
|
+
if (body === null || typeof body !== "object" || Array.isArray(body))
|
|
26
|
+
return false;
|
|
27
|
+
const keys = Object.keys(body);
|
|
28
|
+
return keys.length === 1 && keys[0] === "message" && typeof body.message === "string";
|
|
29
|
+
}
|
|
30
|
+
export class TxflowClient {
|
|
31
|
+
restUrl;
|
|
32
|
+
rateLimit;
|
|
33
|
+
constructor(restUrl, rateLimit) {
|
|
34
|
+
this.restUrl = restUrl;
|
|
35
|
+
this.rateLimit = rateLimit;
|
|
36
|
+
}
|
|
37
|
+
/** POST /info. Returns the parsed body; every error path is a PerpError. */
|
|
38
|
+
async info(body) {
|
|
39
|
+
await acquireRate("txflow", this.rateLimit, body.type);
|
|
40
|
+
let res;
|
|
41
|
+
try {
|
|
42
|
+
res = await fetch(`${this.restUrl}/info`, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: { "content-type": "application/json" },
|
|
45
|
+
body: JSON.stringify(body),
|
|
46
|
+
signal: AbortSignal.timeout(TXFLOW_HTTP_TIMEOUT_MS),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
throw new PerpError("EXCHANGE_UNREACHABLE", `TxFlow ${body.type}: ${extractErrorMessage(e)}`, { exchange: "txflow" });
|
|
51
|
+
}
|
|
52
|
+
const text = await res.text().catch((e) => {
|
|
53
|
+
throw new PerpError("EXCHANGE_UNREACHABLE", `TxFlow ${body.type}: response body unreadable (${extractErrorMessage(e)})`, { exchange: "txflow" });
|
|
54
|
+
});
|
|
55
|
+
if (res.status === 429)
|
|
56
|
+
throw rateLimitedError("txflow", res.headers.get("retry-after"), text);
|
|
57
|
+
let parsed;
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(text);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
parsed = undefined;
|
|
63
|
+
}
|
|
64
|
+
if (res.status === 403) {
|
|
65
|
+
throw new PerpError("EXCHANGE_ERROR", `TxFlow ${body.type}: HTTP 403 ${isMessageOnly(parsed) ? parsed.message : text.slice(0, 200)}`, {
|
|
66
|
+
exchange: "txflow", status: 403, remediation: UNOFFICIAL_REMEDIATION(body.type),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
throw new PerpError("EXCHANGE_ERROR", `TxFlow ${body.type}: HTTP ${res.status} ${text.slice(0, 200)}`, {
|
|
71
|
+
exchange: "txflow", status: res.status, remediation: UNOFFICIAL_REMEDIATION(body.type),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (parsed === undefined)
|
|
75
|
+
throw txflowShapeError(`${body.type} returned a non-JSON body`);
|
|
76
|
+
// HTTP 200 with {message} is an error on this API ("Orderbook for coin '…' not found").
|
|
77
|
+
if (isMessageOnly(parsed)) {
|
|
78
|
+
throw new PerpError("EXCHANGE_ERROR", `TxFlow ${body.type}: ${parsed.message}`, {
|
|
79
|
+
exchange: "txflow", remediation: UNOFFICIAL_REMEDIATION(body.type),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return parsed;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export const defaultWsFactory = url => new WebSocket(url, { handshakeTimeout: TXFLOW_WS_SNAPSHOT_TIMEOUT_MS });
|
|
86
|
+
/**
|
|
87
|
+
* Open the socket, subscribe to `allAssetCtxs`, resolve with the first data frame's `ctxs`, close.
|
|
88
|
+
* No frame within the timeout (or a socket error / close first) → EXCHANGE_UNREACHABLE; a frame
|
|
89
|
+
* of an unexpected shape → EXCHANGE_ERROR (shape drift).
|
|
90
|
+
*/
|
|
91
|
+
export function fetchAllAssetCtxsOnce(wsUrl, factory = defaultWsFactory, timeoutMs = TXFLOW_WS_SNAPSHOT_TIMEOUT_MS) {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
let done = false;
|
|
94
|
+
let ws;
|
|
95
|
+
const finish = (err, value) => {
|
|
96
|
+
if (done)
|
|
97
|
+
return;
|
|
98
|
+
done = true;
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
try {
|
|
101
|
+
ws.close();
|
|
102
|
+
}
|
|
103
|
+
catch { /* already closed */ }
|
|
104
|
+
if (err !== null)
|
|
105
|
+
reject(err);
|
|
106
|
+
else
|
|
107
|
+
resolve(value);
|
|
108
|
+
};
|
|
109
|
+
const timer = setTimeout(() => finish(new PerpError("EXCHANGE_UNREACHABLE", `TxFlow allAssetCtxs snapshot: no data within ${timeoutMs / 1000}s`, { exchange: "txflow" })), timeoutMs);
|
|
110
|
+
try {
|
|
111
|
+
ws = factory(wsUrl);
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
reject(new PerpError("EXCHANGE_UNREACHABLE", `TxFlow WebSocket: ${extractErrorMessage(e)}`, { exchange: "txflow" }));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
ws.on("open", () => {
|
|
119
|
+
try {
|
|
120
|
+
ws.send(JSON.stringify({ method: "subscribe", type: "allAssetCtxs", subscription: { type: "allAssetCtxs" } }));
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
finish(new PerpError("EXCHANGE_UNREACHABLE", `TxFlow WebSocket subscribe failed: ${extractErrorMessage(e)}`, { exchange: "txflow" }));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
ws.on("error", e => finish(new PerpError("EXCHANGE_UNREACHABLE", `TxFlow WebSocket: ${extractErrorMessage(e)}`, { exchange: "txflow" })));
|
|
127
|
+
ws.on("close", code => finish(new PerpError("EXCHANGE_UNREACHABLE", `TxFlow WebSocket closed (code ${code}) before a snapshot arrived`, { exchange: "txflow" })));
|
|
128
|
+
ws.on("message", raw => {
|
|
129
|
+
let msg;
|
|
130
|
+
try {
|
|
131
|
+
msg = JSON.parse(String(raw));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return finish(txflowShapeError("WebSocket frame is not JSON"));
|
|
135
|
+
}
|
|
136
|
+
if (msg === null || typeof msg !== "object")
|
|
137
|
+
return finish(txflowShapeError("WebSocket frame is not an object"));
|
|
138
|
+
const m = msg;
|
|
139
|
+
if (m.channel !== "allAssetCtxs")
|
|
140
|
+
return; // pong / subscription acks / other channels
|
|
141
|
+
const ctxs = m.data?.ctxs;
|
|
142
|
+
if (!Array.isArray(ctxs))
|
|
143
|
+
return finish(txflowShapeError("allAssetCtxs frame has no ctxs array"));
|
|
144
|
+
for (const c of ctxs) {
|
|
145
|
+
if (c === null || typeof c !== "object" || typeof c.coin !== "string"
|
|
146
|
+
|| c.ctx === null || typeof c.ctx !== "object") {
|
|
147
|
+
return finish(txflowShapeError("allAssetCtxs entry without coin / ctx"));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
finish(null, ctxs);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TxFlow public market data — UNOFFICIAL API (see venues/txflow.ts).
|
|
3
|
+
*
|
|
4
|
+
* Sources:
|
|
5
|
+
* POST /info {type:"perpMeta", dex:""} market directory: index (numeric coin id, with gaps),
|
|
6
|
+
* name "BTC-USDC", baseCurrency, szDecimals (can be
|
|
7
|
+
* NEGATIVE: lot 10, 100 …), priceTick, maxLeverage,
|
|
8
|
+
* haltTrading, delisted, tagIds
|
|
9
|
+
* WS allAssetCtxs (one-shot snapshot) per coin: markPx, oraclePx, funding, openInterest,
|
|
10
|
+
* nextFundingTime, prevFundingTime. `coin` == perpMeta
|
|
11
|
+
* `index` (as a string) — joined by id, never by position.
|
|
12
|
+
*
|
|
13
|
+
* Funding: `funding` is PERCENT per hour (settlementIntervalMs 3600000 on every market; 0.00125 is
|
|
14
|
+
* the HL-style 0.00125 %/h baseline). Converted to a fraction per hour by an exact two-place decimal
|
|
15
|
+
* shift (no float maths). The value moves every tick: an accruing / predicted estimate, not a
|
|
16
|
+
* settled print — the venue is fundingVerified false. A market whose next − prev funding time is
|
|
17
|
+
* not one hour gets no funding row.
|
|
18
|
+
*/
|
|
19
|
+
import type { VenuePublicData } from "../venue.js";
|
|
20
|
+
import type { AssetClass, FundingRateBasis, Network, VenueFundingRate, VenueMarket } from "../venue-types.js";
|
|
21
|
+
import { TxflowClient, type TxflowRawAssetCtx, type WsFactory } from "./client.js";
|
|
22
|
+
export declare const TXFLOW_FUNDING_BASIS: FundingRateBasis;
|
|
23
|
+
export declare const TXFLOW_SETTLEMENT_MS = 3600000;
|
|
24
|
+
/** perpMeta tag id of the TradFi (equity / commodity / ETF) markets. */
|
|
25
|
+
export declare const TXFLOW_TRADFI_TAG = 5;
|
|
26
|
+
export interface TxflowMarketMeta {
|
|
27
|
+
/** Numeric coin id (perpMeta `index`); its decimal string is the wire id and venueSymbol. */
|
|
28
|
+
index: number;
|
|
29
|
+
name: string;
|
|
30
|
+
symbol: string;
|
|
31
|
+
quote: string;
|
|
32
|
+
tickSize: string;
|
|
33
|
+
stepSize: string;
|
|
34
|
+
maxLeverage: number;
|
|
35
|
+
halted: boolean;
|
|
36
|
+
tagIds: number[];
|
|
37
|
+
assetClass: AssetClass;
|
|
38
|
+
canonical: string | null;
|
|
39
|
+
}
|
|
40
|
+
/** Lot step from szDecimals, exactly: 4 → "0.0001", 0 → "1", -2 → "100". */
|
|
41
|
+
export declare function stepFromSzDecimals(sz: number): string;
|
|
42
|
+
/** Exact percent → fraction (two-place decimal shift on the string; "0.00125" → "0.0000125"). */
|
|
43
|
+
export declare function percentToFraction(pct: string): string;
|
|
44
|
+
/** Parse one perpMeta row; shape drift is EXCHANGE_ERROR. Returns null for a delisted market. */
|
|
45
|
+
export declare function toTxflowMarketMeta(raw: unknown): TxflowMarketMeta | null;
|
|
46
|
+
export declare function fetchTxflowDirectory(client: TxflowClient): Promise<TxflowMarketMeta[]>;
|
|
47
|
+
export interface TxflowCtx {
|
|
48
|
+
markPx: string;
|
|
49
|
+
oraclePx: string | null;
|
|
50
|
+
/** Percent per hour, as published; null when absent. */
|
|
51
|
+
fundingPct: string | null;
|
|
52
|
+
openInterest: string | null;
|
|
53
|
+
nextFundingTime: number | null;
|
|
54
|
+
prevFundingTime: number | null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Join snapshot entries to the directory by coin id. An entry whose coin id is not a listed
|
|
58
|
+
* market is dropped with a warning (never matched by position); an entry without a mark price
|
|
59
|
+
* is dropped too (a market row needs one).
|
|
60
|
+
*/
|
|
61
|
+
export declare function joinSnapshot(dir: readonly TxflowMarketMeta[], ctxs: readonly TxflowRawAssetCtx[], warn: (msg: string) => void): Map<number, TxflowCtx>;
|
|
62
|
+
/**
|
|
63
|
+
* Hourly funding fraction for one ctx, or null (unknown) when the rate is absent or the one-hour
|
|
64
|
+
* interval cannot be PROVEN from next − prev funding time (a missing time is unknown, never assumed 1h).
|
|
65
|
+
*/
|
|
66
|
+
export declare function hourlyFundingOf(ctx: TxflowCtx): string | null;
|
|
67
|
+
export declare function toTxflowVenueMarket(m: TxflowMarketMeta, ctx: TxflowCtx): VenueMarket;
|
|
68
|
+
export interface TxflowPublicDeps {
|
|
69
|
+
wsFactory: WsFactory;
|
|
70
|
+
warn: (msg: string) => void;
|
|
71
|
+
}
|
|
72
|
+
export declare function fetchTxflowMarkets(net: Network, deps?: TxflowPublicDeps): Promise<VenueMarket[]>;
|
|
73
|
+
export declare function fetchTxflowFundingRates(net: Network, deps?: TxflowPublicDeps): Promise<VenueFundingRate[]>;
|
|
74
|
+
export declare const txflowPublicData: VenuePublicData;
|
|
75
|
+
/** Resolve a CLI symbol ("btc", "BTC-USDC", "BTC-PERP", or the numeric coin id) against the directory. */
|
|
76
|
+
export declare function resolveTxflowMarket(dir: readonly TxflowMarketMeta[], input: string): TxflowMarketMeta;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TxFlow public market data — UNOFFICIAL API (see venues/txflow.ts).
|
|
3
|
+
*
|
|
4
|
+
* Sources:
|
|
5
|
+
* POST /info {type:"perpMeta", dex:""} market directory: index (numeric coin id, with gaps),
|
|
6
|
+
* name "BTC-USDC", baseCurrency, szDecimals (can be
|
|
7
|
+
* NEGATIVE: lot 10, 100 …), priceTick, maxLeverage,
|
|
8
|
+
* haltTrading, delisted, tagIds
|
|
9
|
+
* WS allAssetCtxs (one-shot snapshot) per coin: markPx, oraclePx, funding, openInterest,
|
|
10
|
+
* nextFundingTime, prevFundingTime. `coin` == perpMeta
|
|
11
|
+
* `index` (as a string) — joined by id, never by position.
|
|
12
|
+
*
|
|
13
|
+
* Funding: `funding` is PERCENT per hour (settlementIntervalMs 3600000 on every market; 0.00125 is
|
|
14
|
+
* the HL-style 0.00125 %/h baseline). Converted to a fraction per hour by an exact two-place decimal
|
|
15
|
+
* shift (no float maths). The value moves every tick: an accruing / predicted estimate, not a
|
|
16
|
+
* settled print — the venue is fundingVerified false. A market whose next − prev funding time is
|
|
17
|
+
* not one hour gets no funding row.
|
|
18
|
+
*/
|
|
19
|
+
import { PerpError } from "../../errors.js";
|
|
20
|
+
import { canonicalFor } from "../canonical-symbols.js";
|
|
21
|
+
import { networkConfig } from "../venue.js";
|
|
22
|
+
import { txflowVenue } from "../venues/txflow.js";
|
|
23
|
+
import { fetchAllAssetCtxsOnce, txflowShapeError, TxflowClient, defaultWsFactory } from "./client.js";
|
|
24
|
+
export const TXFLOW_FUNDING_BASIS = { kind: "perHours", hours: 1 };
|
|
25
|
+
export const TXFLOW_SETTLEMENT_MS = 3_600_000;
|
|
26
|
+
/** perpMeta tag id of the TradFi (equity / commodity / ETF) markets. */
|
|
27
|
+
export const TXFLOW_TRADFI_TAG = 5;
|
|
28
|
+
/**
|
|
29
|
+
* TradFi markets by class. Equities are matched by ticker identity; commodities / ETFs / pre-IPO only
|
|
30
|
+
* through EXPLICIT_CANONICAL. A TradFi market missing here is listed as equity with NO canonical
|
|
31
|
+
* symbol (never matched across venues) until it is classified.
|
|
32
|
+
*/
|
|
33
|
+
const TRADFI_CLASS = {
|
|
34
|
+
XAU: "commodity", XAG: "commodity", XPD: "commodity", XPT: "commodity", CL: "commodity", BZ: "commodity",
|
|
35
|
+
SPY: "etf", QQQ: "etf", EWJ: "etf", SOXL: "etf", URNM: "etf", UVXY: "etf",
|
|
36
|
+
SPCX: "preIpo",
|
|
37
|
+
MSTR: "equity", CRCL: "equity", TSLA: "equity", COIN: "equity", HOOD: "equity", META: "equity", MU: "equity",
|
|
38
|
+
AAPL: "equity", AMZN: "equity", GOOGL: "equity", INTC: "equity", NVDA: "equity", PLTR: "equity", SNDK: "equity",
|
|
39
|
+
TSM: "equity", MSFT: "equity", AMD: "equity", LITE: "equity", AVGO: "equity", CRWV: "equity", QCOM: "equity",
|
|
40
|
+
ARM: "equity", LRCX: "equity", SKHYNIX: "equity", SAMSUNG: "equity", STXX: "equity",
|
|
41
|
+
};
|
|
42
|
+
/** Lot step from szDecimals, exactly: 4 → "0.0001", 0 → "1", -2 → "100". */
|
|
43
|
+
export function stepFromSzDecimals(sz) {
|
|
44
|
+
if (!Number.isInteger(sz))
|
|
45
|
+
throw txflowShapeError(`szDecimals ${sz} is not an integer`);
|
|
46
|
+
if (sz > 0)
|
|
47
|
+
return `0.${"0".repeat(sz - 1)}1`;
|
|
48
|
+
return `1${"0".repeat(-sz)}`;
|
|
49
|
+
}
|
|
50
|
+
/** Exact percent → fraction (two-place decimal shift on the string; "0.00125" → "0.0000125"). */
|
|
51
|
+
export function percentToFraction(pct) {
|
|
52
|
+
const s = pct.trim();
|
|
53
|
+
if (!/^-?\d+(\.\d+)?$/.test(s))
|
|
54
|
+
throw txflowShapeError(`funding "${pct}" is not a plain decimal`);
|
|
55
|
+
const neg = s.startsWith("-");
|
|
56
|
+
const body = neg ? s.slice(1) : s;
|
|
57
|
+
const [ip, fp = ""] = body.split(".");
|
|
58
|
+
const digits = `${ip}${fp}`;
|
|
59
|
+
const scale = fp.length + 2;
|
|
60
|
+
const padded = digits.padStart(scale + 1, "0");
|
|
61
|
+
let out = `${padded.slice(0, padded.length - scale)}.${padded.slice(padded.length - scale)}`;
|
|
62
|
+
out = out.replace(/0+$/, "").replace(/\.$/, "");
|
|
63
|
+
out = out.replace(/^0+(?=\d)/, "");
|
|
64
|
+
if (/^0(\.0*)?$/.test(out) || out === "")
|
|
65
|
+
return "0";
|
|
66
|
+
return neg ? `-${out}` : out;
|
|
67
|
+
}
|
|
68
|
+
function assetClassOf(symbol, tagIds) {
|
|
69
|
+
if (!tagIds.includes(TXFLOW_TRADFI_TAG))
|
|
70
|
+
return "crypto";
|
|
71
|
+
return TRADFI_CLASS[symbol] ?? "equity";
|
|
72
|
+
}
|
|
73
|
+
function canonicalOf(symbol, tagIds, assetClass) {
|
|
74
|
+
// An unclassified TradFi market is never matched across venues.
|
|
75
|
+
if (tagIds.includes(TXFLOW_TRADFI_TAG) && TRADFI_CLASS[symbol] === undefined)
|
|
76
|
+
return null;
|
|
77
|
+
return canonicalFor("txflow", symbol, assetClass);
|
|
78
|
+
}
|
|
79
|
+
/** Parse one perpMeta row; shape drift is EXCHANGE_ERROR. Returns null for a delisted market. */
|
|
80
|
+
export function toTxflowMarketMeta(raw) {
|
|
81
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
82
|
+
throw txflowShapeError("perpMeta universe row is not an object");
|
|
83
|
+
const r = raw;
|
|
84
|
+
if (typeof r.index !== "number" || !Number.isInteger(r.index) || r.index < 0)
|
|
85
|
+
throw txflowShapeError(`perpMeta row ${String(r.name)} has a non-numeric index`);
|
|
86
|
+
if (typeof r.name !== "string" || typeof r.baseCurrency !== "string" || r.baseCurrency === "")
|
|
87
|
+
throw txflowShapeError(`perpMeta row ${r.index} has no name / baseCurrency`);
|
|
88
|
+
if (typeof r.quoteCurrency !== "string")
|
|
89
|
+
throw txflowShapeError(`perpMeta ${r.name} has no quoteCurrency`);
|
|
90
|
+
if (typeof r.szDecimals !== "number")
|
|
91
|
+
throw txflowShapeError(`perpMeta ${r.name} has no szDecimals`);
|
|
92
|
+
if (typeof r.priceTick !== "string" || !/^\d+(\.\d+)?$/.test(r.priceTick) || !(Number(r.priceTick) > 0))
|
|
93
|
+
throw txflowShapeError(`perpMeta ${r.name} has no valid priceTick`);
|
|
94
|
+
if (typeof r.maxLeverage !== "number" || !(r.maxLeverage > 0))
|
|
95
|
+
throw txflowShapeError(`perpMeta ${r.name} has no maxLeverage`);
|
|
96
|
+
if (typeof r.haltTrading !== "boolean" || typeof r.delisted !== "boolean")
|
|
97
|
+
throw txflowShapeError(`perpMeta ${r.name} has no haltTrading / delisted flags`);
|
|
98
|
+
if (!Array.isArray(r.tagIds) || r.tagIds.some(t => typeof t !== "number"))
|
|
99
|
+
throw txflowShapeError(`perpMeta ${r.name} has no tagIds`);
|
|
100
|
+
if (r.delisted)
|
|
101
|
+
return null;
|
|
102
|
+
const symbol = r.baseCurrency.toUpperCase();
|
|
103
|
+
const tagIds = r.tagIds;
|
|
104
|
+
const assetClass = assetClassOf(symbol, tagIds);
|
|
105
|
+
return {
|
|
106
|
+
index: r.index,
|
|
107
|
+
name: r.name,
|
|
108
|
+
symbol,
|
|
109
|
+
quote: r.quoteCurrency,
|
|
110
|
+
tickSize: r.priceTick,
|
|
111
|
+
stepSize: stepFromSzDecimals(r.szDecimals),
|
|
112
|
+
maxLeverage: r.maxLeverage,
|
|
113
|
+
halted: r.haltTrading,
|
|
114
|
+
tagIds,
|
|
115
|
+
assetClass,
|
|
116
|
+
canonical: canonicalOf(symbol, tagIds, assetClass),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export async function fetchTxflowDirectory(client) {
|
|
120
|
+
const res = await client.info({ type: "perpMeta", dex: "" });
|
|
121
|
+
if (res === null || typeof res !== "object" || !Array.isArray(res.universe))
|
|
122
|
+
throw txflowShapeError("perpMeta has no universe array");
|
|
123
|
+
const out = [];
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
for (const row of res.universe) {
|
|
126
|
+
const m = toTxflowMarketMeta(row);
|
|
127
|
+
if (m === null)
|
|
128
|
+
continue;
|
|
129
|
+
if (seen.has(m.symbol))
|
|
130
|
+
throw txflowShapeError(`perpMeta lists ${m.symbol} twice`);
|
|
131
|
+
seen.add(m.symbol);
|
|
132
|
+
out.push(m);
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
function decOrNull(v) {
|
|
137
|
+
return typeof v === "string" && /^-?\d+(\.\d+)?$/.test(v) ? v : null;
|
|
138
|
+
}
|
|
139
|
+
function msOrNull(v) {
|
|
140
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Join snapshot entries to the directory by coin id. An entry whose coin id is not a listed
|
|
144
|
+
* market is dropped with a warning (never matched by position); an entry without a mark price
|
|
145
|
+
* is dropped too (a market row needs one).
|
|
146
|
+
*/
|
|
147
|
+
export function joinSnapshot(dir, ctxs, warn) {
|
|
148
|
+
const byIndex = new Map(dir.map(m => [String(m.index), m]));
|
|
149
|
+
const out = new Map();
|
|
150
|
+
const unknown = [];
|
|
151
|
+
const unpriced = [];
|
|
152
|
+
for (const { coin, ctx } of ctxs) {
|
|
153
|
+
const meta = byIndex.get(coin);
|
|
154
|
+
if (meta === undefined) {
|
|
155
|
+
unknown.push(coin);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const markPx = decOrNull(ctx.markPx);
|
|
159
|
+
if (markPx === null || !(Number(markPx) > 0)) {
|
|
160
|
+
unpriced.push(meta.symbol);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
out.set(meta.index, {
|
|
164
|
+
markPx,
|
|
165
|
+
oraclePx: decOrNull(ctx.oraclePx),
|
|
166
|
+
fundingPct: decOrNull(ctx.funding),
|
|
167
|
+
openInterest: decOrNull(ctx.openInterest),
|
|
168
|
+
nextFundingTime: msOrNull(ctx.nextFundingTime),
|
|
169
|
+
prevFundingTime: msOrNull(ctx.prevFundingTime),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (unknown.length > 0)
|
|
173
|
+
warn(`[txflow] dropped ${unknown.length} snapshot entr${unknown.length === 1 ? "y" : "ies"} for unknown coin id(s): ${unknown.join(", ")}`);
|
|
174
|
+
if (unpriced.length > 0)
|
|
175
|
+
warn(`[txflow] dropped ${unpriced.length} snapshot entr${unpriced.length === 1 ? "y" : "ies"} without a mark price: ${unpriced.join(", ")}`);
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Hourly funding fraction for one ctx, or null (unknown) when the rate is absent or the one-hour
|
|
180
|
+
* interval cannot be PROVEN from next − prev funding time (a missing time is unknown, never assumed 1h).
|
|
181
|
+
*/
|
|
182
|
+
export function hourlyFundingOf(ctx) {
|
|
183
|
+
if (ctx.fundingPct === null)
|
|
184
|
+
return null;
|
|
185
|
+
if (ctx.nextFundingTime === null || ctx.prevFundingTime === null)
|
|
186
|
+
return null;
|
|
187
|
+
if (ctx.nextFundingTime - ctx.prevFundingTime !== TXFLOW_SETTLEMENT_MS)
|
|
188
|
+
return null;
|
|
189
|
+
return percentToFraction(ctx.fundingPct);
|
|
190
|
+
}
|
|
191
|
+
// ── Mapping ──
|
|
192
|
+
export function toTxflowVenueMarket(m, ctx) {
|
|
193
|
+
return {
|
|
194
|
+
venue: "txflow",
|
|
195
|
+
venueSymbol: String(m.index),
|
|
196
|
+
symbol: m.symbol,
|
|
197
|
+
canonical: m.canonical,
|
|
198
|
+
assetClass: m.assetClass,
|
|
199
|
+
settle: "USDC",
|
|
200
|
+
quoteAsset: m.quote,
|
|
201
|
+
markPrice: ctx.markPx,
|
|
202
|
+
indexPrice: ctx.oraclePx,
|
|
203
|
+
fundingRate: hourlyFundingOf(ctx),
|
|
204
|
+
fundingRateBasis: TXFLOW_FUNDING_BASIS,
|
|
205
|
+
settlementIntervalHours: 1,
|
|
206
|
+
// Base units (as Hyperliquid publishes it) [inferred].
|
|
207
|
+
openInterest: ctx.openInterest,
|
|
208
|
+
volume24h: null,
|
|
209
|
+
tickSize: m.tickSize,
|
|
210
|
+
stepSize: m.stepSize,
|
|
211
|
+
minSize: null,
|
|
212
|
+
minNotional: null,
|
|
213
|
+
maxLeverage: m.maxLeverage,
|
|
214
|
+
marginTiers: null,
|
|
215
|
+
session: null,
|
|
216
|
+
tradable: !m.halted,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const DEFAULT_DEPS = {
|
|
220
|
+
wsFactory: defaultWsFactory,
|
|
221
|
+
warn: msg => process.stderr.write(`${msg}\n`),
|
|
222
|
+
};
|
|
223
|
+
function clientFor(net) {
|
|
224
|
+
const cfg = networkConfig(txflowVenue, net); // testnet → NOT_IMPLEMENTED (no testnet)
|
|
225
|
+
if (cfg.wsUrl === null)
|
|
226
|
+
throw new Error("txflow venue definition has no wsUrl");
|
|
227
|
+
return { client: new TxflowClient(cfg.restUrl, txflowVenue.rateLimit), wsUrl: cfg.wsUrl };
|
|
228
|
+
}
|
|
229
|
+
/** Directory + one snapshot, joined; markets missing from the snapshot are dropped with a warning. */
|
|
230
|
+
async function fetchJoined(net, deps) {
|
|
231
|
+
const { client, wsUrl } = clientFor(net);
|
|
232
|
+
const [dir, ctxs] = await Promise.all([fetchTxflowDirectory(client), fetchAllAssetCtxsOnce(wsUrl, deps.wsFactory)]);
|
|
233
|
+
const joined = joinSnapshot(dir, ctxs, deps.warn);
|
|
234
|
+
const out = [];
|
|
235
|
+
const missing = [];
|
|
236
|
+
for (const meta of dir) {
|
|
237
|
+
const ctx = joined.get(meta.index);
|
|
238
|
+
if (ctx === undefined) {
|
|
239
|
+
missing.push(meta.symbol);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
out.push({ meta, ctx });
|
|
243
|
+
}
|
|
244
|
+
if (missing.length > 0)
|
|
245
|
+
deps.warn(`[txflow] ${missing.length} listed market(s) had no snapshot entry and are not shown: ${missing.join(", ")}`);
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
export async function fetchTxflowMarkets(net, deps = DEFAULT_DEPS) {
|
|
249
|
+
return (await fetchJoined(net, deps)).map(({ meta, ctx }) => toTxflowVenueMarket(meta, ctx));
|
|
250
|
+
}
|
|
251
|
+
export async function fetchTxflowFundingRates(net, deps = DEFAULT_DEPS) {
|
|
252
|
+
const out = [];
|
|
253
|
+
for (const { meta, ctx } of await fetchJoined(net, deps)) {
|
|
254
|
+
const rate = hourlyFundingOf(ctx);
|
|
255
|
+
if (rate === null)
|
|
256
|
+
continue;
|
|
257
|
+
out.push({
|
|
258
|
+
venue: "txflow",
|
|
259
|
+
venueSymbol: String(meta.index),
|
|
260
|
+
symbol: meta.symbol,
|
|
261
|
+
canonical: meta.canonical,
|
|
262
|
+
rate,
|
|
263
|
+
basis: TXFLOW_FUNDING_BASIS,
|
|
264
|
+
markPrice: ctx.markPx,
|
|
265
|
+
nextFundingTime: ctx.nextFundingTime,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
export const txflowPublicData = {
|
|
271
|
+
fetchMarkets: net => fetchTxflowMarkets(net),
|
|
272
|
+
fetchFundingRates: net => fetchTxflowFundingRates(net),
|
|
273
|
+
};
|
|
274
|
+
/** Resolve a CLI symbol ("btc", "BTC-USDC", "BTC-PERP", or the numeric coin id) against the directory. */
|
|
275
|
+
export function resolveTxflowMarket(dir, input) {
|
|
276
|
+
const up = input.trim().toUpperCase().replace(/-PERP$/, "");
|
|
277
|
+
const hit = dir.find(m => m.symbol === up || m.name.toUpperCase() === up || String(m.index) === up);
|
|
278
|
+
if (hit === undefined) {
|
|
279
|
+
throw new PerpError("SYMBOL_NOT_FOUND", `TxFlow has no listed market ${input}`, {
|
|
280
|
+
exchange: "txflow", remediation: "List markets: perp -e txflow market list",
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return hit;
|
|
284
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unknown-symbol handling shared by the adapters' public market-data reads.
|
|
3
|
+
*
|
|
4
|
+
* Several venues answer a read for a market they do not list with an empty success
|
|
5
|
+
* (Pacifica /trades, /kline, /funding_rate/history; Ondo /v1/perps/trades and candles; Aster
|
|
6
|
+
* /fapi/v3/fundingRate — all `200 []`, live 2026-10-14). An empty list must not stand for
|
|
7
|
+
* "no such market": after an empty answer the symbol is checked against the venue's own market
|
|
8
|
+
* directory (getMarkets) — SYMBOL_NOT_FOUND when it is not listed, the empty list when it is
|
|
9
|
+
* (a listed market with no trades / candles in the window is a real empty result).
|
|
10
|
+
*/
|
|
11
|
+
import { PerpError } from "../errors.js";
|
|
12
|
+
import type { ExchangeMarketInfo } from "./interface.js";
|
|
13
|
+
/** SYMBOL_NOT_FOUND for `symbol` on `venue`, with the listing command as remediation. */
|
|
14
|
+
export declare function symbolNotFound(venue: string, symbol: string, message?: string): PerpError;
|
|
15
|
+
/** Directory match: case-insensitive, a "-PERP" display suffix ignored on either side. */
|
|
16
|
+
export declare function listsSymbol(markets: readonly Pick<ExchangeMarketInfo, "symbol">[], symbol: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* `rows` as returned by the venue, unless they are empty and the market is not in the venue's
|
|
19
|
+
* market directory — then SYMBOL_NOT_FOUND. The directory is read only for an empty answer.
|
|
20
|
+
*
|
|
21
|
+
* `directorySymbol` is the symbol in the directory's form, derived by the adapter from what the
|
|
22
|
+
* user typed (Ondo "TSLA-USD.P" → "TSLA", Aster "BTCUSDT" → "BTC"); `symbol` is kept for messages.
|
|
23
|
+
* A failed directory read is re-thrown with its code (and so its retryable flag), saying what was
|
|
24
|
+
* being checked — never turned into SYMBOL_NOT_FOUND or an empty result.
|
|
25
|
+
*/
|
|
26
|
+
export declare function nonEmptyOrListed<T>(adapter: {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
getMarkets(): Promise<Pick<ExchangeMarketInfo, "symbol">[]>;
|
|
29
|
+
}, symbol: string, rows: T[], directorySymbol?: string): Promise<T[]>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unknown-symbol handling shared by the adapters' public market-data reads.
|
|
3
|
+
*
|
|
4
|
+
* Several venues answer a read for a market they do not list with an empty success
|
|
5
|
+
* (Pacifica /trades, /kline, /funding_rate/history; Ondo /v1/perps/trades and candles; Aster
|
|
6
|
+
* /fapi/v3/fundingRate — all `200 []`, live 2026-10-14). An empty list must not stand for
|
|
7
|
+
* "no such market": after an empty answer the symbol is checked against the venue's own market
|
|
8
|
+
* directory (getMarkets) — SYMBOL_NOT_FOUND when it is not listed, the empty list when it is
|
|
9
|
+
* (a listed market with no trades / candles in the window is a real empty result).
|
|
10
|
+
*/
|
|
11
|
+
import { PerpError, classifyError } from "../errors.js";
|
|
12
|
+
/** SYMBOL_NOT_FOUND for `symbol` on `venue`, with the listing command as remediation. */
|
|
13
|
+
export function symbolNotFound(venue, symbol, message) {
|
|
14
|
+
return new PerpError("SYMBOL_NOT_FOUND", message ?? `${venue} has no market ${symbol.toUpperCase()}`, {
|
|
15
|
+
exchange: venue,
|
|
16
|
+
remediation: `List markets: perp -e ${venue} market list`,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/** Directory match: case-insensitive, a "-PERP" display suffix ignored on either side. */
|
|
20
|
+
export function listsSymbol(markets, symbol) {
|
|
21
|
+
const norm = (s) => s.toUpperCase().replace(/-PERP$/, "");
|
|
22
|
+
const want = norm(symbol);
|
|
23
|
+
return markets.some(m => norm(m.symbol) === want);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* `rows` as returned by the venue, unless they are empty and the market is not in the venue's
|
|
27
|
+
* market directory — then SYMBOL_NOT_FOUND. The directory is read only for an empty answer.
|
|
28
|
+
*
|
|
29
|
+
* `directorySymbol` is the symbol in the directory's form, derived by the adapter from what the
|
|
30
|
+
* user typed (Ondo "TSLA-USD.P" → "TSLA", Aster "BTCUSDT" → "BTC"); `symbol` is kept for messages.
|
|
31
|
+
* A failed directory read is re-thrown with its code (and so its retryable flag), saying what was
|
|
32
|
+
* being checked — never turned into SYMBOL_NOT_FOUND or an empty result.
|
|
33
|
+
*/
|
|
34
|
+
export async function nonEmptyOrListed(adapter, symbol, rows, directorySymbol = symbol) {
|
|
35
|
+
if (rows.length > 0)
|
|
36
|
+
return rows;
|
|
37
|
+
let markets;
|
|
38
|
+
try {
|
|
39
|
+
markets = await adapter.getMarkets();
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
const c = classifyError(e, adapter.name);
|
|
43
|
+
throw new PerpError(c.code, `${adapter.name}: ${symbol} returned no rows and the market list could not be read to check the symbol — ${c.message}`, {
|
|
44
|
+
exchange: adapter.name,
|
|
45
|
+
...(c.remediation !== undefined ? { remediation: c.remediation } : {}),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (!listsSymbol(markets, directorySymbol))
|
|
49
|
+
throw symbolNotFound(adapter.name, symbol);
|
|
50
|
+
return rows;
|
|
51
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variational public-only adapter — OFFICIAL read-only API (venues/variational.ts).
|
|
3
|
+
*
|
|
4
|
+
* Markets, current funding terms and the OLP's indicative RFQ quotes come from the one stats
|
|
5
|
+
* payload (variational/public.ts). Variational is an RFQ venue with a single maker, so there is no
|
|
6
|
+
* order book, trade tape or candle feed in the API: getOrderbook / getRecentTrades / getKlines /
|
|
7
|
+
* getFundingHistory refuse with CAPABILITY_UNSUPPORTED (with the command that does work), and every
|
|
8
|
+
* account / trading method refuses before any request.
|
|
9
|
+
*/
|
|
10
|
+
import type { CurrentFundingTerms, RfqQuotes, ExchangeAdapter, ExchangeBalance, ExchangeFundingPayment, ExchangeKline, ExchangeMarketInfo, ExchangeOrder, ExchangePosition, ExchangeTrade, ExchangePublicTrade } from "../interface.js";
|
|
11
|
+
import { type VenueDefinition } from "../venue.js";
|
|
12
|
+
import { type Network } from "../venue-types.js";
|
|
13
|
+
import { type VariationalSwapRate } from "./public.js";
|
|
14
|
+
/** Legacy market row plus the Variational-only fields every row carries in JSON output. */
|
|
15
|
+
export type VariationalMarketInfo = ExchangeMarketInfo & {
|
|
16
|
+
venueApi: "official";
|
|
17
|
+
ticker: string;
|
|
18
|
+
contractType: "perpetual" | "swap";
|
|
19
|
+
name: string | null;
|
|
20
|
+
/** "predicted" for a perpetual with a rate; null otherwise. */
|
|
21
|
+
fundingRateKind: "predicted" | null;
|
|
22
|
+
nextFundingTime: number | null;
|
|
23
|
+
/** USDC, single-sided (trader positions only; the OLP is the counterparty). */
|
|
24
|
+
openInterestUsd: {
|
|
25
|
+
long: string;
|
|
26
|
+
short: string;
|
|
27
|
+
};
|
|
28
|
+
swapFunding: {
|
|
29
|
+
last: VariationalSwapRate | null;
|
|
30
|
+
next: VariationalSwapRate | null;
|
|
31
|
+
} | null;
|
|
32
|
+
};
|
|
33
|
+
export declare class VariationalPublicAdapter implements ExchangeAdapter {
|
|
34
|
+
readonly venue: VenueDefinition;
|
|
35
|
+
readonly network: Network;
|
|
36
|
+
readonly name: "variational";
|
|
37
|
+
readonly aliases: readonly ["var", "omni"];
|
|
38
|
+
constructor(venue: VenueDefinition, network: Network);
|
|
39
|
+
private warn;
|
|
40
|
+
private listing;
|
|
41
|
+
getMarkets(): Promise<VariationalMarketInfo[]>;
|
|
42
|
+
/** Current funding terms of one market (the API publishes no funding history). */
|
|
43
|
+
getFundingTerms(symbol: string): Promise<CurrentFundingTerms>;
|
|
44
|
+
/** The OLP's indicative RFQ quotes (Variational has no order book). */
|
|
45
|
+
getRfqQuotes(symbol: string): Promise<RfqQuotes>;
|
|
46
|
+
getOrderbook(): Promise<{
|
|
47
|
+
bids: [string, string][];
|
|
48
|
+
asks: [string, string][];
|
|
49
|
+
}>;
|
|
50
|
+
getRecentTrades(): Promise<ExchangePublicTrade[]>;
|
|
51
|
+
getFundingHistory(): Promise<{
|
|
52
|
+
time: number;
|
|
53
|
+
rate: string;
|
|
54
|
+
price: string | null;
|
|
55
|
+
}[]>;
|
|
56
|
+
getKlines(): Promise<ExchangeKline[]>;
|
|
57
|
+
private refuse;
|
|
58
|
+
getBalance(): Promise<ExchangeBalance>;
|
|
59
|
+
getPositions(): Promise<ExchangePosition[]>;
|
|
60
|
+
getOpenOrders(): Promise<ExchangeOrder[]>;
|
|
61
|
+
getOrderHistory(): Promise<ExchangeOrder[]>;
|
|
62
|
+
getTradeHistory(): Promise<ExchangeTrade[]>;
|
|
63
|
+
getFundingPayments(): Promise<ExchangeFundingPayment[]>;
|
|
64
|
+
marketOrder(): Promise<unknown>;
|
|
65
|
+
limitOrder(): Promise<unknown>;
|
|
66
|
+
editOrder(): Promise<unknown>;
|
|
67
|
+
cancelOrder(): Promise<unknown>;
|
|
68
|
+
cancelAllOrders(): Promise<unknown>;
|
|
69
|
+
setLeverage(): Promise<unknown>;
|
|
70
|
+
stopOrder(): Promise<unknown>;
|
|
71
|
+
}
|