@reefclaw/openclaw-plugin 0.1.27 → 0.1.29

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.
@@ -0,0 +1,58 @@
1
+ // Which ReefClaw plugin RELEASE is running, and how it was installed — the two
2
+ // facts the dashboard needs to (a) tell the trader an update exists and (b)
3
+ // offer the right update path (one click for npx installs, ClawHub steps for
4
+ // ClawHub installs, silence for the operator's own source/dist deploys).
5
+ //
6
+ // Version source: the `openclaw.plugin.json` that ships NEXT TO index.js in
7
+ // every installed layout. Both release channels stamp it with the package
8
+ // release version — installer/scripts/bundle-assets.mjs for `npx
9
+ // @reefclaw/connect`, plugin-package/scripts/assemble.mjs for ClawHub. The
10
+ // repo's own manifest carries the unstamped 0.1.0, which is what tells a
11
+ // source-tree / script-deployed box apart from a packaged install.
12
+ //
13
+ // History: the readiness report used to send a hardcoded internal constant
14
+ // ('3.8.0') that lived in a different namespace from the release versions
15
+ // ('0.1.x'), so the dashboard's update banner compared apples to oranges and
16
+ // never fired (memory feedback_plugin_update_banner_inert_version_namespace).
17
+ // The webapp still recognises that legacy value and nudges those boxes once.
18
+ import { existsSync, readFileSync } from 'node:fs';
19
+ import { join, resolve, sep } from 'node:path';
20
+ import { homedir } from 'node:os';
21
+ /** The repo manifest's placeholder version. A box reporting it was not
22
+ * installed from a release package (no release will ever be 0.1.0 — the
23
+ * release line passed it long ago). */
24
+ export const UNSTAMPED_VERSION = '0.1.0';
25
+ const RELEASE_VERSION_RE = /^\d+\.\d+\.\d+$/;
26
+ export function resolvePluginInstallFacts(input) {
27
+ let version;
28
+ try {
29
+ const raw = readFileSync(join(input.pluginRoot, 'openclaw.plugin.json'), 'utf-8');
30
+ const v = JSON.parse(raw).version;
31
+ if (typeof v === 'string' && RELEASE_VERSION_RE.test(v.trim()))
32
+ version = v.trim();
33
+ }
34
+ catch {
35
+ // No (readable) manifest next to index.js — legacy deploy layout. Report
36
+ // nothing rather than a guess; the dashboard fails open (no banner).
37
+ }
38
+ return { version, channel: detectChannel(input, version) };
39
+ }
40
+ function detectChannel(input, version) {
41
+ // Unstamped or absent manifest → the repo's own: source tree or a dist deploy
42
+ // (the operator's rigs, updated by deploy scripts — never by the installer).
43
+ if (version === undefined || version === UNSTAMPED_VERSION)
44
+ return 'source';
45
+ // ClawHub packages carry the bootstrap skill at the package root
46
+ // (plugin-package/scripts/assemble.mjs); the npx layout deliberately does
47
+ // not. Checked FIRST: a box that once ran the npx installer and later
48
+ // installed from ClawHub loads the ClawHub copy.
49
+ if (existsSync(join(input.pluginRoot, 'skills')))
50
+ return 'clawhub';
51
+ const home = resolve(input.reefclawHome ?? join(homedir(), '.reefclaw'));
52
+ const root = resolve(input.pluginRoot);
53
+ if (input.connectorSupervisor === 'on' || root.startsWith(home + sep))
54
+ return 'npx';
55
+ // Stamped release package without an installer marker (placed by hand).
56
+ // The installer still updates it in place, so it gets the one-click path.
57
+ return 'npx';
58
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "_comment": "One line per released plugin version, shown in the dashboard's update banner. Baked into the webapp at build by webapp/scripts/generate-skill-content.mjs (LATEST_PLUGIN_NOTES = the entry for plugin-package/package.json#version). Add a line in the same PR that bumps the version.",
3
+ "0.1.27": "Fixes paper positions showing as closed at $0 in the Journal while still open, and adds a 2-minute grace to the position reconciler.",
4
+ "0.1.28": "Fixes the connector's gateway handshake on OpenClaw 2026.9+ (it now connects as OpenClaw's local-backend client) and adds in-app update notices with one-click updates.",
5
+ "0.1.29": "Paper stop and target fills now use the price the decision was made on when the cached order book is stale (paper P&L gets more honest); on a gateway with several agents the connector shows only its own agent's runs; one-click updates can be switched off per box."
6
+ }
@@ -9,6 +9,7 @@ import { randomUUID } from 'node:crypto';
9
9
  import { logger, formatError } from '../logger.js';
