tradingSystem/app/services/strategy_service.py

137 lines
5.8 KiB
Python
Raw Normal View History

2026-08-11 11:58:16 +08:00
# -*- coding: utf-8 -*-
"""
个股交易方案 (策略) 管理侧: 校验 / 挂载 / 暂停撤下 PER_STOCK_STRATEGY_PLAN.md §/§
运行侧 (每分钟评估下单) strategy_runner.py
挂载前**先过约束校验** (validate): 违反仓位 / 存量 / 上限就挡下给明确中文原因**不写库**
落实理论上不允许违反, 无法操作要在页面提示真正下单时的合规由 rule_gate 再兜一道 (双保险)
校验不写库不抛异常; 读持仓失败按暂不能校验挡下 (不放行未校验的挂载)
"""
from __future__ import annotations
import logging
from datetime import datetime
from app.core import tradedays as td
from app.repo import pms_repo
from app.services import param_store, portfolio
logger = logging.getLogger("pms.strategy_svc")
TYPES = {"T0", "GRID", "TRAIL"}
AUTONOMY = {"auto", "confirm"}
STATUSES = {"ACTIVE", "PAUSED", "CANCELLED", "DONE"}
def _f(v, d=0.0):
try:
return float(v)
except (TypeError, ValueError):
return d
def _pos(view, code):
for x in view["positions"]:
if x["ts_code"] == code:
return x
return None
def validate(cfg: dict, view=None) -> dict:
"""校验一条策略配置。返回 {ok, reasons:[中文原因]}。不写库、不抛异常。"""
cfg = cfg or {}
reasons = []
code = cfg.get("ts_code")
typ = cfg.get("type")
autonomy = cfg.get("autonomy") or "auto"
params = cfg.get("params") or {}
if not code:
reasons.append("未指定股票")
if typ not in TYPES:
reasons.append(f"未知策略类型 {typ}(仅支持 做T=T0 / 网格=GRID / 跟踪止盈=TRAIL)")
if autonomy not in AUTONOMY:
reasons.append(f"自主档 {autonomy} 非法(仅 auto / confirm)")
if reasons:
return {"ok": False, "reasons": reasons}
try:
view = view or portfolio.positions_view()
except Exception as e: # noqa: BLE001 —— 读不到持仓不能放行未校验的挂载
return {"ok": False, "reasons": [f"读持仓失败, 暂不能校验(不放行): {type(e).__name__}: {e}"]}
pos = _pos(view, code)
if not pos or int(pos.get("total_qty") or 0) <= 0:
return {"ok": False, "reasons": [
"该股当前没有持仓 —— 交易方案要在已有底仓上做差价(A股 T+1, 当日买入不可当日卖)"]}
# 同股已有 ACTIVE 策略 → 不重复挂
try:
dup = pms_repo.list_strategies(ts_code=code, statuses=["ACTIVE"], limit=5)
except Exception: # noqa: BLE001
dup = []
if dup:
reasons.append(f"该股已挂着一条 {dup[0].get('type')} 策略(生效中), 先撤下再挂新的")
prm = view["params"]
scale = _f(prm.get("scale"))
stock_cap = _f(prm.get("stock_cap"))
mv = _f(pos.get("market_value"))
avail = int(pos.get("avail_qty") or 0)
cap_room = max(0.0, stock_cap * scale - mv) # 这只票离单股上限还差多少钱
if typ == "T0":
tr = _f(params.get("t_ratio"))
if not (0 < tr <= 0.3334):
reasons.append("做T 的 T 仓比例 t_ratio 必须在 0~1/3 之间(硬上限 1/3)")
if avail <= 0:
reasons.append("做T 需要 T+1 可卖的存量股(当前可卖为 0), 无法在存量上做差价")
elif typ == "GRID":
lo, hi = _f(params.get("lower")), _f(params.get("upper"))
mid = _f(params.get("center")) or _f(pos.get("price"))
step = _f(params.get("step"))
max_cap = _f(params.get("max_capital"))
if not (0 < lo < mid < hi):
reasons.append("网格上下界不成立: 需 0 < 下界 < 中枢 < 上界")
if step <= 0 and _f(params.get("step_pct")) <= 0:
reasons.append("网格档距必须 > 0 (绝对档距 step 或百分比档距 step_pct 至少给一个)")
if max_cap <= 0:
reasons.append("网格最大投入额必须 > 0")
elif max_cap > cap_room + 1e-6:
reasons.append(
f"网格最大投入 {max_cap:,.0f} 元超过该股离单股上限的余量 {cap_room:,.0f}"
f"(单股上限 {stock_cap:.0%}×规模 {scale:,.0f}, 已占市值 {mv:,.0f})")
elif typ == "TRAIL":
gb = _f(params.get("giveback"))
if not (0 < gb < 1):
reasons.append("跟踪止盈的回撤比例 giveback 必须在 0~1 之间(如 0.05=从高点回落 5% 就卖)")
if avail <= 0:
reasons.append("跟踪止盈触发时要卖出, 但当前 T+1 可卖为 0")
return {"ok": not reasons, "reasons": reasons}
def attach(cfg: dict, by: str = "user") -> dict:
"""挂载一条策略。先校验, 违反返回 {ok:false, errors}; 通过则写 pms_strategy(ACTIVE)。"""
v = validate(cfg)
if not v["ok"]:
return {"ok": False, "errors": v["reasons"]}
code = cfg["ts_code"]
ymd = td.ymd()
sid = f"STR_{ymd}_{code.replace('.', '')}_{int(datetime.now().timestamp()) % 1000000}"
pms_repo.insert_strategy(strategy_id=sid, ts_code=code, stype=cfg["type"],
autonomy=cfg.get("autonomy") or "auto",
params=cfg.get("params") or {}, state={},
status="ACTIVE", note=cfg.get("note"))
return {"ok": True, "strategy_id": sid, "ts_code": code}
def set_status(strategy_id: str, status: str, by: str = "user") -> dict:
"""暂停(PAUSED) / 恢复(ACTIVE) / 撤下(CANCELLED)。"""
status = (status or "").upper()
if status not in STATUSES:
return {"ok": False, "error": f"非法状态 {status}"}
st = pms_repo.get_strategy(strategy_id)
if not st:
return {"ok": False, "error": "策略不存在"}
pms_repo.update_strategy(strategy_id, status=status)
return {"ok": True, "strategy_id": strategy_id, "ts_code": st.get("ts_code"), "status": status}