# PMS ↔ 决策系统 (bionic_trader) 接口契约 V1.1

> 待办 #9（择时实现 A）与 #10（研判闸）的实现依据。2026-08-03 定稿并双侧落码；同日按用户
> 审核意见把择时部分整个重做过一版（见 §7 变更记录），当前文本以重做后为准。
> PMS 侧代码：`app/services/judge.py`（原有）+ `app/services/exec_advisor.py`（新增）。
> bionic 侧代码：`app/api/main.py` 两个接口 + `workers/tasks_intraday.py` 新增
> `PMS_JUDGE` 分支 + `app/services/pms_advisor.py` + `config/settings.py` P 段。
> 机器口径：PMS 跑在 factorevaluation；bionic 跑在 4090 机（API 宿主端口 **38000**）。

---

## 1. 分工与原则

设计 `POSITION_MGMT_DESIGN.md` §1/§7/§8 的落点：持仓系统管「做什么、多少」，决策系统管
「该不该、何时」。2026-08-03 用户补充的三条原则，两个接口都必须守：

1. **决策系统还在运行、还有其他职责，改造只做加法。** 新功能全部走新增分支、新增接口、
   带默认值的新配置；原有六个盘中裁决分支、原有 API、每晚判分、建仓仲裁的输入输出一律
   不动。凡是「顺手写进共享表」这类会影响既有功能的副作用，都要在本文写明并给出处置。
2. **提前计算为主、盘中监控为辅，不另做盘中判断。** 大模型推理放在凌晨（每晚的分析本来
   就在产支撑位、压力位、定性结论），盘中接口只读凌晨结论和当天已有的监控产出，做比对、
   不做判断。可以接受买不上——现价跑出执行区间就等待，不追。
3. **拿不到结论时，各按各的纪律降级。** 对 PMS 而言「通过」「驳回」「拿不到」是三件事：
   研判拿不到 → 降级人工确认（绝不当通过，也绝不当驳回——驳回会真的杀掉提议并记一笔
   驳回账）；择时拿不到 → 退回 PMS 内置的实现 B（绝不停出手）。所以 bionic 侧任何给不出
   结论的情况（解析失败、越界裁决、结论缺失、超时、总开关关）都回
   `{"verdict": "UNAVAILABLE", "reason": ...}`，不许硬编一个看起来像结论的默认值。

另外三件事划清归属：配额、14:45 的强制完成、停牌一字板、不追高（当日涨幅与均线偏离）、
仓位上限、一手、T+1 可卖——这些检查全部留在 PMS 本地（择时的在 `exec_timing.hard_gate`，
其余在规则闸），请求根本不会送到 bionic。两个接口对 PMS 永远回 HTTP 200 + verdict 字段，
PMS 不需要区分状态码（网络层异常自然走 PMS 的超时分支，效果相同）。

---

## 2. 研判闸 `POST /api/intraday/pms_judge`（#10，走大模型，慢是正常）

PMS 调用方：`judge.py`（自主提议的二级关口，只审 `PMS_JUDGE_ACTIONS` 里的动作，默认
FILL/ADD/DCA/SWITCH；命令驱动的动作不过研判闸）。超时 `PMS_JUDGE_TIMEOUT`（默认 90 秒）。

### 2.1 请求（PMS → bionic，均为 PMS `judge.request()` 现有产出，未改）

```json
{
  "direction": "PMS_JUDGE",
  "action": "DCA",                        // FILL / ADD / DCA / SWITCH
  "ts_code": "600000.SH",                 // 点式
  "qty": 200,
  "reason": "浮亏 -8.5% 触发第一档补仓评估",   // PMS 动作引擎给出的提议理由
  "hard_numbers": {"cushion_pct": -0.085, "stage": 1, "...": "PMS 已算好的硬数字"},
  "context": {
    "position": {"ts_code": "...", "avg_cost": 9.1, "total_qty": 1000,
                 "cushion_pct": -0.085, "support_ref": 8.9, "...": "账本快照"},
    "recent_ledger": [{"at": "...", "action": "...", "arbiter": "...",
                       "verdict": "...", "price": 9.0, "reason": "..."}]
  },
  "must_answer": ["下跌是杀逻辑还是杀情绪"]     // DCA 必带 (设计 §7)
}
```

### 2.2 bionic 侧处理

