# tradingSystem · 宏观择时接入方案 V2（股汇对冲指数 → 组合仓位 + 个股宏观闸）

> 状态: **V2，按用户 2026-08-18 三条拍板意见修订，待标定后定稿** | 日期: 2026-08-18
> **V2 修订（依用户三点反馈）**：①触发口径不预设默认值，**先跑标定脚本**（`scripts/calibrate_macro_signal.py`，已交付）用历史数据对比"进区即动 vs 极值回落再动"后再定；②仓位调整由固定步长改为**对数映射**——极值越深加码越多、边际递减，参数由标定给建议值；③新增**个股宏观闸**：偏热区确认后，自主增持类个股动作（新建仓/回踩补足/盈利加仓/补仓）一并暂停——这同时堵上 V1 的一个洞（宏观降仓释放的现金会被 `PMS_OPEN_AUTONOMY=full` 的自主新建仓在下一分钟买回去）；④数据源按用户口径：`gp_fx_daily` / `gp_shibor` 在 **192.168.16.153（153 代理）**，标定脚本顺带完成列名探查与核实。
> 写作纪律沿袭项目既有文档：术语先白话、阈值给硬数字且全部可配置、失败路径显式、未确认事项进清单。
> 关联：`POSITION_MGMT_DESIGN.md`（总体设计）、`README.md`（模块地图与三条铁律）、`DEVLOG.md` 2026-08-06「动 tradingSystem 以外的系统之前先答三题」。

---

## 0. 一句话

给 PMS 加一个「宏观择时层」：每个交易日算一次股汇对冲指数，指数进入极热区就**像用户一样在命令台下降仓命令**（REDUCE_EXPOSURE，幅度按对数映射随极值深度加码），进入极冷区下升仓命令，中性不动；偏热确认期间同时**暂停自主增持类个股动作**（宏观闸，防止降仓的钱被自动建仓买回去）。它产生仓位变化的唯一出口是**命令**与**闸**；方案生成、规则闸、择时出手、下发、对账、进度回报，全部走现有链路，**一行不改**。框架按「信号注册表」做，未来再接别的宏观信号（bionic 宏观水温、择时层日 QRS 等）只加注册表项。

---

## 1. 现状盘点：目标需要什么 vs 系统已有什么

| 环节 | 这个目标需要 | 系统现状 | 差距 |
|---|---|---|---|
| 信号数据 | `zs_day_data`（上证）、`gp_fx_daily`（USD/CNH）、`gp_shibor`（SHIBOR 1M） | `zs_day_data` 在 199 库有实证（bionic market_regime 在读，列 `symbol/timestamp/close/amount`，代码点式 `000001.SH`）；**用户口径：fx 与 shibor 两表在 153 代理**。两表在五个项目里零代码引用，列名未经实测 | **小**：连接现成（153=proxy 源、199=index 源都已配），列名由标定脚本探查后回填 §4.2 |
| 信号计算 | 20 日对数收益 → spread → SHIBOR 修正 → 40 日 Z-Score ×10 | 全生态无任何股汇对冲指数实现（`hedge` 关键词全项目零命中）。bionic 有 `market_regime.py`（宏观水温），但那是决策系统内部件，PMS 不读 | **缺**：需新写，约百行纯函数 |
| 组合级自动决策 | 极值 → 升降整体仓位 | **无**。设计 §5 原话：「总仓上限由用户命令控制（升降仓判断权在用户，**不自动联动**）」。本需求是对这条设计决策的一次**有意扩展**，必须带着同样的档位/确认哲学落地 | **缺**：决策引擎（区制/确认/冷却/对数映射/上下限） |
| 个股动作联动 | 偏热时管住增量（用户拍板新增） | 自主提议五类动作只看个股与组合约束，不看宏观；`组合刹车`有"回撤后停自主增持"的同型机制可借鉴 | **缺**：宏观闸（§5.2，实现手势照抄刹车） |
| 执行面 | 把「降 X%」翻成逐股卖出并执行 | ✅ **已完备**。`REDUCE_EXPOSURE` / `INCREASE_EXPOSURE` 任务命令全生命周期：`command_service.issue()` → planner 四档凑额（停新买→清弱票→收利润→等比微减）→ 方案落表 → executor 转指令 → 规则闸终检 → 择时出手 → 回报进度。`pms_command.issued_by` 字段与 `/api/commands` 的 `by` 参数都在 | **无差距，全量复用** |
| 冲突仲裁 | 自动命令不得顶撞用户在途命令 | ✅ `command_spec.detect_conflicts` 已覆盖 REDUCE↔INCREASE、LIQUIDATE、HALT 等对 | 复用，宏观侧遇冲突**跳过不 force** |
| 档位/确认哲学 | 先建议后自动，人可随时收权 | ✅ 有成熟模板（`PMS_AUTONOMY` / `PMS_OPEN_AUTONOMY` 三档）。但 `pms_proposal` 是**个股级**，组合级建议放不进去 | **小**：宏观建议自己承载（§7） |
| 参数 | 阈值全部页面可调 | ✅ `param_store` 登记即出现在设置页、5 秒生效 | 登记即可 |
| 调度 | 每交易日定时算一次 | ✅ celery beat 骨架 + 三条守卫（交易日/休假/吞异常守成）成熟 | 加一个调度位 |
| 页面 | 看得见指数、区制、建议、闸状态 | ✅ 单页已有多面板先例 | 加一块面板 |
| 留痕 | 为什么动/为什么没动，可回查 | ✅ `pms_action_ledger` + 命令表 | 复用 + 新增信号历史表 |
| 历史存储 | 指数序列可回看、可判分、可标定 | 无 | **缺**：新表 `pms_macro_signal` |

