# -*- 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" # 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 # 单个下发分片的挂单有效期(交易分钟), 到点下游自动撤 # --- 择时实现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亏熔断 # --- 决策系统信号消化 (设计 §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 PMS_SIGNAL_SELL_CONF_MIN: float = 0.75 # 低于此置信度的卖出信号不消化 PMS_SIGNAL_AUTO_EXIT_CONF: float = 0.85 # 高于此置信度直接转清仓指令, 之间则落提议 PMS_SIGNAL_TRIM_RATIO: float = 0.3333 # 中等置信度时的减仓比例 # --- 对账与回放 --- 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()