From 18e7c88d162157a6c96ca7c55657bfb0dc2c9a8d Mon Sep 17 00:00:00 2001 From: zlt Date: Tue, 11 Aug 2026 10:35:03 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=B0=E5=AF=8C=E4=BA=A4=E6=98=93=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PER_STOCK_STRATEGY_PLAN.md | 146 +++++++++++++++++++++++++++++++ app/core/action_engine.py | 9 +- app/repo/pms_repo.py | 73 ++++++++++++++++ app/scheduler.py | 3 +- app/services/param_store.py | 1 + app/services/proposal_service.py | 9 +- app/services/strategy_runner.py | 105 ++++++++++++++++++++++ config/settings.py | 1 + ddl_pms_v1.sql | 22 +++++ 9 files changed, 366 insertions(+), 3 deletions(-) create mode 100644 PER_STOCK_STRATEGY_PLAN.md create mode 100644 app/services/strategy_runner.py diff --git a/PER_STOCK_STRATEGY_PLAN.md b/PER_STOCK_STRATEGY_PLAN.md new file mode 100644 index 0000000..b14a0bc --- /dev/null +++ b/PER_STOCK_STRATEGY_PLAN.md @@ -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×(1−giveback)` + → 卖出(从 `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 先行、单测你实机跑。 diff --git a/app/core/action_engine.py b/app/core/action_engine.py index 7d5ad52..1adcbb8 100644 --- a/app/core/action_engine.py +++ b/app/core/action_engine.py @@ -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」「现价恰好等于成本」这类看着正常、实则凭空的结论。 diff --git a/app/repo/pms_repo.py b/app/repo/pms_repo.py index e606a6f..c5f5186 100644 --- a/app/repo/pms_repo.py +++ b/app/repo/pms_repo.py @@ -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) diff --git a/app/scheduler.py b/app/scheduler.py index d5b39f9..76264dc 100644 --- a/app/scheduler.py +++ b/app/scheduler.py @@ -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 diff --git a/app/services/param_store.py b/app/services/param_store.py index 466826f..42f7bdc 100644 --- a/app/services/param_store.py +++ b/app/services/param_store.py @@ -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, 互不抢消息)", diff --git a/app/services/proposal_service.py b/app/services/proposal_service.py index 06d6b49..774cac4 100644 --- a/app/services/proposal_service.py +++ b/app/services/proposal_service.py @@ -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}"]} diff --git a/app/services/strategy_runner.py b/app/services/strategy_runner.py new file mode 100644 index 0000000..afb9467 --- /dev/null +++ b/app/services/strategy_runner.py @@ -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 已留调度位)"} diff --git a/config/settings.py b/config/settings.py index 7a1e640..267b60f 100644 --- a/config/settings.py +++ b/config/settings.py @@ -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 diff --git a/ddl_pms_v1.sql b/ddl_pms_v1.sql index c09a110..1fee208 100644 --- a/ddl_pms_v1.sql +++ b/ddl_pms_v1.sql @@ -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='个股交易方案 (策略)';