Files
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

119 lines
4.2 KiB
Python

"""A 股 15min 双均线策略(AShareDoubleMaStrategy)。
为什么不直接用 ``vnpy_ctastrategy.strategies.DoubleMaStrategy``:
- 标准 DoubleMa 在 ``on_init`` 里 ``BarGenerator(self.on_bar)`` —— 无 ``window`` 参数,
只合成 1min bar;且其 ``parameters = ["fast_window", "slow_window"]`` 不暴露周期配置,
**无法通过 setting 切到 15min**。
- 本策略改用 ``BarGenerator(self._on_1min_bar, window=15, on_window_bar=self.on_bar)``,
让 ``on_bar`` 直接收 15min bar,通过 ``window`` 参数(默认 15)可配。
- 定寸 + 禁做空由 :class:`AShareCtaTemplate` 保证。
- A 股不能做空,死叉时只平多,不开空(原 DoubleMa 的 ``pos<0 → cover+buy`` 分支删除)。
"""
from __future__ import annotations
import logging
logger = logging.getLogger(__name__)
try:
from vnpy.trader.utility import BarGenerator, ArrayManager # type: ignore
from vnpy.trader.constant import Interval # type: ignore
from vnpy.trader.object import BarData, TickData # type: ignore
_DEPS_OK = True
except ImportError: # 本机 dev 容错
BarGenerator = None # type: ignore
ArrayManager = None # type: ignore
Interval = None # type: ignore
BarData = None # type: ignore
TickData = None # type: ignore
_DEPS_OK = False
from sanguo_live.base_template import AShareCtaTemplate
class AShareDoubleMaStrategy(AShareCtaTemplate):
"""A 股 双均线 15min 策略。
金叉(fast 上穿 slow)且无持仓 → 买开 1 手;
死叉(fast 下穿 slow)且持多 → 卖平 1 手;
A 股禁做空 → 不开空单(基类 short 已拦截)。
"""
author = "sanguo_live"
fast_window: int = 10
slow_window: int = 20
# BarGenerator 分钟窗口。15 表示 15min。A 股支持 2/3/5/6/10/15/20/30(必须能整除 60)。
window: int = 15
# 把基类的 size/forbid_short 一起暴露,update_setting 才会回填全部字段。
parameters = ["fast_window", "slow_window", "window", "size", "forbid_short"]
variables = ["fast_ma0", "fast_ma1", "slow_ma0", "slow_ma1"]
fast_ma0: float = 0.0
fast_ma1: float = 0.0
slow_ma0: float = 0.0
slow_ma1: float = 0.0
def on_init(self) -> None:
self.write_log("AShareDoubleMa 策略初始化")
# 关键:用 BarGenerator 把 1min bar 合成 window 分钟 bar 后回调 self.on_bar。
# _on_1min_bar 故意空实现 —— 我们不处理 1min,只让它喂养 BarGenerator。
self.bg: BarGenerator = BarGenerator(
self._on_1min_bar,
window=self.window,
on_window_bar=self.on_bar,
)
self.am: ArrayManager = ArrayManager()
self.load_bar(10)
def on_start(self) -> None:
self.write_log("AShareDoubleMa 策略启动")
self.put_event()
def on_stop(self) -> None:
self.write_log("AShareDoubleMa 策略停止")
self.put_event()
def on_tick(self, tick: TickData) -> None:
"""tick 推入 BarGenerator,由其合成 1min 与 window 分钟 bar。"""
self.bg.update_tick(tick)
def _on_1min_bar(self, bar: BarData) -> None:
"""1min bar 回调:故意忽略(由 BarGenerator 内部累积合成 15min)。"""
return
def on_bar(self, bar: BarData) -> None:
"""15min(或 setting 配置的 window)bar 回调。"""
self.cancel_all()
am: ArrayManager = self.am
am.update_bar(bar)
if not am.inited:
return
fast_ma = am.sma(self.fast_window, array=True)
self.fast_ma0 = fast_ma[-1]
self.fast_ma1 = fast_ma[-2]
slow_ma = am.sma(self.slow_window, array=True)
self.slow_ma0 = slow_ma[-1]
self.slow_ma1 = slow_ma[-2]
cross_over = self.fast_ma0 > self.slow_ma0 and self.fast_ma1 < self.slow_ma1
cross_below = self.fast_ma0 < self.slow_ma0 and self.fast_ma1 > self.slow_ma1
if cross_over:
if self.pos == 0:
self.buy(bar.close_price, 1)
# A 股不可做空:原 DoubleMa 的 pos<0 分支(cover+buy)省略
elif cross_below:
if self.pos > 0:
self.sell(bar.close_price, 1)
# A 股不可做空:原 DoubleMa 的 pos==0 short 分支省略
self.put_event()
__all__ = ["AShareDoubleMaStrategy"]