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 起服务联调
119 lines
4.2 KiB
Python
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"]
|