结论：**执行面九成现成，缺的是"信号计算 + 组合级决策 + 宏观闸 + 一小块页面"这一层薄壳**。风险不在工作量，在接入姿势——所以本方案把大部分篇幅花在护栏上。

---

## 2. 目标翻译成系统语言：为什么走命令通道 + 扫描层闸

「极热 → 降低整体仓位」在本系统里最忠实的表达就是一条 `REDUCE_EXPOSURE {pct, window_tdays}`——语义完全一致（释放金额 = 规模 × pct，planner 按 停新买→清弱票→收利润→等比微减 四档凑额，这正是"pms 根据规则执行对个股的降仓位操作"）。反向就是 `INCREASE_EXPOSURE`。存量靠命令，增量靠闸：偏热期间把自主增持的**扫描**停掉（不产生候选就不会有指令），两头合起来才是完整的"仓位受宏观控制"。

刻意**不做**的两种姿势：

1. **不做"连续仓位控制器"**（宏观层直接持有目标仓位、每分钟收敛）——绕开命令系统另立产生买卖的路径，与命令冲突时没有仲裁者，正是禁区。
2. **不把宏观观点塞进规则闸**——规则闸是合规终检，塞行情观点会让"没通过合规"和"宏观不看好"在评审账本里分不开；宏观闸放在**扫描入口**（proposal_service），与"组合刹车""休假模式"同一落点、同一手势。

走命令通道的三个直接好处：宏观动作在**命令台可见、可撤销、可改窗口**（命令至上铁律天然成立）；与用户命令的冲突由现有 `detect_conflicts` 仲裁；执行成本统计、进度日结、判分全部免费获得。

---

## 3. 总体架构与六条设计原则

```
        每交易日 09:35 (beat: macro_scan)         页面「宏观择时」面板
                 │                                    │ 手动扫描 / 一键采纳
                 ▼                                    ▼
  ┌──────────────────────────────────────────────────────────┐
  │ macro_service.scan()                                      │
  │  取数(macro_repo, 三张源表单表查) → 计算(macro_rules 纯函数) │
  │  → 落 pms_macro_signal (当日一行, 重扫就地更新)              │
  │  → 决策(macro_rules.decide: 区制/确认/冷却/对数映射/上下限)   │
  │  → 分流:                                                  │
  │      autonomy=full         → command_service.issue(       │
  │                                REDUCE/INCREASE_EXPOSURE,  │
  │                                issued_by="macro")          │
  │      autonomy=propose_only → 当日建议落表, 面板等用户点头     │
  │      autonomy=off / ENABLED=False / 信号UNAVAILABLE → 不动 │
  │  → 每一次动作与每一次"想动被拦"落 pms_action_ledger          │
  └──────────────────────────────────────────────────────────┘
        │ (命令一旦下达, 与用户手下的命令零区别)     │ 宏观闸状态 (偏热确认中?)
        ▼                                        ▼
  command_poll → planner → executor        proposal_service 扫描入口:
  → 规则闸 → 择时 → 下发 → 对账             偏热确认 → 增持类扫描暂停(留痕)
```

六条设计原则（每条都能在代码评审时逐一核对）：

1. **默认关**：`PMS_MACRO_ENABLED=False`。部署完成后系统行为与今天完全一致，开关和档位都在页面上，随时收权。
2. **唯一出口**：宏观层产生仓位变化的途径只有两条——`command_service.issue()` 下命令、扫描入口的宏观闸（只拦不发）。不直接写 `pms_plan` / `pms_instruction` / `pms_position`，不调 executor，不碰账本。
3. **故障即守成**：任一数据源取不到、样本不足、日龄超限 → 信号置 `UNAVAILABLE`，**不动作、不落闸**（拿不到 ≠ 偏热，闸的安全方向是"不额外拦"——增持本身另有规则闸把关），面板亮黄条。挂在调度器 `guard` 下，异常吞掉只记 ERROR。
4. **命令至上**：与用户在途命令冲突（issue 返回 CONFLICT）→ 本轮放弃并留痕，**绝不 force**；`HALT_ALL`（休假）由调度守卫直接跳过；`PMS_GLOBAL_BUY_HALT` 生效时不发升仓；组合刹车期间默认不发升仓（可参数放开）。**用户命令永不受宏观闸限制**，且随时可在命令台撤销宏观命令。
5. **限频防抖**：进区确认天数 + 迟滞退出带 + 同方向冷却 + 每信号每交易日至多一次动作 + 对数映射的"目标追踪"（§5.1，同一深度不重复加码）+ 上一条宏观命令在途不叠加。指数在极值区横跳不会造成反复买卖。
6. **全留痕**：信号值每日入 `pms_macro_signal`（含中间量，可复算）；每次触发/被拦/落闸入 `pms_action_ledger`（action=MACRO，hard_numbers 带当时指数值、区制、超额深度、当前仓位、本次步长）；命令表 `issued_by='macro'` 一眼可辨。

