@reefclaw/openclaw-plugin 0.1.5 → 0.1.6
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/ccxt/binance-private.d.ts +21 -0
- package/ccxt/binance-private.js +132 -22
- package/config/plugin-config-io.d.ts +6 -0
- package/config/tool-gate.js +3 -0
- package/exchange-adapter.d.ts +16 -0
- package/index.js +551 -54
- package/lifecycle/trading-operation-lock.d.ts +17 -0
- package/lifecycle/trading-operation-lock.js +14 -0
- package/live/bracket-id.d.ts +2 -3
- package/live/bracket-id.js +22 -9
- package/live/live-adapter.d.ts +24 -1
- package/live/live-adapter.js +114 -2
- package/live/local-signal-service.js +11 -6
- package/live/local-strategy-evaluator.js +4 -0
- package/live/proposal-decision-listener.d.ts +6 -0
- package/live/proposal-decision-listener.js +4 -0
- package/live/stop-watcher.d.ts +27 -1
- package/live/stop-watcher.js +59 -2
- package/onboarding/runtime.d.ts +13 -0
- package/onboarding/runtime.js +22 -2
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
- package/portfolio/wave9-admission.d.ts +67 -0
- package/portfolio/wave9-admission.js +262 -0
- package/portfolio/wave9-policy.d.ts +36 -0
- package/portfolio/wave9-policy.js +183 -0
- package/signals/conditions/registry.js +50 -0
- package/simulator/exchange-simulator.js +5 -0
- package/simulator/fill-engine.js +5 -1
- package/simulator/types.d.ts +6 -1
- package/strategy/evaluator.d.ts +3 -0
- package/strategy/evaluator.js +5 -0
- package/tools/assessment-validation.d.ts +2 -0
- package/tools/attach-brackets.d.ts +7 -2
- package/tools/attach-brackets.js +36 -0
- package/tools/cancel-all-orders.d.ts +2 -0
- package/tools/cancel-all-orders.js +4 -1
- package/tools/cancel-order.d.ts +4 -0
- package/tools/cancel-order.js +49 -3
- package/tools/close-position.d.ts +23 -0
- package/tools/close-position.js +286 -13
- package/tools/create-order.d.ts +28 -0
- package/tools/create-order.js +1364 -192
- package/tools/get-analytics.js +2 -2
- package/tools/get-basis.js +2 -2
- package/tools/get-cascade-risk.js +2 -2
- package/tools/get-crypto-metrics.js +14 -4
- package/tools/get-cvd.js +2 -2
- package/tools/get-divergences.js +2 -2
- package/tools/get-funding-context.js +2 -2
- package/tools/get-liquidation-levels.js +2 -2
- package/tools/get-liquidation-pulse.js +2 -2
- package/tools/get-pattern-scan.js +2 -2
- package/tools/get-regime.js +2 -2
- package/tools/get-resting-liquidity.js +2 -2
- package/tools/get-risk-scenario.js +2 -2
- package/tools/get-session-review.js +2 -2
- package/tools/get-setup-detail.js +9 -1
- package/tools/get-signals.js +2 -2
- package/tools/get-sizing.js +2 -2
- package/tools/get-trade-feedback.js +2 -2
- package/tools/get-trade-flow.js +2 -2
- package/tools/get-volume-profile.js +2 -2
- package/tools/get-wave9-status.d.ts +127 -0
- package/tools/get-wave9-status.js +796 -0
- package/tools/intel-api.d.ts +20 -0
- package/tools/intel-api.js +67 -0
- package/tools/intel-cache.d.ts +1 -1
- package/tools/intel-cache.js +20 -5
- package/tools/list-strategies.d.ts +11 -1
- package/tools/list-strategies.js +17 -0
- package/tools/modify-stop.d.ts +4 -0
- package/tools/modify-stop.js +63 -24
- package/tools/modify-target.d.ts +4 -0
- package/tools/modify-target.js +62 -23
- package/tools/scan-pairs.js +19 -8
- package/tools/toggle-strategy.js +7 -0
- package/types.d.ts +5 -0
- package/venues/hyperliquid/hl-balance.d.ts +116 -0
- package/venues/hyperliquid/hl-balance.js +145 -0
- package/venues/hyperliquid/hl-brackets.d.ts +102 -0
- package/venues/hyperliquid/hl-brackets.js +172 -0
- package/venues/hyperliquid/hl-cloid.d.ts +22 -0
- package/venues/hyperliquid/hl-cloid.js +82 -0
- package/venues/hyperliquid/hl-info-cache.d.ts +46 -0
- package/venues/hyperliquid/hl-info-cache.js +125 -0
- package/venues/hyperliquid/hl-live-adapter.d.ts +88 -0
- package/venues/hyperliquid/hl-live-adapter.js +353 -0
- package/venues/hyperliquid/hl-precision.d.ts +61 -0
- package/venues/hyperliquid/hl-precision.js +176 -0
- package/venues/hyperliquid/hl-private.d.ts +88 -0
- package/venues/hyperliquid/hl-private.js +357 -0
- package/venues/hyperliquid/hl-public.d.ts +31 -4
- package/venues/hyperliquid/hl-public.js +155 -11
- package/venues/hyperliquid/hl-rate-gate.d.ts +57 -0
- package/venues/hyperliquid/hl-rate-gate.js +220 -0
- package/venues/hyperliquid/hl-user-stream.d.ts +90 -0
- package/venues/hyperliquid/hl-user-stream.js +220 -0
- package/venues/registry.d.ts +23 -9
- package/venues/registry.js +12 -13
- package/venues/symbols.d.ts +43 -0
- package/venues/symbols.js +107 -0
- package/wave9/live-account-capture.d.ts +67 -0
- package/wave9/live-account-capture.js +435 -0
- package/wave9/live-autonomous-protection.d.ts +39 -0
- package/wave9/live-autonomous-protection.js +112 -0
- package/wave9/live-durable-reconciliation-scheduler.d.ts +33 -0
- package/wave9/live-durable-reconciliation-scheduler.js +115 -0
- package/wave9/live-execution-ledger.d.ts +107 -0
- package/wave9/live-execution-ledger.js +498 -0
- package/wave9/live-position-confirmation.d.ts +18 -0
- package/wave9/live-position-confirmation.js +111 -0
- package/wave9/live-residual-protection.d.ts +18 -0
- package/wave9/live-residual-protection.js +250 -0
- package/wave9/live-startup-reconciliation.d.ts +38 -0
- package/wave9/live-startup-reconciliation.js +454 -0
- package/wave9/live-symbol-ownership.d.ts +20 -0
- package/wave9/live-symbol-ownership.js +132 -0
- package/wave9/paper-admission-guard.d.ts +199 -0
- package/wave9/paper-admission-guard.js +650 -0
- package/wave9/usdm-evidence-provider.d.ts +42 -0
- package/wave9/usdm-evidence-provider.js +133 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// Hyperliquid user data stream (plan §5.4/§5.9) — the HL analog of
|
|
2
|
+
// `live/user-data-stream.ts`.
|
|
3
|
+
//
|
|
4
|
+
// ★ THE MEASURED FACT THAT SHAPES THIS FILE (T-5, testnet 2026-07-12):
|
|
5
|
+
// **HL's WS replays NOTHING on reconnect.** We killed the socket, placed an
|
|
6
|
+
// order inside the gap, re-subscribed — and received ZERO events for it, while
|
|
7
|
+
// REST showed it plainly. There is no `isSnapshot` backfill for the gap.
|
|
8
|
+
//
|
|
9
|
+
// ⇒ WS is a FAST PATH, never the ledger. Every reconnect MUST be followed by a
|
|
10
|
+
// REST truth-check (positions + open orders + fills since the disconnect), and
|
|
11
|
+
// the caller is REQUIRED to wire `onResyncNeeded`. This class refuses to
|
|
12
|
+
// pretend otherwise: it tracks `lastEventAt`/`disconnectedAt` and hands them
|
|
13
|
+
// to the resync callback so the gap window is explicit.
|
|
14
|
+
//
|
|
15
|
+
// Other HL-specific rules honoured here:
|
|
16
|
+
// - No listenKey. Auth is by SUBSCRIBING with the MASTER address (agent wallets
|
|
17
|
+
// sign; queries always name the master).
|
|
18
|
+
// - Server closes an idle socket after 60s of silence ⇒ app-level ping every 30s
|
|
19
|
+
// (same cadence the intel HL client uses).
|
|
20
|
+
// - Subscriptions are NOT auto-restored by the server: they must be re-sent on
|
|
21
|
+
// every (re)connect.
|
|
22
|
+
// - `orderUpdates` carries trigger orders natively (statuses incl. `triggered`,
|
|
23
|
+
// `siblingFilledCanceled`, `liquidatedCanceled`, `delistedCanceled`,
|
|
24
|
+
// `marginCanceled`) — simpler than Binance, where brackets needed a separate
|
|
25
|
+
// ALGO_UPDATE channel.
|
|
26
|
+
import WebSocket from 'ws';
|
|
27
|
+
import { logger } from '../../logger.js';
|
|
28
|
+
const TAG = 'hl-user-stream';
|
|
29
|
+
const PING_INTERVAL_MS = 30_000; // server cuts idle sockets at 60s
|
|
30
|
+
const STALE_AFTER_MS = 90_000; // no frame at all for 90s ⇒ terminate + reconnect
|
|
31
|
+
const RECONNECT_BASE_MS = 1_000;
|
|
32
|
+
const RECONNECT_MAX_MS = 60_000;
|
|
33
|
+
export class HyperliquidUserStream {
|
|
34
|
+
opts;
|
|
35
|
+
ws = null;
|
|
36
|
+
pingTimer = null;
|
|
37
|
+
staleTimer = null;
|
|
38
|
+
reconnectTimer = null;
|
|
39
|
+
reconnectAttempts = 0;
|
|
40
|
+
stopped = false;
|
|
41
|
+
lastFrameAt = 0;
|
|
42
|
+
/** Last event we actually processed — the low-water mark for the REST gap-fill. */
|
|
43
|
+
lastEventAt = 0;
|
|
44
|
+
disconnectedAt = 0;
|
|
45
|
+
constructor(opts) {
|
|
46
|
+
this.opts = opts;
|
|
47
|
+
}
|
|
48
|
+
get isConnected() {
|
|
49
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
50
|
+
}
|
|
51
|
+
/** Epoch-ms of the last processed event (0 = none yet). */
|
|
52
|
+
get lastEventTimestamp() {
|
|
53
|
+
return this.lastEventAt;
|
|
54
|
+
}
|
|
55
|
+
start() {
|
|
56
|
+
this.stopped = false;
|
|
57
|
+
this.connect();
|
|
58
|
+
}
|
|
59
|
+
stop() {
|
|
60
|
+
this.stopped = true;
|
|
61
|
+
this.clearTimers();
|
|
62
|
+
if (this.reconnectTimer)
|
|
63
|
+
clearTimeout(this.reconnectTimer);
|
|
64
|
+
this.reconnectTimer = null;
|
|
65
|
+
try {
|
|
66
|
+
this.ws?.close();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
/* best-effort */
|
|
70
|
+
}
|
|
71
|
+
this.ws = null;
|
|
72
|
+
}
|
|
73
|
+
url() {
|
|
74
|
+
return this.opts.testnet
|
|
75
|
+
? 'wss://api.hyperliquid-testnet.xyz/ws'
|
|
76
|
+
: 'wss://api.hyperliquid.xyz/ws';
|
|
77
|
+
}
|
|
78
|
+
connect() {
|
|
79
|
+
if (this.stopped)
|
|
80
|
+
return;
|
|
81
|
+
const url = this.url();
|
|
82
|
+
logger.info(TAG, `connecting ${url}`);
|
|
83
|
+
const ws = new WebSocket(url);
|
|
84
|
+
this.ws = ws;
|
|
85
|
+
ws.on('open', () => {
|
|
86
|
+
this.reconnectAttempts = 0;
|
|
87
|
+
this.lastFrameAt = Date.now();
|
|
88
|
+
this.subscribeAll();
|
|
89
|
+
this.armTimers();
|
|
90
|
+
// ★ T-5: the server sends NO backfill for the gap. Tell the caller exactly
|
|
91
|
+
// how blind we were so it can REST-reconcile that window. `lastEventAt || 0`
|
|
92
|
+
// makes the first connect a full-snapshot request too.
|
|
93
|
+
const wasDisconnectedMs = this.disconnectedAt ? Date.now() - this.disconnectedAt : 0;
|
|
94
|
+
const sinceMs = this.lastEventAt || Date.now() - 60_000;
|
|
95
|
+
logger.info(TAG, `connected — REST resync required (blind ${Math.round(wasDisconnectedMs / 1000)}s; ` +
|
|
96
|
+
'HL WS does not replay missed events — verified testnet 2026-07-12)');
|
|
97
|
+
this.opts.callbacks.onResyncNeeded({ sinceMs, wasDisconnectedMs });
|
|
98
|
+
this.disconnectedAt = 0;
|
|
99
|
+
});
|
|
100
|
+
ws.on('message', (raw) => {
|
|
101
|
+
this.lastFrameAt = Date.now();
|
|
102
|
+
let msg;
|
|
103
|
+
try {
|
|
104
|
+
msg = JSON.parse(raw.toString());
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.handle(msg);
|
|
110
|
+
});
|
|
111
|
+
ws.on('close', (code, reason) => {
|
|
112
|
+
logger.warn(TAG, `closed code=${code} reason=${reason?.toString().slice(0, 80)}`);
|
|
113
|
+
this.onDisconnected();
|
|
114
|
+
});
|
|
115
|
+
ws.on('error', (err) => {
|
|
116
|
+
logger.warn(TAG, `socket error: ${err.message}`);
|
|
117
|
+
// 'close' follows — reconnect is handled there.
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
subscribeAll() {
|
|
121
|
+
const user = this.opts.walletAddress;
|
|
122
|
+
// Subscriptions are NOT restored by the server after a reconnect — always re-send.
|
|
123
|
+
const subs = [
|
|
124
|
+
{ type: 'orderUpdates', user },
|
|
125
|
+
{ type: 'userFills', user },
|
|
126
|
+
{ type: 'userEvents', user },
|
|
127
|
+
{ type: 'userFundings', user },
|
|
128
|
+
];
|
|
129
|
+
for (const subscription of subs) {
|
|
130
|
+
this.send({ method: 'subscribe', subscription });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
handle(msg) {
|
|
134
|
+
const { channel, data } = msg;
|
|
135
|
+
if (!channel || channel === 'subscriptionResponse' || channel === 'pong')
|
|
136
|
+
return;
|
|
137
|
+
switch (channel) {
|
|
138
|
+
case 'userFills': {
|
|
139
|
+
const payload = data;
|
|
140
|
+
for (const fill of payload?.fills ?? []) {
|
|
141
|
+
this.lastEventAt = Math.max(this.lastEventAt, fill.time ?? Date.now());
|
|
142
|
+
this.opts.callbacks.onFill(fill);
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case 'orderUpdates': {
|
|
147
|
+
const updates = (Array.isArray(data) ? data : []);
|
|
148
|
+
for (const u of updates) {
|
|
149
|
+
this.lastEventAt = Math.max(this.lastEventAt, u.statusTimestamp ?? Date.now());
|
|
150
|
+
this.opts.callbacks.onOrderUpdate(u);
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case 'userEvents': {
|
|
155
|
+
this.lastEventAt = Date.now();
|
|
156
|
+
this.opts.callbacks.onUserEvent((data ?? {}));
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case 'userFundings': {
|
|
160
|
+
this.lastEventAt = Date.now();
|
|
161
|
+
this.opts.callbacks.onUserEvent({ fundings: data });
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
default:
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
send(payload) {
|
|
169
|
+
if (this.ws?.readyState !== WebSocket.OPEN)
|
|
170
|
+
return;
|
|
171
|
+
try {
|
|
172
|
+
this.ws.send(JSON.stringify(payload));
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
logger.warn(TAG, `send failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
armTimers() {
|
|
179
|
+
this.clearTimers();
|
|
180
|
+
// App-level ping — the server cuts an idle socket at 60s.
|
|
181
|
+
this.pingTimer = setInterval(() => this.send({ method: 'ping' }), PING_INTERVAL_MS);
|
|
182
|
+
// Frame-staleness watchdog — a silent socket is worse than a closed one
|
|
183
|
+
// (we would believe stale positions). Terminate ⇒ reconnect ⇒ REST resync.
|
|
184
|
+
this.staleTimer = setInterval(() => {
|
|
185
|
+
if (!this.lastFrameAt)
|
|
186
|
+
return;
|
|
187
|
+
const silentMs = Date.now() - this.lastFrameAt;
|
|
188
|
+
if (silentMs > STALE_AFTER_MS) {
|
|
189
|
+
logger.warn(TAG, `no frames for ${Math.round(silentMs / 1000)}s — terminating socket`);
|
|
190
|
+
try {
|
|
191
|
+
this.ws?.terminate();
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
/* the close handler drives the reconnect */
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}, 15_000);
|
|
198
|
+
}
|
|
199
|
+
clearTimers() {
|
|
200
|
+
if (this.pingTimer)
|
|
201
|
+
clearInterval(this.pingTimer);
|
|
202
|
+
if (this.staleTimer)
|
|
203
|
+
clearInterval(this.staleTimer);
|
|
204
|
+
this.pingTimer = null;
|
|
205
|
+
this.staleTimer = null;
|
|
206
|
+
}
|
|
207
|
+
onDisconnected() {
|
|
208
|
+
this.clearTimers();
|
|
209
|
+
this.ws = null;
|
|
210
|
+
if (this.stopped)
|
|
211
|
+
return;
|
|
212
|
+
if (!this.disconnectedAt)
|
|
213
|
+
this.disconnectedAt = Date.now();
|
|
214
|
+
const delay = Math.min(RECONNECT_BASE_MS * 2 ** this.reconnectAttempts, RECONNECT_MAX_MS);
|
|
215
|
+
const jittered = delay * (0.8 + Math.random() * 0.4);
|
|
216
|
+
this.reconnectAttempts += 1;
|
|
217
|
+
logger.info(TAG, `reconnecting in ${Math.round(jittered / 1000)}s (attempt ${this.reconnectAttempts})`);
|
|
218
|
+
this.reconnectTimer = setTimeout(() => this.connect(), jittered);
|
|
219
|
+
}
|
|
220
|
+
}
|
package/venues/registry.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { fillExchangeId, parseVenue, type VenueId } from '@reefclaw/shared';
|
|
2
1
|
import { LiveAdapter } from '../live/live-adapter.js';
|
|
2
|
+
import { HyperliquidLiveAdapter, type HlLiveAdapterOptions } from './hyperliquid/hl-live-adapter.js';
|
|
3
|
+
import { fillExchangeId, parseVenue, type VenueId } from './symbols.js';
|
|
3
4
|
export type { VenueId };
|
|
4
5
|
export { fillExchangeId, parseVenue };
|
|
5
6
|
/** Venues this build can construct a LIVE adapter for. PAPER mode is
|
|
@@ -13,12 +14,25 @@ export declare function isLiveVenueSupported(venue: VenueId): boolean;
|
|
|
13
14
|
* USDC venue reads as $0 equity and every order fails "Insufficient USDC". */
|
|
14
15
|
export declare function venueQuoteCurrency(venue: VenueId): 'USDT' | 'USDC';
|
|
15
16
|
type BinanceLiveAdapterArgs = ConstructorParameters<typeof LiveAdapter>;
|
|
16
|
-
/**
|
|
17
|
+
/** Every live adapter this build can construct. Both are EventEmitters exposing
|
|
18
|
+
* the same lifecycle events (`drift_detected`) and both implement
|
|
19
|
+
* IExchangeAdapter — the boot path treats them identically. */
|
|
20
|
+
export type AnyLiveAdapter = LiveAdapter | HyperliquidLiveAdapter;
|
|
21
|
+
/** Venue-typed construction spec. A discriminated union so the compiler — not a
|
|
22
|
+
* runtime branch in the boot path — guarantees each venue gets ITS OWN args
|
|
23
|
+
* (Binance takes API key/secret; Hyperliquid takes a master address + agent key,
|
|
24
|
+
* a completely different credential shape). */
|
|
25
|
+
export type LiveAdapterSpec = {
|
|
26
|
+
venue: 'binance';
|
|
27
|
+
args: BinanceLiveAdapterArgs;
|
|
28
|
+
} | {
|
|
29
|
+
venue: 'hyperliquid';
|
|
30
|
+
args: HlLiveAdapterOptions;
|
|
31
|
+
};
|
|
32
|
+
/** Construct the live adapter for a venue — the ONLY place either class is newed.
|
|
17
33
|
*
|
|
18
|
-
* Binance: a pure pass-through to `new LiveAdapter(...)` — byte-identical to
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
* not a user state. */
|
|
24
|
-
export declare function createLiveAdapter(venue: VenueId, ...args: BinanceLiveAdapterArgs): LiveAdapter;
|
|
34
|
+
* Binance: a pure pass-through to `new LiveAdapter(...)` — byte-identical to the
|
|
35
|
+
* inline construction this factory replaced (the args tuple is derived from the
|
|
36
|
+
* constructor so the two can never drift).
|
|
37
|
+
* Hyperliquid: `new HyperliquidLiveAdapter(...)` (Phase 3). */
|
|
38
|
+
export declare function createLiveAdapter(spec: LiveAdapterSpec): AnyLiveAdapter;
|
package/venues/registry.js
CHANGED
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
// rule: it holds keys + is part of the safety floor) — it is read from
|
|
13
13
|
// ~/.reefclaw/plugin-config.json `exchange.venue` and MUST never be settable
|
|
14
14
|
// from the central config channel.
|
|
15
|
-
import { fillExchangeId, parseVenue } from '@reefclaw/shared';
|
|
16
15
|
import { LiveAdapter } from '../live/live-adapter.js';
|
|
16
|
+
import { HyperliquidLiveAdapter } from './hyperliquid/hl-live-adapter.js';
|
|
17
|
+
import { fillExchangeId, parseVenue } from './symbols.js';
|
|
17
18
|
export { fillExchangeId, parseVenue };
|
|
18
19
|
/** Venues this build can construct a LIVE adapter for. PAPER mode is
|
|
19
20
|
* venue-flavored only by its market-data source (PaperMarketFeed / chart) and
|
|
20
21
|
* is not gated here. */
|
|
21
|
-
export const SUPPORTED_LIVE_VENUES = new Set(['binance']);
|
|
22
|
+
export const SUPPORTED_LIVE_VENUES = new Set(['binance', 'hyperliquid']);
|
|
22
23
|
export function isLiveVenueSupported(venue) {
|
|
23
24
|
return SUPPORTED_LIVE_VENUES.has(venue);
|
|
24
25
|
}
|
|
@@ -29,19 +30,17 @@ export function isLiveVenueSupported(venue) {
|
|
|
29
30
|
export function venueQuoteCurrency(venue) {
|
|
30
31
|
return venue === 'hyperliquid' ? 'USDC' : 'USDT';
|
|
31
32
|
}
|
|
32
|
-
/** Construct the live adapter for a venue.
|
|
33
|
+
/** Construct the live adapter for a venue — the ONLY place either class is newed.
|
|
33
34
|
*
|
|
34
|
-
* Binance: a pure pass-through to `new LiveAdapter(...)` — byte-identical to
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
export function createLiveAdapter(venue, ...args) {
|
|
41
|
-
switch (venue) {
|
|
35
|
+
* Binance: a pure pass-through to `new LiveAdapter(...)` — byte-identical to the
|
|
36
|
+
* inline construction this factory replaced (the args tuple is derived from the
|
|
37
|
+
* constructor so the two can never drift).
|
|
38
|
+
* Hyperliquid: `new HyperliquidLiveAdapter(...)` (Phase 3). */
|
|
39
|
+
export function createLiveAdapter(spec) {
|
|
40
|
+
switch (spec.venue) {
|
|
42
41
|
case 'binance':
|
|
43
|
-
return new LiveAdapter(...args);
|
|
42
|
+
return new LiveAdapter(...spec.args);
|
|
44
43
|
case 'hyperliquid':
|
|
45
|
-
|
|
44
|
+
return new HyperliquidLiveAdapter(spec.args);
|
|
46
45
|
}
|
|
47
46
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type VenueId = 'binance' | 'hyperliquid';
|
|
2
|
+
export declare const VENUE_IDS: readonly VenueId[];
|
|
3
|
+
export declare function isVenueId(value: unknown): value is VenueId;
|
|
4
|
+
/** Parse a raw config value into a venue.
|
|
5
|
+
* Absent/empty → 'binance' (every pre-venue config file keeps today's
|
|
6
|
+
* behavior byte-identically). An unrecognized string is surfaced to the
|
|
7
|
+
* caller instead of being silently coerced — mis-spelling a venue must be
|
|
8
|
+
* loud, never quietly become "trade on Binance". */
|
|
9
|
+
export declare function parseVenue(raw: unknown): {
|
|
10
|
+
venue: VenueId;
|
|
11
|
+
unrecognized?: string;
|
|
12
|
+
};
|
|
13
|
+
/** `FillEvent.exchange` / webapp `trades.exchange` value — half of the
|
|
14
|
+
* audit-trail idempotency key `(exchange, exchange_trade_id)` (migration
|
|
15
|
+
* 0042). 'binance_futures' is the historical literal on every existing row;
|
|
16
|
+
* NEVER change these strings once a venue has written rows. */
|
|
17
|
+
export declare const FILL_EXCHANGE_ID: Record<VenueId, string>;
|
|
18
|
+
export declare function fillExchangeId(venue: VenueId): string;
|
|
19
|
+
/** Quote/settle asset of the venue's linear perps. */
|
|
20
|
+
export declare const VENUE_QUOTE_ASSET: Record<VenueId, string>;
|
|
21
|
+
/** Prefix that namespaces Hyperliquid rows inside the intel symbol column. */
|
|
22
|
+
export declare const HL_INTEL_PREFIX = "HL_";
|
|
23
|
+
/** Canonical symbol → the venue's CCXT unified symbol.
|
|
24
|
+
* 'BTC/USDT' (binance) → 'BTC/USDT:USDT'; 'BTC/USDC' (hyperliquid) →
|
|
25
|
+
* 'BTC/USDC:USDC'. Accepts an already-suffixed input (idempotent). */
|
|
26
|
+
export declare function toCcxtSymbol(venue: VenueId, canonical: string): string;
|
|
27
|
+
/** Canonical symbol → intel DB symbol.
|
|
28
|
+
* binance: 'BTC/USDT' → 'BTCUSDT' (the historical concatenated form intel has
|
|
29
|
+
* always stored); hyperliquid: 'BTC/USDC' → 'HL_BTC' (namespaced coin, case
|
|
30
|
+
* preserved). */
|
|
31
|
+
export declare function toIntelSymbol(venue: VenueId, canonical: string): string;
|
|
32
|
+
/** Inverse of toIntelSymbol. 'HL_'-prefixed → hyperliquid; everything else is
|
|
33
|
+
* the historical Binance namespace. Binance symbols that are not
|
|
34
|
+
* USDT-concatenated (none exist in INTEL_SYMBOLS today) come back verbatim as
|
|
35
|
+
* canonical — honest passthrough, not a guess. */
|
|
36
|
+
export declare function fromIntelSymbol(intelSymbol: string): {
|
|
37
|
+
venue: VenueId;
|
|
38
|
+
canonical: string;
|
|
39
|
+
};
|
|
40
|
+
/** The Hyperliquid venue-native coin name for a canonical symbol
|
|
41
|
+
* ('BTC/USDC' → 'BTC'). Orders address assets by integer index resolved from
|
|
42
|
+
* `meta.universe` at runtime — the coin name is the stable half. */
|
|
43
|
+
export declare function toHyperliquidCoin(canonical: string): string;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// ⚠️ GENERATED FILE — DO NOT EDIT.
|
|
2
|
+
// Canonical source of truth: shared/src/venues/symbols.ts
|
|
3
|
+
// Regenerate: node scripts/sync-shared-code.mjs (enforced by shared-code-sync.test.ts)
|
|
4
|
+
//
|
|
5
|
+
// This copy exists because this package builds with tsc and deploys as a
|
|
6
|
+
// self-contained tree that strips workspace deps, so it cannot import
|
|
7
|
+
// @reefclaw/shared runtime code across the deploy boundary.
|
|
8
|
+
export const VENUE_IDS = ['binance', 'hyperliquid'];
|
|
9
|
+
export function isVenueId(value) {
|
|
10
|
+
return value === 'binance' || value === 'hyperliquid';
|
|
11
|
+
}
|
|
12
|
+
/** Parse a raw config value into a venue.
|
|
13
|
+
* Absent/empty → 'binance' (every pre-venue config file keeps today's
|
|
14
|
+
* behavior byte-identically). An unrecognized string is surfaced to the
|
|
15
|
+
* caller instead of being silently coerced — mis-spelling a venue must be
|
|
16
|
+
* loud, never quietly become "trade on Binance". */
|
|
17
|
+
export function parseVenue(raw) {
|
|
18
|
+
if (raw === undefined || raw === null || raw === '')
|
|
19
|
+
return { venue: 'binance' };
|
|
20
|
+
if (isVenueId(raw))
|
|
21
|
+
return { venue: raw };
|
|
22
|
+
return { venue: 'binance', unrecognized: String(raw) };
|
|
23
|
+
}
|
|
24
|
+
/** `FillEvent.exchange` / webapp `trades.exchange` value — half of the
|
|
25
|
+
* audit-trail idempotency key `(exchange, exchange_trade_id)` (migration
|
|
26
|
+
* 0042). 'binance_futures' is the historical literal on every existing row;
|
|
27
|
+
* NEVER change these strings once a venue has written rows. */
|
|
28
|
+
export const FILL_EXCHANGE_ID = {
|
|
29
|
+
binance: 'binance_futures',
|
|
30
|
+
hyperliquid: 'hyperliquid',
|
|
31
|
+
};
|
|
32
|
+
export function fillExchangeId(venue) {
|
|
33
|
+
return FILL_EXCHANGE_ID[venue];
|
|
34
|
+
}
|
|
35
|
+
/** Quote/settle asset of the venue's linear perps. */
|
|
36
|
+
export const VENUE_QUOTE_ASSET = {
|
|
37
|
+
binance: 'USDT',
|
|
38
|
+
hyperliquid: 'USDC',
|
|
39
|
+
};
|
|
40
|
+
/** Prefix that namespaces Hyperliquid rows inside the intel symbol column. */
|
|
41
|
+
export const HL_INTEL_PREFIX = 'HL_';
|
|
42
|
+
/** Strip a CCXT settle suffix ('BTC/USDT:USDT' → 'BTC/USDT'). Same regex as
|
|
43
|
+
* webapp/src/lib/symbols.ts normalizeSymbol — kept inline so this module has
|
|
44
|
+
* zero imports and survives the sync-copy deploy boundary. */
|
|
45
|
+
function stripSettleSuffix(symbol) {
|
|
46
|
+
return symbol.replace(/:[A-Z]+$/, '');
|
|
47
|
+
}
|
|
48
|
+
function splitCanonical(canonical) {
|
|
49
|
+
const stripped = stripSettleSuffix(canonical.trim());
|
|
50
|
+
const parts = stripped.split('/');
|
|
51
|
+
if (parts.length !== 2 || parts[0].length === 0 || parts[1].length === 0) {
|
|
52
|
+
throw new Error(`Not a canonical BASE/QUOTE symbol: '${canonical}'`);
|
|
53
|
+
}
|
|
54
|
+
return { base: parts[0], quote: parts[1] };
|
|
55
|
+
}
|
|
56
|
+
/** Canonical symbol → the venue's CCXT unified symbol.
|
|
57
|
+
* 'BTC/USDT' (binance) → 'BTC/USDT:USDT'; 'BTC/USDC' (hyperliquid) →
|
|
58
|
+
* 'BTC/USDC:USDC'. Accepts an already-suffixed input (idempotent). */
|
|
59
|
+
export function toCcxtSymbol(venue, canonical) {
|
|
60
|
+
const { base, quote } = splitCanonical(canonical);
|
|
61
|
+
if (quote !== VENUE_QUOTE_ASSET[venue]) {
|
|
62
|
+
throw new Error(`Symbol '${canonical}' is not ${VENUE_QUOTE_ASSET[venue]}-quoted — not a ${venue} linear perp`);
|
|
63
|
+
}
|
|
64
|
+
return `${base}/${quote}:${quote}`;
|
|
65
|
+
}
|
|
66
|
+
/** Canonical symbol → intel DB symbol.
|
|
67
|
+
* binance: 'BTC/USDT' → 'BTCUSDT' (the historical concatenated form intel has
|
|
68
|
+
* always stored); hyperliquid: 'BTC/USDC' → 'HL_BTC' (namespaced coin, case
|
|
69
|
+
* preserved). */
|
|
70
|
+
export function toIntelSymbol(venue, canonical) {
|
|
71
|
+
const { base, quote } = splitCanonical(canonical);
|
|
72
|
+
if (venue === 'hyperliquid') {
|
|
73
|
+
if (quote !== VENUE_QUOTE_ASSET.hyperliquid) {
|
|
74
|
+
throw new Error(`Symbol '${canonical}' is not USDC-quoted — not a Hyperliquid perp`);
|
|
75
|
+
}
|
|
76
|
+
return `${HL_INTEL_PREFIX}${base}`;
|
|
77
|
+
}
|
|
78
|
+
return `${base}${quote}`;
|
|
79
|
+
}
|
|
80
|
+
/** Inverse of toIntelSymbol. 'HL_'-prefixed → hyperliquid; everything else is
|
|
81
|
+
* the historical Binance namespace. Binance symbols that are not
|
|
82
|
+
* USDT-concatenated (none exist in INTEL_SYMBOLS today) come back verbatim as
|
|
83
|
+
* canonical — honest passthrough, not a guess. */
|
|
84
|
+
export function fromIntelSymbol(intelSymbol) {
|
|
85
|
+
if (intelSymbol.startsWith(HL_INTEL_PREFIX)) {
|
|
86
|
+
const coin = intelSymbol.slice(HL_INTEL_PREFIX.length);
|
|
87
|
+
if (coin.length === 0) {
|
|
88
|
+
throw new Error(`Malformed intel symbol '${intelSymbol}' — empty coin after HL_ prefix`);
|
|
89
|
+
}
|
|
90
|
+
return { venue: 'hyperliquid', canonical: `${coin}/${VENUE_QUOTE_ASSET.hyperliquid}` };
|
|
91
|
+
}
|
|
92
|
+
if (intelSymbol.endsWith(VENUE_QUOTE_ASSET.binance) && intelSymbol.length > 4) {
|
|
93
|
+
const base = intelSymbol.slice(0, -VENUE_QUOTE_ASSET.binance.length);
|
|
94
|
+
return { venue: 'binance', canonical: `${base}/${VENUE_QUOTE_ASSET.binance}` };
|
|
95
|
+
}
|
|
96
|
+
return { venue: 'binance', canonical: intelSymbol };
|
|
97
|
+
}
|
|
98
|
+
/** The Hyperliquid venue-native coin name for a canonical symbol
|
|
99
|
+
* ('BTC/USDC' → 'BTC'). Orders address assets by integer index resolved from
|
|
100
|
+
* `meta.universe` at runtime — the coin name is the stable half. */
|
|
101
|
+
export function toHyperliquidCoin(canonical) {
|
|
102
|
+
const { base, quote } = splitCanonical(canonical);
|
|
103
|
+
if (quote !== VENUE_QUOTE_ASSET.hyperliquid) {
|
|
104
|
+
throw new Error(`Symbol '${canonical}' is not USDC-quoted — not a Hyperliquid perp`);
|
|
105
|
+
}
|
|
106
|
+
return base;
|
|
107
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
2
|
+
import { type Wave9ExecutionMode } from './paper-admission-guard.js';
|
|
3
|
+
import { type Wave9LiveExecutionLedger } from './live-execution-ledger.js';
|
|
4
|
+
export interface Wave9NativeStopObservation {
|
|
5
|
+
verified: boolean;
|
|
6
|
+
reason: string;
|
|
7
|
+
bracketId: string | null;
|
|
8
|
+
bracketState: string | null;
|
|
9
|
+
entryClientOrderId: string | null;
|
|
10
|
+
stopClientOrderId: string | null;
|
|
11
|
+
exchangeOrderId: string | null;
|
|
12
|
+
entrySide: 'buy' | 'sell' | null;
|
|
13
|
+
stopSide: 'buy' | 'sell' | null;
|
|
14
|
+
stopPrice: number | null;
|
|
15
|
+
bracketQuantity: number | null;
|
|
16
|
+
closePosition: boolean | null;
|
|
17
|
+
liveness: 'live' | 'terminal' | 'unknown' | null;
|
|
18
|
+
}
|
|
19
|
+
export interface Wave9LiveStopConfirmationInput {
|
|
20
|
+
candidateId: string;
|
|
21
|
+
symbol: string;
|
|
22
|
+
entryClientOrderId: string;
|
|
23
|
+
entrySide: 'buy' | 'sell';
|
|
24
|
+
stopPrice: number;
|
|
25
|
+
filledQuantity: number;
|
|
26
|
+
}
|
|
27
|
+
export interface Wave9LiveObservedPosition {
|
|
28
|
+
symbol: string;
|
|
29
|
+
side: 'long' | 'short';
|
|
30
|
+
quantity: number;
|
|
31
|
+
entryPrice: number;
|
|
32
|
+
markPrice: number;
|
|
33
|
+
stopPrice?: number;
|
|
34
|
+
missionId?: string;
|
|
35
|
+
setupType?: string;
|
|
36
|
+
candidateId?: string;
|
|
37
|
+
strategy?: 'tsmom_28d_long_flat_reversal_1d' | 'tsmom_28d_short_flat_reversal_1d';
|
|
38
|
+
accruedFundingUsd?: number;
|
|
39
|
+
openedAt?: string;
|
|
40
|
+
initialStopPrice?: number;
|
|
41
|
+
nativeStop?: Wave9NativeStopObservation;
|
|
42
|
+
positionFingerprint?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface Wave9LiveAccountCapture {
|
|
45
|
+
fingerprint: string;
|
|
46
|
+
cashEquityUsd: number;
|
|
47
|
+
positions: Wave9LiveObservedPosition[];
|
|
48
|
+
pendingExposureOrderCount: number;
|
|
49
|
+
executedCandidateIds: string[];
|
|
50
|
+
nativeStopProtectionComplete: boolean;
|
|
51
|
+
nativeStopIssues: string[];
|
|
52
|
+
authoritative: true;
|
|
53
|
+
runtimeParity: false;
|
|
54
|
+
provenance: 'live_exchange_structural_snapshot_and_durable_wave9_ledger';
|
|
55
|
+
}
|
|
56
|
+
/** One-shot post-fill confirmation used after the bracket row becomes active. */
|
|
57
|
+
export declare function confirmWave9LiveNativeStop(adapter: IExchangeAdapter, input: Wave9LiveStopConfirmationInput): Promise<boolean>;
|
|
58
|
+
/**
|
|
59
|
+
* Capture exchange-authoritative LIVE geometry without volatile mark prices in
|
|
60
|
+
* the authorization fingerprint. Wallet cash, positions, pending
|
|
61
|
+
* exposure-increasing orders, and durable Wave 9 identities are all bound.
|
|
62
|
+
*/
|
|
63
|
+
export declare function captureWave9LiveAccount(adapter: IExchangeAdapter, ledger: Wave9LiveExecutionLedger, mode: Wave9ExecutionMode): Promise<Wave9LiveAccountCapture>;
|
|
64
|
+
export declare function captureWave9LivePosition(adapter: IExchangeAdapter, ledger: Wave9LiveExecutionLedger, mode: Wave9ExecutionMode, symbol: string): Promise<{
|
|
65
|
+
fingerprint: string;
|
|
66
|
+
position: Wave9LiveObservedPosition;
|
|
67
|
+
}>;
|