@reefclaw/openclaw-plugin 0.1.14 → 0.1.16
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.d.ts +20 -5
- package/bridge/bridge.js +29 -14
- package/bridge/config.js +6 -0
- package/bridge/gateway/gateway-ws-client.d.ts +4 -1
- package/bridge/gateway/gateway-ws-client.js +41 -11
- package/bridge/providers/gateway.d.ts +28 -0
- package/bridge/providers/gateway.js +130 -5
- package/bridge/providers/onboarding-commands.d.ts +8 -5
- package/bridge/providers/onboarding-commands.js +1 -1
- package/bridge/providers/risk-calculator.d.ts +61 -2
- package/bridge/providers/risk-calculator.js +92 -20
- package/bridge/utils/skill-signing.js +8 -3
- package/config/plugin-config-io.js +5 -0
- package/exchange-adapter.d.ts +13 -0
- package/index.js +15 -4
- package/ingest/event-loop-monitor.d.ts +11 -0
- package/ingest/event-loop-monitor.js +77 -0
- package/ingest/readiness-reporter.d.ts +9 -0
- package/ingest/readiness-reporter.js +54 -5
- package/live/user-data-stream.js +10 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +9 -6
- package/risk/pre-trade-check.js +18 -5
- package/skills/reefclaw/SKILL.md +6 -11
- package/strategy/condition-registry.js +9 -2
- package/strategy/evaluator.d.ts +5 -0
- package/tools/cancel-all-orders.js +9 -1
- package/tools/create-order.js +18 -1
- package/tools/get-bracket-config.d.ts +21 -2
- package/tools/get-bracket-config.js +18 -2
- package/tools/set-trading-mode.js +6 -3
- package/venues/hyperliquid/hl-fill-ingest.js +20 -1
- package/venues/hyperliquid/hl-live-adapter.d.ts +4 -0
- package/venues/hyperliquid/hl-live-adapter.js +4 -0
- package/venues/registry.js +8 -7
- package/wave9/paper-admission-guard.d.ts +12 -1
- package/wave9/paper-admission-guard.js +12 -1
- package/scripts/assemble.mjs +0 -130
package/risk/pre-trade-check.js
CHANGED
|
@@ -57,9 +57,22 @@ export function adjustLimitByVol(defaultLimit, volFactor) {
|
|
|
57
57
|
return defaultLimit;
|
|
58
58
|
return defaultLimit / volFactor;
|
|
59
59
|
}
|
|
60
|
+
/** Strip a CCXT settle suffix ('AVAX/USDC:USDC' → 'AVAX/USDC') before any
|
|
61
|
+
* same-symbol comparison. Live adapter positions carry SUFFIXED ccxt symbols
|
|
62
|
+
* while the agent's order symbol is canonical/unsuffixed — an exact string
|
|
63
|
+
* match therefore NEVER matched on live, so a close-shaped order was
|
|
64
|
+
* misclassified as a fresh entry (entry-gated, breaking "exits always work"),
|
|
65
|
+
* a flip was never netted, and a scale-in's existing leg was invisible to the
|
|
66
|
+
* resulting-notional cap. Paper positions are unsuffixed, so paper was
|
|
67
|
+
* unaffected — exactly the CLAUDE.md symbol-normalization trap. Inline copy
|
|
68
|
+
* of venues/symbols.ts stripSettleSuffix (module-private there; this module
|
|
69
|
+
* stays dependency-light like bracket-ledger.ts). */
|
|
70
|
+
function canonSymbol(symbol) {
|
|
71
|
+
return symbol.replace(/:[A-Z]+$/, '');
|
|
72
|
+
}
|
|
60
73
|
/** Check if an order is closing an existing position (partial or full). */
|
|
61
74
|
export function isClosingOrder(order, positions) {
|
|
62
|
-
const pos = positions.find(p => p.symbol === order.symbol);
|
|
75
|
+
const pos = positions.find(p => canonSymbol(p.symbol) === canonSymbol(order.symbol));
|
|
63
76
|
if (!pos)
|
|
64
77
|
return false;
|
|
65
78
|
// Sell against a long = closing; buy against a short = closing
|
|
@@ -103,7 +116,7 @@ export function preTradeRiskCheck(order, portfolio, limits = DEFAULT_PRE_TRADE_L
|
|
|
103
116
|
// exposure — even in the RED zone (M6).
|
|
104
117
|
// - same-direction add (scale-in) / fresh entry → full entry checks (M5).
|
|
105
118
|
// Emergency flatten bypasses this function entirely (EmergencyControls → adapter.createOrder).
|
|
106
|
-
const closingPos = portfolio.positions.find(p => p.symbol === order.symbol &&
|
|
119
|
+
const closingPos = portfolio.positions.find(p => canonSymbol(p.symbol) === canonSymbol(order.symbol) &&
|
|
107
120
|
((p.side === 'long' && order.side === 'sell') || (p.side === 'short' && order.side === 'buy')));
|
|
108
121
|
const flipAmount = closingPos ? order.amount - closingPos.quantity : 0;
|
|
109
122
|
const isFlip = closingPos != null && flipAmount > 1e-9 * Math.max(closingPos.quantity, 1);
|
|
@@ -211,7 +224,7 @@ export function preTradeRiskCheck(order, portfolio, limits = DEFAULT_PRE_TRADE_L
|
|
|
211
224
|
// is only the new opposite leg (the old leg is closed), so the existing
|
|
212
225
|
// notional is NOT added there.
|
|
213
226
|
const entryNotional = entryOrder.amount * order.price;
|
|
214
|
-
const sameSymbolPos = portfolio.positions.find(p => p.symbol === order.symbol);
|
|
227
|
+
const sameSymbolPos = portfolio.positions.find(p => canonSymbol(p.symbol) === canonSymbol(order.symbol));
|
|
215
228
|
const isScaleIn = sameSymbolPos != null &&
|
|
216
229
|
((sameSymbolPos.side === 'long' && order.side === 'buy') ||
|
|
217
230
|
(sameSymbolPos.side === 'short' && order.side === 'sell'));
|
|
@@ -228,8 +241,8 @@ export function preTradeRiskCheck(order, portfolio, limits = DEFAULT_PRE_TRADE_L
|
|
|
228
241
|
});
|
|
229
242
|
}
|
|
230
243
|
// 2. Open positions count (would a new symbol exceed limit?)
|
|
231
|
-
const existingSymbols = new Set(portfolio.positions.map(p => p.symbol));
|
|
232
|
-
if (!existingSymbols.has(order.symbol)) {
|
|
244
|
+
const existingSymbols = new Set(portfolio.positions.map(p => canonSymbol(p.symbol)));
|
|
245
|
+
if (!existingSymbols.has(canonSymbol(order.symbol))) {
|
|
233
246
|
const newCount = existingSymbols.size + 1;
|
|
234
247
|
if (newCount > limits.maxOpenPositions) {
|
|
235
248
|
violations.push({
|
package/skills/reefclaw/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reefclaw
|
|
3
|
-
version: 0.0.
|
|
3
|
+
version: 0.0.7
|
|
4
4
|
description: ReefClaw trading control room — bootstrap (connects your agent; full trading instructions arrive automatically after first connect)
|
|
5
5
|
author: ReefClaw
|
|
6
6
|
homepage: https://reefclaw.com
|
|
@@ -24,18 +24,13 @@ config:
|
|
|
24
24
|
description: ReefClaw relay WebSocket URL. Defaults to the production relay when omitted.
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
-
<!-- BOOTSTRAP SKILL.md — this is the thin installer copy shipped in the
|
|
28
|
-
@reefclaw/connect npm package. It carries ONLY the config schema and the
|
|
29
|
-
connect instructions. The FULL trading instructions (versioned 2.x.y) are
|
|
30
|
-
delivered automatically by the ReefClaw connector minutes after the first
|
|
31
|
-
successful connect, over the authenticated update channel — they replace
|
|
32
|
-
this file in place (a 0.0.x version always loses the semver comparison).
|
|
33
|
-
The "Connecting (first run)" section below MUST stay in sync with the
|
|
34
|
-
same section in skill/SKILL.md. -->
|
|
35
|
-
|
|
36
27
|
# ReefClaw — Connect Your Agent (Bootstrap)
|
|
37
28
|
|
|
38
|
-
|
|
29
|
+
## About this file
|
|
30
|
+
|
|
31
|
+
This is the thin BOOTSTRAP copy shipped in the `@reefclaw/connect` npm package. It covers **installing the ReefClaw plugin and connecting to the user's ReefClaw account** — nothing else. The FULL trading instructions (versioned 2.x.y) are delivered automatically by the ReefClaw connector minutes after the first successful connect, over the authenticated update channel — they replace this file in place (a 0.0.x version always loses the semver comparison). Maintainers: the "Connecting (first run)" section below MUST stay in sync with the same section in `skill/SKILL.md`.
|
|
32
|
+
|
|
33
|
+
You are being connected to **ReefClaw**, a supervised trading control room. Your operator watches everything you do on the ReefClaw dashboard. Some steps below change host configuration or install software; they exist solely to complete the setup **the user initiated by pasting their connect message**. Tell the user what each step changes before you do it, do it only for that user, and revert any temporary permission (like elevated exec) as soon as the install completes.
|
|
39
34
|
|
|
40
35
|
**Do not attempt to trade before the full instructions arrive.** If trading tools are visible but this file is still a bootstrap version (0.0.x), the connection has not completed — finish connecting first.
|
|
41
36
|
|
|
@@ -36,20 +36,27 @@ register('oi_slope', (fact) => ({
|
|
|
36
36
|
name: 'oi_slope',
|
|
37
37
|
met: fact.conditions.oi_slope_up ?? false,
|
|
38
38
|
}));
|
|
39
|
-
// 5. no_liquidation_cluster —
|
|
39
|
+
// 5. no_liquidation_cluster — FAIL-OPEN PLACEHOLDER in scan context: the
|
|
40
|
+
// fact-computer ships no liquidation data, so this preview cannot evaluate
|
|
41
|
+
// it. `note` marks the gap explicitly (never a silent pass); the
|
|
42
|
+
// authoritative evaluation runs in the central signal engine, which reads the
|
|
43
|
+
// live liquidation-levels feed.
|
|
40
44
|
register('no_liquidation_cluster', () => ({
|
|
41
45
|
name: 'no_liquidation_cluster',
|
|
42
46
|
met: true,
|
|
47
|
+
note: 'not evaluated in scan preview (no liquidation data in facts) — authoritative check runs in the signal engine',
|
|
43
48
|
}));
|
|
44
49
|
// 6. price_sweep
|
|
45
50
|
register('price_sweep', (fact) => ({
|
|
46
51
|
name: 'price_sweep',
|
|
47
52
|
met: (fact.conditions.price_sweep_high ?? false) || (fact.conditions.price_sweep_low ?? false),
|
|
48
53
|
}));
|
|
49
|
-
// 7. liquidations_at_sweep —
|
|
54
|
+
// 7. liquidations_at_sweep — FAIL-OPEN PLACEHOLDER in scan context, same gap
|
|
55
|
+
// and same note contract as no_liquidation_cluster above.
|
|
50
56
|
register('liquidations_at_sweep', () => ({
|
|
51
57
|
name: 'liquidations_at_sweep',
|
|
52
58
|
met: true,
|
|
59
|
+
note: 'not evaluated in scan preview (no liquidation data in facts) — authoritative check runs in the signal engine',
|
|
53
60
|
}));
|
|
54
61
|
// 8. order_flow_absorption
|
|
55
62
|
register('order_flow_absorption', (fact) => ({
|
package/strategy/evaluator.d.ts
CHANGED
|
@@ -45,6 +45,11 @@ export interface ConditionEvalResult {
|
|
|
45
45
|
name: string;
|
|
46
46
|
met: boolean;
|
|
47
47
|
value?: number;
|
|
48
|
+
/** Set when `met` is a fail-open placeholder rather than a real evaluation
|
|
49
|
+
* (e.g. the scan facts carry no data for this condition). Surfaces the gap
|
|
50
|
+
* to any consumer instead of letting a pass silently impersonate a check —
|
|
51
|
+
* the authoritative evaluation runs in the central signal engine. */
|
|
52
|
+
note?: string;
|
|
48
53
|
}
|
|
49
54
|
export interface StrategyEvalResult {
|
|
50
55
|
strategy: string;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
// Tool: cancel_all_orders — cancel all open orders (paper or live)
|
|
2
|
-
//
|
|
2
|
+
//
|
|
3
|
+
// NO readiness gate and NO confirmation prompt — BY DESIGN, not an oversight:
|
|
4
|
+
// this is the safety floor's kill-switch primitive ("Hard controls NEVER
|
|
5
|
+
// disabled or hidden"). Any gate added here becomes a failure mode of the
|
|
6
|
+
// emergency path itself — a wedged confirmation would strand live orders
|
|
7
|
+
// during the exact incident the kill switch exists for. Risk-reducing only:
|
|
8
|
+
// it cancels WORKING orders; the adapter layer preserves protective bracket
|
|
9
|
+
// legs (parseBracketCid skip in cancelAllOrders) so positions are never left
|
|
10
|
+
// naked, and it never opens or increases exposure.
|
|
3
11
|
export async function cancelAllOrdersTool(args, deps) {
|
|
4
12
|
const cancel = () => deps.adapter.cancelAllOrders(args.symbol);
|
|
5
13
|
return deps.adapter.isLive && deps.operationLock
|
package/tools/create-order.js
CHANGED
|
@@ -1008,11 +1008,28 @@ export async function createOrderTool(args, deps) {
|
|
|
1008
1008
|
}
|
|
1009
1009
|
// Bracket enforcement only applies in live mode when the feature is enabled.
|
|
1010
1010
|
// Paper mode uses the stop-watcher and doesn't care about these flags.
|
|
1011
|
+
//
|
|
1012
|
+
// ★ `brackets.mode` is a BINANCE-only knob — it is only ever passed to
|
|
1013
|
+
// LiveAdapter, and it defaults to 'off'. Gating solely on it meant an HL
|
|
1014
|
+
// live rig skipped the mandatory-stop check entirely: a stopless entry
|
|
1015
|
+
// passed the gate, and HL only attaches legs when stop/target metadata is
|
|
1016
|
+
// present (`wireBracketsAfterSubmit`), so the position went on the book
|
|
1017
|
+
// NAKED. Venues that always enforce brackets declare it on the adapter.
|
|
1011
1018
|
let bracketEnforcement;
|
|
1012
|
-
if (deps.adapter.isLive && bracketsEnabled(loadBracketMode())) {
|
|
1019
|
+
if (deps.adapter.isLive && (deps.adapter.bracketsAlwaysEnforced || bracketsEnabled(loadBracketMode()))) {
|
|
1013
1020
|
bracketEnforcement = wave9Claimed
|
|
1014
1021
|
? { requireStopLoss: true, requireTakeProfit: false }
|
|
1015
1022
|
: loadBracketRequirements();
|
|
1023
|
+
// On a venue-enforced adapter the stop requirement is NOT operator-
|
|
1024
|
+
// waivable: `requireStopLoss: false` in plugin-config is a Binance-era
|
|
1025
|
+
// toggle whose documented risk assumed a watcher fallback existed. HL has
|
|
1026
|
+
// none — a waved-through stopless entry would sit naked. This also keeps
|
|
1027
|
+
// the gate consistent with get_bracket_config, which reports
|
|
1028
|
+
// requireStopLoss=true for venue-enforced adapters. The TP flag stays
|
|
1029
|
+
// operator-controlled (stop-only "let winners run" is legitimate).
|
|
1030
|
+
if (deps.adapter.bracketsAlwaysEnforced) {
|
|
1031
|
+
bracketEnforcement = { ...bracketEnforcement, requireStopLoss: true };
|
|
1032
|
+
}
|
|
1016
1033
|
}
|
|
1017
1034
|
const riskCheck = preTradeRiskCheck(proposed, portfolio, getDefaultPreTradeLimits(), {
|
|
1018
1035
|
volFactor: !deps.adapter.isLive ? deps.adapter.getSimulator().getVolFactor() : 1.0,
|
|
@@ -3,9 +3,28 @@ export interface GetBracketConfigResult {
|
|
|
3
3
|
mode: BracketMode;
|
|
4
4
|
requireStopLoss: boolean;
|
|
5
5
|
requireTakeProfit: boolean;
|
|
6
|
+
/** True when the mode reported above is the VENUE's unconditional
|
|
7
|
+
* enforcement rather than the `brackets.mode` config value. Lets the
|
|
8
|
+
* dashboard explain why the toggles are inert. */
|
|
9
|
+
venueEnforced?: boolean;
|
|
6
10
|
}
|
|
7
|
-
export
|
|
11
|
+
export interface GetBracketConfigDeps {
|
|
8
12
|
configPath?: string;
|
|
9
|
-
|
|
13
|
+
/** Active adapter. Its `bracketsAlwaysEnforced` capability overrides the
|
|
14
|
+
* config-file mode — see below. */
|
|
15
|
+
adapter?: {
|
|
16
|
+
readonly bracketsAlwaysEnforced?: boolean;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* ★ `brackets.mode` in plugin-config.json is a BINANCE-only lifecycle knob: it
|
|
21
|
+
* is passed to `LiveAdapter` and nowhere else. Reporting it verbatim made a
|
|
22
|
+
* Hyperliquid rig — where `HlBracketCoordinator` attaches legs unconditionally
|
|
23
|
+
* and there is no watcher fallback — render "Brackets off" on a live dashboard
|
|
24
|
+
* whose every position was in fact bracketed. A protection indicator that
|
|
25
|
+
* under-reports is exactly as dangerous as one that over-reports, so the
|
|
26
|
+
* effective venue behaviour wins over the stale config value.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getBracketConfigTool(_args: Record<string, never>, deps?: GetBracketConfigDeps): GetBracketConfigResult | {
|
|
10
29
|
error: string;
|
|
11
30
|
};
|
|
@@ -6,13 +6,29 @@
|
|
|
6
6
|
import { readPluginConfig } from '../config/plugin-config-io.js';
|
|
7
7
|
import { getBracketMode, getBracketRequirements } from '../config/brackets-config.js';
|
|
8
8
|
import { formatError } from '../logger.js';
|
|
9
|
+
/**
|
|
10
|
+
* ★ `brackets.mode` in plugin-config.json is a BINANCE-only lifecycle knob: it
|
|
11
|
+
* is passed to `LiveAdapter` and nowhere else. Reporting it verbatim made a
|
|
12
|
+
* Hyperliquid rig — where `HlBracketCoordinator` attaches legs unconditionally
|
|
13
|
+
* and there is no watcher fallback — render "Brackets off" on a live dashboard
|
|
14
|
+
* whose every position was in fact bracketed. A protection indicator that
|
|
15
|
+
* under-reports is exactly as dangerous as one that over-reports, so the
|
|
16
|
+
* effective venue behaviour wins over the stale config value.
|
|
17
|
+
*/
|
|
9
18
|
export function getBracketConfigTool(_args, deps) {
|
|
10
19
|
try {
|
|
11
20
|
const cfg = readPluginConfig(deps?.configPath);
|
|
12
|
-
const mode = getBracketMode(cfg);
|
|
13
21
|
const req = getBracketRequirements(cfg);
|
|
22
|
+
if (deps?.adapter?.bracketsAlwaysEnforced) {
|
|
23
|
+
return {
|
|
24
|
+
mode: 'enforce',
|
|
25
|
+
requireStopLoss: true,
|
|
26
|
+
requireTakeProfit: req.requireTakeProfit,
|
|
27
|
+
venueEnforced: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
14
30
|
return {
|
|
15
|
-
mode,
|
|
31
|
+
mode: getBracketMode(cfg),
|
|
16
32
|
requireStopLoss: req.requireStopLoss,
|
|
17
33
|
requireTakeProfit: req.requireTakeProfit,
|
|
18
34
|
};
|
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
// the one-rung-at-a-time ladder. Requires valid exchange credentials for
|
|
5
5
|
// any non-PAPER target. Rebuilds the adapter via runtime.reconnect().
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// GUARDED at the dispatch site (plugin/src/index.ts registration): every call
|
|
8
|
+
// must carry the `operator_token` provenance proof and is refused by
|
|
9
|
+
// verifyOperatorProvenance without it (audit 2026-07-26 F12). The agent cannot
|
|
10
|
+
// supply that token — chat redaction strips rc_* tokens — so only the
|
|
11
|
+
// dashboard operator path reaches this handler. This module stays guard-free
|
|
12
|
+
// by design: the check lives once, at registration, for all operator tools.
|
|
10
13
|
import { readPluginConfig } from '../config/plugin-config-io.js';
|
|
11
14
|
import { validateModeTransition, modeRequiresCredentials } from '../onboarding/mode-ladder.js';
|
|
12
15
|
import { parseVenue } from '../venues/registry.js';
|
|
@@ -40,7 +40,26 @@ export function hlFillToFillEvent(fill, wiring, source) {
|
|
|
40
40
|
price === undefined || quantity === undefined || quantity <= 0 ||
|
|
41
41
|
time === undefined ||
|
|
42
42
|
(fill.side !== 'A' && fill.side !== 'B')) {
|
|
43
|
-
|
|
43
|
+
// Log the DIAGNOSIS (which identity fields failed + what shape arrived),
|
|
44
|
+
// never the raw payload — fills carry account trading data that doesn't
|
|
45
|
+
// belong in journals.
|
|
46
|
+
const bad = [];
|
|
47
|
+
if (typeof fill.tid !== 'number' || !Number.isFinite(fill.tid))
|
|
48
|
+
bad.push('tid');
|
|
49
|
+
if (typeof fill.oid !== 'number' || !Number.isFinite(fill.oid))
|
|
50
|
+
bad.push('oid');
|
|
51
|
+
if (typeof fill.coin !== 'string' || fill.coin.length === 0)
|
|
52
|
+
bad.push('coin');
|
|
53
|
+
if (price === undefined)
|
|
54
|
+
bad.push('px');
|
|
55
|
+
if (quantity === undefined || quantity <= 0)
|
|
56
|
+
bad.push('sz');
|
|
57
|
+
if (time === undefined)
|
|
58
|
+
bad.push('time');
|
|
59
|
+
if (fill.side !== 'A' && fill.side !== 'B')
|
|
60
|
+
bad.push('side');
|
|
61
|
+
logger.warn(TAG, `Dropping unmappable HL fill (source=${source}): invalid=[${bad.join(',')}] ` +
|
|
62
|
+
`keys=[${Object.keys(fill).join(',')}]`);
|
|
44
63
|
return null;
|
|
45
64
|
}
|
|
46
65
|
return {
|
|
@@ -38,6 +38,10 @@ export declare class HyperliquidLiveAdapter extends EventEmitter implements IExc
|
|
|
38
38
|
* the exchange-side legs ARE the safety floor. Lazily constructed so that
|
|
39
39
|
* merely constructing the adapter (registry tests) writes no ledger file. */
|
|
40
40
|
private _coordinator;
|
|
41
|
+
/** Venue capability (see IExchangeAdapter): brackets are unconditional here,
|
|
42
|
+
* so every consumer of `brackets.mode` must read this instead of the
|
|
43
|
+
* Binance-only flag. */
|
|
44
|
+
readonly bracketsAlwaysEnforced = true;
|
|
41
45
|
private userStream;
|
|
42
46
|
private truthCheckTimer;
|
|
43
47
|
private truthCheckRunning;
|
|
@@ -66,6 +66,10 @@ export class HyperliquidLiveAdapter extends EventEmitter {
|
|
|
66
66
|
* the exchange-side legs ARE the safety floor. Lazily constructed so that
|
|
67
67
|
* merely constructing the adapter (registry tests) writes no ledger file. */
|
|
68
68
|
_coordinator = null;
|
|
69
|
+
/** Venue capability (see IExchangeAdapter): brackets are unconditional here,
|
|
70
|
+
* so every consumer of `brackets.mode` must read this instead of the
|
|
71
|
+
* Binance-only flag. */
|
|
72
|
+
bracketsAlwaysEnforced = true;
|
|
69
73
|
userStream = null;
|
|
70
74
|
truthCheckTimer = null;
|
|
71
75
|
truthCheckRunning = false;
|
package/venues/registry.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// Venue registry — the single seam where a trading venue's LIVE adapter is
|
|
2
|
-
// constructed (
|
|
2
|
+
// constructed (docs/HYPERLIQUID_INTEGRATION_PLAN.md §5.1/§7.1).
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// boot path never grows a
|
|
4
|
+
// This build trades live on BOTH supported venues: Binance (LiveAdapter) and
|
|
5
|
+
// Hyperliquid (HyperliquidLiveAdapter, Phase 3 — brackets always enforced by
|
|
6
|
+
// HlBracketCoordinator). A venue with missing/invalid credentials falls back
|
|
7
|
+
// to PAPER at boot (never by crashing register() — OpenClaw treats a throwing
|
|
8
|
+
// register as "ignored" and the agent silently loses every tool). Any future
|
|
9
|
+
// venue adds its arm HERE and nowhere else, so the boot path never grows a
|
|
10
|
+
// second venue branch.
|
|
10
11
|
//
|
|
11
12
|
// Venue is LOCAL mechanism (TOOL_DISTRIBUTION_ARCHITECTURE.md §2 decision
|
|
12
13
|
// rule: it holds keys + is part of the safety floor) — it is read from
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
2
2
|
/** One catalog toggle represents both frozen Wave 9 strategy legs. */
|
|
3
3
|
export declare const WAVE9_BUNDLE_SETUP_TYPE = "wave9_28d_momentum_reversal";
|
|
4
|
+
/** TOOL-level close reason: what close_position args + beginExit redemption
|
|
5
|
+
* requests must carry. The `wave9_` namespace keeps it from colliding with
|
|
6
|
+
* generic CloseReason values. */
|
|
4
7
|
export declare const WAVE9_MECHANICAL_EXIT_REASON = "wave9_signal_reversal";
|
|
8
|
+
/** DECISION-level exit cause: what the authorization decision
|
|
9
|
+
* (Wave9PaperExitTokenDecision.reason) records at issuance. Deliberately a
|
|
10
|
+
* DIFFERENT string from WAVE9_MECHANICAL_EXIT_REASON — the decision names
|
|
11
|
+
* WHY the strategy exits (its only exit cause is a signal reversal), the
|
|
12
|
+
* tool reason names WHICH namespaced close path redeems it. issueExitBatch
|
|
13
|
+
* validates this constant; beginExit validates the tool constant. Two fields
|
|
14
|
+
* on two layers, each checked against its own value — not an asymmetry. */
|
|
15
|
+
export declare const WAVE9_EXIT_DECISION_REASON = "signal_reversal";
|
|
5
16
|
export type Wave9ExecutionMode = 'PAPER' | 'LIVE';
|
|
6
17
|
/** Conservative shared identity check for current, legacy, or Wave 9-mission positions. */
|
|
7
18
|
export declare function isWave9ManagedPosition(position: {
|
|
@@ -93,7 +104,7 @@ export interface Wave9PaperExitTokenDecision {
|
|
|
93
104
|
missionId: string;
|
|
94
105
|
symbol: string;
|
|
95
106
|
positionSide: 'long' | 'short';
|
|
96
|
-
reason:
|
|
107
|
+
reason: typeof WAVE9_EXIT_DECISION_REASON;
|
|
97
108
|
notBeforeMs: number;
|
|
98
109
|
deadlineMs: number;
|
|
99
110
|
positionFingerprint: string;
|
|
@@ -2,7 +2,18 @@ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
|
2
2
|
import { WAVE9_SYMBOL_PRIORITY } from '../portfolio/wave9-policy.js';
|
|
3
3
|
/** One catalog toggle represents both frozen Wave 9 strategy legs. */
|
|
4
4
|
export const WAVE9_BUNDLE_SETUP_TYPE = 'wave9_28d_momentum_reversal';
|
|
5
|
+
/** TOOL-level close reason: what close_position args + beginExit redemption
|
|
6
|
+
* requests must carry. The `wave9_` namespace keeps it from colliding with
|
|
7
|
+
* generic CloseReason values. */
|
|
5
8
|
export const WAVE9_MECHANICAL_EXIT_REASON = 'wave9_signal_reversal';
|
|
9
|
+
/** DECISION-level exit cause: what the authorization decision
|
|
10
|
+
* (Wave9PaperExitTokenDecision.reason) records at issuance. Deliberately a
|
|
11
|
+
* DIFFERENT string from WAVE9_MECHANICAL_EXIT_REASON — the decision names
|
|
12
|
+
* WHY the strategy exits (its only exit cause is a signal reversal), the
|
|
13
|
+
* tool reason names WHICH namespaced close path redeems it. issueExitBatch
|
|
14
|
+
* validates this constant; beginExit validates the tool constant. Two fields
|
|
15
|
+
* on two layers, each checked against its own value — not an asymmetry. */
|
|
16
|
+
export const WAVE9_EXIT_DECISION_REASON = 'signal_reversal';
|
|
6
17
|
/** Conservative shared identity check for current, legacy, or Wave 9-mission positions. */
|
|
7
18
|
export function isWave9ManagedPosition(position) {
|
|
8
19
|
return position.setupType === WAVE9_BUNDLE_SETUP_TYPE
|
|
@@ -615,7 +626,7 @@ export class Wave9PaperAdmissionGuard {
|
|
|
615
626
|
exactText(decision.candidateId, 'exit candidateId');
|
|
616
627
|
exactText(decision.missionId, 'missionId');
|
|
617
628
|
requireExecutionMode(decision.tradingMode);
|
|
618
|
-
if (decision.reason !==
|
|
629
|
+
if (decision.reason !== WAVE9_EXIT_DECISION_REASON)
|
|
619
630
|
throw new Wave9PaperAdmissionGuardError('Wave 9 exit reason must be signal_reversal');
|
|
620
631
|
if (!WAVE9_SYMBOL_PRIORITY.includes(decision.symbol)) {
|
|
621
632
|
throw new Wave9PaperAdmissionGuardError(`unsupported Wave 9 symbol ${decision.symbol}`);
|
package/scripts/assemble.mjs
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
// Assemble the publishable @reefclaw/openclaw-plugin package tree.
|
|
2
|
-
//
|
|
3
|
-
// This is the npm-channel distribution of the plugin — installable with ZERO
|
|
4
|
-
// terminal access via OpenClaw's owner chat command:
|
|
5
|
-
// /plugins install npm:@reefclaw/openclaw-plugin
|
|
6
|
-
// (requires commands.plugins: true; the install auto-restarts the gateway).
|
|
7
|
-
//
|
|
8
|
-
// Layout produced IN THIS DIRECTORY (package root = plugin dist root, because
|
|
9
|
-
// openclaw.extensions points at ./index.js):
|
|
10
|
-
// index.js + rest of plugin/dist — the plugin, at package root
|
|
11
|
-
// openclaw.plugin.json — manifest + `skills` declaration (npm
|
|
12
|
-
// package only — the npx-installer copy
|
|
13
|
-
// has no skills/ subdir)
|
|
14
|
-
// bridge/ — the connector (skill dist); the plugin's
|
|
15
|
-
// connector-supervisor auto-starts it when
|
|
16
|
-
// bundled (see connector-supervisor.ts)
|
|
17
|
-
// skills/reefclaw/SKILL.md — the thin bootstrap skill (full playbook
|
|
18
|
-
// arrives token-gated post-connect)
|
|
19
|
-
// Runtime deps (ccxt/ws/json5/@reefclaw/shared) are REAL npm dependencies —
|
|
20
|
-
// OpenClaw's plugin installer runs the package manager, unlike gateway boot.
|
|
21
|
-
|
|
22
|
-
import { cpSync, existsSync, rmSync, mkdirSync, readdirSync, statSync, readFileSync, writeFileSync } from 'node:fs';
|
|
23
|
-
import { join, dirname } from 'node:path';
|
|
24
|
-
import { fileURLToPath } from 'node:url';
|
|
25
|
-
|
|
26
|
-
const here = dirname(fileURLToPath(import.meta.url)); // plugin-package/scripts
|
|
27
|
-
const pkgRoot = join(here, '..');
|
|
28
|
-
const repoRoot = join(pkgRoot, '..');
|
|
29
|
-
|
|
30
|
-
function skip(name) {
|
|
31
|
-
return (
|
|
32
|
-
name === '__tests__' ||
|
|
33
|
-
name.endsWith('.test.js') ||
|
|
34
|
-
name.endsWith('.test.d.ts') ||
|
|
35
|
-
name.endsWith('.map') ||
|
|
36
|
-
name.endsWith('.tsbuildinfo')
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function copyTree(from, to) {
|
|
41
|
-
mkdirSync(to, { recursive: true });
|
|
42
|
-
for (const entry of readdirSync(from)) {
|
|
43
|
-
if (skip(entry)) continue;
|
|
44
|
-
const s = join(from, entry);
|
|
45
|
-
const d = join(to, entry);
|
|
46
|
-
if (statSync(s).isDirectory()) copyTree(s, d);
|
|
47
|
-
else cpSync(s, d);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Clean previous assembly output (everything except the package sources).
|
|
52
|
-
const KEEP = new Set(['package.json', 'scripts', 'README.md', 'node_modules', '.gitignore']);
|
|
53
|
-
for (const entry of readdirSync(pkgRoot)) {
|
|
54
|
-
if (!KEEP.has(entry)) rmSync(join(pkgRoot, entry), { recursive: true, force: true });
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
let missing = 0;
|
|
58
|
-
|
|
59
|
-
// 1. Plugin dist at package root.
|
|
60
|
-
const pluginDist = join(repoRoot, 'plugin', 'dist');
|
|
61
|
-
if (!existsSync(pluginDist)) {
|
|
62
|
-
console.error('[assemble] missing plugin/dist — build the plugin first.');
|
|
63
|
-
missing += 1;
|
|
64
|
-
} else {
|
|
65
|
-
copyTree(pluginDist, pkgRoot);
|
|
66
|
-
console.log('[assemble] plugin/dist -> package root');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// 2. Manifest, with the `skills` declaration injected (npm package only) and the
|
|
70
|
-
// release version stamped from THIS package's package.json.
|
|
71
|
-
//
|
|
72
|
-
// package.json#version is the authoritative release version — that is what
|
|
73
|
-
// ClawHub's `package-manifest-version-drift` rule compares against, and what
|
|
74
|
-
// OpenClaw's own `openclaw plugins build` writes into the manifest. The repo
|
|
75
|
-
// manifest (plugin/openclaw.plugin.json) carries a workspace-local version
|
|
76
|
-
// nobody bumps at release time, so copying it verbatim published
|
|
77
|
-
// manifest:0.1.0 against package:0.1.7 and tripped the validator. Deriving it
|
|
78
|
-
// here makes that drift structurally impossible instead of a bump to remember.
|
|
79
|
-
const manifestSrc = join(repoRoot, 'plugin', 'openclaw.plugin.json');
|
|
80
|
-
const pkgVersion = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf-8')).version;
|
|
81
|
-
if (!existsSync(manifestSrc)) {
|
|
82
|
-
console.error('[assemble] missing plugin/openclaw.plugin.json.');
|
|
83
|
-
missing += 1;
|
|
84
|
-
} else if (typeof pkgVersion !== 'string' || pkgVersion.length === 0) {
|
|
85
|
-
console.error('[assemble] package.json has no version — cannot stamp the manifest.');
|
|
86
|
-
missing += 1;
|
|
87
|
-
} else {
|
|
88
|
-
const manifest = JSON.parse(readFileSync(manifestSrc, 'utf-8'));
|
|
89
|
-
manifest.version = pkgVersion;
|
|
90
|
-
manifest.skills = ['./skills'];
|
|
91
|
-
writeFileSync(join(pkgRoot, 'openclaw.plugin.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
92
|
-
console.log(
|
|
93
|
-
`[assemble] manifest v${pkgVersion} (+skills decl), ${manifest.contracts?.tools?.length ?? 0} contract tools`,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// 3. The connector, bundled — this is what flips the supervisor's auto-start.
|
|
98
|
-
const skillDist = join(repoRoot, 'skill', 'dist');
|
|
99
|
-
if (!existsSync(skillDist)) {
|
|
100
|
-
console.error('[assemble] missing skill/dist — build the skill first.');
|
|
101
|
-
missing += 1;
|
|
102
|
-
} else {
|
|
103
|
-
copyTree(skillDist, join(pkgRoot, 'bridge'));
|
|
104
|
-
console.log('[assemble] skill/dist -> bridge/');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// 4. Bootstrap skill (config schema + connect instructions only — never the
|
|
108
|
-
// full trading playbook; that stays token-gated behind the webapp).
|
|
109
|
-
const bootstrap = join(repoRoot, 'skill', 'SKILL-bootstrap.md');
|
|
110
|
-
if (!existsSync(bootstrap)) {
|
|
111
|
-
console.error('[assemble] missing skill/SKILL-bootstrap.md.');
|
|
112
|
-
missing += 1;
|
|
113
|
-
} else {
|
|
114
|
-
const size = statSync(bootstrap).size;
|
|
115
|
-
if (size > 20_000) {
|
|
116
|
-
console.error(`[assemble] bootstrap is ${size} bytes (>20KB) — is this the FULL SKILL.md? Refusing.`);
|
|
117
|
-
missing += 1;
|
|
118
|
-
} else {
|
|
119
|
-
mkdirSync(join(pkgRoot, 'skills', 'reefclaw'), { recursive: true });
|
|
120
|
-
cpSync(bootstrap, join(pkgRoot, 'skills', 'reefclaw', 'SKILL.md'));
|
|
121
|
-
console.log(`[assemble] bootstrap skill (${size} bytes) -> skills/reefclaw/SKILL.md`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (missing > 0) {
|
|
126
|
-
console.error(`[assemble] ${missing} input(s) missing — package INCOMPLETE; do not publish.`);
|
|
127
|
-
process.exitCode = 1;
|
|
128
|
-
} else {
|
|
129
|
-
console.log('[assemble] package complete.');
|
|
130
|
-
}
|