@reefclaw/openclaw-plugin 0.1.2 → 0.1.4
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/bridge/bridge.js +46 -3
- package/bridge/utils/skill-version.d.ts +9 -0
- package/bridge/utils/skill-version.js +45 -1
- package/ccxt/binance-ban-gate.js +3 -1
- package/ccxt/binance-public.d.ts +17 -1
- package/ccxt/binance-public.js +34 -1
- package/ccxt/intel-public.d.ts +25 -0
- package/ccxt/intel-public.js +80 -0
- package/ccxt/public-market-data-api.d.ts +12 -0
- package/ccxt/public-market-data-api.js +9 -0
- package/config/plugin-config-io.d.ts +15 -0
- package/index.js +117 -7
- package/ingest/position-auto-capture.d.ts +5 -0
- package/ingest/position-auto-capture.js +8 -2
- package/ingest/position-decisions-client.d.ts +4 -0
- package/ingest/readiness-reporter.d.ts +28 -5
- package/ingest/readiness-reporter.js +40 -19
- package/live/bracket-id.d.ts +9 -0
- package/live/bracket-id.js +18 -0
- package/package.json +2 -2
- package/skills/reefclaw/SKILL.md +6 -0
- package/tools/close-position.d.ts +2 -2
- package/tools/create-order.d.ts +2 -2
- package/tools/fetch-ohlcv.d.ts +2 -2
- package/tools/fetch-ticker.d.ts +2 -2
- package/tools/get-crypto-metrics.d.ts +2 -2
- package/tools/get-market-structure.d.ts +2 -2
- package/tools/get-orderbook.d.ts +2 -2
- package/tools/get-volume-analysis.d.ts +2 -2
- package/tools/helpers.d.ts +5 -4
- package/tools/helpers.js +2 -1
- package/types.d.ts +20 -1
- package/venues/hyperliquid/hl-public.d.ts +52 -0
- package/venues/hyperliquid/hl-public.js +285 -0
- package/venues/registry.d.ts +19 -0
- package/venues/registry.js +40 -0
package/bridge/bridge.js
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'path';
|
|
|
6
6
|
import { homedir } from 'os';
|
|
7
7
|
import { logger } from './logger.js';
|
|
8
8
|
import { Connector } from './connector.js';
|
|
9
|
-
import { readLocalSkillVersion, validateSkillContent, compareSemver } from './utils/skill-version.js';
|
|
9
|
+
import { readLocalSkillVersion, readAgentVisibleSkillVersions, validateSkillContent, compareSemver } from './utils/skill-version.js';
|
|
10
10
|
import { verifySkillSignature, signatureRequired, readLastAppliedSignedAtMs, recordAppliedSignedAt, } from './utils/skill-signing.js';
|
|
11
11
|
import { isTradingMode, validateModeTransition, redactTokens } from '@reefclaw/shared';
|
|
12
12
|
import { OPERATOR_WRITE_METHODS } from './types.js';
|
|
@@ -837,6 +837,37 @@ export class Bridge {
|
|
|
837
837
|
logger.warn(TAG, `Failed to update workspace skill SKILL.md: ${scanErr instanceof Error ? scanErr.message : String(scanErr)}`);
|
|
838
838
|
}
|
|
839
839
|
}
|
|
840
|
+
// 2d. Also write to the plugin-extension bundled skill directory — the
|
|
841
|
+
// chat-channel install layout (`/plugins install clawhub:...`) loads the
|
|
842
|
+
// agent-visible skill from ~/.openclaw/extensions/<plugin>/skills/, and a
|
|
843
|
+
// plugin (re)install resets that copy to the bundled bootstrap while the
|
|
844
|
+
// workspace copy above (our version tracker) survives. Without this
|
|
845
|
+
// write the agent stays on the bootstrap forever while the pull reports
|
|
846
|
+
// "already up to date" (observed live 2026-07-11).
|
|
847
|
+
const extensionsDir = join(homedir(), '.openclaw', 'extensions');
|
|
848
|
+
if (existsSync(extensionsDir)) {
|
|
849
|
+
try {
|
|
850
|
+
for (const ext of readdirSync(extensionsDir, { withFileTypes: true })) {
|
|
851
|
+
if (!ext.isDirectory() || !ext.name.includes('reefclaw'))
|
|
852
|
+
continue;
|
|
853
|
+
const extSkillsDir = join(extensionsDir, ext.name, 'skills');
|
|
854
|
+
if (!existsSync(extSkillsDir))
|
|
855
|
+
continue;
|
|
856
|
+
for (const skillEntry of readdirSync(extSkillsDir, { withFileTypes: true })) {
|
|
857
|
+
if (!skillEntry.isDirectory() || !skillEntry.name.includes('reefclaw'))
|
|
858
|
+
continue;
|
|
859
|
+
const extSkillMd = join(extSkillsDir, skillEntry.name, 'SKILL.md');
|
|
860
|
+
if (existsSync(extSkillMd)) {
|
|
861
|
+
writeFileSync(extSkillMd, content, 'utf-8');
|
|
862
|
+
logger.info(TAG, `Updated extension skill SKILL.md: ${extSkillMd}`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
catch (scanErr) {
|
|
868
|
+
logger.warn(TAG, `Failed to update extension skill SKILL.md: ${scanErr instanceof Error ? scanErr.message : String(scanErr)}`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
840
871
|
// 3. Invalidate skillsSnapshot cache WITHOUT wiping sessions.json
|
|
841
872
|
// Previously we wrote '{}' to sessions.json which destroyed OpenClaw chat history.
|
|
842
873
|
// Now we surgically remove only the skillsSnapshot key from each session entry,
|
|
@@ -979,12 +1010,24 @@ export class Bridge {
|
|
|
979
1010
|
return;
|
|
980
1011
|
}
|
|
981
1012
|
this.currentSkillVersion = readLocalSkillVersion();
|
|
982
|
-
|
|
1013
|
+
// The workspace tracker alone is NOT proof the agent has the full skill:
|
|
1014
|
+
// a plugin (re)install resets the agent-visible extension copy to the
|
|
1015
|
+
// bundled bootstrap while the workspace copy survives (observed live
|
|
1016
|
+
// 2026-07-11 — agent stranded on 0.0.5 with the tracker at 2.20.4). If
|
|
1017
|
+
// ANY agent-visible copy is behind the webapp version, re-apply anyway;
|
|
1018
|
+
// applySkillUpdate is idempotent across all copies.
|
|
1019
|
+
const staleAgentCopies = readAgentVisibleSkillVersions().filter((v) => compareSemver(body.version, v) > 0);
|
|
1020
|
+
if (compareSemver(body.version, this.currentSkillVersion) <= 0 && staleAgentCopies.length === 0) {
|
|
983
1021
|
logger.info(TAG, `SKILL.md webapp pull: local v${this.currentSkillVersion ?? 'unknown'} already >= webapp v${body.version} — nothing to do`);
|
|
984
1022
|
this.skillPullInFlight = false;
|
|
985
1023
|
return;
|
|
986
1024
|
}
|
|
987
|
-
|
|
1025
|
+
if (staleAgentCopies.length > 0 && compareSemver(body.version, this.currentSkillVersion) <= 0) {
|
|
1026
|
+
logger.info(TAG, `SKILL.md webapp pull: agent-visible skill copy at v${staleAgentCopies[0]} behind webapp v${body.version} (plugin reinstall reset it) — re-applying`);
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
logger.info(TAG, `SKILL.md webapp pull: local v${this.currentSkillVersion ?? 'unknown'} behind webapp v${body.version} — applying`);
|
|
1030
|
+
}
|
|
988
1031
|
const result = await this.applySkillUpdate({ version: body.version, content: body.content });
|
|
989
1032
|
this.emit('agent_state', 'skill_update_applied', {
|
|
990
1033
|
event: 'skill_update_applied',
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
export declare function parseSkillVersion(content: string): string | null;
|
|
3
3
|
/** Read current SKILL.md version from disk */
|
|
4
4
|
export declare function readLocalSkillVersion(): string | null;
|
|
5
|
+
/** Versions of the AGENT-VISIBLE reefclaw SKILL.md copies under the
|
|
6
|
+
* plugin-extension layout (`~/.openclaw/extensions/<plugin>/skills/<skill>/`)
|
|
7
|
+
* — what OpenClaw's skill loader actually reads on chat-channel installs.
|
|
8
|
+
* A plugin (re)install resets these to the bundled bootstrap while the
|
|
9
|
+
* workspace copy (the version tracker above) survives, so the tracker alone
|
|
10
|
+
* cannot be trusted for "already up to date" (observed live 2026-07-11: agent
|
|
11
|
+
* stranded on bootstrap 0.0.5 while the tracker read 2.20.4). Fail-soft:
|
|
12
|
+
* unreadable dirs/files are skipped; no extensions dir → empty list. */
|
|
13
|
+
export declare function readAgentVisibleSkillVersions(): string[];
|
|
5
14
|
/**
|
|
6
15
|
* Compare two semver strings.
|
|
7
16
|
* Returns 1 if a > b, -1 if a < b, 0 if equal.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SKILL.md version parsing utility for OTA updates
|
|
2
|
-
import { readFileSync } from 'fs';
|
|
2
|
+
import { readFileSync, readdirSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
const SKILL_MD_PATH = join(homedir(), '.openclaw', 'workspace', 'SKILL.md');
|
|
@@ -18,6 +18,50 @@ export function readLocalSkillVersion() {
|
|
|
18
18
|
return null;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
/** Versions of the AGENT-VISIBLE reefclaw SKILL.md copies under the
|
|
22
|
+
* plugin-extension layout (`~/.openclaw/extensions/<plugin>/skills/<skill>/`)
|
|
23
|
+
* — what OpenClaw's skill loader actually reads on chat-channel installs.
|
|
24
|
+
* A plugin (re)install resets these to the bundled bootstrap while the
|
|
25
|
+
* workspace copy (the version tracker above) survives, so the tracker alone
|
|
26
|
+
* cannot be trusted for "already up to date" (observed live 2026-07-11: agent
|
|
27
|
+
* stranded on bootstrap 0.0.5 while the tracker read 2.20.4). Fail-soft:
|
|
28
|
+
* unreadable dirs/files are skipped; no extensions dir → empty list. */
|
|
29
|
+
export function readAgentVisibleSkillVersions() {
|
|
30
|
+
const versions = [];
|
|
31
|
+
const extensionsDir = join(homedir(), '.openclaw', 'extensions');
|
|
32
|
+
let extEntries;
|
|
33
|
+
try {
|
|
34
|
+
extEntries = readdirSync(extensionsDir, { withFileTypes: true });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return versions;
|
|
38
|
+
}
|
|
39
|
+
for (const ext of extEntries) {
|
|
40
|
+
if (!ext.isDirectory() || !ext.name.includes('reefclaw'))
|
|
41
|
+
continue;
|
|
42
|
+
const skillsDir = join(extensionsDir, ext.name, 'skills');
|
|
43
|
+
let skillEntries;
|
|
44
|
+
try {
|
|
45
|
+
skillEntries = readdirSync(skillsDir, { withFileTypes: true });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
for (const s of skillEntries) {
|
|
51
|
+
if (!s.isDirectory() || !s.name.includes('reefclaw'))
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
const v = parseSkillVersion(readFileSync(join(skillsDir, s.name, 'SKILL.md'), 'utf-8'));
|
|
55
|
+
if (v)
|
|
56
|
+
versions.push(v);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// unreadable/absent copy — nothing to report for this entry
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return versions;
|
|
64
|
+
}
|
|
21
65
|
// 120KB — the full SKILL.md crossed 100KB at v2.20.0 (103.7KB), which silently
|
|
22
66
|
// broke every OTA apply against the old 100KB cap. Keep comfortably under the
|
|
23
67
|
// 128KiB Cloudflare Durable-Object per-value hard limit the relay stores into.
|
package/ccxt/binance-ban-gate.js
CHANGED
|
@@ -128,6 +128,8 @@ const CONTEXT_WEIGHT = {
|
|
|
128
128
|
fetchOHLCV: 2,
|
|
129
129
|
fetchFundingRate: 1,
|
|
130
130
|
fetchOpenInterest: 1,
|
|
131
|
+
// Readiness gate's host→Binance reachability probe (fapiPublicGetTime, wt 1).
|
|
132
|
+
reachabilityProbe: 1,
|
|
131
133
|
// ExchangeInfoCache boot load — ccxt loadMarkets() → GET /fapi/v1/exchangeInfo,
|
|
132
134
|
// IP weight 1 (doc-verified developers.binance.com 2026-07-10). Was the
|
|
133
135
|
// known "ungated_ip" ccxt-internal call named in the window-summary comment.
|
|
@@ -200,7 +202,7 @@ const NEVER_PACE = new Set([
|
|
|
200
202
|
const SHED_FIRST = new Set([
|
|
201
203
|
'fetchTodayIncomeBreakdown', 'fetchTransfers', 'fetchRecentTradedSymbols',
|
|
202
204
|
'fetchTicker', 'fetchTickerRaw', 'fetchOHLCV', 'fetchOrderBook',
|
|
203
|
-
'fetchFundingRate', 'fetchOpenInterest',
|
|
205
|
+
'fetchFundingRate', 'fetchOpenInterest', 'reachabilityProbe',
|
|
204
206
|
]);
|
|
205
207
|
let weightWindowStart = 0;
|
|
206
208
|
let weightUsed = 0;
|
package/ccxt/binance-public.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CcxtTicker, CcxtOHLCV } from '../types.js';
|
|
2
2
|
import type { OrderBookDepth } from '../simulator/types.js';
|
|
3
|
-
|
|
3
|
+
import type { PublicMarketDataApi } from './public-market-data-api.js';
|
|
4
|
+
export declare class BinancePublicApi implements PublicMarketDataApi {
|
|
4
5
|
private exchange;
|
|
5
6
|
constructor();
|
|
6
7
|
/** Fetch raw CCXT ticker (includes funding rate, OI, info). Returns null on error. */
|
|
@@ -15,4 +16,19 @@ export declare class BinancePublicApi {
|
|
|
15
16
|
fetchOrderBook(symbol: string, limit?: number): Promise<OrderBookDepth | null>;
|
|
16
17
|
/** Fetch OHLCV candles. Returns null on error. */
|
|
17
18
|
fetchOHLCV(symbol: string, timeframe?: string, limit?: number): Promise<CcxtOHLCV[] | null>;
|
|
19
|
+
/** Probe Binance USD-M FUTURES reachability from this host — the readiness
|
|
20
|
+
* gate's core signal. Calls the futures-explicit implicit method so it hits
|
|
21
|
+
* `fapi.binance.com` (a bare `fetchTime()` on this instance resolves to spot
|
|
22
|
+
* `api.binance.com`, since the public client doesn't set defaultType:'future').
|
|
23
|
+
* HTTP 451 = Binance geo-restriction; the ban gate does NOT classify 451, so
|
|
24
|
+
* we inspect the message here. Ban-gate compliant (assertNotBanned/noteSuccess/
|
|
25
|
+
* noteBinanceError). Outcomes:
|
|
26
|
+
* - 'reachable' clean response (driftMs = serverTime − localTime)
|
|
27
|
+
* - 'geo_blocked' HTTP 451 — host is in a restricted region (actionable)
|
|
28
|
+
* - 'unknown' the ban/weight gate paused us — NOT a host problem
|
|
29
|
+
* - 'unreachable' network / DNS / timeout / other error */
|
|
30
|
+
probeReachability(): Promise<{
|
|
31
|
+
outcome: 'reachable' | 'geo_blocked' | 'unreachable' | 'unknown';
|
|
32
|
+
driftMs: number | null;
|
|
33
|
+
}>;
|
|
18
34
|
}
|
package/ccxt/binance-public.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// No API keys required — only uses public market data.
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { logger } from '../logger.js';
|
|
5
|
-
import { assertNotBanned, noteBinanceError, noteSuccess } from './binance-ban-gate.js';
|
|
5
|
+
import { assertNotBanned, noteBinanceError, noteSuccess, BinanceBannedError } from './binance-ban-gate.js';
|
|
6
6
|
const TAG = 'binance-public';
|
|
7
7
|
// Load ccxt via CJS require — OpenClaw's ESM loader gives wrong module shape
|
|
8
8
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -144,4 +144,37 @@ export class BinancePublicApi {
|
|
|
144
144
|
return null;
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
+
/** Probe Binance USD-M FUTURES reachability from this host — the readiness
|
|
148
|
+
* gate's core signal. Calls the futures-explicit implicit method so it hits
|
|
149
|
+
* `fapi.binance.com` (a bare `fetchTime()` on this instance resolves to spot
|
|
150
|
+
* `api.binance.com`, since the public client doesn't set defaultType:'future').
|
|
151
|
+
* HTTP 451 = Binance geo-restriction; the ban gate does NOT classify 451, so
|
|
152
|
+
* we inspect the message here. Ban-gate compliant (assertNotBanned/noteSuccess/
|
|
153
|
+
* noteBinanceError). Outcomes:
|
|
154
|
+
* - 'reachable' clean response (driftMs = serverTime − localTime)
|
|
155
|
+
* - 'geo_blocked' HTTP 451 — host is in a restricted region (actionable)
|
|
156
|
+
* - 'unknown' the ban/weight gate paused us — NOT a host problem
|
|
157
|
+
* - 'unreachable' network / DNS / timeout / other error */
|
|
158
|
+
async probeReachability() {
|
|
159
|
+
try {
|
|
160
|
+
assertNotBanned('reachabilityProbe');
|
|
161
|
+
const r = await this.exchange.fapiPublicGetTime({});
|
|
162
|
+
noteSuccess();
|
|
163
|
+
const serverTime = Number(r?.serverTime);
|
|
164
|
+
const driftMs = Number.isFinite(serverTime) ? serverTime - Date.now() : null;
|
|
165
|
+
return { outcome: 'reachable', driftMs };
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
noteBinanceError(err);
|
|
169
|
+
if (err instanceof BinanceBannedError) {
|
|
170
|
+
// Gate paused us (418/429 backoff or weight pacer) — we didn't actually
|
|
171
|
+
// reach the host, so the result is UNKNOWN, not a reachability failure.
|
|
172
|
+
return { outcome: 'unknown', driftMs: null };
|
|
173
|
+
}
|
|
174
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
175
|
+
const geo = msg.includes('451');
|
|
176
|
+
logger.warn(TAG, `probeReachability failed${geo ? ' (HTTP 451 geo-block)' : ''}: ${msg}`);
|
|
177
|
+
return { outcome: geo ? 'geo_blocked' : 'unreachable', driftMs: null };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
147
180
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type IntelApiDeps } from '../tools/intel-api.js';
|
|
2
|
+
import type { CcxtTicker, CcxtOHLCV } from '../types.js';
|
|
3
|
+
import type { OrderBookDepth } from '../simulator/types.js';
|
|
4
|
+
import type { PublicMarketDataApi } from './public-market-data-api.js';
|
|
5
|
+
type RawCcxt = Record<string, any>;
|
|
6
|
+
export declare class IntelPublicApi implements PublicMarketDataApi {
|
|
7
|
+
private readonly deps;
|
|
8
|
+
constructor(deps: IntelApiDeps);
|
|
9
|
+
/** Latest price for `symbol` as a CcxtTicker (last=bid=ask=intel 1m close;
|
|
10
|
+
* zero modeled spread — the paper fill engine models slippage itself).
|
|
11
|
+
* Returns null on any intel error, matching BinancePublicApi's contract. */
|
|
12
|
+
fetchTicker(symbol: string): Promise<CcxtTicker | null>;
|
|
13
|
+
/** Intel has no raw-ticker/funding/OI HTTP route yet — null (same as a 451). */
|
|
14
|
+
fetchTickerRaw(): Promise<RawCcxt | null>;
|
|
15
|
+
fetchFundingRate(): Promise<RawCcxt | null>;
|
|
16
|
+
fetchOpenInterest(): Promise<RawCcxt | null>;
|
|
17
|
+
/** No intel depth route yet — null. The paper fill engine falls back to
|
|
18
|
+
* random slippage when the book is absent (see helpers.ts fetchOrderBook). */
|
|
19
|
+
fetchOrderBook(): Promise<OrderBookDepth | null>;
|
|
20
|
+
/** No intel candle route yet — null (candle-consuming analysis tools degrade
|
|
21
|
+
* exactly as they do under a 451; the agent's decisioning reads intel
|
|
22
|
+
* signals/scan tools, not raw candles). */
|
|
23
|
+
fetchOHLCV(): Promise<CcxtOHLCV[] | null>;
|
|
24
|
+
}
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Intel-backed public market-data source for PAPER mode on hosts that Binance
|
|
2
|
+
// geo-blocks (HTTP 451). Paper execution is fully simulated; it only needs a
|
|
3
|
+
// price, which the intel service serves from its own (permitted-region) feeds
|
|
4
|
+
// via GET /api/price/:symbol (latest 1m close, ≤~60s stale). See
|
|
5
|
+
// docs/AGENT_READINESS_GATE_PLAN.md sibling + the wisekid 451 findings.
|
|
6
|
+
//
|
|
7
|
+
// Selected only when plugin-config `paperMarketDataSource: 'intel'` AND paper
|
|
8
|
+
// mode (index.ts). Live trading NEVER uses this — live needs the real exchange.
|
|
9
|
+
//
|
|
10
|
+
// Increment 1 serves PRICE only (fetchTicker), which is the trading-critical
|
|
11
|
+
// path: create_order / close_position price + the PaperMarketFeed mark loop.
|
|
12
|
+
// Candles / order book / funding / OI have no intel HTTP route yet, so those
|
|
13
|
+
// return null here — the SAME shape a 451 produces today (no regression), and
|
|
14
|
+
// order-book absence is already handled by the fill engine's random-slippage
|
|
15
|
+
// fallback. Candle/book/OI intel routes are a follow-up.
|
|
16
|
+
import { logger } from '../logger.js';
|
|
17
|
+
import { fetchIntelApi } from '../tools/intel-api.js';
|
|
18
|
+
const TAG = 'intel-public';
|
|
19
|
+
/** CCXT symbol (`BTC/USDT`, `BTC/USDT:USDT`) → intel format (`BTCUSDT`). */
|
|
20
|
+
function toIntelSymbol(symbol) {
|
|
21
|
+
return symbol.replace('/', '').replace(/:.*$/, '');
|
|
22
|
+
}
|
|
23
|
+
export class IntelPublicApi {
|
|
24
|
+
deps;
|
|
25
|
+
constructor(deps) {
|
|
26
|
+
this.deps = deps;
|
|
27
|
+
logger.info(TAG, 'Intel public market-data source active (paper mode, Binance reads routed to intel)');
|
|
28
|
+
}
|
|
29
|
+
/** Latest price for `symbol` as a CcxtTicker (last=bid=ask=intel 1m close;
|
|
30
|
+
* zero modeled spread — the paper fill engine models slippage itself).
|
|
31
|
+
* Returns null on any intel error, matching BinancePublicApi's contract. */
|
|
32
|
+
async fetchTicker(symbol) {
|
|
33
|
+
const res = await fetchIntelApi(`/api/price/${toIntelSymbol(symbol)}`, this.deps);
|
|
34
|
+
if ('error' in res) {
|
|
35
|
+
logger.warn(TAG, `fetchTicker(${symbol}) via intel failed: ${res.error}`);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const body = res;
|
|
39
|
+
const price = Number(body.price);
|
|
40
|
+
if (!Number.isFinite(price) || price <= 0) {
|
|
41
|
+
logger.warn(TAG, `fetchTicker(${symbol}) via intel returned a bad price: ${body.price}`);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const timestamp = Number.isFinite(body.time) ? body.time : Date.now();
|
|
45
|
+
return {
|
|
46
|
+
// Key on the ORIGINAL ccxt symbol so the simulator's per-symbol maps line up.
|
|
47
|
+
symbol,
|
|
48
|
+
last: price,
|
|
49
|
+
bid: price,
|
|
50
|
+
ask: price,
|
|
51
|
+
baseVolume: 0,
|
|
52
|
+
quoteVolume: 0,
|
|
53
|
+
change: 0,
|
|
54
|
+
percentage: 0,
|
|
55
|
+
timestamp,
|
|
56
|
+
datetime: new Date(timestamp).toISOString(),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Intel has no raw-ticker/funding/OI HTTP route yet — null (same as a 451). */
|
|
60
|
+
async fetchTickerRaw() {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
async fetchFundingRate() {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
async fetchOpenInterest() {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/** No intel depth route yet — null. The paper fill engine falls back to
|
|
70
|
+
* random slippage when the book is absent (see helpers.ts fetchOrderBook). */
|
|
71
|
+
async fetchOrderBook() {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
/** No intel candle route yet — null (candle-consuming analysis tools degrade
|
|
75
|
+
* exactly as they do under a 451; the agent's decisioning reads intel
|
|
76
|
+
* signals/scan tools, not raw candles). */
|
|
77
|
+
async fetchOHLCV() {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CcxtTicker, CcxtOHLCV } from '../types.js';
|
|
2
|
+
import type { OrderBookDepth } from '../simulator/types.js';
|
|
3
|
+
type RawCcxt = Record<string, any>;
|
|
4
|
+
export interface PublicMarketDataApi {
|
|
5
|
+
fetchTickerRaw(symbol: string): Promise<RawCcxt | null>;
|
|
6
|
+
fetchTicker(symbol: string): Promise<CcxtTicker | null>;
|
|
7
|
+
fetchFundingRate(symbol: string): Promise<RawCcxt | null>;
|
|
8
|
+
fetchOpenInterest(symbol: string): Promise<RawCcxt | null>;
|
|
9
|
+
fetchOrderBook(symbol: string, limit?: number): Promise<OrderBookDepth | null>;
|
|
10
|
+
fetchOHLCV(symbol: string, timeframe?: string, limit?: number): Promise<CcxtOHLCV[] | null>;
|
|
11
|
+
}
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The public market-data surface paper mode reads (prices, candles, book,
|
|
2
|
+
// funding, OI). Both BinancePublicApi (direct Binance) and IntelPublicApi
|
|
3
|
+
// (via the intel service, for Binance-451-geo-blocked hosts) implement it, so
|
|
4
|
+
// the paper wiring can select a source without any consumer knowing which.
|
|
5
|
+
//
|
|
6
|
+
// Deliberately EXCLUDES probeReachability: that is Binance-specific and only
|
|
7
|
+
// the readiness reporter uses it — it must always probe the REAL Binance host
|
|
8
|
+
// to detect the 451, so it keeps a concrete BinancePublicApi, never this.
|
|
9
|
+
export {};
|
|
@@ -21,8 +21,23 @@ export interface PluginConfigFile {
|
|
|
21
21
|
* prod runs the bridge under systemd and a supervisor there would
|
|
22
22
|
* double-connect the relay room. Kill-switch: RC_CONNECTOR_SUPERVISOR=off. */
|
|
23
23
|
connectorSupervisor?: 'on' | 'off';
|
|
24
|
+
/** Where PAPER mode reads market-data prices from. 'binance' (default) hits
|
|
25
|
+
* Binance public endpoints directly; 'intel' routes them through the intel
|
|
26
|
+
* service (GET /api/price/:symbol) so a host that Binance geo-blocks (HTTP
|
|
27
|
+
* 451) can still paper-trade. PAPER-ONLY + advisory: live trading always
|
|
28
|
+
* uses the real exchange, and readiness still probes real Binance to detect
|
|
29
|
+
* the 451. Kill-switch: RC_PAPER_MARKET_DATA=binance forces the default. */
|
|
30
|
+
paperMarketDataSource?: 'binance' | 'intel';
|
|
24
31
|
apiBaseUrl?: string;
|
|
25
32
|
intelligenceUrl?: string;
|
|
33
|
+
/** Exchange credentials + venue selection. `exchange.venue` picks the
|
|
34
|
+
* trading venue ('binance' default | 'hyperliquid'); it is LOCAL mechanism
|
|
35
|
+
* (never central-pushable) per docs/HYPERLIQUID_INTEGRATION_PLAN.md §5.2.
|
|
36
|
+
* Until Phase 3 ships the hyperliquid adapter, a non-PAPER mode on
|
|
37
|
+
* venue='hyperliquid' falls back to PAPER at boot. NOTE: updatePluginConfig
|
|
38
|
+
* replaces this block wholesale (the documented `exchange: null` clears
|
|
39
|
+
* credentials), so any writer (set_exchange_credentials, SSH edits) must
|
|
40
|
+
* carry `venue` through or it resets to the binance default. */
|
|
26
41
|
exchange?: ExchangeConfig;
|
|
27
42
|
tradingMode?: TradingMode;
|
|
28
43
|
microLive?: {
|
package/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { homedir } from 'node:os';
|
|
|
11
11
|
import { join, dirname } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { BinancePublicApi } from './ccxt/binance-public.js';
|
|
14
|
+
import { IntelPublicApi } from './ccxt/intel-public.js';
|
|
14
15
|
import { BinancePrivateApi } from './ccxt/binance-private.js';
|
|
15
16
|
import { ExchangeSimulator } from './simulator/exchange-simulator.js';
|
|
16
17
|
import { ShadowTracker } from './shadow/shadow-tracker.js';
|
|
@@ -18,7 +19,8 @@ import { StateManager } from './persistence/state-manager.js';
|
|
|
18
19
|
import { logger, setLogLevel, formatError } from './logger.js';
|
|
19
20
|
import { DEFAULT_CONFIG } from './types.js';
|
|
20
21
|
import { PaperAdapter } from './paper-adapter.js';
|
|
21
|
-
import {
|
|
22
|
+
import { createLiveAdapter, fillExchangeId, isLiveVenueSupported, parseVenue, } from './venues/registry.js';
|
|
23
|
+
import { HyperliquidPublicApi } from './venues/hyperliquid/hl-public.js';
|
|
22
24
|
import { loadBracketMode } from './config/brackets-config.js';
|
|
23
25
|
import { loadUserDataStreamMode, loadUserDataStreamTunables, loadUserDataStreamDbWrite, getUserDataStreamIngestBaseUrl, resolveIngestToken, resolveReefclawUserId, } from './config/user-data-stream-config.js';
|
|
24
26
|
import { TradeStoreClient } from './ingest/trade-store-client.js';
|
|
@@ -29,6 +31,7 @@ import { PositionStateStore } from './live/position-state-store.js';
|
|
|
29
31
|
import { PendingEntryStore } from './ingest/pending-entry-metadata.js';
|
|
30
32
|
import { onReconcilerObservedClose, reconcileStateStoreOnStartup } from './ingest/reconciler-cleanup.js';
|
|
31
33
|
import { reconcileDbOpenVsExchange } from './ingest/reconcile-db-vs-exchange.js';
|
|
34
|
+
import { startReadinessReporter } from './ingest/readiness-reporter.js';
|
|
32
35
|
import { IntelMicrostructureAssembler } from './live/microstructure-assembler.js';
|
|
33
36
|
import { recordPositionReviewsTool } from './tools/record-position-reviews.js';
|
|
34
37
|
import { getMyRecentReviewsTool } from './tools/get-my-recent-reviews.js';
|
|
@@ -911,6 +914,14 @@ const paperTradingPlugin = {
|
|
|
911
914
|
let intelligenceUrl = 'https://intel.reefclaw.com';
|
|
912
915
|
let exchangeConfig = null;
|
|
913
916
|
let tradingMode = 'PAPER';
|
|
917
|
+
// Trading venue (multi-venue Phase 0 — docs/HYPERLIQUID_INTEGRATION_PLAN.md).
|
|
918
|
+
// Absent config field = 'binance', so every pre-venue install is untouched.
|
|
919
|
+
let venue = 'binance';
|
|
920
|
+
// exchange.testnet as written on disk — needed independently of
|
|
921
|
+
// exchangeConfig because the hyperliquid venue never builds a Binance
|
|
922
|
+
// credential config (Phase 0 gate) but its keyless public API still needs
|
|
923
|
+
// the mainnet/testnet routing.
|
|
924
|
+
let venueTestnet = false;
|
|
914
925
|
let signalsEvaluator = 'central';
|
|
915
926
|
let signalsSymbols = [];
|
|
916
927
|
try {
|
|
@@ -927,9 +938,18 @@ const paperTradingPlugin = {
|
|
|
927
938
|
if (Array.isArray(rcConfig?.signals?.symbols)) {
|
|
928
939
|
signalsSymbols = rcConfig.signals.symbols.map((s) => String(s).toUpperCase());
|
|
929
940
|
}
|
|
930
|
-
// Read exchange credentials (Phase 9b)
|
|
941
|
+
// Read exchange credentials (Phase 9b) + venue (multi-venue Phase 0)
|
|
931
942
|
const exchange = rcConfig?.exchange;
|
|
932
|
-
|
|
943
|
+
const venueParse = parseVenue(exchange?.venue);
|
|
944
|
+
venue = venueParse.venue;
|
|
945
|
+
venueTestnet = exchange?.testnet === true;
|
|
946
|
+
if (venueParse.unrecognized) {
|
|
947
|
+
logger.warn(TAG, `Unrecognized exchange.venue '${venueParse.unrecognized}' in plugin-config — treating as 'binance'`);
|
|
948
|
+
}
|
|
949
|
+
// apiKey/secret are a BINANCE credential pair; never build a Binance
|
|
950
|
+
// client config for another venue (a hyperliquid block carries
|
|
951
|
+
// walletAddress/agentPrivateKey instead — consumed from Phase 3 on).
|
|
952
|
+
if (venue === 'binance' && exchange?.apiKey && exchange?.secret) {
|
|
933
953
|
exchangeConfig = {
|
|
934
954
|
apiKey: exchange.apiKey,
|
|
935
955
|
secret: exchange.secret,
|
|
@@ -963,6 +983,14 @@ const paperTradingPlugin = {
|
|
|
963
983
|
else {
|
|
964
984
|
logger.warn(TAG, 'No connection token found — get_market_intel will return errors');
|
|
965
985
|
}
|
|
986
|
+
// Venue support gate (multi-venue Phase 0). Checked BEFORE the credentials
|
|
987
|
+
// gate so a hyperliquid config gets the accurate diagnosis ("venue not
|
|
988
|
+
// supported yet"), not a misleading "missing API keys". Fallback-to-PAPER,
|
|
989
|
+
// never a throw — register() must stay non-crashing.
|
|
990
|
+
if (tradingMode !== 'PAPER' && !isLiveVenueSupported(venue)) {
|
|
991
|
+
logger.warn(TAG, `Trading mode ${tradingMode} on venue '${venue}' is not supported in this build — falling back to PAPER (live support arrives in Phase 3 of docs/HYPERLIQUID_INTEGRATION_PLAN.md)`);
|
|
992
|
+
tradingMode = 'PAPER';
|
|
993
|
+
}
|
|
966
994
|
// Validate trading mode vs credentials
|
|
967
995
|
if (tradingMode !== 'PAPER' && !exchangeConfig) {
|
|
968
996
|
logger.warn(TAG, `Trading mode ${tradingMode} requires exchange API keys — falling back to PAPER`);
|
|
@@ -1062,6 +1090,9 @@ const paperTradingPlugin = {
|
|
|
1062
1090
|
// closure — only invoked on a fill, long after `runtime` is built), so it
|
|
1063
1091
|
// follows a live<->paper reconnect.
|
|
1064
1092
|
resolveMode: () => (runtime.adapter.isLive ? 'live' : 'paper'),
|
|
1093
|
+
// Venue tag (positions.exchange, migration 0058) — static per process;
|
|
1094
|
+
// a venue change requires config edit + restart.
|
|
1095
|
+
venue,
|
|
1065
1096
|
};
|
|
1066
1097
|
if (positionDecisionsClient) {
|
|
1067
1098
|
logger.info(TAG, `Journal close-on-reduce-only-fill ${closeOnReduceOnlyFill ? 'ENABLED' : 'disabled'}`);
|
|
@@ -1145,6 +1176,11 @@ const paperTradingPlugin = {
|
|
|
1145
1176
|
tradeIngest = {
|
|
1146
1177
|
client: new TradeStoreClient({ baseUrl: ingestBaseUrl, ingestToken }),
|
|
1147
1178
|
userId: reefclawUserId,
|
|
1179
|
+
// Venue-derived FillEvent.exchange — 'binance_futures' for the
|
|
1180
|
+
// binance venue, i.e. byte-identical to the pre-venue literal
|
|
1181
|
+
// WsIngest defaulted to. Half of the (exchange, exchange_trade_id)
|
|
1182
|
+
// audit-trail idempotency key; never a fresh string literal.
|
|
1183
|
+
exchange: fillExchangeId(venue),
|
|
1148
1184
|
};
|
|
1149
1185
|
logger.info(TAG, `User-data stream dbWrite=on — WS audit-trail ingest wired to ${ingestBaseUrl} (userId=${reefclawUserId.slice(0, 8)}…)`);
|
|
1150
1186
|
}
|
|
@@ -1202,7 +1238,12 @@ const paperTradingPlugin = {
|
|
|
1202
1238
|
logger.warn(TAG, `approval mode=${approvalModeForTool} but WEBAPP_INGEST_TOKEN or REEFCLAW_USER_ID env missing — proposal path disabled`);
|
|
1203
1239
|
approvalModeForTool = 'off';
|
|
1204
1240
|
}
|
|
1205
|
-
|
|
1241
|
+
// Venue-dispatched construction (multi-venue Phase 0). For 'binance'
|
|
1242
|
+
// this is a pure pass-through to `new LiveAdapter(...)` — identical
|
|
1243
|
+
// args, identical behavior; the unsupported-venue arm is unreachable
|
|
1244
|
+
// here because the isLiveVenueSupported() gate above already fell back
|
|
1245
|
+
// to PAPER. Phase 3 adds the hyperliquid adapter inside the factory.
|
|
1246
|
+
const liveAdapter = createLiveAdapter(venue, exchangeConfig, tradingMode, microLiveConfig, bracketMode, userDataStreamMode, userDataStreamTunables, tradeIngest, autoCapture);
|
|
1206
1247
|
adapter = liveAdapter;
|
|
1207
1248
|
// ---- Approval-mode Phase B — start ProposalDecisionListener ----
|
|
1208
1249
|
// Started only when approval.mode='per_trade' AND proposalManagerCtx
|
|
@@ -1319,11 +1360,64 @@ const paperTradingPlugin = {
|
|
|
1319
1360
|
else {
|
|
1320
1361
|
adapter = new PaperAdapter(simulator);
|
|
1321
1362
|
}
|
|
1363
|
+
// PAPER market-data source selection.
|
|
1364
|
+
//
|
|
1365
|
+
// Venue precedence (Hyperliquid Phase 1 — docs/HYPERLIQUID_INTEGRATION_PLAN.md):
|
|
1366
|
+
// the configured VENUE decides which exchange prices paper mode, full stop.
|
|
1367
|
+
// venue='hyperliquid' → every public read (PaperMarketFeed marks, the
|
|
1368
|
+
// simulator's fill prices, fetch_ticker/fetch_ohlcv/get_orderbook tools)
|
|
1369
|
+
// comes from Hyperliquid's keyless /info endpoints; the Binance-only
|
|
1370
|
+
// `paperMarketDataSource:'intel'` escape hatch does not apply (intel has
|
|
1371
|
+
// no HL rows until Phase 2 — silently serving Binance prices for a
|
|
1372
|
+
// Hyperliquid book would be a lie, the exact class the symbol-translation
|
|
1373
|
+
// rule forbids).
|
|
1374
|
+
//
|
|
1375
|
+
// Binance venue (default) is unchanged: a host Binance geo-blocks (HTTP
|
|
1376
|
+
// 451) can route PRICE reads through the intel service by setting
|
|
1377
|
+
// plugin-config `paperMarketDataSource:'intel'` (GET /api/price/:symbol).
|
|
1378
|
+
// LIVE always uses the real exchange, and the readiness reporter probes
|
|
1379
|
+
// the CONFIGURED venue. Kill-switch RC_PAPER_MARKET_DATA=binance forces
|
|
1380
|
+
// the Binance-direct default (binance venue only).
|
|
1381
|
+
//
|
|
1382
|
+
// hlPublicApi is constructed once per process when the venue is
|
|
1383
|
+
// hyperliquid — shared by the paper data path and the readiness probe.
|
|
1384
|
+
const hlPublicApi = venue === 'hyperliquid' ? new HyperliquidPublicApi({ testnet: venueTestnet }) : null;
|
|
1385
|
+
let configuredPaperSource;
|
|
1386
|
+
try {
|
|
1387
|
+
configuredPaperSource = readPluginConfig().paperMarketDataSource;
|
|
1388
|
+
}
|
|
1389
|
+
catch {
|
|
1390
|
+
configuredPaperSource = undefined;
|
|
1391
|
+
}
|
|
1392
|
+
const paperDataSource = process.env.RC_PAPER_MARKET_DATA === 'binance'
|
|
1393
|
+
? 'binance'
|
|
1394
|
+
: configuredPaperSource ?? 'binance';
|
|
1395
|
+
const useIntelPaperData = venue === 'binance' &&
|
|
1396
|
+
!adapter.isLive && paperDataSource === 'intel' && connectionToken.length > 0;
|
|
1397
|
+
const marketDataApi = !adapter.isLive && hlPublicApi
|
|
1398
|
+
? hlPublicApi
|
|
1399
|
+
: useIntelPaperData
|
|
1400
|
+
? new IntelPublicApi({ connectionToken, intelligenceUrl })
|
|
1401
|
+
: binanceApi;
|
|
1402
|
+
if (!adapter.isLive && hlPublicApi) {
|
|
1403
|
+
logger.info(TAG, `PAPER market data sourced from Hyperliquid${venueTestnet ? ' TESTNET' : ''} (venue=hyperliquid) — use USDC pairs (e.g. BTC/USDC)`);
|
|
1404
|
+
if (paperDataSource === 'intel') {
|
|
1405
|
+
logger.warn(TAG, 'paperMarketDataSource=intel is a Binance-venue option — ignored on venue=hyperliquid (intel has no Hyperliquid rows until Phase 2)');
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
else if (useIntelPaperData) {
|
|
1409
|
+
logger.warn(TAG, 'PAPER market data routed through intel (paperMarketDataSource=intel) — prices from GET /api/price; live + readiness still use Binance directly');
|
|
1410
|
+
}
|
|
1411
|
+
else if (venue === 'binance' && !adapter.isLive && paperDataSource === 'intel' && connectionToken.length === 0) {
|
|
1412
|
+
logger.warn(TAG, 'paperMarketDataSource=intel requested but no connection token — falling back to Binance direct (which will 451 on a geo-blocked host)');
|
|
1413
|
+
}
|
|
1322
1414
|
// Build tool dependencies
|
|
1323
1415
|
// `simDeps` for tools that need the simulator directly (ticker, market structure, risk scenario)
|
|
1324
1416
|
// `adapterDeps` for the 7 adapter-based trading tools
|
|
1325
|
-
|
|
1326
|
-
|
|
1417
|
+
// Both carry `marketDataApi` (Binance direct, or intel in paper mode) so
|
|
1418
|
+
// every paper market-data read follows the selected source at once.
|
|
1419
|
+
const simDeps = { binanceApi: marketDataApi, simulator };
|
|
1420
|
+
const adapterDeps = { binanceApi: marketDataApi, adapter };
|
|
1327
1421
|
// ---- Plugin runtime (mutable holder) ----
|
|
1328
1422
|
// Owns the adapter + mode + stop-watcher and exposes `reconnect()` so the
|
|
1329
1423
|
// operator can swap credentials / trading mode from the dashboard without
|
|
@@ -1356,7 +1450,7 @@ const paperTradingPlugin = {
|
|
|
1356
1450
|
// resting limits fill. Paper-only — started here when the boot adapter is
|
|
1357
1451
|
// paper; PluginRuntime.reconnect() stops it on a switch to live and
|
|
1358
1452
|
// restarts it on a switch back to paper. Reuses the ban-gated fetchTicker.
|
|
1359
|
-
const marketFeed = new PaperMarketFeed(
|
|
1453
|
+
const marketFeed = new PaperMarketFeed(marketDataApi, simulator);
|
|
1360
1454
|
if (!adapter.isLive) {
|
|
1361
1455
|
marketFeed.start();
|
|
1362
1456
|
}
|
|
@@ -2020,6 +2114,22 @@ const paperTradingPlugin = {
|
|
|
2020
2114
|
pluginToolNames = toolNames;
|
|
2021
2115
|
pluginInitialised = true;
|
|
2022
2116
|
logger.info(TAG, `Registered ${gatedTools.length} tools (gate mode=${toolGate.getMode()}): ${toolNames.join(', ')}. Plugin v3.8.0 (${runtime.mode} mode)`);
|
|
2117
|
+
// Agent-readiness reporter (docs/AGENT_READINESS_GATE_PLAN.md Phase 1):
|
|
2118
|
+
// probe host→venue reachability (Binance HTTP 451 geo-block / Hyperliquid
|
|
2119
|
+
// /info) + clock drift on the host and POST a plain-English report to the
|
|
2120
|
+
// webapp, so a silently-broken agent shows an actionable dashboard alert
|
|
2121
|
+
// instead of a false green. Probes ONLY the configured venue — on a
|
|
2122
|
+
// Binance-451 host trading Hyperliquid a Binance probe would be a
|
|
2123
|
+
// permanent false alarm. Advisory + fire-and-forget; no token → no-op.
|
|
2124
|
+
// Runs here once (guarded by the pluginInitialised early-return → once per
|
|
2125
|
+
// process) + on an unref'd interval inside the reporter.
|
|
2126
|
+
startReadinessReporter({
|
|
2127
|
+
apiBaseUrl,
|
|
2128
|
+
token: resolveIngestToken({ connectionToken }),
|
|
2129
|
+
venue,
|
|
2130
|
+
publicApi: hlPublicApi ?? binanceApi,
|
|
2131
|
+
toolCount: toolNames.length,
|
|
2132
|
+
});
|
|
2023
2133
|
maybeStartConnectorSupervisor();
|
|
2024
2134
|
},
|
|
2025
2135
|
};
|
|
@@ -21,6 +21,11 @@ export interface AutoCaptureContext {
|
|
|
21
21
|
* from the current adapter (so it follows a runtime reconnect). Tags the
|
|
22
22
|
* journal position row so paper and live entries can be segregated. */
|
|
23
23
|
resolveMode?: () => 'paper' | 'live';
|
|
24
|
+
/** Venue this gateway trades ('binance' | 'hyperliquid'). Static per process
|
|
25
|
+
* — changing venue requires a config edit + restart (plan §5.2), so unlike
|
|
26
|
+
* `resolveMode` this is a plain value, not a resolver. Tags journal rows
|
|
27
|
+
* (positions.exchange, migration 0058). */
|
|
28
|
+
venue?: 'binance' | 'hyperliquid';
|
|
24
29
|
}
|
|
25
30
|
export interface CreateOrderInputs {
|
|
26
31
|
symbol: string;
|