diff --git a/apps/paper_trader/event_detector.py b/apps/paper_trader/event_detector.py index 32b645f..a7244bb 100644 --- a/apps/paper_trader/event_detector.py +++ b/apps/paper_trader/event_detector.py @@ -28,6 +28,35 @@ class EventDetector: self._db_unavailable: bool = False # circuit breaker: skip after first failure self._screener_unavailable: bool = False # circuit breaker for screener API + @staticmethod + def _compute_score(row: dict[str, Any], config: BacktestConfig) -> float: + """Compute score using the config's scoring_model — matches backtester.""" + model = config.signal.scoring_model + if model == "return_max_long_v5": + from libs.backtest.scoring import compute_return_max_long_score_v5 + return compute_return_max_long_score_v5(row) + elif model == "return_max_long_v7": + from libs.backtest.scoring import compute_return_max_long_score_v7 + return compute_return_max_long_score_v7(row) + elif model == "return_max_long_v8": + from libs.backtest.scoring import compute_return_max_long_score_v8 + return compute_return_max_long_score_v8(row) + elif model == "return_max_long_v9": + from libs.backtest.scoring import compute_return_max_long_score_v9 + return compute_return_max_long_score_v9(row) + elif model == "return_max_long_v9g": + from libs.backtest.scoring import compute_return_max_long_score_v9g + return compute_return_max_long_score_v9g(row) + elif model == "return_max_long_v10": + from libs.backtest.scoring import compute_return_max_long_score_v10 + return compute_return_max_long_score_v10(row) + elif model == "pead": + from libs.backtest.scoring import compute_pead_score + return compute_pead_score(row) + else: + from libs.backtest.scoring import compute_entry_score + return compute_entry_score(row) + async def get_candidates_for_date( self, execution_date: dt.date, @@ -102,11 +131,15 @@ class EventDetector: if not enriched.get("reaction_day_high"): enriched["reaction_day_high"] = sym_bars[rd].get("high") - # Compute market features from Oracle bars if missing in DB feature_json. - # These can be None when the feature builder ran before reaction-day bars settled. + # Compute market features from Oracle bars ONLY if missing in DB + # feature_json. DB values are authoritative because they were computed + # by the feature_builder at event time with the correct reaction_date + # and base price. Oracle bars can produce different values due to + # non-deterministic data or different date alignment. + _db_has_reaction = enriched.get("reaction_day_return") is not None sym_bars = bars_by_symbol.get(sym, {}) rd = _parse_date(enriched.get("reaction_date")) - if rd and rd in sym_bars: + if rd and rd in sym_bars and not _db_has_reaction: sorted_dates = sorted(sym_bars.keys()) try: rd_idx = sorted_dates.index(rd) @@ -117,12 +150,11 @@ class EventDetector: prev_bar = sym_bars[sorted_dates[rd_idx - 1]] reaction_bar = sym_bars[rd] - if not enriched.get("volume_ratio_20d") and not enriched.get("volume_ratio") and prior_dates: + if not enriched.get("volume_ratio_20d") and prior_dates: avg_vol = sum(sym_bars[d]["volume"] for d in prior_dates) / len(prior_dates) if avg_vol > 0: vr = reaction_bar["volume"] / avg_vol enriched["volume_ratio_20d"] = vr - enriched["volume_ratio"] = vr # backward compat if not enriched.get("reaction_day_return") and prev_bar["close"]: enriched["reaction_day_return"] = ( @@ -179,10 +211,10 @@ class EventDetector: ) continue - # Compute score if missing - if "score" not in enriched or enriched.get("score") is None: - from libs.backtest.scoring import compute_entry_score - enriched["score"] = compute_entry_score(enriched) + # Compute score using the SAME scoring model as the backtester. + # Always recompute to ensure consistency with backtest results, + # since DB-stored scores may have been computed with a different model. + enriched["score"] = self._compute_score(enriched, config) enriched_rows.append(enriched)