@reefclaw/openclaw-plugin 0.1.3 → 0.1.5
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-public.d.ts +2 -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 +125 -16
- 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/persistence/state-manager.d.ts +24 -0
- package/persistence/state-manager.js +62 -4
- 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-crypto-metrics.js +10 -3
- 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 +24 -0
- package/venues/registry.js +47 -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. */
|
|
@@ -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, venueQuoteCurrency, } 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';
|
|
@@ -874,11 +876,24 @@ const paperTradingPlugin = {
|
|
|
874
876
|
return;
|
|
875
877
|
}
|
|
876
878
|
logger.info(TAG, 'Initializing paper trading plugin...');
|
|
877
|
-
// Resolve config from plugin settings
|
|
879
|
+
// Resolve config from plugin settings. The paper wallet's quote currency
|
|
880
|
+
// follows the VENUE (USDC on hyperliquid, USDT on binance — issue #174:
|
|
881
|
+
// a hardcoded USDT wallet reads as $0 equity on the USDC venue). The
|
|
882
|
+
// authoritative plugin-config read happens further down, AFTER the
|
|
883
|
+
// simulator exists — peek only the venue here; same file, same parse
|
|
884
|
+
// rules, and an unreadable config falls back to the binance default
|
|
885
|
+
// exactly like the main read does.
|
|
886
|
+
let paperQuoteCurrency = DEFAULT_CONFIG.quoteCurrency;
|
|
887
|
+
try {
|
|
888
|
+
paperQuoteCurrency = venueQuoteCurrency(parseVenue(readPluginConfig().exchange?.venue).venue);
|
|
889
|
+
}
|
|
890
|
+
catch {
|
|
891
|
+
/* unreadable plugin-config → binance default, matching the main read */
|
|
892
|
+
}
|
|
878
893
|
const pluginConfig = {
|
|
879
894
|
startingBalance: DEFAULT_CONFIG.startingBalance,
|
|
880
895
|
symbol: DEFAULT_CONFIG.symbol,
|
|
881
|
-
quoteCurrency:
|
|
896
|
+
quoteCurrency: paperQuoteCurrency,
|
|
882
897
|
};
|
|
883
898
|
logger.info(TAG, `Config: ${pluginConfig.startingBalance} ${pluginConfig.quoteCurrency}, symbol: ${pluginConfig.symbol}`);
|
|
884
899
|
// Load or initialize state (sync — OpenClaw ignores async register())
|
|
@@ -912,6 +927,14 @@ const paperTradingPlugin = {
|
|
|
912
927
|
let intelligenceUrl = 'https://intel.reefclaw.com';
|
|
913
928
|
let exchangeConfig = null;
|
|
914
929
|
let tradingMode = 'PAPER';
|
|
930
|
+
// Trading venue (multi-venue Phase 0 — docs/HYPERLIQUID_INTEGRATION_PLAN.md).
|
|
931
|
+
// Absent config field = 'binance', so every pre-venue install is untouched.
|
|
932
|
+
let venue = 'binance';
|
|
933
|
+
// exchange.testnet as written on disk — needed independently of
|
|
934
|
+
// exchangeConfig because the hyperliquid venue never builds a Binance
|
|
935
|
+
// credential config (Phase 0 gate) but its keyless public API still needs
|
|
936
|
+
// the mainnet/testnet routing.
|
|
937
|
+
let venueTestnet = false;
|
|
915
938
|
let signalsEvaluator = 'central';
|
|
916
939
|
let signalsSymbols = [];
|
|
917
940
|
try {
|
|
@@ -928,9 +951,18 @@ const paperTradingPlugin = {
|
|
|
928
951
|
if (Array.isArray(rcConfig?.signals?.symbols)) {
|
|
929
952
|
signalsSymbols = rcConfig.signals.symbols.map((s) => String(s).toUpperCase());
|
|
930
953
|
}
|
|
931
|
-
// Read exchange credentials (Phase 9b)
|
|
954
|
+
// Read exchange credentials (Phase 9b) + venue (multi-venue Phase 0)
|
|
932
955
|
const exchange = rcConfig?.exchange;
|
|
933
|
-
|
|
956
|
+
const venueParse = parseVenue(exchange?.venue);
|
|
957
|
+
venue = venueParse.venue;
|
|
958
|
+
venueTestnet = exchange?.testnet === true;
|
|
959
|
+
if (venueParse.unrecognized) {
|
|
960
|
+
logger.warn(TAG, `Unrecognized exchange.venue '${venueParse.unrecognized}' in plugin-config — treating as 'binance'`);
|
|
961
|
+
}
|
|
962
|
+
// apiKey/secret are a BINANCE credential pair; never build a Binance
|
|
963
|
+
// client config for another venue (a hyperliquid block carries
|
|
964
|
+
// walletAddress/agentPrivateKey instead — consumed from Phase 3 on).
|
|
965
|
+
if (venue === 'binance' && exchange?.apiKey && exchange?.secret) {
|
|
934
966
|
exchangeConfig = {
|
|
935
967
|
apiKey: exchange.apiKey,
|
|
936
968
|
secret: exchange.secret,
|
|
@@ -964,6 +996,14 @@ const paperTradingPlugin = {
|
|
|
964
996
|
else {
|
|
965
997
|
logger.warn(TAG, 'No connection token found — get_market_intel will return errors');
|
|
966
998
|
}
|
|
999
|
+
// Venue support gate (multi-venue Phase 0). Checked BEFORE the credentials
|
|
1000
|
+
// gate so a hyperliquid config gets the accurate diagnosis ("venue not
|
|
1001
|
+
// supported yet"), not a misleading "missing API keys". Fallback-to-PAPER,
|
|
1002
|
+
// never a throw — register() must stay non-crashing.
|
|
1003
|
+
if (tradingMode !== 'PAPER' && !isLiveVenueSupported(venue)) {
|
|
1004
|
+
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)`);
|
|
1005
|
+
tradingMode = 'PAPER';
|
|
1006
|
+
}
|
|
967
1007
|
// Validate trading mode vs credentials
|
|
968
1008
|
if (tradingMode !== 'PAPER' && !exchangeConfig) {
|
|
969
1009
|
logger.warn(TAG, `Trading mode ${tradingMode} requires exchange API keys — falling back to PAPER`);
|
|
@@ -1063,6 +1103,9 @@ const paperTradingPlugin = {
|
|
|
1063
1103
|
// closure — only invoked on a fill, long after `runtime` is built), so it
|
|
1064
1104
|
// follows a live<->paper reconnect.
|
|
1065
1105
|
resolveMode: () => (runtime.adapter.isLive ? 'live' : 'paper'),
|
|
1106
|
+
// Venue tag (positions.exchange, migration 0058) — static per process;
|
|
1107
|
+
// a venue change requires config edit + restart.
|
|
1108
|
+
venue,
|
|
1066
1109
|
};
|
|
1067
1110
|
if (positionDecisionsClient) {
|
|
1068
1111
|
logger.info(TAG, `Journal close-on-reduce-only-fill ${closeOnReduceOnlyFill ? 'ENABLED' : 'disabled'}`);
|
|
@@ -1146,6 +1189,11 @@ const paperTradingPlugin = {
|
|
|
1146
1189
|
tradeIngest = {
|
|
1147
1190
|
client: new TradeStoreClient({ baseUrl: ingestBaseUrl, ingestToken }),
|
|
1148
1191
|
userId: reefclawUserId,
|
|
1192
|
+
// Venue-derived FillEvent.exchange — 'binance_futures' for the
|
|
1193
|
+
// binance venue, i.e. byte-identical to the pre-venue literal
|
|
1194
|
+
// WsIngest defaulted to. Half of the (exchange, exchange_trade_id)
|
|
1195
|
+
// audit-trail idempotency key; never a fresh string literal.
|
|
1196
|
+
exchange: fillExchangeId(venue),
|
|
1149
1197
|
};
|
|
1150
1198
|
logger.info(TAG, `User-data stream dbWrite=on — WS audit-trail ingest wired to ${ingestBaseUrl} (userId=${reefclawUserId.slice(0, 8)}…)`);
|
|
1151
1199
|
}
|
|
@@ -1203,7 +1251,12 @@ const paperTradingPlugin = {
|
|
|
1203
1251
|
logger.warn(TAG, `approval mode=${approvalModeForTool} but WEBAPP_INGEST_TOKEN or REEFCLAW_USER_ID env missing — proposal path disabled`);
|
|
1204
1252
|
approvalModeForTool = 'off';
|
|
1205
1253
|
}
|
|
1206
|
-
|
|
1254
|
+
// Venue-dispatched construction (multi-venue Phase 0). For 'binance'
|
|
1255
|
+
// this is a pure pass-through to `new LiveAdapter(...)` — identical
|
|
1256
|
+
// args, identical behavior; the unsupported-venue arm is unreachable
|
|
1257
|
+
// here because the isLiveVenueSupported() gate above already fell back
|
|
1258
|
+
// to PAPER. Phase 3 adds the hyperliquid adapter inside the factory.
|
|
1259
|
+
const liveAdapter = createLiveAdapter(venue, exchangeConfig, tradingMode, microLiveConfig, bracketMode, userDataStreamMode, userDataStreamTunables, tradeIngest, autoCapture);
|
|
1207
1260
|
adapter = liveAdapter;
|
|
1208
1261
|
// ---- Approval-mode Phase B — start ProposalDecisionListener ----
|
|
1209
1262
|
// Started only when approval.mode='per_trade' AND proposalManagerCtx
|
|
@@ -1320,11 +1373,64 @@ const paperTradingPlugin = {
|
|
|
1320
1373
|
else {
|
|
1321
1374
|
adapter = new PaperAdapter(simulator);
|
|
1322
1375
|
}
|
|
1376
|
+
// PAPER market-data source selection.
|
|
1377
|
+
//
|
|
1378
|
+
// Venue precedence (Hyperliquid Phase 1 — docs/HYPERLIQUID_INTEGRATION_PLAN.md):
|
|
1379
|
+
// the configured VENUE decides which exchange prices paper mode, full stop.
|
|
1380
|
+
// venue='hyperliquid' → every public read (PaperMarketFeed marks, the
|
|
1381
|
+
// simulator's fill prices, fetch_ticker/fetch_ohlcv/get_orderbook tools)
|
|
1382
|
+
// comes from Hyperliquid's keyless /info endpoints; the Binance-only
|
|
1383
|
+
// `paperMarketDataSource:'intel'` escape hatch does not apply (intel has
|
|
1384
|
+
// no HL rows until Phase 2 — silently serving Binance prices for a
|
|
1385
|
+
// Hyperliquid book would be a lie, the exact class the symbol-translation
|
|
1386
|
+
// rule forbids).
|
|
1387
|
+
//
|
|
1388
|
+
// Binance venue (default) is unchanged: a host Binance geo-blocks (HTTP
|
|
1389
|
+
// 451) can route PRICE reads through the intel service by setting
|
|
1390
|
+
// plugin-config `paperMarketDataSource:'intel'` (GET /api/price/:symbol).
|
|
1391
|
+
// LIVE always uses the real exchange, and the readiness reporter probes
|
|
1392
|
+
// the CONFIGURED venue. Kill-switch RC_PAPER_MARKET_DATA=binance forces
|
|
1393
|
+
// the Binance-direct default (binance venue only).
|
|
1394
|
+
//
|
|
1395
|
+
// hlPublicApi is constructed once per process when the venue is
|
|
1396
|
+
// hyperliquid — shared by the paper data path and the readiness probe.
|
|
1397
|
+
const hlPublicApi = venue === 'hyperliquid' ? new HyperliquidPublicApi({ testnet: venueTestnet }) : null;
|
|
1398
|
+
let configuredPaperSource;
|
|
1399
|
+
try {
|
|
1400
|
+
configuredPaperSource = readPluginConfig().paperMarketDataSource;
|
|
1401
|
+
}
|
|
1402
|
+
catch {
|
|
1403
|
+
configuredPaperSource = undefined;
|
|
1404
|
+
}
|
|
1405
|
+
const paperDataSource = process.env.RC_PAPER_MARKET_DATA === 'binance'
|
|
1406
|
+
? 'binance'
|
|
1407
|
+
: configuredPaperSource ?? 'binance';
|
|
1408
|
+
const useIntelPaperData = venue === 'binance' &&
|
|
1409
|
+
!adapter.isLive && paperDataSource === 'intel' && connectionToken.length > 0;
|
|
1410
|
+
const marketDataApi = !adapter.isLive && hlPublicApi
|
|
1411
|
+
? hlPublicApi
|
|
1412
|
+
: useIntelPaperData
|
|
1413
|
+
? new IntelPublicApi({ connectionToken, intelligenceUrl })
|
|
1414
|
+
: binanceApi;
|
|
1415
|
+
if (!adapter.isLive && hlPublicApi) {
|
|
1416
|
+
logger.info(TAG, `PAPER market data sourced from Hyperliquid${venueTestnet ? ' TESTNET' : ''} (venue=hyperliquid) — use USDC pairs (e.g. BTC/USDC)`);
|
|
1417
|
+
if (paperDataSource === 'intel') {
|
|
1418
|
+
logger.warn(TAG, 'paperMarketDataSource=intel is a Binance-venue option — ignored on venue=hyperliquid (intel has no Hyperliquid rows until Phase 2)');
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
else if (useIntelPaperData) {
|
|
1422
|
+
logger.warn(TAG, 'PAPER market data routed through intel (paperMarketDataSource=intel) — prices from GET /api/price; live + readiness still use Binance directly');
|
|
1423
|
+
}
|
|
1424
|
+
else if (venue === 'binance' && !adapter.isLive && paperDataSource === 'intel' && connectionToken.length === 0) {
|
|
1425
|
+
logger.warn(TAG, 'paperMarketDataSource=intel requested but no connection token — falling back to Binance direct (which will 451 on a geo-blocked host)');
|
|
1426
|
+
}
|
|
1323
1427
|
// Build tool dependencies
|
|
1324
1428
|
// `simDeps` for tools that need the simulator directly (ticker, market structure, risk scenario)
|
|
1325
1429
|
// `adapterDeps` for the 7 adapter-based trading tools
|
|
1326
|
-
|
|
1327
|
-
|
|
1430
|
+
// Both carry `marketDataApi` (Binance direct, or intel in paper mode) so
|
|
1431
|
+
// every paper market-data read follows the selected source at once.
|
|
1432
|
+
const simDeps = { binanceApi: marketDataApi, simulator };
|
|
1433
|
+
const adapterDeps = { binanceApi: marketDataApi, adapter };
|
|
1328
1434
|
// ---- Plugin runtime (mutable holder) ----
|
|
1329
1435
|
// Owns the adapter + mode + stop-watcher and exposes `reconnect()` so the
|
|
1330
1436
|
// operator can swap credentials / trading mode from the dashboard without
|
|
@@ -1357,7 +1463,7 @@ const paperTradingPlugin = {
|
|
|
1357
1463
|
// resting limits fill. Paper-only — started here when the boot adapter is
|
|
1358
1464
|
// paper; PluginRuntime.reconnect() stops it on a switch to live and
|
|
1359
1465
|
// restarts it on a switch back to paper. Reuses the ban-gated fetchTicker.
|
|
1360
|
-
const marketFeed = new PaperMarketFeed(
|
|
1466
|
+
const marketFeed = new PaperMarketFeed(marketDataApi, simulator);
|
|
1361
1467
|
if (!adapter.isLive) {
|
|
1362
1468
|
marketFeed.start();
|
|
1363
1469
|
}
|
|
@@ -2022,16 +2128,19 @@ const paperTradingPlugin = {
|
|
|
2022
2128
|
pluginInitialised = true;
|
|
2023
2129
|
logger.info(TAG, `Registered ${gatedTools.length} tools (gate mode=${toolGate.getMode()}): ${toolNames.join(', ')}. Plugin v3.8.0 (${runtime.mode} mode)`);
|
|
2024
2130
|
// Agent-readiness reporter (docs/AGENT_READINESS_GATE_PLAN.md Phase 1):
|
|
2025
|
-
// probe host→
|
|
2026
|
-
// host and POST a plain-English report to the
|
|
2027
|
-
// agent shows an actionable dashboard alert
|
|
2028
|
-
//
|
|
2029
|
-
//
|
|
2030
|
-
//
|
|
2131
|
+
// probe host→venue reachability (Binance HTTP 451 geo-block / Hyperliquid
|
|
2132
|
+
// /info) + clock drift on the host and POST a plain-English report to the
|
|
2133
|
+
// webapp, so a silently-broken agent shows an actionable dashboard alert
|
|
2134
|
+
// instead of a false green. Probes ONLY the configured venue — on a
|
|
2135
|
+
// Binance-451 host trading Hyperliquid a Binance probe would be a
|
|
2136
|
+
// permanent false alarm. Advisory + fire-and-forget; no token → no-op.
|
|
2137
|
+
// Runs here once (guarded by the pluginInitialised early-return → once per
|
|
2138
|
+
// process) + on an unref'd interval inside the reporter.
|
|
2031
2139
|
startReadinessReporter({
|
|
2032
2140
|
apiBaseUrl,
|
|
2033
2141
|
token: resolveIngestToken({ connectionToken }),
|
|
2034
|
-
|
|
2142
|
+
venue,
|
|
2143
|
+
publicApi: hlPublicApi ?? binanceApi,
|
|
2035
2144
|
toolCount: toolNames.length,
|
|
2036
2145
|
});
|
|
2037
2146
|
maybeStartConnectorSupervisor();
|
|
@@ -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;
|
|
@@ -85,6 +85,7 @@ export async function onCreateOrderFilled(ctx, inputs, order) {
|
|
|
85
85
|
currentSize: filledQty,
|
|
86
86
|
avgEntryPrice: fillPrice,
|
|
87
87
|
mode: ctx.resolveMode?.(),
|
|
88
|
+
exchange: ctx.venue,
|
|
88
89
|
};
|
|
89
90
|
// postPosition is awaited — we need the UUID before posting the entry row.
|
|
90
91
|
const positionId = await ctx.decisionsClient.postPosition(ctx.userId, upsert);
|
|
@@ -253,6 +254,7 @@ export async function onWsFillObserved(ctx, fill) {
|
|
|
253
254
|
currentSize: fill.fillSize,
|
|
254
255
|
avgEntryPrice: fill.fillPrice,
|
|
255
256
|
mode: ctx.resolveMode?.(),
|
|
257
|
+
exchange: ctx.venue,
|
|
256
258
|
};
|
|
257
259
|
const positionId = await ctx.decisionsClient.postPosition(ctx.userId, upsert);
|
|
258
260
|
if (!positionId) {
|
|
@@ -332,8 +334,12 @@ async function handleReduceOnlyExit(ctx, fill) {
|
|
|
332
334
|
regimeConfidence: 0.5,
|
|
333
335
|
fillPrice: fill.fillPrice,
|
|
334
336
|
fillSize: Math.abs(fill.fillSize),
|
|
335
|
-
// realizedPnl is Binance-exact (summed across exit fills)
|
|
336
|
-
//
|
|
337
|
+
// realizedPnl is Binance-exact (summed across exit fills); the zeros below
|
|
338
|
+
// are filled in PER-METRIC by the webapp close route (price-based R from
|
|
339
|
+
// the pinned invalidation_price, mfe/give-back from the last review — see
|
|
340
|
+
// webapp lib/api/close-metrics.ts). The route's old all-four-zero gate
|
|
341
|
+
// meant this non-zero PnL used to suppress that fill-in entirely, persisting
|
|
342
|
+
// realized_r=0 on every bracket_fill close (the 2026-07-11 zero-hole).
|
|
337
343
|
realizedPnl,
|
|
338
344
|
realizedR: 0,
|
|
339
345
|
mfeRAtClose: 0,
|
|
@@ -17,6 +17,10 @@ export interface PositionUpsertPayload {
|
|
|
17
17
|
* journal row so paper and live positions can be segregated. Omitted only
|
|
18
18
|
* if the active adapter can't be resolved (treated as legacy/NULL). */
|
|
19
19
|
mode?: 'paper' | 'live';
|
|
20
|
+
/** Venue the position was opened on ('binance' | 'hyperliquid') — journal /
|
|
21
|
+
* analytics segmentation (fees + funding cadence differ per venue).
|
|
22
|
+
* Absent → NULL (legacy binance), mirroring `mode`. Migration 0058. */
|
|
23
|
+
exchange?: string;
|
|
20
24
|
}
|
|
21
25
|
export interface PositionEntryPayload {
|
|
22
26
|
positionId: string;
|
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
import { type ReadinessReport } from '@reefclaw/shared';
|
|
2
|
-
|
|
1
|
+
import { type ReadinessReport, type VenueId } from '@reefclaw/shared';
|
|
2
|
+
/** Venue-agnostic reachability probe — BinancePublicApi.probeReachability and
|
|
3
|
+
* HyperliquidPublicApi.probeReachability both return exactly this shape. */
|
|
4
|
+
export interface VenueReachabilityProbe {
|
|
5
|
+
probeReachability(): Promise<{
|
|
6
|
+
outcome: 'reachable' | 'geo_blocked' | 'unreachable' | 'unknown';
|
|
7
|
+
driftMs: number | null;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
3
10
|
export interface ReadinessReporterOptions {
|
|
4
11
|
apiBaseUrl: string;
|
|
5
12
|
token: string;
|
|
6
|
-
|
|
13
|
+
/** Which venue this agent trades — decides the reachability check id
|
|
14
|
+
* (binance_reachable vs hyperliquid_reachable) and which host `publicApi`
|
|
15
|
+
* probes. The reporter probes ONLY the configured venue: on a
|
|
16
|
+
* Binance-451-blocked host trading Hyperliquid, a Binance probe would be
|
|
17
|
+
* a permanent false alarm (plan §5.11). */
|
|
18
|
+
venue: VenueId;
|
|
19
|
+
/** Public API of the CONFIGURED venue (probe only). */
|
|
20
|
+
publicApi: VenueReachabilityProbe;
|
|
7
21
|
/** Number of trading tools registered (a health signal). */
|
|
8
22
|
toolCount: number;
|
|
9
23
|
fetchImpl?: typeof fetch;
|
|
@@ -11,8 +25,17 @@ export interface ReadinessReporterOptions {
|
|
|
11
25
|
requestTimeoutMs?: number;
|
|
12
26
|
}
|
|
13
27
|
/** Run every connect-phase check once and assemble the report. Exported for
|
|
14
|
-
* unit tests.
|
|
15
|
-
|
|
28
|
+
* unit tests.
|
|
29
|
+
*
|
|
30
|
+
* `bootWarmup` marks the FIRST cycle, which fires during gateway boot while
|
|
31
|
+
* the event loop is congested (WS start, snapshot, seed all racing). That
|
|
32
|
+
* delay between the probe's server-time response and the local `Date.now()`
|
|
33
|
+
* read inflates the apparent clock drift — observed −4112ms at boot
|
|
34
|
+
* self-correcting to −108ms on the next 5-min cycle. During warm-up a
|
|
35
|
+
* warn/fail drift is reported 'unknown' (not amber/red) so the readiness
|
|
36
|
+
* banner doesn't cry-wolf for ~5 min after every restart; a genuinely
|
|
37
|
+
* skewed clock still surfaces on cycle 2. */
|
|
38
|
+
export declare function collectReadiness(opts: Pick<ReadinessReporterOptions, 'venue' | 'publicApi' | 'toolCount'>, bootWarmup?: boolean): Promise<ReadinessReport>;
|
|
16
39
|
/** Test-only — reset the singleton guard between unit tests. */
|
|
17
40
|
export declare function __resetReadinessReporterForTests(): void;
|
|
18
41
|
/** Fire the readiness report once at boot then on an unref'd interval. */
|
|
@@ -24,10 +24,24 @@ function resolveIntervalMs(explicit) {
|
|
|
24
24
|
return Math.max(MIN_INTERVAL_MS, raw);
|
|
25
25
|
}
|
|
26
26
|
/** Run every connect-phase check once and assemble the report. Exported for
|
|
27
|
-
* unit tests.
|
|
28
|
-
|
|
27
|
+
* unit tests.
|
|
28
|
+
*
|
|
29
|
+
* `bootWarmup` marks the FIRST cycle, which fires during gateway boot while
|
|
30
|
+
* the event loop is congested (WS start, snapshot, seed all racing). That
|
|
31
|
+
* delay between the probe's server-time response and the local `Date.now()`
|
|
32
|
+
* read inflates the apparent clock drift — observed −4112ms at boot
|
|
33
|
+
* self-correcting to −108ms on the next 5-min cycle. During warm-up a
|
|
34
|
+
* warn/fail drift is reported 'unknown' (not amber/red) so the readiness
|
|
35
|
+
* banner doesn't cry-wolf for ~5 min after every restart; a genuinely
|
|
36
|
+
* skewed clock still surfaces on cycle 2. */
|
|
37
|
+
export async function collectReadiness(opts, bootWarmup = false) {
|
|
29
38
|
const now = Date.now();
|
|
30
39
|
const checks = [];
|
|
40
|
+
// The venue decides which reachability check this report carries; the copy
|
|
41
|
+
// for both ids lives in shared/src/readiness.ts. Clock drift comes from the
|
|
42
|
+
// same probe on both venues (Binance fapi serverTime / Hyperliquid
|
|
43
|
+
// exchangeStatus.time — live-verified 2026-07-11).
|
|
44
|
+
const reachId = opts.venue === 'hyperliquid' ? 'hyperliquid_reachable' : 'binance_reachable';
|
|
31
45
|
// plugin_loaded — trivially true (this code runs inside the loaded plugin),
|
|
32
46
|
// but a positive row is what proves the report path is alive at all.
|
|
33
47
|
checks.push(makeReadinessCheck('plugin_loaded', 'pass', { checkedAt: now }));
|
|
@@ -36,37 +50,46 @@ export async function collectReadiness(opts) {
|
|
|
36
50
|
detail: `${opts.toolCount} tools`,
|
|
37
51
|
checkedAt: now,
|
|
38
52
|
}));
|
|
39
|
-
//
|
|
40
|
-
const probe = await opts.
|
|
53
|
+
// venue reachability (+ clock drift from the same probe response)
|
|
54
|
+
const probe = await opts.publicApi.probeReachability();
|
|
41
55
|
if (probe.outcome === 'reachable') {
|
|
42
|
-
checks.push(makeReadinessCheck(
|
|
56
|
+
checks.push(makeReadinessCheck(reachId, 'pass', { checkedAt: now }));
|
|
43
57
|
const drift = probe.driftMs;
|
|
44
58
|
if (drift == null) {
|
|
45
59
|
checks.push(makeReadinessCheck('clock_in_sync', 'unknown', { checkedAt: now }));
|
|
46
60
|
}
|
|
47
61
|
else {
|
|
48
62
|
const abs = Math.abs(drift);
|
|
49
|
-
const
|
|
63
|
+
const rawStatus = abs <= 1000 ? 'pass' : abs <= 5000 ? 'warn' : 'fail';
|
|
64
|
+
// Boot warm-up: a non-pass reading on the first cycle is untrustworthy
|
|
65
|
+
// (event-loop congestion inflates the measured drift) — report 'unknown'
|
|
66
|
+
// and let the next cycle confirm, rather than flash the banner amber/red.
|
|
67
|
+
const status = bootWarmup && rawStatus !== 'pass' ? 'unknown' : rawStatus;
|
|
50
68
|
checks.push(makeReadinessCheck('clock_in_sync', status, {
|
|
51
|
-
detail:
|
|
69
|
+
detail: bootWarmup && rawStatus !== 'pass'
|
|
70
|
+
? `drift ${Math.round(drift)}ms (boot warm-up — rechecking)`
|
|
71
|
+
: `drift ${Math.round(drift)}ms`,
|
|
52
72
|
checkedAt: now,
|
|
53
73
|
}));
|
|
54
74
|
}
|
|
55
75
|
}
|
|
56
76
|
else if (probe.outcome === 'geo_blocked') {
|
|
57
|
-
checks.push(makeReadinessCheck(
|
|
77
|
+
checks.push(makeReadinessCheck(reachId, 'fail', {
|
|
78
|
+
detail: opts.venue === 'binance' ? 'HTTP 451' : 'blocked (HTTP 451/403)',
|
|
79
|
+
checkedAt: now,
|
|
80
|
+
}));
|
|
58
81
|
checks.push(makeReadinessCheck('clock_in_sync', 'unknown', { checkedAt: now }));
|
|
59
82
|
}
|
|
60
83
|
else if (probe.outcome === 'unreachable') {
|
|
61
84
|
// Network/DNS/timeout — could be transient, so warn (amber) rather than
|
|
62
85
|
// asserting a definitive failure. A persistent problem stays amber across
|
|
63
|
-
// re-checks; only the
|
|
64
|
-
checks.push(makeReadinessCheck(
|
|
86
|
+
// re-checks; only the geo-block is a hard red.
|
|
87
|
+
checks.push(makeReadinessCheck(reachId, 'warn', { detail: 'unreachable', checkedAt: now }));
|
|
65
88
|
checks.push(makeReadinessCheck('clock_in_sync', 'unknown', { checkedAt: now }));
|
|
66
89
|
}
|
|
67
90
|
else {
|
|
68
91
|
// 'unknown' — the ban/weight gate paused the probe; don't assert anything.
|
|
69
|
-
checks.push(makeReadinessCheck(
|
|
92
|
+
checks.push(makeReadinessCheck(reachId, 'unknown', { checkedAt: now }));
|
|
70
93
|
checks.push(makeReadinessCheck('clock_in_sync', 'unknown', { checkedAt: now }));
|
|
71
94
|
}
|
|
72
95
|
return {
|
|
@@ -74,7 +97,7 @@ export async function collectReadiness(opts) {
|
|
|
74
97
|
generatedAt: now,
|
|
75
98
|
overall: deriveOverallReadiness(checks),
|
|
76
99
|
checks,
|
|
77
|
-
agent: { pluginVersion: PLUGIN_VERSION, toolCount: opts.toolCount },
|
|
100
|
+
agent: { pluginVersion: PLUGIN_VERSION, toolCount: opts.toolCount, venue: opts.venue },
|
|
78
101
|
};
|
|
79
102
|
}
|
|
80
103
|
async function postReadiness(apiBaseUrl, token, report, fetchImpl, timeoutMs) {
|
|
@@ -119,12 +142,9 @@ export function startReadinessReporter(opts) {
|
|
|
119
142
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
120
143
|
const intervalMs = resolveIntervalMs(opts.intervalMs);
|
|
121
144
|
const timeoutMs = opts.requestTimeoutMs ?? 10_000;
|
|
122
|
-
const cycle = async () => {
|
|
145
|
+
const cycle = async (bootWarmup) => {
|
|
123
146
|
try {
|
|
124
|
-
const report = await collectReadiness({
|
|
125
|
-
binanceApi: opts.binanceApi,
|
|
126
|
-
toolCount: opts.toolCount,
|
|
127
|
-
});
|
|
147
|
+
const report = await collectReadiness({ venue: opts.venue, publicApi: opts.publicApi, toolCount: opts.toolCount }, bootWarmup);
|
|
128
148
|
await postReadiness(opts.apiBaseUrl, opts.token, report, fetchImpl, timeoutMs);
|
|
129
149
|
if (report.overall === 'fail') {
|
|
130
150
|
const failing = report.checks.filter((c) => c.status === 'fail').map((c) => c.id).join(', ');
|
|
@@ -135,8 +155,9 @@ export function startReadinessReporter(opts) {
|
|
|
135
155
|
logger.warn(TAG, `readiness cycle failed: ${formatError(err)}`);
|
|
136
156
|
}
|
|
137
157
|
};
|
|
138
|
-
|
|
139
|
-
|
|
158
|
+
// First fire is the boot cycle (warm-up); the interval cycles are steady-state.
|
|
159
|
+
void cycle(true);
|
|
160
|
+
const timer = setInterval(() => void cycle(false), intervalMs);
|
|
140
161
|
timer.unref();
|
|
141
162
|
logger.info(TAG, `readiness reporter started (interval ${Math.round(intervalMs / 1000)}s)`);
|
|
142
163
|
}
|
package/live/bracket-id.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { VenueId } from '@reefclaw/shared';
|
|
1
2
|
import type { BracketId, BracketRole } from './bracket-types.js';
|
|
2
3
|
/** Generate a fresh 16-char hex bracketId. */
|
|
3
4
|
export declare function generateBracketId(): BracketId;
|
|
@@ -16,3 +17,11 @@ export declare function parseBracketCid(cid: string): {
|
|
|
16
17
|
/** Cheap type-guard for "is this a reefclaw-managed bracket clientOrderId"
|
|
17
18
|
* — true for both the current and legacy schemes. */
|
|
18
19
|
export declare function isBracketCid(cid: string): boolean;
|
|
20
|
+
/** Venue-aware parseBracketCid. Binance delegates to the existing dual-scheme
|
|
21
|
+
* parser; other venues return null until their scheme ships. */
|
|
22
|
+
export declare function parseBracketClientId(venue: VenueId, cid: string): {
|
|
23
|
+
bracketId: BracketId;
|
|
24
|
+
role: BracketRole;
|
|
25
|
+
} | null;
|
|
26
|
+
/** Venue-aware isBracketCid. */
|
|
27
|
+
export declare function isBracketClientId(venue: VenueId, cid: string): boolean;
|