`process_intraday_audit` 新增 `PMS_JUDGE` 分支，复用既有仲裁哲学与代码路径（同一个大模型
呼叫、同一条审计轨迹），按动作类型给定性判据：

| action | 核心问题 | 驳回判据（示例） |
|---|---|---|
| FILL 回踩补足 | 回踩是健康洗盘还是结构转坏 | 放量破位 / 主力持续净流出 / 弱市无个股独立证据 |
| ADD 盈利加仓 | 趋势延续还是冲顶兑现 | 放量滞涨 / 已到压力位 / 主力借涨派发 |
| DCA 补仓 | **必答**：杀逻辑还是杀情绪 | 杀逻辑一律驳回；说不清 → 驳回 |
| SWITCH 调仓 | 换入侧证据是否成立 | — |

bionic 自动补三样 PMS 没有的上下文：实时资金分布（既有的上游接口）、板块动量与区制快照、
自己昨夜的 `strategy_daily_results` 结论。裁决收口只认 PASS/REJECT，**越界与解析失败一律
回 UNAVAILABLE**（§1 原则 3）；「证据不足以支持」按提示词导向驳回（宁可错杀）。

**同股同动作半小时内不重复研判**（`PMS_JUDGE_RETRY_TTL`，默认 1800 秒，TTL 内直接回上次
结论，应答多一个 `"cached": true`）。原因：PMS 侧对研判驳回**有意**不做当日去重（其注释
「研判结论会变」），被驳回的候选可能每分钟重来——节流责任放在 bionic 侧，与建仓仲裁的
`ENTRY_GATE_RETRY_TTL` 是同一个思路。UNAVAILABLE 不缓存（瞬时故障要重试）。

**留痕只写 `strategy_audit_log`**（verdict 记 `PMS_PASS` / `PMS_REJECT` /
`PMS_UNAVAILABLE`，该列 VARCHAR(20) 装得下，已核对建表语句），**不写 `decision_ledger`**：
2026-08-03 复核 `outcome_scorer.py` 确认每晚判分对决策账本是全表扫描、不按裁决类型过滤，
建仓仲裁的历史注入也读整本账——PMS 的裁决写进去会混进这两个既有功能。PMS 自己的评审记录
本来就落在它侧的 `pms_action_ledger`（arbiter=judge），不需要在 bionic 重复记账。

### 2.3 应答（bionic → PMS）

```json
{"verdict": "PASS" | "REJECT" | "UNAVAILABLE",
 "reason": "50字以内一句话", "confidence": 0-100,
 "direction": "PMS_JUDGE", "ts_code": "600000.SH", "action": "DCA",
 "cached": false}
```

PMS 侧映射（`judge.py` 现有逻辑，未改）：PASS/APPROVE 一类 → 放行进档位分流；REJECT/DENY
一类 → 驳回并记账；**其余一律按 UNAVAILABLE → 降级人工确认 + ERROR 告警**。

### 2.4 执行路径与超时

API 收到请求 → 投递 celery 任务（`intraday.process_audit`，队列 `PMS_JUDGE_QUEUE`）→
同步等结果（`PMS_JUDGE_TASK_TIMEOUT`，默认 75 秒）→ 回 JSON。三层超时必须保持
**75（bionic 等任务）< 90（PMS 等 HTTP）**，否则 PMS 先断开、bionic 白算。
队列默认 `brain_queue`（12 并发，零部署改动）；若盘中全量重算把它塞住、PMS 侧频繁超时
降级，再切独立队列（§5.2），这是预埋的第二档不是必选项。

---

## 3. 盘中择时 `POST /api/intraday/pms_exec`（#9，读凌晨结论，秒回）

PMS 调用方：`exec_advisor.py`。只在 `PMS_EXEC_IMPL=A` 且过了本地检查后咨询；应答按
`valid_min` 缓存在指令 `progress.exec_advice`（上限 `PMS_EXEC_ADVICE_TTL_MIN`，默认 10
交易分钟），失败进入冷却（`PMS_EXEC_FAIL_COOLDOWN_MIN`，默认 5 分钟）。超时
`PMS_EXEC_TIMEOUT_SEC` 默认 8 秒——这个接口必须秒回，**大模型不进盘中路径**。

### 3.1 请求（PMS → bionic）

