加入一个回测,

This commit is contained in:
ddrwode
2026-03-05 12:51:12 +08:00
parent b74449989b
commit 01b6a0fdcb
17 changed files with 189157 additions and 29751 deletions

View File

@@ -87,12 +87,12 @@ class BollingerBandBacktest:
self.current_run_label = "default"
def calculate_bollinger_bands(self, df):
"""计算布林带"""
"""计算布林带整体右移1根避免使用当前K收盘价"""
df['sma'] = df['close'].rolling(window=self.bb_period).mean()
df['std'] = df['close'].rolling(window=self.bb_period).std()
df['upper'] = df['sma'] + self.bb_std * df['std']
df['lower'] = df['sma'] - self.bb_std * df['std']
df['middle'] = df['sma']
df['upper'] = (df['sma'] + self.bb_std * df['std']).shift(1)
df['lower'] = (df['sma'] - self.bb_std * df['std']).shift(1)
df['middle'] = df['sma'].shift(1)
return df
def get_rebate_amount(self, fee):
@@ -288,52 +288,54 @@ class BollingerBandBacktest:
self.clear_delay_reversal()
return True
def check_and_execute_delay_reversal(self, i, row, prev_row, timestamp):
"""执行延迟反转三段判断逻辑"""
def check_delay_reversal_signal(self, i, row, prev_row):
"""检查延迟反转是否在当前收盘K成立仅返回信号不直接执行"""
if self.position == 0 or self.delay_reverse_price is None or self.delay_reverse_kline_index is None:
return False
return None
offset = i - self.delay_reverse_kline_index
# 禁止同K确认最早次K确认
if offset <= 0:
return None
high = row['high']
low = row['low']
if self.delay_reverse_type == 'long_to_short':
# 情况1/2:触上轨后,同K或下一K回调到记录上轨价
if offset <= 1 and low <= self.delay_reverse_price:
tag = "K回调确认" if offset == 0 else "次K回调确认"
return self.reverse_position(self.delay_reverse_price, 'short', timestamp, f"延迟反转-{tag}")
# 情况1触上轨后K回调到记录上轨价
if offset == 1 and low <= self.delay_reverse_price:
return 'short', "延迟反转-次K回调确认", self.delay_reverse_price
# 情况3持续等待动态追踪上一根K线条件
# 情况2持续等待动态追踪上一根K线条件
if offset >= 2 and prev_row is not None:
prev_touch_upper = prev_row['high'] >= prev_row['upper']
prev_upper = prev_row['upper']
prev_touch_upper = pd.notna(prev_upper) and prev_row['high'] >= prev_upper
if prev_touch_upper:
ref_price = prev_row['upper']
if low <= ref_price:
return self.reverse_position(ref_price, 'short', timestamp, "延迟反转-上一根触上轨后回调确认")
if low <= prev_upper:
return 'short', "延迟反转-上一根触上轨后回调确认", prev_upper
else:
prev_body_low = min(prev_row['open'], prev_row['close'])
if low <= prev_body_low:
return self.reverse_position(prev_body_low, 'short', timestamp, "延迟反转-跌破上一根实体确认")
return 'short', "延迟反转-跌破上一根实体确认", prev_body_low
elif self.delay_reverse_type == 'short_to_long':
# 情况1/2:触下轨后,同K或下一K反弹到记录下轨价
if offset <= 1 and high >= self.delay_reverse_price:
tag = "K反弹确认" if offset == 0 else "次K反弹确认"
return self.reverse_position(self.delay_reverse_price, 'long', timestamp, f"延迟反转-{tag}")
# 情况1触下轨后K反弹到记录下轨价
if offset == 1 and high >= self.delay_reverse_price:
return 'long', "延迟反转-次K反弹确认", self.delay_reverse_price
# 情况3持续等待动态追踪上一根K线条件
# 情况2持续等待动态追踪上一根K线条件
if offset >= 2 and prev_row is not None:
prev_touch_lower = prev_row['low'] <= prev_row['lower']
prev_lower = prev_row['lower']
prev_touch_lower = pd.notna(prev_lower) and prev_row['low'] <= prev_lower
if prev_touch_lower:
ref_price = prev_row['lower']
if high >= ref_price:
return self.reverse_position(ref_price, 'long', timestamp, "延迟反转-上一根触下轨后反弹确认")
if high >= prev_lower:
return 'long', "延迟反转-上一根触下轨后反弹确认", prev_lower
else:
prev_body_high = max(prev_row['open'], prev_row['close'])
if high >= prev_body_high:
return self.reverse_position(prev_body_high, 'long', timestamp, "延迟反转-突破上一根实体确认")
return 'long', "延迟反转-突破上一根实体确认", prev_body_high
return False
return None
def open_position(self, price, direction, timestamp, reason):
"""开仓或加仓"""
@@ -459,8 +461,8 @@ class BollingerBandBacktest:
f"可用资金: {self.capital:.4f}U | {reason}")
return True
def check_stop_loss(self, high, low, timestamp):
"""检查止损"""
def check_stop_loss(self, high, low):
"""检查当前收盘K是否触发止损信号"""
if self.position == 0:
return False
@@ -470,16 +472,10 @@ class BollingerBandBacktest:
else:
unrealized_pnl = abs(self.position) * (self.entry_price - stop_price)
# 止损条件亏损达到保证金的50%
if unrealized_pnl <= -self.total_margin * self.stop_loss_ratio:
logger.warning(f"触发止损!浮亏: {unrealized_pnl:.2f}U | 保证金: {self.total_margin:.2f}U")
self.close_position(stop_price, 1.0, timestamp, "止损")
return True
return False
return unrealized_pnl <= -self.total_margin * self.stop_loss_ratio
def run_backtest(self, start_date, end_date):
"""运行回测"""
"""运行回测开仓当K触发平仓/止损仍按下一根K开盘执行"""
# 重置状态,支持同一实例重复回测
self.capital = self.initial_capital
self.position = 0
@@ -533,67 +529,102 @@ class BollingerBandBacktest:
logger.info(f"加载数据: {len(df)} 根K线")
logger.info(f"时间范围: {df['datetime'].min()} ~ {df['datetime'].max()}")
# 加载对应时间范围的1分钟数据用于中轨平半确认
one_min_start = int(df['timestamp'].min())
one_min_end = int(df['timestamp'].max()) + 300000
self.load_one_minute_cache(one_min_start, one_min_end)
# 计算布林带
df = self.calculate_bollinger_bands(df)
# 逐根K线回测
for i in range(self.bb_period, len(df)):
if len(df) <= self.bb_period + 1:
logger.error("数据不足,无法执行回测")
return None
# 逐根K线回测开仓当K触发平仓/止损下一K开盘执行
for i in range(self.bb_period, len(df) - 1):
row = df.iloc[i]
prev_row = df.iloc[i-1] if i > 0 else None
timestamp = row['datetime'].strftime('%Y-%m-%d %H:%M')
next_row = df.iloc[i + 1]
signal_dt = row['datetime']
signal_ts = signal_dt.strftime('%Y-%m-%d %H:%M')
five_min_ts = int(row['timestamp'])
execute_ts = next_row['datetime'].strftime('%Y-%m-%d %H:%M')
next_open = float(next_row['open']) if pd.notna(next_row['open']) else None
high = row['high']
low = row['low']
upper = row['upper']
lower = row['lower']
middle = row['middle']
# 先处理当前时刻返佣到账
self.apply_pending_rebates(row['datetime'])
# 检查止损
if self.check_stop_loss(high, low, timestamp):
# 先处理当前收盘时刻返佣到账
self.apply_pending_rebates(signal_dt)
if pd.isna(upper) or pd.isna(lower) or pd.isna(middle):
continue
# 已处于延迟反转状态时,先执行确认逻辑
if next_open is None:
continue
# 检查止损收盘确认下一K开盘平仓
if self.check_stop_loss(high, low):
logger.warning(f"[{signal_ts}] 触发止损信号下一K开盘执行")
self.close_position(next_open, 1.0, execute_ts, f"止损-收盘确认({signal_ts})")
continue
# 已处于延迟反转状态时,先检查确认逻辑
if self.delay_reverse_price is not None:
if self.check_and_execute_delay_reversal(i, row, prev_row, timestamp):
reversal_signal = self.check_delay_reversal_signal(i, row, prev_row)
if reversal_signal is not None and self.position != 0:
new_direction, reason, reversal_price = reversal_signal
if reversal_price is None or pd.isna(reversal_price):
reversal_price = float(row['close'])
close_side = "" if self.position > 0 else ""
open_side = "" if new_direction == 'long' else ""
self.close_position(
reversal_price,
1.0,
signal_ts,
f"{reason}-当K确认({signal_ts})-平{close_side}"
)
open_price = self.apply_entry_slippage(reversal_price, new_direction)
self.open_position(
open_price,
new_direction,
signal_ts,
f"{reason}-当K确认({signal_ts})-开{open_side}"
)
self.mid_closed_half = False
self.clear_delay_reversal()
continue
# === 中轨平仓逻辑 ===
if self.position != 0:
had_mid_closed_half = self.mid_closed_half
if self.position > 0: # 多仓
# 1m顺序确认先上方再回踩中轨平50%
if not self.mid_closed_half and low <= middle <= high:
should_close_half, reason_1m = self.should_close_half_by_1m_trend(
five_min_ts, middle, 'long'
)
if should_close_half:
self.close_position(middle, 0.5, timestamp, f"触中轨平50%-{reason_1m}")
self.mid_closed_half = True
# 继续回落到开仓均价全平
if self.position != 0 and self.mid_closed_half and low <= self.entry_price:
self.close_position(self.entry_price, 1.0, timestamp, "回开仓价全平")
# 回到开仓价全平+反手(仅在此前已平半的前提下触发)
if had_mid_closed_half and low <= self.entry_price:
self.close_position(next_open, 1.0, execute_ts, f"回开仓价全平-收盘确认({signal_ts})")
entry_price = self.apply_entry_slippage(next_open, 'short')
self.open_position(entry_price, 'short', execute_ts, f"回开仓价反手开空-收盘确认({signal_ts})")
self.mid_closed_half = False
continue
# 触中轨平半(收盘确认)
if not had_mid_closed_half and low <= middle <= high:
self.close_position(next_open, 0.5, execute_ts, f"触中轨平50%-收盘确认({signal_ts})")
self.mid_closed_half = True
continue
else: # 空仓
# 1m顺序确认先下方再反抽中轨平50%
if not self.mid_closed_half and low <= middle <= high:
should_close_half, reason_1m = self.should_close_half_by_1m_trend(
five_min_ts, middle, 'short'
)
if should_close_half:
self.close_position(middle, 0.5, timestamp, f"触中轨平50%-{reason_1m}")
self.mid_closed_half = True
# 继续反弹到开仓均价全平
if self.position != 0 and self.mid_closed_half and high >= self.entry_price:
self.close_position(self.entry_price, 1.0, timestamp, "回开仓价全平")
# 回到开仓价全平+反手(仅在此前已平半的前提下触发)
if had_mid_closed_half and high >= self.entry_price:
self.close_position(next_open, 1.0, execute_ts, f"回开仓价全平-收盘确认({signal_ts})")
entry_price = self.apply_entry_slippage(next_open, 'long')
self.open_position(entry_price, 'long', execute_ts, f"回开仓价反手开多-收盘确认({signal_ts})")
self.mid_closed_half = False
continue
# 触中轨平半(收盘确认)
if not had_mid_closed_half and low <= middle <= high:
self.close_position(next_open, 0.5, execute_ts, f"触中轨平50%-收盘确认({signal_ts})")
self.mid_closed_half = True
continue
# === 开仓与加仓逻辑 ===
if self.position == 0: # 空仓
self.clear_delay_reversal()
@@ -601,24 +632,22 @@ class BollingerBandBacktest:
# 触上轨开空
if high >= upper:
entry_price = self.get_touch_entry_price(five_min_ts, 'short', upper)
self.open_position(entry_price, 'short', timestamp, "触上轨开空")
self.open_position(entry_price, 'short', signal_ts, f"触上轨开空-当K触发({signal_ts})")
# 触下轨开多
elif low <= lower:
entry_price = self.get_touch_entry_price(five_min_ts, 'long', lower)
self.open_position(entry_price, 'long', timestamp, "触下轨开多")
self.open_position(entry_price, 'long', signal_ts, f"触下轨开多-当K触发({signal_ts})")
continue
# 有持仓:先检查是否触发延迟反转(核心)
if self.position > 0 and high >= upper:
self.mark_delay_reversal('long_to_short', upper, i, timestamp)
if self.check_and_execute_delay_reversal(i, row, prev_row, timestamp):
continue
self.mark_delay_reversal('long_to_short', upper, i, signal_ts)
continue
elif self.position < 0 and low <= lower:
self.mark_delay_reversal('short_to_long', lower, i, timestamp)
if self.check_and_execute_delay_reversal(i, row, prev_row, timestamp):
continue
self.mark_delay_reversal('short_to_long', lower, i, signal_ts)
continue
# 进入延迟反转等待后,不再执行加仓
if self.delay_reverse_price is not None:
@@ -628,11 +657,14 @@ class BollingerBandBacktest:
if self.position_count == 1:
if self.position > 0 and low <= lower:
entry_price = self.get_touch_entry_price(five_min_ts, 'long', lower)
self.open_position(entry_price, 'long', timestamp, "触下轨加多")
self.open_position(entry_price, 'long', signal_ts, f"触下轨加多-当K触发({signal_ts})")
elif self.position < 0 and high >= upper:
entry_price = self.get_touch_entry_price(five_min_ts, 'short', upper)
self.open_position(entry_price, 'short', timestamp, "触上轨加空")
self.open_position(entry_price, 'short', signal_ts, f"触上轨加空-当K触发({signal_ts})")
# 回测末尾再处理一次返佣到账
self.apply_pending_rebates(df.iloc[-1]['datetime'])
# 最后平仓
if self.position != 0:
final_price = df.iloc[-1]['close']
@@ -680,7 +712,9 @@ class BollingerBandBacktest:
# 保存交易记录
trades_df = pd.DataFrame(self.trades)
output_file = f'bb_backtest_{self.current_run_label}_trades.csv'
output_dir = Path(__file__).parent / 'backtest_outputs' / 'trades'
output_dir.mkdir(parents=True, exist_ok=True)
output_file = output_dir / f'bb_backtest_{self.current_run_label}_trades.csv'
trades_df.to_csv(output_file, index=False, encoding='utf-8-sig')
logger.info(f"\n交易记录已保存到: {output_file}")
@@ -697,7 +731,7 @@ class BollingerBandBacktest:
'total_trades': total_trades,
'win_trades': win_trades,
'loss_trades': loss_trades,
'trades_file': output_file,
'trades_file': str(output_file),
'trades': self.trades
}
@@ -710,8 +744,8 @@ if __name__ == '__main__':
# 创建回测实例
backtest = BollingerBandBacktest()
# 运行回测2026年3月
result = backtest.run_backtest('2026-03-01', '2026-03-31')
# 运行回测2026年全年
result = backtest.run_backtest('2026-01-01', '2026-12-31')
if result:
logger.success(f"\n回测完成!最终收益率: {result['roi']:.2f}%")