10
10
  import { MAX_TRADE_HISTORY, DEFAULT_SIMULATION_CONFIG } from './types.js';
11
11
  import { fillMarketOrder, fillLimitOrder, parseSymbol } from './fill-engine.js';
12
+ import { MAX_BOOK_DRIFT_BPS } from './realistic-fills.js';
12
13
  import { updateMfe } from '../mfe.js';
13
14
  import { computeInvalidationHit } from '../pinned-plan.js';
14
15
  const TAG = 'simulator';
@@ -805,7 +806,10 @@ export class ExchangeSimulator extends EventEmitter {
805
806
  logger.info(TAG, `Market order filled: ${order.side} ${order.amount} ${order.symbol} @ ${result.order.average}` +
806
807
  ` (decision: ${eq.decisionPrice.toFixed(2)}, slippage: ${eq.slippageBps.toFixed(2)}bps` +
807
808
  `, latency: ${eq.latencyMs.toFixed(0)}ms, fee: ${eq.feeRate * 100}%` +
808
- `, book: ${eq.bookDepthAvailable ? `${eq.bookLevelsConsumed} levels` : 'unavailable'})`);
809
+ `, book: ${eq.bookDepthAvailable ? `${eq.bookLevelsConsumed} levels` : 'unavailable'}` +
810
+ `${eq.bookDriftBps !== undefined && Math.abs(eq.bookDriftBps) > MAX_BOOK_DRIFT_BPS
811
+ ? `, stale book ${eq.bookDriftBps.toFixed(0)}bps off the decision price: fill anchored`
812
+ : ''})`);
809
813
  }
