Files
stonks-oracle/.kiro/specs/math-core-v3-engine/tasks.md
T

27 KiB
Raw Blame History

Implementation Plan: Math Core v3 Engine

Overview

Incremental upgrade of the Stonks Oracle signal processing engine from dual-mode heuristic/probabilistic to the v3 Calibrated Evidence Engine. Implementation follows the v3 math doc section 20 order: EvidenceUnit → reliability → LLR → clustering → posterior → contradiction → confidence → macro/competitive layers → EV gate → Kelly sizing → stop-defined heat → retire heuristic scoring. All v3 code operates behind the v3_engine_enabled feature flag with heuristic fallback.

Tasks

  • 1. EvidenceUnit and LLR conversion behind feature flag

    • 1.1 Implement EvidenceUnit dataclass and normalization functions in services/aggregation/scoring.py

      • Add frozen dataclass EvidenceUnit with all 16 fields (symbol, layer, event_type, source_id, source_group, timestamp, horizon, direction, sentiment_strength, impact, extraction_conf, source_cred, novelty, event_base_rate, cluster_id)
      • Implement normalize_company_signal(), normalize_macro_signal(), normalize_competitive_signal()
      • Validate required fields (symbol, timestamp, source_id) — reject with warning on missing
      • Substitute 0.5 for missing optional numeric fields
      • Map direction from sentiment/impact_direction strings (+1/-1/0)
      • Assign event_base_rate from EVENT_TYPE_BASE_RATES lookup (default 0.10)
      • Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8
    • 1.2 Implement calibrated reliability pipeline (compute_v3_reliability) in services/aggregation/scoring.py

      • Add SourceStats dataclass (source_id, hits, misses, alpha_0=3, beta_0=3)
      • Add ReliabilityComponents dataclass (q_ext, q_source, q_recency, q_uniqueness, q_i)
      • Implement q_ext = sigmoid(8.0 × (extraction_conf - 0.55))
      • Implement q_source via Bayesian shrinkage: E[theta_s] = (alpha_0 + hits) / (alpha_0 + beta_0 + hits + misses), then clamp((E - 0.50) / 0.35, 0, 1)
      • Implement q_recency = 2^(-age_hours / tau_adaptive) with adaptive half-life formula
      • Implement q_uniqueness = clamp(0.5 + 0.5 × novelty, 0.5, 1.0) × (1 / sqrt(1 + dup_count))
      • Implement q_i = clamp(q_ext × q_source × source_cred × q_recency × q_uniqueness, 0, 1)
      • Apply floor of 0.01 on q_recency only for explainability display
      • Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9
    • 1.3 Implement LLR conversion (compute_llr) in services/aggregation/scoring.py

      • Compute p_correct = clamp(0.50 + 0.35 × q_i × impact × sentiment_strength, 0.501, 0.85)
      • Compute LLR_i = direction × ln(p_correct / (1 - p_correct))
      • Return 0.0 for neutral signals (direction == 0)
      • Ensure LLR sign always matches direction for directional signals
      • Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6
    • 1.4 Add feature flag routing in services/aggregation/worker.py

      • Read v3_engine_enabled from risk_configs table at start of each aggregation cycle
      • Route to v3 pipeline when True, heuristic when False
      • Default to heuristic mode if DB read fails (log warning)
      • Wrap v3 pipeline in try/except — fall back to heuristic on unhandled error (log ERROR with traceback)
      • Store pipeline_mode field ("v3" or "heuristic") in output metadata
      • Requirements: 19.1, 19.2, 19.3, 19.4, 19.5, 19.6
    • 1.5 Write property tests for EvidenceUnit, reliability, and LLR (tests/test_pbt_v3_reliability.py)

      • Property 1: Reliability q_i is bounded in [0, 1]
      • Property 2: p_correct is bounded in [0.501, 0.85]
      • Property 3: LLR sign matches direction and magnitude is bounded
      • Property 4: Neutral signals produce zero LLR
      • Property 21: EvidenceUnit normalization preserves field ranges
      • Validates: Requirements 1.11.8, 2.12.9, 3.13.6, 21.121.3
      • Use Hypothesis with @settings(max_examples=100)
    • 1.6 Write unit tests for EvidenceUnit normalization and LLR conversion (tests/test_v3_evidence_unit.py)

      • Test company signal → EvidenceUnit with correct field mapping
      • Test macro signal → EvidenceUnit with correct horizon mapping (short_term→7d, medium_term→30d, long_term→90d)
      • Test competitive signal → EvidenceUnit with correct direction mapping
      • Test missing required fields → rejection with warning
      • Test missing optional fields → default 0.5 substitution
      • Test known inputs through full q_i pipeline → expected outputs
      • Test LLR boundary cases: p_correct at clamp boundaries
      • Requirements: 1.11.8, 2.12.9, 3.13.6
  • 2. Evidence clustering and n_eff

    • 2.1 Implement correlation-aware clustering in services/aggregation/worker.py

      • Add EvidenceCluster dataclass (cluster_id, units, llrs, n_eff, cluster_llr)
      • Implement cluster_evidence() — group EvidenceUnits by (symbol, horizon, event_type, source_group, time_bucket)
      • Compute cluster_id as hash of grouping key
      • Define time_bucket resolution per horizon (intraday=1h, 1d=4h, 7d=24h, 30d=72h, 90d=168h)
      • Requirements: 4.1
    • 2.2 Implement n_eff computation in services/aggregation/worker.py

      • Implement compute_n_eff(llrs, correlations) using formula: (sum w_i)² / (sum w_i² + 2 × sum_{i<j} rho_ij × w_i × w_j)
      • Use default pairwise correlations: same wire=0.80, same event diff publisher=0.50, same theme diff event=0.25, independent=0.00
      • Guard against division by zero (denominator → return n_eff=1.0)
      • Requirements: 4.2, 4.3
    • 2.3 Implement cluster LLR computation in services/aggregation/worker.py

      • Implement compute_cluster_llr(llrs, n_eff) = clamp(weighted_mean(LLR_i, |LLR_i|) × sqrt(n_eff), -2.5, 2.5)
      • Handle single-signal clusters (LLR_c = LLR_i clamped)
      • Handle all-zero LLR clusters (cluster_llr = 0.0)
      • Requirements: 4.4, 4.5
    • 2.4 Write property tests for clustering (tests/test_pbt_v3_clustering.py)

      • Property 5: Effective evidence count n_eff is bounded by cluster size
      • Property 6: Cluster LLR is clamped to [-2.5, 2.5]
      • Validates: Requirements 4.2, 4.3, 4.4, 4.5, 21.4
      • Use Hypothesis with @settings(max_examples=100)
    • 2.5 Write unit tests for clustering (tests/test_v3_clustering.py)

      • Test 3 identical articles from same source → n_eff < 3
      • Test 3 independent articles → n_eff ≈ 3
      • Test single signal cluster → n_eff = 1.0
      • Test cluster LLR clamp at ±2.5
      • Test grouping by correct key dimensions
      • Requirements: 4.14.5
  • 3. Checkpoint - Verify foundation layer

    • Ensure all tests pass for EvidenceUnit, reliability, LLR, and clustering.
    • Ensure all tests pass, ask the user if questions arise.
  • 4. Replace trend assembly with posterior P_up

    • 4.1 Implement regime detection v3 in services/aggregation/regime.py

      • Add V3RegimeClassification dataclass (regime, trend_z, vol_ratio, evidence_multiplier, confidence_multiplier, phi_decay, atr_multiplier)
      • Implement classify_regime_v3(closing_prices, daily_returns, atr_20) using ATR-normalized trend_z = (EMA_20 - EMA_100) / ATR_20
      • Compute vol_ratio = sigma_20 / sigma_100
      • Classification rules in priority: panic (vol_ratio > 1.5 OR |trend_z| > 2.5), trend_following (|trend_z| >= 0.75 AND vol_ratio < 1.3), mean_reversion (|trend_z| < 0.50 AND vol_ratio < 1.0), uncertainty (default)
      • Assign regime parameters: gamma, confidence_mult, phi, ATR_mult, min_edge
      • Default to uncertainty when data insufficient
      • Requirements: 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7, 6.8
    • 4.2 Implement posterior assembly via log-odds in services/aggregation/bayesian.py

      • Add V3Posterior dataclass (p_up, p_down, log_odds, strength, direction, n_eff_total, regime)
      • Implement compute_v3_posterior(clusters, regime, p_prior=0.50)
      • Compute logit(P_up) = logit(P_prior) + sum(gamma_regime × LLR_c)
      • Compute P_up = sigmoid(log_odds), clamp to [1e-10, 1 - 1e-10]
      • Compute strength = abs(2 × P_up - 1)
      • Classify direction using regime-specific thresholds (panic: 0.68/0.32, trend_following: 0.60/0.40, mean_reversion: 0.63/0.37, uncertainty: 0.65/0.35)
      • Load calibrated prior from risk_configs if available (clamp to [0.40, 0.60]), fall back to 0.50 on error
      • Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7
    • 4.3 Write property tests for posterior and regime (tests/test_pbt_v3_posterior.py)

      • Property 7: Posterior P_up is in open interval (0, 1)
      • Property 20: Regime classification is exhaustive and deterministic
      • Validates: Requirements 5.3, 6.26.5, 21.5
      • Use Hypothesis with @settings(max_examples=100)
    • 4.4 Write unit tests for posterior assembly (tests/test_v3_posterior.py)

      • Test empty clusters → P_up = 0.50 (neutral prior)
      • Test all bullish clusters → P_up > 0.50
      • Test regime direction thresholds at boundary values
      • Test prior clamp [0.40, 0.60]
      • Test regime classification with known inputs
      • Requirements: 5.15.7, 6.16.8
  • 5. Replace contradiction with LLR entropy

    • 5.1 Implement LLR entropy contradiction in services/aggregation/contradiction.py

      • Add compute_v3_contradiction(clusters: list[EvidenceCluster]) -> float
      • Compute E_pos = sum(max(LLR_c, 0)), E_neg = sum(max(-LLR_c, 0)), E_total = E_pos + E_neg
      • When E_total == 0 → return 0.0
      • When only one direction exists (E_pos == 0 or E_neg == 0) → return 0.0
      • Compute f_pos = E_pos / E_total, f_neg = E_neg / E_total
      • Compute H_conflict = -f_pos × log2(f_pos) - f_neg × log2(f_neg), treating 0×log2(0) = 0
      • Compute volume_factor = 1 - exp(-E_total / 3.0)
      • Return H_conflict × volume_factor, bounded in [0.0, 1.0]
      • Requirements: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7
    • 5.2 Write property tests for contradiction (tests/test_pbt_v3_posterior.py)

      • Property 8: Contradiction is zero when evidence is unidirectional
      • Property 9: Contradiction score is bounded in [0, 1]
      • Validates: Requirements 7.6, 7.7, 21.7
      • Use Hypothesis with @settings(max_examples=100)
    • 5.3 Write unit tests for LLR entropy contradiction (tests/test_v3_contradiction.py)

      • Test all bullish clusters → contradiction = 0.0
      • Test equal split of evidence → high contradiction near 1.0
      • Test E_total = 0 → contradiction = 0.0
      • Test volume_factor growth: small evidence mass → suppressed score
      • Requirements: 7.17.7
  • 6. Replace confidence formula

    • 6.1 Implement multiplicative confidence v3 in services/aggregation/worker.py

      • Add compute_v3_confidence(n_eff_total, q_values, llrs, strength, regime_confidence_mult, contradiction, data_quality) -> float
      • Compute C_evidence = 1 - exp(-n_eff_total / 5.0)
      • Compute C_quality = weighted_mean(q_i, weight=|LLR_i|)
      • Compute confidence = clamp(C_evidence × sqrt(C_quality) × sqrt(max(strength, 0.05)) × regime_confidence_mult × (1 - contradiction) × data_quality, 0, 1)
      • Requirements: 8.1, 8.2, 8.3, 8.4, 8.5
    • 6.2 Implement data quality v3 in services/aggregation/worker.py

      • Add compute_v3_data_quality(units, extraction_failure_rate, age_newest_hours, n_source_types) -> float
      • Q_parse = 1 - extraction_failure_rate
      • Q_conf = weighted_mean(extraction_conf_i, weight=impact_i)
      • Q_fresh = exp(-age_newest_hours / 168)
      • Q_coverage = 1 - exp(-N_valid / 5)
      • Q_diversity = min(1, log2(1 + N_source_types) / log2(4))
      • data_quality_score = clamp(Q_parse × sqrt(Q_conf) × Q_fresh × Q_coverage × Q_diversity, 0, 1)
      • Force informational mode when data_quality < 0.50 OR N_valid < 2 OR Q_parse < 0.50
      • Force informational when only macro/competitive evidence unless macro_only_enabled
      • Requirements: 17.1, 17.2, 17.3, 17.4, 17.5, 17.6, 17.7, 17.8
    • 6.3 Write property tests for confidence and data quality (tests/test_pbt_v3_posterior.py)

      • Property 10: Multiplicative confidence is bounded in [0, 1] and suppressed by weak dimensions
      • Property 17: Data quality score is bounded in [0, 1]
      • Validates: Requirements 8.3, 8.5, 17.6, 21.6
      • Use Hypothesis with @settings(max_examples=100)
    • 6.4 Write unit tests for confidence and data quality (tests/test_v3_confidence.py)

      • Test zero data quality → near-zero confidence
      • Test full contradiction (1.0) → zero confidence
      • Test low n_eff → suppressed C_evidence
      • Test data quality boundary cases (Q_parse < 0.50 → informational)
      • Requirements: 8.18.5, 17.117.8
  • 7. Checkpoint - Verify core pipeline

    • Ensure all tests pass for posterior, contradiction, confidence, and data quality.
    • Ensure all tests pass, ask the user if questions arise.
  • 8. Convert macro and competitive layers to emit LLR

    • 8.1 Implement noisy-OR macro exposure and LLR emission in services/aggregation/interpolation.py

      • Add compute_normalized_macro_exposure(overlaps: dict[str, float]) -> float
      • E_raw = 1 - product(1 - w_k × O_k) with weights: w_geo=0.35, w_supply=0.25, w_commodity=0.25, w_sector=0.15
      • E_max = 1 - product(1 - w_k)
      • E_macro = E_raw / E_max (normalized to [0, 1])
      • Apply resilience dampener per tier: global_leader=0.70, multinational=0.85, regional=1.00, domestic=1.20
      • Add compute_macro_llr(macro_impact, event_confidence, q_recency, macro_direction) -> float
      • p_macro = clamp(0.50 + 0.30 × macro_impact × event_confidence × q_recency, 0.501, 0.80)
      • LLR_macro = macro_direction × ln(p_macro / (1 - p_macro))
      • Feed macro LLR into shared posterior without separate post-hoc modifier
      • Requirements: 9.1, 9.2, 9.3, 9.4, 9.5
    • 8.2 Implement correlation-shrunk competitive propagation in services/aggregation/signal_propagation.py

      • Add compute_shrunk_correlation(rho_rolling, n_observations, same_sector) -> float
      • rho_prior = 0.30 (same_sector) or 0.10 (cross_sector)
      • rho_shrunk = (n/(n+30)) × rho_rolling + (30/(n+30)) × rho_prior
      • rho_effective = max(rho_shrunk, 0)
      • Add compute_competitive_llr(llr_source, rho_effective, d_network, pattern_confidence) -> float
      • attenuation = rho_effective × exp(-0.85 × d_network), max_distance = 3
      • LLR_competitive = clamp(llr_source × attenuation × pattern_confidence, -1.25, 1.25)
      • Requirements: 10.1, 10.2, 10.3, 10.4, 10.5
    • 8.3 Write property tests for macro and competitive layers (tests/test_pbt_v3_layers.py)

      • Property 13: Noisy-OR normalized exposure is bounded in [0, 1]
      • Property 14: Competitive LLR is clamped to [-1.25, 1.25]
      • Property 15: Graph attenuation is zero beyond max distance
      • Validates: Requirements 9.1, 9.2, 10.3, 10.4, 10.5
      • Use Hypothesis with @settings(max_examples=100)
    • 8.4 Write unit tests for macro and competitive layers (tests/test_v3_layers.py)

      • Test noisy-OR: all O_k = 1.0 → E_macro = 1.0; all O_k = 0 → E_macro = 0
      • Test resilience dampener per tier
      • Test macro LLR at boundary values
      • Test shrunk correlation convergence (n → ∞ approaches rho_rolling)
      • Test competitive LLR clamp at ±1.25
      • Test distance > 3 → zero attenuation
      • Requirements: 9.19.5, 10.110.5
  • 9. Replace EV gate with expected-return distribution

    • 9.1 Implement posterior state projection in services/aggregation/projection.py

      • Add V3ProjectionState dataclass (a_t, p_up_projected, projected_strength, diverges, phi_regime)
      • Implement compute_v3_projection(a_prev, cluster_llrs, regime, p_prior, projection_horizon, known_catalyst_llr=0.0)
      • Evidence state: A_t = phi_regime × A_{t-1} + sum(LLR_c), init A_0 = 0.0
      • Regime decay: panic=0.35, trend_following=0.80, mean_reversion=0.55, uncertainty=0.50
      • Projected alpha: A_projected = phi^h × A_t + known_catalyst_LLR
      • P_up_projected = sigmoid(logit(P_prior) + A_projected)
      • Projected strength = abs(2 × P_up_projected - 1)
      • Flag divergence when sign(P_up_projected - 0.5) ≠ sign(P_up_t - 0.5)
      • Requirements: 11.1, 11.2, 11.3, 11.4, 11.5, 11.6, 11.7
    • 9.2 Implement return distribution and EV gate in services/recommendation/eligibility.py

      • Add ReturnDistribution dataclass (sigma_h, mu_h, ev_long, min_edge, eligible)
      • Add compute_return_distribution(a_projected, confidence, realized_vol_20d, horizon_days, costs, regime)
      • sigma_h = realized_vol_20d × sqrt(horizon_days / 252); default vol = 0.25 if unavailable
      • mu_h = tanh(A_projected / 3.0) × confidence × sigma_h
      • CVaR_5 = sigma_h × 1.645 × 1.4
      • EV_long = mu_h - costs - 0.10 × CVaR_5
      • Regime min_edge: panic=0.0100, trend_following=0.0035, mean_reversion=0.0050, uncertainty=0.0075
      • Eligibility: EV_long > min_edge AND EV_long > max(0.0025, 0.25 × costs)
      • Also require: confidence >= regime_confidence_min, contradiction <= regime_contradiction_max, n_eff_total >= 2.0, data_quality >= 0.50
      • Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7
    • 9.3 Implement regime-aware eligibility and mode escalation in services/recommendation/eligibility.py

      • Add regime-specific eligibility thresholds: panic (conf≥0.70, contra≤0.25, str≥0.36), trend_following (conf≥0.55, contra≤0.40, str≥0.20), mean_reversion (conf≥0.60, contra≤0.35, str≥0.26), uncertainty (conf≥0.65, contra≤0.30, str≥0.30)
      • Action mapping: BUY when P_up >= bullish_threshold and EV > min_edge; SELL when existing position and EV_exit > EV_hold; HOLD when existing; WATCH otherwise
      • live_eligible: BUY/SELL + conf >= 0.75 + contra <= 0.20 + n_eff >= 5 + EV > 2×min_edge + risk_passed
      • paper_eligible: BUY/SELL + conf >= 0.60 + EV > min_edge + risk_passed
      • Otherwise: informational
      • Requirements: 13.1, 13.2, 13.3, 13.4, 13.5
    • 9.4 Write property tests for projection (tests/test_pbt_v3_projection.py)

      • Property 16: Projection evidence state decays toward zero
      • Validates: Requirements 11.1, 11.3
      • Use Hypothesis with @settings(max_examples=100)
    • 9.5 Write unit tests for EV gate and eligibility (tests/test_v3_eligibility.py)

      • Test EV_long positive → eligible
      • Test EV_long negative → ineligible
      • Test regime-specific min_edge thresholds
      • Test mode escalation: live vs paper vs informational
      • Test projection decay convergence
      • Test divergence flag behavior
      • Requirements: 11.111.7, 12.112.7, 13.113.5
  • 10. Checkpoint - Verify decision layer

    • Ensure all tests pass for projection, EV gate, eligibility, and layer integrations.
    • Ensure all tests pass, ask the user if questions arise.
  • 11. Replace sizing with fractional Kelly under existing risk caps

    • 11.1 Implement fractional Kelly position sizing in services/trading/position_sizer.py

      • Add compute_kelly_sizing(p_win, b, confidence, data_quality, contradiction, max_position_pct, available_caps) -> float
      • Compute reward ratio b = clamp(1.2 + 2.0 × confidence + 1.0 × strength - contradiction, 1.2, 3.0)
      • Compute f_kelly = (p_win × b - (1 - p_win)) / b
      • portfolio_pct = clamp(max(0, f_kelly) × 0.25 × confidence × data_quality × (1 - contradiction), 0, max_position_pct)
      • Apply min of: max_position_pct, sector_capacity, correlation_capacity (0 if avg corr > 0.80), heat_capacity
      • If portfolio_pct < 0.005 → downgrade to WATCH (reason: position_below_minimum)
      • If f_kelly <= 0 → portfolio_pct = 0, downgrade to WATCH (reason: negative_edge)
      • Requirements: 14.1, 14.2, 14.3, 14.4, 14.5, 14.6, 14.7
    • 11.2 Implement regime-aware stop loss and take profit in services/trading/stop_loss_manager.py

      • Add v3 stop computation: stop_distance_pct = max(ATR_pct × regime_ATR_mult, sigma_h × 1.25, 0.005)
      • stop_loss = entry_price × (1 - stop_distance_pct)
      • take_profit = entry_price × (1 + b × stop_distance_pct) where b = reward ratio
      • Activate trailing stop when unrealized_gain >= 0.50 × TP distance
      • trailing_stop = max(existing_stop, current_price × (1 - trailing_distance_pct))
      • trailing_distance_pct = max(ATR_pct × trailing_ATR_mult, sigma_h × 0.75)
      • Trailing stop must be monotonically non-decreasing
      • Requirements: 16.1, 16.2, 16.3, 16.4, 16.5
    • 11.3 Write property tests for Kelly sizing and stops (tests/test_pbt_v3_decision.py)

      • Property 11: Fractional Kelly sizing is bounded and respects negative edge
      • Property 18: Stop loss is below entry price and take profit is above
      • Property 19: Trailing stop never decreases
      • Validates: Requirements 14.4, 14.7, 16.2, 16.3, 16.5, 21.8, 21.9
      • Use Hypothesis with @settings(max_examples=100)
    • 11.4 Write unit tests for Kelly sizing and stops (tests/test_v3_sizing.py)

      • Test p_win=0.3, b=2 → f_kelly < 0 → size = 0
      • Test p_win=0.7, b=2 → positive size within caps
      • Test cap enforcement (sector, correlation, heat)
      • Test position_below_minimum downgrade
      • Test stop/TP computation with known inputs
      • Test trailing stop monotonicity over a price sequence
      • Requirements: 14.114.7, 16.116.5
  • 12. Replace portfolio heat with stop-defined risk dollars

    • 12.1 Implement stop-defined portfolio heat in services/risk/engine.py

      • Add compute_portfolio_heat(positions, stop_distances) -> float
      • risk_dollars = position_value × stop_distance_pct for each position
      • portfolio_heat = sum(risk_dollars)
      • Add check_heat_capacity(current_heat, new_risk_dollars, max_heat_pct, portfolio_value) -> bool
      • Reject new entry when current_heat + new_risk_dollars > max_heat_pct × portfolio_value
      • Integrate available_heat_capacity into Kelly sizing pipeline
      • Requirements: 15.1, 15.2, 15.3, 15.4, 15.5
    • 12.2 Implement risk tier auto-adjustment v3 in services/risk/engine.py

      • Add TierMetrics dataclass (profit_factor_30d, max_drawdown_30d, calibration_error, realized_sharpe_30d, n_trades_30d, reserve_pool_pct)
      • Add evaluate_tier_adjustment(metrics) -> str returning 'upgrade'|'downgrade'|'hold'
      • Downgrade if ANY: profit_factor < 1.0 OR drawdown > 0.12 OR calibration_error > 0.20 OR sharpe < 0
      • Upgrade only if ALL: profit_factor > 1.35 AND drawdown < 0.05 AND calibration_error < 0.12 AND reserve > 0.20 AND N_trades >= 20
      • Apply downgrade immediately; enforce 7-day upgrade cooldown
      • Evaluate once per calendar day after session close
      • Requirements: 18.1, 18.2, 18.3, 18.4, 18.5, 18.6
    • 12.3 Write property tests for heat and tier adjustment (tests/test_pbt_v3_decision.py)

      • Property 22: Portfolio heat rejection is correct
      • Property 23: Tier auto-adjustment obeys downgrade-any, upgrade-all logic
      • Validates: Requirements 15.3, 15.5, 18.3, 18.4
      • Use Hypothesis with @settings(max_examples=100)
    • 12.4 Write unit tests for heat and tier (tests/test_v3_risk.py)

      • Test heat computation: 3 positions with known stops → expected heat
      • Test heat rejection: heat at limit → new entry blocked
      • Test tier downgrade: single bad metric triggers downgrade
      • Test tier upgrade: all metrics good → upgrade
      • Test tier upgrade: one metric bad → hold (not upgrade)
      • Test 7-day cooldown enforcement
      • Requirements: 15.115.5, 18.118.6
  • 13. Checkpoint - Verify sizing and risk layer

    • Ensure all tests pass for Kelly sizing, stops, heat, and tier adjustment.
    • Ensure all tests pass, ask the user if questions arise.
  • 14. Retire heuristic scoring to explainability-only mode

    • 14.1 Wire v3 pipeline end-to-end in services/aggregation/worker.py

      • Orchestrate full pipeline: EvidenceUnit → q_i → LLR → cluster → posterior → contradiction → confidence → data_quality
      • Store v3 posterior in TrendSummary JSONB metadata (p_up, log_odds, strength, confidence, contradiction, n_eff, data_quality, regime)
      • Store return model in Recommendation JSONB metadata (mu_h, sigma_h, ev_long, min_edge)
      • Store explainability payload (top_positive_clusters, top_negative_clusters, suppression_reasons, risk_adjustments)
      • Preserve WeightedSignal as intermediate representation before LLR conversion
      • Requirements: 20.1, 20.2, 20.3, 20.4, 20.5
    • 14.2 Retain heuristic pipeline as fallback with explainability overlay

      • Keep existing heuristic scoring path fully functional (no removal)
      • Mark heuristic outputs with pipeline_mode: "heuristic" in metadata
      • Ensure heuristic mode still produces valid TrendSummary and Recommendation objects
      • Test feature flag toggle: v3 → heuristic → v3 round-trip
      • Requirements: 19.1, 19.2, 19.5, 20.4
    • 14.3 Write property test for JSON round-trip (tests/test_pbt_v3_posterior.py)

      • Property 12: Posterior state JSON round-trip
      • Validates: Requirements 20.1, 21.10
      • Serialize V3Posterior to JSON and deserialize, verify equivalence within 1e-10
      • Use Hypothesis with @settings(max_examples=100)
    • 14.4 Write integration tests for full pipeline (tests/test_v3_integration.py)

      • Test full path: raw signals → EvidenceUnit → q_i → LLR → cluster → posterior → recommendation
      • Test feature flag false → heuristic path, flag true → v3 path
      • Test v3 exception → heuristic fallback + error logged
      • Test output JSONB contains expected v3 fields
      • Requirements: 19.119.6, 20.120.5
  • 15. Final checkpoint - All tests green

    • Ensure all tests pass, ask the user if questions arise.
    • Run full test suite: .venv/bin/python -m pytest tests/test_pbt_v3_*.py tests/test_v3_*.py -x --tb=short -q
    • Verify no regressions in existing heuristic pipeline tests