---

## 4. 信号层设计

### 4.1 计算口径（按定义逐条落地）

数据统一为按国内交易日对齐的日频序列，全部用**收盘值**：

```
① stock_ret = ln(close_t / close_{t-20})            # zs_day_data, symbol='000001.SH'
   fx_ret    = ln(fx_t / fx_{t-20})                  # gp_fx_daily USD/CNH, 正值=人民币贬值
   （fx 的日期先 +1 自然日, 再对齐到国内交易日: 当日无值取最近前值, 即 as-of 对齐）
② spread     = stock_ret + fx_ret
③ spread_adj = spread − β × (SHIBOR1M_t − SHIBOR1M_{t-20})     # β 默认 0.02
④ hedge_index = (spread_adj − mean40(spread_adj)) / std40(spread_adj) × 10
```

硬性口径，写进单测：窗口内任一序列缺口用 as-of 前值补齐但**记录补齐天数**；有效样本 < 20+40+5 个交易日 → `UNAVAILABLE`；`std40 = 0` → `UNAVAILABLE`；三源中最新日期落后当前交易日超过 `PMS_MACRO_STALE_TDAYS`（默认 3）个交易日 → `UNAVAILABLE`。

区制（zone）判定带迟滞，防横跳（阈值初值 ±25/±15，**以标定结果为准回填**）：

| zone | 进入条件 | 退出条件 |
|---|---|---|
| HOT（偏热，即 positive 区） | hedge_index > `PMS_MACRO_HOT_TH` | 回落到 < `PMS_MACRO_EXIT_BAND` |
| COLD（偏冷，negative 区） | hedge_index < `PMS_MACRO_COLD_TH` | 回升到 > −`PMS_MACRO_EXIT_BAND` |
| NEUTRAL | 其余 | — |
| UNAVAILABLE | 数据守卫未过 | 数据恢复 |

### 4.2 数据访问（严格单表，三条独立查询）

新增 `app/repo/macro_repo.py`。**用户口径：`gp_fx_daily` / `gp_shibor` 在 153 代理** → 走 `db.session.fetch_all(..., source="proxy")`；`zs_day_data` 已证实在 199（`source="index"`），若标定脚本探出 153 也可达则统一走 proxy（少一个依赖库）。每张表一条单表 SQL（`WHERE ... ORDER BY 日期 DESC LIMIT 120` 量级），完全符合单表守卫。**列名以标定脚本探查结果为准回填此处，回填前不写 macro_repo**：

| 表 | 源 | 日期列 | 取值列 | 过滤条件 |
|---|---|---|---|---|
| zs_day_data | index（153 待探） | timestamp | close | symbol='000001.SH' |
| gp_fx_daily | proxy(153) | 待探 | 待探 | 待探（USD/CNH 筛选） |
| gp_shibor | proxy(153) | 待探 | 待探（1M） | 待探 |

`pms_macro_signal` 的读写走 `source="proxy"`（与其余 pms_* 表同侧）。

### 4.3 信号注册表（多信号扩展点）

`app/services/macro_service.py` 内一张注册表，V1 只有一项：

```python
SIGNALS = {
    "hedge_fx": {                       # 股汇对冲指数
        "label": "股汇对冲指数",
        "fetch": macro_repo.fetch_hedge_inputs,      # -> 原始序列
        "compute": macro_rules.compute_hedge_index,  # -> {value, zone, detail}
    },
    # 未来: "bionic_regime" (决策系统宏观水温) / "index_qrs" (择时层日QRS) ——
    # 只加表项与各自参数(命名空间 PMS_MACRO_<KEY大写>_*), 决策层/执行层/闸/页面零改动。
}
```

启用哪些由 `PMS_MACRO_SIGNALS`（逗号分隔，默认 `hedge_fx`）控制。多信号合成 V1 定死**最保守者优先**：任一启用信号 HOT → 整体偏热；无 HOT 而有 COLD → 偏冷；否则中性。加权合成属二期，不做预设计。

### 4.4 新表 `pms_macro_signal`（第 18 张表）

