"""One-time script: rebuild features + labels for cross-midnight misclassified events. Cross-midnight events: filed_at ET date > event_date AND hour < 10. These were stored with ftb=pre_market but should be post_market. """ from __future__ import annotations import asyncio import datetime as dt from sqlalchemy import delete, select, text from libs.common.logging import configure_logging, get_logger from libs.db.models import Event, EventLabel, FeatureSnapshot, SymbolMaster from libs.db.session import get_session from libs.features.builder import build_features_for_event from libs.labeler.label_generator import generate_labels from libs.oracle_client import CompanyService, FinancialService, PriceService, make_oracle_client logger = get_logger(__name__) async def rebuild_crossmidnight(dry_run: bool = False) -> None: configure_logging() async with get_session() as session, make_oracle_client() as oracle: price_svc = PriceService(oracle) financial_svc = FinancialService(oracle) company_svc = CompanyService(oracle) # Find cross-midnight events result = await session.execute(text(""" SELECT e.event_id FROM events e WHERE e.filed_at_utc IS NOT NULL AND DATE(e.filed_at_utc AT TIME ZONE 'America/New_York') > e.event_date AND EXTRACT(HOUR FROM (e.filed_at_utc AT TIME ZONE 'America/New_York')) < 10 AND e.status = 'valid' ORDER BY e.event_date """)) event_ids = [r[0] for r in result.fetchall()] logger.info("cross_midnight_events_found", count=len(event_ids)) if dry_run: logger.info("dry_run_mode_no_changes") return stats = {"rebuilt": 0, "label_rebuilt": 0, "errors": 0} for i, eid in enumerate(event_ids): try: # Fetch event + symbol evt_result = await session.execute( select(Event, SymbolMaster) .join(SymbolMaster, Event.symbol_id == SymbolMaster.symbol_id, isouter=True) .where(Event.event_id == eid) ) row = evt_result.one_or_none() if row is None: continue event, symbol = row # Delete old feature snapshots await session.execute( delete(FeatureSnapshot).where(FeatureSnapshot.event_id == eid) ) # Delete old labels await session.execute( delete(EventLabel).where(EventLabel.event_id == eid) ) await session.flush() # Rebuild features snapshots = await build_features_for_event( session, event, price_svc, financial_service=financial_svc, company_service=company_svc, ) if snapshots is None: logger.warning("feature_rebuild_failed", event_id=eid) stats["errors"] += 1 continue stats["rebuilt"] += 1 # Rebuild labels (both conventions) ticker = symbol.ticker if symbol else None if ticker: for convention in ("next_open_after_reaction_close", "reaction_close"): lbl = await generate_labels( session, event, price_svc, ticker, entry_convention=convention ) if lbl is not None: session.add(lbl) await session.flush() stats["label_rebuilt"] += 1 if (i + 1) % 50 == 0: logger.info("rebuild_progress", done=i + 1, total=len(event_ids), **stats) await session.commit() except Exception as exc: logger.error("rebuild_error", event_id=eid, error=str(exc)) stats["errors"] += 1 await session.rollback() await session.commit() logger.info("rebuild_done", total=len(event_ids), **stats) if __name__ == "__main__": import sys dry_run = "--dry-run" in sys.argv asyncio.run(rebuild_crossmidnight(dry_run=dry_run))