```json
{
  "direction": "PMS_EXEC", "ts_code": "600000.SH",
  "side": "buy" | "sell", "action": "OPEN|FILL|ADD|DCA|EXIT|TRIM",
  "qty_left": 500,                        // 今日还该投放的量 (仅参考, 配额归 PMS)
  "is_last_day": false, "tdays_left": 3, "now": "10:31",
  "day": {"price": 10.0, "vwap": 10.05, "...": "PMS 的当日快照, 可整个留空"},
  "refs": {"support": 9.5, "pressure": 11.0, "stop": 9.2, "source": "bionic"},
  "position": {"total_qty": 0, "avail_qty": 0, "avg_cost": null, "cushion_pct": null}
}
```

bionic 只用其中的 `ts_code` / `side` / `day.price`（现价缺失时自己从 db13 分钟线取），
其余字段留作排查与将来扩展；区间一律按 bionic 自己库里的昨夜结论算，不用 PMS 转送的参考位
（PMS 的参考位在结论停更时会换成它自己兜底算的值，那不能当决策系统的结论用）。

### 3.2 bionic 侧处理（`pms_advisor.py`，版本标记 `daily_band_v1`）

**只有三步，全部是读现成的数、做比对，没有任何盘中判断：**

第一步，读昨夜结论。`strategy_daily_results` 里当晚算好的支撑位、压力位、定性。结论缺失、
超过 `PMS_EXEC_YSTRAT_MAX_AGE_DAYS`（默认 5 个自然日）、或支撑压力倒挂 → 回 UNAVAILABLE，
PMS 退实现 B。由支撑压力推出两个执行区间（推导是算术，宽度是参数）：

```
买入区间 = [ 支撑 × (1 − 外沿0.01),  支撑 + (压力 − 支撑) × 0.3 ]
卖出区间 = [ 压力 − (压力 − 支撑) × 0.3,  压力 × (1 + 外沿0.01) ]
只有单边参考位时, 区间宽度按该位的 3% 取, 另一侧回 UNAVAILABLE。
```

第二步，读当天监控。`strategy_audit_log` 今天对该股的裁决（盘前复审与盘中研判本来就在写
这张表；PMS_ 开头的行是我们替 PMS 写的，读的时候排除）。今天已有离场类结论
（REVERSAL_SELL / TAKE_PROFIT / FAIL）→ 买入一律等待、卖出直接放行。监控查询失败不拦
执行（监控是「为辅」），但应答的 `missing` 里必须写明 `today_audit`。

第三步，比对现价与区间：

| 情形 | 应答 |
|---|---|
| 买入，现价在买入区间内 | FIRE，**限价 = 区间上沿**（委托能一直活到价格离开区间，不再是贴着现价的窄缝——2026-08-03 委托全部到期的病根即在此） |
| 买入，现价高于区间上沿 | WAIT「不追，接受买不上」 |
| 买入，现价低于区间下沿（支撑位下方） | WAIT「区间外不买入」 |
| 买入，今天已有离场结论 | WAIT，理由带上是哪条结论 |
| 卖出，现价到达卖出区间（或更高） | FIRE，限价不给（交回 PMS 按它自己的口径贴现价挂出） |
| 卖出，现价未到卖出区间 | WAIT「等高一点卖，当日收尾的强制完成仍由 PMS 兜底」 |
| 卖出，今天已有离场结论 | FIRE（与 PMS 已订阅的卖出信号同源，不会互相矛盾） |

### 3.3 应答（bionic → PMS）

```json
{"verdict": "FIRE" | "WAIT" | "UNAVAILABLE",
 "limit_price": 9.95,           // 买入=区间上沿; 卖出与等待时为 null (PMS 用本地口径)
 "valid_min": 10, "reason": "现价 9.7 在买入区间 [9.4, 9.95] 内 (支撑 9.5/压力 11.0), 限价挂区间上沿",
 "confidence": null,
 "observed": {"price": 9.7, "support": 9.5, "pressure": 11.0,
              "buy_band": [9.4, 9.95], "sell_band": [10.55, 11.11],
              "y_signal": "BUY", "today_exit": null},
 "missing": [], "advisor": "daily_band_v1"}
```

PMS 侧对建议价有一道保护：偏离现价超过 `PMS_EXEC_LIMIT_BAND`（默认 10%，即 A 股单日
涨跌幅上限）视为异常数据，改回本地口径并在理由里说明。区间边缘离现价百分之几属正常，
正常联调不会触发这条。