```sql
CREATE TABLE IF NOT EXISTS pms_macro_signal (
  id          BIGINT PRIMARY KEY AUTO_INCREMENT,
  signal_key  VARCHAR(32)  NOT NULL COMMENT '信号名, 如 hedge_fx',
  trade_date  INT          NOT NULL COMMENT '北京交易日 YYYYMMDD',
  value       DECIMAL(12,4)         COMMENT 'hedge_index 值',
  zone        VARCHAR(16)  NOT NULL DEFAULT 'NEUTRAL' COMMENT 'HOT/COLD/NEUTRAL/UNAVAILABLE',
  detail_json TEXT                  COMMENT '中间量: stock_ret/fx_ret/spread/spread_adj/超额深度/各源末日/补齐天数/周期已执行调整',
  action      VARCHAR(32)  NOT NULL DEFAULT 'NONE' COMMENT 'NONE/ADVICE_REDUCE/ADVICE_INCREASE/CMD_ISSUED/BLOCKED/GATED',
  ref_id      VARCHAR(64)  NOT NULL DEFAULT '' COMMENT '关联 command_id (下达或采纳后回填)',
  note        VARCHAR(500) NOT NULL DEFAULT '' COMMENT '被拦原因/建议文案/闸状态',
  updated_at  DATETIME COMMENT '写入或重扫更新时间',
  UNIQUE KEY uk_sig_date (signal_key, trade_date)
) COMMENT='宏观信号日快照与动作记录 (每信号每交易日一行, 重扫就地更新)';
```

一行四用：历史序列（面板画近 20 日）、当日建议的承载（propose 档）、冷却与"周期已执行调整"的事实源、宏观闸的状态源（扫描层 5 秒缓存读当日行）。

---

## 5. 决策层设计

### 5.1 组合仓位：对数映射（V2 改，替代固定步长）

极值越深，调整越多，但**边际递减**——既回应"越极端越该动"，又天然防止 Z-Score 尾部把仓位一次打爆。纯函数 `macro_rules.decide(...)`，规则按优先级：

1. `PMS_MACRO_ENABLED=False` 或档位 off 或 zone ∈ {NEUTRAL, UNAVAILABLE} → NONE。
2. 确认：连续处于同一极值区 ≥ `PMS_MACRO_CONFIRM_DAYS`；触发口径 `PMS_MACRO_TRIGGER_MODE`（zone_enter / zone_exit，**默认值由标定结果定**，两种都实现、页面可切）。
3. **对数映射（核心）**：
   - 超额深度 `e = |hedge_index| − 进入阈值`（e ≥ 0，单位=指数点）；
   - 本轮极值周期的**累计目标调整** `target(e) = min( S0 × ln(1 + e / k), PMS_MACRO_SHIFT_MAX )`（占规模的百分点）；
   - 本次步长 `step = target(e_now) − done_shift`（done_shift = 周期内已执行的宏观调整，记在当日信号行 detail 里）；`step < PMS_MACRO_STEP_MIN`（默认 0.02）→ 不动；
   - **周期**定义：进区 → 迟滞退出；退出后 done_shift 清零，下个周期重新累计。同一深度不重复加码，指数横盘在区内不会连环下命令；指数**更深**才有新步；
   - `S0` / `k` / `SHIFT_MAX` 初值由标定脚本给建议（目标形状：e 中位数 → 约 5 个点，e 95 分位 → 约 12 个点，封顶 20 个点），页面可调。
4. 冷却：同方向上一次**生效**动作距今 < `PMS_MACRO_COOLDOWN_TDAYS`（默认 3 个交易日）→ 本轮不动（下轮 target 追踪仍在，冷却只是把步子隔开）。「生效」= 所发命令未被立刻 CANCELLED（零方案的升仓命令不占冷却，次日重试）。
5. 在途：上一条宏观命令仍在 EXECUTING/PARTIAL → 不叠加。
6. 方向裁剪：
   - HOT → 降仓，`step` 再受地板裁剪：不把总仓位降到 `PMS_MACRO_MIN_PCT`（默认 0.20）以下（用户命令不受此限）；
   - COLD → 升仓，`step` 受 `PMS_MACRO_MAX_PCT`（默认 0=由 `PMS_PORTFOLIO_CAP` 兜底）裁剪；
   - 升仓独有护栏：`PMS_GLOBAL_BUY_HALT` 生效不发；组合刹车生效且 `PMS_MACRO_RESPECT_BRAKE=True`（默认，用户未反对，标注为**假设**）不发。降仓不受这两条限制（与"冻结与刹车只挡增持不挡减持"同口径）。

### 5.2 个股宏观闸（V2 新增，用户拍板"个股交易动作受宏观信号控制"）

**语义**：偏热（HOT）确认期间，PMS 自己发起的**增持类**个股动作暂停——自主新建仓（OPEN）、回踩补足（FILL）、盈利加仓（ADD）、补仓（DCA）的扫描整体跳过并留痕；**减持类动作（TRIM/风控卖出/清仓）与用户命令永不受限**。COLD 期间不加额外限制（自主动作照常，加量由升仓命令负责）。这与"组合刹车"的既有语义完全同型（"自主增持停，命令类不受限"），实现手势照抄。

