tradingSystem/app/services/strategy_runner.py

106 lines
5.1 KiB
Python
Raw Normal View History

2026-08-11 10:35:03 +08:00
# -*- 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 已留调度位)"}