first commit
This commit is contained in:
24
backend/app/backtest/strategies/__init__.py
Normal file
24
backend/app/backtest/strategies/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""策略注册表与工厂。
|
||||
|
||||
新增策略:实现 Strategy(base.py),在此注册 {name: Class},前端下拉即可选。
|
||||
策略构造参数由请求的 params(dict) 以 **kwargs 传入。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import Strategy
|
||||
from .macd_cross import MACDCrossStrategy
|
||||
from .ma_strategies import MACrossStrategy, SingleMAStrategy
|
||||
|
||||
STRATEGIES: dict[str, type[Strategy]] = {
|
||||
"macd_cross": MACDCrossStrategy,
|
||||
"ma_cross": MACrossStrategy,
|
||||
"single_ma": SingleMAStrategy,
|
||||
}
|
||||
|
||||
|
||||
def build_strategy(name: str, params: dict | None) -> Strategy:
|
||||
cls = STRATEGIES.get(name)
|
||||
if cls is None:
|
||||
raise ValueError(f"未知策略: {name}(可用: {', '.join(STRATEGIES)})")
|
||||
kwargs = {k: v for k, v in (params or {}).items()}
|
||||
return cls(**kwargs)
|
||||
20
backend/app/backtest/strategies/base.py
Normal file
20
backend/app/backtest/strategies/base.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""策略抽象基类。策略只产生买卖意图(向 broker 下单),不负责撮合/费用。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from ..broker import PaperBroker
|
||||
|
||||
|
||||
class Strategy(ABC):
|
||||
"""compute 预算指标;on_bar 逐 bar 决策并向 broker 下单。"""
|
||||
|
||||
@abstractmethod
|
||||
def compute(self, close: pd.Series, high: pd.Series, low: pd.Series) -> pd.DataFrame:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def on_bar(self, i: int, row: pd.Series, broker: PaperBroker) -> None:
|
||||
...
|
||||
74
backend/app/backtest/strategies/ma_strategies.py
Normal file
74
backend/app/backtest/strategies/ma_strategies.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""均线类策略:双均线交叉、单均线(价格上穿/下穿)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from ...indicators import ma
|
||||
from ..broker import PaperBroker
|
||||
from .base import Strategy
|
||||
|
||||
|
||||
class MACrossStrategy(Strategy):
|
||||
"""双均线交叉:快线上穿慢线买入,下穿卖出(金叉/死叉)。"""
|
||||
|
||||
def __init__(self, fast: float = 5, slow: float = 20):
|
||||
self.fast = int(fast)
|
||||
self.slow = int(slow)
|
||||
self._ind: pd.DataFrame | None = None
|
||||
self._prev_fast: float | None = None
|
||||
self._prev_slow: float | None = None
|
||||
|
||||
def compute(self, close: pd.Series, high: pd.Series, low: pd.Series) -> pd.DataFrame:
|
||||
self._ind = pd.DataFrame({"fast": ma(close, self.fast), "slow": ma(close, self.slow)})
|
||||
return self._ind
|
||||
|
||||
def on_bar(self, i: int, row: pd.Series, broker: PaperBroker) -> None:
|
||||
f = float(self._ind["fast"].iloc[i])
|
||||
s = float(self._ind["slow"].iloc[i])
|
||||
if self._prev_fast is None:
|
||||
self._prev_fast, self._prev_slow = f, s
|
||||
return
|
||||
|
||||
golden = self._prev_fast <= self._prev_slow and f > s
|
||||
death = self._prev_fast >= self._prev_slow and f < s
|
||||
|
||||
price = float(row["close"])
|
||||
ts = row["ts"]
|
||||
if golden and pd.notna(f):
|
||||
broker.buy_max(ts, price)
|
||||
elif death and broker.position > 0 and pd.notna(f):
|
||||
broker.sell_all(ts, price)
|
||||
|
||||
self._prev_fast, self._prev_slow = f, s
|
||||
|
||||
|
||||
class SingleMAStrategy(Strategy):
|
||||
"""单均线:收盘价上穿均线买入,下穿均线卖出。"""
|
||||
|
||||
def __init__(self, period: float = 20):
|
||||
self.period = int(period)
|
||||
self._ind: pd.DataFrame | None = None
|
||||
self._prev_above: bool | None = None
|
||||
|
||||
def compute(self, close: pd.Series, high: pd.Series, low: pd.Series) -> pd.DataFrame:
|
||||
self._ind = pd.DataFrame({"ma": ma(close, self.period)})
|
||||
return self._ind
|
||||
|
||||
def on_bar(self, i: int, row: pd.Series, broker: PaperBroker) -> None:
|
||||
m = float(self._ind["ma"].iloc[i])
|
||||
price = float(row["close"])
|
||||
ts = row["ts"]
|
||||
above = price > m
|
||||
if self._prev_above is None or pd.isna(m):
|
||||
self._prev_above = above
|
||||
return
|
||||
|
||||
cross_up = above and not self._prev_above # 上穿
|
||||
cross_down = (not above) and self._prev_above # 下穿
|
||||
|
||||
if cross_up:
|
||||
broker.buy_max(ts, price)
|
||||
elif cross_down and broker.position > 0:
|
||||
broker.sell_all(ts, price)
|
||||
|
||||
self._prev_above = above
|
||||
42
backend/app/backtest/strategies/macd_cross.py
Normal file
42
backend/app/backtest/strategies/macd_cross.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""MACD 金叉死叉策略。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from ...indicators import macd
|
||||
from ..broker import PaperBroker
|
||||
from .base import Strategy
|
||||
|
||||
|
||||
class MACDCrossStrategy(Strategy):
|
||||
def __init__(self, fast: float = 12, slow: float = 26, signal: float = 9):
|
||||
self.fast = int(fast)
|
||||
self.slow = int(slow)
|
||||
self.signal = int(signal)
|
||||
self._ind: pd.DataFrame | None = None
|
||||
self._prev_dif: float | None = None
|
||||
self._prev_dea: float | None = None
|
||||
|
||||
def compute(self, close: pd.Series, high: pd.Series, low: pd.Series) -> pd.DataFrame:
|
||||
self._ind = macd(close, self.fast, self.slow, self.signal)
|
||||
return self._ind
|
||||
|
||||
def on_bar(self, i: int, row: pd.Series, broker: PaperBroker) -> None:
|
||||
dif = float(self._ind["macd"].iloc[i])
|
||||
dea = float(self._ind["signal"].iloc[i])
|
||||
|
||||
if self._prev_dif is None:
|
||||
self._prev_dif, self._prev_dea = dif, dea
|
||||
return
|
||||
|
||||
golden = self._prev_dif <= self._prev_dea and dif > dea # 金叉
|
||||
death = self._prev_dif >= self._prev_dea and dif < dea # 死叉
|
||||
|
||||
price = float(row["close"])
|
||||
ts = row["ts"]
|
||||
if golden and pd.notna(dif):
|
||||
broker.buy_max(ts, price)
|
||||
elif death and broker.position > 0 and pd.notna(dif):
|
||||
broker.sell_all(ts, price)
|
||||
|
||||
self._prev_dif, self._prev_dea = dif, dea
|
||||
Reference in New Issue
Block a user