106 lines
5.1 KiB
Python
106 lines
5.1 KiB
Python
|
|
# -*- 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 已留调度位)"}
|