from __future__ import annotations from typing import Literal, Optional from pydantic import BaseModel, Field class NodeMeta(BaseModel): name: str label: str default_params: dict param_schema: dict class NodesResponse(BaseModel): score_nodes: list[NodeMeta] gate_nodes: list[NodeMeta] class SettingsBody(BaseModel): weights: dict[str, float] node_params: dict[str, dict] = Field(default_factory=dict) gate_params: dict top_n: int = 20 rr_ratio: float = 2.0 atr_window: int = 14 atr_stop_mult: float = 2.0 class SettingsResponse(SettingsBody): updated_at: str class RunRequest(BaseModel): mode: Literal["preview", "manual_save", "auto"] = "preview" asof: Optional[str] = None weights: Optional[dict[str, float]] = None node_params: Optional[dict[str, dict]] = None gate_params: Optional[dict] = None top_n: Optional[int] = None class ResultRow(BaseModel): rank: int ticker: str name: str total_score: float scores: dict[str, float] close: int market_cap: int entry_price: Optional[int] = None stop_price: Optional[int] = None target_price: Optional[int] = None atr14: Optional[float] = None r_pct: Optional[float] = None class TelegramPayload(BaseModel): chat_target: str parse_mode: str text: str class RunResponse(BaseModel): asof: str mode: str status: Literal["success", "failed", "skipped_holiday"] run_id: Optional[int] = None survivors_count: Optional[int] = None weights: dict[str, float] top_n: int results: list[ResultRow] = Field(default_factory=list) telegram_payload: Optional[TelegramPayload] = None warnings: list[str] = Field(default_factory=list) error: Optional[str] = None class RunSummary(BaseModel): id: int asof: str mode: str status: str started_at: str finished_at: Optional[str] = None top_n: int survivors_count: Optional[int] = None telegram_sent: bool