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.

126 lines
3.7 KiB
Python

"""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(),
}