设计定型后提交

This commit is contained in:
zlt 2026-07-27 15:50:57 +08:00
commit dc96db734f
15 changed files with 1264 additions and 0 deletions

10
.idea/.gitignore vendored Normal file
View File

@ -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

View File

@ -0,0 +1,39 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<list>
<option value="fastapi" />
<option value="uvicorn" />
<option value="python-multipart" />
<option value="requests" />
<option value="httpx" />
<option value="asgiref" />
<option value="chinesecalendar" />
<option value="sqlalchemy" />
<option value="pymysql" />
<option value="psycopg2-binary" />
<option value="redis" />
<option value="pymilvus" />
<option value="pymongo" />
<option value="celery" />
<option value="pydantic" />
<option value="pydantic-settings" />
<option value="python-dotenv" />
<option value="pandas" />
<option value="numpy" />
<option value="scipy" />
<option value="mplfinance" />
<option value="fastdtw" />
<option value="numba" />
<option value="PyYAML" />
<option value="pyarrow" />
<option value="APScheduler" />
<option value="duckdb" />
<option value="openpyxl" />
</list>
</option>
</inspection_tool>
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/tradingSystem.iml" filepath="$PROJECT_DIR$/.idea/tradingSystem.iml" />
</modules>
</component>
</project>

12
.idea/tradingSystem.iml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

329
POSITION_MGMT_DESIGN.md Normal file
View File

@ -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 | 只有授权的股票才做TT 仓 = 持仓 × 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 + 单页 |

View File

@ -0,0 +1,72 @@
# 持仓系统 ↔ QMT 侧下游系统 · 数据与接口需求清单
> 用途:本清单由用户持有,与 QMT 侧(下游交易系统)协商。请下游按编号逐项答复(能提供/字段差异/时延/替代方案),答复直接回填本文档「答复」列,作为对接定稿依据。
> 背景架构调整后对下游的指挥权由决策系统bionic_trader移交持仓系统tradingSystem/PMS。`trading_order` / `trading_position` 两表仍归下游维护PMS 只读PMS 新增一条带数量的统一指令通道(见 B 部分)。
> 版本V1.02026-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 或到期置 PARTIALexec_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 停用核对,防遗漏) | |

31
README.md Normal file
View File

@ -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 提交。

125
app/core/cushion.py Normal file
View File

@ -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

128
app/core/sizer.py Normal file
View File

@ -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

110
config/settings.py Normal file
View File

@ -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()

188
ddl_pms_v1.sql Normal file
View File

@ -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='页面可调参数当前值';

10
requirements.txt Normal file
View File

@ -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

190
scripts/test_core_units.py Normal file
View File

@ -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()