810
814
  else {
811
815
  logger.info(TAG, `Market order filled: ${order.side} ${order.amount} ${order.symbol} @ ${result.order.average}`);
@@ -1,4 +1,18 @@
1
1
  import type { OrderBookDepth, SimulationConfig, ExecutionQuality } from './types.js';
2
+ /**
3
+ * How far (bps) the cached book's mid may sit from the decision price before
4
+ * the fill is anchored to the decision price instead of the raw book VWAP.
5
+ *
6
+ * The book is refreshed only when a tool fetches it (tools/helpers.ts
7
+ * fetchOrderBook, typically at entry), so a stop or target hours later used to
8
+ * fill against an hours-old book: an ADA/USDT short stop with a 0.2123 breach
9
+ * mark filled at 0.207103, 245 bps in the trader's favour, booking a -1.04R
10
+ * loss as -0.02R (2026-09-16 audit). Inside the band the book is fresh enough
11
+ * to price the fill directly (unchanged behaviour); outside it the book only
12
+ * shapes the impact (VWAP vs mid), applied to the price the decision was
13
+ * actually made on.
14
+ */
15
+ export declare const MAX_BOOK_DRIFT_BPS = 10;
2
16
  /**
3
17
  * Walk the order book to compute a volume-weighted average fill price.
4
18
  *
@@ -45,6 +59,8 @@ export declare function getFeeRate(orderType: 'market' | 'limit', config: Simula
45
59
  *
46
60
  * Combines: orderbook VWAP + latency drift + appropriate fee rate.
47
61
  * Falls back to simple random slippage if no orderbook is available.
62
+ * A book whose mid has drifted more than MAX_BOOK_DRIFT_BPS from the decision
63
+ * price is stale: its impact is kept, the level is re-anchored.
48
64
  *
49
65
  * @returns fillPrice and ExecutionQuality metrics
50
66
  */
@@ -1,6 +1,20 @@
1
1
  // Phase 9a: Realistic fill simulation.
2
2
  // Order book-aware VWAP fills, latency modeling, maker/taker fees.
3
3
  import { DEFAULT_SIMULATION_CONFIG, priceToBps } from './types.js';
4
+ /**
5
+ * How far (bps) the cached book's mid may sit from the decision price before
6
+ * the fill is anchored to the decision price instead of the raw book VWAP.
7
+ *
8
+ * The book is refreshed only when a tool fetches it (tools/helpers.ts
9
+ * fetchOrderBook, typically at entry), so a stop or target hours later used to
10
+ * fill against an hours-old book: an ADA/USDT short stop with a 0.2123 breach
11
+ * mark filled at 0.207103, 245 bps in the trader's favour, booking a -1.04R
12
+ * loss as -0.02R (2026-09-16 audit). Inside the band the book is fresh enough
13
+ * to price the fill directly (unchanged behaviour); outside it the book only
14
+ * shapes the impact (VWAP vs mid), applied to the price the decision was
15
+ * actually made on.
16
+ */
17
+ export const MAX_BOOK_DRIFT_BPS = 10;
4
18
  /**
5
19
  * Walk the order book to compute a volume-weighted average fill price.
6
20
  *
@@ -92,6 +106,8 @@ export function getFeeRate(orderType, config) {
92
106
  *
93
107
  * Combines: orderbook VWAP + latency drift + appropriate fee rate.
94
108
  * Falls back to simple random slippage if no orderbook is available.
109
+ * A book whose mid has drifted more than MAX_BOOK_DRIFT_BPS from the decision
110
+ * price is stale: its impact is kept, the level is re-anchored.
95
111
  *
96
112
  * @returns fillPrice and ExecutionQuality metrics
97
113
  */
@@ -103,6 +119,7 @@ export function computeRealisticMarketFill(side, amount, decisionPrice, orderboo
103
119
  let latencyImpactBps;
104
120
  let bookLevelsConsumed;
105
121
  let bookDepthAvailable;
122
+ let bookDriftBps;
106
123
  if (orderbook && orderbook.asks.length > 0 && orderbook.bids.length > 0) {
107
124
  // Book-aware VWAP fill
108
125
  const { vwap, levelsConsumed } = computeBookAwareFillPrice(side, amount, orderbook);
@@ -113,8 +130,14 @@ export function computeRealisticMarketFill(side, amount, decisionPrice, orderboo
113
130
  marketImpactBps = priceToBps(vwap, midPrice);
114
131
  // For sells, impact is negative (received less), so take absolute for the metric
115
132
  // but keep signed for the actual price
116
- // Apply latency drift on top of VWAP
117
- const latencyResult = applyLatencyDrift(vwap, side, latencyMs, volFactor);
133
+ // A book that has drifted from the decision price is stale: keep its
134
+ // impact, re-anchor the level (see MAX_BOOK_DRIFT_BPS).
135
+ bookDriftBps = priceToBps(midPrice, decisionPrice);
136
+ const level = Math.abs(bookDriftBps) > MAX_BOOK_DRIFT_BPS
137
+ ? decisionPrice * (1 + marketImpactBps / 10_000)
138
+ : vwap;
139
+ // Apply latency drift on top of the (possibly re-anchored) VWAP
140
+ const latencyResult = applyLatencyDrift(level, side, latencyMs, volFactor);
118
141
  fillPrice = latencyResult.adjustedPrice;
119
142
  latencyImpactBps = latencyResult.latencyImpactBps;
120
143
  }
@@ -147,6 +170,7 @@ export function computeRealisticMarketFill(side, amount, decisionPrice, orderboo
147
170
  feePaid,
148
171
  bookLevelsConsumed,
149
172
  bookDepthAvailable,
173
+ ...(bookDriftBps !== undefined ? { bookDriftBps } : {}),
150
174
  };
151
175
  return { fillPrice, executionQuality };
152
176
  }
@@ -39,6 +39,10 @@ export interface ExecutionQuality {
39
39
  feePaid: number;
40
40
  bookLevelsConsumed: number;
41
41
  bookDepthAvailable: boolean;
42
+ /** Signed bps between the cached book's mid and the decision price at fill
43
+ * time. Beyond MAX_BOOK_DRIFT_BPS the fill was anchored to the decision
44
+ * price (stale book). Absent when no book was available. */
45
+ bookDriftBps?: number;
42
46
  /** Age of the quote the fill priced against (fill time − ticker.timestamp).
43
47
  * Surfaces feed staleness (issue #202); absent on records from before the
44
48
  * field existed or when the ticker carried no usable timestamp. */