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