Files
sanguo_vnpy_v2/sanguo_live/base_template.py
T
claude_dev 96b1924fd5 feat: 实盘模拟(live) + 组合回测MVP(portfolio)
[live] 实盘模拟 vnpy+miniQMT 直连(supervisor 轮询, 前后端):
- sanguo_live: LiveTradingEngine + AShareCtaTemplate(定寸/禁做空) + runner_supervisor(DB驱动) + persistence(4表WAL)
- sanguo_api/routes_live: 9路由(create/start/stop/positions/trades/account/status)
- frontend live: New/List/Monitor + api/live.ts; config/live.yaml

[portfolio] 组合回测 MVP(BulletTrade, 链路代码完成待验证):
- runner_backtest 加 JSON 入口(--json, BacktestEngine 顶层 import)
- sanguo_api/routes_portfolio: POST /portfolio/backtest SSH 触发 VPS 跑
- frontend PortfolioBacktest.vue + api/portfolio.ts: 表单+结果+净值曲线
- 路由/菜单注册(/backtest/portfolio 组合回测)
- 已知: MVP 链路未端到端验证, agent 改至中途被停; 待 Mac 起服务联调
2026-07-18 20:04:16 +08:00

118 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""A股 CTA 策略基类:定寸(1 手 = 100 股)+ 禁做空。
为什么需要这一层:
- vnpy_qmt 的 send_order 把 ``volume`` 当**股数**直接传给 xtquant,不会自动乘合约 size。
标准 CtaTemplate 策略写 ``buy(price, 1)`` 表示 1 手 → 实际只下 1 股,会被券商拒单
或当成废单。AShareCtaTemplate 在 buy/sell/cover 内部把 volume ×= ``self.size``
(默认 100),让策略保留""的语义。
- A 股不可做空:``short`` 直接返回 [] 并写日志(``forbid_short=True`` 生效时)。
依赖 vnpy_ctastrategy.CtaTemplate。本机未装时 CtaTemplate fallback 为 ``object``,
模块 import 不崩,只有运行时实例化或调用方法才会报错。
"""
from __future__ import annotations
import logging
from typing import Any
logger = logging.getLogger(__name__)
try:
from vnpy_ctastrategy import CtaTemplate # type: ignore
_HAS_CTA_BASE = True
_IMPORT_ERROR: Exception | None = None
except ImportError as _e: # 本机 dev 环境常未装 vnpy_ctastrategy
CtaTemplate = object # type: ignore[assignment,misc]
_HAS_CTA_BASE = False
_IMPORT_ERROR = _e
class AShareCtaTemplate(CtaTemplate): # type: ignore[misc]
"""A 股 CTA 策略基类。
定寸:``buy`` / ``cover`` / ``sell`` 的 ``volume`` 自动 ``×= size``(默认 100,
即 1 手 = 100 股),不足 1 手向下取整并告警。
禁做空:``short`` 直接返回 [](由 ``forbid_short`` 控制,默认 True)。
子类化注意:必须把 ``size``/``forbid_short`` 加入自己的 ``parameters`` 列表,
否则 ``update_setting`` 不会回填这两个字段。
"""
# 1 手 = 100 股(A 股最小交易单位)。ContractData.size 也是 100。
size: int = 100
# True: short() 被拦截(A 股不能开空);False: 透传到基类(仅供测试/期货场景)。
forbid_short: bool = True
parameters = ["size", "forbid_short"]
def buy(
self,
price: float,
volume: float,
stop: bool = False,
lock: bool = False,
net: bool = False,
) -> list:
"""开多 → A 股买入,定寸到整手。"""
return super().buy(price, self._to_lots(volume), stop, lock, net)
def cover(
self,
price: float,
volume: float,
stop: bool = False,
lock: bool = False,
net: bool = False,
) -> list:
"""平空 → A 股 Normally 不会触达(禁做空);保留定寸以防策略逻辑误调。"""
return super().cover(price, self._to_lots(volume), stop, lock, net)
def sell(
self,
price: float,
volume: float,
stop: bool = False,
lock: bool = False,
net: bool = False,
) -> list:
"""平多 → A 股卖出持仓,定寸到整手。"""
return super().sell(price, self._to_lots(volume), stop, lock, net)
def short(
self,
price: float,
volume: float,
stop: bool = False,
lock: bool = False,
net: bool = False,
) -> list:
"""开空 → A 股不可做空,默认拦截。"""
if self.forbid_short:
self.write_log(
f"A股禁做空: 拦截 short price={price} volume={volume}"
)
return []
return super().short(price, self._to_lots(volume), stop, lock, net)
def _to_lots(self, volume: float) -> int:
"""策略手数 → A 股股数(``volume × size``,不足 1 手向下取整)。"""
target = volume * self.size
lots = int(target)
if lots != target:
logger.warning(
"%s 定寸出现零股: volume=%s size=%s%d (向下取整)",
getattr(self, "strategy_name", "?"), volume, self.size, lots,
)
return lots
def ensure_base_available() -> None:
"""显式检查 vnpy_ctastrategy 是否就绪。模块加载时容错,真正实盘前调一次。"""
if not _HAS_CTA_BASE:
raise RuntimeError(
f"vnpy_ctastrategy 未安装,无法实例化 AShareCtaTemplate: {_IMPORT_ERROR}"
)
__all__ = ["AShareCtaTemplate", "ensure_base_available"]