丰富交易逻辑

This commit is contained in:
zlt 2026-08-11 10:35:03 +08:00
parent 46f1a09a72
commit 18e7c88d16
9 changed files with 366 additions and 3 deletions

146
PER_STOCK_STRATEGY_PLAN.md Normal file
View File

@ -0,0 +1,146 @@
# PMS 个股交易方案(策略)框架 · 设计 v1.1
> 2026-08-11。这份 governs 下单口的改动是全系统风险最高的一段。v1.1 并入用户四点拍板。
> **已定方向**v1 做 **做T + 网格 + 跟踪止盈**(同一自动机的三种配置);新挂策略**默认自动执行**
> `autonomy=auto`,仍可逐策略改成「待确认」);**挂了策略的股票从动作引擎自动提议里排除**。
> 纪律:只做加法(不改既有执行/闸门/动作引擎逻辑)、与现有引擎清晰隔离、**开发期先 shadow 验代码再切实盘**。
>
> **四点拍板2026-08-11**:① 网格跌破下界 = **继续持有、不再买**(停买入腿、保留已买、回升仍按已买档卖、告警);
> ② 做T 当日次数上限 = **3 次**;③ 新挂策略默认自主档 = **直接自动执行**;④ **跟踪止盈补进 v1**
---
## 一、目标与判收
**最终检验**用户能给某只持仓挂一套价格触发的交易方案做T / 网格 / 跟踪止盈),系统在 **T+1 与仓位纪律**
约束内、按方案自动(或待确认)地做买卖;全程留痕、可暂停、可看实时状态、有熔断/上限/平回兜底。
**输出级判收实机shadow 先行)**
1. 做T一笔**完整平回** —— 当日买卖数量对齐T 仓过夜为 0当日达 3 次后不再做;
2. 网格:一档买入 + 一档卖出各走通并落账本;跌破下界后**不再买**、已买仓位保留;
3. 跟踪止盈:价格从高点回落到设定比例时**触发一笔卖出**并落账本;
4. 任何策略动作都**过规则闸** —— 违反单股上限 / T+1 可卖 / 暂停买入 时被拦下;
5. 挂了策略的票,**不再冒自动提议**;暂停某策略后它**不再出手**;全局暂停买入 / 休假 / 一键清仓对策略同样生效。
---
## 二、现状(以代码为准)
**做T = 搭了架子、没通电**:参数全(`PMS_T0_RATIO_MAX`、`PMS_T0_PULLBACK_PCT`、`PMS_T0_RALLY_PCT`、
`PMS_T0_ROUND_TARGET`、`PMS_T0_CLOSE_TIME`、单票/全局当日T亏熔断、授权命令`T0_ENABLE`)、
账本全(`t0_qty/t0_ratio/t0_count_today/realized_t_profit`、T0 批次、卖出先核销 T0、日初重置、成本含T利润
`planner``A_T0="T0_ROUND"`**但 `executor.run_tick` 的动作集不含 T0无正T/反T触发逻辑**。设计标「二期、默认关」。
**网格、跟踪止盈 = 无**(跟踪止盈与既有「保垫减仓」同源但那是动作引擎里按安全垫自动跑的,非逐股用户配置)。
**执行链(可整条复用)**`run_tick` 每分钟一跳,遍历在途 `pms_instruction``exec_advisor` 择时 → 切片 →
(卖)按 `avail_qty` 封顶(**T+1 在此挡住**)→ `rule_gate.check` 合规终检 → `dispatcher.dispatch`→ws→QMT
子单 + 评审账本。指令来源 = 命令方案 / 提议采纳 / 信号。
---
## 三、一条铁律定了整个框架T+1
当日买入不可当日卖出 → 「今天低买高卖同一批股」做不到 → 做T / 网格 / 跟踪止盈本质都是「在一只**底仓**上、
用 T+1 可卖的存量股做买卖」→ **同一自动机的三种配置**。`run_tick` 已按 `avail_qty` 封顶卖出T+1 天然被挡住。
---
## 四、框架:策略层 = 一个新的「指令来源」(只做加法)
新增 `strategy_runner`,挂在**现有每分钟调度位**(与 `run_tick` / `scan_proposals` 并列)。每跳:载入 `ACTIVE`
策略 → 取该股 `day_snapshot` + 持仓(`avail_qty`/成本)→ 按类型 `evaluate()` 出决策 → 过**策略层 rails** →
触发就:`autonomy=auto` 发一张**短窗口指令**`window_tdays=1`、`is_command=True`、`origin_type="strategy"`
`run_tick` 用现有管线执行;`autonomy=confirm` 落一条提议进「等我拍板」。再更新 `state_json`
**复用全部闸门**(单股上限/一手/T+1/冻结/黑名单/暂停买入),一道不重写。**不碰动作引擎**,唯一加法是下一条 skip。
**动作引擎唯一触点**`action_engine.scan` 跳过「有 ACTIVE 策略」的票 —— 避免两个大脑抢同一只。命令驱动(用户手动清/减该股)不受影响。
---
## 五、数据模型(新增一张表,走 ddl 文件、非例行事务内嵌 DDL
`pms_strategy``strategy_id`(唯一) / `ts_code` / `type`(T0|GRID|TRAIL) / `status`(ACTIVE|PAUSED|DONE|CANCELLED) /
`autonomy`(auto|confirm**默认 auto**) / `params_json` / `state_json` / `created_at`/`updated_at`/`note`。
- `state_json`做T=`{t_count_today, open_leg, day_realized}`;网格=`{filled_levels[], below_floor}`;跟踪止盈=`{high_water, armed}`。
- `pms_instruction.origin_type` 增加取值 `"strategy"`≤8 字符,列宽兼容;注释同步)。
---
## 六、做T 规格(把已有架子接通)
- **触发**正T = 距当日高点回落 ≥ `PMS_T0_PULLBACK_PCT` 且近支撑 → 买 `t_qty`(=持仓×`t_ratio``≤ PMS_T0_RATIO_MAX`
即 1/3),按 `PMS_T0_ROUND_TARGET` 价差高卖反T = 近压力或日内涨 ≥ `PMS_T0_RALLY_PCT` 滞涨 → 卖 `avail``t_qty`,低买回。
- **rails****当日 ≤ 3 次**`t_count_today`**`PMS_T0_CLOSE_TIME`14:50强制平回**(当日买卖数量对齐、绝不过夜);
单票 `PMS_T0_STOCK_DAY_LOSS` / 全局 `PMS_T0_GLOBAL_DAY_LOSS` 当日T亏熔断后当日禁T须有底仓且 `avail_qty` 足。
---
## 七、网格 规格(新)
- **配置**:中枢价(手动或取当前价)、档距(% 或元)、上界/下界、每档股数或金额、最大投入额(须落在单股上限内)。
- **规则**:现价跌破某未买档 → 买 1 份(过单股上限/资金闸);现价涨破某已买档 → 卖 1 份(从 `avail`T+1 挡);越上界停做。
- **跌破下界(已定)****继续持有、不再买** —— 停掉买入腿,保留已买的档位,若价格回升到已买档之上仍按规则卖出;
并在页面/账本告警「跌破下界,已停止网格买入」。不清仓、不继续往下接。
- **state**`filled_levels` + `below_floor` 标记。不强制平回(多日底仓策略);今天买的今天卖不了(管线挡)。
- **安全**:最大投入额 = 硬上限;单股上限闸兜底。
---
## 七·B、跟踪止盈 规格v1 纳入)
- **用途**:锁住已有浮盈 —— 价格创新高就抬止盈线,从高点回落到设定比例就卖出离场。**只卖不买**(纯离场保护)。
- **配置**:启动线(浮盈 ≥ X% 才开始跟踪,默认取 `PMS_CUSHION_SOLID` 一线)、回撤比例 giveback从高点回落 ≥ Y% 触发卖)、
卖出比例(触发时卖 `avail` 的百分之多少,默认全清)、可选硬止盈目标(浮盈 ≥ Z% 直接全清)。
- **state**`high_water`(启动后的最高价)、`armed`。
- **规则**:每跳更新 `high_water=max(high_water, 现价)`;浮盈达启动线则 `armed=true`;已武装且现价 ≤ `high_water×(1giveback)`
→ 卖出(从 `avail`T+1。命中硬止盈目标直接全清。
- **与既有「保垫减仓」的关系**:挂了跟踪止盈的票已从动作引擎排除,不会和自动保垫减仓双触发;这是该股的用户版止盈。
---
## 八、补充策略同一自动机v1 之后、留接口)
`evaluate()` 加一个 `type` 分支即可:**分批建仓/定投**、**区间高抛低吸**(两档简化网格)、**突破加仓**。
---
## 九、隔离与安全(重点:动的是下单口)
1. **逐股显式挂载**opt-in绝不自动套用
2. 挂策略的票**从动作引擎自动提议排除**
3. 策略动作走**命令口径**(过规则闸、不过研判闸);
4. **每策略自主档**:默认 `auto`(直接自动执行,你的拍板);可逐策略改 `confirm` 进「等我拍板」观察;
5. **双保险**rails 在策略层先拦(熔断/上限/平回/当日次数/下界),规则闸再拦一道(合规);
6. **开发期先 shadow 后实盘**`PMS_DISPATCH_MODE=shadow` 下先验证策略只发到影子出口、不碰真 QMT判收通过再切 live
—— 这是验**新代码**下单对不对,不是给你的策略设门槛;
7. 全局开关对策略同样生效:暂停买入 → 策略买入被闸拦;休假 → 策略不出手;一键清仓照常。
---
## 十、UI交易员视图只做加法
- 「我的持仓」每行加**挂交易方案**选类型做T/网格/跟踪止盈)→ 填该类型参数(带当前价/持仓量参照,不盲填)→
选自主档(默认自动,可改待确认);
- 显示策略**实时状态**做T「今日 2/3 次,日内 +¥320平回 ✓」网格「6 档已买 3投入 ¥180k/上限 ¥240k」
跟踪止盈「高点 ¥18.6,回落 3.1%,未触发」;可**暂停/恢复/撤下**
- 策略动作在「今日在办与动向」里与普通指令一起看,标来源 = 策略。
---
## 十一、分步落地每步你实机验shadow 先行)
1. 建表 `pms_strategy` + `strategy_runner` 骨架 + `origin_type=strategy` + 动作引擎 skip先不含任何策略规则空跑通
2. **做T** `evaluate` + rails(含 3 次/平回/熔断) → shadow 验「一笔完整平回」;
3. **网格** `evaluate` + rails(含下界=继续持有不再买) → shadow 验「一档买 + 一档卖」;
4. **跟踪止盈** `evaluate` + rails → shadow 验「回落触发一笔卖出」;
5. UI 挂载/状态/暂停;
6. 三种各切实盘观察 → 稳定。
---
## 十二、已定(本轮四点,无遗留待决)
① 网格跌破下界 = 继续持有、不再买;② 做T 当日 ≤ 3 次;③ 新挂策略默认自主档 = 自动执行;④ 跟踪止盈纳入 v1。
开工顺序按第十一节;每步 shadow 先行、单测你实机跑。

