commit dc96db734f53c44927bf9e4f09f15df333d27981 Author: zlt Date: Mon Jul 27 15:50:57 2026 +0800 设计定型后提交 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..c492292 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,39 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..b6d926b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/tradingSystem.iml b/.idea/tradingSystem.iml new file mode 100644 index 0000000..8b8c395 --- /dev/null +++ b/.idea/tradingSystem.iml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/POSITION_MGMT_DESIGN.md b/POSITION_MGMT_DESIGN.md new file mode 100644 index 0000000..6be7730 --- /dev/null +++ b/POSITION_MGMT_DESIGN.md @@ -0,0 +1,329 @@ +# tradingSystem · 持仓管理系统总体设计 (PMS) + +> 状态: **V0.4,设计定稿·开发启动** | 日期: 2026-07-27 +> **V0.4 修订(依用户六点反馈)**:①命令目录**不再分期,一次性全部实现**——功能开发一次到位,上线按外部依赖分三步切换(§14);②新增独立交付物 `QMT_INTERFACE_REQUIREMENTS.md`(与 QMT 侧协商的数据与接口需求清单,含资金快照与指令通道建议 DDL);③决策系统配套修改获批,可直接开发;④行业划分接口给出具体方案(§5:`pms_industry_map` 自定义映射表 + 可插拔适配器);⑤管理页面技术形态确认(FastAPI + 单页)。 +> **V0.3 修订(依用户九点反馈)**:①术语可读性修订,清除"买腿/卖腿/回轧"等机翻词;②**重大更正——决策系统保持在线**:形态计算、每日定时分析、盘中研判等核心功能一概不动,改动仅限"执行出口改道"(与 QMT 侧的直接读写一期即切断,权限移交持仓系统),V0.2 按"决策系统整体下线"的理解作废;③上游量化系统输出不变,**持仓系统的建仓计划围绕上游输出建立**;④一期 autonomy 默认"提议待确认"获确认;⑤`trading_order`/`trading_position` 仍归下游系统维护,仅管理权限由决策系统移交持仓系统;⑥参数设置、命令下达与数据展示以**可视化后端页面**实现;⑦命令场景目录扩充(§3.1,待用户决断取舍);⑧内置择时规则全部参数化、页面可调;⑨行业集中度改为**硬性拦截**,行业划分依据留接口。 +> 沿革:V0.1 中间层/两级闸门/安全垫三义 → V0.2 命令驱动/百分比仓位 → 本版。冲突处以本版为准。 +> 关联文档(bionic_trader 仓库): `DECISION_LOOP_DESIGN.md`、`DATA_MODEL.md`、`PIPELINES.md`。写作纪律沿袭既有文档:术语先白话、阈值给硬数字且可配置、失败路径显式、未确认事项进清单。 + +--- + +## 0. 一句话目标与角色分工 + +**一句话**:用户通过管理页面下达大方向命令(规模、上限、升降仓、对某股做T),持仓系统把命令翻译成分股分批的行动方案并管好账本与纪律,决策系统负责研判与择时,下游系统负责挂单成交——四层各司其职,谁也不越权。 + +| 层 | 职责 | 频次 | +|---|---|---| +| 用户 | 中低频大方向命令:总规模 200 万、总仓上限 60%、单股上限 8%、降仓 10%、对某股做T、清仓某股 | 天/周级 | +| **持仓系统(本系统,PMS)** | 命令解析 → 方案制订(哪只股、什么动作、多少数量)→ 账本/批次/安全垫/纪律 → 指令编排与下发 | 持续 | +| 决策系统(bionic_trader) | **核心功能照旧**:每日认知闭环、形态计算、盘中研判、风控仲裁、反思闭环;对持仓系统提供研判与择时服务。**不再直接指挥下游** | 每日 + 盘中 | +| 下游系统(QMT 侧) | 挂单、成交、维护 `trading_order` / `trading_position`(两表仍归其维护) | 盘中 | + +**V0.3 已定决策**: +1. **命令驱动**:用户命令是最高约束与主要驱动源;持仓系统的自主判断(如"跌到 −15% 能不能补仓")是第二驱动源,受命令参数约束。 +2. **连接关系(一期即生效)**:持仓系统 ↔ 决策系统直接交互;持仓系统 ↔ 下游系统交互;**决策系统与下游系统断开**——对下游的管理权限(下单、审批、卖出指挥)由决策系统移交持仓系统。决策系统的分析产出全部保留并成为持仓系统的输入。 +3. **上游不变**:上游量化系统的输出(买入计划、告警、评分池)原样保留,持仓系统的建仓计划围绕这些输出建立。 +4. **决策方式**:规则引擎为主 + 关键点研判。仓位数学全部确定性代码;定性研判(该不该补仓、回踩是洗盘还是出货、何时出手)委托决策系统——它本来就是研判系统,持仓系统不另建一套研判栈。 +5. **仓位框架**:百分比制(占总规模的 %),单股分批 50/25/25,含一手可行性检查;风险敞口只做披露告警,不反推数量。 +6. **事实源**:命令 = 用户;实际持仓/成交 = 下游两表(持仓系统账本定期对账);研判 = 决策系统。 + +--- + +## 1. 总体架构与决策系统配套修改清单 + +``` + 用户(管理页面:参数设置 / 命令下达 / 进度与账本查看 / 提议确认) + │ + ┌─────────▼──────────────────────────────────────────────┐ + │ 持仓系统 tradingSystem (PMS) │ + │ 命令系统 → 方案生成器 → 持仓账本(批次/成本/安全垫) │ + │ → 动作引擎(命令驱动 + 自主提议) → 规则闸 → 研判/择时 │ + └────┬────────────────────────────▲───────────────────────┘ + │ 指令(买入/卖出, 带数量限价) │ 研判与择时答复 / 每日结论 / 盘中信号 + ▼ │ + 下游系统(QMT侧) 决策系统 bionic_trader + 挂单/成交/两表维护 (核心分析功能照旧, 与下游断开) + │ ▲ + └── 成交回放/对账 ──▶ PMS ────┘(上游量化输出 → 供 PMS 建仓规划 + 供决策系统分析, 均不变) +``` + +**三条铁律**: + +1. **命令至上**:任何自动决策不得突破用户命令参数;任务命令与自主动作冲突时命令优先,在途自主指令自动撤销;命令之间冲突由用户在页面裁决,系统不代替用户仲裁。 +2. **分工不越权**:持仓系统决定「做什么、多少」,决策系统决定「该不该、何时」(定性研判与择时),下游只管执行。决策系统研判服务不可用时,持仓系统降级为保守规则 + 人工确认,**不自建第二套研判**。 +3. **先记账后动作 + 故障即守成**:指令先落表(幂等键)再下发;持仓系统故障 = 不产生新指令、持仓保持现状;每条降级路径显式日志;账本与下游定期对账,以下游为实际持仓事实源。 + +**决策系统配套修改清单**(原则:**分析核心零改动,仅执行出口改道**;在 bionic_trader 仓库实施): + +| 类别 | 内容 | +|---|---| +| 零改动(核心功能,已复查确认保留) | 每日认知闭环(22:30 扫描 → 信号/支撑压力/研报/预测曲线)、视觉形态识别、宏观水温、盘中事件研判(watcher/审计)、风控仲裁、持仓体检 HOLD_CHECK、watch_conditions、反思闭环 L1/L2/L3 与运营日报、月度记忆挖掘、对外 API | +| 出口改道 | ①ENTRY_GATE 不再回写 `trading_buy_plan`(建仓审批权移交 PMS),其仲裁能力转为**应 PMS 请求的建仓研判服务**;②风控 SELL、盘中 ENTRY/EXIT 广播照常产出,但**下游停止直接执行**,改由 PMS 订阅消化后统一决定卖出指令;③持仓体检、止盈研判结论同样以信号形式供 PMS 消费 | +| 新增(接入性) | 研判服务扩展:`process_intraday_audit` 新增 PMS 请求类 direction(建仓研判 / 补仓研判 / 择时研判),输入由 PMS 提供 context,输出结构化裁决给 PMS——复用其既有仲裁哲学与代码路径,不重写 | + +--- + +## 2. 术语表 + +| 术语 | 白话解释 | +|---|---| +| 参数命令 | 改变长期约束的命令(规模/上限/持仓数/自主档位/做T授权等),立即生效并持久化,页面可随时修改 | +| 任务命令 | 有生命周期的一次性使命(降仓 10%、清仓某股、建仓某股至 6%),状态机 待处理→规划中→执行中→完成/撤销,进度可查、可撤销 | +| 方案 (plan) | 一条任务命令展开成的分股行动清单(A 减半、B 清仓、C 停止买入…),每项带数量、优先级、执行窗口 | +| 择时 | 一条指令在执行窗口内的具体出手时机,由决策系统研判;未接通或不可用时用内置保守规则 | +| 底仓 / 批次 | 单股目标仓位按 50%/25%/25% 分三批:底仓(BASE)/回踩补足(FILL)/盈利加仓(ADD);另有补仓(DCA)/做T(T0) 批 | +| 安全垫 (cushion) | (现价 − 摊薄成本)/摊薄成本,含做T利润。≥3% 为厚垫(SOLID),解锁加仓资格 | +| 自主提议 | 非命令触发的动作候选(规则扫描出的补足/加仓/补仓/减仓机会),按自主档位决定自动执行还是待用户确认 | +| 自主档位 (autonomy) | `full`(闸门与研判通过即执行) / `propose_only`(增持类全部待用户确认,一期默认) / `off`(只执行命令) | +| 一手检查 | A股 100 股起买。批次金额买不足一手 → 自动合并批次(50/25/25→60/40→100),仍不足则放弃该股并提示 | + +代码格式:PMS 内部一律 Tushare 点式(`600000.SH`);读决策系统结论表转前缀式。金额单位元,数量单位股(整百)。 + +--- + +## 3. 命令系统与管理页面 + +### 3.1 命令场景目录(已定:不分期,全部一次性实现) + +**A. 资金与总体参数(参数命令)** + +| 命令 | 语义与要点 | +|---|---| +| 设定总规模 | 总操作资金(如 200 万)。注资/抽资即改此值,PMS 重算所有百分比约束;抽资导致超限 → 生成降仓提议供确认 | +| 设定总仓上限 | 如 60%。调低后超限 → 生成降仓提议供确认(不自动强平) | +| 设定单股上限 / 默认目标仓位 | 如 8% / 6% | +| 设定最大持仓数 | 如 15 只 | +| 设定自主档位 | full / propose_only / off | +| 设定预留现金比例 | 总规模中永不动用的部分(如 10%),与总仓上限双重约束 | + +**B. 组合级动作(任务命令)** + +| 命令 | 语义与要点 | +|---|---| +| 降仓 X%(窗口 N 日) | §3.2 全流程示例。优先级:停新买 → 清弱票 → 收利润 → 最后动底仓 | +| 升仓 X%(窗口 N 日) | 加仓对象来源:①既有持仓中垫厚且决策系统看多的票补到目标 ②上游计划池 + 决策系统研判筛出的新票建仓 | +| 全局暂停买入 / 恢复 | 避险冻结:停止一切新增买入(在途买入指令撤销),卖出与止损不受影响 | +| 全局暂停执行 / 恢复 | "休假模式":所有自动动作暂停,仅保留账本对账与日报 | +| 一键清仓(紧急) | 全部持仓按最快节奏卖出(窗口默认 1 日,不做择时优化),页面二次确认后执行 | +| 清仓某行业 / 限制某行业上限 | 依赖行业划分接口(§5),数据源未配置时此类命令置灰并提示 | + +**C. 个股级动作(任务/参数命令)** + +| 命令 | 语义与要点 | +|---|---| +| 建仓某股至目标 % | 围绕上游计划/用户指定,分批投放(50/25/25),决策系统研判把关 | +| 清仓某股 / 减至 X% | 窗口内分日卖出 | +| 冻结某股 / 解冻 | 停止该股一切增持(在途撤销),卖出不受影响 | +| 做T授权 / 取消(t_ratio) | 只有授权的股票才做T;T 仓 = 持仓 × t_ratio(上限 1/3) | +| 设定某股止损价 / 目标价 | 覆盖系统默认参考位,触发即生成卖出方案(止损)或止盈提议 | +| 黑名单(永不买入)/ 白名单(优先规划) | 影响建仓候选筛选 | +| 调整命令窗口 / 撤销命令 | 对在途任务命令 | + +### 3.2 「降仓 10%」全流程(任务命令示例) + +``` +用户在页面下达: 降仓 10% (窗口 3 个交易日) + 1. 规划: 释放金额 = 规模×10% = 20 万, 按卖出优先级凑足: + ①撤销在途买入类任务与指令(先停止继续投入) + ②清弱票: 浮亏且安全垫为负、持续 ≥5 日的票 → 清仓(如 B 股 8 万) + ③收利润: 浮盈票卖出加仓批/补足批与浮盈部分, 保留底仓(如 A 股减半 7 万) + ④仍不足 → 各票按市值等比例微减(凑足 5 万) + 2. 方案落表并在页面展示(逐股动作+数量+优先级+截止日),用户可改可撤 + 3. 执行: 每条卖出指令经规则闸校验 → 决策系统择时(未接通时用内置保守择时)→ 下游挂单 + 4. 每日收盘更新进度(已释放 12/20 万);窗口末日仍未完成 → 按限价兜底完成或顺延一日并告警 + 5. 完成: 页面回执(实际释放额/明细/相对命令日收盘价的执行成本) +``` + +### 3.3 管理页面(一期最小版) + +FastAPI + 单页应用(延续 bionic `dashboard.html` 的轻量风格),四块:**参数设置**(A 类命令全部可视化编辑,改动即持久化并生效)、**命令台**(B/C 类命令下达、在途命令进度与撤销)、**持仓与账本**(每票批次/成本/安全垫/冻结状态,总览仪表)、**提议确认**(propose_only 档位下的待确认队列,一键采纳/驳回)。日报在页面可查。 + +--- + +## 4. 持仓账本(基座) + +**pms_position(一票一行)**:`ts_code / status(计划|建仓中|持有|退出中|已清)` / `frozen_reason(无|命令冻结|刹车|人工)` / `total_qty / avail_qty(T+1 可卖)` / 分类持仓 `base_qty/fill_qty/add_qty/dca_qty/t0_qty` / `avg_cost(摊薄, 含T利润)` / `realized_t_profit` / `cushion_pct/cushion_state/cushion_peak` / `pct_of_scale(市值占规模%)` / 参考位快照(决策系统支撑压力,或兜底自算)/ `t0_enabled` / 纪律计数器。 + +**pms_lot(批次)**:`lot_type(BASE|FILL|ADD|DCA|T0)` / 数量价格日期 / 已实现盈亏。卖出核销次序 **T0 → ADD(新→旧) → DCA → FILL → BASE**(保底仓纪律);清仓命令则全量核销。 + +**成交回放与对账(生命线)**:每 5 分钟增量回放 `trading_order` 成交单入批次(对不上指令的外部成交并入 BASE 并告警留痕);盘中轻对账 + 日终全量对账,**以下游为准**修正并留调整痕迹;除权检测(数量与价格反比突变 → 按比例调整批次,比例不吻合 → ERROR 待人工);`avail_qty` 按 T+1 规则每日重置。 + +--- + +## 5. 仓位框架:百分比制 + +以用户参数命令为纲(默认值即初始配置):**规模 200 万、总仓 ≤60%、单股 ≤8%、持仓 ≤15 只**。 + +``` +单股目标仓位 = 建仓命令指定, 默认 6%(12 万) +分批投放: 底仓 50%(6 万)→ 回踩补足 25%(3 万)→ 盈利加仓 25%(3 万) +补仓 DCA ≤ 底仓的 50%(3 万),计入单股上限,终身一次 +T 仓 = 持仓市值 × t_ratio(命令参数,硬上限 1/3) +一手检查: 任一批金额 < 现价×100 → 批次自动合并(50/25/25 → 60/40 → 100),仍不足一手 → 放弃并提示 +``` + +**风险披露线(不定量,只告警)**:每笔买入指令计算敞口 = 数量 × (买价 − 止损参考)。单笔敞口 > 规模×1% 或组合累计 > 规模×6% → 告警并在研判请求中标红。止损参考 = 决策系统支撑位(主口径)/ 自算参考位(兜底,§13)。 + +**组合层**:总仓上限由用户命令控制(升降仓判断权在用户,PMS 在日报与页面提示大盘区制供参考,不自动联动)。**行业集中度为硬性拦截**:同行业 ≤ 4 只 且 ≤ 总仓的 40%,超限的买入指令直接拒绝。 + +**行业划分接口(具体方案,已定)**:抽象为 `IndustryClassifier.get(ts_code) -> 行业名 | None`,数据源经 `PMS_SECTOR_SOURCE` 切换: +- `custom_table`(**默认建议**):自定义映射表 `pms_industry_map(ts_code 主键, industry 行业名, updated_at)`——用户后续提供何种划分(申万/中信/自定义板块)都只是往这张表灌数据,代码零改动;页面提供简单的查看与导入入口。 +- `gp_stock_category`:现成适配器,接决策系统生态已有的行业表(153 侧),作为映射表就绪前的临时数据源。 +- `""`(未配置):行业约束自动停用,页面显著提示(不静默放行也不误拦),行业类命令置灰。 + +组合刹车:自高水位回撤 ≥5% → 自主增持停 3 个交易日(命令类不受限,执行前提示)。 + +--- + +## 6. 动作引擎(双轨:命令驱动 + 自主提议) + +**命令驱动**动作由方案生成器产出,只过规则闸(上限/一手/冻结/T+1 可卖等合规校验)即入择时队列——用户意志不再送研判。**自主提议**动作由规则扫描触发,先过规则闸,再送决策系统研判(§7),最后按自主档位分流:`full` → 自动执行;`propose_only`(一期默认)→ 落提议队列待用户在页面确认;`off` → 不扫描。 + +**建仓来源(呼应上游不变)**:上游量化系统照常产出买入计划与评分池 → PMS 作为唯一承接方消费(原决策系统 ENTRY_GATE 的承接角色移交至此):候选进入建仓规划 → 按百分比框架定批次数量 → 送决策系统建仓研判(复用其 gate 仲裁能力,出口改道)→ 通过则生成买入指令。用户的建仓/升仓命令同样从这个候选池 + 白名单中选股。 + +| 引擎 | 触发(自主提议口径) | 关键约束 | +|---|---|---| +| FILL 回踩补足 | 建仓期(≤10 交易日)内回踩支撑带不破,浮亏 < 3% | 每票 1 次;补后 ≤ 目标仓位 | +| ADD 盈利加仓 | 安全垫 ≥ +3% 且创 5 日新高或站上压力位 | 距上次 ≥2 交易日;≤ 单股上限;距 MA5 <+6%(不追高) | +| DCA 补仓 | 浮亏触及 **−8% / −15%** 评估档(各评估一次,执行终身一次) | ≤ 底仓 50%;止损参考不下移;**−15% 及更深永远需用户确认**;研判必答"下跌是杀逻辑还是杀情绪" | +| TRIM 保垫减仓 | 安全垫峰值 ≥6% 且回吐过半 → 减 1/3 锁盈 | 纯规则自动执行(减持方向不设确认门槛) | +| SWITCH 调仓 | 一期仅命令驱动;自主汰弱换强二期 | 换股成对编排:**卖出成交确认后再下达买入** | +| T0 做T | 仅命令授权股票。正T:距当日高点回落 ≥3% 且近支撑;反T:近压力位或涨 ≥5% 后滞涨 | 当日 ≤1 次;**14:50 强制平回**(当日买卖数量对齐,绝不让 T 仓过夜变成变相加减仓);单票/全局当日T亏熔断(0.3%/1% 规模)后禁T;二期上线 | + +--- + +## 7. 两道关口(规则闸 + 研判闸) + +- **规则闸(一级,全指令必过,PMS 纯代码)**:下发前用最新数据终检——命令参数上限、一手检查、T+1 可卖、冻结状态、刹车状态、行业集中度、(买入)不追高。任一不过或必要输入缺失 → 拒绝(宁可不动),未通过项落账本。 +- **研判闸(二级,仅自主提议的补足/加仓/补仓/调仓,由决策系统承担)**:PMS 备齐硬数字与账本流水作为 context → 请求决策系统研判(其 `process_intraday_audit` 新增 PMS 请求类 direction,复用既有仲裁哲学:代码算数、AI 裁定性、越界收口)→ 结构化答复 通过/驳回+理由。研判服务不可用 → 自动降级 propose_only(人工确认替位)+ ERROR 告警。命令驱动动作不过研判闸(不用 AI 审用户)。 +- 两道关口的每次评审都落 `pms_action_ledger`(评审方=规则/研判/人工、当时现价、硬数字、未通过项/理由;驳回同样落)——"拒了的后来涨了多少"是后期调参的核心数据。 + +--- + +## 8. 择时执行器(双实现,接口先行) + +``` +输入: {指令ID, 股票, 方向, 总数量, 执行窗口, 限价提示, 优先级} +输出(逐日): {当日配额, 出手时点/价格带, 成交回执} +实现A(主): 委托决策系统盘中择时研判(PMS_EXEC 类 direction)——执行/等待 + 建议价, PMS 只管配额与兜底 +实现B(兜底, 一期先行): 内置保守择时 +``` + +**内置保守择时(实现B,参数全部可配置且页面可调)**:每日配额 = 剩余量 ÷ 剩余窗口天数(向上取整到一手)。卖出:避开开盘 30 分钟;现价 ≥ 当日均价(VWAP)时分笔卖出配额;14:45 未完成 → 现价×0.998 限价兜底。买入:现价 ≤ 当日均价或进入回踩带时买入配额;当日涨幅 > 5% 停止买入(不追高);窗口末日 14:45 强制限价完成或按命令属性作废。停牌/一字板当日跳过顺延;窗口耗尽未完成 → 命令置"部分完成"并告警。每次出手记子指令,父指令聚合进度。 + +--- + +## 9. 与下游系统的通道(权限移交的落点) + +`trading_order` / `trading_position` **仍归下游系统维护**;变化在于对下游的指挥权:原先由决策系统行使的部分管理权限(建仓审批、卖出指挥)移交持仓系统,下游停止直接执行决策系统的信号。通道形态(与下游对账后定稿): + +- **方案 Y(推荐,趁架构调整一步到位)**:新指令接口 `pms_order_request`(表或 Redis 流,买入卖出统一):`指令ID(幂等) / 股票 / 方向 / 数量 / 限价 / 有效期 / 来源='pms'`,下游轮询执行并回写状态;成交仍落 `trading_order`(PMS 回放对账口径不变)。 +- **方案 X(过渡兼容)**:买入沿用 `trading_buy_plan`(PMS 直接置 6 待挂单、署名 `approved_by='pms'`);卖出扩展现有通道补数量字段。 + +卖出通道确认前,减持类自主动作只记账不下发(影子运行);**命令类的降仓/清仓是用户意志,通道未通前由用户人工执行、PMS 记账跟踪**——这是一期最优先打通的环节。 + +--- + +## 10. 调度总表(Asia/Shanghai,全任务带交易日守卫 + 收尾统计摘要) + +| 调度 | 时间 | 任务 | +|---|---|---| +| 盘前准备 | 交易日 08:50 | 命令收敛、当日方案与配额刷新、参考位取数(决策系统昨夜结论)、冻结与刹车结算 | +| 命令轮询 | 每 1 分钟(全天) | 新命令解析 → 方案生成 → 任务状态机推进 | +| 盘中执行 | 交易时段每 1 分钟 | 择时执行器出手 + 自主提议扫描(规则闸 → 研判闸 → 按档位分流) | +| 信号消化 | 常驻 | 订阅决策系统盘中信号(风控 SELL/止盈/反转),转化为卖出方案或提议 | +| 成交回放 | 交易时段每 5 分钟 | `trading_order` 增量回放 + 轻对账 | +| T 仓平回 | 14:50(二期) | 做T强制平回 | +| 日终结算 | 15:10 | 全量对账、除权检测、安全垫峰值、命令进度日结 | +| 运营日报 | 15:30 | 关注区(命令进度/待确认提议/对账差异/刹车/影子指令)+ 全量统计,页面可查 | + +--- + +## 11. 数据模型(153 代理侧建表,严格单表访问;DDL 出 `ddl_pms_v1.sql`) + +| 表 | 关键列 | 说明 | +|---|---|---| +| `pms_command` | command_id(uk) / cmd_type / params_json / status / progress_json / issued_at / done_at | 命令与进度(参数命令的当前值以本表最新生效记录为准) | +| `pms_plan` | plan_id / command_id idx / ts_code / action / qty_or_amount / priority / deadline / status | 方案明细 | +| `pms_position` / `pms_lot` | §4 | 账本 | +| `pms_instruction` | instruction_id(uk) / 来源(plan/proposal) / ts_code / action / qty / limit_price / status(待检→规则闸过→研判过→已下发→已成交确认 / 拒绝 / 过期 / 撤销) / dispatch_ref / 子单聚合 | 指令状态机 | +| `pms_proposal` | proposal_id / ts_code / action / qty / hard_numbers_json / status(待确认→采纳/驳回/过期) | 自主提议待确认队列 | +| `pms_action_ledger` | 评审方(rule\|judge\|user) / verdict / price_at / hard_numbers_json / failed_checks_json / reason / outcome_scored | 评审账本(调参判分锚) | +| `pms_daily_report` | ymd(uk) / report_json | 日报 | + +--- + +## 12. 配置项(初值;**全部参数可经管理页面在线修改**,页面改动持久化后优先于文件初值) + +```python +# 命令类参数初值 +PMS_TOTAL_SCALE = 2_000_000; PMS_PORTFOLIO_CAP = 0.60; PMS_STOCK_CAP = 0.08 +PMS_STOCK_TARGET_DEFAULT = 0.06; PMS_MAX_NAMES = 15; PMS_AUTONOMY = "propose_only" +# 批次与安全垫 +PMS_BATCH_SPLIT = "0.5,0.25,0.25"; PMS_CUSHION_SOLID = 0.03 +PMS_TRIM_PEAK = 0.06; PMS_TRIM_GIVEBACK = 0.5 +PMS_DCA_TRIGGERS = "-0.08,-0.15"; PMS_DCA_DEEP_CONFIRM = -0.15; PMS_DCA_MAX_RATIO = 0.5 +PMS_NO_CHASE_MA5 = 0.06 +# 行业约束(硬拦截;划分依据接口化) +PMS_SECTOR_MAX_NAMES = 4; PMS_SECTOR_MAX_RATIO = 0.40; PMS_SECTOR_SOURCE = "" # 空=约束停用并页面提示 +# 执行与择时(实现B 全参数) +PMS_EXEC_WINDOW_TDAYS = 3; PMS_BUY_HALT_DAYUP = 0.05; PMS_SELL_AVOID_OPEN_MIN = 30 +PMS_EOD_FORCE_TIME = "14:45"; PMS_EOD_FORCE_DISCOUNT = 0.998; PMS_MIN_LOT_MERGE = True +# 风险披露与刹车 +PMS_RISK_WARN_ENTRY = 0.01; PMS_RISK_WARN_PORTFOLIO = 0.06 +PMS_BRAKE_DRAWDOWN = 0.05; PMS_BRAKE_DAYS = 3 +# 研判闸 +PMS_JUDGE_ENABLED = True; PMS_JUDGE_ACTIONS = "FILL,ADD,DCA,SWITCH"; PMS_JUDGE_TIMEOUT = 90 +# T0(二期) +PMS_T0_ENABLED = False; PMS_T0_RATIO_MAX = 0.333 +PMS_T0_STOCK_DAY_LOSS = 0.003; PMS_T0_GLOBAL_DAY_LOSS = 0.01; PMS_T0_CLOSE_TIME = "14:50" +``` + +--- + +## 13. 降级与容错 + +| 场景 | 行为 | +|---|---| +| 决策系统研判/择时服务不可用 | 择时退实现B;自主增持降级 propose_only;参考位若停更(数据日龄 >3 交易日)用**自算参考位**:支撑 = max(MA20, 近20日低点×1.01)、压力 = 近60日高点、止损 = 底仓成本−2×ATR 与支撑取高(因子表由上游数据管道维护,独立可用);全部标注"兜底口径" | +| 行业划分数据源未配置 | 行业硬拦截自动停用,页面显著提示(不静默放行也不误拦) | +| 实时价/因子缺失 | 该票该轮跳过;连续 3 日缺失 → 冻结自主动作并告警 | +| 指令下发失败/超时 | 指令置过期 + ERROR,不自动重发;命令进度受阻 → 日报与页面关注区置顶 | +| 对账不一致 | 以下游为准修正 + 调整痕迹 + 告警;连续 3 日 → ERROR 待人工 | +| 命令冲突 | 后到命令入队并在页面提示冲突,由用户裁决执行顺序 | + +--- + +## 14. 实施方式:功能一次性开发,上线按依赖分步切换 + +**开发(一次到位,已定)**:全部功能一个开发周期完成——完整命令目录 + 管理页面(四块)+ 账本与对账 + 方案生成器 + 百分比仓位框架 + 规则闸 + 全部动作引擎(含 T0)+ 择时执行器(实现 A 接口 + 实现 B 内置)+ 决策系统信号订阅 + 日报。决策系统侧的配套修改(出口改道 + PMS 研判 direction)同步在 bionic_trader 仓库开发。 + +**上线切换(按外部依赖排序,不是功能分期)**: + +| 步 | 前置依赖 | 启用内容 | 通过标准 | +|---|---|---|---| +| 1 影子运行 | 无(只读权限即可) | 账本/回放/对账、页面查看、命令与方案生成(不下发)、自主提议记录 | 账本连续 5 日对账零差异;方案与提议在页面可核 | +| 2 实盘接管 | `QMT_INTERFACE_REQUIREMENTS.md` 协商落地(指令通道 + 数据项);下游停止直接执行决策系统信号 | 命令执行(内置择时)、自主提议 propose_only、T0(对已授权股票) | 一条降仓命令端到端走通(页面下达→方案→分日执行→回执);一笔做T完整平回 | +| 3 研判接通 | 决策系统配套修改联调完成 | 择时切实现 A、建仓/补仓/加仓研判闸生效、自主档位可开 full | 择时 A/B 可切换;一笔 −15% 补仓走完"研判+用户确认"链 | + +**判分与调参**为长期例行(评审账本与命令回执判分:降仓时机成本、提议采纳率与胜率、择时 A vs B 对比 → 阈值回调),随步骤 2 起自然积累数据,月度出校准报告。 + +--- + +## 15. 验证方案(实机,用户执行) + +1. 上线前:方案生成器单测(降仓凑额排序/一手合并/上限与行业拦截各边界);命令状态机单测;页面参数改动 → 配置生效链路。 +2. 首日检查点:①命令解析与方案落表 ②每笔指令的规则闸硬数字日志 ③择时配额与出手记录 ④回放/对账计数 ⑤页面四块可用。 +3. SQL 抽查:`pms_plan` 对照命令意图;`pms_action_ledger` 的 price_at 与行情对照。 +4. 两周后:命令执行成本统计(相对命令日收盘价);提议队列的反事实收益——为三期判分定口径。 + +## 16. 确认状态(2026-07-27 六点反馈后) + +| # | 事项 | 状态 | +|---|---|---| +| 1 | 命令场景目录取舍与分期 | ✅ 已定:全部实现、不分期(§3.1、§14) | +| 2 | 与下游系统的数据与通道协商 | 🔄 进行中:需求清单已成文 → `QMT_INTERFACE_REQUIREMENTS.md`,由用户持此与 QMT 侧协商,答复回填该文档 | +| 3 | 决策系统配套修改(§1 清单) | ✅ 已批准,直接开发(bionic 仓库实施) | +| 4 | 账户资金快照 | ✅ 已并入需求清单(数据项 A3) | +| 5 | 行业划分数据源 | ✅ 接口方案已定(§5:`pms_industry_map` 自定义映射表为默认,用户后续灌数即可) | +| 6 | 管理页面技术形态 | ✅ 已定:FastAPI + 单页 | diff --git a/QMT_INTERFACE_REQUIREMENTS.md b/QMT_INTERFACE_REQUIREMENTS.md new file mode 100644 index 0000000..509b418 --- /dev/null +++ b/QMT_INTERFACE_REQUIREMENTS.md @@ -0,0 +1,72 @@ +# 持仓系统 ↔ QMT 侧下游系统 · 数据与接口需求清单 + +> 用途:本清单由用户持有,与 QMT 侧(下游交易系统)协商。请下游按编号逐项答复(能提供/字段差异/时延/替代方案),答复直接回填本文档「答复」列,作为对接定稿依据。 +> 背景:架构调整后,对下游的指挥权由决策系统(bionic_trader)移交持仓系统(tradingSystem/PMS)。`trading_order` / `trading_position` 两表仍归下游维护,PMS 只读;PMS 新增一条带数量的统一指令通道(见 B 部分)。 +> 版本:V1.0(2026-07-27) + +--- + +## A. PMS 需要读取的数据(下游提供,153 代理可达、严格单表查询) + +| # | 数据项 | 需要的字段 | 期望更新时延 | 用途 | 答复 | +|---|---|---|---|---|---| +| A1 | 持仓快照 `trading_position` | 股票代码(点式)、持仓数量、**可用数量(T+1 可卖)**、成本价(如有)、冻结数量(如有) | 盘中 ≤ 5 分钟 | 账本对账基准。当前 PMS 只确认过 `stock_code` 列可用,**请提供该表完整字段定义(DDL)**,尤其确认是否已有"可用数量"列——若无,PMS 自行按 T+1 规则推算 | | +| A2 | 委托与成交 `trading_order` | 委托号、股票代码、方向、委托价、委托量、状态(**完整状态枚举文档**)、成交量、成交均价、委托/成交时间、来源标识 | 状态变更后 ≤ 1 分钟 | 成交回放入账(批次/成本)、指令执行确认。**请提供完整 DDL 与状态流转说明**(现掌握的 submitted/filled/completed/pending/failed 为推断口径,需正式确认) | | +| A3 | 账户资金快照(**新增需求**) | 总资产、可用资金、冻结资金、当日卖出可用资金(T+0 回笼) | 盘中 ≤ 5 分钟;日终必须 | 总规模校准与买入前资金校验。形式不限:新表 / 现有表 / HTTP 接口均可,请给出可行方案 | | +| A4 | 成交回报明细(可选) | 若 A2 已含逐笔或聚合成交(成交量/均价),本项可免;否则请提供逐笔成交表 | 同 A2 | 部分成交场景的精确入账 | | +| A5 | 上游买入计划 `trading_buy_plan` | PMS 将作为该表的承接方(替代原决策系统 ENTRY_GATE 的角色)。请确认:①下游当前是否仍轮询 `is_active=6` 自动挂单?②切换后是否可以**停止**该轮询(统一走 B1 通道),或保留作为过渡(方案 X) | — | 旧通道处置(见 C2) | | + +## B. PMS 写入:统一指令通道(新增,核心协商项) + +### B1 `pms_order_request` —— 带数量的统一买卖指令(建议表结构,可等价改为 Redis 流) + +```sql +CREATE TABLE pms_order_request ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + instruction_id VARCHAR(64) NOT NULL UNIQUE COMMENT '幂等键, PMS 生成, 重复插入应被拒绝', + ts_code VARCHAR(16) NOT NULL COMMENT '点式, 如 600000.SH', + side VARCHAR(8) NOT NULL COMMENT 'buy / sell', + qty INT NOT NULL COMMENT '股数, 整百', + limit_price DECIMAL(10,2) NULL COMMENT '限价; NULL=按下游默认方式', + valid_until DATETIME NOT NULL COMMENT '有效期, 过期未执行由下游置 EXPIRED', + source VARCHAR(16) NOT NULL DEFAULT 'pms', + status VARCHAR(16) NOT NULL DEFAULT 'NEW' + COMMENT 'NEW→ACCEPTED→EXECUTING→FILLED / PARTIAL / REJECTED / EXPIRED / CANCELLED', + exec_qty INT NULL COMMENT '已成交数量(聚合)', + exec_avg_price DECIMAL(10,2) NULL COMMENT '成交均价(聚合)', + reject_reason VARCHAR(200) NULL, + cancel_flag TINYINT NOT NULL DEFAULT 0 COMMENT 'PMS 置 1 请求撤单, 下游确认后置 status=CANCELLED', + create_time DATETIME NOT NULL, + update_time DATETIME NOT NULL, + KEY idx_status (status), KEY idx_code (ts_code) +); +``` + +| # | 协商点 | 说明 | 答复 | +|---|---|---|---| +| B1.1 | 通道形式 | 表轮询(上述 DDL)还是 Redis 流?下游选定形式,字段语义不变 | | +| B1.2 | 轮询/响应节奏 | 下游多久拉一次 NEW?期望 ≤ 1 分钟;状态回写时延期望 ≤ 1 分钟 | | +| B1.3 | 部分成交 | 有效期内持续执行到 FILLED 或到期置 PARTIAL(exec_qty 如实回写)——可否按此语义? | | +| B1.4 | 撤单 | PMS 置 cancel_flag=1 → 下游撤在途委托并回写 CANCELLED(已成交部分保留在 exec_qty)——可行? | | +| B1.5 | 拒绝码 | 涨跌停/停牌/资金不足/数量非法等 reject_reason 枚举,请提供清单 | | +| B1.6 | 市价语义 | limit_price=NULL 时下游按什么方式执行(对手价/最新价±滑点)? | | +| B1.7 | 建表归属 | 该表建在下游库还是 153 代理侧?(PMS 经 153 代理单表读写均可) | | + +## C. 行为与切换约定 + +| # | 事项 | 说明 | 答复 | +|---|---|---|---| +| C1 | 幂等与重复防护 | instruction_id 唯一约束由表/流层保证;下游对同一 instruction_id 只执行一次 | | +| C2 | 旧通道停用清单 | 切换生效后,下游**停止**:①直接执行决策系统的卖出指令与盘中 ENTRY/EXIT 信号 ②(若 A5 确认)轮询 trading_buy_plan 自动挂单。此后下游只接受 B1 通道指令。请确认停用方式与时点 | | +| C3 | 灰度共存期 | 切换初期建议双轨观察 N 个交易日(旧通道只读不执行、B1 实际执行),请确认可行性 | | +| C4 | 时钟与代码格式 | 双方统一点式代码(600000.SH)与服务器时钟(NTP);日期时间字段时区 Asia/Shanghai | | +| C5 | 故障约定 | 下游不可用时 PMS 指令停发并告警(PMS 侧守成);下游恢复后不补执行已过期指令 | | +| C6 | 账号与权限 | PMS 需要的库账号/代理路由(读 A1-A5、读写 B1),请提供 | | + +## D. 请提供的文档 + +| # | 文档 | 说明 | 答复 | +|---|---|---|---| +| D1 | `trading_position` / `trading_order` / `trading_buy_plan` 完整 DDL | 含索引与状态枚举正式定义 | | +| D2 | 下游执行行为说明 | 委托拆单逻辑(如有)、涨跌停处理、集合竞价时段行为 | | +| D3 | 现有信号消费点清单 | 下游当前消费决策系统信号的全部位置(用于 C2 停用核对,防遗漏) | | diff --git a/README.md b/README.md new file mode 100644 index 0000000..ae63d1a --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# tradingSystem — 综合交易系统(PMS 持仓管理) + +命令驱动的持仓管理系统:**用户通过管理页面下达大方向命令**(总规模/仓位上限/升降仓/对某股做T)→ **持仓系统制订分股分批方案并管理账本**(批次、摊薄成本、安全垫、纪律)→ **决策系统负责研判与择时**(其形态计算、每日定时分析等核心功能照旧运行)→ **下游系统(QMT 侧)挂单成交**。 + +架构要点:持仓系统 ↔ 决策系统直接交互;持仓系统 ↔ 下游系统交互;**决策系统与下游断开**——对下游的管理权限(建仓审批、卖出指挥)由决策系统移交持仓系统,其全部分析产出保留并成为持仓系统的输入。上游量化系统输出不变,持仓系统的建仓计划围绕上游输出建立。 + +仓位框架:百分比制(规模 200 万 / 总仓 ≤60% / 单股 ≤8% / ≤15 只,均为用户参数命令、页面可调),单股分批 50/25/25,含一手可行性检查,行业集中度硬性拦截(划分依据留接口)。决策方式:规则引擎为主,定性研判委托决策系统(持仓系统不自建研判栈)。安全垫三义:分批建仓留缓冲、浮盈垫后加仓、做T降成本(命令授权制)。 + +## 文档与交付物 + +| 文件 | 内容 | +|---|---| +| `POSITION_MGMT_DESIGN.md` | 总体设计 **V0.4(定稿,开发启动)**:命令系统与管理页面/账本/仓位框架/动作引擎/两道关口/择时执行/下游通道。功能一次性开发,上线按依赖分三步切换 | +| `QMT_INTERFACE_REQUIREMENTS.md` | 与 QMT 侧下游系统协商用的数据与接口需求清单(含资金快照、统一指令通道建议 DDL),按编号答复回填 | +| `ddl_pms_v1.sql` | PMS 全部自有表建表语句(153 代理侧,10 张) | +| `config/settings.py` | 配置(基础设施键名对齐 bionic;业务参数为初值,页面调参持久化到 `pms_runtime_param` 后优先) | +| `app/core/sizer.py` / `app/core/cushion.py` | 首批核心纯逻辑:批次拆分与一手合并、组合约束、风险披露;摊薄成本、安全垫状态机、保垫触发、卖出核销次序 | +| `scripts/test_core_units.py` | 上述模块的实机单测(`python scripts/test_core_units.py`,零外部依赖,14 例) | + +## 三条铁律 + +1. **命令至上**:自动决策不得突破用户命令参数;冲突时命令优先;命令间冲突由用户裁决。 +2. **分工不越权**:持仓系统管「做什么、多少」,决策系统管「该不该、何时」,下游只管执行;研判不可用时降级为保守规则 + 人工确认,不自建第二套研判。 +3. **先记账后动作 + 故障即守成**:指令先落表再下发;故障不产生新指令;账本与下游定期对账,以下游为实际持仓事实源。 + +## 开发约定 + +- 开发机与服务器经 git 同步代码;测试脚本由用户在实机运行,结果回传后迭代。 +- 153 代理侧数据库严格单表访问;持仓系统内部代码统一 Tushare 点式。 +- 配置全部在 `config/settings.py` 显式声明字段;参数可经管理页面在线修改并持久化。 +- 里程碑(设计定稿、建表、各期上线)及时 git 提交。 diff --git a/app/core/cushion.py b/app/core/cushion.py new file mode 100644 index 0000000..acdd036 --- /dev/null +++ b/app/core/cushion.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +""" +安全垫与成本账 (纯函数/纯对象, 无外部依赖, 可单测) +================================================== +职责 (POSITION_MGMT_DESIGN.md §4/§6): + 1. 摊薄成本口径: avg_cost = (累计买入额 − 累计卖出额 − 做T利润) / 当前持股数。 + 卖得比成本高、做T赚了钱, 摊薄成本都会下降 —— 安全垫由此增厚。 + 2. 安全垫状态机: NONE(<0) / THIN(0~solid) / SOLID(≥solid, 解锁盈利加仓)。 + 3. 保垫减仓触发: 垫子峰值 ≥ peak_min 且 现值回吐 ≥ 峰值 × giveback。 + 4. 卖出核销次序: T0 → ADD(新→旧) → DCA → FILL → BASE (保底仓纪律)。 +""" +from __future__ import annotations + + +class PositionCost: + """一只股票的成本账 (摊薄口径)。所有金额单位元, 数量单位股。""" + + def __init__(self): + self.qty = 0 + self.cum_buy_amt = 0.0 + self.cum_sell_amt = 0.0 + self.t_profit = 0.0 + + def buy(self, qty: int, price: float): + if qty <= 0 or price <= 0: + raise ValueError(f"非法买入: qty={qty}, price={price}") + self.qty += qty + self.cum_buy_amt += qty * price + + def sell(self, qty: int, price: float): + if qty <= 0 or price <= 0: + raise ValueError(f"非法卖出: qty={qty}, price={price}") + if qty > self.qty: + raise ValueError(f"卖出超持仓: sell={qty} > hold={self.qty}") + self.qty -= qty + self.cum_sell_amt += qty * price + if self.qty == 0: # 清仓即结账, 防止空仓残留成本影响下一轮 + self.reset_keep_nothing() + + def add_t_profit(self, amount: float): + """做T已实现利润 (可为负 = T亏损), 直接摊入成本。""" + self.t_profit += amount + + def reset_keep_nothing(self): + self.qty = 0 + self.cum_buy_amt = 0.0 + self.cum_sell_amt = 0.0 + self.t_profit = 0.0 + + @property + def avg_cost(self): + """摊薄成本。空仓返回 None; 净成本为负 (卖出已收回全部本金) 时返回 0.0 (垫子视为无限厚)。""" + if self.qty <= 0: + return None + net = self.cum_buy_amt - self.cum_sell_amt - self.t_profit + return max(net / self.qty, 0.0) + + def cushion(self, price: float): + """安全垫幅度 = 现价/摊薄成本 − 1。空仓返回 None; 成本≤0 返回大数 (视为极厚)。""" + c = self.avg_cost + if c is None: + return None + if c <= 0: + return 9.99 + return price / c - 1.0 + + +def cushion_state(cushion_pct, solid: float = 0.03) -> str: + """垫子状态: NONE(<0) / THIN(0~solid) / SOLID(≥solid)。None 视为 NONE。""" + if cushion_pct is None or cushion_pct < 0: + return "NONE" + return "SOLID" if cushion_pct >= solid else "THIN" + + +def trim_trigger(cushion_peak: float, cushion_now, peak_min: float = 0.06, + giveback: float = 0.5) -> bool: + """保垫减仓触发: 峰值曾 ≥ peak_min 且 现值回吐 ≥ 峰值 × giveback。""" + if cushion_now is None or cushion_peak is None: + return False + if cushion_peak < peak_min: + return False + return (cushion_peak - cushion_now) >= cushion_peak * giveback - 1e-12 + + +# 卖出核销优先级 (数值小者先卖): 保底仓纪律 +_SELL_ORDER = {"T0": 0, "ADD": 1, "DCA": 2, "FILL": 3, "BASE": 4, "RECON": 5} + + +def sell_allocation(lots: list, sell_qty: int) -> list: + """把卖出数量分配到批次: T0 → ADD(新→旧) → DCA → FILL → BASE。 + + lots: [{"lot_id", "lot_type", "qty"(未核销数量), "open_date"(YYYYMMDD int 或可比较值)}, ...] + 返回 [{"lot_id", "qty"}...]; 卖出量超过批次总量 → ValueError (调用方应先对账)。 + """ + if sell_qty <= 0: + return [] + total = sum(l["qty"] for l in lots) + if sell_qty > total: + raise ValueError(f"卖出超批次总量: sell={sell_qty} > lots={total} (先对账再动作)") + + def key(l): + tier = _SELL_ORDER.get(l["lot_type"], 9) + # ADD 批内部按开仓日新→旧 (负号); 其余批按旧→新 + date_key = -l["open_date"] if l["lot_type"] == "ADD" else l["open_date"] + return (tier, date_key) + + out, remain = [], sell_qty + for l in sorted(lots, key=key): + if remain <= 0: + break + take = min(l["qty"], remain) + if take > 0: + out.append({"lot_id": l["lot_id"], "qty": take}) + remain -= take + return out + + +def dca_stage(loss_pct: float, triggers=(-0.08, -0.15)) -> int: + """补仓评估档: 返回已触及的最深档序号 (1 起), 未触及任何档返回 0。 + triggers 按由浅到深排列 (如 -0.08, -0.15)。""" + stage = 0 + for i, t in enumerate(sorted(triggers, reverse=True), start=1): # 浅档在前 + if loss_pct <= t + 1e-12: + stage = i + return stage diff --git a/app/core/sizer.py b/app/core/sizer.py new file mode 100644 index 0000000..0cc92b5 --- /dev/null +++ b/app/core/sizer.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +""" +仓位规划器 (纯函数, 无外部依赖, 可单测) +======================================== +职责 (POSITION_MGMT_DESIGN.md §5): + 1. 百分比制批次拆分: 单股目标仓位按 50/25/25 分批, 含一手可行性检查与自动合并 + (50/25/25 → 60/40 → 100, 仍买不足一手 → 放弃并给出原因)。 + 2. 组合约束校验: 总仓上限 / 单股上限 / 最大持仓数 / 行业集中度 (硬拦截)。 + 3. 风险敞口披露: 不定量、只计算与告警。 + +约定: 金额单位元, 价格单位元, 数量单位股 (A股一手=100股)。 +""" +from __future__ import annotations + +LOT = 100 # A股一手 + +# 批次合并阶梯: 一手检查不过时逐级降档 +MERGE_LADDER = [ + ("BASE/FILL/ADD", (0.5, 0.25, 0.25)), + ("BASE/FILL", (0.6, 0.4)), + ("BASE", (1.0,)), +] +BATCH_NAMES = ["BASE", "FILL", "ADD"] + + +def lot_qty(amount: float, price: float) -> int: + """金额换算成整手股数 (向下取整到一手)。价格非法返回 0。""" + if price is None or price <= 0 or amount is None or amount <= 0: + return 0 + return int(amount / price / LOT) * LOT + + +def split_batches(target_amount: float, price: float, splits=None, merge: bool = True) -> dict: + """把单股目标金额拆成分批投放计划, 含一手检查与自动合并。 + + 返回 {"ok": bool, "scheme": tuple, "batches": [{"name","amount","qty"}...], "reason": str} + - ok=False 时 batches 为空, reason 说明原因 (如"目标金额买不足一手")。 + - 自动合并: 首选方案任一批次不足一手 → 逐级降档 (60/40 → 100)。 + merge=False 时不合并, 直接返回失败明细 (供页面提示)。 + """ + if price is None or price <= 0: + return {"ok": False, "scheme": (), "batches": [], "reason": "价格非法"} + if target_amount is None or target_amount <= 0: + return {"ok": False, "scheme": (), "batches": [], "reason": "目标金额非法"} + + ladders = MERGE_LADDER if splits is None else [("CUSTOM", tuple(splits))] + (MERGE_LADDER[1:] if merge else []) + tried = [] + for label, scheme in (ladders if merge else ladders[:1]): + batches = [] + feasible = True + for i, ratio in enumerate(scheme): + amt = target_amount * ratio + q = lot_qty(amt, price) + if q < LOT: + feasible = False + break + name = BATCH_NAMES[i] if i < len(BATCH_NAMES) else f"B{i+1}" + batches.append({"name": name, "amount": round(amt, 2), "qty": q}) + tried.append(label) + if feasible: + return {"ok": True, "scheme": scheme, "batches": batches, "reason": ""} + return { + "ok": False, "scheme": (), "batches": [], + "reason": f"目标金额 {target_amount:.0f} 元按现价 {price:.2f} 买不足一手 (已尝试: {' → '.join(tried)})", + } + + +def check_caps(*, ts_code: str, add_amount: float, ctx: dict) -> list: + """组合约束硬校验。返回未通过项列表 (空列表 = 全过)。 + + ctx (由调用方备齐, 全部为「加仓前」快照): + scale 总规模 (元) + portfolio_cap 总仓上限 (比例) + stock_cap 单股上限 (比例) + max_names 最大持仓数 + portfolio_mv 当前组合市值 (元) + names_count 当前持仓数 + stock_mv 该股当前市值 (元, 无仓=0) + is_new_name 本次是否新开仓 + sector 该股行业名 (None=行业数据源未配置 → 行业约束跳过) + sector_names 同行业当前持仓数 + sector_mv 同行业当前市值 (元) + sector_max_names / sector_max_ratio 行业约束参数 + """ + v = [] + scale = float(ctx["scale"]) + if scale <= 0: + return ["SCALE_INVALID: 总规模未设置"] + + if (ctx["portfolio_mv"] + add_amount) / scale > ctx["portfolio_cap"] + 1e-9: + v.append(f"PORTFOLIO_CAP: 加后总仓 {(ctx['portfolio_mv'] + add_amount) / scale:.1%} " + f"> 上限 {ctx['portfolio_cap']:.0%}") + if (ctx["stock_mv"] + add_amount) / scale > ctx["stock_cap"] + 1e-9: + v.append(f"STOCK_CAP: {ts_code} 加后 {(ctx['stock_mv'] + add_amount) / scale:.1%} " + f"> 单股上限 {ctx['stock_cap']:.0%}") + if ctx.get("is_new_name") and ctx["names_count"] + 1 > ctx["max_names"]: + v.append(f"MAX_NAMES: 持仓数将达 {ctx['names_count'] + 1} > 上限 {ctx['max_names']}") + + sector = ctx.get("sector") + if sector: # None/"" = 行业数据源未配置, 约束停用 (调用方负责页面提示) + if ctx.get("is_new_name") and ctx.get("sector_names", 0) + 1 > ctx["sector_max_names"]: + v.append(f"SECTOR_NAMES: 行业[{sector}]将达 {ctx['sector_names'] + 1} 只 " + f"> 上限 {ctx['sector_max_names']}") + port_after = ctx["portfolio_mv"] + add_amount + if port_after > 0 and (ctx.get("sector_mv", 0) + add_amount) / port_after > ctx["sector_max_ratio"] + 1e-9: + v.append(f"SECTOR_RATIO: 行业[{sector}]占总仓将达 " + f"{(ctx['sector_mv'] + add_amount) / port_after:.1%} > 上限 {ctx['sector_max_ratio']:.0%}") + return v + + +def risk_exposure(qty: int, price: float, stop_ref: float) -> float: + """单笔风险敞口 (元) = 数量 × max(0, 买价 − 止损参考)。stop_ref 缺失返回 -1 表示无法计算。""" + if stop_ref is None or stop_ref <= 0: + return -1.0 + return qty * max(0.0, price - stop_ref) + + +def risk_warnings(*, entry_exposure: float, portfolio_exposure: float, scale: float, + warn_entry: float = 0.01, warn_portfolio: float = 0.06) -> list: + """风险披露告警 (不拦截, 只提示)。entry_exposure=-1 时提示无法计算。""" + w = [] + if entry_exposure < 0: + w.append("RISK_UNKNOWN: 无止损参考位, 敞口无法计算") + elif scale > 0 and entry_exposure / scale > warn_entry: + w.append(f"RISK_ENTRY: 单笔敞口 {entry_exposure / scale:.2%} > 披露线 {warn_entry:.0%}") + if scale > 0 and portfolio_exposure > 0 and portfolio_exposure / scale > warn_portfolio: + w.append(f"RISK_PORTFOLIO: 组合敞口 {portfolio_exposure / scale:.2%} > 披露线 {warn_portfolio:.0%}") + return w diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..f70ea18 --- /dev/null +++ b/config/settings.py @@ -0,0 +1,110 @@ +# -*- 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" + # 因子分表 gp_stock_factor_pro_* (自算参考位/MA/ATR/量比) + 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_SECTOR_SOURCE: str = "" # "" = 停用并页面提示 / custom_table / gp_stock_category + 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_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" + PMS_JUDGE_TIMEOUT: int = 90 # 超时 → 降级 propose_only + + # --- 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_REPLAY_INTERVAL_MIN: int = 5 + PMS_RECON_ALARM_DAYS: int = 3 # 连续不一致 N 日升级 ERROR + + +settings = Settings() diff --git a/ddl_pms_v1.sql b/ddl_pms_v1.sql new file mode 100644 index 0000000..a3d33b0 --- /dev/null +++ b/ddl_pms_v1.sql @@ -0,0 +1,188 @@ +-- ===================================================================== +-- tradingSystem (PMS) V1 建表 DDL +-- 目标库: 153 代理侧 (与 decision_ledger 等同库), 一律经代理严格单表访问 +-- 字符集: utf8mb4; 代码格式: Tushare 点式 (600000.SH); 时区: Asia/Shanghai +-- 对应设计: POSITION_MGMT_DESIGN.md V0.4 §11 +-- 注: pms_order_request (指令通道) 归属待 B1.7 协商, 其 DDL 见 +-- QMT_INTERFACE_REQUIREMENTS.md, 不在本文件建立。 +-- ===================================================================== + +-- 1. 命令表 (参数命令 + 任务命令; 参数命令当前值 = 该类型最新一条 EFFECTIVE 记录) +CREATE TABLE IF NOT EXISTS pms_command ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + command_id VARCHAR(64) NOT NULL UNIQUE COMMENT '幂等键: CMD_{ymd}_{seq}', + cmd_class VARCHAR(8) NOT NULL COMMENT 'param(参数命令) / task(任务命令)', + cmd_type VARCHAR(32) NOT NULL COMMENT 'SET_SCALE / REDUCE_EXPOSURE / OPEN_TARGET / T0_ENABLE ...', + ts_code VARCHAR(16) NULL COMMENT '个股级命令的标的; 组合级为 NULL', + params_json TEXT NOT NULL COMMENT '命令参数 (比例/窗口/价格等)', + status VARCHAR(16) NOT NULL DEFAULT 'PENDING' + COMMENT '参数命令: EFFECTIVE/SUPERSEDED; 任务命令: PENDING/PLANNING/EXECUTING/PARTIAL/DONE/CANCELLED', + progress_json TEXT NULL COMMENT '任务进度 (目标额/已完成额/明细指针/回执)', + issued_by VARCHAR(32) NOT NULL DEFAULT 'user', + issued_at DATETIME NOT NULL, + done_at DATETIME NULL, + note VARCHAR(500) NULL, + KEY idx_type_status (cmd_type, status), + KEY idx_status (status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户命令与进度'; + +-- 2. 方案明细 (任务命令展开的分股行动清单) +CREATE TABLE IF NOT EXISTS pms_plan ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + plan_id VARCHAR(64) NOT NULL UNIQUE COMMENT 'PLAN_{command_id}_{seq}', + command_id VARCHAR(64) NOT NULL, + ts_code VARCHAR(16) NOT NULL, + action VARCHAR(16) NOT NULL COMMENT 'OPEN/FILL/ADD/DCA/TRIM/EXIT/HALT/T0_ROUND', + qty INT NULL COMMENT '目标股数 (与 amount 二选一)', + amount DECIMAL(14,2) NULL COMMENT '目标金额 (元)', + priority INT NOT NULL DEFAULT 100 COMMENT '越小越先执行', + deadline DATE NULL COMMENT '执行窗口截止日', + status VARCHAR(16) NOT NULL DEFAULT 'PENDING' + COMMENT 'PENDING/EXECUTING/DONE/PARTIAL/CANCELLED', + filled_qty INT NOT NULL DEFAULT 0, + reason VARCHAR(300) NULL COMMENT '进方案的理由 (弱票清仓/收利润/等比减 等)', + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + KEY idx_cmd (command_id), + KEY idx_code_status (ts_code, status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='方案明细'; + +-- 3. 持仓主档 (一票一行; 实际持仓以下游 trading_position 为准, 本表是结构解释账本) +CREATE TABLE IF NOT EXISTS pms_position ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + ts_code VARCHAR(16) NOT NULL UNIQUE, + status VARCHAR(16) NOT NULL DEFAULT 'HOLDING' + COMMENT 'PLANNED/OPENING/HOLDING/EXITING/CLOSED', + frozen_reason VARCHAR(16) NOT NULL DEFAULT 'NONE' + COMMENT 'NONE/COMMAND_HALT/BRAKE/MANUAL (只禁增持不禁减持)', + total_qty INT NOT NULL DEFAULT 0, + avail_qty INT NOT NULL DEFAULT 0 COMMENT 'T+1 可卖数, 日初重置', + base_qty INT NOT NULL DEFAULT 0, + fill_qty INT NOT NULL DEFAULT 0, + add_qty INT NOT NULL DEFAULT 0, + dca_qty INT NOT NULL DEFAULT 0, + t0_qty INT NOT NULL DEFAULT 0 COMMENT '日内T仓 (收盘应为0)', + avg_cost DECIMAL(10,3) NULL COMMENT '摊薄成本 (含已实现T利润与批次盈亏)', + realized_t_profit DECIMAL(14,2) NOT NULL DEFAULT 0, + cushion_pct DECIMAL(8,4) NULL COMMENT '安全垫 = 现价/摊薄成本 - 1', + cushion_state VARCHAR(8) NOT NULL DEFAULT 'NONE' COMMENT 'NONE/THIN/SOLID', + cushion_peak DECIMAL(8,4) NOT NULL DEFAULT 0, + pct_of_scale DECIMAL(8,4) NULL COMMENT '市值占总规模比', + target_pct DECIMAL(8,4) NULL COMMENT '目标仓位比 (建仓命令设定)', + stop_ref DECIMAL(10,2) NULL COMMENT '止损参考位', + support_ref DECIMAL(10,2) NULL, + pressure_ref DECIMAL(10,2) NULL, + ref_source VARCHAR(16) NULL COMMENT 'bionic / self_calc(兜底自算) / user(命令覆盖)', + t0_enabled TINYINT NOT NULL DEFAULT 0, + t0_ratio DECIMAL(6,4) NULL, + opened_date DATE NULL, + fill_count INT NOT NULL DEFAULT 0, + last_add_date DATE NULL, + dca_count INT NOT NULL DEFAULT 0, + t0_count_today INT NOT NULL DEFAULT 0, + updated_at DATETIME NOT NULL, + KEY idx_status (status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='持仓主档'; + +-- 4. 批次账 +CREATE TABLE IF NOT EXISTS pms_lot ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + ts_code VARCHAR(16) NOT NULL, + lot_type VARCHAR(8) NOT NULL COMMENT 'BASE/FILL/ADD/DCA/T0/RECON(对账调整)', + qty INT NOT NULL, + open_price DECIMAL(10,3) NOT NULL, + open_date DATE NOT NULL, + closed_qty INT NOT NULL DEFAULT 0, + close_avg_price DECIMAL(10,3) NULL, + realized_pnl DECIMAL(14,2) NOT NULL DEFAULT 0, + status VARCHAR(8) NOT NULL DEFAULT 'OPEN' COMMENT 'OPEN/CLOSED', + instruction_id VARCHAR(64) NULL COMMENT '来源指令; NULL=外部成交并入', + note VARCHAR(200) NULL, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + KEY idx_code_status (ts_code, status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='批次账'; + +-- 5. 指令状态机 (父指令; 分日出手的子单记 progress_json) +CREATE TABLE IF NOT EXISTS pms_instruction ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + instruction_id VARCHAR(64) NOT NULL UNIQUE COMMENT '幂等键: INS_{ymd}_{code}_{action}_{seq}', + origin_type VARCHAR(8) NOT NULL COMMENT 'plan(命令方案) / proposal(自主提议) / system(保垫等自动)', + origin_id VARCHAR(64) NULL, + ts_code VARCHAR(16) NOT NULL, + action VARCHAR(16) NOT NULL, + side VARCHAR(8) NOT NULL COMMENT 'buy/sell', + qty INT NOT NULL, + limit_price DECIMAL(10,2) NULL, + window_tdays INT NOT NULL DEFAULT 3, + status VARCHAR(20) NOT NULL DEFAULT 'PROPOSED' + COMMENT 'PROPOSED/RULE_PASSED/JUDGE_PASSED/DISPATCHED/CONFIRMED/REJECTED/EXPIRED/CANCELLED', + dispatch_ref VARCHAR(64) NULL COMMENT '下游通道引用 (pms_order_request.instruction_id)', + exec_qty INT NOT NULL DEFAULT 0, + exec_avg_price DECIMAL(10,3) NULL, + progress_json TEXT NULL COMMENT '分日子单与出手记录', + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + KEY idx_code_status (ts_code, status), + KEY idx_status (status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='指令状态机'; + +-- 6. 自主提议待确认队列 (propose_only 档) +CREATE TABLE IF NOT EXISTS pms_proposal ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + proposal_id VARCHAR(64) NOT NULL UNIQUE, + ts_code VARCHAR(16) NOT NULL, + action VARCHAR(16) NOT NULL, + qty INT NOT NULL, + hard_numbers_json TEXT NOT NULL COMMENT '安全垫/浮亏/参考位距离/敞口等, 页面展示', + judge_verdict VARCHAR(16) NULL COMMENT '决策系统研判结论 (接通后)', + judge_reason VARCHAR(500) NULL, + status VARCHAR(16) NOT NULL DEFAULT 'WAIT_USER' + COMMENT 'WAIT_USER/ACCEPTED/DECLINED/EXPIRED', + expire_at DATETIME NOT NULL, + decided_at DATETIME NULL, + created_at DATETIME NOT NULL, + KEY idx_status (status), + KEY idx_code (ts_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='自主提议待确认'; + +-- 7. 评审账本 (规则闸/研判闸/人工确认 全量留痕, 判分锚) +CREATE TABLE IF NOT EXISTS pms_action_ledger ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + ts_code VARCHAR(16) NOT NULL, + decided_at DATETIME NOT NULL, + action VARCHAR(16) NOT NULL, + arbiter VARCHAR(8) NOT NULL COMMENT 'rule/judge/user', + verdict VARCHAR(8) NOT NULL COMMENT 'PASS/REJECT', + price_at DECIMAL(10,3) NOT NULL COMMENT '评审时现价 (反事实判分锚, 必填)', + hard_numbers_json TEXT NULL, + failed_checks_json TEXT NULL COMMENT '未通过项列表 (规则闸)', + reason VARCHAR(500) NULL COMMENT '理由 (研判闸/人工)', + ref_id VARCHAR(64) NULL COMMENT '关联 instruction/proposal/plan', + outcome_scored TINYINT NOT NULL DEFAULT 0 COMMENT '判分位图: 1=T+1, 2=T+5, 4=T+20', + KEY idx_code_time (ts_code, decided_at), + KEY idx_scored (outcome_scored) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评审账本'; + +-- 8. 运营日报 (可重跑覆盖) +CREATE TABLE IF NOT EXISTS pms_daily_report ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + ymd INT NOT NULL UNIQUE COMMENT 'YYYYMMDD', + report_json MEDIUMTEXT NOT NULL, + created_at DATETIME NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运营日报'; + +-- 9. 行业映射 (行业划分接口的 custom_table 数据源; 用户后续灌数, 灌何种划分不限) +CREATE TABLE IF NOT EXISTS pms_industry_map ( + ts_code VARCHAR(16) PRIMARY KEY, + industry VARCHAR(64) NOT NULL, + updated_at DATETIME NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='行业映射 (自定义数据源)'; + +-- 10. 运行参数持久层 (页面修改的参数落此, 优先于 settings 文件初值) +CREATE TABLE IF NOT EXISTS pms_runtime_param ( + param_key VARCHAR(64) PRIMARY KEY, + param_value VARCHAR(200) NOT NULL, + updated_by VARCHAR(32) NOT NULL DEFAULT 'user', + updated_at DATETIME NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='页面可调参数当前值'; diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d26daf7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +pydantic>=2.5 +pydantic-settings>=2.1 +SQLAlchemy>=2.0 +PyMySQL>=1.1 +redis>=5.0 +celery>=5.3 +fastapi>=0.110 +uvicorn>=0.27 +requests>=2.31 +chinesecalendar>=1.9 diff --git a/scripts/test_core_units.py b/scripts/test_core_units.py new file mode 100644 index 0000000..cd65af8 --- /dev/null +++ b/scripts/test_core_units.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +""" +核心纯逻辑模块单测 (实机运行, 零外部依赖) +========================================== +运行: 在 tradingSystem 仓库根目录执行 python scripts/test_core_units.py +约定: 全部断言通过输出 "ALL PASS (n cases)" 并退出码 0; 任一失败输出明细并退出码 1。 +覆盖: sizer 批次拆分/一手合并/组合约束/风险披露; cushion 摊薄成本/垫子状态/保垫触发/核销次序/补仓档。 +""" +import os +import sys +import traceback + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from app.core.sizer import lot_qty, split_batches, check_caps, risk_exposure, risk_warnings # noqa: E402 +from app.core.cushion import (PositionCost, cushion_state, trim_trigger, # noqa: E402 + sell_allocation, dca_stage) + +RESULTS = [] + + +def case(name): + def deco(fn): + RESULTS.append((name, fn)) + return fn + return deco + + +# ---------------------------------------------------------------- sizer +@case("批次拆分·常规 (12万目标, 10元股 → 6000/3000/3000 股)") +def _(): + r = split_batches(120_000, 10.0) + assert r["ok"] and r["scheme"] == (0.5, 0.25, 0.25), r + assert [b["qty"] for b in r["batches"]] == [6000, 3000, 3000], r + + +@case("批次拆分·高价股自动合并 (12万目标, 700元股 → 合并为单批 100 股)") +def _(): + # 50%=6万/700=85股 不足一手 → 60/40: 40%=4.8万=68股 仍不足 → 100%: 171→100股 + r = split_batches(120_000, 700.0) + assert r["ok"] and r["scheme"] == (1.0,), r + assert len(r["batches"]) == 1 and r["batches"][0]["qty"] == 100, r + + +@case("批次拆分·极端高价买不起 (12万目标, 2000元股 → 放弃并给原因)") +def _(): + r = split_batches(120_000, 2000.0) + assert not r["ok"] and "买不足一手" in r["reason"], r + + +@case("批次拆分·非法输入") +def _(): + assert not split_batches(120_000, 0)["ok"] + assert not split_batches(0, 10.0)["ok"] + assert lot_qty(9_999, 100.0) == 0 and lot_qty(10_000, 100.0) == 100 + + +@case("组合约束·单股上限拦截 (200万规模, 8%上限, 已持14万再买4万)") +def _(): + ctx = dict(scale=2_000_000, portfolio_cap=0.60, stock_cap=0.08, max_names=15, + portfolio_mv=800_000, names_count=8, stock_mv=140_000, is_new_name=False, + sector=None, sector_max_names=4, sector_max_ratio=0.40) + v = check_caps(ts_code="600000.SH", add_amount=40_000, ctx=ctx) + assert len(v) == 1 and v[0].startswith("STOCK_CAP"), v + v2 = check_caps(ts_code="600000.SH", add_amount=10_000, ctx=ctx) # 14+1=15万 < 16万上限 + assert v2 == [], v2 + + +@case("组合约束·总仓+持仓数+行业同时拦截") +def _(): + ctx = dict(scale=2_000_000, portfolio_cap=0.60, stock_cap=0.08, max_names=10, + portfolio_mv=1_180_000, names_count=10, stock_mv=0, is_new_name=True, + sector="半导体", sector_names=4, sector_mv=470_000, + sector_max_names=4, sector_max_ratio=0.40) + v = check_caps(ts_code="688000.SH", add_amount=60_000, ctx=ctx) + kinds = {x.split(":")[0] for x in v} + assert kinds == {"PORTFOLIO_CAP", "MAX_NAMES", "SECTOR_NAMES", "SECTOR_RATIO"}, v + + +@case("组合约束·行业数据源未配置时行业约束跳过") +def _(): + ctx = dict(scale=2_000_000, portfolio_cap=0.60, stock_cap=0.08, max_names=15, + portfolio_mv=100_000, names_count=2, stock_mv=0, is_new_name=True, + sector=None, sector_names=99, sector_mv=9_999_999, + sector_max_names=4, sector_max_ratio=0.40) + assert check_caps(ts_code="000001.SZ", add_amount=60_000, ctx=ctx) == [] + + +@case("风险披露·敞口计算与告警") +def _(): + e = risk_exposure(6000, 10.0, 9.5) # 6000×0.5 = 3000 元 + assert abs(e - 3000) < 1e-6, e + w = risk_warnings(entry_exposure=25_000, portfolio_exposure=0, scale=2_000_000) + assert len(w) == 1 and w[0].startswith("RISK_ENTRY"), w # 1.25% > 1% + w2 = risk_warnings(entry_exposure=-1, portfolio_exposure=0, scale=2_000_000) + assert w2 and w2[0].startswith("RISK_UNKNOWN"), w2 + + +# ---------------------------------------------------------------- cushion +@case("摊薄成本·买入/卖出/T利润全链 (卖高于成本与做T都摊低成本)") +def _(): + pc = PositionCost() + pc.buy(6000, 10.0) # 均价 10 + assert abs(pc.avg_cost - 10.0) < 1e-9 + pc.buy(3000, 11.0) # (60000+33000)/9000 = 10.333 + assert abs(pc.avg_cost - 93000 / 9000) < 1e-9 + pc.sell(3000, 12.0) # 净成本 93000-36000=57000, 6000股 → 9.5 + assert abs(pc.avg_cost - 9.5) < 1e-9 + pc.add_t_profit(1200) # (57000-1200)/6000 = 9.3 + assert abs(pc.avg_cost - 9.3) < 1e-9 + assert abs(pc.cushion(10.23) - 0.1) < 1e-3 + pc.sell(6000, 10.0) # 清仓即结账 + assert pc.avg_cost is None and pc.qty == 0 + + +@case("摊薄成本·本金全部收回后成本归零 (垫子视为极厚)") +def _(): + pc = PositionCost() + pc.buy(1000, 10.0) + pc.buy(1000, 10.0) + pc.sell(1000, 21.0) # 收回 21000 > 总投入 20000, 剩 1000 股净成本为负 → 0 + assert pc.avg_cost == 0.0 and pc.cushion(10.0) == 9.99 + + +@case("垫子状态机·边界 (solid=3%)") +def _(): + assert cushion_state(None) == "NONE" + assert cushion_state(-0.001) == "NONE" + assert cushion_state(0.0) == "THIN" + assert cushion_state(0.0299) == "THIN" + assert cushion_state(0.03) == "SOLID" + + +@case("保垫减仓·触发边界 (峰值6%回吐一半)") +def _(): + assert trim_trigger(0.08, 0.04) is True # 回吐 0.04 = 峰值一半 + assert trim_trigger(0.08, 0.0401) is False + assert trim_trigger(0.05, -0.01) is False # 峰值不足 6% 永不触发 + assert trim_trigger(0.06, 0.03) is True + + +@case("卖出核销次序·T0 → ADD(新→旧) → DCA → FILL → BASE") +def _(): + lots = [ + {"lot_id": "L_base", "lot_type": "BASE", "qty": 6000, "open_date": 20260701}, + {"lot_id": "L_fill", "lot_type": "FILL", "qty": 3000, "open_date": 20260703}, + {"lot_id": "L_add1", "lot_type": "ADD", "qty": 2000, "open_date": 20260710}, + {"lot_id": "L_add2", "lot_type": "ADD", "qty": 1000, "open_date": 20260720}, + {"lot_id": "L_t0", "lot_type": "T0", "qty": 1000, "open_date": 20260727}, + ] + alloc = sell_allocation(lots, 4500) + assert [a["lot_id"] for a in alloc] == ["L_t0", "L_add2", "L_add1", "L_fill"], alloc + assert [a["qty"] for a in alloc] == [1000, 1000, 2000, 500], alloc + try: + sell_allocation(lots, 99_999) + assert False, "超量卖出未拦截" + except ValueError: + pass + + +@case("补仓评估档·-8%/-15% 两档") +def _(): + assert dca_stage(-0.05) == 0 + assert dca_stage(-0.08) == 1 + assert dca_stage(-0.12) == 1 + assert dca_stage(-0.15) == 2 + assert dca_stage(-0.30) == 2 + + +# ---------------------------------------------------------------- runner +def main(): + passed, failed = 0, 0 + for name, fn in RESULTS: + try: + fn() + print(f" PASS {name}") + passed += 1 + except Exception: + print(f" FAIL {name}") + traceback.print_exc() + failed += 1 + print("-" * 60) + if failed: + print(f"FAILED: {failed} / {passed + failed}") + sys.exit(1) + print(f"ALL PASS ({passed} cases)") + + +if __name__ == "__main__": + main()