You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

125 lines
4.1 KiB
Python

"""Feature orchestrator: combines market, event, and financial features."""
from __future__ import annotations
import datetime as dt
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from libs.common.logging import get_logger
from libs.db.models import Event, EventParse, FeatureSnapshot
from libs.features.event_features import compute_event_features
from libs.features.financial_features import compute_financial_features
from libs.features.market_features import compute_market_features
from libs.oracle_client.financial import FinancialService
from libs.oracle_client.price import PriceService
logger = get_logger(__name__)
SNAPSHOT_VERSION = "1.0.0"
async def build_features_for_event(
session: AsyncSession,
event: Event,
price_service: PriceService,
financial_service: FinancialService | None = None,
) -> tuple[FeatureSnapshot, FeatureSnapshot] | None:
"""Build market_v1 and event_v1 feature snapshots for an event.
Returns (market_snapshot, event_snapshot) or None on failure.
"""
# Get ticker from event (via symbol)
if not event.symbol_id:
logger.warning("event_no_symbol", event_id=event.event_id)
return None
# Extract ticker from symbol_id (format: SYM::{ticker}::{venue})
parts = event.symbol_id.split("::")
ticker = parts[1] if len(parts) >= 2 else None
if not ticker:
logger.warning("event_bad_symbol_id", event_id=event.event_id, symbol_id=event.symbol_id)
return None
event_date_str = event.event_date.isoformat()
# Fetch price bars from Stock Oracle
try:
# 30 trading days before event
start_date = (event.event_date - dt.timedelta(days=45)).isoformat()
end_date = (event.event_date + dt.timedelta(days=5)).isoformat()
price_response = await price_service.get_daily_bars(
ticker, start=start_date, end=end_date
)
bars = price_response.bars
except Exception as exc:
logger.error("price_fetch_failed", event_id=event.event_id, error=str(exc))
return None
# Compute market features
mf = compute_market_features(bars, event_date_str)
# Get latest valid event parse
result = await session.execute(
select(EventParse)
.where(EventParse.event_id == event.event_id)
.where(EventParse.validation_status == "valid")
.order_by(EventParse.event_parse_id.desc())
.limit(1)
)
parse = result.scalar_one_or_none()
if parse is None:
logger.warning("no_valid_parse", event_id=event.event_id)
return None
ef = compute_event_features(parse.output_json)
market_snapshot = FeatureSnapshot(
event_id=event.event_id,
snapshot_name="market_v1",
snapshot_version=SNAPSHOT_VERSION,
feature_json=mf,
)
event_snapshot = FeatureSnapshot(
event_id=event.event_id,
snapshot_name="event_v1",
snapshot_version=SNAPSHOT_VERSION,
feature_json=ef,
)
session.add(market_snapshot)
session.add(event_snapshot)
await session.flush()
# Optional: financial features (non-fatal if unavailable)
if financial_service is not None:
try:
fin_response = await financial_service.get_financial_data(ticker)
ff = compute_financial_features(fin_response)
if ff:
financial_snapshot = FeatureSnapshot(
event_id=event.event_id,
snapshot_name="financial_v1",
snapshot_version=SNAPSHOT_VERSION,
feature_json=ff,
)
session.add(financial_snapshot)
await session.flush()
logger.info("financial_features_built", event_id=event.event_id, ticker=ticker)
except Exception as exc:
logger.warning(
"financial_features_skipped",
event_id=event.event_id,
ticker=ticker,
error=str(exc),
)
logger.info(
"features_built",
event_id=event.event_id,
ticker=ticker,
market_features=list(mf.keys()),
)
return market_snapshot, event_snapshot