@reefclaw/openclaw-plugin 0.1.23 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bridge/bridge.js +72 -5
- package/bridge/connector.d.ts +3 -1
- package/bridge/connector.js +51 -4
- package/bridge/gateway/heartbeat-cron.js +31 -7
- package/bridge/gateway/poller.d.ts +5 -0
- package/bridge/gateway/poller.js +9 -0
- package/bridge/index.js +21 -0
- package/bridge/provider.d.ts +15 -0
- package/bridge/providers/connector-update.d.ts +89 -0
- package/bridge/providers/connector-update.js +212 -0
- package/bridge/providers/emergency-commands.d.ts +36 -0
- package/bridge/providers/emergency-commands.js +91 -0
- package/bridge/providers/gateway.d.ts +26 -1
- package/bridge/providers/gateway.js +159 -8
- package/bridge/providers/mock.js +1 -0
- package/bridge/shock-wake.d.ts +80 -0
- package/bridge/shock-wake.js +291 -0
- package/bridge/types.d.ts +5 -1
- package/bridge/types.js +5 -0
- package/bridge/utils/instance-id.d.ts +3 -0
- package/bridge/utils/instance-id.js +48 -0
- package/ccxt/binance-private.js +2 -1
- package/ccxt/binance-public.js +6 -1
- package/config/agent-config-client.d.ts +7 -2
- package/config/agent-config-client.js +17 -0
- package/config/agent-config-poller.js +5 -1
- package/config/brackets-config.d.ts +2 -1
- package/config/brackets-config.js +25 -3
- package/config/gate-store.d.ts +12 -0
- package/config/gate-store.js +26 -2
- package/config/loss-streak-config.d.ts +2 -0
- package/config/loss-streak-config.js +33 -0
- package/config/plugin-config-io.d.ts +19 -0
- package/config/plugin-config-io.js +24 -2
- package/config/reentry-cooldown-config.d.ts +7 -0
- package/config/reentry-cooldown-config.js +59 -0
- package/http/keepalive-fetch.d.ts +5 -0
- package/http/keepalive-fetch.js +50 -0
- package/index.js +77 -8
- package/ingest/position-auto-capture.js +49 -4
- package/ingest/position-decisions-client.d.ts +6 -0
- package/ingest/position-decisions-client.js +27 -9
- package/ingest/readiness-reporter.d.ts +23 -2
- package/ingest/readiness-reporter.js +56 -1
- package/live/approval-lifecycle.d.ts +10 -0
- package/live/approval-lifecycle.js +16 -2
- package/live/microstructure-assembler.js +11 -2
- package/live/proposal-decision-listener.d.ts +21 -0
- package/live/proposal-decision-listener.js +39 -0
- package/live/proposal-manager.d.ts +12 -0
- package/live/proposal-manager.js +47 -0
- package/live/stop-watcher.d.ts +16 -1
- package/live/stop-watcher.js +48 -8
- package/onboarding/runtime.js +4 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +38 -38
- package/persistence/state-manager.d.ts +7 -0
- package/persistence/state-manager.js +28 -1
- package/portfolio/directional-scoreboard.d.ts +17 -0
- package/portfolio/directional-scoreboard.js +71 -0
- package/portfolio/reentry-tracker.d.ts +38 -1
- package/portfolio/reentry-tracker.js +49 -0
- package/signals/change-of-character.d.ts +38 -0
- package/signals/change-of-character.js +93 -0
- package/simulator/exchange-simulator.d.ts +27 -1
- package/simulator/exchange-simulator.js +98 -38
- package/simulator/types.d.ts +11 -0
- package/skills/reefclaw/SKILL.md +2 -2
- package/strategy/evaluator.d.ts +4 -0
- package/tools/audit-bracket-protection.js +11 -7
- package/tools/close-position.js +10 -1
- package/tools/create-order.js +121 -9
- package/tools/get-funding-context.js +6 -1
- package/tools/get-liquidation-levels.js +5 -1
- package/tools/get-liquidation-pulse.js +7 -1
- package/tools/get-market-intel.js +2 -1
- package/tools/get-relevant-learnings.js +20 -1
- package/tools/get-resting-liquidity.js +6 -1
- package/tools/get-wave9-status.js +17 -0
- package/tools/hl-provision-agent-wallet.js +29 -11
- package/tools/intel-api.d.ts +9 -0
- package/tools/intel-api.js +32 -1
- package/tools/record-position-reviews.js +2 -2
- package/tools/reentry-cooldown.d.ts +33 -0
- package/tools/reentry-cooldown.js +74 -0
- package/tools/scan-pairs.d.ts +7 -0
- package/tools/scan-pairs.js +67 -11
- package/tools/set-exchange-credentials.js +19 -0
- package/tools/set-trading-mode.d.ts +6 -0
- package/tools/set-trading-mode.js +48 -1
- package/types.d.ts +7 -0
- package/venues/hyperliquid/hl-agent-wallet.d.ts +26 -0
- package/venues/hyperliquid/hl-agent-wallet.js +32 -0
- package/venues/hyperliquid/hl-live-adapter.d.ts +27 -2
- package/venues/hyperliquid/hl-live-adapter.js +101 -13
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Saves/loads state to ~/.openclaw/plugins/reefclaw-paper-trading/state.json
|
|
3
3
|
// Uses atomic writes (temp file + rename) to prevent corruption.
|
|
4
4
|
import { readFile, writeFile, rename, mkdir, unlink } from 'node:fs/promises';
|
|
5
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
5
|
+
import { existsSync, readFileSync, statSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { join, dirname } from 'node:path';
|
|
8
8
|
import { randomUUID } from 'node:crypto';
|
|
@@ -11,6 +11,7 @@ import { createDefaultState } from '../simulator/types.js';
|
|
|
11
11
|
const TAG = 'state-manager';
|
|
12
12
|
export class StateManager {
|
|
13
13
|
statePath;
|
|
14
|
+
lastLoadedStat;
|
|
14
15
|
saveTimer = null;
|
|
15
16
|
pendingState = null;
|
|
16
17
|
constructor(pluginId, baseDir) {
|
|
@@ -131,8 +132,17 @@ export class StateManager {
|
|
|
131
132
|
return null;
|
|
132
133
|
}
|
|
133
134
|
try {
|
|
135
|
+
// Stat BEFORE read: a write racing in between then causes one extra
|
|
136
|
+
// reload next time, never a skipped-but-needed one.
|
|
137
|
+
let stat;
|
|
138
|
+
try {
|
|
139
|
+
const s = statSync(this.statePath, { bigint: true });
|
|
140
|
+
stat = { mtimeNs: s.mtimeNs, size: s.size };
|
|
141
|
+
}
|
|
142
|
+
catch { /* stat raced a delete — plain read below decides */ }
|
|
134
143
|
const raw = readFileSync(this.statePath, 'utf-8');
|
|
135
144
|
const state = JSON.parse(raw);
|
|
145
|
+
this.lastLoadedStat = stat;
|
|
136
146
|
logger.info(TAG, `Loaded state: ${state.positions.length} positions, ${state.openOrders.length} orders, ${state.tradeHistory.length} trades`);
|
|
137
147
|
return state;
|
|
138
148
|
}
|
|
@@ -141,6 +151,23 @@ export class StateManager {
|
|
|
141
151
|
return null;
|
|
142
152
|
}
|
|
143
153
|
}
|
|
154
|
+
/** loadSync that returns null when the file is unchanged since the last
|
|
155
|
+
* loadSync. The reload exists for the two-process case (another process
|
|
156
|
+
* wrote the file); an unchanged file means the caller's replaceState would
|
|
157
|
+
* be a no-op re-parse of the whole trade history — which grows with the
|
|
158
|
+
* account's age and was being paid on every paper-mode tool call. */
|
|
159
|
+
loadSyncIfChanged() {
|
|
160
|
+
try {
|
|
161
|
+
const s = statSync(this.statePath, { bigint: true });
|
|
162
|
+
if (this.lastLoadedStat &&
|
|
163
|
+
this.lastLoadedStat.mtimeNs === s.mtimeNs &&
|
|
164
|
+
this.lastLoadedStat.size === s.size) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch { /* missing/unstatable — loadSync handles + logs it */ }
|
|
169
|
+
return this.loadSync();
|
|
170
|
+
}
|
|
144
171
|
/** Synchronous load-or-create-default — for use in synchronous plugin register(). */
|
|
145
172
|
loadOrDefaultSync(startingBalance, quoteCurrency) {
|
|
146
173
|
const loaded = this.loadSync();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReentryExitRecord } from './reentry-tracker.js';
|
|
2
|
+
export interface ScoreboardInputs {
|
|
3
|
+
/** Tracked open positions on the current book (state store, not exchange). */
|
|
4
|
+
openPositions: Array<{
|
|
5
|
+
side: 'long' | 'short';
|
|
6
|
+
}>;
|
|
7
|
+
/** Tracker exit records, oldest first (as stored). */
|
|
8
|
+
exitRecords: readonly ReentryExitRecord[];
|
|
9
|
+
book: 'paper' | 'live';
|
|
10
|
+
/** e.g. "BTC 4h −2.4% (−2.3×ATR)" — from the leader fact's changeOfCharacter. */
|
|
11
|
+
tapeLine?: string;
|
|
12
|
+
/** How many recent closes to summarize. */
|
|
13
|
+
lastN?: number;
|
|
14
|
+
}
|
|
15
|
+
/** Build the scoreboard line, or undefined when there is nothing to show
|
|
16
|
+
* (no open positions AND no recent closes on this book). */
|
|
17
|
+
export declare function buildDirectionalScoreboard(inputs: ScoreboardInputs): string | undefined;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Directional scoreboard — WS2 of docs/MARKET_ADAPTIVITY_PLAN.md.
|
|
2
|
+
//
|
|
3
|
+
// One compact line of counter-evidence attached to the entry funnel
|
|
4
|
+
// (scan_pairs): the book's current directional tilt, how the last N closes
|
|
5
|
+
// per direction actually went, and what the market-leader tape did — the
|
|
6
|
+
// three facts an anchored agent never sees together. Pure indication: it
|
|
7
|
+
// never blocks or vetoes anything (that is the refuted-ledger's territory).
|
|
8
|
+
//
|
|
9
|
+
// Data sources are all local + free: the PositionStateStore (tracked open
|
|
10
|
+
// positions — no exchange round-trip, so no HL address-budget cost) and the
|
|
11
|
+
// ReentryTracker exit records (persisted; realizedR/realizedPnl where a
|
|
12
|
+
// source had them). The tape line comes from the market leader's
|
|
13
|
+
// change-of-character block on the intel facts the caller already fetched.
|
|
14
|
+
const emptyStats = () => ({
|
|
15
|
+
wins: 0, losses: 0, unknown: 0, pnl: 0, pnlKnown: false, rSum: 0, rKnown: false,
|
|
16
|
+
});
|
|
17
|
+
function fmtDir(label, s) {
|
|
18
|
+
const n = s.wins + s.losses + s.unknown;
|
|
19
|
+
if (n === 0)
|
|
20
|
+
return undefined;
|
|
21
|
+
let out = `${label} ${s.wins}W/${s.losses}L${s.unknown > 0 ? `/${s.unknown}?` : ''}`;
|
|
22
|
+
if (s.rKnown)
|
|
23
|
+
out += ` ${s.rSum >= 0 ? '+' : ''}${Math.round(s.rSum * 100) / 100}R`;
|
|
24
|
+
else if (s.pnlKnown) {
|
|
25
|
+
const abs = Math.round(Math.abs(s.pnl) * 100) / 100;
|
|
26
|
+
out += ` ${s.pnl >= 0 ? '+' : '-'}$${abs}`;
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
/** Build the scoreboard line, or undefined when there is nothing to show
|
|
31
|
+
* (no open positions AND no recent closes on this book). */
|
|
32
|
+
export function buildDirectionalScoreboard(inputs) {
|
|
33
|
+
const lastN = inputs.lastN ?? 8;
|
|
34
|
+
const longs = inputs.openPositions.filter((p) => p.side === 'long').length;
|
|
35
|
+
const shorts = inputs.openPositions.filter((p) => p.side === 'short').length;
|
|
36
|
+
// Newest-first walk over this book's records (legacy untagged match either).
|
|
37
|
+
const recent = [];
|
|
38
|
+
for (let i = inputs.exitRecords.length - 1; i >= 0 && recent.length < lastN; i--) {
|
|
39
|
+
const r = inputs.exitRecords[i];
|
|
40
|
+
if (r.mode && r.mode !== inputs.book)
|
|
41
|
+
continue;
|
|
42
|
+
recent.push(r);
|
|
43
|
+
}
|
|
44
|
+
if (longs + shorts === 0 && recent.length === 0)
|
|
45
|
+
return undefined;
|
|
46
|
+
const stats = { long: emptyStats(), short: emptyStats() };
|
|
47
|
+
for (const r of recent) {
|
|
48
|
+
const s = stats[r.side];
|
|
49
|
+
if (r.wasLoss === true)
|
|
50
|
+
s.losses++;
|
|
51
|
+
else if (r.wasLoss === false)
|
|
52
|
+
s.wins++;
|
|
53
|
+
else
|
|
54
|
+
s.unknown++;
|
|
55
|
+
if (typeof r.realizedPnl === 'number' && Number.isFinite(r.realizedPnl)) {
|
|
56
|
+
s.pnl += r.realizedPnl;
|
|
57
|
+
s.pnlKnown = true;
|
|
58
|
+
}
|
|
59
|
+
if (typeof r.realizedR === 'number' && Number.isFinite(r.realizedR)) {
|
|
60
|
+
s.rSum += r.realizedR;
|
|
61
|
+
s.rKnown = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const parts = [`book: ${longs}L/${shorts}S open`];
|
|
65
|
+
const closeBits = [fmtDir('longs', stats.long), fmtDir('shorts', stats.short)].filter((v) => v !== undefined);
|
|
66
|
+
if (closeBits.length > 0)
|
|
67
|
+
parts.push(`last ${recent.length} closes: ${closeBits.join(', ')}`);
|
|
68
|
+
if (inputs.tapeLine)
|
|
69
|
+
parts.push(`tape: ${inputs.tapeLine}`);
|
|
70
|
+
return parts.join(' · ');
|
|
71
|
+
}
|
|
@@ -4,8 +4,24 @@ export interface ReentryExitRecord {
|
|
|
4
4
|
/** setup_type / strategy name from the entry metadata, when known. */
|
|
5
5
|
setupType?: string;
|
|
6
6
|
side: 'long' | 'short';
|
|
7
|
-
/** Whether the closed trade realized a loss (drives the stronger caution
|
|
7
|
+
/** Whether the closed trade realized a loss (drives the stronger caution
|
|
8
|
+
* and arms the reentryCooldown gate). */
|
|
8
9
|
wasLoss?: boolean;
|
|
10
|
+
/** Where the wasLoss sign came from, for auditing sign-accuracy before the
|
|
11
|
+
* cooldown gate is promoted past shadow: engine-exact ('paper_engine',
|
|
12
|
+
* 'ws_fill') vs the agent's own close assessment ('assessment_r'). */
|
|
13
|
+
lossSource?: 'paper_engine' | 'assessment_r' | 'ws_fill';
|
|
14
|
+
/** Trading book at close time. The cooldown gate filters to the current
|
|
15
|
+
* book so a paper loss can't cool down a live entry after a mode flip.
|
|
16
|
+
* Legacy records (absent) match either book — they age out of any
|
|
17
|
+
* realistic cooldown window within the hour anyway. */
|
|
18
|
+
mode?: 'paper' | 'live';
|
|
19
|
+
/** Realized R at close where a source had it (agent assessment on live
|
|
20
|
+
* tool-closes). Feeds the WS2 directional scoreboard. */
|
|
21
|
+
realizedR?: number;
|
|
22
|
+
/** Realized net PnL (quote ccy) where engine/exchange-exact (paper engine,
|
|
23
|
+
* WS bracket fills). Feeds the WS2 directional scoreboard. */
|
|
24
|
+
realizedPnl?: number;
|
|
9
25
|
closedAtMs: number;
|
|
10
26
|
}
|
|
11
27
|
/** Signal-bar duration for a strategy/setup name. Name-suffix inference:
|
|
@@ -26,6 +42,27 @@ export declare class ReentryTracker {
|
|
|
26
42
|
/** Most recent exit for (symbol[, setup]). A setup-specific record wins over
|
|
27
43
|
* a symbol-only match so multi-strategy books get precise cautions. */
|
|
28
44
|
lastExit(symbol: string, setupType?: string): ReentryExitRecord | undefined;
|
|
45
|
+
/** Most recent LOSSY exit for `symbol` within `windowMs`, or undefined.
|
|
46
|
+
* Feeds the reentryCooldown gate (evidence 2026-09-02: 58 re-entries within
|
|
47
|
+
* 60m of a same-symbol losing close ran −0.077R mean vs +0.090R baseline).
|
|
48
|
+
* Any direction, any setup — the measured pathology is symbol churn, not
|
|
49
|
+
* setup churn. `mode` filters to the current trading book; records without
|
|
50
|
+
* a mode tag (pre-upgrade) match either book. */
|
|
51
|
+
lastLossyExit(symbol: string, windowMs: number, opts?: {
|
|
52
|
+
mode?: 'paper' | 'live';
|
|
53
|
+
nowMs?: number;
|
|
54
|
+
}): ReentryExitRecord | undefined;
|
|
55
|
+
/** Trailing consecutive LOSSY closes on `book`, newest first, across ALL
|
|
56
|
+
* symbols — the live feed for the graduated loss-streak sizing brake in
|
|
57
|
+
* preTradeRiskCheck (WS1, docs/MARKET_ADAPTIVITY_PLAN.md §3; live was fed a
|
|
58
|
+
* hardcoded 0 since the beginning while paper computed it from engine
|
|
59
|
+
* history). Semantics: a WIN breaks the streak, and so does an
|
|
60
|
+
* UNKNOWN-outcome record (wasLoss undefined) — unknown is never counted as
|
|
61
|
+
* a loss, and stopping there under-counts, which only makes the brake
|
|
62
|
+
* gentler. Records from the OTHER book are skipped entirely (a paper loss
|
|
63
|
+
* neither extends nor breaks a live streak); legacy untagged records match
|
|
64
|
+
* either book, same as lastLossyExit. */
|
|
65
|
+
consecutiveLosses(book: 'paper' | 'live'): number;
|
|
29
66
|
/** Structured caution when (symbol, strategy) was already traded within the
|
|
30
67
|
* current signal bar. Undefined = no caution. Pure indication (issue #204):
|
|
31
68
|
* the agent decides; nothing here blocks an order. */
|
|
@@ -98,6 +98,55 @@ export class ReentryTracker {
|
|
|
98
98
|
}
|
|
99
99
|
return bySetup ?? bySymbol;
|
|
100
100
|
}
|
|
101
|
+
/** Most recent LOSSY exit for `symbol` within `windowMs`, or undefined.
|
|
102
|
+
* Feeds the reentryCooldown gate (evidence 2026-09-02: 58 re-entries within
|
|
103
|
+
* 60m of a same-symbol losing close ran −0.077R mean vs +0.090R baseline).
|
|
104
|
+
* Any direction, any setup — the measured pathology is symbol churn, not
|
|
105
|
+
* setup churn. `mode` filters to the current trading book; records without
|
|
106
|
+
* a mode tag (pre-upgrade) match either book. */
|
|
107
|
+
lastLossyExit(symbol, windowMs, opts) {
|
|
108
|
+
const key = normalizeBracketSymbol(symbol);
|
|
109
|
+
const now = opts?.nowMs ?? Date.now();
|
|
110
|
+
for (let i = this.records.length - 1; i >= 0; i--) {
|
|
111
|
+
const r = this.records[i];
|
|
112
|
+
if (r.symbol !== key)
|
|
113
|
+
continue;
|
|
114
|
+
if (r.wasLoss !== true)
|
|
115
|
+
continue;
|
|
116
|
+
if (opts?.mode && r.mode && r.mode !== opts.mode)
|
|
117
|
+
continue;
|
|
118
|
+
const ageMs = now - r.closedAtMs;
|
|
119
|
+
// Tolerate small clock skew on exchange timestamps (up to 60s in the
|
|
120
|
+
// future still counts as "just closed").
|
|
121
|
+
if (ageMs >= -60_000 && ageMs <= windowMs)
|
|
122
|
+
return r;
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
/** Trailing consecutive LOSSY closes on `book`, newest first, across ALL
|
|
127
|
+
* symbols — the live feed for the graduated loss-streak sizing brake in
|
|
128
|
+
* preTradeRiskCheck (WS1, docs/MARKET_ADAPTIVITY_PLAN.md §3; live was fed a
|
|
129
|
+
* hardcoded 0 since the beginning while paper computed it from engine
|
|
130
|
+
* history). Semantics: a WIN breaks the streak, and so does an
|
|
131
|
+
* UNKNOWN-outcome record (wasLoss undefined) — unknown is never counted as
|
|
132
|
+
* a loss, and stopping there under-counts, which only makes the brake
|
|
133
|
+
* gentler. Records from the OTHER book are skipped entirely (a paper loss
|
|
134
|
+
* neither extends nor breaks a live streak); legacy untagged records match
|
|
135
|
+
* either book, same as lastLossyExit. */
|
|
136
|
+
consecutiveLosses(book) {
|
|
137
|
+
let n = 0;
|
|
138
|
+
for (let i = this.records.length - 1; i >= 0; i--) {
|
|
139
|
+
const r = this.records[i];
|
|
140
|
+
if (r.mode && r.mode !== book)
|
|
141
|
+
continue;
|
|
142
|
+
if (r.wasLoss === true) {
|
|
143
|
+
n++;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
break; // win or unknown outcome ends the trailing streak
|
|
147
|
+
}
|
|
148
|
+
return n;
|
|
149
|
+
}
|
|
101
150
|
/** Structured caution when (symbol, strategy) was already traded within the
|
|
102
151
|
* current signal bar. Undefined = no caution. Pure indication (issue #204):
|
|
103
152
|
* the agent decides; nothing here blocks an order. */
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** 30m move ≥ this ×ATR(1h) ⇒ 'shock'. */
|
|
2
|
+
export declare const SHOCK_ATR_MULT_30M = 2;
|
|
3
|
+
/** 4h tape opposing a directional regime label by ≥ this ×ATR(1h) ⇒ 'regime_tape_disagreement'. */
|
|
4
|
+
export declare const DISAGREEMENT_ATR_MULT_4H = 2;
|
|
5
|
+
export type ChangeOfCharacterFlag = 'shock' | 'regime_tape_disagreement';
|
|
6
|
+
export interface ChangeOfCharacter {
|
|
7
|
+
/** |price now − close ~30m ago| ÷ ATR(1h). */
|
|
8
|
+
shock30mAtr: number;
|
|
9
|
+
/** (max high − min low) over the last ~30m ÷ ATR(1h) — catches a spike-and-revert the close-to-close move misses. */
|
|
10
|
+
range30mAtr: number;
|
|
11
|
+
/** % return vs the close ~1h ago. */
|
|
12
|
+
return1hPct: number;
|
|
13
|
+
/** % return vs the close ~4h ago. */
|
|
14
|
+
return4hPct: number;
|
|
15
|
+
/** Signed (price now − close ~4h ago) ÷ ATR(1h) — the tape the regime label must answer to. */
|
|
16
|
+
tape4hAtr: number;
|
|
17
|
+
/** ATR(1h) as % of price — the volatility yardstick the multiples are in. */
|
|
18
|
+
atrPct: number;
|
|
19
|
+
flags: ChangeOfCharacterFlag[];
|
|
20
|
+
/** Agent-facing one-liner. Present ONLY when a flag fired — silence stays silent. */
|
|
21
|
+
summary?: string;
|
|
22
|
+
}
|
|
23
|
+
interface BarLike {
|
|
24
|
+
high: number;
|
|
25
|
+
low: number;
|
|
26
|
+
close: number;
|
|
27
|
+
}
|
|
28
|
+
export interface ChangeOfCharacterInputs {
|
|
29
|
+
/** 5m bars, oldest first (≥7 required). */
|
|
30
|
+
ohlcv5m: BarLike[];
|
|
31
|
+
/** 1h bars, oldest first (≥5 required). */
|
|
32
|
+
ohlcv1h: BarLike[];
|
|
33
|
+
atr14: number;
|
|
34
|
+
currentPrice: number;
|
|
35
|
+
regime: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function computeChangeOfCharacter(inputs: ChangeOfCharacterInputs): ChangeOfCharacter | undefined;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// ⚠️ GENERATED FILE — DO NOT EDIT.
|
|
2
|
+
// Canonical source of truth: shared/src/signals/change-of-character.ts
|
|
3
|
+
// Regenerate: node scripts/sync-shared-code.mjs (enforced by shared-code-sync.test.ts)
|
|
4
|
+
//
|
|
5
|
+
// This copy exists because this package builds with tsc and deploys as a
|
|
6
|
+
// self-contained tree that strips workspace deps, so it cannot import
|
|
7
|
+
// @reefclaw/shared runtime code across the deploy boundary.
|
|
8
|
+
// Change-of-character detection — WS2 of docs/MARKET_ADAPTIVITY_PLAN.md.
|
|
9
|
+
//
|
|
10
|
+
// The 2026-09 adaptivity investigation found the agent keeps trading a stale
|
|
11
|
+
// directional view after event-driven regime flips because (a) the regime
|
|
12
|
+
// label is computed from realized bars and lags a shock by hours, and (b) no
|
|
13
|
+
// input ever shows it the contradiction. This module computes the two fast
|
|
14
|
+
// counter-evidence facts from data the fact-computer already has in hand —
|
|
15
|
+
// NO new DB queries (the intel hot-query chunk-exclusion rule is not in play):
|
|
16
|
+
//
|
|
17
|
+
// - SHOCK: the last ~30 minutes moved a multiple of the hourly ATR. An
|
|
18
|
+
// hourly ATR is roughly "a typical hour's range", so 2×ATR inside 30m is
|
|
19
|
+
// ~4× the typical rate — announcement territory.
|
|
20
|
+
// - REGIME/TAPE DISAGREEMENT: the regime label says TREND_UP (or DOWN) but
|
|
21
|
+
// the realized 4h tape moved ≥2×ATR the other way — the label is lagging.
|
|
22
|
+
//
|
|
23
|
+
// Pure function, deterministic, fail-open: insufficient bars or a degenerate
|
|
24
|
+
// ATR returns undefined and every consumer treats that as "no signal".
|
|
25
|
+
// Thresholds are exported so the soak can be re-cut without archaeology.
|
|
26
|
+
// Canonical copy lives in shared/src/signals/ and is synced to
|
|
27
|
+
// intelligence/ + plugin/ via scripts/sync-shared-code.mjs (drift-tested).
|
|
28
|
+
/** 30m move ≥ this ×ATR(1h) ⇒ 'shock'. */
|
|
29
|
+
export const SHOCK_ATR_MULT_30M = 2.0;
|
|
30
|
+
/** 4h tape opposing a directional regime label by ≥ this ×ATR(1h) ⇒ 'regime_tape_disagreement'. */
|
|
31
|
+
export const DISAGREEMENT_ATR_MULT_4H = 2.0;
|
|
32
|
+
const fin = (v) => Number.isFinite(v);
|
|
33
|
+
const round2 = (v) => Math.round(v * 100) / 100;
|
|
34
|
+
export function computeChangeOfCharacter(inputs) {
|
|
35
|
+
const { ohlcv5m, ohlcv1h, atr14, currentPrice, regime } = inputs;
|
|
36
|
+
if (!fin(atr14) || atr14 <= 0 || !fin(currentPrice) || currentPrice <= 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
if (!Array.isArray(ohlcv5m) || ohlcv5m.length < 7)
|
|
39
|
+
return undefined;
|
|
40
|
+
if (!Array.isArray(ohlcv1h) || ohlcv1h.length < 5)
|
|
41
|
+
return undefined;
|
|
42
|
+
// ~30m window = last 6 closed 5m bars; the reference close sits just before it.
|
|
43
|
+
const last6 = ohlcv5m.slice(-6);
|
|
44
|
+
const ref30m = ohlcv5m[ohlcv5m.length - 7].close;
|
|
45
|
+
const ref1h = ohlcv1h[ohlcv1h.length - 2].close;
|
|
46
|
+
const ref4h = ohlcv1h[ohlcv1h.length - 5].close;
|
|
47
|
+
if (!fin(ref30m) || ref30m <= 0 || !fin(ref1h) || ref1h <= 0 || !fin(ref4h) || ref4h <= 0) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const shock30mAtr = Math.abs(currentPrice - ref30m) / atr14;
|
|
51
|
+
const hi30 = Math.max(...last6.map((b) => b.high));
|
|
52
|
+
const lo30 = Math.min(...last6.map((b) => b.low));
|
|
53
|
+
const range30mAtr = fin(hi30) && fin(lo30) && hi30 >= lo30 ? (hi30 - lo30) / atr14 : 0;
|
|
54
|
+
const return1hPct = (currentPrice / ref1h - 1) * 100;
|
|
55
|
+
const return4hPct = (currentPrice / ref4h - 1) * 100;
|
|
56
|
+
const tape4hAtr = (currentPrice - ref4h) / atr14;
|
|
57
|
+
const atrPct = (atr14 / currentPrice) * 100;
|
|
58
|
+
const flags = [];
|
|
59
|
+
if (Math.max(shock30mAtr, range30mAtr) >= SHOCK_ATR_MULT_30M)
|
|
60
|
+
flags.push('shock');
|
|
61
|
+
const labelDir = regime === 'TREND_UP' ? 1 : regime === 'TREND_DOWN' ? -1 : 0;
|
|
62
|
+
const opposing = labelDir !== 0 && Math.sign(tape4hAtr) === -labelDir;
|
|
63
|
+
if (opposing && Math.abs(tape4hAtr) >= DISAGREEMENT_ATR_MULT_4H) {
|
|
64
|
+
flags.push('regime_tape_disagreement');
|
|
65
|
+
}
|
|
66
|
+
const out = {
|
|
67
|
+
shock30mAtr: round2(shock30mAtr),
|
|
68
|
+
range30mAtr: round2(range30mAtr),
|
|
69
|
+
return1hPct: round2(return1hPct),
|
|
70
|
+
return4hPct: round2(return4hPct),
|
|
71
|
+
tape4hAtr: round2(tape4hAtr),
|
|
72
|
+
atrPct: round2(atrPct),
|
|
73
|
+
flags,
|
|
74
|
+
};
|
|
75
|
+
if (flags.length > 0)
|
|
76
|
+
out.summary = buildSummary(out, regime);
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
function buildSummary(c, regime) {
|
|
80
|
+
const parts = [];
|
|
81
|
+
if (c.flags.includes('shock')) {
|
|
82
|
+
parts.push(`SHOCK: last 30m moved ${round2(Math.max(c.shock30mAtr, c.range30mAtr))}×ATR ` +
|
|
83
|
+
`(${c.return1hPct >= 0 ? '+' : ''}${c.return1hPct}% on the hour)`);
|
|
84
|
+
}
|
|
85
|
+
if (c.flags.includes('regime_tape_disagreement')) {
|
|
86
|
+
parts.push(`regime label '${regime}' contradicts the 4h tape ` +
|
|
87
|
+
`(${c.tape4hAtr >= 0 ? '+' : ''}${c.tape4hAtr}×ATR / ` +
|
|
88
|
+
`${c.return4hPct >= 0 ? '+' : ''}${c.return4hPct}%) — the label LAGS realized price`);
|
|
89
|
+
}
|
|
90
|
+
return (`MARKET SHIFT — ${parts.join('; ')}. Re-derive direction from fresh price data ` +
|
|
91
|
+
`before entering; on open positions, check the pinned invalidation FIRST and do not ` +
|
|
92
|
+
`lean on prior theses formed before this move.`);
|
|
93
|
+
}
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import type { SimulatorState, OrderBookDepth, SimulationConfig, ExecutionStats, PositionMetadata, CloseReason } from './types.js';
|
|
3
3
|
import type { CcxtOrder, CcxtBalance, CcxtPosition, CcxtTicker } from '../types.js';
|
|
4
|
+
/** Canonical key for the per-symbol quote/orderbook caches and for matching a
|
|
5
|
+
* ticker against stored positions/orders.
|
|
6
|
+
*
|
|
7
|
+
* CCXT echoes the VENUE-UNIFIED symbol back from fetchTicker: ask Binance USDM
|
|
8
|
+
* for 'ETH/USDT' and the ticker returns as 'ETH/USDT:USDT'. Positions and
|
|
9
|
+
* orders, however, are stored under whatever form the caller opened them with,
|
|
10
|
+
* so `lastTicker.get(position.symbol)` silently missed for every position held
|
|
11
|
+
* in the un-suffixed form. The consequences were all silent:
|
|
12
|
+
* - getPositions()/computeEquity() fell back to entryPrice, so the mark was
|
|
13
|
+
* FROZEN at entry — the stop-watcher compared a frozen mark, never saw a
|
|
14
|
+
* breach, and the position ran unprotected past its stop indefinitely;
|
|
15
|
+
* - createOrder() threw 'No ticker data for X' on the market leg, so the
|
|
16
|
+
* agent could not close by hand either (both automatic and manual exits
|
|
17
|
+
* were dead at once);
|
|
18
|
+
* - MFE, take-profit legs and resting-limit fills never advanced.
|
|
19
|
+
* Keying both sides through this normalizer makes the caches format-agnostic,
|
|
20
|
+
* which also repairs books already persisted with a mix of both forms.
|
|
21
|
+
*
|
|
22
|
+
* Same regex as webapp/src/lib/symbols.ts normalizeSymbol and
|
|
23
|
+
* plugin/src/venues/symbols.ts stripSettleSuffix — kept inline so the
|
|
24
|
+
* simulator keeps its zero-import-for-hot-path shape. */
|
|
25
|
+
export declare function tickerKey(symbol: string): string;
|
|
4
26
|
export declare class ExchangeSimulator extends EventEmitter {
|
|
5
27
|
private state;
|
|
6
28
|
private lastTicker;
|
|
@@ -54,7 +76,11 @@ export declare class ExchangeSimulator extends EventEmitter {
|
|
|
54
76
|
* market branch prices off THIS instead of the last tick, and skips the
|
|
55
77
|
* stale-quote guard — the caller supplied the price, so quote age is
|
|
56
78
|
* irrelevant, and a protective exit must never be blocked (issue #202). */
|
|
57
|
-
referencePrice?: number
|
|
79
|
+
referencePrice?: number,
|
|
80
|
+
/** Set ONLY by closePosition for mechanical/operator-initiated exits —
|
|
81
|
+
* exempts them from the startup lockout. Never reachable from the
|
|
82
|
+
* agent-facing create_order path. */
|
|
83
|
+
protectiveExit?: boolean): CcxtOrder;
|
|
58
84
|
cancelOrder(orderId: string): CcxtOrder;
|
|
59
85
|
cancelAllOrders(symbol?: string): CcxtOrder[];
|
|
60
86
|
/**
|