from __future__ import annotations from pathlib import Path from typing import Literal import yaml from pydantic import BaseModel, Field, ValidationError class AppConfig(BaseModel): host: str = "0.0.0.0" port: int = 8088 poll_interval_seconds: int = 120 log_file: str = "./runtime/system.log" database_url: str = "sqlite+aiosqlite:///./runtime/alerts.db" session_secret: str = "please-change-me" class AuthConfig(BaseModel): """ enabled: 为 false 时跳过登录(仅建议纯局域网、无外网暴露时使用)。 """ enabled: bool = True username: str password: str class WeComConfig(BaseModel): webhook: str mentioned_mobile_list: list[str] = Field(default_factory=list) class GateConfig(BaseModel): """Gate.io 公共 REST v4(USDT 永续 settle=usdt)。""" api_base: str = "https://api.gateio.ws/api/v4" settle: str = "usdt" quote_currency: str = "USDT" class ProxyConfig(BaseModel): """ 出站 HTTP 客户端代理(httpx),用于访问 Gate 等外网。 企业微信与本机/局域网 Ollama(Gemma)默认直连,不使用此配置。 可写 socks5h://…;程序在交给 httpx 时会自动改为 socks5://(避免 Unknown scheme)。 """ enabled: bool = False url: str = "socks5h://127.0.0.1:1080" class OrderExecutorConfig(BaseModel): """ 与 gate_order_executor 联动:企微突破推送 **成功之后**,向执行器 POST /v1/signal。 请求不走 proxy.url(直连 base_url),便于同机 127.0.0.1。 webhook_secret 须与执行器 config.yaml 的 security.webhook_secret 一致。 """ enabled: bool = False base_url: str = "http://127.0.0.1:8090" webhook_secret: str = "" timeout_seconds: float = Field(15.0, ge=3.0, le=120.0) class WatchSymbol(BaseModel): """Gate USDT 永续 base 资产符号,如 BTC、ORDI、1000PEPE(与合约名 BTC_USDT 的左侧一致)。""" symbol: str class MonitorConfig(BaseModel): """ 监控侧过滤。 universe: - all_swaps: 监控 Gate 全部 USDT 本位线性永续中,24h 成交额达标的合约(不依赖 watch_symbols)。 - watchlist: 仅监控 watch_symbols 中列出且满足成交额阈值的标的。 min_24h_quote_volume_usdt: 近 24h 成交额下限(USDT)。优先使用 Gate ticker 的 volume_24h_quote。 all_swaps 模式下若设为 0 或负数,将拒绝整轮扫描(避免无阈值拉全市场)。 watchlist 模式下 0 表示关闭成交额过滤。 btc_daily_gate_enabled: 可选;true 时仍计算 BTC 日线 regime 供面板/日志参考,不再拦截山寨扫描。 btc_sideways_lookback_days / btc_sideways_max_range_pct: 与上述辅助门控配套的横盘区分参数。 """ universe: Literal["all_swaps", "watchlist"] = "all_swaps" min_24h_quote_volume_usdt: float = 10_000_000 # 可选:BTC 日线 regime 仅展示/记录;推送门控用「近8h×15m BTC 环境(横盘则多空均可;否则涨→LONG、跌→SHORT)+ 本币4h同向」 btc_daily_gate_enabled: bool = True btc_sideways_lookback_days: int = 14 btc_sideways_max_range_pct: float = 10.0 # 同一币种在 N 小时内对同一条「链路」只落库一条告警、只推送一次(0 表示关闭去重) # 链路含:GATE-USDT 5m WATCH / GATE-USDT 5m TRIGGER(分级)与 FUNNEL-GEMMA(漏斗) symbol_signal_dedupe_hours: float = 4.0 # 企业微信主推送(突破预警):仅对本轮监控池内 24h 成交额排名前 N 的合约推送;0 表示不限制 wecom_push_max_volume_rank: int = 30 class GemmaConfig(BaseModel): """ 本地 Ollama 跑 Gemma(或其它模型)做漏斗二次分拣。 需在机器上自行启动 ollama 并拉取模型;开启后仅对本轮 5m 扫描命中的 WATCH/TRIGGER 按成交额取前 N 再请求。 """ enabled: bool = False ollama_base_url: str = "http://127.0.0.1:11434" model: str = "gemma2:2b" timeout_seconds: float = 180.0 temperature: float = 0.15 json_mode: bool = True send_chart_image: bool = True max_funnel_per_cycle: int = 12 vision_top_n: int = 4 gemma_push_priority_min: float = 7.0 composite_push_min: float = 72.0 class DailyReportConfig(BaseModel): """每日晨报:北京时间定时生成昨天复盘,并可推送企业微信。""" enabled: bool = True run_time_cn: str = "08:30" push_wecom: bool = True run_on_startup: bool = False class Settings(BaseModel): app: AppConfig auth: AuthConfig wecom: WeComConfig gate: GateConfig proxy: ProxyConfig = Field(default_factory=ProxyConfig) order_executor: OrderExecutorConfig = Field(default_factory=OrderExecutorConfig) monitor: MonitorConfig = Field(default_factory=MonitorConfig) gemma: GemmaConfig = Field(default_factory=GemmaConfig) daily_report: DailyReportConfig = Field(default_factory=DailyReportConfig) watch_symbols: list[WatchSymbol] = Field(default_factory=list) def load_settings(config_path: str = "config.yaml") -> Settings: path = Path(config_path).expanduser().resolve() if not path.exists(): raise FileNotFoundError( f"配置文件不存在: {path}. 请先复制 config.example.yaml 为 config.yaml 并填写密钥。" ) raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {} try: return Settings.model_validate(raw) except ValidationError as exc: raise ValueError(f"配置文件校验失败: {exc}") from exc # 兼容原 OKX 风格 bar 字符串(映射见 app.gate._to_gate_interval) GATE_BAR_CHOICES: tuple[str, ...] = ( "1m", "3m", "5m", "15m", "30m", "1H", "2H", "4H", "6H", "12H", "1D", "1W", "1M", )