""" Configuration schema for the opportunity engine (oe_em). Parsed from config.yaml into OeEmSettings. Controls logging, signal thresholds, the fee discount flag, symbol subscription, and the socket path to the executor. """ import asyncio from pathlib import Path from typing import Optional import yaml from pydantic import BaseModel, Field from pydantic_settings import BaseSettings class OeEmSettings(BaseModel): """Settings that control oe_em's runtime behaviour.""" fh_ob_url: str = Field( default="http://127.0.0.1:8000", description="REST URL of fh_ob server", ) socket_path: Path = Field( default=Path("/tmp/fh_ob.sock"), description="Unix domain socket path for fh_ob", ) log_level: str = Field(default="INFO", description="Logging level") log_file: Path = Field( default=Path("/tmp/oe_em.log"), description="Path to log file. Logs are written here in addition to stdout.", ) signal_threshold_bps: float = Field( default=0.2, description="Minimum net return in basis points to fire a signal", ) opportunity_log_path: Path = Field( default=Path("/tmp/opportunities.log"), description="Path to log detected opportunities", ) stats_interval_seconds: float = Field( default=60.0, description="Seconds between stats log lines. 0 to disable.", ) cooldown_seconds: float = Field( default=0.0, description="Deprecated — use executor's in-flight blocking instead. " "Kept here for operational flexibility; set to 0.", ) excluded_currencies: list[str] = Field( default_factory=list, description="Currencies to exclude from triangle enumeration", ) hold_currencies: list[str] = Field( default=["USDT"], description="Currencies held as capital. Only triangles starting and ending in one of these are evaluated.", ) kcs_discount_active: bool = Field( default=False, description="If true, all taker fees are multiplied by 0.8 (KCS 20% fee discount)", ) executor_socket_path: Path = Field( default=Path("/tmp/executor.sock"), description="Unix domain socket path for executor", ) send_signals: bool = Field( default=False, description="If true, emit signals to executor socket when opportunities are found", ) class Settings(BaseSettings): """Top-level settings parsed from config.yaml.""" oe_em: OeEmSettings = Field(default_factory=OeEmSettings) fh_ob_url: Optional[str] = None @classmethod async def from_yaml(cls, path: Path) -> "Settings": """Load settings from a YAML file.""" loop = asyncio.get_running_loop() def _read() -> dict: with open(path) as f: return yaml.safe_load(f) data = await loop.run_in_executor(None, _read) return cls(**data) model_config = {"env_prefix": "TRIArb_", "extra": "ignore"}