Complete engine unification Phase 5-6: residual reserve + macro data

Phase 5 — Engine selection (both entry paths):
  - Added residual_reserve_selected tracking between engines
  - Added prelimit amplification (5x) for attention-requiring engines
  - Added truncate_to parameter to select_candidates calls
  Matches BacktestRunner._select_candidates_for_date() behavior.

Phase 6 — Macro data:
  - Added FRED series fetch (VIXCLS, BAMLH0A0HYM2) to _fetch_macro()
  - Matches SnapshotStore._fetch_macro() which loads from MacroObservation DB
  - Enables VIX/HY regime sizing in live paper trading

All 6 phases of BacktestRunner ↔ PaperTradingEngine unification complete.
450 unit tests pass. Multi-strategy paper backtest verified.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
main
I Luk Kim 5 months ago
parent 26ca89c058
commit 07bcfbe51d

@ -295,23 +295,38 @@ class PaperTradingEngine:
)
if engines:
# Residual reserve: engines that set residual_reserve_selected=True
# prevent later engines from picking the same event_id/symbol.
# Matches BacktestRunner._select_candidates_for_date().
reserved_event_ids: set[str] = {
ss.event_id for ss in strategy_states_after_exits.values()
}
reserved_symbols: set[str] = {
p.symbol for p in alpaca_positions_after_exits
if p.symbol in strategy_states_after_exits
}
for engine_cfg in engines:
prelimit = self._config.signal.max_candidates_per_day
if self._attention_service.engine_requires_attention(engine_cfg):
prelimit = max(prelimit * 5, prelimit)
engine_candidates = select_candidates(
raw_rows=candidate_rows,
universe_config=self._config.universe,
signal_config=self._config.signal,
event_type_profiles=self._config.event_type_profiles or {},
strategy_engine=engine_cfg,
excluded_event_ids={
ss.event_id
for ss in strategy_states_after_exits.values()
},
excluded_symbols={p.symbol for p in alpaca_positions_after_exits if p.symbol in strategy_states_after_exits},
truncate_to=prelimit,
excluded_event_ids=reserved_event_ids,
excluded_symbols=reserved_symbols,
)
# Attention filtering (matches BacktestRunner)
engine_candidates = self._attention_service.apply_filters(
engine_candidates, engine_cfg, self._config.signal,
)
# Residual reserve for next engine
if engine_cfg.residual_reserve_selected and engine_candidates:
reserved_event_ids.update(c.event_id for c in engine_candidates)
reserved_symbols.update(c.symbol.upper() for c in engine_candidates)
engine_risk_used = engine_daily_risk_used.get(engine_cfg.engine_id, 0.0)
for candidate in engine_candidates:
@ -945,21 +960,30 @@ class PaperTradingEngine:
)
engine_list = engines if engines else [None]
reserved_event_ids: set[str] = {ss.event_id for ss in strategy_states.values()}
reserved_symbols: set[str] = {p.symbol for p in alpaca_positions if p.symbol in strategy_states}
for engine_cfg in engine_list:
if engine_cfg is not None:
prelimit = self._config.signal.max_candidates_per_day
if self._attention_service.engine_requires_attention(engine_cfg):
prelimit = max(prelimit * 5, prelimit)
engine_candidates = select_candidates(
raw_rows=candidate_rows,
universe_config=self._config.universe,
signal_config=self._config.signal,
event_type_profiles=self._config.event_type_profiles or {},
strategy_engine=engine_cfg,
excluded_event_ids={ss.event_id for ss in strategy_states.values()},
excluded_symbols={p.symbol for p in alpaca_positions if p.symbol in strategy_states},
truncate_to=prelimit,
excluded_event_ids=reserved_event_ids,
excluded_symbols=reserved_symbols,
)
# Attention filtering (matches BacktestRunner)
engine_candidates = self._attention_service.apply_filters(
engine_candidates, engine_cfg, self._config.signal,
)
if engine_cfg.residual_reserve_selected and engine_candidates:
reserved_event_ids.update(c.event_id for c in engine_candidates)
reserved_symbols.update(c.symbol.upper() for c in engine_candidates)
engine_risk_used = engine_daily_risk_used.get(engine_cfg.engine_id, 0.0)
else:
engine_candidates = select_candidates(
@ -967,8 +991,8 @@ class PaperTradingEngine:
universe_config=self._config.universe,
signal_config=self._config.signal,
event_type_profiles=self._config.event_type_profiles or {},
excluded_event_ids={ss.event_id for ss in strategy_states.values()},
excluded_symbols={p.symbol for p in alpaca_positions if p.symbol in strategy_states},
excluded_event_ids=reserved_event_ids,
excluded_symbols=reserved_symbols,
)
engine_risk_used = 0.0
@ -1296,6 +1320,24 @@ class PaperTradingEngine:
if len(closes) >= sma_period:
macro[f"{key_prefix}_sma_{sma_period}"] = sum(closes[-sma_period:]) / sma_period
# Fetch FRED macro data (VIX, HY spread) for regime sizing
# Matches SnapshotStore._fetch_macro() which loads MacroObservation from DB
try:
from libs.oracle_client import FredService, OracleClient as _OC
async with _OC(base_url=self._detector._oracle_url) as fred_client:
fred_svc = FredService(fred_client)
for series_id in ("VIXCLS", "BAMLH0A0HYM2"):
try:
resp = await fred_svc.get_observations(series_id, start=start.isoformat(), end=date.isoformat())
if resp.observations:
latest = [o for o in resp.observations if o.value is not None]
if latest:
macro[series_id] = latest[-1].value
except Exception:
pass
except Exception:
pass
return macro
except Exception as exc:

Loading…
Cancel
Save