@reefclaw/openclaw-plugin 0.1.13 → 0.1.15
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-config.d.ts +16 -5
- package/bridge/gateway/gateway-config.js +68 -12
- package/bridge/gateway/gateway-ws-client.d.ts +4 -1
- package/bridge/gateway/gateway-ws-client.js +41 -11
- package/bridge/gateway/poller.js +18 -8
- package/bridge/providers/emergency-commands.d.ts +9 -1
- package/bridge/providers/emergency-commands.js +38 -1
- package/bridge/providers/gateway.d.ts +51 -1
- package/bridge/providers/gateway.js +209 -22
- package/bridge/providers/onboarding-commands.d.ts +11 -0
- package/bridge/providers/onboarding-commands.js +5 -5
- 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/ccxt/binance-public.d.ts +17 -5
- package/ccxt/binance-public.js +31 -3
- package/config/operator-provenance.d.ts +6 -0
- package/config/operator-provenance.js +50 -0
- package/config/plugin-config-io.d.ts +15 -1
- package/config/plugin-config-io.js +29 -0
- package/exchange-adapter.d.ts +13 -0
- package/index.js +230 -176
- package/ingest/event-loop-monitor.d.ts +22 -0
- package/ingest/event-loop-monitor.js +190 -0
- package/ingest/position-auto-capture.d.ts +5 -0
- package/ingest/position-auto-capture.js +14 -5
- package/ingest/readiness-reporter.d.ts +26 -6
- package/ingest/readiness-reporter.js +137 -9
- package/ingest/skill-version-reader.d.ts +16 -0
- package/ingest/skill-version-reader.js +64 -0
- package/live/approval-lifecycle.d.ts +30 -0
- package/live/approval-lifecycle.js +80 -0
- package/live/bracket-types.d.ts +9 -0
- package/live/live-adapter.d.ts +0 -1
- package/live/user-data-stream.js +10 -2
- package/onboarding/runtime.d.ts +34 -1
- package/onboarding/runtime.js +56 -5
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -5
- package/risk/pre-trade-check.js +18 -5
- package/simulator/exchange-simulator.d.ts +45 -2
- package/simulator/exchange-simulator.js +96 -4
- package/simulator/types.d.ts +17 -0
- package/skills/reefclaw/SKILL.md +6 -11
- package/strategy/condition-registry.js +9 -2
- package/strategy/evaluator.d.ts +5 -0
- package/tools/attach-brackets.js +50 -1
- 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-bracket-coordinator.d.ts +25 -1
- package/venues/hyperliquid/hl-bracket-coordinator.js +57 -0
- package/venues/hyperliquid/hl-brackets.d.ts +10 -0
- package/venues/hyperliquid/hl-brackets.js +45 -13
- package/venues/hyperliquid/hl-fill-ingest.d.ts +18 -0
- package/venues/hyperliquid/hl-fill-ingest.js +88 -0
- package/venues/hyperliquid/hl-live-adapter.d.ts +36 -0
- package/venues/hyperliquid/hl-live-adapter.js +116 -7
- package/venues/hyperliquid/hl-public.d.ts +12 -5
- package/venues/hyperliquid/hl-public.js +24 -3
- package/venues/hyperliquid/hl-user-stream.d.ts +13 -1
- package/venues/hyperliquid/hl-user-stream.js +4 -1
- 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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Approval-listener lifecycle — follows adapter swaps instead of boot state
|
|
2
|
+
// (audit 2026-07-26 F10).
|
|
3
|
+
//
|
|
4
|
+
// The ProposalDecisionListener used to start ONCE at boot, only when the
|
|
5
|
+
// process booted live with approval.mode='per_trade'. Every real trader
|
|
6
|
+
// onboards in PAPER and goes live later from the dashboard, so on their box
|
|
7
|
+
// the listener never existed — approvals would sit unfired. Worse, the
|
|
8
|
+
// listener held the adapter it was CONSTRUCTED with: a live→PAPER flip left
|
|
9
|
+
// it wired to the orphaned live adapter (an operator approval would fire a
|
|
10
|
+
// real exchange order while the dashboard said PAPER), and a credential swap
|
|
11
|
+
// left it firing through the stopped old adapter.
|
|
12
|
+
//
|
|
13
|
+
// This class owns exactly one listener at a time and is driven by
|
|
14
|
+
// PluginRuntime.onAdapterSwapped (boot calls it once with the boot adapter):
|
|
15
|
+
// - live adapter + approval.mode='per_trade' (re-read at swap time) → a
|
|
16
|
+
// fresh listener bound to THAT adapter;
|
|
17
|
+
// - anything else → no listener (stopping any previous one first, awaiting
|
|
18
|
+
// its in-flight tick so a fire's result PATCH lands before teardown).
|
|
19
|
+
//
|
|
20
|
+
// Swaps are serialized through a promise chain: a second swap arriving while
|
|
21
|
+
// the first is still draining queues behind it, so two listeners can never
|
|
22
|
+
// run concurrently (the DB fire-claim makes a double-fire impossible anyway —
|
|
23
|
+
// this keeps the process tidy, not the money safe).
|
|
24
|
+
import { logger, formatError } from '../logger.js';
|
|
25
|
+
const TAG = 'approval-lifecycle';
|
|
26
|
+
export class ApprovalListenerLifecycle {
|
|
27
|
+
deps;
|
|
28
|
+
listener;
|
|
29
|
+
chain = Promise.resolve();
|
|
30
|
+
constructor(deps) {
|
|
31
|
+
this.deps = deps;
|
|
32
|
+
}
|
|
33
|
+
get active() {
|
|
34
|
+
return this.listener !== undefined;
|
|
35
|
+
}
|
|
36
|
+
/** Apply the lifecycle for a freshly published adapter. Serialized. */
|
|
37
|
+
onAdapterSwapped(adapter) {
|
|
38
|
+
this.chain = this.chain
|
|
39
|
+
.then(() => this.apply(adapter))
|
|
40
|
+
.catch((err) => {
|
|
41
|
+
logger.error(TAG, `listener swap failed: ${formatError(err)}`);
|
|
42
|
+
});
|
|
43
|
+
return this.chain;
|
|
44
|
+
}
|
|
45
|
+
/** Terminal stop (shutdown drain) — also serialized behind pending swaps. */
|
|
46
|
+
stop() {
|
|
47
|
+
this.chain = this.chain
|
|
48
|
+
.then(() => this.stopCurrent())
|
|
49
|
+
.catch((err) => {
|
|
50
|
+
logger.warn(TAG, `listener stop failed: ${formatError(err)}`);
|
|
51
|
+
});
|
|
52
|
+
return this.chain;
|
|
53
|
+
}
|
|
54
|
+
async apply(adapter) {
|
|
55
|
+
// Always tear down the previous listener first — it is bound to the OLD
|
|
56
|
+
// adapter and must never fire through it again.
|
|
57
|
+
await this.stopCurrent();
|
|
58
|
+
if (!adapter.isLive)
|
|
59
|
+
return;
|
|
60
|
+
const mode = this.deps.resolveApprovalMode();
|
|
61
|
+
if (mode !== 'per_trade')
|
|
62
|
+
return;
|
|
63
|
+
if (!this.deps.hasWiring()) {
|
|
64
|
+
logger.warn(TAG, "approval.mode='per_trade' but ingest credentials/proposal manager missing — listener NOT started; approvals will not fire");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const fresh = this.deps.buildListener(adapter);
|
|
68
|
+
fresh.start();
|
|
69
|
+
this.listener = fresh;
|
|
70
|
+
logger.info(TAG, 'ProposalDecisionListener started (bound to the current adapter)');
|
|
71
|
+
}
|
|
72
|
+
async stopCurrent() {
|
|
73
|
+
if (!this.listener)
|
|
74
|
+
return;
|
|
75
|
+
const old = this.listener;
|
|
76
|
+
this.listener = undefined;
|
|
77
|
+
await old.stop();
|
|
78
|
+
logger.info(TAG, 'ProposalDecisionListener stopped');
|
|
79
|
+
}
|
|
80
|
+
}
|
package/live/bracket-types.d.ts
CHANGED
|
@@ -20,6 +20,15 @@ export interface BracketLedgerEntry {
|
|
|
20
20
|
symbol: string;
|
|
21
21
|
entrySide: 'buy' | 'sell';
|
|
22
22
|
entryCid: string;
|
|
23
|
+
/** Additional entry clientOrderIds submitted against this SAME bracket row —
|
|
24
|
+
* a scale-in, or a second entry placed while the first is still resting.
|
|
25
|
+
* Each order gets a fresh cloid but the row keeps the original `entryCid`,
|
|
26
|
+
* so a fill matcher keyed on `entryCid` alone would ignore their fills
|
|
27
|
+
* entirely (audit 2026-07-26 F5). HL-only today: Binance legs are
|
|
28
|
+
* `closePosition:true` (whole-position, self-resizing), while HL legs are
|
|
29
|
+
* FIXED SIZE — an unmatched scale-in fill there means NAKED contracts.
|
|
30
|
+
* Bounded; oldest dropped. */
|
|
31
|
+
extraEntryCids?: string[];
|
|
23
32
|
slCid?: string;
|
|
24
33
|
tpCid?: string;
|
|
25
34
|
stopPrice?: number;
|
package/live/live-adapter.d.ts
CHANGED
|
@@ -85,7 +85,6 @@ export declare class LiveAdapter extends EventEmitter implements IExchangeAdapte
|
|
|
85
85
|
/** Session-start NAV, captured once at initialization. Used for drawdown calculation. */
|
|
86
86
|
getSessionStartNav(): number | null;
|
|
87
87
|
constructor(config: ExchangeConfig, mode: 'MICRO_LIVE' | 'LIVE', microLiveConfig?: {
|
|
88
|
-
sizeCapPercent?: number;
|
|
89
88
|
maxPositionUSDT?: number;
|
|
90
89
|
}, bracketMode?: BracketMode, userDataStreamMode?: UserDataStreamMode, userDataStreamTunables?: UserDataStreamTunables,
|
|
91
90
|
/** TRADE_AUDIT_TRAIL_PLAN Phase 1 — optional audit-trail wiring. Wired
|
package/live/user-data-stream.js
CHANGED
|
@@ -297,8 +297,16 @@ export class UserDataStream extends EventEmitter {
|
|
|
297
297
|
// Capped to avoid flooding the journal under heavy bracket
|
|
298
298
|
// traffic. Operators read these first-N samples once after
|
|
299
299
|
// deploy to verify the parser matches Binance's actual shape.
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
// Raw account payloads (order ids, prices, quantities) only
|
|
301
|
+
// reach the journal behind an explicit opt-in — by default we
|
|
302
|
+
// log the parser-relevant SHAPE (field names), which is what
|
|
303
|
+
// shape-drift verification actually needs.
|
|
304
|
+
const raw = process.env.RC_LOG_RAW_WS === 'on';
|
|
305
|
+
const shape = raw
|
|
306
|
+
? JSON.stringify(obj)
|
|
307
|
+
: `keys=[${Object.keys(obj).join(',')}] event.keys=[${Object.keys(obj.o ?? {}).join(',')}]`;
|
|
308
|
+
logger.info(TAG, `ALGO_UPDATE ${raw ? 'raw' : 'shape'} (${this.algoUpdateRawLogged + 1}/` +
|
|
309
|
+
`${UserDataStream.ALGO_UPDATE_RAW_LOG_CAP}): ${shape}`);
|
|
302
310
|
this.algoUpdateRawLogged++;
|
|
303
311
|
}
|
|
304
312
|
this.emit('algoUpdate', ev);
|
package/onboarding/runtime.d.ts
CHANGED
|
@@ -3,14 +3,31 @@ import type { ExchangeConfig, TradingMode } from '../types.js';
|
|
|
3
3
|
import type { ExchangeSimulator } from '../simulator/exchange-simulator.js';
|
|
4
4
|
import type { PaperMarketFeed } from '../simulator/paper-market-feed.js';
|
|
5
5
|
import { LiveAdapter } from '../live/live-adapter.js';
|
|
6
|
+
import { HyperliquidLiveAdapter } from '../venues/hyperliquid/hl-live-adapter.js';
|
|
6
7
|
import type { HlCredentials } from '../venues/hyperliquid/hl-private.js';
|
|
7
8
|
import { type VenueId } from '../venues/registry.js';
|
|
8
9
|
import { PositionWatcher } from '../live/stop-watcher.js';
|
|
10
|
+
import type { TradeStoreClient } from '../ingest/trade-store-client.js';
|
|
11
|
+
import type { AutoCaptureContext } from '../ingest/position-auto-capture.js';
|
|
9
12
|
import type { TradingOperationLock } from '../lifecycle/trading-operation-lock.js';
|
|
10
13
|
export interface MicroLiveConfig {
|
|
11
|
-
sizeCapPercent?: number;
|
|
12
14
|
maxPositionUSDT?: number;
|
|
13
15
|
}
|
|
16
|
+
/** Boot-constructed live wiring reapplied to EVERY adapter build (audit
|
|
17
|
+
* 2026-07-26 F9). Reconnects previously rebuilt adapters with only the
|
|
18
|
+
* bracket mode — silently reverting WS authority to REST, dropping the
|
|
19
|
+
* audit-trail ingest, and losing journal auto-capture until restart. The
|
|
20
|
+
* clients live for the process lifetime (the SIGTERM drain holds them), so
|
|
21
|
+
* rebuilds must REUSE them, never re-instantiate. */
|
|
22
|
+
export interface LiveAdapterWiring {
|
|
23
|
+
/** Base of TradeIngestWiring — `exchange` is stamped per build from the
|
|
24
|
+
* TARGET venue (fillExchangeId), because a venue flip changes it. */
|
|
25
|
+
tradeIngestBase?: {
|
|
26
|
+
client: TradeStoreClient;
|
|
27
|
+
userId: string;
|
|
28
|
+
};
|
|
29
|
+
autoCapture?: AutoCaptureContext;
|
|
30
|
+
}
|
|
14
31
|
export interface BuildAdapterInput {
|
|
15
32
|
mode: TradingMode;
|
|
16
33
|
exchange: ExchangeConfig | null;
|
|
@@ -24,6 +41,8 @@ export interface BuildAdapterInput {
|
|
|
24
41
|
venue?: VenueId;
|
|
25
42
|
/** Required for a live-mode build on the hyperliquid venue. */
|
|
26
43
|
hlCredentials?: HlCredentials | null;
|
|
44
|
+
/** Boot wiring reapplied on every build — see LiveAdapterWiring (F9). */
|
|
45
|
+
wiring?: LiveAdapterWiring;
|
|
27
46
|
}
|
|
28
47
|
/** Wave 9 safety wiring is created only after its durable ledger is loaded.
|
|
29
48
|
* Runtime reapplies these hooks to both the bootstrap objects and every
|
|
@@ -62,6 +81,17 @@ export declare class PluginRuntime {
|
|
|
62
81
|
* for watcher closes (issue #199) — without it, a live<->paper reconnect
|
|
63
82
|
* would silently shed the capture wiring. */
|
|
64
83
|
private readonly onWatcherCreated?;
|
|
84
|
+
/** Observer applied to EVERY live adapter this runtime creates (audit F9,
|
|
85
|
+
* same pattern as onWatcherCreated). index.ts uses it to install the
|
|
86
|
+
* drift_detected → journal close-bypass cleanup listener — previously
|
|
87
|
+
* installed only on the BOOT adapter and lost on every reconnect. */
|
|
88
|
+
private readonly onAdapterCreated?;
|
|
89
|
+
/** Boot live wiring threaded into every buildAdapter call (audit F9). */
|
|
90
|
+
private readonly liveWiring?;
|
|
91
|
+
/** Fired after EVERY reconnect publishes its adapter (paper ones included) —
|
|
92
|
+
* set post-construction because its consumer (the approval-listener
|
|
93
|
+
* lifecycle, audit F10) is built after the runtime. */
|
|
94
|
+
private onAdapterSwapped?;
|
|
65
95
|
/** Reconnect is serialized — a second caller waits for the first to finish
|
|
66
96
|
* so we never tear down an adapter that's mid-rebuild. */
|
|
67
97
|
private reconnectInFlight;
|
|
@@ -73,11 +103,14 @@ export declare class PluginRuntime {
|
|
|
73
103
|
marketFeed?: PaperMarketFeed | null;
|
|
74
104
|
operationLock?: TradingOperationLock;
|
|
75
105
|
onWatcherCreated?: (watcher: PositionWatcher) => void;
|
|
106
|
+
onAdapterCreated?: (adapter: LiveAdapter | HyperliquidLiveAdapter) => void;
|
|
107
|
+
liveWiring?: LiveAdapterWiring;
|
|
76
108
|
});
|
|
77
109
|
get adapter(): IExchangeAdapter;
|
|
78
110
|
get mode(): TradingMode;
|
|
79
111
|
get stopWatcher(): PositionWatcher | null;
|
|
80
112
|
get marketFeed(): PaperMarketFeed | null;
|
|
113
|
+
setOnAdapterSwapped(cb?: (adapter: IExchangeAdapter) => void): void;
|
|
81
114
|
setWave9LiveLifecycleHooks(hooks?: Wave9LiveLifecycleHooks): void;
|
|
82
115
|
/**
|
|
83
116
|
* Swap the current adapter for a new one built from `next`. The old
|
package/onboarding/runtime.js
CHANGED
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
import { PaperAdapter } from '../paper-adapter.js';
|
|
15
15
|
import { LiveAdapter } from '../live/live-adapter.js';
|
|
16
16
|
import { HyperliquidLiveAdapter } from '../venues/hyperliquid/hl-live-adapter.js';
|
|
17
|
-
import { createLiveAdapter } from '../venues/registry.js';
|
|
17
|
+
import { createLiveAdapter, fillExchangeId } from '../venues/registry.js';
|
|
18
18
|
import { PositionWatcher } from '../live/stop-watcher.js';
|
|
19
19
|
import { loadBracketMode } from '../config/brackets-config.js';
|
|
20
|
-
import { loadStopWatcherIntervalMs, readPluginConfig } from '../config/plugin-config-io.js';
|
|
20
|
+
import { loadMicroLiveConfig, loadStopWatcherIntervalMs, readPluginConfig } from '../config/plugin-config-io.js';
|
|
21
|
+
import { loadUserDataStreamMode, loadUserDataStreamTunables } from '../config/user-data-stream-config.js';
|
|
21
22
|
import { logger, formatError } from '../logger.js';
|
|
22
23
|
const TAG = 'plugin-runtime';
|
|
23
24
|
/** Pure-ish factory: builds an adapter for the requested mode.
|
|
@@ -25,12 +26,27 @@ const TAG = 'plugin-runtime';
|
|
|
25
26
|
* callers should pre-validate via `modeRequiresCredentials`, but this
|
|
26
27
|
* defense-in-depth prevents a crash if validation is bypassed. */
|
|
27
28
|
export function buildAdapter(input) {
|
|
28
|
-
const { mode, exchange,
|
|
29
|
+
const { mode, exchange, simulator } = input;
|
|
29
30
|
if (mode === 'PAPER' || mode === 'SHADOW') {
|
|
30
31
|
// Shadow mode uses a paper adapter for execution; the separate
|
|
31
32
|
// ShadowTracker wraps a BinancePrivateApi for real-balance comparison.
|
|
32
33
|
return new PaperAdapter(simulator);
|
|
33
34
|
}
|
|
35
|
+
// Micro-live cap resolves from plugin-config at adapter build time when the
|
|
36
|
+
// caller didn't pass one — every reconnect path (mode flip, credential save)
|
|
37
|
+
// used to omit it, silently resetting an operator-raised OR -lowered cap
|
|
38
|
+
// back to the $50 default (audit 2026-07-26 F8).
|
|
39
|
+
const microLive = input.microLive ?? loadMicroLiveConfig();
|
|
40
|
+
// Audit-trail ingest (F9): reuse the boot-constructed client, stamp the
|
|
41
|
+
// exchange id from the TARGET venue (a venue flip changes it; the id is
|
|
42
|
+
// half of the trades idempotency key and must never be stale or minted).
|
|
43
|
+
const tradeIngest = input.wiring?.tradeIngestBase
|
|
44
|
+
? {
|
|
45
|
+
client: input.wiring.tradeIngestBase.client,
|
|
46
|
+
userId: input.wiring.tradeIngestBase.userId,
|
|
47
|
+
exchange: fillExchangeId(input.venue ?? 'binance'),
|
|
48
|
+
}
|
|
49
|
+
: undefined;
|
|
34
50
|
// Hyperliquid live (issue #217): mirror the boot path's construction —
|
|
35
51
|
// per-venue credential shape, the SAME factory (createLiveAdapter), and
|
|
36
52
|
// the same fall-back-to-paper defense when credentials are absent.
|
|
@@ -45,6 +61,8 @@ export function buildAdapter(input) {
|
|
|
45
61
|
credentials: input.hlCredentials,
|
|
46
62
|
mode: mode,
|
|
47
63
|
marketSlippagePct: readPluginConfig().hl?.marketSlippagePct,
|
|
64
|
+
microLive,
|
|
65
|
+
tradeIngest,
|
|
48
66
|
},
|
|
49
67
|
});
|
|
50
68
|
}
|
|
@@ -55,7 +73,12 @@ export function buildAdapter(input) {
|
|
|
55
73
|
// Read bracket-mode from plugin-config at adapter build time so a config
|
|
56
74
|
// flip + "Reconnect Exchange" pattern picks up the new mode on the next swap.
|
|
57
75
|
const bracketMode = loadBracketMode();
|
|
58
|
-
|
|
76
|
+
// Same read-at-build-time rule for the user-data stream (F9): a rebuilt
|
|
77
|
+
// adapter wires its WS at construction, so passing nothing here reverted
|
|
78
|
+
// prod's `enforce` to REST-only polling on every dashboard reconnect.
|
|
79
|
+
const userDataStreamMode = loadUserDataStreamMode();
|
|
80
|
+
const userDataStreamTunables = loadUserDataStreamTunables();
|
|
81
|
+
return new LiveAdapter(exchange, mode, microLive, bracketMode, userDataStreamMode, userDataStreamTunables, tradeIngest, input.wiring?.autoCapture);
|
|
59
82
|
}
|
|
60
83
|
/**
|
|
61
84
|
* Mutable runtime holder. Held once per plugin registration.
|
|
@@ -82,6 +105,17 @@ export class PluginRuntime {
|
|
|
82
105
|
* for watcher closes (issue #199) — without it, a live<->paper reconnect
|
|
83
106
|
* would silently shed the capture wiring. */
|
|
84
107
|
onWatcherCreated;
|
|
108
|
+
/** Observer applied to EVERY live adapter this runtime creates (audit F9,
|
|
109
|
+
* same pattern as onWatcherCreated). index.ts uses it to install the
|
|
110
|
+
* drift_detected → journal close-bypass cleanup listener — previously
|
|
111
|
+
* installed only on the BOOT adapter and lost on every reconnect. */
|
|
112
|
+
onAdapterCreated;
|
|
113
|
+
/** Boot live wiring threaded into every buildAdapter call (audit F9). */
|
|
114
|
+
liveWiring;
|
|
115
|
+
/** Fired after EVERY reconnect publishes its adapter (paper ones included) —
|
|
116
|
+
* set post-construction because its consumer (the approval-listener
|
|
117
|
+
* lifecycle, audit F10) is built after the runtime. */
|
|
118
|
+
onAdapterSwapped;
|
|
85
119
|
/** Reconnect is serialized — a second caller waits for the first to finish
|
|
86
120
|
* so we never tear down an adapter that's mid-rebuild. */
|
|
87
121
|
reconnectInFlight = null;
|
|
@@ -93,11 +127,16 @@ export class PluginRuntime {
|
|
|
93
127
|
this._marketFeed = initial.marketFeed ?? null;
|
|
94
128
|
this.operationLock = initial.operationLock;
|
|
95
129
|
this.onWatcherCreated = initial.onWatcherCreated;
|
|
130
|
+
this.onAdapterCreated = initial.onAdapterCreated;
|
|
131
|
+
this.liveWiring = initial.liveWiring;
|
|
96
132
|
}
|
|
97
133
|
get adapter() { return this._adapter; }
|
|
98
134
|
get mode() { return this._mode; }
|
|
99
135
|
get stopWatcher() { return this._stopWatcher; }
|
|
100
136
|
get marketFeed() { return this._marketFeed; }
|
|
137
|
+
setOnAdapterSwapped(cb) {
|
|
138
|
+
this.onAdapterSwapped = cb;
|
|
139
|
+
}
|
|
101
140
|
setWave9LiveLifecycleHooks(hooks) {
|
|
102
141
|
this.wave9LiveLifecycleHooks = hooks;
|
|
103
142
|
if (this._adapter instanceof LiveAdapter) {
|
|
@@ -160,7 +199,9 @@ export class PluginRuntime {
|
|
|
160
199
|
logger.warn(TAG, `old HL adapter stop failed: ${formatError(err)}`);
|
|
161
200
|
}
|
|
162
201
|
}
|
|
163
|
-
// 3. Build the new adapter
|
|
202
|
+
// 3. Build the new adapter — with the boot live wiring, so a reconnect
|
|
203
|
+
// can never silently shed WS authority / audit ingest / auto-capture
|
|
204
|
+
// (audit F9).
|
|
164
205
|
const fresh = buildAdapter({
|
|
165
206
|
mode: next.mode,
|
|
166
207
|
exchange: next.exchange,
|
|
@@ -168,12 +209,18 @@ export class PluginRuntime {
|
|
|
168
209
|
simulator: this.simulator,
|
|
169
210
|
venue: next.venue,
|
|
170
211
|
hlCredentials: next.hlCredentials,
|
|
212
|
+
wiring: this.liveWiring,
|
|
171
213
|
});
|
|
172
214
|
// Install autonomous protection callbacks before initialization can emit
|
|
173
215
|
// user-data or bracket-reconciler events.
|
|
174
216
|
if (fresh instanceof LiveAdapter) {
|
|
175
217
|
this.wave9LiveLifecycleHooks?.configureLiveAdapter?.(fresh);
|
|
176
218
|
}
|
|
219
|
+
// Same before-init rule for the drift_detected journal cleanup (F9): the
|
|
220
|
+
// reconciler can emit on its first poll.
|
|
221
|
+
if (fresh instanceof LiveAdapter || fresh instanceof HyperliquidLiveAdapter) {
|
|
222
|
+
this.onAdapterCreated?.(fresh);
|
|
223
|
+
}
|
|
177
224
|
// 4. Fire async init for live adapters (non-blocking — readiness flips
|
|
178
225
|
// INIT_PENDING → READY/DEGRADED/BLOCKED on its own).
|
|
179
226
|
if (fresh instanceof LiveAdapter || fresh instanceof HyperliquidLiveAdapter) {
|
|
@@ -210,6 +257,10 @@ export class PluginRuntime {
|
|
|
210
257
|
this._marketFeed.start();
|
|
211
258
|
}
|
|
212
259
|
}
|
|
260
|
+
// 8. Notify swap observers (approval-listener lifecycle etc. — F10).
|
|
261
|
+
// Fired for EVERY swap including paper, so a live→PAPER flip can tear
|
|
262
|
+
// down consumers bound to the orphaned live adapter.
|
|
263
|
+
this.onAdapterSwapped?.(fresh);
|
|
213
264
|
logger.info(TAG, `Reconnect complete: now in ${this._mode} mode (readiness=${fresh.readiness})`);
|
|
214
265
|
}
|
|
215
266
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "reefclaw-paper-trading",
|
|
3
3
|
"name": "ReefClaw Trading",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.15",
|
|
5
5
|
"description": "Supervised trading plugin for the ReefClaw dashboard: paper trading with real market data (no API keys required), and optional live trading on Binance or Hyperliquid behind explicit operator opt-in, exchange API credentials, and always-on protective stop brackets. Includes the dashboard connector bridge, heartbeat automation, and remote SKILL.md instruction updates from the ReefClaw webapp.",
|
|
6
6
|
"author": "ReefClaw",
|
|
7
7
|
"activation": {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reefclaw/openclaw-plugin",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "ReefClaw supervised trading plugin for OpenClaw
|
|
3
|
+
"version": "0.1.15",
|
|
4
|
+
"description": "ReefClaw supervised trading plugin for OpenClaw — paper trading with real market data, optional live trading on Binance or Hyperliquid (operator opt-in, API keys, always-on protective brackets), plus the ReefClaw dashboard connector with heartbeat automation and remote SKILL.md updates from the ReefClaw webapp. Install: /plugins install clawhub:@reefclaw/openclaw-plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"openclaw": {
|
|
@@ -16,16 +16,17 @@
|
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
|
-
"**/*"
|
|
19
|
+
"**/*",
|
|
20
|
+
"!scripts/**"
|
|
20
21
|
],
|
|
21
22
|
"engines": {
|
|
22
23
|
"node": ">=20"
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
25
|
-
"@reefclaw/shared": "0.1.
|
|
26
|
+
"@reefclaw/shared": "0.1.3",
|
|
26
27
|
"ccxt": "4.5.37",
|
|
27
28
|
"json5": "2.2.3",
|
|
28
|
-
"ws": "8.
|
|
29
|
+
"ws": "8.21.1"
|
|
29
30
|
},
|
|
30
31
|
"scripts": {
|
|
31
32
|
"build": "node scripts/assemble.mjs"
|
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({
|
|
@@ -4,6 +4,9 @@ import type { CcxtOrder, CcxtBalance, CcxtPosition, CcxtTicker } from '../types.
|
|
|
4
4
|
export declare class ExchangeSimulator extends EventEmitter {
|
|
5
5
|
private state;
|
|
6
6
|
private lastTicker;
|
|
7
|
+
/** Symbols with a take-profit close in flight — suppresses a re-entrant
|
|
8
|
+
* tick firing a second close on the same position. */
|
|
9
|
+
private takeProfitPending;
|
|
7
10
|
private lastOrderBook;
|
|
8
11
|
private simulationConfig;
|
|
9
12
|
/** Metadata for pending limit orders, keyed by order ID. Cleaned up on fill/cancel. */
|
|
@@ -46,10 +49,21 @@ export declare class ExchangeSimulator extends EventEmitter {
|
|
|
46
49
|
/** Cache the latest order book snapshot for a symbol. */
|
|
47
50
|
updateOrderBook(symbol: string, orderbook: OrderBookDepth): void;
|
|
48
51
|
getLastOrderBook(symbol: string): OrderBookDepth | undefined;
|
|
49
|
-
createOrder(symbol: string, side: 'buy' | 'sell', type: 'market' | 'limit', amount: number, price?: number, metadata?: PositionMetadata
|
|
52
|
+
createOrder(symbol: string, side: 'buy' | 'sell', type: 'market' | 'limit', amount: number, price?: number, metadata?: PositionMetadata,
|
|
53
|
+
/** Paper-only market-fill price override (take-profit leg). When set, the
|
|
54
|
+
* market branch prices off THIS instead of the last tick, and skips the
|
|
55
|
+
* stale-quote guard — the caller supplied the price, so quote age is
|
|
56
|
+
* irrelevant, and a protective exit must never be blocked (issue #202). */
|
|
57
|
+
referencePrice?: number): CcxtOrder;
|
|
50
58
|
cancelOrder(orderId: string): CcxtOrder;
|
|
51
59
|
cancelAllOrders(symbol?: string): CcxtOrder[];
|
|
52
|
-
|
|
60
|
+
/**
|
|
61
|
+
* @param referencePrice Paper-only fill-price override. Used by the
|
|
62
|
+
* take-profit leg to fill AT the target level instead of the (possibly
|
|
63
|
+
* gapped-past) tick price — see `checkTakeProfitLegs`. Omitted everywhere
|
|
64
|
+
* else, which keeps the normal market-close path byte-identical.
|
|
65
|
+
*/
|
|
66
|
+
closePosition(symbol: string, closeReason?: CloseReason, referencePrice?: number): CcxtOrder;
|
|
53
67
|
/** Paper-only: move an open position's MUTABLE protective levels (stopPrice /
|
|
54
68
|
* targetPrice) in place and persist, WITHOUT the close+reopen round-trip
|
|
55
69
|
* (which pays an extra taker fee and resets the R/MFE denominators). The
|
|
@@ -63,6 +77,35 @@ export declare class ExchangeSimulator extends EventEmitter {
|
|
|
63
77
|
}): void;
|
|
64
78
|
updateTicker(ticker: CcxtTicker): void;
|
|
65
79
|
getLastTicker(symbol: string): CcxtTicker | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Take-profit legs — the paper analog of the exchange-native
|
|
82
|
+
* `TAKE_PROFIT_MARKET` order live attaches at entry.
|
|
83
|
+
*
|
|
84
|
+
* ★ Why this exists: paper STORED `metadata.targetPrice` and surfaced it
|
|
85
|
+
* (chart line, positions table) but nothing ever closed on it, so the TP was
|
|
86
|
+
* a drawing rather than an order. Every paper winner ran straight past its
|
|
87
|
+
* exit — observed live 2026-07-27 on an OP/USDT short that reached +2.07R
|
|
88
|
+
* against a 1.0R target. That made paper the odd one out of three: the
|
|
89
|
+
* BACKTEST exits at target (`backtest/engine.ts` exitReason 'target') and
|
|
90
|
+
* LIVE exits at target (Binance TP_MARKET leg / HL coordinator TP leg), so
|
|
91
|
+
* a strategy forward-validated on paper was being measured on a book that
|
|
92
|
+
* let every winner run.
|
|
93
|
+
*
|
|
94
|
+
* Fill convention: the TARGET LEVEL is the decision price, and the normal
|
|
95
|
+
* realistic-fill engine applies its own adverse slippage around it (book
|
|
96
|
+
* VWAP + vol factor + taker fee) — the same model every other paper fill
|
|
97
|
+
* uses, rather than a second hand-rolled slippage constant. When a tick gaps
|
|
98
|
+
* past the target we deliberately do NOT credit the gap: filling at the
|
|
99
|
+
* level is worse for us than filling at the gapped tick, so this stays
|
|
100
|
+
* conservative against both the backtest and a real TP_MARKET (which would
|
|
101
|
+
* fill at the gapped price).
|
|
102
|
+
*
|
|
103
|
+
* Stops stay with the PositionWatcher: it is the safety floor and re-homing
|
|
104
|
+
* it is a separate, riskier change. A single tick can only breach one leg
|
|
105
|
+
* (stop and target sit on opposite sides of entry), so there is no
|
|
106
|
+
* stop-vs-target ordering ambiguity to resolve here.
|
|
107
|
+
*/
|
|
108
|
+
private checkTakeProfitLegs;
|
|
66
109
|
/** Walk every position for `symbol` and refresh MFE / give-back from the
|
|
67
110
|
* latest mark. Idempotent — pure update of `metadata.mfePeakPrice` (only
|
|
68
111
|
* ratchets favourably) plus derived `mfeR` and `giveBackRatio`. Safe to
|