### 3.4 下一个迭代（本轮明确不做）

凌晨管线加一步大模型推理，对每只票明示写出买入区间与卖出区间，盘中改为直接读它——届时
只换本模块第一步的取数来源和 `advisor` 版本标记，应答结构与 PMS 侧都不动。动凌晨管线
属于重大迭代，做之前会先出细方案确认（覆盖范围、存放位置、算不出来怎么办）。

---

## 4. 拿不到结论时两侧各自怎么办

| 情形 | bionic 回什么 | PMS 怎么办 | 人看哪里 |
|---|---|---|---|
| 研判超时/队列拥堵 | UNAVAILABLE（原因带超时） | 降级人工确认 + ERROR | PMS 日志 `[研判闸]`；频繁出现 → §5.2 切独立队列 |
| 研判裁决解析失败/越界 | UNAVAILABLE | 同上 | bionic 日志 `[PMS_JUDGE]` |
| 该股没有昨夜结论 / 结论过期 | UNAVAILABLE（原因写明） | 本轮退实现 B + 冷却 5 分钟 | 指令 `progress.exec_advice.error`；`last_decision.source` 显示 `B(实现A不可用: ...)` |
| 收盘后 / 无实时价 | UNAVAILABLE「无实时价」 | 同上 | 同上 |
| 择时接口网络不通/超时 | —（HTTP 层异常） | 同上 | 同上 |
| 当天监控表查询失败 | 照常按区间应答，`missing` 带 `today_audit` | 正常执行 | 应答的 missing 字段；bionic 日志 |
| `PMS_API_ENABLED=False` | UNAVAILABLE（原因写明开关） | 各按上述降级 | bionic `.env` |
| PMS 侧未配置（地址空 / `PMS_EXEC_IMPL=B`） | 不会发请求 | 研判闸=人工确认；择时=实现 B | PMS 参数设置页 |

---

## 5. bionic 侧部署（4090 机）

### 5.1 常规路径（本次交付走这条）

bionic 的源码是**挂载卷**（compose 里 `.:/app`），与 PMS 打进镜像正相反——**不需要
build，`git pull` 后重启进程即可**（backend-api 带自动重载，稳妥起见一起重启）。新增
配置全部有默认值，`.env` 不用动：

```bash
# 【4090 机 · bionic_trader 仓库根目录】
git pull
docker compose restart backend-api worker-brain
# 本机自测 (两接口, 不需要 PMS 参与; 研判走大模型要等 30~90 秒):
docker compose exec backend-api python scripts/pms_smoke.py
```

改动面：`config/settings.py`（P 段，全默认值）、`app/api/main.py`（两个接口）、
`workers/tasks_intraday.py`（PMS_JUDGE 分支，含同股同动作的重复研判缓存）、
`app/services/pms_advisor.py`（新）、`scripts/pms_smoke.py`（新）。
既有六个盘中裁决分支、既有 API、每晚判分、建仓仲裁的行为一行未动（PMS_JUDGE 是新增分支
且提前返回，不经过广播与重算；择时接口不写任何表）。

### 5.2 研判独立队列（预埋第二档，brain_queue 拥堵时再启用）

判据：PMS 侧 `[研判闸] 请求失败 ... Timeout` 成为常态（偶发超时降级人工确认是设计内
行为，不用切）。启用：bionic `.env` 加 `PMS_JUDGE_QUEUE=pms_queue`，compose 加一个
专属 worker 后 `docker compose up -d worker-pms && docker compose restart backend-api`：

```yaml
  worker-pms:
    build: {context: ., dockerfile: docker/Dockerfile}
    container_name: trader_worker_pms
    command: celery -A workers.celery_app worker -Q pms_queue -l info -c 2 --prefetch-multiplier=1
    env_file: .env
    environment: [TZ=Asia/Shanghai]
    volumes: ["/etc/localtime:/etc/localtime:ro", ".:/app"]
    depends_on: [redis]
    networks: [trader_net]
    restart: always
```

（astock-kg 公告链 2026-07-23 单队列把交互请求饿死的同款处方：交互请求与长任务分开排队。）

### 5.3 bionic 侧新增配置一览（均有默认值）

