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 KeyMonitorConfig(BaseModel): """ 人工关键位突破监控(漏斗下方录入)。 触发后按录入的标准/趋势方案计算单一 SL/TP,企微推送并可选转发执行器。 """ enabled: bool = True poll_interval_seconds: int = Field(5, ge=2, le=120) push_wecom: bool = True forward_executor: bool = True standard_stop_outside_pct: float = Field(0.3, ge=0.0, le=5.0) trend_stop_outside_pct: float = Field(1.0, ge=0.0, le=10.0) min_planned_rr: float = Field(1.5, gt=0.0) volume_ma_bars: int = Field(20, ge=5) volume_ratio_min: float = Field(1.3, gt=0.0) breakout_amp_min_pct: float = Field(0.03, ge=0.0) breakout_amp_max_pct: float = Field(0.5, gt=0.0) daily_volume_rank_max: int = Field(30, ge=1) # 全市场 5m TRIGGER 是否仍转发执行器(默认关;关键位走 forward_executor) auto_scan_forward_executor: bool = False 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 # 全市场自动箱体 WATCH/TRIGGER 是否发企微(默认关;仅 GEMMA 漏斗优先推送 + 关键位监控推送) push_watch_trigger_wecom: bool = False class GemmaConfig(BaseModel): """ Gemma 漏斗:默认直连本机 Ollama(/api/chat)。 若使用 OpenAI 兼容网关(如 https://op.bz121.com/v1 + Bearer),设 api_style=openai 并填写 api_key。 """ enabled: bool = False ollama_base_url: str = "http://127.0.0.1:11434" api_key: str = "" api_style: Literal["ollama", "openai"] = "ollama" 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) key_monitor: KeyMonitorConfig = Field(default_factory=KeyMonitorConfig) 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", )