@reefclaw/openclaw-plugin 0.1.11 → 0.1.12

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.
@@ -155,6 +155,29 @@ export declare function extractBracketField(pos: {
155
155
  tpPrice?: number;
156
156
  state: string;
157
157
  } | undefined;
158
+ /**
159
+ * Narrow the agent's PLANNED protective levels off a plugin-decorated position.
160
+ *
161
+ * `bracket` above is exchange truth and only exists where a venue-side ledger
162
+ * row does. In PAPER (either venue) there are no exchange legs — the levels
163
+ * live in the position's metadata and the stop-watcher enforces them — so
164
+ * without these fields the dashboard had NO target at all and drew the
165
+ * frozen-at-fill `originalStopPrice` as the stop even after a modify_stop.
166
+ * Both adapters already emit `stopPrice` / `targetPrice` on the CCXT position
167
+ * (ExchangeSimulator.getPositions, LiveAdapter.decoratePositionsWithMetadata);
168
+ * we only re-validate the shape here because CcxtPosition's index signature
169
+ * is `unknown`.
170
+ *
171
+ * ★ `plannedStopPrice` is the CURRENT stop (modify_stop moves it), NOT the
172
+ * frozen `originalStopPrice` R-denominator — never conflate the two. Display
173
+ * precedence is bracket (exchange truth) → planned → original.
174
+ */
175
+ export declare function extractPlannedLevels(pos: {
176
+ [key: string]: unknown;
177
+ }): {
178
+ plannedStopPrice?: number;
179
+ plannedTargetPrice?: number;
180
+ };
158
181
  /** The payload shape received from GatewayWsClient 'agent' event */
159
182
  export interface AgentEventPayload {
160
183
  runId: string;
@@ -304,6 +304,42 @@ export function extractBracketField(pos) {
304
304
  out.tpPrice = tpPrice;
305
305
  return out;
306
306
  }
307
+ /**
308
+ * Narrow the agent's PLANNED protective levels off a plugin-decorated position.
309
+ *
310
+ * `bracket` above is exchange truth and only exists where a venue-side ledger
311
+ * row does. In PAPER (either venue) there are no exchange legs — the levels
312
+ * live in the position's metadata and the stop-watcher enforces them — so
313
+ * without these fields the dashboard had NO target at all and drew the
314
+ * frozen-at-fill `originalStopPrice` as the stop even after a modify_stop.
315
+ * Both adapters already emit `stopPrice` / `targetPrice` on the CCXT position
316
+ * (ExchangeSimulator.getPositions, LiveAdapter.decoratePositionsWithMetadata);
317
+ * we only re-validate the shape here because CcxtPosition's index signature
318
+ * is `unknown`.
319
+ *
320
+ * ★ `plannedStopPrice` is the CURRENT stop (modify_stop moves it), NOT the
321
+ * frozen `originalStopPrice` R-denominator — never conflate the two. Display
322
+ * precedence is bracket (exchange truth) → planned → original.
323
+ */
324
+ export function extractPlannedLevels(pos) {
325
+ const num = (v) => typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined;
326
+ // A `fixed_target` realization rule carries the same number; use it only as a
327
+ // fallback for entries that pinned the rule without a top-level targetPrice.
328
+ const rule = pos.realizationRule;
329
+ const ruleTarget = rule && typeof rule === 'object'
330
+ ? rule.type === 'fixed_target'
331
+ ? num(rule.targetPrice)
332
+ : undefined
333
+ : undefined;
334
+ const stop = num(pos.stopPrice);
335
+ const target = num(pos.targetPrice) ?? ruleTarget;
336
+ const out = {};
337
+ if (stop !== undefined)
338
+ out.plannedStopPrice = stop;
339
+ if (target !== undefined)
340
+ out.plannedTargetPrice = target;
341
+ return out;
342
+ }
307
343
  /**
308
344
  * Build the wallet/available/locked tuple from Binance Futures `info.assets[]`
309
345
  * when present. Returns null when the field is absent or unusable so the
@@ -9,7 +9,7 @@ import { isTradingMode } from '../types.js';
9
9
  import { GatewayHttpClient } from '../gateway/gateway-http-client.js';
10
10
  import { GatewayWsClient } from '../gateway/gateway-ws-client.js';
11
11
  import { discoverTools } from '../gateway/tool-discovery.js';
12
- import { EventParser, mapCcxtBalance, mapCcxtOrder, extractLiquidationFields, extractBracketField, } from '../gateway/event-parser.js';
12
+ import { EventParser, mapCcxtBalance, mapCcxtOrder, extractLiquidationFields, extractBracketField, extractPlannedLevels, } from '../gateway/event-parser.js';
13
13
  import { Poller } from '../gateway/poller.js';
14
14
  import { ensureHeartbeatCron } from '../gateway/heartbeat-cron.js';
15
15
  import { computeEquity as _computeEquity, computePositionNotional as _computePositionNotional, computeRiskMetrics as _computeRiskMetrics, DEFAULT_RISK_LIMITS, } from './risk-calculator.js';
@@ -766,6 +766,8 @@ export class GatewayProvider {
766
766
  giveBackRatio: typeof pos.giveBackRatio === 'number' ? pos.giveBackRatio : undefined,
767
767
  originalStopPrice: typeof pos.originalStopPrice === 'number' ? pos.originalStopPrice : undefined,
768
768
  ...(bracketField !== undefined ? { bracket: bracketField } : {}),
769
+ // Agent's planned stop/target — the ONLY SL/TP source in paper mode.
770
+ ...extractPlannedLevels(pos),
769
771
  ...liqFields,
770
772
  });
771
773
  }
@@ -2063,6 +2065,8 @@ export class GatewayProvider {
2063
2065
  giveBackRatio: typeof p.giveBackRatio === 'number' ? p.giveBackRatio : undefined,
2064
2066
  originalStopPrice: typeof p.originalStopPrice === 'number' ? p.originalStopPrice : undefined,
2065
2067
  ...(bracketField !== undefined ? { bracket: bracketField } : {}),
2068
+ // Agent's planned stop/target — the ONLY SL/TP source in paper mode.
2069
+ ...extractPlannedLevels(p),
2066
2070
  ...liqFields,
2067
2071
  };
2068
2072
  });
package/bridge/types.d.ts CHANGED
@@ -186,6 +186,8 @@ export interface RiskUpdatePayload {
186
186
  tpPrice?: number;
187
187
  state: string;
188
188
  };
189
+ plannedStopPrice?: number;
190
+ plannedTargetPrice?: number;
189
191
  }>;
190
192
  balance?: {
191
193
  total: number;
@@ -239,6 +241,8 @@ export interface ReconciliationSnapshot {
239
241
  tpPrice?: number;
240
242
  state: string;
241
243
  };
244
+ plannedStopPrice?: number;
245
+ plannedTargetPrice?: number;
242
246
  }>;
243
247
  balance: {
244
248
  currency: string;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "reefclaw-paper-trading",
3
3
  "name": "ReefClaw Trading",
4
- "version": "0.1.11",
4
+ "version": "0.1.12",
5
5
  "description": "Supervised trading plugin for the ReefClaw dashboard: paper trading with real market data (no API keys required), and optional live trading on Binance or Hyperliquid behind explicit operator opt-in, exchange API credentials, and always-on protective stop brackets. Includes the dashboard connector bridge, heartbeat automation, and remote SKILL.md instruction updates from the ReefClaw webapp.",
6
6
  "author": "ReefClaw",
7
7
  "activation": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reefclaw/openclaw-plugin",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "ReefClaw supervised trading plugin for OpenClaw \u2014 paper trading with real market data, optional live trading on Binance or Hyperliquid (operator opt-in, API keys, always-on protective brackets), plus the ReefClaw dashboard connector with heartbeat automation and remote SKILL.md updates from the ReefClaw webapp. Install: /plugins install clawhub:@reefclaw/openclaw-plugin",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -79,6 +79,14 @@ export declare class HyperliquidLiveAdapter extends EventEmitter implements IExc
79
79
  */
80
80
  closePosition(symbol: string, _closeReason?: CloseReason): Promise<CcxtOrder>;
81
81
  getBalance(): Promise<CcxtBalance>;
82
+ /** The live SL/TP trigger prices for `symbol`, or undefined when no
83
+ * non-terminal ledger row exists. Mirrors LiveAdapter's Binance-side
84
+ * `lookupBracket` so the dashboard's protective-level surfaces read the same
85
+ * shape on both venues. Reads `_coordinator` directly rather than through
86
+ * the lazy getter — merely displaying positions must not construct the
87
+ * coordinator (which writes a ledger file); with no coordinator there are
88
+ * no brackets to report anyway. */
89
+ private lookupBracket;
82
90
  /** Display contract (`?? []`) — the 20+ KPI/display callers. */
83
91
  getPositions(symbol?: string): Promise<CcxtPosition[]>;
84
92
  /** ★ Decision contract — null means UNKNOWN, and destructive paths must not act. */
@@ -120,6 +120,11 @@ export class HyperliquidLiveAdapter extends EventEmitter {
120
120
  // Prime the ADDRESS action budget (the starvation guard, §5.6).
121
121
  await this.api.refreshAddressBudget();
122
122
  // ---- Bracket wiring (issue #209) ----
123
+ // Build the coordinator here, not on first bracket action: getPositions
124
+ // reads its ledger to surface live SL/TP, and until the first truth-check
125
+ // fired (60s) a fresh boot would otherwise show every open position as
126
+ // unprotected on the dashboard. Construction is idempotent + cheap.
127
+ this.getHlBracketCoordinator();
123
128
  // Fast path: the user stream (fills drive attach for resting limits;
124
129
  // orderUpdates is the authoritative leg-lifecycle signal — the HL analog
125
130
  // of Binance's ALGO_UPDATE). Truth path: T-5 proved the WS replays
@@ -497,9 +502,29 @@ export class HyperliquidLiveAdapter extends EventEmitter {
497
502
  enriched.equity = round4(nav.equity);
498
503
  return enriched;
499
504
  }
505
+ /** The live SL/TP trigger prices for `symbol`, or undefined when no
506
+ * non-terminal ledger row exists. Mirrors LiveAdapter's Binance-side
507
+ * `lookupBracket` so the dashboard's protective-level surfaces read the same
508
+ * shape on both venues. Reads `_coordinator` directly rather than through
509
+ * the lazy getter — merely displaying positions must not construct the
510
+ * coordinator (which writes a ledger file); with no coordinator there are
511
+ * no brackets to report anyway. */
512
+ lookupBracket(symbol) {
513
+ const row = this._coordinator?.getLedger().getBySymbol(symbol);
514
+ if (!row)
515
+ return undefined;
516
+ if (row.state !== 'active' && row.state !== 'partial' && row.state !== 'attaching') {
517
+ return undefined;
518
+ }
519
+ return { slPrice: row.stopPrice, tpPrice: row.targetPrice, state: row.state };
520
+ }
500
521
  /** Display contract (`?? []`) — the 20+ KPI/display callers. */
501
522
  async getPositions(symbol) {
502
- return (await this.api.fetchPositions(symbol)) ?? [];
523
+ const positions = (await this.api.fetchPositions(symbol)) ?? [];
524
+ // HL live keeps no per-symbol metadata map, so the bracket ledger is the
525
+ // ONLY protective-level source here — without this the dashboard drew
526
+ // neither a stop nor a target for an HL live position.
527
+ return positions.map((p) => ({ ...p, bracket: this.lookupBracket(p.symbol) }));
503
528
  }
504
529
  /** ★ Decision contract — null means UNKNOWN, and destructive paths must not act. */
505
530
  async getPositionsOrNull(symbol) {