@reefclaw/openclaw-plugin 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bridge/gateway/event-parser.d.ts +6 -1
- package/bridge/gateway/event-parser.js +19 -2
- package/bridge/gateway/poller.d.ts +1 -0
- package/bridge/gateway/poller.js +14 -2
- package/bridge/providers/gateway.d.ts +22 -2
- package/bridge/providers/gateway.js +67 -9
- package/ccxt/binance-private.d.ts +21 -0
- package/ccxt/binance-private.js +132 -22
- package/ccxt/public-market-data-api.d.ts +14 -0
- package/ccxt/public-market-data-api.js +15 -1
- package/config/plugin-config-io.d.ts +13 -0
- package/config/plugin-config-io.js +15 -0
- package/config/tool-gate.js +3 -0
- package/exchange-adapter.d.ts +16 -0
- package/index.js +658 -83
- package/ingest/position-auto-capture.d.ts +68 -0
- package/ingest/position-auto-capture.js +321 -23
- package/ingest/position-decisions-client.d.ts +7 -2
- package/ingest/position-decisions-client.js +13 -3
- package/ingest/reconcile-db-vs-exchange.d.ts +39 -1
- package/ingest/reconcile-db-vs-exchange.js +66 -10
- package/lifecycle/trading-operation-lock.d.ts +17 -0
- package/lifecycle/trading-operation-lock.js +14 -0
- package/live/bracket-id.d.ts +2 -3
- package/live/bracket-id.js +22 -9
- package/live/fill-price.d.ts +13 -0
- package/live/fill-price.js +37 -0
- package/live/live-adapter.d.ts +57 -2
- package/live/live-adapter.js +290 -49
- package/live/local-signal-service.js +11 -6
- package/live/local-strategy-evaluator.js +4 -0
- package/live/position-state-store.d.ts +4 -0
- package/live/proposal-decision-listener.d.ts +6 -0
- package/live/proposal-decision-listener.js +4 -0
- package/live/stop-watcher.d.ts +35 -2
- package/live/stop-watcher.js +63 -3
- package/onboarding/runtime.d.ts +19 -0
- package/onboarding/runtime.js +34 -3
- package/openclaw.plugin.json +1 -0
- package/package.json +2 -2
- package/portfolio/reentry-tracker.d.ts +36 -0
- package/portfolio/reentry-tracker.js +127 -0
- package/portfolio/wave9-admission.d.ts +67 -0
- package/portfolio/wave9-admission.js +262 -0
- package/portfolio/wave9-policy.d.ts +36 -0
- package/portfolio/wave9-policy.js +183 -0
- package/signals/conditions/registry.js +61 -2
- package/signals/strategy-adapter.js +17 -7
- package/simulator/exchange-simulator.d.ts +12 -0
- package/simulator/exchange-simulator.js +78 -3
- package/simulator/fill-engine.js +5 -1
- package/simulator/types.d.ts +10 -1
- package/skills/reefclaw/SKILL.md +2 -0
- package/strategy/evaluator.d.ts +3 -0
- package/strategy/evaluator.js +5 -0
- package/tools/assessment-validation.d.ts +23 -0
- package/tools/assessment-validation.js +58 -0
- package/tools/attach-brackets.d.ts +7 -2
- package/tools/attach-brackets.js +201 -0
- package/tools/audit-bracket-protection.js +157 -1
- package/tools/bracket-control.d.ts +12 -0
- package/tools/bracket-control.js +35 -0
- package/tools/cancel-all-orders.d.ts +2 -0
- package/tools/cancel-all-orders.js +4 -1
- package/tools/cancel-order.d.ts +4 -0
- package/tools/cancel-order.js +49 -3
- package/tools/close-position.d.ts +23 -0
- package/tools/close-position.js +286 -13
- package/tools/create-order.d.ts +28 -0
- package/tools/create-order.js +1390 -190
- package/tools/get-analytics.js +2 -2
- package/tools/get-basis.js +2 -2
- package/tools/get-cascade-risk.js +2 -2
- package/tools/get-crypto-metrics.js +14 -4
- package/tools/get-cvd.js +2 -2
- package/tools/get-divergences.js +2 -2
- package/tools/get-funding-context.js +2 -2
- package/tools/get-liquidation-levels.js +2 -2
- package/tools/get-liquidation-pulse.js +2 -2
- package/tools/get-pattern-scan.js +2 -2
- package/tools/get-regime.js +2 -2
- package/tools/get-resting-liquidity.js +2 -2
- package/tools/get-risk-scenario.js +2 -2
- package/tools/get-session-review.js +2 -2
- package/tools/get-setup-detail.js +21 -2
- package/tools/get-signals.js +2 -2
- package/tools/get-sizing.js +2 -2
- package/tools/get-trade-feedback.js +2 -2
- package/tools/get-trade-flow.js +2 -2
- package/tools/get-volume-profile.js +2 -2
- package/tools/get-wave9-status.d.ts +127 -0
- package/tools/get-wave9-status.js +796 -0
- package/tools/intel-api.d.ts +20 -0
- package/tools/intel-api.js +67 -0
- package/tools/intel-cache.d.ts +1 -1
- package/tools/intel-cache.js +20 -5
- package/tools/list-strategies.d.ts +11 -1
- package/tools/list-strategies.js +17 -0
- package/tools/modify-stop.d.ts +4 -0
- package/tools/modify-stop.js +63 -24
- package/tools/modify-target.d.ts +4 -0
- package/tools/modify-target.js +62 -23
- package/tools/scan-pairs.d.ts +4 -0
- package/tools/scan-pairs.js +22 -8
- package/tools/toggle-strategy.js +7 -0
- package/types.d.ts +5 -0
- package/venues/hyperliquid/hl-balance.d.ts +116 -0
- package/venues/hyperliquid/hl-balance.js +145 -0
- package/venues/hyperliquid/hl-bracket-coordinator.d.ts +123 -0
- package/venues/hyperliquid/hl-bracket-coordinator.js +533 -0
- package/venues/hyperliquid/hl-brackets.d.ts +102 -0
- package/venues/hyperliquid/hl-brackets.js +172 -0
- package/venues/hyperliquid/hl-cloid.d.ts +22 -0
- package/venues/hyperliquid/hl-cloid.js +82 -0
- package/venues/hyperliquid/hl-info-cache.d.ts +46 -0
- package/venues/hyperliquid/hl-info-cache.js +125 -0
- package/venues/hyperliquid/hl-live-adapter.d.ts +146 -0
- package/venues/hyperliquid/hl-live-adapter.js +728 -0
- package/venues/hyperliquid/hl-precision.d.ts +61 -0
- package/venues/hyperliquid/hl-precision.js +176 -0
- package/venues/hyperliquid/hl-private.d.ts +88 -0
- package/venues/hyperliquid/hl-private.js +357 -0
- package/venues/hyperliquid/hl-public.d.ts +31 -4
- package/venues/hyperliquid/hl-public.js +163 -12
- package/venues/hyperliquid/hl-rate-gate.d.ts +57 -0
- package/venues/hyperliquid/hl-rate-gate.js +220 -0
- package/venues/hyperliquid/hl-user-stream.d.ts +90 -0
- package/venues/hyperliquid/hl-user-stream.js +220 -0
- package/venues/registry.d.ts +23 -9
- package/venues/registry.js +12 -13
- package/venues/symbols.d.ts +43 -0
- package/venues/symbols.js +107 -0
- package/wave9/live-account-capture.d.ts +67 -0
- package/wave9/live-account-capture.js +435 -0
- package/wave9/live-autonomous-protection.d.ts +39 -0
- package/wave9/live-autonomous-protection.js +112 -0
- package/wave9/live-durable-reconciliation-scheduler.d.ts +33 -0
- package/wave9/live-durable-reconciliation-scheduler.js +115 -0
- package/wave9/live-execution-ledger.d.ts +107 -0
- package/wave9/live-execution-ledger.js +498 -0
- package/wave9/live-position-confirmation.d.ts +18 -0
- package/wave9/live-position-confirmation.js +111 -0
- package/wave9/live-residual-protection.d.ts +18 -0
- package/wave9/live-residual-protection.js +250 -0
- package/wave9/live-startup-reconciliation.d.ts +38 -0
- package/wave9/live-startup-reconciliation.js +454 -0
- package/wave9/live-symbol-ownership.d.ts +20 -0
- package/wave9/live-symbol-ownership.js +132 -0
- package/wave9/paper-admission-guard.d.ts +199 -0
- package/wave9/paper-admission-guard.js +650 -0
- package/wave9/usdm-evidence-provider.d.ts +42 -0
- package/wave9/usdm-evidence-provider.js +133 -0
|
@@ -205,6 +205,11 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
205
205
|
// Surface entry metadata (if available)
|
|
206
206
|
...(p.metadata && {
|
|
207
207
|
setupType: p.metadata.setupType,
|
|
208
|
+
missionId: p.metadata.missionId,
|
|
209
|
+
candidateId: p.metadata.candidateId,
|
|
210
|
+
strategy: p.metadata.strategy,
|
|
211
|
+
accruedFundingUsd: p.metadata.accruedFundingUsd,
|
|
212
|
+
fundingThroughTime: p.metadata.fundingThroughTime,
|
|
208
213
|
thesis: p.metadata.thesis,
|
|
209
214
|
stopPrice: p.metadata.stopPrice,
|
|
210
215
|
targetPrice: p.metadata.targetPrice,
|
|
@@ -281,11 +286,18 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
281
286
|
if (!ticker) {
|
|
282
287
|
throw new Error(`No ticker data for ${symbol}. Call updateTicker() first.`);
|
|
283
288
|
}
|
|
289
|
+
// Stale-quote guard (issue #202): a market fill priced off an aged quote
|
|
290
|
+
// books phantom P&L the moment a fresh price arrives (measured up to
|
|
291
|
+
// ~0.6% off on the HL paper book — a real −$22.99 in 4 seconds). Reject
|
|
292
|
+
// instead; the agent refreshes via fetch_ticker and retries.
|
|
293
|
+
this.assertQuoteFresh(symbol, ticker);
|
|
284
294
|
return this.executeMarketFill(order, ticker.last, metadata);
|
|
285
295
|
}
|
|
286
|
-
// Limit order — check if it crosses the current price
|
|
296
|
+
// Limit order — check if it crosses the current price. A stale quote must
|
|
297
|
+
// not price an immediate cross-fill (same hazard as market fills); the
|
|
298
|
+
// order RESTS instead and fills on the next fresh tick via updateTicker.
|
|
287
299
|
const ticker = this.lastTicker.get(symbol);
|
|
288
|
-
if (ticker && this.shouldFillLimit(order, ticker.last)) {
|
|
300
|
+
if (ticker && this.quoteAgeMs(ticker) <= this.maxQuoteAgeMs() && this.shouldFillLimit(order, ticker.last)) {
|
|
289
301
|
return this.executeLimitFill(order, ticker.last, metadata);
|
|
290
302
|
}
|
|
291
303
|
// Limit order doesn't cross — add to open orders. Pin the entry metadata
|
|
@@ -543,6 +555,45 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
543
555
|
};
|
|
544
556
|
}
|
|
545
557
|
// ---- Private helpers ----
|
|
558
|
+
/** Default max quote age a NEW-exposure fill may price against (issue #202).
|
|
559
|
+
* Generous vs the 5s paper feed cadence; env RC_PAPER_MAX_QUOTE_AGE_MS
|
|
560
|
+
* overrides. */
|
|
561
|
+
static DEFAULT_MAX_QUOTE_AGE_MS = 45_000;
|
|
562
|
+
maxQuoteAgeMs() {
|
|
563
|
+
const raw = Number(process.env.RC_PAPER_MAX_QUOTE_AGE_MS);
|
|
564
|
+
return Number.isFinite(raw) && raw > 0 ? raw : ExchangeSimulator.DEFAULT_MAX_QUOTE_AGE_MS;
|
|
565
|
+
}
|
|
566
|
+
/** Quote age from the ticker's own timestamp. A missing/invalid timestamp
|
|
567
|
+
* reads as age 0 (fail-open — the guard cannot fire on it). */
|
|
568
|
+
quoteAgeMs(ticker) {
|
|
569
|
+
const ts = ticker.timestamp;
|
|
570
|
+
if (!Number.isFinite(ts) || ts <= 0)
|
|
571
|
+
return 0;
|
|
572
|
+
return Math.max(0, Date.now() - ts);
|
|
573
|
+
}
|
|
574
|
+
/** Reject fills that would OPEN or GROW exposure on a stale quote. Risk-
|
|
575
|
+
* reducing fills (closes/partials against an existing position) are always
|
|
576
|
+
* allowed — blocking a close on a broken feed compounds the risk. */
|
|
577
|
+
assertQuoteFresh(symbol, ticker) {
|
|
578
|
+
const age = this.quoteAgeMs(ticker);
|
|
579
|
+
const max = this.maxQuoteAgeMs();
|
|
580
|
+
if (age <= max)
|
|
581
|
+
return;
|
|
582
|
+
const pos = this.state.positions.find(p => p.symbol === symbol);
|
|
583
|
+
// The order side reaching here is the one being filled — derive reduce vs
|
|
584
|
+
// grow from the position side at the call site instead? The market path
|
|
585
|
+
// calls this before fill with the order side unavailable; use position
|
|
586
|
+
// presence: any existing position keeps closes flowing, and a stale-quote
|
|
587
|
+
// scale-in on an open position is bounded by the feed refreshing open
|
|
588
|
+
// symbols every 5s (only NEW symbols go minutes without a tick).
|
|
589
|
+
if (pos) {
|
|
590
|
+
logger.warn(TAG, `Stale quote for ${symbol} (${Math.round(age / 1000)}s old) — allowing fill because an open ` +
|
|
591
|
+
`position exists (risk-reducing paths are never blocked)`);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
throw new Error(`Order rejected: market data for ${symbol} is stale (${Math.round(age / 1000)}s old, ` +
|
|
595
|
+
`max ${Math.round(max / 1000)}s). Refresh the price (fetch_ticker) and retry.`);
|
|
596
|
+
}
|
|
546
597
|
shouldFillLimit(order, currentPrice) {
|
|
547
598
|
if (order.price === null)
|
|
548
599
|
return false;
|
|
@@ -562,6 +613,12 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
562
613
|
metadata,
|
|
563
614
|
};
|
|
564
615
|
const result = fillMarketOrder(order, currentPrice, this.state.wallet, position, realistic);
|
|
616
|
+
// Observability for issue #202: stamp the quote's age onto the fill's
|
|
617
|
+
// execution-quality record so staleness is visible in trade history.
|
|
618
|
+
const tickerAtFill = this.lastTicker.get(order.symbol);
|
|
619
|
+
if (result.executionQuality && tickerAtFill) {
|
|
620
|
+
result.executionQuality.quoteAgeMs = this.quoteAgeMs(tickerAtFill);
|
|
621
|
+
}
|
|
565
622
|
const ccxtOrder = this.applyFillResult(result);
|
|
566
623
|
const eq = result.executionQuality;
|
|
567
624
|
if (eq) {
|
|
@@ -608,7 +665,25 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
608
665
|
fillPrice: result.order.average ?? 0,
|
|
609
666
|
fee: result.order.fee.cost,
|
|
610
667
|
});
|
|
611
|
-
|
|
668
|
+
const ccxtOrder = this.toCcxtOrder(result.order);
|
|
669
|
+
// Reducing fills carry a Trade record — attach its engine-exact economics
|
|
670
|
+
// (net-of-fee P&L + both fee legs) so the journal close capture records
|
|
671
|
+
// NET, matching the live book's convention (issue #201). The wallet/NAV
|
|
672
|
+
// was already net; only the journal was blind to fees.
|
|
673
|
+
if (result.trade) {
|
|
674
|
+
const t = result.trade;
|
|
675
|
+
ccxtOrder.info = {
|
|
676
|
+
...(ccxtOrder.info ?? {}),
|
|
677
|
+
paperTrade: {
|
|
678
|
+
grossRealizedPnl: t.realizedPnl,
|
|
679
|
+
netRealizedPnl: t.realizedPnl - (t.fee + (t.openFee ?? 0)),
|
|
680
|
+
openFee: t.openFee ?? 0,
|
|
681
|
+
closeFee: t.fee,
|
|
682
|
+
...(typeof t.metadata?.setupType === 'string' ? { setupType: t.metadata.setupType } : {}),
|
|
683
|
+
},
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
return ccxtOrder;
|
|
612
687
|
}
|
|
613
688
|
updatePosition(symbol, newPosition) {
|
|
614
689
|
const idx = this.state.positions.findIndex(p => p.symbol === symbol);
|
package/simulator/fill-engine.js
CHANGED
|
@@ -53,7 +53,11 @@ export function parseSymbol(symbol) {
|
|
|
53
53
|
if (parts.length !== 2) {
|
|
54
54
|
throw new Error(`Invalid symbol format: ${symbol}. Expected BASE/QUOTE`);
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
const [quote, settle, ...extra] = parts[1].split(':');
|
|
57
|
+
if (!quote || extra.length > 0 || (settle !== undefined && settle !== quote)) {
|
|
58
|
+
throw new Error(`Invalid symbol format: ${symbol}. Expected BASE/QUOTE or BASE/QUOTE:QUOTE`);
|
|
59
|
+
}
|
|
60
|
+
return { base: parts[0], quote };
|
|
57
61
|
}
|
|
58
62
|
/**
|
|
59
63
|
* Ensure a currency exists in the wallet with at least zero balance.
|
package/simulator/types.d.ts
CHANGED
|
@@ -38,6 +38,10 @@ export interface ExecutionQuality {
|
|
|
38
38
|
feePaid: number;
|
|
39
39
|
bookLevelsConsumed: number;
|
|
40
40
|
bookDepthAvailable: boolean;
|
|
41
|
+
/** Age of the quote the fill priced against (fill time − ticker.timestamp).
|
|
42
|
+
* Surfaces feed staleness (issue #202); absent on records from before the
|
|
43
|
+
* field existed or when the ticker carried no usable timestamp. */
|
|
44
|
+
quoteAgeMs?: number;
|
|
41
45
|
}
|
|
42
46
|
export interface SimulatorState {
|
|
43
47
|
version: string;
|
|
@@ -83,7 +87,7 @@ export interface CurrencyBalance {
|
|
|
83
87
|
* - `exchange_stop` / `exchange_target`: exchange-native STOP_MARKET / TAKE_PROFIT_MARKET triggered on Binance
|
|
84
88
|
* - `cancelled_auto`: bracket reconciler cleaned up after detecting the position closed externally
|
|
85
89
|
* - `operator` / `agent` / `emergency`: explicit initiator */
|
|
86
|
-
export type CloseReason = 'agent' | 'stop_watcher' | 'emergency' | 'operator' | 'bracket_attach_failed' | 'exchange_stop' | 'exchange_target' | 'cancelled_auto';
|
|
90
|
+
export type CloseReason = 'agent' | 'stop_watcher' | 'emergency' | 'operator' | 'bracket_attach_failed' | 'exchange_stop' | 'exchange_target' | 'cancelled_auto' | 'wave9_signal_reversal' | 'liquidated' | 'adl' | 'venue_delisted';
|
|
87
91
|
/** The agent's stated profit-realization plan, pinned at entry. PURE DATA /
|
|
88
92
|
* indication — the plugin does NOT enforce it; it is surfaced back so the agent
|
|
89
93
|
* can hold to its own plan instead of re-litigating. The agent fills the parts
|
|
@@ -107,6 +111,11 @@ export interface RealizationRule {
|
|
|
107
111
|
* moment of close. */
|
|
108
112
|
export interface PositionMetadata {
|
|
109
113
|
setupType?: string;
|
|
114
|
+
missionId?: string;
|
|
115
|
+
candidateId?: string;
|
|
116
|
+
strategy?: 'tsmom_28d_long_flat_reversal_1d' | 'tsmom_28d_short_flat_reversal_1d';
|
|
117
|
+
accruedFundingUsd?: number;
|
|
118
|
+
fundingThroughTime?: string;
|
|
110
119
|
thesis?: string;
|
|
111
120
|
stopPrice?: number;
|
|
112
121
|
targetPrice?: number;
|
package/skills/reefclaw/SKILL.md
CHANGED
|
@@ -80,6 +80,8 @@ OpenClaw can install the ReefClaw plugin through its own owner chat command —
|
|
|
80
80
|
2. Tell the user to type `/restart` in this chat.
|
|
81
81
|
3. Then the user types: `/plugins install clawhub:@reefclaw/openclaw-plugin` — the install restarts OpenClaw automatically; the bundled connector starts by itself and the dashboard flips to Connected.
|
|
82
82
|
|
|
83
|
+
If that install is **cancelled asking for a ClawHub risk acknowledgement** (OpenClaw 2026.7+ does this; older versions never ask and reject the flag), rerun the identical command with the acknowledgement appended: `/plugins install clawhub:@reefclaw/openclaw-plugin --acknowledge-clawhub-risk`. If it is still refused, use the `npx @reefclaw/connect` fallback — it does not go through ClawHub.
|
|
84
|
+
|
|
83
85
|
Only fall back to `npx @reefclaw/connect` (below) when the chat install is unavailable.
|
|
84
86
|
|
|
85
87
|
## If you cannot run the installer (elevated exec disabled)
|
package/strategy/evaluator.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface StrategyConfig {
|
|
|
27
27
|
params: Record<string, number>;
|
|
28
28
|
};
|
|
29
29
|
targetRMultiples: number[];
|
|
30
|
+
/** Dedicated portfolio catalog rows are not declarative generic strategies. */
|
|
31
|
+
executionModel?: string;
|
|
30
32
|
}
|
|
31
33
|
export interface StrategyRow {
|
|
32
34
|
id: number;
|
|
@@ -55,6 +57,7 @@ export interface StrategyEvalResult {
|
|
|
55
57
|
regime_confidence: number;
|
|
56
58
|
summary: string;
|
|
57
59
|
}
|
|
60
|
+
export declare function isDeclarativeStrategyConfig(strategy: StrategyConfig): boolean;
|
|
58
61
|
/**
|
|
59
62
|
* Evaluate a strategy against a symbol's fact object.
|
|
60
63
|
* Returns null if the strategy is regime-gated and the symbol's regime doesn't match.
|
package/strategy/evaluator.js
CHANGED
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
// and optionally checks threshold-based conditions against indicator values.
|
|
5
5
|
import { CONDITION_EVALUATORS } from './condition-registry.js';
|
|
6
6
|
import { getCachedTradingParams } from '../trading-params-cache.js';
|
|
7
|
+
export function isDeclarativeStrategyConfig(strategy) {
|
|
8
|
+
return strategy.executionModel === undefined || strategy.executionModel === 'declarative';
|
|
9
|
+
}
|
|
7
10
|
// ─── Evaluator ──────────────────────────────────────────────────────────
|
|
8
11
|
/**
|
|
9
12
|
* Evaluate a strategy against a symbol's fact object.
|
|
10
13
|
* Returns null if the strategy is regime-gated and the symbol's regime doesn't match.
|
|
11
14
|
*/
|
|
12
15
|
export function evaluateStrategy(strategy, fact) {
|
|
16
|
+
if (!isDeclarativeStrategyConfig(strategy))
|
|
17
|
+
return null;
|
|
13
18
|
// Check regime gate
|
|
14
19
|
if (strategy.regimeWhitelist.length > 0) {
|
|
15
20
|
const matchesRegime = strategy.regimeWhitelist.some(r => r === fact.regime || r === 'ANY');
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
import type { RealizationRule } from '../simulator/types.js';
|
|
2
|
+
/** Minimum pre-entry stop distance in basis points (the SKILL v2.10.0
|
|
3
|
+
* discipline floor's fixed component; the ATR component is enforced by the
|
|
4
|
+
* live bracket path where ATR is authoritative). */
|
|
5
|
+
export declare const MIN_STOP_DISTANCE_BPS = 30;
|
|
6
|
+
export interface ProtectiveGeometryArgs {
|
|
7
|
+
side: 'buy' | 'sell';
|
|
8
|
+
/** Reference entry price: the limit price, or last trade for market orders. */
|
|
9
|
+
refPrice: number;
|
|
10
|
+
stopPrice?: number;
|
|
11
|
+
invalidationPrice?: number;
|
|
12
|
+
targetPrice?: number;
|
|
13
|
+
}
|
|
14
|
+
/** Validate stop / invalidation / target geometry against the entry side.
|
|
15
|
+
*
|
|
16
|
+
* Before this existed the wrong-side check ran ONLY on the wave9 path
|
|
17
|
+
* (create-order.ts) — a generic short entered with its stop BELOW entry
|
|
18
|
+
* passed validation and was closed by the stop-watcher seconds later
|
|
19
|
+
* (7 sub-5-minute kills on the 2026-07 HL soak, issue #200). Returns an
|
|
20
|
+
* error string, or null when the geometry is sound. Fields left undefined
|
|
21
|
+
* are not judged (stop-required policy stays with the caller). */
|
|
22
|
+
export declare function validateProtectiveGeometry(args: ProtectiveGeometryArgs): string | null;
|
|
2
23
|
/** The five legitimate reasons to manually close an otherwise-bracketed
|
|
3
24
|
* position (see SKILL.md v2.10.0 Position Management Discipline). Any other
|
|
4
25
|
* value is rejected. */
|
|
@@ -31,6 +52,8 @@ export interface ClosePositionArgs {
|
|
|
31
52
|
symbol: string;
|
|
32
53
|
reason?: string;
|
|
33
54
|
assessment?: CloseAssessment;
|
|
55
|
+
candidate_id?: string;
|
|
56
|
+
wave9_exit_token?: string;
|
|
34
57
|
}
|
|
35
58
|
export interface ValidationResult {
|
|
36
59
|
ok: boolean;
|
|
@@ -13,6 +13,64 @@
|
|
|
13
13
|
// live mode.
|
|
14
14
|
// Tested in: assessment-validation.test.ts.
|
|
15
15
|
import { normalizeBracketSymbol } from '../live/bracket-ledger.js';
|
|
16
|
+
// ---- protective geometry (create_order, issue #200) ----
|
|
17
|
+
/** Minimum pre-entry stop distance in basis points (the SKILL v2.10.0
|
|
18
|
+
* discipline floor's fixed component; the ATR component is enforced by the
|
|
19
|
+
* live bracket path where ATR is authoritative). */
|
|
20
|
+
export const MIN_STOP_DISTANCE_BPS = 30;
|
|
21
|
+
/** Validate stop / invalidation / target geometry against the entry side.
|
|
22
|
+
*
|
|
23
|
+
* Before this existed the wrong-side check ran ONLY on the wave9 path
|
|
24
|
+
* (create-order.ts) — a generic short entered with its stop BELOW entry
|
|
25
|
+
* passed validation and was closed by the stop-watcher seconds later
|
|
26
|
+
* (7 sub-5-minute kills on the 2026-07 HL soak, issue #200). Returns an
|
|
27
|
+
* error string, or null when the geometry is sound. Fields left undefined
|
|
28
|
+
* are not judged (stop-required policy stays with the caller). */
|
|
29
|
+
export function validateProtectiveGeometry(args) {
|
|
30
|
+
const { side, refPrice } = args;
|
|
31
|
+
if (!Number.isFinite(refPrice) || refPrice <= 0)
|
|
32
|
+
return null; // no reference — cannot judge
|
|
33
|
+
const dirWord = side === 'buy' ? 'long' : 'short';
|
|
34
|
+
if (args.stopPrice !== undefined) {
|
|
35
|
+
const stop = args.stopPrice;
|
|
36
|
+
if (!Number.isFinite(stop) || stop <= 0) {
|
|
37
|
+
return 'stopPrice must be a positive finite number.';
|
|
38
|
+
}
|
|
39
|
+
if ((side === 'buy' && stop >= refPrice) || (side === 'sell' && stop <= refPrice)) {
|
|
40
|
+
return (`stopPrice ${stop} is on the WRONG SIDE of entry ${refPrice} for a ${dirWord}: a ${dirWord}'s ` +
|
|
41
|
+
`protective stop must be ${side === 'buy' ? 'below' : 'above'} entry. A wrong-side stop is ` +
|
|
42
|
+
`instantly "breached" and the stop-watcher closes the position seconds after entry.`);
|
|
43
|
+
}
|
|
44
|
+
const distanceBps = (Math.abs(refPrice - stop) / refPrice) * 10_000;
|
|
45
|
+
if (distanceBps < MIN_STOP_DISTANCE_BPS) {
|
|
46
|
+
return (`stopPrice ${stop} is only ${distanceBps.toFixed(1)}bps from entry ${refPrice} — below the ` +
|
|
47
|
+
`${MIN_STOP_DISTANCE_BPS}bps discipline floor. Place the stop at a structural level ` +
|
|
48
|
+
`(≥ max(0.3×ATR, ${MIN_STOP_DISTANCE_BPS}bps) from entry).`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (args.invalidationPrice !== undefined) {
|
|
52
|
+
const inv = args.invalidationPrice;
|
|
53
|
+
if (!Number.isFinite(inv) || inv <= 0) {
|
|
54
|
+
return 'invalidation_price must be a positive finite number.';
|
|
55
|
+
}
|
|
56
|
+
if ((side === 'buy' && inv >= refPrice) || (side === 'sell' && inv <= refPrice)) {
|
|
57
|
+
return (`invalidation_price ${inv} is on the WRONG SIDE of entry ${refPrice} for a ${dirWord}: ` +
|
|
58
|
+
`invalidation must be ${side === 'buy' ? 'below' : 'above'} entry (it marks where the thesis ` +
|
|
59
|
+
`is WRONG, not where it profits). Fix the pinned plan before entering.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (args.targetPrice !== undefined) {
|
|
63
|
+
const target = args.targetPrice;
|
|
64
|
+
if (!Number.isFinite(target) || target <= 0) {
|
|
65
|
+
return 'target_price must be a positive finite number.';
|
|
66
|
+
}
|
|
67
|
+
if ((side === 'buy' && target <= refPrice) || (side === 'sell' && target >= refPrice)) {
|
|
68
|
+
return (`target_price ${target} is on the WRONG SIDE of entry ${refPrice} for a ${dirWord}: ` +
|
|
69
|
+
`the profit target must be ${side === 'buy' ? 'above' : 'below'} entry.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
16
74
|
// ---- close_position ----
|
|
17
75
|
/** The five legitimate reasons to manually close an otherwise-bracketed
|
|
18
76
|
* position (see SKILL.md v2.10.0 Position Management Discipline). Any other
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
2
|
+
import type { TradingOperationLock } from '../lifecycle/trading-operation-lock.js';
|
|
3
|
+
import type { Wave9LiveSymbolOwnershipCheck } from '../wave9/live-symbol-ownership.js';
|
|
2
4
|
interface AttachBracketsArgs {
|
|
3
5
|
symbol: string;
|
|
4
6
|
stop_price?: number;
|
|
@@ -26,9 +28,12 @@ interface AttachBracketsSuccess {
|
|
|
26
28
|
idempotent_no_op?: boolean;
|
|
27
29
|
note?: string;
|
|
28
30
|
}
|
|
29
|
-
|
|
31
|
+
interface AttachBracketsDeps {
|
|
30
32
|
adapter: IExchangeAdapter;
|
|
31
|
-
|
|
33
|
+
operationLock?: TradingOperationLock;
|
|
34
|
+
checkWave9LiveSymbolOwnership?: Wave9LiveSymbolOwnershipCheck;
|
|
35
|
+
}
|
|
36
|
+
export declare function attachBracketsTool(args: AttachBracketsArgs, deps: AttachBracketsDeps): Promise<AttachBracketsSuccess | {
|
|
32
37
|
error: string;
|
|
33
38
|
}>;
|
|
34
39
|
export {};
|
package/tools/attach-brackets.js
CHANGED
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
import { generateBracketId, buildBracketCid } from '../live/bracket-id.js';
|
|
23
23
|
import { validateStopDirection, validateTargetDirection } from '../live/bracket-params.js';
|
|
24
24
|
import { formatError, logger } from '../logger.js';
|
|
25
|
+
import { HyperliquidLiveAdapter } from '../venues/hyperliquid/hl-live-adapter.js';
|
|
26
|
+
import { isTerminalBracketState } from '../venues/hyperliquid/hl-bracket-coordinator.js';
|
|
27
|
+
import { buildHlBracketCloid } from '../venues/hyperliquid/hl-cloid.js';
|
|
25
28
|
const TAG = 'attach-brackets';
|
|
26
29
|
export async function attachBracketsTool(args, deps) {
|
|
27
30
|
// ---- 1. Argument validation ----
|
|
@@ -41,6 +44,48 @@ export async function attachBracketsTool(args, deps) {
|
|
|
41
44
|
if (!deps.adapter.isLive) {
|
|
42
45
|
return { error: 'attach_brackets is live-mode only (it places exchange-side STOP/TP orders). In paper mode, use modify_stop / modify_target to set or move the position\'s protective levels in place — the paper stop-watcher enforces metadata.stopPrice.' };
|
|
43
46
|
}
|
|
47
|
+
if (deps.checkWave9LiveSymbolOwnership && !deps.operationLock) {
|
|
48
|
+
return {
|
|
49
|
+
error: 'Live attach_brackets refused: the shared trading operation lock is required ' +
|
|
50
|
+
'while Wave 9 LIVE ownership checks are enabled.',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const attachLive = () => attachBracketsLive(args, deps);
|
|
54
|
+
try {
|
|
55
|
+
return deps.operationLock
|
|
56
|
+
? await deps.operationLock.withAccountLock(`generic-attach-brackets-final:${args.symbol}`, attachLive)
|
|
57
|
+
: await attachLive();
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
return { error: formatError(err) };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function attachBracketsLive(args, deps) {
|
|
64
|
+
if (deps.checkWave9LiveSymbolOwnership) {
|
|
65
|
+
let ownership;
|
|
66
|
+
try {
|
|
67
|
+
ownership = await deps.checkWave9LiveSymbolOwnership(args.symbol);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
return {
|
|
71
|
+
error: `attach_brackets refused at mutation boundary: Wave 9 LIVE ownership check failed ` +
|
|
72
|
+
`for ${args.symbol} (${formatError(err)}).`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (ownership.status === 'owned' || ownership.status === 'ambiguous') {
|
|
76
|
+
return {
|
|
77
|
+
error: `attach_brackets refused at mutation boundary: ${args.symbol} has ` +
|
|
78
|
+
`${ownership.status} Wave 9 LIVE ownership (${ownership.reason}). ` +
|
|
79
|
+
'Wave 9 protection is candidate-bound and cannot be replaced by the generic bracket tool.',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Venue dispatch (issue #209): the HL arm drives the HlBracketCoordinator —
|
|
84
|
+
// the Binance-shaped path below would silently no-op ("not enabled") on HL,
|
|
85
|
+
// which is exactly the recovery gap the boot guard existed for.
|
|
86
|
+
if (deps.adapter instanceof HyperliquidLiveAdapter) {
|
|
87
|
+
return attachBracketsHl(args, deps.adapter);
|
|
88
|
+
}
|
|
44
89
|
const live = deps.adapter;
|
|
45
90
|
const mgr = live.getBracketManager?.();
|
|
46
91
|
const ledger = live.getBracketLedger?.();
|
|
@@ -231,6 +276,162 @@ export async function attachBracketsTool(args, deps) {
|
|
|
231
276
|
attempts: attachResult.attempts,
|
|
232
277
|
};
|
|
233
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Hyperliquid recovery attach (issue #209). Same safety contract as the
|
|
281
|
+
* Binance path — null ≠ empty, positive-evidence-only destructive decisions,
|
|
282
|
+
* idempotent no-op on a live matching bracket — expressed against the
|
|
283
|
+
* HlBracketCoordinator. Classification is the d59e51b floor: a stored cid
|
|
284
|
+
* positively present in a successful getOpenOrders is 'live'; a NON-EMPTY set
|
|
285
|
+
* lacking every stored cid is 'stale'; an EMPTY set or a thrown fetch is
|
|
286
|
+
* 'unknown' (empty is never positive disconfirmation).
|
|
287
|
+
*/
|
|
288
|
+
async function attachBracketsHl(args, adapter) {
|
|
289
|
+
const coordinator = adapter.getHlBracketCoordinator();
|
|
290
|
+
const ledger = coordinator.getLedger();
|
|
291
|
+
// ---- Position must exist (null ≠ empty) ----
|
|
292
|
+
const positions = await adapter.getPositionsOrNull(args.symbol);
|
|
293
|
+
if (positions === null) {
|
|
294
|
+
return {
|
|
295
|
+
error: `Exchange data unavailable for ${args.symbol} (Hyperliquid fetch failed / rate-gated) — ` +
|
|
296
|
+
`position state UNKNOWN. NOT treating this as "no position"; bracket attach skipped ` +
|
|
297
|
+
`this cycle. Retry on next heartbeat.`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const base = args.symbol.split(':')[0];
|
|
301
|
+
const position = positions.find(p => p.symbol?.startsWith(base)) ?? positions[0];
|
|
302
|
+
const contracts = Math.abs(Number(position?.contracts ?? 0));
|
|
303
|
+
if (!position || contracts <= 0) {
|
|
304
|
+
return { error: `No open position for ${args.symbol}.` };
|
|
305
|
+
}
|
|
306
|
+
const entrySide = position.side === 'long' ? 'buy' : 'sell';
|
|
307
|
+
// ---- Direction sanity vs entry price ----
|
|
308
|
+
const refPrice = position.entryPrice > 0 ? position.entryPrice : await adapter.getLastPrice(position.symbol);
|
|
309
|
+
if (refPrice != null && refPrice > 0) {
|
|
310
|
+
if (args.stop_price !== undefined) {
|
|
311
|
+
const msg = validateStopDirection(entrySide, refPrice, args.stop_price);
|
|
312
|
+
if (msg)
|
|
313
|
+
return { error: msg };
|
|
314
|
+
}
|
|
315
|
+
if (args.target_price !== undefined) {
|
|
316
|
+
const msg = validateTargetDirection(entrySide, refPrice, args.target_price);
|
|
317
|
+
if (msg)
|
|
318
|
+
return { error: msg };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// ---- Existing ledger row: classify against the exchange ----
|
|
322
|
+
const existing = ledger.getBySymbol(position.symbol);
|
|
323
|
+
let clearedStaleLedgerRow = false;
|
|
324
|
+
if (existing && !isTerminalBracketState(existing.state)) {
|
|
325
|
+
let cls = 'unknown';
|
|
326
|
+
const hasCids = Boolean(existing.slCid || existing.tpCid);
|
|
327
|
+
if (hasCids) {
|
|
328
|
+
try {
|
|
329
|
+
const open = await adapter.getOpenOrders(position.symbol);
|
|
330
|
+
const liveCids = new Set(open.map(o => o.clientOrderId).filter(Boolean));
|
|
331
|
+
if ((existing.slCid && liveCids.has(existing.slCid)) || (existing.tpCid && liveCids.has(existing.tpCid))) {
|
|
332
|
+
cls = 'live';
|
|
333
|
+
}
|
|
334
|
+
else if (open.length > 0) {
|
|
335
|
+
cls = 'stale'; // non-empty set positively lacking our cids
|
|
336
|
+
} // empty set → 'unknown' (d59e51b: empty is never disconfirmation)
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
cls = 'unknown';
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (cls === 'unknown') {
|
|
343
|
+
return {
|
|
344
|
+
error: `Cannot confirm whether ${position.symbol} brackets are live on Hyperliquid ` +
|
|
345
|
+
`(open-orders unavailable, the set came back empty, or the row is in-flight with no ` +
|
|
346
|
+
`cids yet). NOT assuming protected and NOT reattaching this cycle. Retry next ` +
|
|
347
|
+
`heartbeat; if this persists treat the position as potentially NAKED and verify on ` +
|
|
348
|
+
`the exchange. Do NOT record a hold/protected review on the basis of this call.`,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
if (cls === 'live') {
|
|
352
|
+
if (pricesMatch(args.stop_price, existing.stopPrice)
|
|
353
|
+
&& pricesMatch(args.target_price, existing.targetPrice)) {
|
|
354
|
+
return {
|
|
355
|
+
ok: true,
|
|
356
|
+
symbol: position.symbol,
|
|
357
|
+
bracket_id: existing.bracketId,
|
|
358
|
+
entry_side: entrySide,
|
|
359
|
+
stop_price: existing.stopPrice,
|
|
360
|
+
target_price: existing.targetPrice,
|
|
361
|
+
sl_cid: existing.slCid,
|
|
362
|
+
tp_cid: existing.tpCid,
|
|
363
|
+
attach_latency_ms: 0,
|
|
364
|
+
cancelled_stale_bracket_orders: 0,
|
|
365
|
+
cleared_stale_ledger_row: false,
|
|
366
|
+
attempts: 0,
|
|
367
|
+
idempotent_no_op: true,
|
|
368
|
+
note: `Bracket already ${existing.state} on exchange with matching prices — no-op.`,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
error: `Bracket already ${existing.state} for ${position.symbol} at ` +
|
|
373
|
+
`stop=${existing.stopPrice ?? 'none'} target=${existing.targetPrice ?? 'none'}. ` +
|
|
374
|
+
`Use modify_stop / modify_target to change levels, or close_position to exit.`,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
logger.warn(TAG, `HL ledger row for ${position.symbol} is stale (state=${existing.state}; stored cloids not ` +
|
|
378
|
+
`on exchange, non-empty order set) — forcing terminal to re-attach`);
|
|
379
|
+
ledger.markState(position.symbol, 'cancelled', {
|
|
380
|
+
closeReason: 'cancelled_auto',
|
|
381
|
+
lastError: 'stale_ledger_detected_by_attach_brackets',
|
|
382
|
+
});
|
|
383
|
+
clearedStaleLedgerRow = true;
|
|
384
|
+
}
|
|
385
|
+
// ---- Orphan cleanup (best-effort; protection beats hygiene) ----
|
|
386
|
+
let cancelledOrphans = 0;
|
|
387
|
+
try {
|
|
388
|
+
cancelledOrphans = await adapter.cancelSymbolBracketLegs(position.symbol);
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
logger.warn(TAG, `HL orphan cleanup failed for ${position.symbol}: ${formatError(err)}`);
|
|
392
|
+
}
|
|
393
|
+
// ---- Register + attach ----
|
|
394
|
+
const bracketId = generateBracketId();
|
|
395
|
+
try {
|
|
396
|
+
coordinator.registerEntry({
|
|
397
|
+
symbol: position.symbol,
|
|
398
|
+
side: entrySide,
|
|
399
|
+
stopPrice: args.stop_price,
|
|
400
|
+
targetPrice: args.target_price,
|
|
401
|
+
}, bracketId, buildHlBracketCloid(bracketId, 'entry'));
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
return { error: `registerEntry failed: ${formatError(err)}` };
|
|
405
|
+
}
|
|
406
|
+
let attachResult;
|
|
407
|
+
try {
|
|
408
|
+
attachResult = await coordinator.attachOnFill(position.symbol, contracts);
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
return { error: `attach failed: ${formatError(err)}` };
|
|
412
|
+
}
|
|
413
|
+
if (!attachResult.ok) {
|
|
414
|
+
return {
|
|
415
|
+
error: `Bracket attach failed after ${attachResult.attempts} attempt(s): ` +
|
|
416
|
+
`${attachResult.error ?? 'unknown error'}. Position remains open. Try again with ` +
|
|
417
|
+
`adjusted levels, or close_position to exit.`,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
ok: true,
|
|
422
|
+
symbol: position.symbol,
|
|
423
|
+
bracket_id: bracketId,
|
|
424
|
+
entry_side: entrySide,
|
|
425
|
+
stop_price: args.stop_price,
|
|
426
|
+
target_price: args.target_price,
|
|
427
|
+
sl_cid: attachResult.slCid,
|
|
428
|
+
tp_cid: attachResult.tpCid,
|
|
429
|
+
attach_latency_ms: attachResult.latencyMs,
|
|
430
|
+
cancelled_stale_bracket_orders: cancelledOrphans,
|
|
431
|
+
cleared_stale_ledger_row: clearedStaleLedgerRow,
|
|
432
|
+
attempts: attachResult.attempts,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
234
435
|
// Non-terminal bracket states — a row in any of these means brackets are
|
|
235
436
|
// either in-flight or fully live; attach_brackets refuses to clobber them
|
|
236
437
|
// UNLESS the ledger cids don't match any live exchange order (stale row).
|