| 键 | 默认 | 说明 |
|---|---|---|
| `PMS_API_ENABLED` | True | 总开关，False = 两接口一律回 UNAVAILABLE |
| `PMS_JUDGE_QUEUE` | brain_queue | 研判任务队列 |
| `PMS_JUDGE_TASK_TIMEOUT` | 75 | API 等任务上限（秒），必须小于 PMS 侧的 90 |
| `PMS_JUDGE_RETRY_TTL` | 1800 | 同股同动作重复研判的间隔（秒），间隔内回上次结论 |
| `PMS_EXEC_BUY_BAND_RATIO` | 0.3 | 买入区间上沿 = 支撑 + (压力−支撑)×此比例 |
| `PMS_EXEC_SELL_BAND_RATIO` | 0.3 | 卖出区间下沿 = 压力 − (压力−支撑)×此比例 |
| `PMS_EXEC_BAND_EDGE` | 0.01 | 区间外沿的放宽（买入下沿=支撑×(1−此值)，卖出上沿对称） |
| `PMS_EXEC_ONE_SIDE_BAND` | 0.03 | 只有单边参考位时区间宽度按该位的此比例取 |
| `PMS_EXEC_VALID_MIN` | 10 | 应答建议有效期（交易分钟） |
| `PMS_EXEC_YSTRAT_MAX_AGE_DAYS` | 5 | 昨夜结论超此自然日龄按过期处理 |

区间比例是初值，联调后按应答里的 `observed`（区间与现价的实际关系）回看再调。

---

## 6. 联调步骤与判收

前提：factorevaluation 能路由到 4090 机的 38000 端口（同网段应当直通；不通先
`curl http://<4090内网IP>:38000/health` 排网络）。以下 `<BIONIC>` 代指
`http://<4090内网IP>:38000`。

```bash
# 1.【4090 机】部署与本机自测 —— §5.1 的三条命令
# 2.【factorevaluation · tradingSystem 根目录】跨机探活 (只读, 不落表不产生指令):
docker compose run --rm --no-deps pms-web python scripts/probe_bionic.py --base <BIONIC>
#    判读: pms_exec 回 FIRE/WAIT 即通, 看应答里的区间对不对得上该股昨夜的支撑压力;
#          回 UNAVAILABLE 看原因 (无实时价=收盘后正常; 没有昨夜结论=该股不在每晚分析
#          范围, 换一只探)。pms_judge 回 PASS/REJECT 即通, 30~90 秒正常。
# 3.【factorevaluation】页面「参数设置」填 PMS_JUDGE_API_BASE=<BIONIC>  → 研判闸生效。
#    验收: 下一轮自主提议扫描里, 需研判的动作 (FILL/ADD/DCA) 的评审账本出现
#    arbiter=judge 的 PASS/REJECT 行 (make t-gate 看), 而不再是"降级人工确认"。
# 4.【factorevaluation】要切择时A: 页面把 PMS_EXEC_IMPL 改 A (随时可改回 B)。
#    验收: make watch 里在途指令的 last_decision 出现 source=A / A缓存;
#    make t-ins 里 progress.exec_advice 有结论、区间理由与有效期。
# 5. 盘中观察一天: 每次咨询 bionic 侧日志有 [PMS 择时] 行; 对比实现A按区间给出的
#    出手/等待与实现B的差别 (关注限价挂区间上沿后, 委托是否不再成批到期)。
```

判收纪律照旧：**代码就绪 ≠ 接通，接通 ≠ 判收**。#10 判收 = 一笔真实提议走完
「规则闸 → 研判通过/驳回 → 档位分流」并在 PMS 评审账本留下 arbiter=judge 的行；
#9 判收 = 盘中一条指令按区间出手或等待、且对端故障时当轮自动退实现 B 有留痕
（`source=B(实现A不可用)`）。

## 7. 变更记录

| 版本 | 日期 | 内容 |
|---|---|---|
| V1.0 | 2026-08-03 | 首版定稿并双侧落码。择时部分是一套盘中判定规则（资金阈值、动量追买等） |
| V1.1 | 2026-08-03 | 按用户审核意见重做择时：删掉 V1.0 的盘中判定规则（违反「提前计算为主、盘中监控为辅、不另做盘中判断、可以接受买不上」），改为由昨夜支撑压力推出执行区间、盘中只做区间比对与当日监控核对；研判留痕不再写 `decision_ledger`（每晚判分全表扫描，已核实）；文档与代码注释清理生造词 |