View File

@ -224,7 +224,8 @@ def skip_why(skip, key):
# ================================================================ 扫描入口
def scan(*, positions: list, params: dict, market: dict, skip=None) -> dict:
def scan(*, positions: list, params: dict, market: dict, skip=None,
strategy_codes=None) -> dict:
"""扫描全部持仓, 产出候选动作。
positions: portfolio.positions_view()["held"] 的口径
@ -233,11 +234,17 @@ def scan(*, positions: list, params: dict, market: dict, skip=None) -> dict:
返回 {"candidates": [...], "skipped": [...]}
"""
skip = skip or {}
strategy_codes = strategy_codes or set()
out, skipped = [], []
for p in positions or []:
code = p.get("ts_code")
if not code or int(p.get("total_qty") or 0) <= 0:
continue
# 挂了 ACTIVE 交易方案(策略)的票由策略层接管, 动作引擎不自动提议 (设计 §四)
if code in strategy_codes:
skipped.append({"ts_code": code, "action": "*",
"why": "挂了交易方案(策略), 由策略层接管, 动作引擎不自动提议"})
continue
# 取不到现价的票整只跳过, **并且留痕**。上游 (portfolio.positions_view) 在拿不到
# 行情时会用摊薄成本顶住 price 让市值还能算, 但那个价不是行情 —— 拿它评动作会得出
# 「安全垫恰好 0」「现价恰好等于成本」这类看着正常、实则凭空的结论。

