Remove deprecated modules and files related to the backtesting framework, including backtest.py, cli.py, config.py, data.py, intrabar.py, logging_utils.py, market_costs.py, metrics.py, trade.py, and supertrend indicators. Introduce a new structure for the backtesting engine with improved organization and functionality, including a CLI handler, data manager, and reporting capabilities. Update dependencies in pyproject.toml to support the new architecture.

This commit is contained in:
2026-01-12 21:11:39 +08:00
parent c4aa965a98
commit 44fac1ed25
37 changed files with 5253 additions and 393 deletions

View File

@@ -0,0 +1,6 @@
"""
Meta Supertrend strategy package.
"""
from .strategy import MetaSupertrendStrategy
__all__ = ['MetaSupertrendStrategy']

View File

@@ -0,0 +1,128 @@
"""
Supertrend indicators and helper functions.
"""
import numpy as np
import vectorbt as vbt
from numba import njit
# --- Numba Compiled Helper Functions ---
@njit(cache=False) # Disable cache to avoid stale compilation issues
def get_tr_nb(high, low, close):
"""Calculate True Range (Numba compiled)."""
# Ensure 1D arrays
high = high.ravel()
low = low.ravel()
close = close.ravel()
tr = np.empty_like(close)
tr[0] = high[0] - low[0]
for i in range(1, len(close)):
tr[i] = max(high[i] - low[i], abs(high[i] - close[i-1]), abs(low[i] - close[i-1]))
return tr
@njit(cache=False)
def get_atr_nb(high, low, close, period):
"""Calculate ATR using Wilder's Smoothing (Numba compiled)."""
# Ensure 1D arrays
high = high.ravel()
low = low.ravel()
close = close.ravel()
# Ensure period is native Python int (critical for Numba array indexing)
n = len(close)
p = int(period)
tr = get_tr_nb(high, low, close)
atr = np.full(n, np.nan, dtype=np.float64)
if n < p:
return atr
# Initial ATR is simple average of TR
sum_tr = 0.0
for i in range(p):
sum_tr += tr[i]
atr[p - 1] = sum_tr / p
# Subsequent ATR is Wilder's smoothed
for i in range(p, n):
atr[i] = (atr[i - 1] * (p - 1) + tr[i]) / p
return atr
@njit(cache=False)
def get_supertrend_nb(high, low, close, period, multiplier):
"""Calculate SuperTrend completely in Numba."""
# Ensure 1D arrays
high = high.ravel()
low = low.ravel()
close = close.ravel()
# Ensure params are native Python types (critical for Numba)
n = len(close)
p = int(period)
m = float(multiplier)
atr = get_atr_nb(high, low, close, p)
final_upper = np.full(n, np.nan, dtype=np.float64)
final_lower = np.full(n, np.nan, dtype=np.float64)
trend = np.ones(n, dtype=np.int8) # 1 Bull, -1 Bear
# Skip until we have valid ATR
start_idx = p
if start_idx >= n:
return trend
# Init first valid point
hl2 = (high[start_idx] + low[start_idx]) / 2
final_upper[start_idx] = hl2 + m * atr[start_idx]
final_lower[start_idx] = hl2 - m * atr[start_idx]
# Loop
for i in range(start_idx + 1, n):
cur_hl2 = (high[i] + low[i]) / 2
cur_atr = atr[i]
basic_upper = cur_hl2 + m * cur_atr
basic_lower = cur_hl2 - m * cur_atr
# Upper Band Logic
if basic_upper < final_upper[i-1] or close[i-1] > final_upper[i-1]:
final_upper[i] = basic_upper
else:
final_upper[i] = final_upper[i-1]
# Lower Band Logic
if basic_lower > final_lower[i-1] or close[i-1] < final_lower[i-1]:
final_lower[i] = basic_lower
else:
final_lower[i] = final_lower[i-1]
# Trend Logic
if trend[i-1] == 1:
if close[i] < final_lower[i-1]:
trend[i] = -1
else:
trend[i] = 1
else:
if close[i] > final_upper[i-1]:
trend[i] = 1
else:
trend[i] = -1
return trend
# --- VectorBT Indicator Factory ---
SuperTrendIndicator = vbt.IndicatorFactory(
class_name='SuperTrend',
short_name='st',
input_names=['high', 'low', 'close'],
param_names=['period', 'multiplier'],
output_names=['trend']
).from_apply_func(
get_supertrend_nb,
keep_pd=False, # Disable automatic Pandas wrapping of inputs
param_product=True # Enable Cartesian product for list params
)

View File

