feat(strategy): 策略库灌入vnpy内置8策略模板(DoubleMa/AtrRsi/BollChannel/DualThrust/KingKeltner/MultiSignal/MultiTimeframe/TurtleSignal); registry自研目录优先加载(编辑副本即刻生效,pip兜底); 分类器识别vnpy_ctastrategy import风格 [vps]
This commit is contained in:
@@ -59,8 +59,7 @@ def list_strategies() -> list[dict]:
|
||||
|
||||
def strategy_params(name: str) -> dict:
|
||||
"""Return {parameters: [...], defaults: {...}} for a strategy's dynamic form."""
|
||||
classes = _load_strategy_classes()
|
||||
cls = classes.get(name)
|
||||
cls = _load_self_owned_module_classes().get(name) or _load_strategy_classes().get(name)
|
||||
if cls is None:
|
||||
return {"parameters": [], "defaults": {}}
|
||||
params = list(getattr(cls, "parameters", []))
|
||||
@@ -68,8 +67,37 @@ def strategy_params(name: str) -> dict:
|
||||
return {"parameters": params, "defaults": defaults}
|
||||
|
||||
|
||||
def _load_self_owned_module_classes() -> dict[str, type]:
|
||||
"""从自研 CTA 目录(sanguo_trader/strategy)动态 import 策略类。
|
||||
|
||||
让「复制进来的 vnpy 内置模板 + 在线编辑的自研策略」真正可被回测加载,
|
||||
且优先于 pip 包同名类(编辑副本即刻生效)。import 失败(如本机 dev 无
|
||||
vnpy_ctastrategy)静默降级到 pip 包。
|
||||
"""
|
||||
classes: dict[str, type] = {}
|
||||
pkg = SELF_OWNED_DIRS[0].replace("/", ".")
|
||||
for f in _scan_self_owned():
|
||||
if f["type"] != "cta" or f["dir"] != SELF_OWNED_DIRS[0] + "/":
|
||||
continue
|
||||
stem = f["name"][:-3]
|
||||
try:
|
||||
mod = importlib.import_module(f"{pkg}.{stem}")
|
||||
cls = getattr(mod, f["class_name"], None)
|
||||
if isinstance(cls, type):
|
||||
classes[f["class_name"]] = cls
|
||||
except Exception as e:
|
||||
logger.debug("自研策略 %s 导入失败(降级 pip 包): %s", f["name"], e)
|
||||
return classes
|
||||
|
||||
|
||||
def get_strategy_class(name: str) -> type | None:
|
||||
"""Return the strategy class by name (None if unavailable)."""
|
||||
"""Return the strategy class by name (None if unavailable).
|
||||
|
||||
自研目录优先(编辑生效),pip 包兜底。
|
||||
"""
|
||||
cls = _load_self_owned_module_classes().get(name)
|
||||
if cls is not None:
|
||||
return cls
|
||||
return _load_strategy_classes().get(name)
|
||||
|
||||
|
||||
@@ -88,13 +116,18 @@ def _repo_root() -> str:
|
||||
|
||||
|
||||
def _classify_strategy_type(tree: ast.Module) -> str:
|
||||
"""AST 判定 portfolio vs cta:import 了 vnpy StrategyTemplate → cta,否则 portfolio。"""
|
||||
"""AST 判定 portfolio vs cta:
|
||||
- import 了 bullet_trade 风格 StrategyTemplate → cta
|
||||
- from vnpy_ctastrategy import ...(vnpy 内置/自研 CTA 文件风格)→ cta
|
||||
否则 portfolio。"""
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and (node.module or "").endswith("StrategyTemplate"):
|
||||
return "cta"
|
||||
if isinstance(node, ast.ImportFrom):
|
||||
mod = node.module or ""
|
||||
if mod.endswith("StrategyTemplate") or mod.endswith("vnpy_ctastrategy"):
|
||||
return "cta"
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if "StrategyTemplate" in alias.name:
|
||||
if "StrategyTemplate" in alias.name or "vnpy_ctastrategy" in alias.name:
|
||||
return "cta"
|
||||
return "portfolio"
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import numpy as np
|
||||
|
||||
from vnpy_ctastrategy import (
|
||||
CtaTemplate,
|
||||
StopOrder,
|
||||
TickData,
|
||||
BarData,
|
||||
TradeData,
|
||||
OrderData,
|
||||
BarGenerator,
|
||||
ArrayManager,
|
||||
)
|
||||
|
||||
|
||||
class AtrRsiStrategy(CtaTemplate):
|
||||
""""""
|
||||
|
||||
author = "用Python的交易员"
|
||||
|
||||
atr_length: int = 22
|
||||
atr_ma_length: int = 10
|
||||
rsi_length: int = 5
|
||||
rsi_entry: int = 16
|
||||
trailing_percent: float = 0.8
|
||||
fixed_size: int = 1
|
||||
|
||||
atr_value: float = 0
|
||||
atr_ma: float = 0
|
||||
rsi_value: float = 0
|
||||
rsi_buy: float = 0
|
||||
rsi_sell: float = 0
|
||||
intra_trade_high: float = 0
|
||||
intra_trade_low: float = 0
|
||||
|
||||
parameters = [
|
||||
"atr_length",
|
||||
"atr_ma_length",
|
||||
"rsi_length",
|
||||
"rsi_entry",
|
||||
"trailing_percent",
|
||||
"fixed_size"
|
||||
]
|
||||
variables = [
|
||||
"atr_value",
|
||||
"atr_ma",
|
||||
"rsi_value",
|
||||
"rsi_buy",
|
||||
"rsi_sell",
|
||||
"intra_trade_high",
|
||||
"intra_trade_low"
|
||||
]
|
||||
|
||||
def on_init(self) -> None:
|
||||
"""
|
||||
Callback when strategy is inited.
|
||||
"""
|
||||
self.write_log("策略初始化")
|
||||
|
||||
self.bg: BarGenerator = BarGenerator(self.on_bar)
|
||||
self.am: ArrayManager = ArrayManager()
|
||||
|
||||
self.rsi_buy = 50 + self.rsi_entry
|
||||
self.rsi_sell = 50 - self.rsi_entry
|
||||
|
||||
self.load_bar(10)
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
Callback when strategy is started.
|
||||
"""
|
||||
self.write_log("策略启动")
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""
|
||||
Callback when strategy is stopped.
|
||||
"""
|
||||
self.write_log("策略停止")
|
||||
|
||||
def on_tick(self, tick: TickData) -> None:
|
||||
"""
|
||||
Callback of new tick data update.
|
||||
"""
|
||||
self.bg.update_tick(tick)
|
||||
|
||||
def on_bar(self, bar: BarData) -> None:
|
||||
"""
|
||||
Callback of new bar data update.
|
||||
"""
|
||||
self.cancel_all()
|
||||
|
||||
am: ArrayManager = self.am
|
||||
am.update_bar(bar)
|
||||
if not am.inited:
|
||||
return
|
||||
|
||||
atr_array: np.ndarray = am.atr(self.atr_length, array=True)
|
||||
self.atr_value = atr_array[-1]
|
||||
self.atr_ma = atr_array[-self.atr_ma_length:].mean()
|
||||
self.rsi_value = am.rsi(self.rsi_length)
|
||||
|
||||
if self.pos == 0:
|
||||
self.intra_trade_high = bar.high_price
|
||||
self.intra_trade_low = bar.low_price
|
||||
|
||||
if self.atr_value > self.atr_ma:
|
||||
if self.rsi_value > self.rsi_buy:
|
||||
self.buy(bar.close_price + 5, self.fixed_size)
|
||||
elif self.rsi_value < self.rsi_sell:
|
||||
self.short(bar.close_price - 5, self.fixed_size)
|
||||
|
||||
elif self.pos > 0:
|
||||
self.intra_trade_high = max(self.intra_trade_high, bar.high_price)
|
||||
self.intra_trade_low = bar.low_price
|
||||
|
||||
long_stop: float = self.intra_trade_high * (1 - self.trailing_percent / 100)
|
||||
self.sell(long_stop, abs(self.pos), stop=True)
|
||||
|
||||
elif self.pos < 0:
|
||||
self.intra_trade_low = min(self.intra_trade_low, bar.low_price)
|
||||
self.intra_trade_high = bar.high_price
|
||||
|
||||
short_stop: float = self.intra_trade_low * (1 + self.trailing_percent / 100)
|
||||
self.cover(short_stop, abs(self.pos), stop=True)
|
||||
|
||||
self.put_event()
|
||||
|
||||
def on_order(self, order: OrderData) -> None:
|
||||
"""
|
||||
Callback of new order data update.
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_trade(self, trade: TradeData) -> None:
|
||||
"""
|
||||
Callback of new trade data update.
|
||||
"""
|
||||
self.put_event()
|
||||
|
||||
def on_stop_order(self, stop_order: StopOrder) -> None:
|
||||
"""
|
||||
Callback of stop order update.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,142 @@
|
||||
from vnpy_ctastrategy import (
|
||||
CtaTemplate,
|
||||
StopOrder,
|
||||
TickData,
|
||||
BarData,
|
||||
TradeData,
|
||||
OrderData,
|
||||
BarGenerator,
|
||||
ArrayManager,
|
||||
)
|
||||
|
||||
|
||||
class BollChannelStrategy(CtaTemplate):
|
||||
""""""
|
||||
|
||||
author = "用Python的交易员"
|
||||
|
||||
boll_window: int = 18
|
||||
boll_dev: float = 3.4
|
||||
cci_window: int = 10
|
||||
atr_window: int = 30
|
||||
sl_multiplier: float = 5.2
|
||||
fixed_size: int = 1
|
||||
|
||||
boll_up: float = 0
|
||||
boll_down: float = 0
|
||||
cci_value: float = 0
|
||||
atr_value: float = 0
|
||||
intra_trade_high: float = 0
|
||||
intra_trade_low: float = 0
|
||||
long_stop: float = 0
|
||||
short_stop: float = 0
|
||||
|
||||
parameters = [
|
||||
"boll_window",
|
||||
"boll_dev",
|
||||
"cci_window",
|
||||
"atr_window",
|
||||
"sl_multiplier",
|
||||
"fixed_size"
|
||||
]
|
||||
variables = [
|
||||
"boll_up",
|
||||
"boll_down",
|
||||
"cci_value",
|
||||
"atr_value",
|
||||
"intra_trade_high",
|
||||
"intra_trade_low",
|
||||
"long_stop",
|
||||
"short_stop"
|
||||
]
|
||||
|
||||
def on_init(self) -> None:
|
||||
"""
|
||||
Callback when strategy is inited.
|
||||
"""
|
||||
self.write_log("策略初始化")
|
||||
|
||||
self.bg = BarGenerator(self.on_bar, 15, self.on_15min_bar)
|
||||
self.am = ArrayManager()
|
||||
|
||||
self.load_bar(10)
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
Callback when strategy is started.
|
||||
"""
|
||||
self.write_log("策略启动")
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""
|
||||
Callback when strategy is stopped.
|
||||
"""
|
||||
self.write_log("策略停止")
|
||||
|
||||
def on_tick(self, tick: TickData) -> None:
|
||||
"""
|
||||
Callback of new tick data update.
|
||||
"""
|
||||
self.bg.update_tick(tick)
|
||||
|
||||
def on_bar(self, bar: BarData) -> None:
|
||||
"""
|
||||
Callback of new bar data update.
|
||||
"""
|
||||
self.bg.update_bar(bar)
|
||||
|
||||
def on_15min_bar(self, bar: BarData) -> None:
|
||||
""""""
|
||||
self.cancel_all()
|
||||
|
||||
am = self.am
|
||||
am.update_bar(bar)
|
||||
if not am.inited:
|
||||
return
|
||||
|
||||
self.boll_up, self.boll_down = am.boll(self.boll_window, self.boll_dev)
|
||||
self.cci_value = am.cci(self.cci_window)
|
||||
self.atr_value = am.atr(self.atr_window)
|
||||
|
||||
if self.pos == 0:
|
||||
self.intra_trade_high = bar.high_price
|
||||
self.intra_trade_low = bar.low_price
|
||||
|
||||
if self.cci_value > 0:
|
||||
self.buy(self.boll_up, self.fixed_size, True)
|
||||
elif self.cci_value < 0:
|
||||
self.short(self.boll_down, self.fixed_size, True)
|
||||
|
||||
elif self.pos > 0:
|
||||
self.intra_trade_high = max(self.intra_trade_high, bar.high_price)
|
||||
self.intra_trade_low = bar.low_price
|
||||
|
||||
self.long_stop = self.intra_trade_high - self.atr_value * self.sl_multiplier
|
||||
self.sell(self.long_stop, abs(self.pos), True)
|
||||
|
||||
elif self.pos < 0:
|
||||
self.intra_trade_high = bar.high_price
|
||||
self.intra_trade_low = min(self.intra_trade_low, bar.low_price)
|
||||
|
||||
self.short_stop = self.intra_trade_low + self.atr_value * self.sl_multiplier
|
||||
self.cover(self.short_stop, abs(self.pos), True)
|
||||
|
||||
self.put_event()
|
||||
|
||||
def on_order(self, order: OrderData) -> None:
|
||||
"""
|
||||
Callback of new order data update.
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_trade(self, trade: TradeData) -> None:
|
||||
"""
|
||||
Callback of new trade data update.
|
||||
"""
|
||||
self.put_event()
|
||||
|
||||
def on_stop_order(self, stop_order: StopOrder) -> None:
|
||||
"""
|
||||
Callback of stop order update.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,117 @@
|
||||
import numpy as np
|
||||
|
||||
from vnpy_ctastrategy import (
|
||||
CtaTemplate,
|
||||
StopOrder,
|
||||
TickData,
|
||||
BarData,
|
||||
TradeData,
|
||||
OrderData,
|
||||
BarGenerator,
|
||||
ArrayManager,
|
||||
)
|
||||
|
||||
|
||||
class DoubleMaStrategy(CtaTemplate):
|
||||
""""""
|
||||
|
||||
author = "用Python的交易员"
|
||||
|
||||
fast_window: int = 10
|
||||
slow_window: int = 20
|
||||
|
||||
fast_ma0: float = 0.0
|
||||
fast_ma1: float = 0.0
|
||||
slow_ma0: float = 0.0
|
||||
slow_ma1: float = 0.0
|
||||
|
||||
parameters = ["fast_window", "slow_window"]
|
||||
variables = ["fast_ma0", "fast_ma1", "slow_ma0", "slow_ma1"]
|
||||
|
||||
def on_init(self) -> None:
|
||||
"""
|
||||
Callback when strategy is inited.
|
||||
"""
|
||||
self.write_log("策略初始化")
|
||||
|
||||
self.bg: BarGenerator = BarGenerator(self.on_bar)
|
||||
self.am: ArrayManager = ArrayManager()
|
||||
|
||||
self.load_bar(10)
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
Callback when strategy is started.
|
||||
"""
|
||||
self.write_log("策略启动")
|
||||
self.put_event()
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""
|
||||
Callback when strategy is stopped.
|
||||
"""
|
||||
self.write_log("策略停止")
|
||||
|
||||
self.put_event()
|
||||
|
||||
def on_tick(self, tick: TickData) -> None:
|
||||
"""
|
||||
Callback of new tick data update.
|
||||
"""
|
||||
self.bg.update_tick(tick)
|
||||
|
||||
def on_bar(self, bar: BarData) -> None:
|
||||
"""
|
||||
Callback of new bar data update.
|
||||
"""
|
||||
self.cancel_all()
|
||||
|
||||
am: ArrayManager = self.am
|
||||
am.update_bar(bar)
|
||||
if not am.inited:
|
||||
return
|
||||
|
||||
fast_ma: np.ndarray = am.sma(self.fast_window, array=True)
|
||||
self.fast_ma0 = fast_ma[-1]
|
||||
self.fast_ma1 = fast_ma[-2]
|
||||
|
||||
slow_ma: np.ndarray = am.sma(self.slow_window, array=True)
|
||||
self.slow_ma0 = slow_ma[-1]
|
||||
self.slow_ma1 = slow_ma[-2]
|
||||
|
||||
cross_over: bool = self.fast_ma0 > self.slow_ma0 and self.fast_ma1 < self.slow_ma1
|
||||
cross_below: bool = 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)
|
||||
elif self.pos < 0:
|
||||
self.cover(bar.close_price, 1)
|
||||
self.buy(bar.close_price, 1)
|
||||
|
||||
elif cross_below:
|
||||
if self.pos == 0:
|
||||
self.short(bar.close_price, 1)
|
||||
elif self.pos > 0:
|
||||
self.sell(bar.close_price, 1)
|
||||
self.short(bar.close_price, 1)
|
||||
|
||||
self.put_event()
|
||||
|
||||
def on_order(self, order: OrderData) -> None:
|
||||
"""
|
||||
Callback of new order data update.
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_trade(self, trade: TradeData) -> None:
|
||||
"""
|
||||
Callback of new trade data update.
|
||||
"""
|
||||
self.put_event()
|
||||
|
||||
def on_stop_order(self, stop_order: StopOrder) -> None:
|
||||
"""
|
||||
Callback of stop order update.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,149 @@
|
||||
from datetime import time
|
||||
from vnpy_ctastrategy import (
|
||||
CtaTemplate,
|
||||
StopOrder,
|
||||
TickData,
|
||||
BarData,
|
||||
TradeData,
|
||||
OrderData,
|
||||
BarGenerator,
|
||||
ArrayManager,
|
||||
)
|
||||
|
||||
|
||||
class DualThrustStrategy(CtaTemplate):
|
||||
""""""
|
||||
|
||||
author = "用Python的交易员"
|
||||
|
||||
fixed_size: int = 1
|
||||
k1: float = 0.4
|
||||
k2: float = 0.6
|
||||
|
||||
day_open: float = 0
|
||||
day_high: float = 0
|
||||
day_low: float = 0
|
||||
day_range: float = 0
|
||||
long_entry: float = 0
|
||||
short_entry: float = 0
|
||||
long_entered: bool = False
|
||||
short_entered: bool = False
|
||||
|
||||
parameters = ["k1", "k2", "fixed_size"]
|
||||
variables = ["day_range", "long_entry", "short_entry"]
|
||||
|
||||
def on_init(self) -> None:
|
||||
"""
|
||||
Callback when strategy is inited.
|
||||
"""
|
||||
self.write_log("策略初始化")
|
||||
|
||||
self.bg: BarGenerator = BarGenerator(self.on_bar)
|
||||
self.am: ArrayManager = ArrayManager()
|
||||
|
||||
self.bars: list[BarData] = []
|
||||
self.exit_time: time = time(hour=14, minute=55)
|
||||
|
||||
self.load_bar(10)
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
Callback when strategy is started.
|
||||
"""
|
||||
self.write_log("策略启动")
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""
|
||||
Callback when strategy is stopped.
|
||||
"""
|
||||
self.write_log("策略停止")
|
||||
|
||||
def on_tick(self, tick: TickData) -> None:
|
||||
"""
|
||||
Callback of new tick data update.
|
||||
"""
|
||||
self.bg.update_tick(tick)
|
||||
|
||||
def on_bar(self, bar: BarData) -> None:
|
||||
"""
|
||||
Callback of new bar data update.
|
||||
"""
|
||||
self.cancel_all()
|
||||
|
||||
self.bars.append(bar)
|
||||
if len(self.bars) <= 2:
|
||||
return
|
||||
else:
|
||||
self.bars.pop(0)
|
||||
last_bar: BarData = self.bars[-2]
|
||||
|
||||
if last_bar.datetime.date() != bar.datetime.date():
|
||||
if self.day_high:
|
||||
self.day_range = self.day_high - self.day_low
|
||||
self.long_entry = bar.open_price + self.k1 * self.day_range
|
||||
self.short_entry = bar.open_price - self.k2 * self.day_range
|
||||
|
||||
self.day_open = bar.open_price
|
||||
self.day_high = bar.high_price
|
||||
self.day_low = bar.low_price
|
||||
|
||||
self.long_entered = False
|
||||
self.short_entered = False
|
||||
else:
|
||||
self.day_high = max(self.day_high, bar.high_price)
|
||||
self.day_low = min(self.day_low, bar.low_price)
|
||||
|
||||
if not self.day_range:
|
||||
return
|
||||
|
||||
if bar.datetime.time() < self.exit_time:
|
||||
if self.pos == 0:
|
||||
if bar.close_price > self.day_open:
|
||||
if not self.long_entered:
|
||||
self.buy(self.long_entry, self.fixed_size, stop=True)
|
||||
else:
|
||||
if not self.short_entered:
|
||||
self.short(self.short_entry,
|
||||
self.fixed_size, stop=True)
|
||||
|
||||
elif self.pos > 0:
|
||||
self.long_entered = True
|
||||
|
||||
self.sell(self.short_entry, self.fixed_size, stop=True)
|
||||
|
||||
if not self.short_entered:
|
||||
self.short(self.short_entry, self.fixed_size, stop=True)
|
||||
|
||||
elif self.pos < 0:
|
||||
self.short_entered = True
|
||||
|
||||
self.cover(self.long_entry, self.fixed_size, stop=True)
|
||||
|
||||
if not self.long_entered:
|
||||
self.buy(self.long_entry, self.fixed_size, stop=True)
|
||||
|
||||
else:
|
||||
if self.pos > 0:
|
||||
self.sell(bar.close_price * 0.99, abs(self.pos))
|
||||
elif self.pos < 0:
|
||||
self.cover(bar.close_price * 1.01, abs(self.pos))
|
||||
|
||||
self.put_event()
|
||||
|
||||
def on_order(self, order: OrderData) -> None:
|
||||
"""
|
||||
Callback of new order data update.
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_trade(self, trade: TradeData) -> None:
|
||||
"""
|
||||
Callback of new trade data update.
|
||||
"""
|
||||
self.put_event()
|
||||
|
||||
def on_stop_order(self, stop_order: StopOrder) -> None:
|
||||
"""
|
||||
Callback of stop order update.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,149 @@
|
||||
from vnpy_ctastrategy import (
|
||||
CtaTemplate,
|
||||
StopOrder,
|
||||
TickData,
|
||||
BarData,
|
||||
TradeData,
|
||||
OrderData,
|
||||
BarGenerator,
|
||||
ArrayManager,
|
||||
)
|
||||
|
||||
|
||||
class KingKeltnerStrategy(CtaTemplate):
|
||||
""""""
|
||||
|
||||
author = "用Python的交易员"
|
||||
|
||||
kk_length: int = 11
|
||||
kk_dev: float = 1.6
|
||||
trailing_percent: float = 0.8
|
||||
fixed_size: int = 1
|
||||
|
||||
kk_up: float = 0
|
||||
kk_down: float = 0
|
||||
intra_trade_high: float = 0
|
||||
intra_trade_low: float = 0
|
||||
|
||||
parameters = ["kk_length", "kk_dev", "trailing_percent", "fixed_size"]
|
||||
variables = ["kk_up", "kk_down"]
|
||||
|
||||
def on_init(self) -> None:
|
||||
"""
|
||||
Callback when strategy is inited.
|
||||
"""
|
||||
self.write_log("策略初始化")
|
||||
|
||||
self.bg: BarGenerator = BarGenerator(self.on_bar, 5, self.on_5min_bar)
|
||||
self.am: ArrayManager = ArrayManager()
|
||||
|
||||
self.long_vt_orderids: list[str] = []
|
||||
self.short_vt_orderids: list[str] = []
|
||||
self.vt_orderids: list[str] = []
|
||||
|
||||
self.load_bar(10)
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
Callback when strategy is started.
|
||||
"""
|
||||
self.write_log("策略启动")
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""
|
||||
Callback when strategy is stopped.
|
||||
"""
|
||||
self.write_log("策略停止")
|
||||
|
||||
def on_tick(self, tick: TickData) -> None:
|
||||
"""
|
||||
Callback of new tick data update.
|
||||
"""
|
||||
self.bg.update_tick(tick)
|
||||
|
||||
def on_bar(self, bar: BarData) -> None:
|
||||
"""
|
||||
Callback of new bar data update.
|
||||
"""
|
||||
self.bg.update_bar(bar)
|
||||
|
||||
def on_5min_bar(self, bar: BarData) -> None:
|
||||
""""""
|
||||
for orderid in self.vt_orderids:
|
||||
self.cancel_order(orderid)
|
||||
self.vt_orderids.clear()
|
||||
|
||||
am: ArrayManager = self.am
|
||||
am.update_bar(bar)
|
||||
if not am.inited:
|
||||
return
|
||||
|
||||
self.kk_up, self.kk_down = am.keltner(self.kk_length, self.kk_dev)
|
||||
|
||||
if self.pos == 0:
|
||||
self.intra_trade_high = bar.high_price
|
||||
self.intra_trade_low = bar.low_price
|
||||
self.send_oco_order(self.kk_up, self.kk_down, self.fixed_size)
|
||||
|
||||
elif self.pos > 0:
|
||||
self.intra_trade_high = max(self.intra_trade_high, bar.high_price)
|
||||
self.intra_trade_low = bar.low_price
|
||||
|
||||
sell_orderids: list[str] = self.sell(
|
||||
self.intra_trade_high * (1 - self.trailing_percent / 100),
|
||||
abs(self.pos),
|
||||
True
|
||||
)
|
||||
self.vt_orderids.extend(sell_orderids)
|
||||
|
||||
elif self.pos < 0:
|
||||
self.intra_trade_high = bar.high_price
|
||||
self.intra_trade_low = min(self.intra_trade_low, bar.low_price)
|
||||
|
||||
cover_orderids: list[str] = self.cover(
|
||||
self.intra_trade_low * (1 + self.trailing_percent / 100),
|
||||
abs(self.pos),
|
||||
True
|
||||
)
|
||||
self.vt_orderids.extend(cover_orderids)
|
||||
|
||||
self.put_event()
|
||||
|
||||
def on_order(self, order: OrderData) -> None:
|
||||
"""
|
||||
Callback of new order data update.
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_trade(self, trade: TradeData) -> None:
|
||||
"""
|
||||
Callback of new trade data update.
|
||||
"""
|
||||
if self.pos != 0:
|
||||
if self.pos > 0:
|
||||
for short_orderid in self.short_vt_orderids:
|
||||
self.cancel_order(short_orderid)
|
||||
|
||||
elif self.pos < 0:
|
||||
for buy_orderid in self.long_vt_orderids:
|
||||
self.cancel_order(buy_orderid)
|
||||
|
||||
for orderid in (self.long_vt_orderids + self.short_vt_orderids):
|
||||
if orderid in self.vt_orderids:
|
||||
self.vt_orderids.remove(orderid)
|
||||
|
||||
self.put_event()
|
||||
|
||||
def send_oco_order(self, buy_price: float, short_price: float, volume: float) -> None:
|
||||
""""""
|
||||
self.long_vt_orderids = self.buy(buy_price, volume, True)
|
||||
self.short_vt_orderids = self.short(short_price, volume, True)
|
||||
|
||||
self.vt_orderids.extend(self.long_vt_orderids)
|
||||
self.vt_orderids.extend(self.short_vt_orderids)
|
||||
|
||||
def on_stop_order(self, stop_order: StopOrder) -> None:
|
||||
"""
|
||||
Callback of stop order update.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,231 @@
|
||||
from vnpy_ctastrategy import (
|
||||
StopOrder,
|
||||
TickData,
|
||||
BarData,
|
||||
TradeData,
|
||||
OrderData,
|
||||
BarGenerator,
|
||||
ArrayManager,
|
||||
CtaSignal,
|
||||
TargetPosTemplate
|
||||
)
|
||||
|
||||
|
||||
class RsiSignal(CtaSignal):
|
||||
""""""
|
||||
|
||||
def __init__(self, rsi_window: int, rsi_level: float) -> None:
|
||||
"""Constructor"""
|
||||
super().__init__()
|
||||
|
||||
self.rsi_window: int = rsi_window
|
||||
self.rsi_level: float = rsi_level
|
||||
self.rsi_long: float = 50 + self.rsi_level
|
||||
self.rsi_short: float = 50 - self.rsi_level
|
||||
|
||||
self.bg: BarGenerator = BarGenerator(self.on_bar)
|
||||
self.am: ArrayManager = ArrayManager()
|
||||
|
||||
def on_tick(self, tick: TickData) -> None:
|
||||
"""
|
||||
Callback of new tick data update.
|
||||
"""
|
||||
self.bg.update_tick(tick)
|
||||
|
||||
def on_bar(self, bar: BarData) -> None:
|
||||
"""
|
||||
Callback of new bar data update.
|
||||
"""
|
||||
self.am.update_bar(bar)
|
||||
if not self.am.inited:
|
||||
self.set_signal_pos(0)
|
||||
|
||||
rsi_value: float = self.am.rsi(self.rsi_window)
|
||||
|
||||
if rsi_value >= self.rsi_long:
|
||||
self.set_signal_pos(1)
|
||||
elif rsi_value <= self.rsi_short:
|
||||
self.set_signal_pos(-1)
|
||||
else:
|
||||
self.set_signal_pos(0)
|
||||
|
||||
|
||||
class CciSignal(CtaSignal):
|
||||
""""""
|
||||
|
||||
def __init__(self, cci_window: int, cci_level: float) -> None:
|
||||
""""""
|
||||
super().__init__()
|
||||
|
||||
self.cci_window: int = cci_window
|
||||
self.cci_level: float = cci_level
|
||||
self.cci_long: float = self.cci_level
|
||||
self.cci_short: float = -self.cci_level
|
||||
|
||||
self.bg: BarGenerator = BarGenerator(self.on_bar)
|
||||
self.am: ArrayManager = ArrayManager()
|
||||
|
||||
def on_tick(self, tick: TickData) -> None:
|
||||
"""
|
||||
Callback of new tick data update.
|
||||
"""
|
||||
self.bg.update_tick(tick)
|
||||
|
||||
def on_bar(self, bar: BarData) -> None:
|
||||
"""
|
||||
Callback of new bar data update.
|
||||
"""
|
||||
self.am.update_bar(bar)
|
||||
if not self.am.inited:
|
||||
self.set_signal_pos(0)
|
||||
|
||||
cci_value: float = self.am.cci(self.cci_window)
|
||||
|
||||
if cci_value >= self.cci_long:
|
||||
self.set_signal_pos(1)
|
||||
elif cci_value <= self.cci_short:
|
||||
self.set_signal_pos(-1)
|
||||
else:
|
||||
self.set_signal_pos(0)
|
||||
|
||||
|
||||
class MaSignal(CtaSignal):
|
||||
""""""
|
||||
|
||||
def __init__(self, fast_window: int, slow_window: int) -> None:
|
||||
""""""
|
||||
super().__init__()
|
||||
|
||||
self.fast_window: int = fast_window
|
||||
self.slow_window: int = slow_window
|
||||
|
||||
self.bg: BarGenerator = BarGenerator(self.on_bar, 5, self.on_5min_bar)
|
||||
self.am: ArrayManager = ArrayManager()
|
||||
|
||||
def on_tick(self, tick: TickData) -> None:
|
||||
"""
|
||||
Callback of new tick data update.
|
||||
"""
|
||||
self.bg.update_tick(tick)
|
||||
|
||||
def on_bar(self, bar: BarData) -> None:
|
||||
"""
|
||||
Callback of new bar data update.
|
||||
"""
|
||||
self.bg.update_bar(bar)
|
||||
|
||||
def on_5min_bar(self, bar: BarData) -> None:
|
||||
""""""
|
||||
self.am.update_bar(bar)
|
||||
if not self.am.inited:
|
||||
self.set_signal_pos(0)
|
||||
|
||||
fast_ma: float = self.am.sma(self.fast_window)
|
||||
slow_ma: float = self.am.sma(self.slow_window)
|
||||
|
||||
if fast_ma > slow_ma:
|
||||
self.set_signal_pos(1)
|
||||
elif fast_ma < slow_ma:
|
||||
self.set_signal_pos(-1)
|
||||
else:
|
||||
self.set_signal_pos(0)
|
||||
|
||||
|
||||
class MultiSignalStrategy(TargetPosTemplate):
|
||||
""""""
|
||||
|
||||
author = "用Python的交易员"
|
||||
|
||||
rsi_window: int = 14
|
||||
rsi_level: int = 20
|
||||
cci_window: int = 30
|
||||
cci_level: int = 10
|
||||
fast_window: int = 5
|
||||
slow_window: int = 20
|
||||
|
||||
parameters = ["rsi_window", "rsi_level", "cci_window",
|
||||
"cci_level", "fast_window", "slow_window"]
|
||||
|
||||
def on_init(self) -> None:
|
||||
"""
|
||||
Callback when strategy is inited.
|
||||
"""
|
||||
self.write_log("策略初始化")
|
||||
|
||||
self.rsi_signal: RsiSignal = RsiSignal(self.rsi_window, self.rsi_level)
|
||||
self.cci_signal: CciSignal = CciSignal(self.cci_window, self.cci_level)
|
||||
self.ma_signal: MaSignal = MaSignal(self.fast_window, self.slow_window)
|
||||
|
||||
self.signal_pos: dict[str, int] = {
|
||||
"rsi": 0,
|
||||
"cci": 0,
|
||||
"ma": 0
|
||||
}
|
||||
|
||||
self.load_bar(10)
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
Callback when strategy is started.
|
||||
"""
|
||||
self.write_log("策略启动")
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""
|
||||
Callback when strategy is stopped.
|
||||
"""
|
||||
self.write_log("策略停止")
|
||||
|
||||
def on_tick(self, tick: TickData) -> None:
|
||||
"""
|
||||
Callback of new tick data update.
|
||||
"""
|
||||
super().on_tick(tick)
|
||||
|
||||
self.rsi_signal.on_tick(tick)
|
||||
self.cci_signal.on_tick(tick)
|
||||
self.ma_signal.on_tick(tick)
|
||||
|
||||
self.calculate_target_pos()
|
||||
|
||||
def on_bar(self, bar: BarData) -> None:
|
||||
"""
|
||||
Callback of new bar data update.
|
||||
"""
|
||||
super().on_bar(bar)
|
||||
|
||||
self.rsi_signal.on_bar(bar)
|
||||
self.cci_signal.on_bar(bar)
|
||||
self.ma_signal.on_bar(bar)
|
||||
|
||||
self.calculate_target_pos()
|
||||
|
||||
def calculate_target_pos(self) -> None:
|
||||
""""""
|
||||
self.signal_pos["rsi"] = self.rsi_signal.get_signal_pos()
|
||||
self.signal_pos["cci"] = self.cci_signal.get_signal_pos()
|
||||
self.signal_pos["ma"] = self.ma_signal.get_signal_pos()
|
||||
|
||||
target_pos: int = 0
|
||||
for v in self.signal_pos.values():
|
||||
target_pos += v
|
||||
|
||||
self.set_target_pos(target_pos)
|
||||
|
||||
def on_order(self, order: OrderData) -> None:
|
||||
"""
|
||||
Callback of new order data update.
|
||||
"""
|
||||
super().on_order(order)
|
||||
|
||||
def on_trade(self, trade: TradeData) -> None:
|
||||
"""
|
||||
Callback of new trade data update.
|
||||
"""
|
||||
self.put_event()
|
||||
|
||||
def on_stop_order(self, stop_order: StopOrder) -> None:
|
||||
"""
|
||||
Callback of stop order update.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,138 @@
|
||||
from vnpy_ctastrategy import (
|
||||
CtaTemplate,
|
||||
StopOrder,
|
||||
TickData,
|
||||
BarData,
|
||||
TradeData,
|
||||
OrderData,
|
||||
BarGenerator,
|
||||
ArrayManager,
|
||||
)
|
||||
|
||||
|
||||
class MultiTimeframeStrategy(CtaTemplate):
|
||||
""""""
|
||||
author = "用Python的交易员"
|
||||
|
||||
rsi_signal: int = 20
|
||||
rsi_window: int = 14
|
||||
fast_window: int = 5
|
||||
slow_window: int = 20
|
||||
fixed_size: int = 1
|
||||
|
||||
rsi_value: float = 0
|
||||
rsi_long: float = 0
|
||||
rsi_short: float = 0
|
||||
fast_ma: float = 0
|
||||
slow_ma: float = 0
|
||||
ma_trend: float = 0
|
||||
|
||||
parameters = ["rsi_signal", "rsi_window",
|
||||
"fast_window", "slow_window",
|
||||
"fixed_size"]
|
||||
|
||||
variables = ["rsi_value", "rsi_long", "rsi_short",
|
||||
"fast_ma", "slow_ma", "ma_trend"]
|
||||
|
||||
def on_init(self) -> None:
|
||||
"""
|
||||
Callback when strategy is inited.
|
||||
"""
|
||||
self.write_log("策略初始化")
|
||||
|
||||
self.rsi_long: float = 50 + self.rsi_signal
|
||||
self.rsi_short: float = 50 - self.rsi_signal
|
||||
|
||||
self.bg5: BarGenerator = BarGenerator(self.on_bar, 5, self.on_5min_bar)
|
||||
self.am5: ArrayManager = ArrayManager()
|
||||
|
||||
self.bg15: BarGenerator = BarGenerator(self.on_bar, 15, self.on_15min_bar)
|
||||
self.am15: ArrayManager = ArrayManager()
|
||||
|
||||
self.load_bar(10)
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
Callback when strategy is started.
|
||||
"""
|
||||
self.write_log("策略启动")
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""
|
||||
Callback when strategy is stopped.
|
||||
"""
|
||||
self.write_log("策略停止")
|
||||
|
||||
def on_tick(self, tick: TickData) -> None:
|
||||
"""
|
||||
Callback of new tick data update.
|
||||
"""
|
||||
self.bg5.update_tick(tick)
|
||||
|
||||
def on_bar(self, bar: BarData) -> None:
|
||||
"""
|
||||
Callback of new bar data update.
|
||||
"""
|
||||
self.bg5.update_bar(bar)
|
||||
self.bg15.update_bar(bar)
|
||||
|
||||
def on_5min_bar(self, bar: BarData) -> None:
|
||||
""""""
|
||||
self.cancel_all()
|
||||
|
||||
self.am5.update_bar(bar)
|
||||
if not self.am5.inited:
|
||||
return
|
||||
|
||||
if not self.ma_trend:
|
||||
return
|
||||
|
||||
self.rsi_value = self.am5.rsi(self.rsi_window)
|
||||
|
||||
if self.pos == 0:
|
||||
if self.ma_trend > 0 and self.rsi_value >= self.rsi_long:
|
||||
self.buy(bar.close_price + 5, self.fixed_size)
|
||||
elif self.ma_trend < 0 and self.rsi_value <= self.rsi_short:
|
||||
self.short(bar.close_price - 5, self.fixed_size)
|
||||
|
||||
elif self.pos > 0:
|
||||
if self.ma_trend < 0 or self.rsi_value < 50:
|
||||
self.sell(bar.close_price - 5, abs(self.pos))
|
||||
|
||||
elif self.pos < 0:
|
||||
if self.ma_trend > 0 or self.rsi_value > 50:
|
||||
self.cover(bar.close_price + 5, abs(self.pos))
|
||||
|
||||
self.put_event()
|
||||
|
||||
def on_15min_bar(self, bar: BarData) -> None:
|
||||
""""""
|
||||
self.am15.update_bar(bar)
|
||||
if not self.am15.inited:
|
||||
return
|
||||
|
||||
self.fast_ma = self.am15.sma(self.fast_window)
|
||||
self.slow_ma = self.am15.sma(self.slow_window)
|
||||
|
||||
if self.fast_ma > self.slow_ma:
|
||||
self.ma_trend = 1
|
||||
else:
|
||||
self.ma_trend = -1
|
||||
|
||||
def on_order(self, order: OrderData) -> None:
|
||||
"""
|
||||
Callback of new order data update.
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_trade(self, trade: TradeData) -> None:
|
||||
"""
|
||||
Callback of new trade data update.
|
||||
"""
|
||||
self.put_event()
|
||||
|
||||
def on_stop_order(self, stop_order: StopOrder) -> None:
|
||||
"""
|
||||
Callback of stop order update.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,160 @@
|
||||
from vnpy_ctastrategy import (
|
||||
CtaTemplate,
|
||||
StopOrder,
|
||||
Direction,
|
||||
TickData,
|
||||
BarData,
|
||||
TradeData,
|
||||
OrderData,
|
||||
BarGenerator,
|
||||
ArrayManager,
|
||||
)
|
||||
|
||||
|
||||
class TurtleSignalStrategy(CtaTemplate):
|
||||
""""""
|
||||
author = "用Python的交易员"
|
||||
|
||||
entry_window: int = 20
|
||||
exit_window: int = 10
|
||||
atr_window: int = 20
|
||||
fixed_size: int = 1
|
||||
|
||||
entry_up: float = 0
|
||||
entry_down: float = 0
|
||||
exit_up: float = 0
|
||||
exit_down: float = 0
|
||||
atr_value: float = 0
|
||||
long_entry: float = 0
|
||||
short_entry: float = 0
|
||||
long_stop: float = 0
|
||||
short_stop: float = 0
|
||||
|
||||
parameters = ["entry_window", "exit_window", "atr_window", "fixed_size"]
|
||||
variables = ["entry_up", "entry_down", "exit_up", "exit_down", "atr_value"]
|
||||
|
||||
def on_init(self) -> None:
|
||||
"""
|
||||
Callback when strategy is inited.
|
||||
"""
|
||||
self.write_log("策略初始化")
|
||||
|
||||
self.bg: BarGenerator = BarGenerator(self.on_bar)
|
||||
self.am: ArrayManager = ArrayManager()
|
||||
|
||||
self.load_bar(20)
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
Callback when strategy is started.
|
||||
"""
|
||||
self.write_log("策略启动")
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""
|
||||
Callback when strategy is stopped.
|
||||
"""
|
||||
self.write_log("策略停止")
|
||||
|
||||
def on_tick(self, tick: TickData) -> None:
|
||||
"""
|
||||
Callback of new tick data update.
|
||||
"""
|
||||
self.bg.update_tick(tick)
|
||||
|
||||
def on_bar(self, bar: BarData) -> None:
|
||||
"""
|
||||
Callback of new bar data update.
|
||||
"""
|
||||
self.cancel_all()
|
||||
|
||||
self.am.update_bar(bar)
|
||||
if not self.am.inited:
|
||||
return
|
||||
|
||||
# Only calculates new entry channel when no position holding
|
||||
if not self.pos:
|
||||
self.entry_up, self.entry_down = self.am.donchian(
|
||||
self.entry_window
|
||||
)
|
||||
|
||||
self.exit_up, self.exit_down = self.am.donchian(self.exit_window)
|
||||
|
||||
if not self.pos:
|
||||
self.atr_value = self.am.atr(self.atr_window)
|
||||
|
||||
self.long_entry = 0
|
||||
self.short_entry = 0
|
||||
self.long_stop = 0
|
||||
self.short_stop = 0
|
||||
|
||||
self.send_buy_orders(self.entry_up)
|
||||
self.send_short_orders(self.entry_down)
|
||||
elif self.pos > 0:
|
||||
self.send_buy_orders(self.entry_up)
|
||||
|
||||
sell_price: float = max(self.long_stop, self.exit_down)
|
||||
self.sell(sell_price, abs(self.pos), True)
|
||||
|
||||
elif self.pos < 0:
|
||||
self.send_short_orders(self.entry_down)
|
||||
|
||||
cover_price: float = min(self.short_stop, self.exit_up)
|
||||
self.cover(cover_price, abs(self.pos), True)
|
||||
|
||||
self.put_event()
|
||||
|
||||
def on_trade(self, trade: TradeData) -> None:
|
||||
"""
|
||||
Callback of new trade data update.
|
||||
"""
|
||||
if trade.direction == Direction.LONG:
|
||||
self.long_entry = trade.price
|
||||
self.long_stop = self.long_entry - 2 * self.atr_value
|
||||
else:
|
||||
self.short_entry = trade.price
|
||||
self.short_stop = self.short_entry + 2 * self.atr_value
|
||||
|
||||
def on_order(self, order: OrderData) -> None:
|
||||
"""
|
||||
Callback of new order data update.
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_stop_order(self, stop_order: StopOrder) -> None:
|
||||
"""
|
||||
Callback of stop order update.
|
||||
"""
|
||||
pass
|
||||
|
||||
def send_buy_orders(self, price: float) -> None:
|
||||
""""""
|
||||
t: float = self.pos / self.fixed_size
|
||||
|
||||
if t < 1:
|
||||
self.buy(price, self.fixed_size, True)
|
||||
|
||||
if t < 2:
|
||||
self.buy(price + self.atr_value * 0.5, self.fixed_size, True)
|
||||
|
||||
if t < 3:
|
||||
self.buy(price + self.atr_value, self.fixed_size, True)
|
||||
|
||||
if t < 4:
|
||||
self.buy(price + self.atr_value * 1.5, self.fixed_size, True)
|
||||
|
||||
def send_short_orders(self, price: float) -> None:
|
||||
""""""
|
||||
t: float = self.pos / self.fixed_size
|
||||
|
||||
if t > -1:
|
||||
self.short(price, self.fixed_size, True)
|
||||
|
||||
if t > -2:
|
||||
self.short(price - self.atr_value * 0.5, self.fixed_size, True)
|
||||
|
||||
if t > -3:
|
||||
self.short(price - self.atr_value, self.fixed_size, True)
|
||||
|
||||
if t > -4:
|
||||
self.short(price - self.atr_value * 1.5, self.fixed_size, True)
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tests for sanguo_api.strategy_registry (Task S1.3)."""
|
||||
from sanguo_api.strategy_registry import (
|
||||
list_strategies, strategy_params, STRATEGY_NAMES,
|
||||
list_strategy_files, read_strategy_file,
|
||||
list_strategy_files, read_strategy_file, get_strategy_class,
|
||||
)
|
||||
|
||||
|
||||
@@ -57,3 +57,22 @@ def test_read_strategy_file_returns_code():
|
||||
content = read_strategy_file(f["name"])
|
||||
assert "code" in content and isinstance(content["code"], str)
|
||||
assert content["class_name"] == f["class_name"]
|
||||
|
||||
|
||||
def test_vnpy_builtin_copies_classified_cta():
|
||||
"""灌入的 vnpy 内置模板(from vnpy_ctastrategy import ...)必须判成 cta。"""
|
||||
data = list_strategy_files()
|
||||
cta_files = {f["name"]: f for f in data["files"] if f["dir"] == "sanguo_trader/strategy/"}
|
||||
assert "double_ma_strategy.py" in cta_files, "内置策略模板未灌入 sanguo_trader/strategy"
|
||||
assert cta_files["double_ma_strategy.py"]["type"] == "cta"
|
||||
assert cta_files["double_ma_strategy.py"]["class_name"] == "DoubleMaStrategy"
|
||||
|
||||
|
||||
def test_get_strategy_class_self_owned_priority_or_fallback():
|
||||
"""有 vnpy_ctastrategy 环境:自研目录类可加载;无:优雅返 None/降级不崩。"""
|
||||
cls = get_strategy_class("DoubleMaStrategy")
|
||||
# 本机 dev 无 vnpy_ctastrategy 时 pip 侧也拿不到 → None 不崩即可;
|
||||
# 容器内应返回 sanguo_trader.strategy.double_ma_strategy 的类(自研优先)
|
||||
if cls is not None:
|
||||
assert cls.__name__ == "DoubleMaStrategy"
|
||||
assert cls.__module__.startswith("sanguo_trader.strategy")
|
||||
|
||||
Reference in New Issue
Block a user