# -*- 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 已留调度位)"}