加入一个回测,
This commit is contained in:
@@ -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}%")
|
||||
|
||||
Reference in New Issue
Block a user