丰富交易逻辑
This commit is contained in:
parent
46f1a09a72
commit
18e7c88d16
|
|
@ -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 先行、单测你实机跑。
|
||||
|
|
@ -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」「现价恰好等于成本」这类看着正常、实则凭空的结论。
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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, 互不抢消息)",
|
||||
|
|
|
|||
|
|
@ -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}"]}
|
||||
|
|
|
|||
|
|
@ -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 已留调度位)"}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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='个股交易方案 (策略)';
|
||||
|
|
|
|||
Loading…
Reference in New Issue