**为什么必须有**：不加闸的话存在一条自我打架的路径——宏观降仓卖出 → 现金变多 → `PMS_OPEN_AUTONOMY=full` 的自主新建仓下一分钟把钱又买回去。闸把增量管住，命令管存量，才闭环。

**实现落点（两处，均为纯增量，与既有 `exec_halt` / 刹车检查并排）**：
- `proposal_service.scan_and_route` 入口：宏观闸生效 → 四类已有持仓动作中**买入侧**候选不扫（TRIM 照常），skipped 留痕"宏观偏热闸"；
- `proposal_service._scan_open` 入口：宏观闸生效 → 本轮不建仓，skipped 留痕。

闸状态由 `macro_service.gate_state()` 提供（读当日 `pms_macro_signal`，进程内 5 秒缓存，**读不到 = 不落闸**——闸的安全方向是"不额外拦"，增持自有规则闸把关，不能让宏观层故障把整个自主引擎摁死）。开关 `PMS_MACRO_STOCK_GATE`（默认 True，随 `PMS_MACRO_ENABLED` 总开关生效；即便档位是 propose_only，闸也生效——"停止买入"是保守方向动作，与"减持方向不设确认门槛"同哲学）。

**边界（待拍板 §12.3）**：个股交易方案（网格/做T/跟踪）的买入侧要不要一并受闸。V2 建议**先不动策略层**——策略是用户特意设的（signal_service 对挂策略票的处理哲学是"只提示不替你做"），宏观闸自动暂停它们容易造成困惑；若你要管，复用现成的 `strategy_service.pause_buy`（页面可恢复）一行接入即可。

---

## 6. 执行接入（兼容性核心节）

### 6.1 下达即普通命令

```python
res = command_service.issue(
    "REDUCE_EXPOSURE",                       # 或 INCREASE_EXPOSURE
    {"pct": step, "window_tdays": param_store.get_int("PMS_MACRO_WINDOW_TDAYS", 3)},
    issued_by="macro",
    note="宏观择时: hedge_fx=+31.2 偏热确认(阈值+25, 超额6.2), 周期目标降8.6点已降0点, 本次降8.6点")
```

- `issue()` 内部自动做冲突检测：返回 `ok=False` 且 errors 含 CONFLICT → 宏观层**留痕跳过**（ledger verdict=REJECT），当日不再重试。
- 命令落表后与用户命令完全同路：`command_poll` 每分钟排方案、`intraday_exec` 择时出手、`refresh_progress` 日结进度。`shadow` 通道下只记账不真发（与现状一致，宏观层无需感知通道模式）。

### 6.2 调度时点：09:35，不是盘前

一个已核实的坑决定了时点：`positions_view` 在取不到实时价时用摊薄成本顶住价格并标 `price_ok=False`，而 `planner._usable` **排除** `price_ok=False` 的票（2026-07-31 静默失败专项第九条）。行情 db13 只有当日分钟线，盘前全部无价——盘前下达的 REDUCE 命令会在一分钟内被排成**零方案并置 CANCELLED**。所以 `macro_scan` 定在 **09:35**（开盘后行情已就位，也在 08:40 拉候选池之后，升仓有票可选）。信号用的是 T-1 日终数据，几点算值都一样。beat 表加一行：

```python
"macro_scan": {"task": "pms.macro_scan", "schedule": crontab(hour=9, minute=35)},
```

任务本体 `@guard(trade_day=True, respect_exec_halt=True)`——休假模式自动跳过，异常吞掉守成。宏观闸的状态在 09:35 扫描落表后当日恒定（信号是日频慢变量），盘中每一跳扫描层读的是同一行。另设手动端点（§7.3）供盘中任意时刻重扫/试算。

### 6.3 护栏对照表（每条对应代码里一处显式检查）

| 场景 | 行为 | 靠什么保证 |
|---|---|---|
| 休假模式 HALT_ALL | 整跳跳过 | 调度 guard `respect_exec_halt`；手动端点内 `macro_service` 自查一遍 |
| 用户在途 REDUCE/INCREASE/LIQUIDATE 冲突 | 跳过 + 留痕，不 force | `issue()` 现有 `detect_conflicts`，宏观层永不传 `force_conflict=True` |
| 全局暂停买入 | 升仓不发 | 引擎显式读 `PMS_GLOBAL_BUY_HALT`（此开关在参数表而非命令表，冲突检测看不见它，必须自查） |
| 组合刹车 | 升仓默认不发 | 引擎读 `PMS_BRAKE_UNTIL` 比对当日 |
| 数据缺失/停更 | 不动作、不落闸 + 面板黄条 | 信号 UNAVAILABLE 分支 |
| 极值区横跳/横盘 | 不反复动 | 迟滞退出带 + 对数目标追踪（同深度不重复加码）+ 冷却 + 每日一次 |
| 上一条宏观命令未走完 | 不叠加 | 查 `issued_by='macro'` 的在途命令 |
| 参数表读不到 | 宏观层整体失效（安全方向） | `PMS_MACRO_ENABLED` 文件初值 False，表挂了退初值即关；闸读不到状态 = 不拦 |
| 仓位已到地板/天花板 | 不动 + 留痕 | decide() 映射裁剪 |
| 宏观闸误伤减持 | 不可能 | 闸只挡买入侧扫描，TRIM/EXIT/风控卖出路径不经过闸 |