@@ -0,0 +1,142 @@
"""
Meta Supertrend strategy implementation.
"""
import numpy as np
import pandas as pd
from engine.market import MarketType
from strategies.base import BaseStrategy
from .indicators import SuperTrendIndicator
class MetaSupertrendStrategy(BaseStrategy):
"""
Meta Supertrend Strategy using 3 Supertrend indicators.
Enters long when all 3 Supertrends are bullish.
Enters short when all 3 Supertrends are bearish.
Designed for perpetual futures with leverage and short-selling support.
"""
# Market configuration
default_market_type = MarketType.PERPETUAL
default_leverage = 5
# Risk management parameters
default_sl_stop = 0.02 # 2% stop loss
default_sl_trail = True # Trailing stop enabled
default_exit_on_bearish_flip = False # Rely on SL/TP, not bearish flip
def run(
self,
close: pd.Series,
high: pd.Series = None,
low: pd.Series = None,
period1: int = 10,
multiplier1: float = 3.0,
period2: int = 11,
multiplier2: float = 2.0,
period3: int = 12,
multiplier3: float = 1.0,
exit_on_bearish_flip: bool = None,
enable_short: bool = True,
**kwargs
) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame]:
# 1. Validation & Setup
if exit_on_bearish_flip is None:
exit_on_bearish_flip = self.default_exit_on_bearish_flip
if high is None or low is None:
raise ValueError("MetaSupertrendStrategy requires High and Low prices.")
# 2. Calculate Supertrends
t1, t2, t3 = self._calculate_supertrends(
high, low, close,
period1, multiplier1,
period2, multiplier2,
period3, multiplier3
)
# 3. Meta Signals
bullish, bearish = self._calculate_meta_signals(t1, t2, t3, close)
# 4. Generate Entry/Exit Signals
return self._generate_signals(bullish, bearish, exit_on_bearish_flip, enable_short)
def _calculate_supertrends(
self, high, low, close, p1, m1, p2, m2, p3, m3
):
"""Run the 3 Supertrend indicators."""
# Pass NumPy arrays explicitly to avoid Numba typing errors
h_vals = high.values
l_vals = low.values
c_vals = close.values
def run_st(p, m):
st = SuperTrendIndicator.run(h_vals, l_vals, c_vals, period=p, multiplier=m)
trend = st.trend
if isinstance(trend, pd.DataFrame):
trend.index = close.index
if trend.shape[1] == 1:
trend = trend.iloc[:, 0]
elif isinstance(trend, pd.Series):
trend.index = close.index
return trend
t1 = run_st(p1, m1)
t2 = run_st(p2, m2)
t3 = run_st(p3, m3)
return t1, t2, t3
def _calculate_meta_signals(self, t1, t2, t3, close_series):
"""Combine 3 Supertrends into boolean Bullish/Bearish signals."""
# Use NumPy broadcasting
t1_vals = t1.values if isinstance(t1, pd.DataFrame) else t1.values.reshape(-1, 1)
# Force column vectors for broadcasting if scalar result
t2_vals = t2.values.reshape(-1, 1)
t3_vals = t3.values.reshape(-1, 1)
# Boolean logic on numpy arrays (1 = Bull, -1 = Bear)
bullish_vals = (t1_vals == 1) & (t2_vals == 1) & (t3_vals == 1)
bearish_vals = (t1_vals == -1) & (t2_vals == -1) & (t3_vals == -1)
# Reconstruct Pandas objects
if isinstance(t1, pd.DataFrame):
bullish = pd.DataFrame(bullish_vals, index=t1.index, columns=t1.columns)
bearish = pd.DataFrame(bearish_vals, index=t1.index, columns=t1.columns)
else:
bullish = pd.Series(bullish_vals.flatten(), index=t1.index)
bearish = pd.Series(bearish_vals.flatten(), index=t1.index)
return bullish, bearish
def _generate_signals(
self, bullish, bearish, exit_on_bearish_flip, enable_short
):
"""Generate long/short entry/exit signals based on meta trend."""
# Long Entries: Change from Not Bullish to Bullish
prev_bullish = bullish.shift(1).fillna(False)
long_entries = bullish & (~prev_bullish)
# Long Exits
if exit_on_bearish_flip:
prev_bearish = bearish.shift(1).fillna(False)
long_exits = bearish & (~prev_bearish)
else:
long_exits = BaseStrategy.create_empty_signals(long_entries)
# Short signals
if enable_short:
prev_bearish = bearish.shift(1).fillna(False)
short_entries = bearish & (~prev_bearish)
if exit_on_bearish_flip:
short_exits = bullish & (~prev_bullish)
else:
short_exits = BaseStrategy.create_empty_signals(long_entries)
else:
short_entries = BaseStrategy.create_empty_signals(long_entries)
short_exits = BaseStrategy.create_empty_signals(long_entries)
return long_entries, long_exits, short_entries, short_exits