"""Single-ticker advisor service. Runs the PEAD and ORB single-ticker adapters concurrently for one (ticker, asof, optional held position) request and returns a unified verdict pair. """ from __future__ import annotations import asyncio import datetime as dt from datetime import datetime from pathlib import Path from typing import Any, Literal from zoneinfo import ZoneInfo from pydantic import BaseModel, Field from libs.advisor.orb_adapter import OrbVerdict, evaluate_orb_buy, evaluate_orb_sell from libs.advisor.pead_adapter import PeadVerdict, evaluate_pead_buy, evaluate_pead_sell from libs.common.logging import get_logger logger = get_logger(__name__) _ET = ZoneInfo("America/New_York") PEAD_CHAMPION_CONFIG = "configs/experiments/return_max_long_v7.364_composed_gld.json" ORB_CHAMPION_CONFIG = "configs/intraday/strategies/orb_gainers_v49.yaml" class HeldPosition(BaseModel): strategy: Literal["pead", "orb"] entry_date: dt.date entry_price: float class AdvisorRequest(BaseModel): ticker: str asof: dt.date | None = None held: HeldPosition | None = None class StrategyVerdict(BaseModel): signal: Literal["BUY", "SELL", "HOLD", "NO_SIGNAL", "ERROR"] score: float | None = None reason: str details: dict[str, Any] = Field(default_factory=dict) warning: str | None = None class AdvisorResponse(BaseModel): ticker: str asof: dt.date evaluated_at: str pead: StrategyVerdict orb: StrategyVerdict def _today_et() -> dt.date: return datetime.now(_ET).date() def _now_et_iso() -> str: return datetime.now(_ET).isoformat() def _to_strategy_verdict(v: PeadVerdict | OrbVerdict | BaseException) -> StrategyVerdict: if isinstance(v, BaseException): return StrategyVerdict( signal="ERROR", reason=f"adapter raised: {type(v).__name__}: {v}", ) return StrategyVerdict( signal=v.signal, score=v.score, reason=v.reason, details=v.details, warning=v.warning, ) async def _run_pead(req: AdvisorRequest, asof: dt.date) -> PeadVerdict: if req.held and req.held.strategy == "pead": return await evaluate_pead_sell( req.ticker, req.held.entry_date, req.held.entry_price, asof, PEAD_CHAMPION_CONFIG, ) return await evaluate_pead_buy(req.ticker, asof, PEAD_CHAMPION_CONFIG) async def _run_orb(req: AdvisorRequest, asof: dt.date) -> OrbVerdict: if req.held and req.held.strategy == "orb": return await evaluate_orb_sell( req.ticker, req.held.entry_date, asof, ORB_CHAMPION_CONFIG, ) return await evaluate_orb_buy(req.ticker, asof, ORB_CHAMPION_CONFIG) async def evaluate(req: AdvisorRequest) -> AdvisorResponse: asof = req.asof or _today_et() pead_task = asyncio.create_task(_run_pead(req, asof)) orb_task = asyncio.create_task(_run_orb(req, asof)) pead_result, orb_result = await asyncio.gather( pead_task, orb_task, return_exceptions=True ) if isinstance(pead_result, BaseException): logger.warning("advisor_pead_failed", ticker=req.ticker, error=str(pead_result)) if isinstance(orb_result, BaseException): logger.warning("advisor_orb_failed", ticker=req.ticker, error=str(orb_result)) return AdvisorResponse( ticker=req.ticker.upper(), asof=asof, evaluated_at=_now_et_iso(), pead=_to_strategy_verdict(pead_result), orb=_to_strategy_verdict(orb_result), ) def champions() -> dict[str, str]: return {"pead": PEAD_CHAMPION_CONFIG, "orb": ORB_CHAMPION_CONFIG} def champion_paths_exist() -> dict[str, bool]: return { "pead": Path(PEAD_CHAMPION_CONFIG).exists(), "orb": Path(ORB_CHAMPION_CONFIG).exists(), }