Notes

  • Tasks marked with * are optional and can be skipped for faster MVP
  • Each task references specific requirements for traceability
  • Checkpoints ensure incremental validation after each logical phase
  • Property tests validate the 23 correctness properties defined in the design
  • Unit tests validate specific examples, edge cases, and error handling
  • The implementation preserves the existing heuristic pipeline as a fully functional fallback
  • All v3 code is gated behind v3_engine_enabled — no changes to production behavior until flag is flipped
  • Run tests with: .venv/bin/python -m pytest tests/ -x --tb=short -q
  • Property tests use Hypothesis: @settings(max_examples=100)

Task Dependency Graph

{
  "waves": [
    { "id": 0, "tasks": ["1.1"] },
    { "id": 1, "tasks": ["1.2", "1.4"] },
    { "id": 2, "tasks": ["1.3"] },
    { "id": 3, "tasks": ["1.5", "1.6"] },
    { "id": 4, "tasks": ["2.1"] },
    { "id": 5, "tasks": ["2.2"] },
    { "id": 6, "tasks": ["2.3"] },
    { "id": 7, "tasks": ["2.4", "2.5"] },
    { "id": 8, "tasks": ["4.1"] },
    { "id": 9, "tasks": ["4.2"] },
    { "id": 10, "tasks": ["4.3", "4.4"] },
    { "id": 11, "tasks": ["5.1"] },
    { "id": 12, "tasks": ["5.2", "5.3"] },
    { "id": 13, "tasks": ["6.1", "6.2"] },
    { "id": 14, "tasks": ["6.3", "6.4"] },
    { "id": 15, "tasks": ["8.1", "8.2"] },
    { "id": 16, "tasks": ["8.3", "8.4"] },
    { "id": 17, "tasks": ["9.1"] },
    { "id": 18, "tasks": ["9.2", "9.3"] },
    { "id": 19, "tasks": ["9.4", "9.5"] },
    { "id": 20, "tasks": ["11.1", "11.2"] },
    { "id": 21, "tasks": ["11.3", "11.4"] },
    { "id": 22, "tasks": ["12.1", "12.2"] },
    { "id": 23, "tasks": ["12.3", "12.4"] },
    { "id": 24, "tasks": ["14.1"] },
    { "id": 25, "tasks": ["14.2"] },
    { "id": 26, "tasks": ["14.3", "14.4"] }
  ]
}