feat(strategies): 内置策略从 5 个扩充到 18 个

新增 13 个经典策略,覆盖趋势/通道/震荡/均线四大类:

趋势类:
- ema_cross  EMA 双线交叉(比 MA 反应更灵敏)
- triple_ma  三均线系统(多头/空头排列)
- dmi        DMI 趋向指标(+DI/-DI 交叉)
- trix       TRIX 三重平滑(过滤短期波动)

通道/突破类:
- donchian   唐安奇通道突破(海龟交易法)
- keltner    肯特纳通道(ATR-based)
- atr_breakout ATR 通道突破(均线±K×ATR)

震荡/反转类:
- cci        CCI 超卖反弹
- wr_reversal WR 威廉超卖
- bias_reversal BIAS 乖离反弹
- emv        EMV 简易波动(量价结合)
- dpo        DPO 区间震荡

均线类:
- bbi        BBI 多空指标(4 均线综合)

全部基于 MyTT 现成指标实现,每个策略声明参数 schema 供 Web 表单动态渲染。
18/18 策略冒烟测试通过(合成数据回测无运行时错误)。
This commit is contained in:
Justin Gu
2026-07-03 04:36:04 +08:00
parent 0cabae4eda
commit 2903d8c800
2 changed files with 380 additions and 3 deletions
+378 -1
View File
@@ -14,7 +14,26 @@ from easy_tdx.backtest.strategies.registry import (
ParametrizedStrategy, ParametrizedStrategy,
register_strategy, register_strategy,
) )
from easy_tdx.MyTT import BOLL, CROSS, KDJ, MA, MACD, RSI from easy_tdx.MyTT import (
ATR,
BBI,
BIAS,
BOLL,
CCI,
CROSS,
DMI,
DPO,
EMA,
EMV,
KDJ,
KTN,
MA,
MACD,
RSI,
TAQ,
TRIX,
WR,
)
__all__: list[str] = [] # 注册副作用即可,无需导出符号 __all__: list[str] = [] # 注册副作用即可,无需导出符号
@@ -175,3 +194,361 @@ class KdjCrossStrategy(ParametrizedStrategy):
self.buy() self.buy()
elif self.dead[i] and self.position["size"] > 0: elif self.dead[i] and self.position["size"] > 0:
self.sell() self.sell()
# ── EMA 双线交叉 ──────────────────────────────────────────────────────────────
@register_strategy(
name="ema_cross",
label="EMA 双线交叉",
description="指数均线金叉买入、死叉卖出。比简单均线反应更灵敏。",
)
class EmaCrossStrategy(ParametrizedStrategy):
params = [
Param("fast", int, default=12, min_value=2, max_value=60, label="快线周期"),
Param("slow", int, default=26, min_value=5, max_value=120, label="慢线周期"),
]
def init(self) -> None:
self.ema_fast = self.I(EMA, self.data.close, self.p["fast"])
self.ema_slow = self.I(EMA, self.data.close, self.p["slow"])
self.gold = self.I(CROSS, self.ema_fast, self.ema_slow)
self.dead = self.I(CROSS, self.ema_slow, self.ema_fast)
def next(self) -> None:
i = self._bar_index
if self.gold[i]:
self.buy()
elif self.dead[i] and self.position["size"] > 0:
self.sell()
# ── 三均线系统 ────────────────────────────────────────────────────────────────
@register_strategy(
name="triple_ma",
label="三均线系统",
description="短中长期均线多头排列买入、空头排列卖出。",
)
class TripleMaStrategy(ParametrizedStrategy):
params = [
Param("short", int, default=5, min_value=1, max_value=30, label="短期"),
Param("mid", int, default=20, min_value=5, max_value=60, label="中期"),
Param("long", int, default=60, min_value=20, max_value=250, label="长期"),
]
def init(self) -> None:
self.ma_s = self.I(MA, self.data.close, self.p["short"])
self.ma_m = self.I(MA, self.data.close, self.p["mid"])
self.ma_l = self.I(MA, self.data.close, self.p["long"])
def next(self) -> None:
i = self._bar_index
if self.ma_s[i] > self.ma_m[i] > self.ma_l[i] and self.position["size"] == 0:
self.buy()
elif self.ma_s[i] < self.ma_m[i] < self.ma_l[i] and self.position["size"] > 0:
self.sell()
# ── 唐安奇通道(海龟)────────────────────────────────────────────────────────
@register_strategy(
name="donchian",
label="唐安奇通道突破",
description="突破N日最高价买入,跌破N日最低价卖出。海龟交易法核心。",
)
class DonchianStrategy(ParametrizedStrategy):
params = [
Param("n", int, default=20, min_value=5, max_value=100, label="通道周期"),
]
def init(self) -> None:
self.upper, self._mid, self.lower = self.I(TAQ, self.data.high, self.data.low, self.p["n"])
def next(self) -> None:
i = self._bar_index
close = self.data.close[0]
if close >= self.upper[i] and self.position["size"] == 0:
self.buy()
elif close <= self.lower[i] and self.position["size"] > 0:
self.sell()
# ── 肯特纳通道 ────────────────────────────────────────────────────────────────
@register_strategy(
name="keltner",
label="肯特纳通道",
description="收盘价突破上轨买入,跌破下轨卖出。ATR-based 通道。",
)
class KeltnerStrategy(ParametrizedStrategy):
params = [
Param("n", int, default=20, min_value=5, max_value=100, label="均线周期"),
Param("m", int, default=10, min_value=2, max_value=50, label="ATR周期"),
]
def init(self) -> None:
self.upper, self._mid, self.lower = self.I(
KTN, self.data.close, self.data.high, self.data.low, self.p["n"], self.p["m"]
)
def next(self) -> None:
i = self._bar_index
close = self.data.close[0]
if close >= self.upper[i] and self.position["size"] == 0:
self.buy()
elif close <= self.lower[i] and self.position["size"] > 0:
self.sell()
# ── BBI 多空指标 ──────────────────────────────────────────────────────────────
@register_strategy(
name="bbi",
label="BBI 多空指标",
description="收盘价上穿BBI买入,下穿BBI卖出。多空综合指标。",
)
class BbiStrategy(ParametrizedStrategy):
params = [
Param("m1", int, default=3, min_value=1, max_value=20, label="均线1"),
Param("m2", int, default=6, min_value=2, max_value=30, label="均线2"),
Param("m3", int, default=12, min_value=5, max_value=60, label="均线3"),
Param("m4", int, default=20, min_value=10, max_value=120, label="均线4"),
]
def init(self) -> None:
self.bbi = self.I(
BBI, self.data.close, self.p["m1"], self.p["m2"], self.p["m3"], self.p["m4"]
)
def next(self) -> None:
i = self._bar_index
close = self.data.close[0]
if close > self.bbi[i] and self.position["size"] == 0:
self.buy()
elif close < self.bbi[i] and self.position["size"] > 0:
self.sell()
# ── CCI 顺势指标 ──────────────────────────────────────────────────────────────
@register_strategy(
name="cci",
label="CCI 超卖反弹",
description="CCI 跌破-100后回升买入,涨破+100卖出。",
)
class CciStrategy(ParametrizedStrategy):
params = [
Param("n", int, default=14, min_value=2, max_value=50, label="CCI周期"),
Param("oversold", int, default=-100, min_value=-200, max_value=0, label="超卖线"),
Param("overbought", int, default=100, min_value=0, max_value=200, label="超买线"),
]
def init(self) -> None:
self.cci = self.I(CCI, self.data.close, self.data.high, self.data.low, self.p["n"])
def next(self) -> None:
i = self._bar_index
cci = self.cci[i]
if cci <= self.p["oversold"] and self.position["size"] == 0:
self.buy()
elif cci >= self.p["overbought"] and self.position["size"] > 0:
self.sell()
# ── WR 威廉指标 ───────────────────────────────────────────────────────────────
@register_strategy(
name="wr_reversal",
label="WR 威廉超卖",
description="WR 进入超卖区(<-80)买入,进入超买区(>-20)卖出。",
)
class WrReversalStrategy(ParametrizedStrategy):
params = [
Param("n", int, default=14, min_value=2, max_value=50, label="WR周期"),
Param("oversold", int, default=-80, min_value=-100, max_value=-40, label="超卖线"),
Param("overbought", int, default=-20, min_value=-60, max_value=0, label="超买线"),
]
def init(self) -> None:
self.wr, self._wr1 = self.I(WR, self.data.close, self.data.high, self.data.low, self.p["n"])
def next(self) -> None:
i = self._bar_index
wr = self.wr[i]
if wr <= self.p["oversold"] and self.position["size"] == 0:
self.buy()
elif wr >= self.p["overbought"] and self.position["size"] > 0:
self.sell()
# ── BIAS 乖离率 ───────────────────────────────────────────────────────────────
@register_strategy(
name="bias_reversal",
label="BIAS 乖离反弹",
description="乖离率低于负阈值(超跌)买入,高于正阈值(超涨)卖出。",
)
class BiasReversalStrategy(ParametrizedStrategy):
params = [
Param("n", int, default=6, min_value=2, max_value=30, label="均线周期"),
Param("threshold", float, default=5.0, min_value=1.0, max_value=20.0, label="乖离阈值%"),
]
def init(self) -> None:
self.bias, self._b2, self._b3 = self.I(BIAS, self.data.close, self.p["n"], 12, 24)
def next(self) -> None:
i = self._bar_index
bias_pct = self.bias[i] * 100
threshold = self.p["threshold"]
if bias_pct <= -threshold and self.position["size"] == 0:
self.buy()
elif bias_pct >= threshold and self.position["size"] > 0:
self.sell()
# ── DMI 趋向指标 ──────────────────────────────────────────────────────────────
@register_strategy(
name="dmi",
label="DMI 趋向指标",
description="+DI 上穿-DI 买入(多头趋强),+DI 下穿-DI 卖出。",
)
class DmiStrategy(ParametrizedStrategy):
params = [
Param("m1", int, default=14, min_value=2, max_value=30, label="DI周期"),
Param("m2", int, default=6, min_value=2, max_value=20, label="ADX周期"),
]
def init(self) -> None:
self.pdi, self.mdi, self._adx, self._adxr = self.I(
DMI, self.data.close, self.data.high, self.data.low, self.p["m1"], self.p["m2"]
)
self.gold = self.I(CROSS, self.pdi, self.mdi)
self.dead = self.I(CROSS, self.mdi, self.pdi)
def next(self) -> None:
i = self._bar_index
if self.gold[i]:
self.buy()
elif self.dead[i] and self.position["size"] > 0:
self.sell()
# ── TRIX 三重平滑 ─────────────────────────────────────────────────────────────
@register_strategy(
name="trix",
label="TRIX 三重平滑",
description="TRIX 上穿信号线买入,下穿卖出。过滤短期波动的趋势指标。",
)
class TrixStrategy(ParametrizedStrategy):
params = [
Param("m1", int, default=12, min_value=2, max_value=30, label="TRIX周期"),
Param("m2", int, default=20, min_value=5, max_value=60, label="信号周期"),
]
def init(self) -> None:
self.trix, self.trma = self.I(TRIX, self.data.close, self.p["m1"], self.p["m2"])
self.gold = self.I(CROSS, self.trix, self.trma)
self.dead = self.I(CROSS, self.trma, self.trix)
def next(self) -> None:
i = self._bar_index
if self.gold[i]:
self.buy()
elif self.dead[i] and self.position["size"] > 0:
self.sell()
# ── EMV 简易波动 ──────────────────────────────────────────────────────────────
@register_strategy(
name="emv",
label="EMV 简易波动",
description="EMV 上穿0轴买入,下穿0轴卖出。量价结合指标。",
)
class EmvStrategy(ParametrizedStrategy):
params = [
Param("n", int, default=14, min_value=2, max_value=30, label="EMV周期"),
]
def init(self) -> None:
self.emv, self._maemv = self.I(
EMV, self.data.high, self.data.low, self.data.vol, self.p["n"]
)
def next(self) -> None:
i = self._bar_index
if self.emv[i] > 0 and self.position["size"] == 0:
self.buy()
elif self.emv[i] < 0 and self.position["size"] > 0:
self.sell()
# ── DPO 区间震荡 ──────────────────────────────────────────────────────────────
@register_strategy(
name="dpo",
label="DPO 区间震荡",
description="DPO 上穿信号线买入,下穿卖出。去除趋势的震荡指标。",
)
class DpoStrategy(ParametrizedStrategy):
params = [
Param("m1", int, default=20, min_value=5, max_value=60, label="DPO周期"),
]
def init(self) -> None:
self.dpo, self.madpo = self.I(DPO, self.data.close, self.p["m1"])
self.gold = self.I(CROSS, self.dpo, self.madpo)
self.dead = self.I(CROSS, self.madpo, self.dpo)
def next(self) -> None:
i = self._bar_index
if self.gold[i]:
self.buy()
elif self.dead[i] and self.position["size"] > 0:
self.sell()
# ── ATR 通道突破 ──────────────────────────────────────────────────────────────
@register_strategy(
name="atr_breakout",
label="ATR 通道突破",
description="收盘价突破 均线+K×ATR 买入,跌破 均线-K×ATR 卖出。",
)
class AtrBreakoutStrategy(ParametrizedStrategy):
params = [
Param("n_ma", int, default=20, min_value=5, max_value=100, label="均线周期"),
Param("n_atr", int, default=20, min_value=5, max_value=50, label="ATR周期"),
Param("k", float, default=2.0, min_value=0.5, max_value=5.0, label="ATR倍数"),
]
def init(self) -> None:
self.ma = self.I(MA, self.data.close, self.p["n_ma"])
self.atr = self.I(ATR, self.data.close, self.data.high, self.data.low, self.p["n_atr"])
def next(self) -> None:
i = self._bar_index
close = self.data.close[0]
upper = self.ma[i] + self.p["k"] * self.atr[i]
lower = self.ma[i] - self.p["k"] * self.atr[i]
if close >= upper and self.position["size"] == 0:
self.buy()
elif close <= lower and self.position["size"] > 0:
self.sell()
+2 -2
View File
@@ -59,7 +59,7 @@ def test_registry_has_builtin_strategies():
assert "boll_breakout" in names assert "boll_breakout" in names
assert "rsi_reversal" in names assert "rsi_reversal" in names
assert "kdj_cross" in names assert "kdj_cross" in names
assert len(names) >= 5 assert len(names) >= 18
def test_strategy_schema_serialization(): def test_strategy_schema_serialization():
@@ -522,7 +522,7 @@ def test_list_strategies_endpoint(client):
resp = client.get("/api/v1/backtest/strategies") resp = client.get("/api/v1/backtest/strategies")
assert resp.status_code == 200 assert resp.status_code == 200
body = resp.json() body = resp.json()
assert body["count"] >= 5 assert body["count"] >= 18
names = [s["name"] for s in body["strategies"]] names = [s["name"] for s in body["strategies"]]
assert "ma_cross" in names assert "ma_cross" in names
# 每个策略的 schema 结构完整 # 每个策略的 schema 结构完整