@reefclaw/openclaw-plugin 0.1.5 → 0.1.7
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/gateway/event-parser.d.ts +6 -1
- package/bridge/gateway/event-parser.js +19 -2
- package/bridge/gateway/poller.d.ts +1 -0
- package/bridge/gateway/poller.js +14 -2
- package/bridge/providers/gateway.d.ts +22 -2
- package/bridge/providers/gateway.js +67 -9
- package/ccxt/binance-private.d.ts +21 -0
- package/ccxt/binance-private.js +132 -22
- package/ccxt/public-market-data-api.d.ts +14 -0
- package/ccxt/public-market-data-api.js +15 -1
- package/config/plugin-config-io.d.ts +13 -0
- package/config/plugin-config-io.js +15 -0
- package/config/tool-gate.js +3 -0
- package/exchange-adapter.d.ts +16 -0
- package/index.js +658 -83
- package/ingest/position-auto-capture.d.ts +68 -0
- package/ingest/position-auto-capture.js +321 -23
- package/ingest/position-decisions-client.d.ts +7 -2
- package/ingest/position-decisions-client.js +13 -3
- package/ingest/reconcile-db-vs-exchange.d.ts +39 -1
- package/ingest/reconcile-db-vs-exchange.js +66 -10
- package/lifecycle/trading-operation-lock.d.ts +17 -0
- package/lifecycle/trading-operation-lock.js +14 -0
- package/live/bracket-id.d.ts +2 -3
- package/live/bracket-id.js +22 -9
- package/live/fill-price.d.ts +13 -0
- package/live/fill-price.js +37 -0
- package/live/live-adapter.d.ts +57 -2
- package/live/live-adapter.js +290 -49
- package/live/local-signal-service.js +11 -6
- package/live/local-strategy-evaluator.js +4 -0
- package/live/position-state-store.d.ts +4 -0
- package/live/proposal-decision-listener.d.ts +6 -0
- package/live/proposal-decision-listener.js +4 -0
- package/live/stop-watcher.d.ts +35 -2
- package/live/stop-watcher.js +63 -3
- package/onboarding/runtime.d.ts +19 -0
- package/onboarding/runtime.js +34 -3
- package/openclaw.plugin.json +1 -0
- package/package.json +2 -2
- package/portfolio/reentry-tracker.d.ts +36 -0
- package/portfolio/reentry-tracker.js +127 -0
- package/portfolio/wave9-admission.d.ts +67 -0
- package/portfolio/wave9-admission.js +262 -0
- package/portfolio/wave9-policy.d.ts +36 -0
- package/portfolio/wave9-policy.js +183 -0
- package/signals/conditions/registry.js +61 -2
- package/signals/strategy-adapter.js +17 -7
- package/simulator/exchange-simulator.d.ts +12 -0
- package/simulator/exchange-simulator.js +78 -3
- package/simulator/fill-engine.js +5 -1
- package/simulator/types.d.ts +10 -1
- package/skills/reefclaw/SKILL.md +2 -0
- package/strategy/evaluator.d.ts +3 -0
- package/strategy/evaluator.js +5 -0
- package/tools/assessment-validation.d.ts +23 -0
- package/tools/assessment-validation.js +58 -0
- package/tools/attach-brackets.d.ts +7 -2
- package/tools/attach-brackets.js +201 -0
- package/tools/audit-bracket-protection.js +157 -1
- package/tools/bracket-control.d.ts +12 -0
- package/tools/bracket-control.js +35 -0
- package/tools/cancel-all-orders.d.ts +2 -0
- package/tools/cancel-all-orders.js +4 -1
- package/tools/cancel-order.d.ts +4 -0
- package/tools/cancel-order.js +49 -3
- package/tools/close-position.d.ts +23 -0
- package/tools/close-position.js +286 -13
- package/tools/create-order.d.ts +28 -0
- package/tools/create-order.js +1390 -190
- package/tools/get-analytics.js +2 -2
- package/tools/get-basis.js +2 -2
- package/tools/get-cascade-risk.js +2 -2
- package/tools/get-crypto-metrics.js +14 -4
- package/tools/get-cvd.js +2 -2
- package/tools/get-divergences.js +2 -2
- package/tools/get-funding-context.js +2 -2
- package/tools/get-liquidation-levels.js +2 -2
- package/tools/get-liquidation-pulse.js +2 -2
- package/tools/get-pattern-scan.js +2 -2
- package/tools/get-regime.js +2 -2
- package/tools/get-resting-liquidity.js +2 -2
- package/tools/get-risk-scenario.js +2 -2
- package/tools/get-session-review.js +2 -2
- package/tools/get-setup-detail.js +21 -2
- package/tools/get-signals.js +2 -2
- package/tools/get-sizing.js +2 -2
- package/tools/get-trade-feedback.js +2 -2
- package/tools/get-trade-flow.js +2 -2
- package/tools/get-volume-profile.js +2 -2
- package/tools/get-wave9-status.d.ts +127 -0
- package/tools/get-wave9-status.js +796 -0
- package/tools/intel-api.d.ts +20 -0
- package/tools/intel-api.js +67 -0
- package/tools/intel-cache.d.ts +1 -1
- package/tools/intel-cache.js +20 -5
- package/tools/list-strategies.d.ts +11 -1
- package/tools/list-strategies.js +17 -0
- package/tools/modify-stop.d.ts +4 -0
- package/tools/modify-stop.js +63 -24
- package/tools/modify-target.d.ts +4 -0
- package/tools/modify-target.js +62 -23
- package/tools/scan-pairs.d.ts +4 -0
- package/tools/scan-pairs.js +22 -8
- package/tools/toggle-strategy.js +7 -0
- package/types.d.ts +5 -0
- package/venues/hyperliquid/hl-balance.d.ts +116 -0
- package/venues/hyperliquid/hl-balance.js +145 -0
- package/venues/hyperliquid/hl-bracket-coordinator.d.ts +123 -0
- package/venues/hyperliquid/hl-bracket-coordinator.js +533 -0
- package/venues/hyperliquid/hl-brackets.d.ts +102 -0
- package/venues/hyperliquid/hl-brackets.js +172 -0
- package/venues/hyperliquid/hl-cloid.d.ts +22 -0
- package/venues/hyperliquid/hl-cloid.js +82 -0
- package/venues/hyperliquid/hl-info-cache.d.ts +46 -0
- package/venues/hyperliquid/hl-info-cache.js +125 -0
- package/venues/hyperliquid/hl-live-adapter.d.ts +146 -0
- package/venues/hyperliquid/hl-live-adapter.js +728 -0
- package/venues/hyperliquid/hl-precision.d.ts +61 -0
- package/venues/hyperliquid/hl-precision.js +176 -0
- package/venues/hyperliquid/hl-private.d.ts +88 -0
- package/venues/hyperliquid/hl-private.js +357 -0
- package/venues/hyperliquid/hl-public.d.ts +31 -4
- package/venues/hyperliquid/hl-public.js +163 -12
- package/venues/hyperliquid/hl-rate-gate.d.ts +57 -0
- package/venues/hyperliquid/hl-rate-gate.js +220 -0
- package/venues/hyperliquid/hl-user-stream.d.ts +90 -0
- package/venues/hyperliquid/hl-user-stream.js +220 -0
- package/venues/registry.d.ts +23 -9
- package/venues/registry.js +12 -13
- package/venues/symbols.d.ts +43 -0
- package/venues/symbols.js +107 -0
- package/wave9/live-account-capture.d.ts +67 -0
- package/wave9/live-account-capture.js +435 -0
- package/wave9/live-autonomous-protection.d.ts +39 -0
- package/wave9/live-autonomous-protection.js +112 -0
- package/wave9/live-durable-reconciliation-scheduler.d.ts +33 -0
- package/wave9/live-durable-reconciliation-scheduler.js +115 -0
- package/wave9/live-execution-ledger.d.ts +107 -0
- package/wave9/live-execution-ledger.js +498 -0
- package/wave9/live-position-confirmation.d.ts +18 -0
- package/wave9/live-position-confirmation.js +111 -0
- package/wave9/live-residual-protection.d.ts +18 -0
- package/wave9/live-residual-protection.js +250 -0
- package/wave9/live-startup-reconciliation.d.ts +38 -0
- package/wave9/live-startup-reconciliation.js +454 -0
- package/wave9/live-symbol-ownership.d.ts +20 -0
- package/wave9/live-symbol-ownership.js +132 -0
- package/wave9/paper-admission-guard.d.ts +199 -0
- package/wave9/paper-admission-guard.js +650 -0
- package/wave9/usdm-evidence-provider.d.ts +42 -0
- package/wave9/usdm-evidence-provider.js +133 -0
package/live/stop-watcher.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
3
|
-
import type { CcxtPosition } from '../types.js';
|
|
3
|
+
import type { CcxtOrder, CcxtPosition } from '../types.js';
|
|
4
|
+
import type { TradingOperationLock } from '../lifecycle/trading-operation-lock.js';
|
|
4
5
|
export declare const DEFAULT_INTERVAL_MS = 10000;
|
|
5
6
|
export interface StopTriggeredEvent {
|
|
6
7
|
symbol: string;
|
|
@@ -9,6 +10,33 @@ export interface StopTriggeredEvent {
|
|
|
9
10
|
markPrice: number;
|
|
10
11
|
quantity: number;
|
|
11
12
|
}
|
|
13
|
+
/** Emitted with 'stop_closed' — carries the executed close order so listeners
|
|
14
|
+
* (the journal auto-capture wiring in index.ts) can record the close with the
|
|
15
|
+
* real fill. Without this, watcher closes bypassed the Position Decision
|
|
16
|
+
* Journal entirely and left phantom-open journal positions (issue #199). */
|
|
17
|
+
export interface StopClosedEvent extends StopTriggeredEvent {
|
|
18
|
+
order?: CcxtOrder;
|
|
19
|
+
}
|
|
20
|
+
export type Wave9StopCloseOutcome = {
|
|
21
|
+
status: 'flat';
|
|
22
|
+
detail: string;
|
|
23
|
+
candidateId: string;
|
|
24
|
+
symbol: string;
|
|
25
|
+
} | {
|
|
26
|
+
status: 'protected' | 'manual_intervention';
|
|
27
|
+
detail: string;
|
|
28
|
+
candidateId: string;
|
|
29
|
+
symbol: string;
|
|
30
|
+
stopPrice: number;
|
|
31
|
+
residualQuantity?: number;
|
|
32
|
+
stopClientOrderId?: string;
|
|
33
|
+
};
|
|
34
|
+
/** Optional durable Wave 9 lifecycle. Generic watcher behavior is unchanged
|
|
35
|
+
* when this is absent or the current position has no owned candidate. */
|
|
36
|
+
export interface Wave9StopCloseLifecycle {
|
|
37
|
+
resolveCandidateId(position: CcxtPosition): Promise<string | undefined> | string | undefined;
|
|
38
|
+
settleAfterClose(candidateId: string, symbol: string): Promise<Wave9StopCloseOutcome>;
|
|
39
|
+
}
|
|
12
40
|
/** Decide whether a position has crossed its stop.
|
|
13
41
|
* Exported for direct unit-testing without spinning up a watcher loop. */
|
|
14
42
|
export declare function isStopBreached(position: CcxtPosition): boolean;
|
|
@@ -16,6 +44,8 @@ export declare class PositionWatcher extends EventEmitter {
|
|
|
16
44
|
private interval;
|
|
17
45
|
private intervalMs;
|
|
18
46
|
private adapter;
|
|
47
|
+
private readonly operationLock?;
|
|
48
|
+
private wave9CloseLifecycle?;
|
|
19
49
|
/** Symbols currently awaiting a close RPC — second trigger in the same tick
|
|
20
50
|
* is suppressed so the watcher cannot double-close. Cleared after the close
|
|
21
51
|
* call resolves (success or failure). */
|
|
@@ -23,7 +53,10 @@ export declare class PositionWatcher extends EventEmitter {
|
|
|
23
53
|
/** Symbols we've already logged a breach for this session, to avoid spamming
|
|
24
54
|
* the log every tick while the close is in flight. */
|
|
25
55
|
private notifiedBreach;
|
|
26
|
-
constructor(adapter: IExchangeAdapter, intervalMs?: number);
|
|
56
|
+
constructor(adapter: IExchangeAdapter, intervalMs?: number, operationLock?: TradingOperationLock);
|
|
57
|
+
/** Configure this after the durable execution ledger is ready. Runtime
|
|
58
|
+
* lifecycle hooks reapply it to every watcher after reconnect. */
|
|
59
|
+
setWave9CloseLifecycle(lifecycle?: Wave9StopCloseLifecycle): void;
|
|
27
60
|
start(): void;
|
|
28
61
|
stop(): void;
|
|
29
62
|
/** Run one check cycle. Exposed for tests (skip setInterval). */
|
package/live/stop-watcher.js
CHANGED
|
@@ -45,6 +45,8 @@ export class PositionWatcher extends EventEmitter {
|
|
|
45
45
|
interval = null;
|
|
46
46
|
intervalMs;
|
|
47
47
|
adapter;
|
|
48
|
+
operationLock;
|
|
49
|
+
wave9CloseLifecycle;
|
|
48
50
|
/** Symbols currently awaiting a close RPC — second trigger in the same tick
|
|
49
51
|
* is suppressed so the watcher cannot double-close. Cleared after the close
|
|
50
52
|
* call resolves (success or failure). */
|
|
@@ -52,10 +54,16 @@ export class PositionWatcher extends EventEmitter {
|
|
|
52
54
|
/** Symbols we've already logged a breach for this session, to avoid spamming
|
|
53
55
|
* the log every tick while the close is in flight. */
|
|
54
56
|
notifiedBreach = new Set();
|
|
55
|
-
constructor(adapter, intervalMs = DEFAULT_INTERVAL_MS) {
|
|
57
|
+
constructor(adapter, intervalMs = DEFAULT_INTERVAL_MS, operationLock) {
|
|
56
58
|
super();
|
|
57
59
|
this.adapter = adapter;
|
|
58
60
|
this.intervalMs = intervalMs;
|
|
61
|
+
this.operationLock = operationLock;
|
|
62
|
+
}
|
|
63
|
+
/** Configure this after the durable execution ledger is ready. Runtime
|
|
64
|
+
* lifecycle hooks reapply it to every watcher after reconnect. */
|
|
65
|
+
setWave9CloseLifecycle(lifecycle) {
|
|
66
|
+
this.wave9CloseLifecycle = lifecycle;
|
|
59
67
|
}
|
|
60
68
|
start() {
|
|
61
69
|
if (this.interval) {
|
|
@@ -130,10 +138,62 @@ export class PositionWatcher extends EventEmitter {
|
|
|
130
138
|
quantity: position.contracts,
|
|
131
139
|
};
|
|
132
140
|
this.emit('stop_triggered', event);
|
|
141
|
+
// Captured close order for the 'stop_closed' event — set inside close()
|
|
142
|
+
// (which may run under the symbol lock) and read after it resolves.
|
|
143
|
+
let closeOrder;
|
|
144
|
+
const close = async () => {
|
|
145
|
+
let trustedPosition = position;
|
|
146
|
+
// Re-read after acquiring the shared trading boundary. A Wave 9
|
|
147
|
+
// reversal or another stop may have flattened the symbol while this
|
|
148
|
+
// watcher was queued.
|
|
149
|
+
if (this.operationLock) {
|
|
150
|
+
const current = await this.adapter.getPositionsOrNull(position.symbol);
|
|
151
|
+
if (current === null) {
|
|
152
|
+
throw new Error('position recheck unavailable while holding trading operation lock');
|
|
153
|
+
}
|
|
154
|
+
const latest = current.find((candidate) => candidate.symbol === position.symbol);
|
|
155
|
+
if (!latest || !isStopBreached(latest)) {
|
|
156
|
+
logger.info(TAG, `${position.symbol} no longer stop-triggered after locked recheck — skipping`);
|
|
157
|
+
return 'skipped';
|
|
158
|
+
}
|
|
159
|
+
trustedPosition = latest;
|
|
160
|
+
}
|
|
161
|
+
let candidateId;
|
|
162
|
+
let ownershipError;
|
|
163
|
+
if (this.wave9CloseLifecycle) {
|
|
164
|
+
try {
|
|
165
|
+
candidateId = await this.wave9CloseLifecycle.resolveCandidateId(trustedPosition);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
ownershipError = formatError(error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (ownershipError) {
|
|
172
|
+
throw new Error(`Wave 9 ownership resolution failed before stop close submission: ${ownershipError}; ` +
|
|
173
|
+
'IMMEDIATE MANUAL INTERVENTION REQUIRED');
|
|
174
|
+
}
|
|
175
|
+
closeOrder = await this.adapter.closePosition(trustedPosition.symbol, 'stop_watcher');
|
|
176
|
+
if (!candidateId || !this.wave9CloseLifecycle)
|
|
177
|
+
return 'closed';
|
|
178
|
+
const outcome = await this.wave9CloseLifecycle.settleAfterClose(candidateId, trustedPosition.symbol);
|
|
179
|
+
if (outcome.status === 'flat')
|
|
180
|
+
return 'closed';
|
|
181
|
+
if (outcome.status === 'protected') {
|
|
182
|
+
logger.warn(TAG, `Stop close left a Wave 9 residual on ${trustedPosition.symbol}; ` +
|
|
183
|
+
`${outcome.detail}. The exact frozen stop is proven and the watcher will retry.`);
|
|
184
|
+
this.emit('stop_residual_protected', { ...event, ...outcome });
|
|
185
|
+
return 'residual_protected';
|
|
186
|
+
}
|
|
187
|
+
throw new Error(outcome.detail);
|
|
188
|
+
};
|
|
133
189
|
try {
|
|
134
|
-
|
|
190
|
+
const result = this.operationLock
|
|
191
|
+
? await this.operationLock.withSymbolLock(position.symbol, `stop_watcher:${position.symbol}`, close)
|
|
192
|
+
: await close();
|
|
193
|
+
if (result !== 'closed')
|
|
194
|
+
return;
|
|
135
195
|
logger.info(TAG, `Auto-closed ${position.symbol} (reason=stop_watcher)`);
|
|
136
|
-
this.emit('stop_closed', event);
|
|
196
|
+
this.emit('stop_closed', { ...event, order: closeOrder });
|
|
137
197
|
}
|
|
138
198
|
catch (err) {
|
|
139
199
|
const msg = formatError(err);
|
package/onboarding/runtime.d.ts
CHANGED
|
@@ -2,7 +2,9 @@ import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
|
2
2
|
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
|
+
import { LiveAdapter } from '../live/live-adapter.js';
|
|
5
6
|
import { PositionWatcher } from '../live/stop-watcher.js';
|
|
7
|
+
import type { TradingOperationLock } from '../lifecycle/trading-operation-lock.js';
|
|
6
8
|
export interface MicroLiveConfig {
|
|
7
9
|
sizeCapPercent?: number;
|
|
8
10
|
maxPositionUSDT?: number;
|
|
@@ -13,6 +15,13 @@ export interface BuildAdapterInput {
|
|
|
13
15
|
microLive?: MicroLiveConfig;
|
|
14
16
|
simulator: ExchangeSimulator;
|
|
15
17
|
}
|
|
18
|
+
/** Wave 9 safety wiring is created only after its durable ledger is loaded.
|
|
19
|
+
* Runtime reapplies these hooks to both the bootstrap objects and every
|
|
20
|
+
* adapter/watcher built by a reconnect. */
|
|
21
|
+
export interface Wave9LiveLifecycleHooks {
|
|
22
|
+
configureLiveAdapter?(adapter: LiveAdapter): void;
|
|
23
|
+
configurePositionWatcher?(watcher: PositionWatcher, adapter: IExchangeAdapter): void;
|
|
24
|
+
}
|
|
16
25
|
/** Pure-ish factory: builds an adapter for the requested mode.
|
|
17
26
|
* Falls back to PaperAdapter if a live mode is requested without credentials —
|
|
18
27
|
* callers should pre-validate via `modeRequiresCredentials`, but this
|
|
@@ -36,6 +45,13 @@ export declare class PluginRuntime {
|
|
|
36
45
|
* stops fire and NAV/uPnL stay live for non-dashboard symbols). Lives for
|
|
37
46
|
* the runtime's lifetime; started in paper, stopped in live. */
|
|
38
47
|
private readonly _marketFeed;
|
|
48
|
+
private readonly operationLock?;
|
|
49
|
+
private wave9LiveLifecycleHooks?;
|
|
50
|
+
/** Observer applied to EVERY stop-watcher this runtime creates (reconnects
|
|
51
|
+
* included). index.ts uses it to attach the journal auto-capture listener
|
|
52
|
+
* for watcher closes (issue #199) — without it, a live<->paper reconnect
|
|
53
|
+
* would silently shed the capture wiring. */
|
|
54
|
+
private readonly onWatcherCreated?;
|
|
39
55
|
/** Reconnect is serialized — a second caller waits for the first to finish
|
|
40
56
|
* so we never tear down an adapter that's mid-rebuild. */
|
|
41
57
|
private reconnectInFlight;
|
|
@@ -45,11 +61,14 @@ export declare class PluginRuntime {
|
|
|
45
61
|
simulator: ExchangeSimulator;
|
|
46
62
|
stopWatcher?: PositionWatcher | null;
|
|
47
63
|
marketFeed?: PaperMarketFeed | null;
|
|
64
|
+
operationLock?: TradingOperationLock;
|
|
65
|
+
onWatcherCreated?: (watcher: PositionWatcher) => void;
|
|
48
66
|
});
|
|
49
67
|
get adapter(): IExchangeAdapter;
|
|
50
68
|
get mode(): TradingMode;
|
|
51
69
|
get stopWatcher(): PositionWatcher | null;
|
|
52
70
|
get marketFeed(): PaperMarketFeed | null;
|
|
71
|
+
setWave9LiveLifecycleHooks(hooks?: Wave9LiveLifecycleHooks): void;
|
|
53
72
|
/**
|
|
54
73
|
* Swap the current adapter for a new one built from `next`. The old
|
|
55
74
|
* adapter's background loops are stopped before the new one is wired in.
|
package/onboarding/runtime.js
CHANGED
|
@@ -15,6 +15,7 @@ import { PaperAdapter } from '../paper-adapter.js';
|
|
|
15
15
|
import { LiveAdapter } from '../live/live-adapter.js';
|
|
16
16
|
import { PositionWatcher } from '../live/stop-watcher.js';
|
|
17
17
|
import { loadBracketMode } from '../config/brackets-config.js';
|
|
18
|
+
import { loadStopWatcherIntervalMs } from '../config/plugin-config-io.js';
|
|
18
19
|
import { logger, formatError } from '../logger.js';
|
|
19
20
|
const TAG = 'plugin-runtime';
|
|
20
21
|
/** Pure-ish factory: builds an adapter for the requested mode.
|
|
@@ -55,6 +56,13 @@ export class PluginRuntime {
|
|
|
55
56
|
* stops fire and NAV/uPnL stay live for non-dashboard symbols). Lives for
|
|
56
57
|
* the runtime's lifetime; started in paper, stopped in live. */
|
|
57
58
|
_marketFeed;
|
|
59
|
+
operationLock;
|
|
60
|
+
wave9LiveLifecycleHooks;
|
|
61
|
+
/** Observer applied to EVERY stop-watcher this runtime creates (reconnects
|
|
62
|
+
* included). index.ts uses it to attach the journal auto-capture listener
|
|
63
|
+
* for watcher closes (issue #199) — without it, a live<->paper reconnect
|
|
64
|
+
* would silently shed the capture wiring. */
|
|
65
|
+
onWatcherCreated;
|
|
58
66
|
/** Reconnect is serialized — a second caller waits for the first to finish
|
|
59
67
|
* so we never tear down an adapter that's mid-rebuild. */
|
|
60
68
|
reconnectInFlight = null;
|
|
@@ -64,11 +72,22 @@ export class PluginRuntime {
|
|
|
64
72
|
this.simulator = initial.simulator;
|
|
65
73
|
this._stopWatcher = initial.stopWatcher ?? null;
|
|
66
74
|
this._marketFeed = initial.marketFeed ?? null;
|
|
75
|
+
this.operationLock = initial.operationLock;
|
|
76
|
+
this.onWatcherCreated = initial.onWatcherCreated;
|
|
67
77
|
}
|
|
68
78
|
get adapter() { return this._adapter; }
|
|
69
79
|
get mode() { return this._mode; }
|
|
70
80
|
get stopWatcher() { return this._stopWatcher; }
|
|
71
81
|
get marketFeed() { return this._marketFeed; }
|
|
82
|
+
setWave9LiveLifecycleHooks(hooks) {
|
|
83
|
+
this.wave9LiveLifecycleHooks = hooks;
|
|
84
|
+
if (this._adapter instanceof LiveAdapter) {
|
|
85
|
+
hooks?.configureLiveAdapter?.(this._adapter);
|
|
86
|
+
}
|
|
87
|
+
if (this._stopWatcher) {
|
|
88
|
+
hooks?.configurePositionWatcher?.(this._stopWatcher, this._adapter);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
72
91
|
/**
|
|
73
92
|
* Swap the current adapter for a new one built from `next`. The old
|
|
74
93
|
* adapter's background loops are stopped before the new one is wired in.
|
|
@@ -82,7 +101,9 @@ export class PluginRuntime {
|
|
|
82
101
|
if (this.reconnectInFlight) {
|
|
83
102
|
await this.reconnectInFlight;
|
|
84
103
|
}
|
|
85
|
-
const run = this.
|
|
104
|
+
const run = this.operationLock
|
|
105
|
+
? this.operationLock.withAccountLock('runtime-reconnect', () => this.doReconnect(next, deps))
|
|
106
|
+
: this.doReconnect(next, deps);
|
|
86
107
|
this.reconnectInFlight = run.finally(() => {
|
|
87
108
|
this.reconnectInFlight = null;
|
|
88
109
|
});
|
|
@@ -117,6 +138,11 @@ export class PluginRuntime {
|
|
|
117
138
|
microLive: next.microLive,
|
|
118
139
|
simulator: this.simulator,
|
|
119
140
|
});
|
|
141
|
+
// Install autonomous protection callbacks before initialization can emit
|
|
142
|
+
// user-data or bracket-reconciler events.
|
|
143
|
+
if (fresh instanceof LiveAdapter) {
|
|
144
|
+
this.wave9LiveLifecycleHooks?.configureLiveAdapter?.(fresh);
|
|
145
|
+
}
|
|
120
146
|
// 4. Fire async init for live adapters (non-blocking — readiness flips
|
|
121
147
|
// INIT_PENDING → READY/DEGRADED/BLOCKED on its own).
|
|
122
148
|
if (fresh instanceof LiveAdapter) {
|
|
@@ -133,8 +159,13 @@ export class PluginRuntime {
|
|
|
133
159
|
this._adapter = fresh;
|
|
134
160
|
this._mode = next.mode;
|
|
135
161
|
deps.adapterDeps.adapter = fresh;
|
|
136
|
-
// 6. Start a new stop-watcher bound to the new adapter.
|
|
137
|
-
|
|
162
|
+
// 6. Start a new stop-watcher bound to the new adapter. Re-read the
|
|
163
|
+
// operator's cadence override — building with `undefined` here
|
|
164
|
+
// silently reverted plugin-config `stopWatcher.intervalMs` to the
|
|
165
|
+
// default on every reconnect.
|
|
166
|
+
const watcher = new PositionWatcher(fresh, loadStopWatcherIntervalMs(), this.operationLock);
|
|
167
|
+
this.wave9LiveLifecycleHooks?.configurePositionWatcher?.(watcher, fresh);
|
|
168
|
+
this.onWatcherCreated?.(watcher);
|
|
138
169
|
watcher.start();
|
|
139
170
|
this._stopWatcher = watcher;
|
|
140
171
|
// 7. Paper market feed follows the mode: run it when the new adapter is
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reefclaw/openclaw-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "ReefClaw trading plugin for OpenClaw \u2014 paper trading with real Binance market data, plus the ReefClaw dashboard connector (supervised by OpenClaw, no service manager needed). Install: /plugins install clawhub:@reefclaw/openclaw-plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"node": ">=20"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@reefclaw/shared": "0.1.
|
|
25
|
+
"@reefclaw/shared": "0.1.2",
|
|
26
26
|
"ccxt": "4.5.37",
|
|
27
27
|
"json5": "2.2.3",
|
|
28
28
|
"ws": "8.19.0"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface ReentryExitRecord {
|
|
2
|
+
/** Canonical symbol (no settle suffix). */
|
|
3
|
+
symbol: string;
|
|
4
|
+
/** setup_type / strategy name from the entry metadata, when known. */
|
|
5
|
+
setupType?: string;
|
|
6
|
+
side: 'long' | 'short';
|
|
7
|
+
/** Whether the closed trade realized a loss (drives the stronger caution). */
|
|
8
|
+
wasLoss?: boolean;
|
|
9
|
+
closedAtMs: number;
|
|
10
|
+
}
|
|
11
|
+
/** Signal-bar duration for a strategy/setup name. Name-suffix inference:
|
|
12
|
+
* `..._4h` → 240min, `..._1d` / daily → 1440min, `..._Nh` → N×60. Unknown
|
|
13
|
+
* shapes default to 60min so short-TF strategies are never over-warned. */
|
|
14
|
+
export declare function strategyBarMinutes(name: string | undefined): number;
|
|
15
|
+
export declare class ReentryTracker {
|
|
16
|
+
private records;
|
|
17
|
+
private readonly filePath;
|
|
18
|
+
private readonly dir;
|
|
19
|
+
constructor(pluginId?: string, opts?: {
|
|
20
|
+
basePath?: string;
|
|
21
|
+
});
|
|
22
|
+
/** Record a position exit. Never throws (best-effort persistence). */
|
|
23
|
+
recordExit(record: Omit<ReentryExitRecord, 'symbol'> & {
|
|
24
|
+
symbol: string;
|
|
25
|
+
}): void;
|
|
26
|
+
/** Most recent exit for (symbol[, setup]). A setup-specific record wins over
|
|
27
|
+
* a symbol-only match so multi-strategy books get precise cautions. */
|
|
28
|
+
lastExit(symbol: string, setupType?: string): ReentryExitRecord | undefined;
|
|
29
|
+
/** Structured caution when (symbol, strategy) was already traded within the
|
|
30
|
+
* current signal bar. Undefined = no caution. Pure indication (issue #204):
|
|
31
|
+
* the agent decides; nothing here blocks an order. */
|
|
32
|
+
cautionFor(symbol: string, strategy: string | undefined, nowMs?: number): string | undefined;
|
|
33
|
+
/** Test seam. */
|
|
34
|
+
getRecords(): readonly ReentryExitRecord[];
|
|
35
|
+
private persist;
|
|
36
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Re-entry tracker — records recent position exits per (symbol, setup) so the
|
|
2
|
+
// entry funnel (scan_pairs / get_signals) can flag setups the agent already
|
|
3
|
+
// traded within the current signal bar (issue #204).
|
|
4
|
+
//
|
|
5
|
+
// WHY: the validated backtests for the 4h/1d templates have one-trade-per-
|
|
6
|
+
// signal semantics — a signal bar produces at most one trade. Live, the 4h
|
|
7
|
+
// condition stays true for hours, so after every exit the next heartbeat
|
|
8
|
+
// re-entered the same setup: measured 19× the backtest cadence with 79-minute
|
|
9
|
+
// median holds, including 22 same-direction re-entries within one bar of a
|
|
10
|
+
// LOSING close (2026-07-14 → 07-20 HL soak). Per the tools-not-mandates
|
|
11
|
+
// doctrine this ships as STRUCTURED INDICATION — the annotation tells the
|
|
12
|
+
// agent the setup was already traded this bar; it never blocks the order.
|
|
13
|
+
//
|
|
14
|
+
// Persistence: small JSON in the plugins base dir (same pattern as
|
|
15
|
+
// position-state-store) so restarts don't blind the indication. Best-effort —
|
|
16
|
+
// a persistence failure degrades to in-memory-only, never throws into the
|
|
17
|
+
// trading path.
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { logger, formatError } from '../logger.js';
|
|
21
|
+
import { normalizeBracketSymbol } from '../live/bracket-ledger.js';
|
|
22
|
+
import { resolvePluginsBaseDir } from '../util/plugin-paths.js';
|
|
23
|
+
const TAG = 'reentry-tracker';
|
|
24
|
+
const DEFAULT_PLUGIN_ID = 'reefclaw-paper-trading';
|
|
25
|
+
const STATE_FILENAME = 'reentry-log.json';
|
|
26
|
+
const MAX_RECORDS = 300;
|
|
27
|
+
/** Signal-bar duration for a strategy/setup name. Name-suffix inference:
|
|
28
|
+
* `..._4h` → 240min, `..._1d` / daily → 1440min, `..._Nh` → N×60. Unknown
|
|
29
|
+
* shapes default to 60min so short-TF strategies are never over-warned. */
|
|
30
|
+
export function strategyBarMinutes(name) {
|
|
31
|
+
if (!name)
|
|
32
|
+
return 60;
|
|
33
|
+
const lower = name.toLowerCase();
|
|
34
|
+
const hourMatch = lower.match(/_(\d{1,2})h\b|_(\d{1,2})h_|_(\d{1,2})h$/);
|
|
35
|
+
if (hourMatch) {
|
|
36
|
+
const h = Number(hourMatch[1] ?? hourMatch[2] ?? hourMatch[3]);
|
|
37
|
+
if (Number.isFinite(h) && h > 0)
|
|
38
|
+
return h * 60;
|
|
39
|
+
}
|
|
40
|
+
if (/_1d\b|_1d_|_1d$|daily/.test(lower))
|
|
41
|
+
return 1440;
|
|
42
|
+
return 60;
|
|
43
|
+
}
|
|
44
|
+
export class ReentryTracker {
|
|
45
|
+
records = [];
|
|
46
|
+
filePath;
|
|
47
|
+
dir;
|
|
48
|
+
constructor(pluginId, opts) {
|
|
49
|
+
const base = resolvePluginsBaseDir(opts?.basePath);
|
|
50
|
+
this.dir = join(base, pluginId ?? DEFAULT_PLUGIN_ID);
|
|
51
|
+
this.filePath = join(this.dir, STATE_FILENAME);
|
|
52
|
+
try {
|
|
53
|
+
if (!existsSync(this.dir))
|
|
54
|
+
mkdirSync(this.dir, { recursive: true });
|
|
55
|
+
if (existsSync(this.filePath)) {
|
|
56
|
+
const parsed = JSON.parse(readFileSync(this.filePath, 'utf-8'));
|
|
57
|
+
if (Array.isArray(parsed?.records)) {
|
|
58
|
+
this.records = parsed.records.filter((r) => typeof r?.symbol === 'string' && Number.isFinite(r?.closedAtMs));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
logger.warn(TAG, `load failed (${formatError(err)}) — starting empty`);
|
|
64
|
+
this.records = [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Record a position exit. Never throws (best-effort persistence). */
|
|
68
|
+
recordExit(record) {
|
|
69
|
+
try {
|
|
70
|
+
this.records.push({ ...record, symbol: normalizeBracketSymbol(record.symbol) });
|
|
71
|
+
if (this.records.length > MAX_RECORDS) {
|
|
72
|
+
this.records = this.records.slice(-MAX_RECORDS);
|
|
73
|
+
}
|
|
74
|
+
this.persist();
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
logger.warn(TAG, `recordExit failed: ${formatError(err)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Most recent exit for (symbol[, setup]). A setup-specific record wins over
|
|
81
|
+
* a symbol-only match so multi-strategy books get precise cautions. */
|
|
82
|
+
lastExit(symbol, setupType) {
|
|
83
|
+
const key = normalizeBracketSymbol(symbol);
|
|
84
|
+
let bySetup;
|
|
85
|
+
let bySymbol;
|
|
86
|
+
for (let i = this.records.length - 1; i >= 0; i--) {
|
|
87
|
+
const r = this.records[i];
|
|
88
|
+
if (r.symbol !== key)
|
|
89
|
+
continue;
|
|
90
|
+
if (!bySymbol)
|
|
91
|
+
bySymbol = r;
|
|
92
|
+
if (setupType && r.setupType === setupType) {
|
|
93
|
+
bySetup = r;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
if (!setupType)
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
return bySetup ?? bySymbol;
|
|
100
|
+
}
|
|
101
|
+
/** Structured caution when (symbol, strategy) was already traded within the
|
|
102
|
+
* current signal bar. Undefined = no caution. Pure indication (issue #204):
|
|
103
|
+
* the agent decides; nothing here blocks an order. */
|
|
104
|
+
cautionFor(symbol, strategy, nowMs = Date.now()) {
|
|
105
|
+
const last = this.lastExit(symbol, strategy);
|
|
106
|
+
if (!last)
|
|
107
|
+
return undefined;
|
|
108
|
+
const barMin = strategyBarMinutes(strategy ?? last.setupType);
|
|
109
|
+
const agoMin = Math.round((nowMs - last.closedAtMs) / 60_000);
|
|
110
|
+
if (agoMin < 0 || agoMin > barMin)
|
|
111
|
+
return undefined;
|
|
112
|
+
const lossNote = last.wasLoss ? ' at a LOSS' : '';
|
|
113
|
+
return (`already traded this signal bar: exited a ${last.side} on this setup ${agoMin}m ago${lossNote} ` +
|
|
114
|
+
`(bar=${barMin}m). The validated backtest takes ONE trade per signal bar — re-enter only if ` +
|
|
115
|
+
`you can name what NEW information arrived since that exit.`);
|
|
116
|
+
}
|
|
117
|
+
/** Test seam. */
|
|
118
|
+
getRecords() {
|
|
119
|
+
return this.records;
|
|
120
|
+
}
|
|
121
|
+
persist() {
|
|
122
|
+
const payload = { schemaVersion: 1, records: this.records };
|
|
123
|
+
const tmp = `${this.filePath}.tmp`;
|
|
124
|
+
writeFileSync(tmp, JSON.stringify(payload), 'utf-8');
|
|
125
|
+
renameSync(tmp, this.filePath);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Direction } from '../signals/types.js';
|
|
2
|
+
import { type Wave9StrategyName } from './wave9-policy.js';
|
|
3
|
+
export interface Wave9AdmissionPolicy {
|
|
4
|
+
riskPct: number;
|
|
5
|
+
maxPositions: number;
|
|
6
|
+
maxOpenRiskPct: number;
|
|
7
|
+
maxGrossExposureMultiple: number;
|
|
8
|
+
maxPositionUsd: number;
|
|
9
|
+
feeBps: number;
|
|
10
|
+
slippageBps: number;
|
|
11
|
+
capEnforcement: 'admission_only_no_forced_deleveraging';
|
|
12
|
+
riskMeasure: 'fixed_initial_stop_risk_usd';
|
|
13
|
+
}
|
|
14
|
+
export declare const WAVE9_ADMISSION_POLICY: Readonly<Wave9AdmissionPolicy>;
|
|
15
|
+
export interface Wave9AdmissionPosition {
|
|
16
|
+
candidateId: string;
|
|
17
|
+
symbol: string;
|
|
18
|
+
strategy: Wave9StrategyName;
|
|
19
|
+
direction: Direction;
|
|
20
|
+
entryPrice: number;
|
|
21
|
+
quantity: number;
|
|
22
|
+
markPrice: number;
|
|
23
|
+
initialStop: number;
|
|
24
|
+
dollarRiskUsd: number;
|
|
25
|
+
accruedFundingUsd: number;
|
|
26
|
+
}
|
|
27
|
+
export interface Wave9AdmissionState {
|
|
28
|
+
cashEquityUsd: number;
|
|
29
|
+
positions: Wave9AdmissionPosition[];
|
|
30
|
+
}
|
|
31
|
+
export interface Wave9AdmissionCandidate {
|
|
32
|
+
id: string;
|
|
33
|
+
eventTime: string;
|
|
34
|
+
symbol: string;
|
|
35
|
+
strategy: Wave9StrategyName;
|
|
36
|
+
direction: Direction;
|
|
37
|
+
referencePrice: number;
|
|
38
|
+
stopPrice: number;
|
|
39
|
+
}
|
|
40
|
+
export type Wave9AdmissionReason = 'accepted' | 'invalidGeometry' | 'symbolAlreadyOpen' | 'maxPositions' | 'maxGrossExposure' | 'maxOpenRisk' | 'invalidSizing';
|
|
41
|
+
export interface Wave9AdmissionLedgerRow extends Wave9AdmissionPosition {
|
|
42
|
+
estimatedExitCostUsd: number;
|
|
43
|
+
}
|
|
44
|
+
export interface Wave9AdmissionDecision extends Wave9AdmissionCandidate {
|
|
45
|
+
accepted: boolean;
|
|
46
|
+
reason: Wave9AdmissionReason;
|
|
47
|
+
entryPrice?: number;
|
|
48
|
+
quantity?: number;
|
|
49
|
+
notionalUsd?: number;
|
|
50
|
+
riskUsd?: number;
|
|
51
|
+
projectedCashEquityUsd?: number;
|
|
52
|
+
projectedLiquidationEquityUsd?: number;
|
|
53
|
+
projectedGrossExposureUsd?: number;
|
|
54
|
+
projectedOpenRiskUsd?: number;
|
|
55
|
+
projectedOpenPositions?: number;
|
|
56
|
+
projectedPositionLedger?: Wave9AdmissionLedgerRow[];
|
|
57
|
+
}
|
|
58
|
+
export interface Wave9AdmissionBatchResult {
|
|
59
|
+
decisions: Wave9AdmissionDecision[];
|
|
60
|
+
state: Wave9AdmissionState;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Apply a same-event candidate batch in the exact frozen priority. The caller
|
|
64
|
+
* must release stops and completed-daily reversals before invoking this
|
|
65
|
+
* function, just as the chronological research engine does.
|
|
66
|
+
*/
|
|
67
|
+
export declare function applyWave9AdmissionBatch(inputState: Wave9AdmissionState, rawCandidates: readonly Wave9AdmissionCandidate[], policy?: Readonly<Wave9AdmissionPolicy>): Wave9AdmissionBatchResult;
|