diff --git a/sanguo_api/strategy_registry.py b/sanguo_api/strategy_registry.py index 9733b85..554c6fe 100644 --- a/sanguo_api/strategy_registry.py +++ b/sanguo_api/strategy_registry.py @@ -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" diff --git a/sanguo_trader/strategy/__init__.py b/sanguo_trader/strategy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sanguo_trader/strategy/atr_rsi_strategy.py b/sanguo_trader/strategy/atr_rsi_strategy.py new file mode 100644 index 0000000..cc3050c --- /dev/null +++ b/sanguo_trader/strategy/atr_rsi_strategy.py @@ -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 diff --git a/sanguo_trader/strategy/boll_channel_strategy.py b/sanguo_trader/strategy/boll_channel_strategy.py new file mode 100644 index 0000000..22b3f07 --- /dev/null +++ b/sanguo_trader/strategy/boll_channel_strategy.py @@ -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 diff --git a/sanguo_trader/strategy/double_ma_strategy.py b/sanguo_trader/strategy/double_ma_strategy.py new file mode 100644 index 0000000..8e5771b --- /dev/null +++ b/sanguo_trader/strategy/double_ma_strategy.py @@ -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 diff --git a/sanguo_trader/strategy/dual_thrust_strategy.py b/sanguo_trader/strategy/dual_thrust_strategy.py new file mode 100644 index 0000000..ee92936 --- /dev/null +++ b/sanguo_trader/strategy/dual_thrust_strategy.py @@ -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 diff --git a/sanguo_trader/strategy/king_keltner_strategy.py b/sanguo_trader/strategy/king_keltner_strategy.py new file mode 100644 index 0000000..d9fae49 --- /dev/null +++ b/sanguo_trader/strategy/king_keltner_strategy.py @@ -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 diff --git a/sanguo_trader/strategy/multi_signal_strategy.py b/sanguo_trader/strategy/multi_signal_strategy.py new file mode 100644 index 0000000..f5e2201 --- /dev/null +++ b/sanguo_trader/strategy/multi_signal_strategy.py @@ -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 diff --git a/sanguo_trader/strategy/multi_timeframe_strategy.py b/sanguo_trader/strategy/multi_timeframe_strategy.py new file mode 100644 index 0000000..b48d76d --- /dev/null +++ b/sanguo_trader/strategy/multi_timeframe_strategy.py @@ -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 diff --git a/sanguo_trader/strategy/turtle_signal_strategy.py b/sanguo_trader/strategy/turtle_signal_strategy.py new file mode 100644 index 0000000..52fab45 --- /dev/null +++ b/sanguo_trader/strategy/turtle_signal_strategy.py @@ -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) diff --git a/tests/api/test_strategy_registry.py b/tests/api/test_strategy_registry.py index 90f0aae..953b877 100644 --- a/tests/api/test_strategy_registry.py +++ b/tests/api/test_strategy_registry.py @@ -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")