96b1924fd5
[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 起服务联调
118 lines
4.1 KiB
Python
118 lines
4.1 KiB
Python
"""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"]
|