V16: Rules 22-25 (trust calibration, error-feedback, attribution gate, backtest integrity) + land prior uncommitted Rule 15 two-phase exit rewrite
This commit is contained in:
@@ -483,20 +483,29 @@ correct but structurally insufficient — exits are not happening fast enough
|
|||||||
to sustain the reset.
|
to sustain the reset.
|
||||||
|
|
||||||
**Fix:** Do NOT wait for another 4h cycle. Classify as INTERNAL structural
|
**Fix:** Do NOT wait for another 4h cycle. Classify as INTERNAL structural
|
||||||
deadlock. Generate exit orders for the 2 highest-beta positions (sourced
|
deadlock. Generate exit orders in two phases:
|
||||||
from portfolio_beta.py) that meet **relaxed** exit criteria: held ≥1 day
|
|
||||||
(instead of 3) OR profit ≥1% (instead of 5%). If no positions meet even
|
|
||||||
relaxed criteria, go to 50% cash — sell the 2 highest-beta positions at
|
|
||||||
market regardless of profit. The breaker is self-correct; the exits are not.
|
|
||||||
|
|
||||||
**Verify:** At least 2 exit orders submitted with status `accepted` or
|
*Phase 1 (Relaxed exits):* Select up to 2 positions that meet **relaxed**
|
||||||
`filled`. Re-compute position count — < 10/10.
|
exit criteria: held ≥1 day (instead of the adaptive STALE_EXIT_DAYS,
|
||||||
|
default 5) OR profit ≥1% (instead of the adaptive PROFIT_TAKE_PCT, default
|
||||||
|
5%). Among qualifying positions, exit the 2 with the **largest market
|
||||||
|
value** first — this maximizes deadlock-breaking impact by freeing the most
|
||||||
|
capacity.
|
||||||
|
|
||||||
**Escalate:** After 1 cycle — breaker cycling is a structural deadlock. The
|
*Phase 2 (Emergency — 50% cash):* If no positions meet even relaxed
|
||||||
contract has exhausted its self-resolution surface. Escalate with: (a) the
|
criteria, go to 50% cash — sell the 2 **highest-beta** positions (sourced
|
||||||
breaker history (IDs and timestamps of the last 3), (b) all current positions
|
from `compute_position_beta()` in `scripts/portfolio_adaptation.py`) at
|
||||||
with age + beta + unrealized P&L, (c) which exit conditions each position
|
market regardless of profit, with market value as tiebreaker. The breaker
|
||||||
failed, and (d) the specific relaxed criteria applied.
|
is self-correct; the exits are not.
|
||||||
|
|
||||||
|
**Verify:** At least 2 exit orders executed with status `executed`.
|
||||||
|
Re-compute position count — must be < MAX_EQUITY_POSITIONS (default 5).
|
||||||
|
|
||||||
|
**Escalate:** Immediately — breaker cycling is a structural deadlock and
|
||||||
|
the contract has exhausted its self-resolution surface. Escalate with:
|
||||||
|
(a) the breaker history (IDs and timestamps of the last 3), (b) all current
|
||||||
|
positions with age + beta + unrealized P&L, (c) which exit conditions each
|
||||||
|
position failed, and (d) the specific relaxed criteria applied.
|
||||||
|
|
||||||
#### Rule 16: Data Pipeline Integrity — Stale Local Tables
|
#### Rule 16: Data Pipeline Integrity — Stale Local Tables
|
||||||
**Detect:** Any OSS sub-component or performance-summary field returns 0, None,
|
**Detect:** Any OSS sub-component or performance-summary field returns 0, None,
|
||||||
@@ -635,6 +644,39 @@ coverage gap counts trend downward over rolling 90-day window.
|
|||||||
cycles — the system's data universe is shrinking, not expanding. Escalate
|
cycles — the system's data universe is shrinking, not expanding. Escalate
|
||||||
with per-source coverage breadth trend.
|
with per-source coverage breadth trend.
|
||||||
|
|
||||||
|
#### Rule 20: Duplicate-Symbol Consensus Prevention (Position-Aware Sophie)
|
||||||
|
|
||||||
|
**Detect:** Sophie's signal aggregation produces `action=execute` BUY signals
|
||||||
|
for tickers that are already held in the live portfolio. Ross Gate 6 will
|
||||||
|
invariably block these as duplicate-symbol — creating a perpetual cycle of:
|
||||||
|
generate BUY → block → gap analysis noise. This is structurally wasteful
|
||||||
|
and generates false-positive "GAP" verdicts that obscure genuine execution
|
||||||
|
failures.
|
||||||
|
|
||||||
|
**Prevention (Sophie pre-filter, `sophie.py:_get_held_symbols()`):**
|
||||||
|
Before processing any signals, Sophie queries live broker adapters
|
||||||
|
(RobinhoodCrypto.get_positions() for crypto, AlpacaLiveAdapter.get_positions()
|
||||||
|
for equity) and builds a set of currently-held ticker symbols. For every
|
||||||
|
incoming bullish signal where the ticker is already held, Sophie SKIPs the
|
||||||
|
signal at the aggregation layer — the consensus never reaches Ross.
|
||||||
|
Bearish/sell signals on held positions are unaffected (valid exits).
|
||||||
|
|
||||||
|
**Detection (gap_analysis.py, passive):**
|
||||||
|
`gap_analysis.py` now cross-references `consensus.last_skip_reason` and
|
||||||
|
`broker_orders` to distinguish "blocked-valid" from "genuinely missed."
|
||||||
|
Consensus with `last_skip_reason` matching `duplicate-symbol`, `Already holding`,
|
||||||
|
`no_position`, `circuit_breaker`, or `asset_not_found` is classified as
|
||||||
|
`blocked_valid` — NOT a gap. Consensus with any `broker_orders` row is also
|
||||||
|
excluded from gap counting.
|
||||||
|
|
||||||
|
**Verify:** Gap analysis returns `Verdict: CLEAN` when all unexecuted consensus
|
||||||
|
are blocked-valid. Sophie logs `⏭️ ALREADY-HELD` for skipped bullish signals.
|
||||||
|
Zero duplicate-symbol blocks appear in Ross output for Sophie-generated consensus.
|
||||||
|
|
||||||
|
**Escalate:** Never — blocked-valid is expected behavior, not a failure.
|
||||||
|
The pre-filter eliminates the cycle at source; gap_analysis correctly
|
||||||
|
classifies any remaining blocked-valid consensus.
|
||||||
|
|
||||||
### Invariants
|
### Invariants
|
||||||
|
|
||||||
- **P-001 — Paper trading is dead code.** The paper trading adapter
|
- **P-001 — Paper trading is dead code.** The paper trading adapter
|
||||||
@@ -898,17 +940,20 @@ with per-source coverage breadth trend.
|
|||||||
cycle.
|
cycle.
|
||||||
|
|
||||||
**Tiered, fail-closed health — defined relative to what is achievable.** The
|
**Tiered, fail-closed health — defined relative to what is achievable.** The
|
||||||
adaptation controller SHALL read a health tier derived from clean-snapshot
|
adaptation controller SHALL read a health tier derived from the count of
|
||||||
availability, where the clean-snapshot floor `MIN_CLEAN` ramps with system
|
DISTINCT CALENDAR DAYS carrying at least one clean snapshot (not raw clean
|
||||||
age (`clamp(age_days × 0.5, 3, 8)`) so a young or freshly-healed system is
|
row count — multiple same-day snapshots SHALL NOT inflate the tier or
|
||||||
|
false-promote the controller to HEALTHY on a single day of data), where the
|
||||||
|
clean-day floor `MIN_CLEAN` ramps with system age
|
||||||
|
(`clamp(age_days × 0.5, 3, 8)`) so a young or freshly-healed system is
|
||||||
never starved into permanent inaction:
|
never starved into permanent inaction:
|
||||||
- **HEALTHY** (clean ≥ `MIN_CLEAN`): full loop — propose AND commit to
|
- **HEALTHY** (clean_days ≥ `MIN_CLEAN`): full loop — propose AND commit to
|
||||||
`.env.prod`.
|
`.env.prod`.
|
||||||
- **DEGRADED** (1 ≤ clean < `MIN_CLEAN`): evaluate + auto-unwind run
|
- **DEGRADED** (1 ≤ clean_days < `MIN_CLEAN`): evaluate + auto-unwind run
|
||||||
(bookkeeping / immune system), the proposal is computed and REPORTED, but
|
(bookkeeping / immune system), the proposal is computed and REPORTED, but
|
||||||
SHALL NOT be committed to `.env.prod`.
|
SHALL NOT be committed to `.env.prod`.
|
||||||
- **CRITICAL** (clean = 0): hold all adaptation; only bookkeeping runs; emit
|
- **CRITICAL** (clean_days = 0): hold all adaptation; only bookkeeping runs;
|
||||||
a loud alert.
|
emit a loud alert.
|
||||||
|
|
||||||
**Self-healing by natural refill.** Because poisoned history is quarantined
|
**Self-healing by natural refill.** Because poisoned history is quarantined
|
||||||
rather than resurrected, the OSS window refills with genuine post-inception
|
rather than resurrected, the OSS window refills with genuine post-inception
|
||||||
@@ -1226,3 +1271,118 @@ the system-health contract detects, adapts, and self-heals.
|
|||||||
- `cli:python3`: required for running auditor_query.py and track scripts
|
- `cli:python3`: required for running auditor_query.py and track scripts
|
||||||
- `cli:sqlite3`: required for direct database queries when auditor is unavailable
|
- `cli:sqlite3`: required for direct database queries when auditor is unavailable
|
||||||
- `cli:curl`: required for DNS reachability tests and API health checks
|
- `cli:curl`: required for DNS reachability tests and API health checks
|
||||||
|
---
|
||||||
|
|
||||||
|
## V17.2 Additions — Council-Verified (2026-07-15)
|
||||||
|
|
||||||
|
### Pre-Sophie Completeness Gate (H6)
|
||||||
|
|
||||||
|
The pipeline SHALL verify that all 9 signal tracks have produced data (via
|
||||||
|
`track_status.last_run_at` timestamps) before running Sophie consensus. If any
|
||||||
|
track is stale, the pipeline SHALL emit a WARNING but SHALL NOT block — partial
|
||||||
|
data is better than no consensus. This gate prevents Sophie from running on
|
||||||
|
empty or incomplete signal data after a cascade of track failures.
|
||||||
|
|
||||||
|
**Implementation:** `consensus/run_pipeline.py:213-245`
|
||||||
|
|
||||||
|
### Market Holiday Calendar (L2)
|
||||||
|
|
||||||
|
The system SHALL maintain a zero-dependency static calendar of NYSE market
|
||||||
|
holidays (`consensus/market_holidays.json`) covering the current and next two
|
||||||
|
calendar years. All signal tracks SHALL check `is_market_open()` before
|
||||||
|
initiating yfinance API calls on weekdays. The holiday data SHALL be refreshed
|
||||||
|
quarterly by a council-verified cron job with explicit stale-holiday detection.
|
||||||
|
|
||||||
|
**Implementation:** `consensus/market_holidays.json`, `consensus/market_calendar.py`
|
||||||
|
**Cron:** `b53521c1e0cd` (quarterly validation)
|
||||||
|
|
||||||
|
### Cron Health Monitor (C2 + W1)
|
||||||
|
|
||||||
|
The system SHALL maintain a standalone heartbeat cron job that checks ALL
|
||||||
|
trading pipeline, watchdog, and infrastructure jobs for staleness every 15
|
||||||
|
minutes. If the health monitor itself stops reporting, its absence SHALL be
|
||||||
|
detectable by the config hash watchdog. The monitor covers 22 jobs across three
|
||||||
|
categories: trading (11), watchdog (3), and infrastructure (8).
|
||||||
|
|
||||||
|
**Implementation:** `~/.hermes/scripts/cron_health_check.py`
|
||||||
|
**Cron:** `d9adf1bf25a8` (every 15 min)
|
||||||
|
|
||||||
|
### Validation Promotion Pipeline (C1)
|
||||||
|
|
||||||
|
The validation buffer SHALL permanently promote the Rico RSI track to
|
||||||
|
PRE_PROMOTED status (bypassing the validation gate), reflecting its proven
|
||||||
|
signal quality. Luna and Iris SHALL require 5 and 3 consecutive time-based
|
||||||
|
cycles respectively before promotion. This prevents validation starvation where
|
||||||
|
new tracks accumulate zero-signal cycles and never exit the validation phase.
|
||||||
|
|
||||||
|
**Implementation:** `consensus/validation_buffer.py:33-41`
|
||||||
|
|
||||||
|
### Discovery Deadlock Skip (C3)
|
||||||
|
|
||||||
|
When Ross discovery encounters 3+ consecutive timeout cycles against the Alpaca
|
||||||
|
API, the pipeline SHALL skip discovery for that cycle and use the frozen ticker
|
||||||
|
universe. This prevents the pipeline from stalling indefinitely when the Alpaca
|
||||||
|
discovery endpoint is unavailable. The timeout SHALL be 180 seconds.
|
||||||
|
|
||||||
|
**Implementation:** `consensus/run_pipeline.py:89-150`
|
||||||
|
|
||||||
|
### Ticker Denylist (C5)
|
||||||
|
|
||||||
|
The system SHALL maintain a shared denylist of non-tradable ticker symbols
|
||||||
|
(`PORTFOLIO`, `REGIME`, `MACRO`) in the data validation module. All signal
|
||||||
|
tracks that fetch per-ticker OHLCV data SHALL filter their ticker universe
|
||||||
|
against this denylist before making yfinance API calls. This prevents spurious
|
||||||
|
OHLCV validation failures from contaminating track status.
|
||||||
|
|
||||||
|
**Implementation:** `consensus/data_validation.py:23`
|
||||||
|
|
||||||
|
### Infrastructure Monitoring (F7)
|
||||||
|
|
||||||
|
The cron health monitor SHALL be expanded to include EOD Digest, Adaptation
|
||||||
|
Cycle, Reconcile Watchdog, Health Monitor, DB Trigger Watchdog, Jobs.json
|
||||||
|
Watchdog, Config Hash Watchdog, and Market Holiday Calendar. Per-job staleness
|
||||||
|
thresholds SHALL account for weekend gaps and trading-hours-only schedules.
|
||||||
|
|
||||||
|
**Implementation:** `~/.hermes/scripts/cron_health_check.py:45-56`
|
||||||
|
|
||||||
|
#### Rule 22: Calibrated Trust in Consensus (R3/R4)
|
||||||
|
**Detect:** A track's rolling 60-day Brier score exceeds 0.25 (worse than coin-flip
|
||||||
|
calibration) with n_obs ≥ 20.
|
||||||
|
**Fix:** Trust weight auto-discounts via `track_trust` (mapping in
|
||||||
|
scripts/calibrate_tracks.py). Consensus uses weighted_score ≥ 1.6 instead of raw
|
||||||
|
track count. Trust floor 0.30 — tracks are discounted, never silenced.
|
||||||
|
**Verify:** `SELECT track, trust, brier, n_obs FROM track_trust` shows fresh rows
|
||||||
|
(computed_at within 24h on trading days).
|
||||||
|
**Escalate:** If ALL tracks sit at trust floor for 5 consecutive cycles — the
|
||||||
|
signal population itself is degrading; escalate with per-track Brier history.
|
||||||
|
|
||||||
|
#### Rule 23: Error-Feedback Suppression (R5)
|
||||||
|
**Detect:** A track produces 2+ consecutive wrong outcomes on the same
|
||||||
|
ticker+direction within 10 days (signal_outcomes ledger).
|
||||||
|
**Fix:** Sophie drops that track from that ticker's group for the current cycle
|
||||||
|
(bypass tracks janet/eddie exempt). Suppression lifts automatically when the
|
||||||
|
streak breaks.
|
||||||
|
**Verify:** Pipeline logs show `MEM-SUPPRESS` lines only when ledger streaks ≥ 2.
|
||||||
|
**Escalate:** If one track is suppressed on >50% of its signals for 5 cycles —
|
||||||
|
the track is systematically miscalibrated; escalate for track review.
|
||||||
|
|
||||||
|
#### Rule 24: Edge Claims Require Attribution (R1/R8)
|
||||||
|
**Detect:** Any report (OSS, health monitor, EOD digest) claiming outperformance
|
||||||
|
vs SPY.
|
||||||
|
**Fix:** The claim must cite scripts/attribution.py output and is only valid when
|
||||||
|
`selection_alpha_significant=true` (n_days ≥ 60 AND |t-stat| ≥ 2). Otherwise the
|
||||||
|
report must read "performance attribution insufficient — return may be beta/noise."
|
||||||
|
**Verify:** Health Monitor output includes the attribution verdict line verbatim.
|
||||||
|
**Escalate:** If attribution shows significant NEGATIVE alpha (t ≤ −2) for 20+
|
||||||
|
trading days — the strategy destroys value vs beta; escalate with full
|
||||||
|
attribution JSON.
|
||||||
|
|
||||||
|
#### Rule 25: Backtest Integrity (R1/R2)
|
||||||
|
**Detect:** Any parameter change justified by backtest results.
|
||||||
|
**Fix:** The justification must name the walk-forward windows used (train vs test
|
||||||
|
from scripts/backtest.py::walk_forward_windows). Same-window tuning is invalid
|
||||||
|
and must be rejected on review. Frictions of 30 bps equity / 50 bps crypto per
|
||||||
|
side must have been charged.
|
||||||
|
**Verify:** Backtest JSON contains `frictions_bps` and the report cites
|
||||||
|
train/test window pairs.
|
||||||
|
**Escalate:** Never to automation — integrity violations go to human review.
|
||||||
|
|||||||
Reference in New Issue
Block a user