"""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"]