---

## 7. 档位、建议承载与页面

### 7.1 三档（沿用系统词汇）

`PMS_MACRO_AUTONOMY`: `off` / `propose_only`（**默认**）/ `full`。
- off：只算信号、只落历史、只展示，不出建议不下命令（**闸也不生效**——off 是"只看不动"档）。
- propose_only：触发时把建议写进当日 `pms_macro_signal` 行（action=ADVICE_*，note 带完整理由与建议 pct），面板出卡片等一键采纳；**当日有效**，隔日自动作废。**宏观闸照常生效**（保守方向自动，见 §5.2）。
- full：直接 `issue(issued_by="macro")`。三条铁律下用户仍可在命令台撤。

不复用 `pms_proposal`：它的采纳路径是"生成该股指令"，是个股级语义；硬塞组合级建议要改共用 decide 端点。宏观建议放自己的表行 + 自己的采纳端点，互不沾染。

### 7.2 采纳路径

`POST /api/macro/adopt {signal_key}` → 校验建议仍是当日且未采纳 → 重跑一遍 decide 的护栏（冲突/开关/地板可能在建议挂出后变化）→ `command_service.issue(..., issued_by="user")`（用户点的头就是用户意志）→ 回填 `ref_id=command_id, action=CMD_ISSUED`。幂等：同一行只能采纳一次。

### 7.3 API 与页面

- `GET /api/macro/status`：各信号 {value, zone, 超额深度, 近 20 日序列, 数据日龄, 当日建议, 闸状态, 最近宏观命令及进度, 冷却剩余, 周期已执行调整}。
- `POST /api/ops/macro-scan?dry_run=true|false`：手动扫描；dry_run 只算不落表不下达。
- `POST /api/macro/adopt`：见上。
- 页面 `index.html` 加「宏观择时」面板：指数当前值 + 区制色带（热红/冷蓝/中性灰/不可用黄）+ 近 20 日迷你走势 + 当日建议卡（一键采纳/忽略）+ **宏观闸状态条**（"偏热闸生效中：自主增持已暂停"）+ 最近宏观命令进度。参数不用做界面——`PMS_MACRO_*` 登记后自动出现在「参数设置」。
- Makefile 顺手加 `t-macro`（status + 试算），非必需。

---

## 8. 参数清单（settings.py 初值；全部经 param_store 页面可调；标 ⚙ 的初值由标定回填）

| 参数 | 默认 | 说明 |
|---|---|---|
| PMS_MACRO_ENABLED | False | 宏观择时总开关（关=调度位空转，不取数不计算，面板显示"未启用"） |
| PMS_MACRO_AUTONOMY | propose_only | off / propose_only / full |
| PMS_MACRO_SIGNALS | hedge_fx | 启用的信号清单（逗号分隔） |
| PMS_MACRO_STOCK_GATE | True | 个股宏观闸：偏热确认期间暂停自主增持类扫描（off 档整体不生效） |
| PMS_MACRO_HOT_TH ⚙ | 25.0 | 偏热进入阈值 |
| PMS_MACRO_COLD_TH ⚙ | -25.0 | 偏冷进入阈值 |
| PMS_MACRO_EXIT_BAND ⚙ | 15.0 | 迟滞退出带（\|值\| 回落到此内算离区） |
| PMS_MACRO_CONFIRM_DAYS ⚙ | 1 | 进区连续 N 个交易日才触发 |
| PMS_MACRO_TRIGGER_MODE ⚙ | zone_enter | zone_enter=进区即动 / zone_exit=极值回落再动（**默认值待标定对比后定**） |
| PMS_MACRO_LOG_S0 ⚙ | 0.12 | 对数映射系数：target = S0·ln(1+e/k) |
| PMS_MACRO_LOG_K ⚙ | 10.0 | 对数映射尺度 k |
| PMS_MACRO_SHIFT_MAX ⚙ | 0.20 | 单个极值周期的累计调整封顶（占规模的百分点） |
| PMS_MACRO_STEP_MIN | 0.02 | 裁剪后步长小于此不动 |
| PMS_MACRO_COOLDOWN_TDAYS | 3 | 同方向两次动作最小间隔（交易日） |
| PMS_MACRO_MIN_PCT | 0.20 | 宏观降仓地板（自动降仓不把总仓位降到此下；用户命令不受限） |
| PMS_MACRO_MAX_PCT | 0.0 | 宏观升仓天花板；0=不单设，由 PMS_PORTFOLIO_CAP 兜底 |
| PMS_MACRO_WINDOW_TDAYS | 3 | 宏观命令的执行窗口 |
| PMS_MACRO_RESPECT_BRAKE | True | 组合刹车期间不自动升仓（假设项，见 §12.4） |
| PMS_MACRO_STALE_TDAYS | 3 | 任一数据源日龄超此（交易日）→ UNAVAILABLE |
| PMS_MACRO_RET_WIN / Z_WIN | 20 / 40 | 收益窗 / Z-Score 窗 |
| PMS_MACRO_SHIBOR_BETA | 0.02 | SHIBOR 修正系数 β |