View File

@ -730,3 +730,76 @@ def _snapshot_row(r: dict) -> dict:
out["capped_main"] = bool(out.get("capped_main"))
out["capped_obs"] = bool(out.get("capped_obs"))
return out
# ================================================================ pms_strategy (个股交易方案)
# 只允许更新的列白名单 (与 DDL 一致; `type` 只在建时设, 不入更新集, 避开保留字与误改)
STRATEGY_COLS = {"status", "autonomy", "params_json", "state_json", "note"}
def list_strategies(*, ts_code=None, statuses=None, limit: int = 500) -> list:
where, p = [], {"n": int(limit)}
if ts_code:
where.append("ts_code = :code")
p["code"] = ts_code
if statuses:
keys = []
for i, s in enumerate(statuses):
keys.append(f":st{i}")
p[f"st{i}"] = s
where.append(f"status IN ({', '.join(keys)})")
sql = "SELECT * FROM pms_strategy"
if where:
sql += " WHERE " + " AND ".join(where)
sql += " ORDER BY id DESC LIMIT :n"
rows = fetch_all(sql, p)
for r in rows:
r["params"] = _loads(r.get("params_json"), {})
r["state"] = _loads(r.get("state_json"), {})
return rows
def get_strategy(strategy_id: str):
r = fetch_one("SELECT * FROM pms_strategy WHERE strategy_id = :sid", {"sid": strategy_id})
if r:
r["params"] = _loads(r.get("params_json"), {})
r["state"] = _loads(r.get("state_json"), {})
return r
def active_strategies() -> list:
return list_strategies(statuses=["ACTIVE"], limit=1000)
def active_strategy_codes() -> set:
"""有 ACTIVE 策略的 ts_code 集合 —— 动作引擎据此排除这些票。"""
rows = fetch_all("SELECT DISTINCT ts_code FROM pms_strategy WHERE status = 'ACTIVE'")
return {r["ts_code"] for r in rows if r.get("ts_code")}
def insert_strategy(*, strategy_id, ts_code, stype, autonomy="auto", params=None,
state=None, status="ACTIVE", note=None) -> int:
now = _NOW()
return execute(
"INSERT INTO pms_strategy (strategy_id, ts_code, `type`, status, autonomy, "
"params_json, state_json, note, created_at, updated_at) "
"VALUES (:sid, :code, :type, :status, :auto, :params, :state, :note, :ts, :ts)",
{"sid": strategy_id, "code": ts_code, "type": stype, "status": status,
"auto": autonomy, "params": _dumps(params or {}), "state": _dumps(state or {}),
"note": note, "ts": now})
def update_strategy(strategy_id: str, **fields) -> int:
"""更新策略。params/state 传 dict 会自动 json 化到 *_json 列。"""
upd = dict(fields)
if "params" in upd:
upd["params_json"] = _dumps(upd.pop("params"))
if "state" in upd:
upd["state_json"] = _dumps(upd.pop("state"))
clause, p = _set_clause(upd, STRATEGY_COLS)
if not clause:
return 0
p["sid"] = strategy_id
p["ts"] = _NOW()
return execute(
f"UPDATE pms_strategy SET {clause}, updated_at = :ts WHERE strategy_id = :sid", p)

