@reefclaw/openclaw-plugin 0.1.24 → 0.1.26
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/connector.d.ts +3 -1
- package/bridge/connector.js +37 -2
- package/bridge/gateway/gateway-ws-client.d.ts +2 -0
- package/bridge/gateway/gateway-ws-client.js +6 -0
- package/bridge/gateway/heartbeat-cron.js +2 -1
- package/bridge/heartbeat-runs-state.d.ts +16 -0
- package/bridge/heartbeat-runs-state.js +58 -0
- package/bridge/heartbeat-runs.d.ts +99 -0
- package/bridge/heartbeat-runs.js +300 -0
- package/bridge/heartbeat-transcript.d.ts +209 -0
- package/bridge/heartbeat-transcript.js +688 -0
- package/bridge/index.js +35 -0
- package/bridge/model-health.d.ts +37 -0
- package/bridge/model-health.js +97 -0
- package/bridge/provider.d.ts +5 -1
- package/bridge/providers/gateway.d.ts +25 -1
- package/bridge/providers/gateway.js +167 -2
- 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 +45 -0
- package/bridge/utils/instance-id.d.ts +3 -0
- package/bridge/utils/instance-id.js +48 -0
- package/config/agent-config-client.d.ts +3 -1
- package/config/agent-config-client.js +4 -0
- package/config/brackets-config.d.ts +2 -1
- package/config/brackets-config.js +25 -3
- package/config/gate-store.d.ts +3 -0
- package/config/gate-store.js +11 -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/reentry-cooldown-config.d.ts +7 -0
- package/config/reentry-cooldown-config.js +59 -0
- package/index.js +29 -2
- package/ingest/position-auto-capture.js +49 -4
- package/ingest/readiness-reporter.d.ts +23 -2
- package/ingest/readiness-reporter.js +56 -1
- package/onboarding/runtime.js +4 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- 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/signals/types.js +1 -1
- package/simulator/exchange-simulator.d.ts +5 -1
- package/simulator/exchange-simulator.js +24 -6
- package/simulator/types.d.ts +11 -0
- package/skills/reefclaw/SKILL.md +2 -2
- package/strategy/evaluator.d.ts +4 -0
- package/tools/close-position.js +10 -1
- package/tools/create-order.js +72 -2
- package/tools/hl-provision-agent-wallet.js +29 -11
- 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 +47 -0
- 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/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
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Feature-flag readers for the re-entry cooldown gate (create_order).
|
|
2
|
+
//
|
|
3
|
+
// Mode default 'off' so the gate ships dead-code; the cooldown window default
|
|
4
|
+
// (60 min) matches the 2026-09-02 measurement window that motivated the gate.
|
|
5
|
+
// Mode resolution follows the exitGate pattern (config-service slice 2):
|
|
6
|
+
//
|
|
7
|
+
// central (agent_config.gates.reentryCooldown via gate-store)
|
|
8
|
+
// → plugin-config.json reentryCooldown.mode
|
|
9
|
+
// → 'off'
|
|
10
|
+
//
|
|
11
|
+
// create_order reads the mode PER CALL, so a dashboard/API flip via
|
|
12
|
+
// scripts/enable-reentry-cooldown.py hot-applies within one config poll —
|
|
13
|
+
// no restart. Kill-switch RC_CENTRAL_GATES=off hands control back to the
|
|
14
|
+
// local file. The minutes knob is LOCAL-only (mechanism tunable, not a
|
|
15
|
+
// ladder) — central carries only the mode.
|
|
16
|
+
import { readPluginConfig } from './plugin-config-io.js';
|
|
17
|
+
import { gateStore } from './gate-store.js';
|
|
18
|
+
const VALID_MODES = new Set([
|
|
19
|
+
'off',
|
|
20
|
+
'shadow',
|
|
21
|
+
'observe',
|
|
22
|
+
'enforce',
|
|
23
|
+
]);
|
|
24
|
+
export const DEFAULT_REENTRY_COOLDOWN_MINUTES = 60;
|
|
25
|
+
const MIN_COOLDOWN_MINUTES = 5;
|
|
26
|
+
const MAX_COOLDOWN_MINUTES = 1440;
|
|
27
|
+
export function getReentryCooldownMode(config) {
|
|
28
|
+
const raw = config?.reentryCooldown?.mode;
|
|
29
|
+
if (typeof raw === 'string' && VALID_MODES.has(raw)) {
|
|
30
|
+
return raw;
|
|
31
|
+
}
|
|
32
|
+
return 'off';
|
|
33
|
+
}
|
|
34
|
+
export function loadReentryCooldownMode() {
|
|
35
|
+
const central = gateStore.getReentryCooldown();
|
|
36
|
+
if (central)
|
|
37
|
+
return central;
|
|
38
|
+
try {
|
|
39
|
+
return getReentryCooldownMode(readPluginConfig());
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return 'off';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function getReentryCooldownMinutes(config) {
|
|
46
|
+
const raw = config?.reentryCooldown?.minutes;
|
|
47
|
+
if (typeof raw === 'number' && Number.isFinite(raw)) {
|
|
48
|
+
return Math.max(MIN_COOLDOWN_MINUTES, Math.min(MAX_COOLDOWN_MINUTES, raw));
|
|
49
|
+
}
|
|
50
|
+
return DEFAULT_REENTRY_COOLDOWN_MINUTES;
|
|
51
|
+
}
|
|
52
|
+
export function loadReentryCooldownMinutes() {
|
|
53
|
+
try {
|
|
54
|
+
return getReentryCooldownMinutes(readPluginConfig());
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return DEFAULT_REENTRY_COOLDOWN_MINUTES;
|
|
58
|
+
}
|
|
59
|
+
}
|
package/index.js
CHANGED
|
@@ -1385,12 +1385,21 @@ const paperTradingPlugin = {
|
|
|
1385
1385
|
// Micro-live cap — same loader every runtime reconnect uses
|
|
1386
1386
|
// (buildAdapter), so boot and reconnect can never disagree (F8).
|
|
1387
1387
|
const microLiveConfig = loadMicroLiveConfig();
|
|
1388
|
-
// Bracket-orders feature flag read from plugin-config at construction
|
|
1389
|
-
//
|
|
1388
|
+
// Bracket-orders feature flag read from plugin-config at construction
|
|
1389
|
+
// time. Since 2026-08-25 (E2E audit #3) the default on a live-Binance
|
|
1390
|
+
// box is 'enforce' — the old 'off' default meant a fresh npx install
|
|
1391
|
+
// flipped to live traded genuinely naked (no exchange stops AND the
|
|
1392
|
+
// mandatory-stop gate skipped, since it is wired behind
|
|
1393
|
+
// bracketsEnabled). An 'off' here can only be an explicit override.
|
|
1390
1394
|
const bracketMode = loadBracketMode();
|
|
1391
1395
|
if (bracketMode !== 'off') {
|
|
1392
1396
|
logger.info(TAG, `Bracket orders enabled in mode=${bracketMode}`);
|
|
1393
1397
|
}
|
|
1398
|
+
else {
|
|
1399
|
+
logger.warn(TAG, `LIVE Binance with brackets.mode='off' (explicit config override) — NO exchange-side ` +
|
|
1400
|
+
`stops; positions rely on the software watcher alone and the mandatory-stop ` +
|
|
1401
|
+
`pre-trade gate is OFF. The dashboard readiness banner will show this red.`);
|
|
1402
|
+
}
|
|
1394
1403
|
// User-data WebSocket stream flag — same mode-ladder pattern as brackets.
|
|
1395
1404
|
// Default 'off' keeps REST polling authoritative. Phase 1 ships dead-code;
|
|
1396
1405
|
// the flag flip to 'shadow' / 'observe' / 'enforce' is operator-driven.
|
|
@@ -1418,6 +1427,11 @@ const paperTradingPlugin = {
|
|
|
1418
1427
|
// F26: same wiring object as the Binance arm — the SIGTERM
|
|
1419
1428
|
// drain covers both venues because it drains this client.
|
|
1420
1429
|
tradeIngest,
|
|
1430
|
+
// Journal close capture (close-bypass fix, HL arm): without
|
|
1431
|
+
// this, every bracket SL/TP fill leaked as status='open'
|
|
1432
|
+
// until the reconciler healed it reason-less (50% of wisekid
|
|
1433
|
+
// 30d closes were reconciler_observed_flat).
|
|
1434
|
+
autoCapture,
|
|
1421
1435
|
},
|
|
1422
1436
|
}
|
|
1423
1437
|
: {
|
|
@@ -2623,6 +2637,11 @@ const paperTradingPlugin = {
|
|
|
2623
2637
|
decisionsClient: positionDecisionsClient,
|
|
2624
2638
|
userId: positionDecisionsUserId,
|
|
2625
2639
|
reentryTracker,
|
|
2640
|
+
// WS2 directional scoreboard (docs/MARKET_ADAPTIVITY_PLAN.md §3):
|
|
2641
|
+
// tracked positions from the state store (no exchange round-trip)
|
|
2642
|
+
// + book resolved per call so a paper↔live flip follows.
|
|
2643
|
+
openPositions: () => positionStateStore.getAll().map((e) => ({ side: e.side })),
|
|
2644
|
+
book: () => (runtime.adapter.isLive ? 'live' : 'paper'),
|
|
2626
2645
|
})),
|
|
2627
2646
|
},
|
|
2628
2647
|
{
|
|
@@ -2920,6 +2939,14 @@ const paperTradingPlugin = {
|
|
|
2920
2939
|
venue,
|
|
2921
2940
|
publicApi: hlPublicApi ?? binanceApi,
|
|
2922
2941
|
toolCount: toolNames.length,
|
|
2942
|
+
// live_stop_protection (E2E audit #3): what would stop a losing live
|
|
2943
|
+
// position. Deferred closure over the runtime so paper↔live flips and
|
|
2944
|
+
// adapter swaps surface on the next 5-min report without a restart.
|
|
2945
|
+
resolveStopProtection: () => ({
|
|
2946
|
+
tradingMode: runtime.mode,
|
|
2947
|
+
venueEnforced: runtime.adapter.bracketsAlwaysEnforced === true,
|
|
2948
|
+
bracketMode: loadBracketMode(),
|
|
2949
|
+
}),
|
|
2923
2950
|
});
|
|
2924
2951
|
maybeStartConnectorSupervisor();
|
|
2925
2952
|
},
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// that hooks into the WS-ingest pipeline (see POSITION_DECISION_JOURNAL_PLAN
|
|
20
20
|
// §5.1 for the longer-term design).
|
|
21
21
|
import { logger } from '../logger.js';
|
|
22
|
-
import {
|
|
22
|
+
import { isBracketClientId, parseBracketClientId } from '../live/bracket-id.js';
|
|
23
23
|
import { normalizeBracketSymbol } from '../live/bracket-ledger.js';
|
|
24
24
|
import { fillPriceFromOrder } from '../live/fill-price.js';
|
|
25
25
|
import { getSkillVersionCached, withSkillVersion } from './skill-version-reader.js';
|
|
@@ -306,12 +306,27 @@ export async function onClosePositionFilled(ctx, inputs, order) {
|
|
|
306
306
|
};
|
|
307
307
|
ctx.decisionsClient.postClose(ctx.userId, close);
|
|
308
308
|
// Re-entry indication (issue #204) — record the exit so scan_pairs can flag
|
|
309
|
-
// same-bar re-entries on this (symbol, setup)
|
|
309
|
+
// same-bar re-entries on this (symbol, setup), and so the reentryCooldown
|
|
310
|
+
// gate can see recent losses. Live agent-closes have no engine trade record
|
|
311
|
+
// here (exchange-exact PnL arrives later on the WS fill, racing this path),
|
|
312
|
+
// so the loss sign falls back to the agent's own r_multiple_at_close — the
|
|
313
|
+
// same validator-checked field the exit gate trusts. lossSource lets the
|
|
314
|
+
// shadow soak audit that sign against the DB before enforce.
|
|
315
|
+
const rRaw = inputs.closeAssessment?.['r_multiple_at_close'];
|
|
316
|
+
const rAtClose = typeof rRaw === 'number' && Number.isFinite(rRaw) ? rRaw : undefined;
|
|
310
317
|
ctx.reentryTracker?.recordExit({
|
|
311
318
|
symbol: inputs.symbol,
|
|
312
319
|
setupType: stateEntry.setupType ?? paperTrade?.setupType,
|
|
313
320
|
side: stateEntry.side,
|
|
314
|
-
wasLoss: paperTrade
|
|
321
|
+
wasLoss: paperTrade
|
|
322
|
+
? paperTrade.netRealizedPnl < 0
|
|
323
|
+
: rAtClose != null
|
|
324
|
+
? rAtClose < 0
|
|
325
|
+
: undefined,
|
|
326
|
+
lossSource: paperTrade ? 'paper_engine' : rAtClose != null ? 'assessment_r' : undefined,
|
|
327
|
+
mode: ctx.resolveMode?.(),
|
|
328
|
+
realizedR: rAtClose,
|
|
329
|
+
realizedPnl: paperTrade?.netRealizedPnl,
|
|
315
330
|
closedAtMs: closeAtMs,
|
|
316
331
|
});
|
|
317
332
|
// Drop local state — symbol can re-enter as a new position.
|
|
@@ -388,6 +403,7 @@ export async function onAutoFlattenClose(ctx, inputs, lookup = {
|
|
|
388
403
|
symbol: inputs.symbol,
|
|
389
404
|
setupType: flattenState?.setupType,
|
|
390
405
|
side: flattenState?.side ?? 'long',
|
|
406
|
+
mode: ctx.resolveMode?.(),
|
|
391
407
|
closedAtMs: inputs.observedAtMs ?? Date.now(),
|
|
392
408
|
});
|
|
393
409
|
ctx.stateStore.remove(inputs.symbol);
|
|
@@ -421,6 +437,9 @@ export async function onStopWatcherClose(ctx, inputs) {
|
|
|
421
437
|
setupType: stateEntry?.setupType ?? paperTrade?.setupType,
|
|
422
438
|
side: stateEntry?.side ?? 'long',
|
|
423
439
|
wasLoss: paperTrade ? paperTrade.netRealizedPnl < 0 : undefined,
|
|
440
|
+
lossSource: paperTrade ? 'paper_engine' : undefined,
|
|
441
|
+
mode: ctx.resolveMode?.(),
|
|
442
|
+
realizedPnl: paperTrade?.netRealizedPnl,
|
|
424
443
|
closedAtMs: closeAtMs,
|
|
425
444
|
});
|
|
426
445
|
const dropState = () => { ctx.stateStore?.remove(inputs.symbol); };
|
|
@@ -627,7 +646,13 @@ async function handleReduceOnlyExit(ctx, fill) {
|
|
|
627
646
|
// onClosePositionFilled) or a manual/external close — both are handled by
|
|
628
647
|
// their own paths (close_position's rich reason+assessment, or the reconciler
|
|
629
648
|
// backstop). Closing here would clobber the agent's close reasoning, so defer.
|
|
630
|
-
|
|
649
|
+
// Recognition is VENUE-DISPATCHED (bracket-id rule): Binance `bkt…`/`rc-…`
|
|
650
|
+
// cids, Hyperliquid `0xbc7…` cloids — the Binance-only check silently
|
|
651
|
+
// classed every HL bracket fill as external and deferred it forever.
|
|
652
|
+
const cidVenue = ctx.venue ?? 'binance';
|
|
653
|
+
const isBracket = fill.clientOrderId
|
|
654
|
+
? isBracketClientId(cidVenue, fill.clientOrderId)
|
|
655
|
+
: false;
|
|
631
656
|
if (!isBracket) {
|
|
632
657
|
logger.info(TAG, `${fill.symbol} flat via non-bracket reduce-only fill (cid=${fill.clientOrderId ?? 'none'}) — ` +
|
|
633
658
|
`deferring close to close_position / reconciler backstop (no clobber)`);
|
|
@@ -643,6 +668,12 @@ async function handleReduceOnlyExit(ctx, fill) {
|
|
|
643
668
|
'Auto-journaled from the WS fill — no close_position call (close-bypass path).',
|
|
644
669
|
observedFrom: 'ws_reduce_only_fill',
|
|
645
670
|
clientOrderId: fill.clientOrderId,
|
|
671
|
+
// Which protective leg fired ('stop' | 'target'), parsed from the cid.
|
|
672
|
+
// Kept in the assessment (not a new close reason) so the close_reason
|
|
673
|
+
// vocabulary stays stable for the miner's plan-adherence classifier.
|
|
674
|
+
leg: fill.clientOrderId
|
|
675
|
+
? parseBracketClientId(cidVenue, fill.clientOrderId)?.role
|
|
676
|
+
: undefined,
|
|
646
677
|
},
|
|
647
678
|
scorecardVerdict: 'NO_GO',
|
|
648
679
|
confluenceScore: 0,
|
|
@@ -668,6 +699,9 @@ async function handleReduceOnlyExit(ctx, fill) {
|
|
|
668
699
|
setupType: stateEntry.setupType,
|
|
669
700
|
side: stateEntry.side,
|
|
670
701
|
wasLoss: realizedPnl < 0,
|
|
702
|
+
lossSource: 'ws_fill',
|
|
703
|
+
mode: ctx.resolveMode?.(),
|
|
704
|
+
realizedPnl,
|
|
671
705
|
closedAtMs: fill.exchangeTimeMs ?? Date.now(),
|
|
672
706
|
});
|
|
673
707
|
ctx.stateStore.remove(fill.symbol);
|
|
@@ -733,5 +767,16 @@ export function buildEntryPlanMetadata(md) {
|
|
|
733
767
|
j.note = rr.note;
|
|
734
768
|
out.realization_rule = j;
|
|
735
769
|
}
|
|
770
|
+
// Cooldown-gate measurement tag (snake_case per the canonical JSONB key
|
|
771
|
+
// rule) — present only when the gate triggered and the entry fired anyway.
|
|
772
|
+
const rc = md.reentryCooldown;
|
|
773
|
+
if (rc) {
|
|
774
|
+
out.reentry_cooldown = {
|
|
775
|
+
mode: rc.mode,
|
|
776
|
+
minutes_since_loss: rc.minutesSinceLoss,
|
|
777
|
+
cooldown_minutes: rc.cooldownMinutes,
|
|
778
|
+
would_block: rc.wouldBlock,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
736
781
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
737
782
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ReadinessReport, type VenueId, type VenueReachabilityResult } from '@reefclaw/shared';
|
|
1
|
+
import { type ReadinessCheck, type ReadinessReport, type VenueId, type VenueReachabilityResult } from '@reefclaw/shared';
|
|
2
2
|
/** Who froze the loop. 'unknown' when the kernel counter is unreadable (not
|
|
3
3
|
* Linux / no CONFIG_SCHEDSTATS / first cycle) — attribution is evidence, and
|
|
4
4
|
* absent evidence stays absent rather than defaulting to a blame. */
|
|
@@ -11,6 +11,23 @@ export declare function attributeStall(stallMs: number, runqueueWaitMs: number |
|
|
|
11
11
|
export interface VenueReachabilityProbe {
|
|
12
12
|
probeReachability(): Promise<VenueReachabilityResult>;
|
|
13
13
|
}
|
|
14
|
+
/** Snapshot for the `live_stop_protection` check (E2E audit #3, 2026-08-25):
|
|
15
|
+
* what — if anything — would stop a losing live position. Resolved per cycle
|
|
16
|
+
* via a deferred closure over the runtime so it follows paper↔live flips and
|
|
17
|
+
* adapter swaps without a restart. */
|
|
18
|
+
export interface StopProtectionSnapshot {
|
|
19
|
+
/** 'PAPER' | 'SHADOW' | 'MICRO_LIVE' | 'LIVE' (runtime.mode). */
|
|
20
|
+
tradingMode: string;
|
|
21
|
+
/** Adapter declares venue-enforced brackets (Hyperliquid live). */
|
|
22
|
+
venueEnforced: boolean;
|
|
23
|
+
/** The effective Binance bracket mode ('off' | 'observe' | 'enforce'). */
|
|
24
|
+
bracketMode: string;
|
|
25
|
+
}
|
|
26
|
+
/** Map a snapshot to the readiness check. Pure — the whole point of the row is
|
|
27
|
+
* that `fail` means REAL MONEY WITH NO STOP, so the mapping is unit-tested
|
|
28
|
+
* branch by branch. Null snapshot → null (older wiring: omit the row rather
|
|
29
|
+
* than fabricate a verdict). */
|
|
30
|
+
export declare function stopProtectionCheck(snap: StopProtectionSnapshot | null, checkedAt: number): ReadinessCheck | null;
|
|
14
31
|
/** Cross-cycle memory for the debounced rungs. Held by the interval loop and
|
|
15
32
|
* passed in explicitly so `collectReadiness` stays a pure function of its
|
|
16
33
|
* inputs — a module-global counter would leak between unit tests. */
|
|
@@ -31,6 +48,10 @@ export interface ReadinessReporterOptions {
|
|
|
31
48
|
publicApi: VenueReachabilityProbe;
|
|
32
49
|
/** Number of trading tools registered (a health signal). */
|
|
33
50
|
toolCount: number;
|
|
51
|
+
/** Resolve the stop-protection snapshot at CALL time (deferred closure over
|
|
52
|
+
* the runtime — follows paper↔live flips and adapter swaps). Absent/null →
|
|
53
|
+
* the `live_stop_protection` row is omitted, never fabricated. */
|
|
54
|
+
resolveStopProtection?: () => StopProtectionSnapshot | null;
|
|
34
55
|
fetchImpl?: typeof fetch;
|
|
35
56
|
intervalMs?: number;
|
|
36
57
|
requestTimeoutMs?: number;
|
|
@@ -46,7 +67,7 @@ export interface ReadinessReporterOptions {
|
|
|
46
67
|
* warn/fail drift is reported 'unknown' (not amber/red) so the readiness
|
|
47
68
|
* banner doesn't cry-wolf for ~5 min after every restart; a genuinely
|
|
48
69
|
* skewed clock still surfaces on cycle 2. */
|
|
49
|
-
export declare function collectReadiness(opts: Pick<ReadinessReporterOptions, 'venue' | 'publicApi' | 'toolCount'>, bootWarmup?: boolean, deps?: {
|
|
70
|
+
export declare function collectReadiness(opts: Pick<ReadinessReporterOptions, 'venue' | 'publicApi' | 'toolCount' | 'resolveStopProtection'>, bootWarmup?: boolean, deps?: {
|
|
50
71
|
/** Debounce memory. Omitted → a fresh state, so a lone unreachable reads
|
|
51
72
|
* `unknown`; only a caller that persists state across cycles can ever
|
|
52
73
|
* reach the warn rung. */
|
|
@@ -43,6 +43,45 @@ export function attributeStall(stallMs, runqueueWaitMs) {
|
|
|
43
43
|
const hostThreshold = Math.max(HOST_WAIT_FLOOR_MS, stallMs * HOST_WAIT_SHARE_OF_STALL);
|
|
44
44
|
return runqueueWaitMs >= hostThreshold ? 'host' : 'self';
|
|
45
45
|
}
|
|
46
|
+
/** Map a snapshot to the readiness check. Pure — the whole point of the row is
|
|
47
|
+
* that `fail` means REAL MONEY WITH NO STOP, so the mapping is unit-tested
|
|
48
|
+
* branch by branch. Null snapshot → null (older wiring: omit the row rather
|
|
49
|
+
* than fabricate a verdict). */
|
|
50
|
+
export function stopProtectionCheck(snap, checkedAt) {
|
|
51
|
+
if (!snap)
|
|
52
|
+
return null;
|
|
53
|
+
const live = snap.tradingMode === 'LIVE' || snap.tradingMode === 'MICRO_LIVE';
|
|
54
|
+
if (!live) {
|
|
55
|
+
return makeReadinessCheck('live_stop_protection', 'pass', {
|
|
56
|
+
detail: 'paper — software stop-watcher',
|
|
57
|
+
checkedAt,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (snap.venueEnforced) {
|
|
61
|
+
return makeReadinessCheck('live_stop_protection', 'pass', {
|
|
62
|
+
detail: 'venue-enforced exchange brackets',
|
|
63
|
+
checkedAt,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (snap.bracketMode === 'enforce') {
|
|
67
|
+
return makeReadinessCheck('live_stop_protection', 'pass', {
|
|
68
|
+
detail: 'exchange-native brackets (enforce)',
|
|
69
|
+
checkedAt,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (snap.bracketMode === 'observe') {
|
|
73
|
+
return makeReadinessCheck('live_stop_protection', 'pass', {
|
|
74
|
+
detail: 'exchange brackets (observe) + software watcher',
|
|
75
|
+
checkedAt,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// LIVE with brackets off — with the live-enforce default this can only be an
|
|
79
|
+
// explicit operator override, and it must burn red on every surface.
|
|
80
|
+
return makeReadinessCheck('live_stop_protection', 'fail', {
|
|
81
|
+
detail: `brackets.mode='${snap.bracketMode}' on a ${snap.tradingMode} box`,
|
|
82
|
+
checkedAt,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
46
85
|
export function createReadinessCycleState() {
|
|
47
86
|
return { consecutiveReachFailures: 0 };
|
|
48
87
|
}
|
|
@@ -84,6 +123,17 @@ export async function collectReadiness(opts, bootWarmup = false, deps = {}) {
|
|
|
84
123
|
detail: `${opts.toolCount} tools`,
|
|
85
124
|
checkedAt: now,
|
|
86
125
|
}));
|
|
126
|
+
// live_stop_protection — a local config/adapter read, no network. Resolved
|
|
127
|
+
// per cycle so a paper→live flip surfaces on the next report without a
|
|
128
|
+
// restart. Resolver failure → omit the row (absent evidence stays absent).
|
|
129
|
+
try {
|
|
130
|
+
const stopCheck = stopProtectionCheck(opts.resolveStopProtection?.() ?? null, now);
|
|
131
|
+
if (stopCheck)
|
|
132
|
+
checks.push(stopCheck);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
logger.warn(TAG, `stop-protection snapshot failed (row omitted): ${formatError(err)}`);
|
|
136
|
+
}
|
|
87
137
|
// ★ Probe FIRST, sample the loop delay AFTER. The freeze that makes a probe
|
|
88
138
|
// abort happens *during* the probe, so sampling first would file the evidence
|
|
89
139
|
// in the NEXT cycle's window — reachability would read 'stalled' this cycle
|
|
@@ -296,7 +346,12 @@ export function startReadinessReporter(opts) {
|
|
|
296
346
|
const state = createReadinessCycleState();
|
|
297
347
|
const cycle = async (bootWarmup) => {
|
|
298
348
|
try {
|
|
299
|
-
const report = await collectReadiness({
|
|
349
|
+
const report = await collectReadiness({
|
|
350
|
+
venue: opts.venue,
|
|
351
|
+
publicApi: opts.publicApi,
|
|
352
|
+
toolCount: opts.toolCount,
|
|
353
|
+
resolveStopProtection: opts.resolveStopProtection,
|
|
354
|
+
}, bootWarmup, { state });
|
|
300
355
|
await postReadiness(opts.apiBaseUrl, opts.token, report, fetchImpl, timeoutMs);
|
|
301
356
|
if (report.overall === 'fail') {
|
|
302
357
|
const failing = report.checks.filter((c) => c.status === 'fail').map((c) => c.id).join(', ');
|
package/onboarding/runtime.js
CHANGED
|
@@ -63,6 +63,10 @@ export function buildAdapter(input) {
|
|
|
63
63
|
marketSlippagePct: readPluginConfig().hl?.marketSlippagePct,
|
|
64
64
|
microLive,
|
|
65
65
|
tradeIngest,
|
|
66
|
+
// Journal close capture (close-bypass fix) — same wiring the boot
|
|
67
|
+
// path passes; omitting it here would shed the capture on every
|
|
68
|
+
// reconnect-built adapter (the F9 class).
|
|
69
|
+
autoCapture: input.wiring?.autoCapture,
|
|
66
70
|
},
|
|
67
71
|
});
|
|
68
72
|
}
|
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.26",
|
|
5
5
|
"description": "Supervised trading plugin for the ReefClaw dashboard. It runs on YOUR machine and starts in PAPER mode with no API keys. It cannot trade real funds until you supply exchange credentials and step PAPER→MICRO_LIVE→LIVE yourself from the dashboard — the agent cannot make that change (the tool is refused without operator provenance). Exchange keys stay local, are used only to sign requests to the exchange, and are never transmitted to ReefClaw (asserted by a test in this package). Trading telemetry — positions, fills, decision journal — is sent to ReefClaw to render the dashboard. Every live position carries exchange-native protective stops. Remote updates to the agent's trading instructions are applied only after an Ed25519 signature is verified against a public key pinned in this build.",
|
|
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. Runs entirely on YOUR machine and starts in PAPER mode — it cannot trade real funds until you supply exchange credentials and walk the PAPER→MICRO_LIVE→LIVE ladder yourself from the ReefClaw dashboard (the agent cannot make that change; it is refused without operator provenance). Your exchange API keys stay on your machine to sign requests to the exchange and are NEVER sent to ReefClaw — a test in the package asserts this. What does reach ReefClaw is trading telemetry for the dashboard (positions, fills, decision journal). Live trading always carries exchange-native protective stops. Trading instructions can be updated remotely, and every update must carry a valid Ed25519 signature verified against a key pinned in this build before it is applied. Install: /plugins install clawhub:@reefclaw/openclaw-plugin",
|
|
3
|
+
"version": "0.1.26",
|
|
4
|
+
"description": "ReefClaw supervised trading plugin for OpenClaw. Runs entirely on YOUR machine and starts in PAPER mode — it cannot trade real funds until you supply exchange credentials and walk the PAPER→MICRO_LIVE→LIVE ladder yourself from the ReefClaw dashboard (the agent cannot make that change; it is refused without operator provenance). Your exchange API keys stay on your machine to sign requests to the exchange and are NEVER sent to ReefClaw — a test in the package asserts this. What does reach ReefClaw is trading telemetry for the dashboard (positions, fills, decision journal). Live trading always carries exchange-native protective stops. Trading instructions can be updated remotely, and every update must carry a valid Ed25519 signature verified against a key pinned in this build before it is applied. Install: npx --yes @reefclaw/connect, or from ClawHub on OpenClaw 2026.8.1+ (Control UI Plugins > Discover, or /plugins install clawhub:@reefclaw/openclaw-plugin then the same with --accept-capabilities after reviewing the listed capabilities)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"openclaw": {
|
|
@@ -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 {};
|