`_RANGES` 同步登记，`DESC` 写中文说明。**不进 FAIL_CLOSED**：ENABLED 的文件初值就是 False，表读不到退初值即安全方向。

---

## 9. 触碰面清单与三条铁律自证

新增文件（5 个，互相独立，删净即回到今天——"三题"规矩第 3 题）：

| 文件 | 内容 |
|---|---|
| app/core/macro_rules.py | 纯逻辑：对齐与四步计算、zone 迟滞判定、对数映射 decide（无 IO，可单测） |
| app/repo/macro_repo.py | 三张源表单表取数 + pms_macro_signal 读写 |
| app/services/macro_service.py | 编排：守卫→取数→算→落表→决策→分流→留痕；status / adopt / gate_state |
| scripts/calibrate_macro_signal.py | 标定脚本（**已交付**，探查+分布+口径对比+对数参数，只读） |
| scripts/test_batch14_units.py | 单测约 25 例（§11） |

触碰的既有文件（9 处，**全部纯增量**——只加行/加检查，不改既有行为，逐处可 diff 核对）：

| 文件 | 加什么 | 不动什么 |
|---|---|---|
| config/settings.py | PMS_MACRO_* 字段一段 | 既有字段零改动 |
| app/services/param_store.py | DESC / _RANGES / _range_check 各加宏观项 | 缓存、FAIL_CLOSED、既有键 |
| app/scheduler.py | `macro_scan` 任务 + beat 一行 | 既有九个调度位 |
| app/services/proposal_service.py | 两处入口各加一个宏观闸检查（与 exec_halt 检查并排，闸关/读不到=原行为） | 扫描、闸门、分流逻辑零改动 |
| app/web/main.py | 三个新端点 | 既有端点 |
| app/web/static/index.html | 一块面板 | 既有面板 |
| ddl_pms_v1.sql | 第 18 张表 CREATE IF NOT EXISTS | 既有表 |
| scripts/check_db.py | PMS_TABLES 加 pms_macro_signal | （顺带发现：pms_strategy / pms_op_log 也不在这份清单里，属既有欠账，按"不夹带"纪律单独提，不在本次一起改） |
| scripts/run_tests.py | SUITES 登记 batch14 | 既有批次 |

三条铁律逐条自证：**命令至上**——宏观动作本身就是命令，冲突让位用户，可撤可改；宏观闸只拦 PMS 自主动作，用户命令与减持全不受限。**分工不越权**——宏观层只回答"整体该多重、增量该不该停"，逐股怎么卖买仍由 planner/择时/规则闸决定，不碰决策系统（bionic/桥/择时层**零改动**，"三题"规矩天然满足）。**先记账后动作 + 故障即守成**——信号先落表再决策，命令走 issue 的先落表路径，任何取数/计算失败 = 不产生新指令、不落闸。

---

## 10. 前置标定（先于一切编码，脚本已交付）

`scripts/calibrate_macro_signal.py`（**严格只读**，只发 SELECT/SHOW）在桥机跑，一次回答四件事：

1. **列名探查**：对 153（proxy）与 199（index）两源探查三张表的位置、列名、样本行——结果回填 §4.2 后才写 macro_repo；
2. **分布体检**：hedge_index 历史分位数、±20/25/30 各阈值的触发天数与年频次、极值区段的持续天数与最大深度——回答"±25 合不合身"；
3. **触发口径对比**：zone_enter vs zone_exit 各自触发事件后 5/10/20 交易日上证的均值/中位数/胜率（HOT 后应偏负、COLD 后应偏正才算有效）——**注意样本量会很小，结论当参考不当真理**；
4. **对数映射标定**：极值周期超额深度 e 的分布 → 解出 S0/k 建议值（e 中位数→5 点、95 分位→12 点、封顶 20 点），并打 e→调整幅度对照表。

**运行方式（不需要重建镜像**——脚本从宿主工作树经 stdin 喂给容器 python，`python -` 的 sys.path[0] 是工作目录 /app，`config.settings` 照常可导入）：

