# -*- coding: utf-8 -*-
"""
tradingSystem (PMS) 配置
========================
纪律 (沿袭 bionic_trader 踩坑经验):
  1. 所有配置字段必须在 Settings 类中显式声明 —— pydantic 的 extra="ignore" 会静默忽略
     未声明的环境变量, 这是历史头号坑。新增配置必须同时加字段。
  2. 基础设施键名与 bionic_trader 对齐 (PROXY_DB_URL / SIGNAL_REDIS_* / SOURCE_DB_EXT_DSN
     / DB_MYSQL_URL), 同一份 .env 可服务两个系统。
  3. 业务参数在此处只是「初值」: 页面修改后持久化到 pms_runtime_param 表,
     运行时经 ParamStore 读取 (表值优先于本文件)。禁止在业务代码里直接
     读 settings 取业务参数 —— 统一走 ParamStore, 保证页面调参即时生效。
"""
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")

    # =========================================================
    # 一、基础设施 (键名对齐 bionic_trader, 同一 .env 可共用)
    # =========================================================
    PROXY_DB_URL: str = "mysql+pymysql://user:pass@192.168.16.153:3307/my_quant_db"
    #   153 代理: pms_* 全部表 + trading_position / trading_order / trading_buy_plan /
    #   strategy_daily_results (决策系统结论) —— 严格单表访问, 代理禁多表联查
    SOURCE_DB_EXT_DSN: str = "mysql+pymysql://user:pass@192.168.16.150:3306/factordb_mysql"
    #   因子库直连。**PMS 当前不使用**: 因子分表 gp_stock_factor_pro_YYYYMM 经
    #   ShardingSphere 动态分配物理落点, 直连单台会在分片变动的月份漏表, 必须走
    #   PROXY_DB_URL 逐表单查 (沿用 bionic_trader 的踩坑结论)。保留本键仅为与 bionic
    #   的 .env 对齐, 连不通不影响任何功能。
    DB_MYSQL_URL: str = "mysql+pymysql://user:pass@192.168.18.199:3306/db_gp_cj"
    #   大盘指数 zs_day_data (页面区制提示用, 非约束)

    SIGNAL_REDIS_HOST: str = "192.168.18.208"
    SIGNAL_REDIS_PORT: int = 6379
    SIGNAL_REDIS_PASSWORD: str = ""
    SIGNAL_REDIS_DB_INTRADAY: int = 2      # 决策系统盘中信号流 (PMS 订阅消化)
    SIGNAL_REDIS_DB_ACTIONS: int = 3       # 决策系统风控 SELL 流 (PMS 订阅消化)
    SIGNAL_REDIS_DB_QUOTES: int = 13       # 实时分钟行情 (择时/扫描)
    SIGNAL_REDIS_SOCKET_TIMEOUT: int = 5

    PMS_REDIS_URL: str = "redis://:pass@192.168.16.150:6379/8"
    # 上游信号(mtf 实况分)只读源 —— 214 db0。密钥只从 .env 注入(见下「密钥」纪律), 代码默认留空。
    # .env 里加一行:  UP_MR_REDIS_URL=redis://:<密码>@192.168.16.214:36379/0
    # (208 上的决策系统/择时层/资金异动几条流复用现有 SIGNAL_REDIS_*, 无需另配。)
    UP_MR_REDIS_URL: str = ""
    #   PMS 自己的 Celery 总线, 独立 db=8 (决策系统用 db7, 物理同机逻辑隔离)

    PMS_WEB_HOST: str = "0.0.0.0"          # 管理页面 (FastAPI + 单页)
    PMS_WEB_PORT: int = 38100

    # =========================================================
    # 二、业务参数「初值」(运行时以 pms_runtime_param 表为准, 页面可调)
    # =========================================================
    # --- 资金与总体 (对应参数命令) ---
    PMS_TOTAL_SCALE: float = 2_000_000     # 总操作规模 (元)
    PMS_PORTFOLIO_CAP: float = 0.60        # 总仓上限 (占规模)
    PMS_STOCK_CAP: float = 0.08            # 单股上限
    PMS_STOCK_TARGET_DEFAULT: float = 0.06 # 默认单股目标仓位
    PMS_MAX_NAMES: int = 15                # 最大持仓数
    PMS_CASH_RESERVE: float = 0.0          # 预留现金比例 (永不动用)
    PMS_AUTONOMY: str = "propose_only"     # full / propose_only / off

    # --- 批次与安全垫 ---
    PMS_BATCH_SPLIT: str = "0.5,0.25,0.25" # 底仓/回踩补足/盈利加仓
    PMS_CUSHION_SOLID: float = 0.03        # 安全垫厚垫线 (解锁加仓)
    PMS_TRIM_PEAK: float = 0.06            # 保垫减仓: 垫子峰值门槛
    PMS_TRIM_GIVEBACK: float = 0.5         # 保垫减仓: 回吐比例门槛
    PMS_DCA_TRIGGERS: str = "-0.08,-0.15"  # 补仓评估档
    PMS_DCA_DEEP_CONFIRM: float = -0.15    # 此档及更深永远需用户确认
    PMS_DCA_MAX_RATIO: float = 0.5         # 补仓上限 (占底仓)
    PMS_NO_CHASE_MA5: float = 0.06         # 距 MA5 超此幅度不追买
    PMS_BUILD_WINDOW_TDAYS: int = 10       # 建仓期窗口 (交易日)
    PMS_FILL_MAX_LOSS: float = -0.03       # 浮亏深于此不走回踩补足 (转补仓域)
    PMS_WEAK_NEG_DAYS: int = 5             # 降仓「清弱票」判定: 安全垫连续为负 N 日 (设计 §3.2)
    PMS_PROPOSAL_TTL_HOURS: int = 24       # 自主提议待确认有效期 (超时置 EXPIRED)

    # --- 自主新建仓 (动作引擎第五类动作 OPEN, 2026-08-06) ---
    #     让自主提议除了管已有持仓, 也能从上游候选池里挑新票建底仓, 走同一条
    #     规则闸 → 研判闸 → 档位分流。要解决的是「每天靠人下一条升仓命令」这件事。
    #     **刻意不设每日开仓上限**: 名额与金额在 action_engine.scan_open 里边走边扣,
    #     候选数天生不超过剩余名额; 再往后还有择时的买入区间做天然过滤 (现价不在区间内
    #     一律等待、不追)。每天实际建成几只由这几层自己收敛, 不由一个人为的数字决定。
    PMS_OPEN_AUTONOMY: str = "full"        # 新建仓档位兼总开关: full / propose_only / off
    #   与 PMS_AUTONOMY 分开的理由: 「新建仓要不要人点头」和「加仓要不要人点头」是两个
    #   不同的决定, 不该被一个开关捆住。off = 根本不扫描新建仓, 行为回到 2026-08-06 之前。
    PMS_OPEN_REQUIRE_WS_CASH: bool = True  # 拿不到 ws 资金快照时不自动新建仓 (写 skipped 留痕)
    #   规则闸那条「拿不到真实资金只告警不拦」是为**已经排好的命令**设计的 —— 通道故障
    #   不该升级成业务停摆。但无人值守地从零建仓是可以等的, 所以这里比规则闸严一档:
    #   07-30 教训 (scale 200 万 / 账户实际 98 万, 方案一路放行到下游才被拒) 不能重演。
    PMS_OPEN_SIGNAL_PRIORITY: bool = True  # 今天被决策系统判过盘中转多的候选票排最前
    #   决策系统判出 REVERSAL_BUY 时会往 db2 的 intraday_signals 流广播, PMS 一直订阅得到,
    #   但 2026-08-06 之前 signal_rules 把买入类信号一律归进"只留痕", 而留痕又只给持仓票写 ——
    #   于是「决策系统今天看多了某只没持仓的票」这件事, PMS 收到了却一个字都不留。
    #   现在补上了留痕 (action=SIGNAL_BUY, verdict=NOTE), 动作引擎读它给候选排序。
    #   **只影响先后, 不影响资格**: 不在候选池里的票不会因为有信号就被建仓。
    PMS_OPEN_REF_DRIFT_MAX: float = 0.03   # 参考位盘中被改写超此幅度 → 该票当日暂停新建仓
    #   择时读的 strategy_daily_results 会被盘中补扫就地改写 (实证: 002335 支撑 31.27→29.00,
    #   差 7.3%)。已有持仓有摊薄成本与安全垫做锚, 漂一点不翻转判断; 而新建仓的买入区间
    #   完全由支撑压力推出来, 区间一平移, 上午「不追」的票下午可能变成「出手」, 而无人在看。
    #   处置: 当日首答把 observed 的支撑/压力/区间锁进指令 progress.ref_lock, 之后每答比对。

    # --- 上游选股计划接口 (候选池的事实源; 口径与待确认项见 UPSTREAM_PLAN_API.md) ---
    #     上游只回答「买什么、排第几」, 不给价格金额 —— 买多少/什么价是 PMS 自己算。
    #     接口拿不到一律显式失败 (候选池为空 + ERROR), 绝不静默回退旧表。
    PMS_CANDIDATE_SOURCE: str = "plan_api"  # plan_api(默认) / buy_plan(旧表) / both(并集)
    #   计划服务与 PMS 同机: 走 host.docker.internal (compose 里配了 host-gateway)。
    #   容器直连宿主机物理 IP 192.168.16.155:8300 会被宿主机 INPUT 链丢包 (超时非拒绝);
    #   若日后计划服务搬到别的机器, 这里直接填那台的 IP:端口。空=停用, 候选池恒为空。
    PMS_PLAN_API_BASE: str = "http://host.docker.internal:8300"
    PMS_PLAN_API_PATH: str = "/plan"
    PMS_PLAN_TIMEOUT: int = 10             # 单次请求超时 (秒)
    PMS_PLAN_CACHE_SEC: int = 300          # 计划缓存秒数 (上游日频产出, 没必要每跳都拉)
    #   下面三个是**发给上游的请求参数** (对方 api.py: top=20/obs_top=10/theme_cap=5 都是
    #   可传的, 不是它的既定政策)。0 = 不传, 用上游默认。要个宽池子回来, 主题分散交给
    #   PMS_PLAN_THEME_CAP_LOCAL 在候选阶段做 —— 理由见 plan_feed.select_candidates。
    PMS_PLAN_TOP: int = 300                # 上游 top: 主榜要多少条
    PMS_PLAN_OBS_TOP: int = 100            # 上游 obs_top: 观察档要多少条
    PMS_PLAN_THEME_CAP: int = 999          # 上游 theme_cap: 让它别裁 (PMS 自己裁)
    PMS_PLAN_THEME_CAP_LOCAL: int = 5      # PMS 侧同主题限额, 在 TOP_N 截断**之前**生效, 0=不限
    PMS_PLAN_TOP_N: int = 30               # 主榜按 score 降序取前 N 进池 (900+ 只是排序池不是清单)
    PMS_PLAN_TIERS: str = "强传导"           # 传导档白名单; 留空=不按档过滤
    #   **已定纪律 (2026-07-31, 用户): 候选宁缺毋滥, 不因当天强传导少就放宽到弱传导。**
    #   上游文档说强传导规模随市场结构波动、无板块传导的日子可能一个都没有 —— 那种日子
    #   候选池就该是空的。只有"热度启动信息经确认"时才考虑放宽, 且要显式改这个参数。
    PMS_PLAN_INCLUDE_OBSERVE: bool = False # 观察档是否进候选池 (上游把它定位为备选, 无 upside)
    PMS_PLAN_MIN_SCORE: float = 0.0        # score 下限, 0=不设
    PMS_PLAN_MIN_SOURCES: int = 0          # evidence.n_sources 下限, 0=不设
    PMS_PLAN_MIN_UPSIDE: float = 0.0       # 预期空间下限 (0.5=+50%), 0=不设。券商目标价
    #   口径, 噪音大 (榜首能到 +212%), 只做下限过滤, **不参与排序** —— 排序始终是 score
    PMS_PLAN_STALE_TDAYS: int = 1          # 计划日龄超此交易日数即判过期并拒用 (防上游停更)
    PMS_PLAN_EXCLUDE_ST: bool = True       # 剔除 ST/*ST/退市 (上游明确不剔除, 这道闸归 PMS)
    PMS_PLAN_THEME_SYNC: bool = False      # 把 theme 灌进 pms_industry_map。**默认关**:
    #   theme 是事件驱动的传导主题, 覆盖率约三成且词表不规范, 当行业标签会让集中度约束
    #   天天漂 —— 行业源请用 PMS_SECTOR_SOURCE=gp_stock_category
    PMS_PLAN_QUERY_EXTRA: str = ""         # 附加查询串逃生口, 如 "foo=1"。上游哪天加了新参数
    #   不用改代码即可透传; top/obs_top/theme_cap 已有显式参数, 同名键以显式参数为准

    # --- 榜单变化提示 (PMS 自算; 上游 changes 字段恒为 null, 见 UPSTREAM_PLAN_API.md §9) ---
    PMS_PLAN_SNAPSHOT: bool = True         # 每次拉计划落一份名册快照 (变化提示的底本)。
    #   关掉只是没有变化提示, 不影响候选池; 但同时也失去"手上这份是哪一版"的自证能力
    PMS_PLAN_SNAPSHOT_KEEP: int = 200      # 快照保留份数 (一份 300 只约 20KB)
    PMS_PLAN_DIFF_RANK_JUMP: int = 50      # 名次跳变多少名才报
    PMS_PLAN_DIFF_TAIL_GUARD: float = 0.5  # 榜尾进出的噪音闸: 排名进入榜单前这个比例才算数。
    #   榜是按 top 截断过的 —— 从 rank 250 掉到 310 只是被截掉, 不是上游摘了它。不设这道闸
    #   每天会多几十上百条假掉榜, 提示就没人看了。1.0=只挡榜长之外, 0=一律当噪音

    # --- 行业约束 (硬拦截; 数据源接口化) ---
    #   行业划分数据源。gp_hybk = 199 库的行业板块表 (三级 884*, 每票只认一个主行业),
    #   2026-07-31 起的正式口径; gp_stock_category 实测不可用 (无 stock_code 列), 勿用。
    PMS_SECTOR_SOURCE: str = ""            # "" = 停用并页面提示 / gp_hybk / custom_table
    PMS_SECTOR_HYBK_LEVEL: str = "l3"      # gp_hybk 取哪一级: l3=884(三级, 默认) / l2=881(二级)
    PMS_SECTOR_MAX_NAMES: int = 4
    PMS_SECTOR_MAX_RATIO: float = 0.40     # 占总仓

    # --- 择时执行 (内置实现B; 决策系统接通后切实现A) ---
    PMS_EXEC_WINDOW_TDAYS: int = 3         # 任务命令默认执行窗口
    PMS_SELL_AVOID_OPEN_MIN: int = 30      # 卖出避开开盘 N 分钟
    PMS_BUY_HALT_DAYUP: float = 0.05       # 当日涨幅超此停止买入
    PMS_EOD_FORCE_TIME: str = "14:45"      # 当日配额兜底时点
    PMS_EOD_FORCE_DISCOUNT: float = 0.998  # 兜底限价 = 现价 × 此系数 (卖出)
    PMS_MIN_LOT_MERGE: bool = True         # 一手检查: 批次自动合并
    PMS_DISPATCH_EXPIRE_MIN: int = 30      # 指令下发后未被接受的过期时间
    PMS_DISPATCH_MODE: str = "shadow"      # 下发通道: shadow(影子,默认) / ws(直连QMT,待实现)
    PMS_EXEC_SLICES: int = 1               # 当日配额分几笔出手 (设计「分笔卖出配额」)
    PMS_ORDER_TTL_MIN: int = 10            # 单个下发分片的挂单有效期(交易分钟), 到点下游自动撤
    # --- 卖出分桶收口与紧急直通 (2026-08-17 清仓择时改造) ---
    #     「现价不低于均价才卖」是只在强势时放行的条件, 下跌日全天不满足, 全部数量会
    #     堆到 14:45 一笔打出。分桶: 桶内照旧择价, 桶收口无条件补齐该桶份额, 把跌日的
    #     强制完成分散到多个时点。留空 = 不分桶, 行为与改造前一致。最后一桶恒为兜底时点。
    PMS_SELL_BUCKET_TIMES: str = "11:30,14:00"
    PMS_URGENT_SELL_DISCOUNT: float = 0.995  # 紧急卖出限价系数 (一键清仓/高置信风控清仓
    #     直通出手用, 比兜底的 0.998 更激进; 每分钟按新现价重定, 下跌中追着走)

    # --- 择时实现A (委托决策系统, 待办 #9; 契约见 BIONIC_PMS_INTERFACE.md) ---
    #     决策系统按它凌晨算好的支撑/压力给出执行区间, 盘中只回答「现价在不在区间内」;
    #     配额/兜底/停牌/一字板/不追高(当日涨幅) 这些检查始终留在 PMS 本地。
    #     不可用自动退实现B (设计 §13), 绝不因对端故障停出手。
    PMS_EXEC_IMPL: str = "B"               # B=内置保守择时(默认) / A=委托决策系统
    PMS_EXEC_API_BASE: str = ""            # 实现A接口根地址; 留空=沿用 PMS_JUDGE_API_BASE (同一个 bionic 服务)
    PMS_EXEC_PATH: str = "/api/intraday/pms_exec"
    PMS_EXEC_TIMEOUT_SEC: int = 8          # 咨询超时(秒)。盘中 tick 等不起长超时, 超了本轮退B
    PMS_EXEC_ADVICE_TTL_MIN: int = 10      # 应答有效期(交易分钟), 期内不重复咨询; 对端 valid_min 只缩不放
    PMS_EXEC_FAIL_COOLDOWN_MIN: int = 5    # 咨询失败后的冷却(交易分钟), 冷却内直接走B不再咨询
    PMS_EXEC_LIMIT_BAND: float = 0.10      # 建议价偏离现价超此幅度视为异常数据, 改用本地口径。
    #   建议价通常是执行区间的边缘, 离现价百分之几属正常, 所以放到 10% (A股单日涨跌幅上限);
    #   真被这条改写时 reason 里会说明

    # --- QMT WebSocket 直连通道 (协议见 QMT_WS_PROTOCOL.md V1.0) ---
    #     连接由 pms-ws 常驻进程独占 (app/ws/runner.py); executor 经 pms_qmt_order
    #     出口表递单, 详见 app/services/dispatcher.py 头部的「进程边界」一节。
    PMS_QMT_WS_URL: str = "ws://192.168.16.98:9443/pms"
    PMS_QMT_WS_ENABLED: bool = False       # pms-ws 进程总开关; False = 空转不连接不写心跳
    PMS_QMT_ACK_BATCH: int = 20            # 累积确认: 每落库 N 条发一次 ack_seq
    PMS_QMT_ACK_INTERVAL_SEC: float = 2.0  # 累积确认: 或每 N 秒发一次 (取先到)
    PMS_QMT_HEARTBEAT_SEC: int = 5         # ping 间隔
    PMS_QMT_IDLE_TIMEOUT_SEC: int = 15     # 超过此时长未收到对端消息即重连
    PMS_QMT_OUTBOX_POLL_SEC: float = 0.5   # 出口队列轮询间隔 (择时本是分钟级, 无需更快)
    PMS_QMT_HEARTBEAT_DB_SEC: int = 2      # ws 进程写存活心跳的间隔
    PMS_QMT_HEARTBEAT_STALE_SEC: int = 15  # 心跳陈旧超此秒数 → 判定进程已死, dispatcher 拒发
    PMS_QMT_CONNECT_TIMEOUT_SEC: int = 10  # 建连超时
    PMS_QMT_QUERY_INTERVAL_SEC: int = 300  # 全量快照轮询 query_positions+query_funds (协议 §6.2)
    PMS_QMT_SEND_MAX_ATTEMPTS: int = 3     # 单张委托发送重试上限, 试满置 SEND_FAILED
    # 下面两项是**密钥**: 只从 .env 注入, 不入库、不进 ParamStore、不上页面 (协议 §10.1.1)。
    # param_store.SECRET_KEYS 已把它们挡在可调参数与页面快照之外。
    PMS_QMT_SIGN_SEED_HEX: str = ""        # PMS 私钥 seed (Ed25519, 32 字节 hex)
    PMS_QMT_PEER_PUBKEY_B64: str = ""      # QMT 公钥 (base64), 验上行签名用

    # --- 风险披露与刹车 ---
    PMS_RISK_WARN_ENTRY: float = 0.01      # 单笔敞口告警线 (占规模)
    PMS_RISK_WARN_PORTFOLIO: float = 0.06  # 组合敞口告警线
    PMS_BRAKE_DRAWDOWN: float = 0.05       # 组合刹车: 高水位回撤
    PMS_BRAKE_DAYS: int = 3                # 刹车持续交易日
    PMS_STOP_ATR_MULT: float = 2.0         # 自算止损参考: 成本 - N×ATR
    PMS_REF_STALE_TDAYS: int = 3           # 决策系统结论日龄超此转自算兜底

    # --- 研判闸 (委托决策系统) ---
    PMS_JUDGE_ENABLED: bool = True
    PMS_JUDGE_ACTIONS: str = "FILL,ADD,DCA,SWITCH,OPEN"
    PMS_JUDGE_TIMEOUT: int = 90            # 超时 → 降级 propose_only
    PMS_JUDGE_TICK_BUDGET_SEC: int = 150   # 单轮提议扫描用于研判的时间预算 (秒)
    #   **这不是节流, 是让一次心跳做得完。** judge.request 是同步阻塞的, 单次上限 90 秒,
    #   而 scheduler 给所有调度任务设的软超时是 240 秒 —— 三只票送研判就顶破了。
    #   以前送研判的只有三只持仓票且多数轮次被去重挡掉, 所以没撞上; 新建仓上线后
    #   冷启动那天会有十来条, 第一跳就会把任务打死在中途 (而且是已经落了一部分表之后)。
    #   预算用尽时本轮剩下的候选整条跳过并写 skipped, 下一分钟的心跳接着做 —— 一条都不丢,
    #   也没有任何按天计的上限。候选按分数降序, 先做完的一定是分数最高的那些。
    PMS_JUDGE_API_BASE: str = ""           # 决策系统 PMS 研判接口根地址; 空=未接通(自动降级人工确认)
    PMS_JUDGE_PATH: str = "/api/intraday/pms_judge"   # 研判接口路径 (bionic 侧配套改造后确定)

    # --- T0 做T (命令授权制) ---
    PMS_T0_RATIO_MAX: float = 0.333        # T仓硬上限 (占持仓)
    PMS_T0_PULLBACK_PCT: float = 0.03      # 正T: 距当日高点回落触发
    PMS_T0_RALLY_PCT: float = 0.05         # 反T: 日内涨幅触发
    PMS_T0_ROUND_TARGET: float = 0.015     # 单次T目标价差
    PMS_T0_CLOSE_TIME: str = "14:50"       # 强制平回时点
    PMS_T0_STOCK_DAY_LOSS: float = 0.003   # 单票当日T亏熔断 (占规模)
    PMS_T0_GLOBAL_DAY_LOSS: float = 0.01   # 全局当日T亏熔断
    PMS_STRATEGY_ENABLED: bool = False     # 个股交易方案(策略)层总开关; False=strategy_runner 空转不评估

    # --- 决策系统信号消化 (设计 §10) ---
    PMS_SIGNAL_ENABLED: bool = True
    PMS_SIGNAL_GROUP: str = "pms_signal_consumer"   # 独立消费组, 不与 trading_service 抢消息
    PMS_SIGNAL_CONSUMER: str = "pms_1"
    PMS_SIGNAL_STREAM_INTRADAY: str = "intraday_signals:{ymd}"   # db2, 每日一条流
    PMS_SIGNAL_STREAM_SELL: str = "bionic:signals:llm_sell_actions"  # db3, 固定 key
    # 2026-08-18 实测: bionic 对持仓票的卖出置信度(0~100 证据一致性)多落在 60~84,
    # 原门槛 0.75 把大量持仓票的卖出静默忽略。降到 0.60: 60~84 一律浮上来变提议(可见、
    # 一键采纳), ≥85 仍自动清仓; 加上 signal_service 对持仓票的"未采纳也留痕", 不再丢信号。
    PMS_SIGNAL_SELL_CONF_MIN: float = 0.60   # 低于此置信度的卖出信号不消化(仅持仓票留痕)
    PMS_SIGNAL_AUTO_EXIT_CONF: float = 0.85  # 高于此置信度直接转清仓指令, 之间则落提议
    PMS_SIGNAL_TRIM_RATIO: float = 0.3333    # 中等置信度时的减仓比例

    # --- 宏观择时层 (股汇对冲指数 → 组合仓位命令 + 个股宏观闸) ---
    #     方案 MACRO_TIMING_PLAN.md; 阈值与对数映射参数依据标定报告 MACRO_CALIB_2026-08-18.md。
    #     模拟仓拍板 (2026-08-18): 默认总开关开 + 全自动档, 不设观察期; **转实盘前
    #     改回 False / propose_only**。ENABLED 已进 param_store.FAIL_CLOSED: 参数表
    #     读不到时按关闭处理, 基础设施故障期间宁可不动。
    PMS_MACRO_ENABLED: bool = True         # 总开关; 关 = 调度位空转, 不取数不计算
    PMS_MACRO_AUTONOMY: str = "full"       # off 只算不动 / propose_only 出建议 / full 自动下命令
    PMS_MACRO_SIGNALS: str = "hedge_fx"    # 启用的信号清单 (逗号分隔; 注册表在 macro_service)
    PMS_MACRO_STOCK_GATE: bool = True      # 个股宏观闸: 偏热确认期间暂停自主增持与策略买开腿
    PMS_MACRO_HOT_TH: float = 25.0         # 偏热进入阈值 (标定: 约 1.4 段/年)
    PMS_MACRO_COLD_TH: float = -20.0       # 偏冷进入阈值 (标定: 约 4 段/年, 用户确认取 -20)
    PMS_MACRO_EXIT_BAND: float = 15.0      # 迟滞退出带; 偏热侧它同时是降仓命令的触发线
    PMS_MACRO_CONFIRM_DAYS: int = 1        # 偏冷进区连续 N 个交易日才触发 (标定: 等第二天更差)
    PMS_MACRO_TRIGGER_HOT: str = "zone_exit"    # 偏热口径: 回落再动 (标定: 机会成本最低)
    PMS_MACRO_TRIGGER_COLD: str = "zone_enter"  # 偏冷口径: 进区即动 (标定: 反弹在极值当口)
    PMS_MACRO_LOG_S0: float = 0.049        # 对数映射 target = S0·ln(1+e/k), 标定回填
    PMS_MACRO_LOG_K: float = 1.6           # 对数映射尺度 k, 标定回填
    PMS_MACRO_SHIFT_MAX: float = 0.20      # 单个极值周期累计调整封顶 (占规模百分点)
    PMS_MACRO_STEP_MIN: float = 0.02       # 裁剪后步长小于此不动; 也是确认当日首步保底
    PMS_MACRO_COOLDOWN_TDAYS: int = 3      # 同方向两次动作最小间隔 (交易日)
    PMS_MACRO_MIN_PCT: float = 0.20        # 宏观降仓地板 (用户命令不受此限)
    PMS_MACRO_MAX_PCT: float = 0.0         # 宏观升仓天花板; 0 = 不单设, 由总仓上限兜底
    PMS_MACRO_WINDOW_TDAYS: int = 3        # 宏观命令的执行窗口
    PMS_MACRO_RESPECT_BRAKE: bool = True   # 组合刹车期间不自动升仓 (方案唯一假设项)
    PMS_MACRO_STALE_TDAYS: int = 3         # 任一数据源日龄超此 (交易日) → 信号不可用
    PMS_MACRO_RET_WIN: int = 20            # 收益计算窗口
    PMS_MACRO_Z_WIN: int = 40              # 标准化窗口
    PMS_MACRO_SHIBOR_BETA: float = 0.02    # 利率修正系数 β

    # --- 对账与回放 ---
    PMS_REPLAY_INTERVAL_MIN: int = 5
    PMS_RECON_ALARM_DAYS: int = 3          # 连续不一致 N 日升级 ERROR
    # 持仓事实源: ws_first(默认, ws 快照为主/表为兜底) / ws_only / table_only
    # 目标架构下 trading_service 全量退出业务, 那张表没有明确写入方; 协议 §6.2 定的也是 ws 快照。
    PMS_RECON_SOURCE: str = "ws_first"
    PMS_RECON_WS_SNAPSHOT_MAX_AGE_SEC: int = 900   # ws 快照超此秒数视为过期 (轮询 300s + 余量)


settings = Settings()
