27 KiB
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
EvidenceUnitwith 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
- Add frozen dataclass
-
1.2 Implement calibrated reliability pipeline (
compute_v3_reliability) inservices/aggregation/scoring.py- Add
SourceStatsdataclass (source_id, hits, misses, alpha_0=3, beta_0=3) - Add
ReliabilityComponentsdataclass (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
- Add
-
1.3 Implement LLR conversion (
compute_llr) inservices/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_enabledfrom 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_modefield ("v3" or "heuristic") in output metadata - Requirements: 19.1, 19.2, 19.3, 19.4, 19.5, 19.6
- Read
-
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.1–1.8, 2.1–2.9, 3.1–3.6, 21.1–21.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.1–1.8, 2.1–2.9, 3.1–3.6
-
-
2. Evidence clustering and n_eff
-
2.1 Implement correlation-aware clustering in
services/aggregation/worker.py- Add
EvidenceClusterdataclass (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
- Add
-
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
- Implement
-
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
- Implement
-
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.1–4.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
V3RegimeClassificationdataclass (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
- Add
-
4.2 Implement posterior assembly via log-odds in
services/aggregation/bayesian.py- Add
V3Posteriordataclass (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
- Add
-
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.2–6.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.1–5.7, 6.1–6.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
- Add
-
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.1–7.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
- Add
-
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
- Add
-
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.1–8.5, 17.1–17.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
- Add
-
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
- Add
-
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.1–9.5, 10.1–10.5
-
-
9. Replace EV gate with expected-return distribution
-
9.1 Implement posterior state projection in
services/aggregation/projection.py- Add
V3ProjectionStatedataclass (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
- Add
-
9.2 Implement return distribution and EV gate in
services/recommendation/eligibility.py- Add
ReturnDistributiondataclass (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
- Add
-
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.1–11.7, 12.1–12.7, 13.1–13.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
- Add
-
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.1–14.7, 16.1–16.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
- Add
-
12.2 Implement risk tier auto-adjustment v3 in
services/risk/engine.py- Add
TierMetricsdataclass (profit_factor_30d, max_drawdown_30d, calibration_error, realized_sharpe_30d, n_trades_30d, reserve_pool_pct) - Add
evaluate_tier_adjustment(metrics) -> strreturning '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
- Add
-
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.1–15.5, 18.1–18.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.1–19.6, 20.1–20.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"] }
]
}