```bash
cd ~/tradingSystem        # git pull 之后
docker compose run --rm -T pms-web python - < scripts/calibrate_macro_signal.py > /tmp/macro_calib_report.md
cat /tmp/macro_calib_report.md
# 需要完整序列时:
docker compose run --rm -T pms-web python - --dump-csv < scripts/calibrate_macro_signal.py > /tmp/hedge_series.csv
```

探查失败的退路：脚本会打出该表全部列名与样本行，按提示带 `--fx-table/--fx-date-col/--fx-price-col/--fx-where/--shibor-*` 覆盖重跑；表真不存在 → 数据先落地（数据管道补），或立项由桥/择时层供数（跨系统改动，按"三题"规矩单独议，本方案不带）。

---

## 11. 实施步骤与判收

| 步 | 内容 | 判收标准 |
|---|---|---|
| 1 | 桥机跑标定脚本（§10，可立即做） | 三表列名落实；报告出分布/口径对比/S0-k 建议；把报告拿回来定 §8 带 ⚙ 的初值 |
| 2 | 写码 + batch14 单测 | `make test` ALL SUITES PASS。用例：合成序列对照手算值验四步计算；fx +1 对齐与缺口 as-of；样本不足/停更→UNAVAILABLE；zone 迟滞进出与横跳；对数映射（target 单调、同深度不重复加码、周期清零、封顶）；confirm/cooldown/每日一次；零方案命令不占冷却；冲突/HALT_BUY/刹车跳过（打桩）；宏观闸（HOT 拦买入侧不拦 TRIM、读不到不拦、off 档不拦）；建议采纳幂等与隔日作废 |
| 3 | ddl 建表 + `make deploy`（收盘后） | `make check` 见 18 表；页面参数区出现 PMS_MACRO_*，ENABLED=False，系统行为与部署前一致 |
| 4 | 手动 `POST /api/ops/macro-scan?dry_run=true` | 面板出指数值与区制；与标定脚本同日值一致（同一套纯函数，天然一致） |
| 5 | 开 ENABLED=True，档位 propose_only，观察 ≥1 周 | 每日一行信号历史；极值日出建议卡不出命令；宏观闸在 HOT 日真的把自主增持 skipped（留痕可查）；采纳一次走通 建议→命令→方案→指令 全链 |
| 6 | 冲突演练：挂一条在途 INCREASE，手动扫描触发降仓 | 账本见 MACRO REJECT 留痕，无命令产生 |
| 7 | （拍板后）切 full | 首条自动命令端到端回执，日报关注区可见 |

回滚：页面把 `PMS_MACRO_ENABLED` 置 False 即回到现状（秒级，命令、闸、建议全停）；代码级回滚 = 还原上表 9 处触碰 + 删 5 个新文件；新表留着无任何读方，无副作用。

---

## 12. 待拍板清单（V2 更新）

1. ~~触发口径默认值~~ → **改为由标定结果定**：跑完 §10 脚本，拿两种口径的事后对比数据来选（样本小的话建议 zone_enter + CONFIRM_DAYS 取 1~2 里标定更稳的那档）。
2. ~~固定步长 vs 目标带~~ → **已定：对数映射**（用户拍板）。S0/k/SHIFT_MAX 初值等标定回填。
3. **策略层（网格/做T）的买入侧要不要受宏观闸**：V2 建议先不动（理由 §5.2 末），要管的话复用 `strategy_service.pause_buy` 一行接入。
4. **升仓是否受组合刹车约束**：上轮问题未获直接答复，V2 按**受约束（True）**落地并标注为假设；要放开改参数即可。
5. **数据源**：已答——153。zs_day_data 是否也统一走 153 由探查定。

## 13. 风险与已知限制（说在前面）

- **信号本身是慢变量**：20 日收益 + 40 日 Z，天然滞后，抓的是阶段冷热不是日内顶底；对数映射 + 冷却是为此设的缓冲。
- **z-score 的尾部行为**：40 日窗里刚经历过极端行情时 std 变大，新极端可能"钝化"；反之平静期小波动被放大。标定报告的分位数表就是用来看这个的。
- **事后对比样本小**：±25 级别的极值一年可能只有几次，标定里的口径对比是参考不是显著性检验，别过度拟合。
- **升仓依赖候选池**：极冷区触发升仓但当日无强传导候选 → 命令零方案 CANCELLED（留痕可见，不占冷却，次日重试）——"宁缺毋滥"纪律优先于宏观意愿，这是设计取向不是缺陷。
- **宏观闸的机会成本**：偏热确认期间个股级的好机会也会被一并拦下（闸不看个股质地）——这是"个股动作受宏观控制"的题中之义；propose_only 观察期里可以从 skipped 留痕回看拦了什么，评估闸的松紧。
- **UNAVAILABLE 长期化**：数据停更时宏观层安静失效（不动作、不落闸），只有面板黄条与日报提示——不会误动作，但也不会提醒你"该人工判断了"，需要把面板纳入日常一眼。