View File

@ -153,9 +153,10 @@ def intraday_exec():
含自主提议扫描 (FILL/ADD/DCA/TRIM 动作引擎 规则闸 研判闸 按档位分流)
"""
from app.services import executor, proposal_service
from app.services import executor, proposal_service, strategy_runner
r = {"materialized": executor.materialize_plans()}
r["proposals"] = proposal_service.scan_and_route()
r["strategies"] = strategy_runner.tick() # 个股交易方案: 触发即发短窗口指令, 由下面 run_tick 执行
r.update(executor.run_tick())
return r

View File

@ -136,6 +136,7 @@ DESC = {
"PMS_T0_RALLY_PCT": "反T: 日内涨幅触发", "PMS_T0_ROUND_TARGET": "单次T目标价差",
"PMS_T0_CLOSE_TIME": "T仓强制平回时点", "PMS_T0_STOCK_DAY_LOSS": "单票当日T亏熔断",
"PMS_T0_GLOBAL_DAY_LOSS": "全局当日T亏熔断",
"PMS_STRATEGY_ENABLED": "个股交易方案(策略)层总开关 (关=strategy_runner 空转)",
"PMS_REPLAY_INTERVAL_MIN": "成交回放间隔 (分钟)", "PMS_RECON_ALARM_DAYS": "连续不一致升级天数",
"PMS_SIGNAL_ENABLED": "是否消化决策系统盘中信号",
"PMS_SIGNAL_GROUP": "信号消费组名 (独立于 trading_service, 互不抢消息)",

View File

@ -76,7 +76,14 @@ def scan_and_route(*, now=None, dry_run: bool = False) -> dict:
# skipped 里长得一模一样, 看的人判断不了这只票明天还会不会再被评估。
# 一只票既有在途指令又今天被拒过时, 显示"有在途"更贴近它此刻的实际状态。
skip = {**_judge_rejected_open_keys(), **_rejected_today_keys(), **_inflight_keys()}
scanned = ae.scan(positions=view["held"], params=params, market=mkt, skip=skip)
# 挂了 ACTIVE 交易方案(策略)的票交策略层接管, 动作引擎不再对它自动提议
# (读库失败按空集 —— 宁可这轮不排除, 也不能因读不到把全体持仓都排除)
try:
strategy_codes = pms_repo.active_strategy_codes()
except Exception:
strategy_codes = set()
scanned = ae.scan(positions=view["held"], params=params, market=mkt, skip=skip,
strategy_codes=strategy_codes)
except Exception as e:
logger.exception("提议扫描失败")
return {**out, "ok": False, "errors": [f"扫描失败: {type(e).__name__}: {e}"]}

View File

@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
"""
个股交易方案 (策略) 运行器 PER_STOCK_STRATEGY_PLAN.md §
============================================================
每分钟一跳 (挂在 scheduler.intraday_exec , run_tick 并列)载入 ACTIVE 策略, 按类型
评估, 触发就**发一张短窗口指令** (window_tdays=1, is_command=True, origin_type='strategy'),
executor.run_tick 用现有管线执行 (择时 / 规则闸 / T+1 / 下发 / 账本 一道不重写);
autonomy=confirm 的落一条提议进等我拍板
安全 (设计 §):
* 全局开关 PMS_STRATEGY_ENABLED (默认 False) 关着时本模块整体空转
* 挂了 ACTIVE 策略的票由 action_engine.scan 排除 ( active_codes)
* 策略动作走命令口径 (过规则闸不过研判闸)
* scheduler @guard(session=True) 兜住: 非交易时段 / 休假模式不跑
**本文件是第 1 步骨架**: 三个评估器 (做T / 网格 / 跟踪止盈) 为占位, 一律返回 None, 全链空跑通;
规则在第 2~4 步按设计 § / § / §七B 接入, 接入点在 tick() 里已注明
"""
from __future__ import annotations
import logging
from app.repo import pms_repo
from app.services import param_store
logger = logging.getLogger("pms.strategy")
# 评估器签名 (第 2~4 步接入): fn(st, pos, day, now) ->
# None 或 {"side": "buy|sell", "qty": int, "limit": float, "reason": str,
# "leg": str, "state_patch": dict}
def _eval_t0(st, pos, day, now):
return None # 第 2 步: 做T 正T/反T + 3 次/日 + 14:50 平回 + T亏熔断 (设计 §六)
def _eval_grid(st, pos, day, now):
return None # 第 3 步: 网格 高抛低吸 + 下界=继续持有不再买 (设计 §七)
def _eval_trail(st, pos, day, now):
return None # 第 4 步: 跟踪止盈 高水位回撤触发卖出 (设计 §七B)
EVALUATORS = {"T0": _eval_t0, "GRID": _eval_grid, "TRAIL": _eval_trail}
def active_codes() -> set:
"""有 ACTIVE 策略的 ts_code —— 供 action_engine 排除。读库失败按空集 (不误排除)。"""
try:
return pms_repo.active_strategy_codes()
except Exception as e: # noqa: BLE001 —— 读不到不能把全体持仓都从动作引擎排除
logger.warning("[strategy] 读取 ACTIVE 策略集失败 (按空集): %s", e)
return set()
def tick(*, now=None, dry_run: bool = False) -> dict:
"""盘中每分钟一跳。
1 步只做载入与分发: 评估器占位返回 None, 不取行情不发指令不落库 全链空跑通
2~4 步在下面标注的接入点补: 取持仓行与当日行情 评估 rails 发短窗口指令或落提议 更新 state
"""
out = {"enabled": False, "checked": 0, "fired": [], "queued": [], "skipped": [], "errors": []}
if not param_store.get_bool("PMS_STRATEGY_ENABLED", False):
out["skipped"].append("PMS_STRATEGY_ENABLED=False, 策略层整体停用")
return out
out["enabled"] = True
try:
strategies = pms_repo.active_strategies()
except Exception as e: # noqa: BLE001
logger.exception("[strategy] 载入 ACTIVE 策略失败")
return {**out, "errors": [f"载入失败: {type(e).__name__}: {e}"]}
for st in strategies:
out["checked"] += 1
fn = EVALUATORS.get(st.get("type"))
if not fn:
out["skipped"].append({"strategy_id": st.get("strategy_id"),
"why": f"未知策略类型 {st.get('type')}"})
continue
# —— 第 2~4 步接入点 ——
# pos = portfolio.positions_view() 里该股的行 (或 pms_repo.get_position)
# day = market.day_snapshot(st["ts_code"])
# decision = fn(st, pos, day, now); 过策略层 rails (熔断/上限/平回/次数/下界)
# auto: _emit_instruction(st, decision) 发短窗口指令走 run_tick
# confirm: _emit_proposal(st, decision) 落一条提议进「等我拍板」
# 最后 pms_repo.update_strategy(st["strategy_id"], state=<新 state>)
try:
decision = fn(st, None, None, now)
except Exception as e: # noqa: BLE001 —— 单策略异常不拖垮整轮
logger.exception("[strategy] 评估失败 %s", st.get("strategy_id"))
out["errors"].append(f"{st.get('strategy_id')}: {type(e).__name__}: {e}")
continue
if decision is None:
continue
# 骨架阶段评估器不会返回非 None; 真返回了说明有人提前接了规则却没接下发 —— 明着记一条, 不静默下单
out["skipped"].append({"strategy_id": st.get("strategy_id"),
"why": "评估器已产出决策, 但下发/落提议在第 2~4 步接入 (骨架阶段不下单)"})
out["ok"] = not out["errors"]
return out
def force_t0_close(*, now=None) -> dict:
"""14:50 做T 强制平回 (设计 §六 rails) —— 第 2 步接入。骨架阶段: 自证无 T 仓残留占位。"""
return {"phase": "骨架", "note": "做T平回在第 2 步接入 (scheduler.t0_close 已留调度位)"}

View File

@ -225,6 +225,7 @@ class Settings(BaseSettings):
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_STRATEGY_ENABLED: bool = False # 个股交易方案(策略)层总开关; False=strategy_runner 空转不评估
# --- 决策系统信号消化 (设计 §10) ---
PMS_SIGNAL_ENABLED: bool = True

View File

@ -333,3 +333,25 @@ CREATE TABLE IF NOT EXISTS pms_plan_snapshot (
KEY idx_date_id (plan_date, id),
KEY idx_fetched (fetched_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='上游计划名册快照 (榜单变化比对的底本)';
-- 16. 个股交易方案 (策略: 做T / 网格 / 跟踪止盈) —— 见 PER_STOCK_STRATEGY_PLAN.md
-- 逐股显式挂载; strategy_runner 每分钟评估 ACTIVE 策略, 触发就发短窗口指令 (origin_type=strategy)
-- 走现有执行管线。挂了 ACTIVE 策略的票由动作引擎排除 (action_engine.scan 的 strategy_codes)。
-- 注: pms_instruction.origin_type 增加取值 'strategy' (5 号表列宽 VARCHAR(8) 兼容, 无需 ALTER,
-- 该列注释里的 'plan/proposal/system' 口径同步扩为含 'strategy', 仅口径、不改结构)。
CREATE TABLE IF NOT EXISTS pms_strategy (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
strategy_id VARCHAR(64) NOT NULL UNIQUE COMMENT '幂等键: STR_{ymd}_{code}_{seq}',
ts_code VARCHAR(16) NOT NULL,
`type` VARCHAR(16) NOT NULL COMMENT 'T0(做T) / GRID(网格) / TRAIL(跟踪止盈)',
status VARCHAR(12) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE/PAUSED/DONE/CANCELLED',
autonomy VARCHAR(8) NOT NULL DEFAULT 'auto' COMMENT 'auto(直接执行) / confirm(进等我拍板)',
params_json TEXT NOT NULL COMMENT '各类型配置 (档距/触发/上限/回撤等)',
state_json TEXT NULL COMMENT '运行态 (网格已买档 / 做T今日次数 / 跟踪高水位)',
note VARCHAR(300) NULL,
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='个股交易方案 (策略)';