加入一个回测,
This commit is contained in:
42500
bb_backtest_20250101_20251231_trades.csv
Normal file
42500
bb_backtest_20250101_20251231_trades.csv
Normal file
File diff suppressed because it is too large
Load Diff
7346
bb_backtest_20260101_20260228_trades.csv
Normal file
7346
bb_backtest_20260101_20260228_trades.csv
Normal file
File diff suppressed because it is too large
Load Diff
@@ -56,9 +56,10 @@ class BollingerBandBacktest:
|
|||||||
self.fee_rate = 0.0005 # 万五手续费
|
self.fee_rate = 0.0005 # 万五手续费
|
||||||
self.rebate_rate = 0.9 # 90%返佣
|
self.rebate_rate = 0.9 # 90%返佣
|
||||||
self.margin_ratio_1 = 0.01 # 首次开仓保证金比例1%
|
self.margin_ratio_1 = 0.01 # 首次开仓保证金比例1%
|
||||||
self.margin_ratio_2 = 0.02 # 加仓保证金比例2%
|
self.margin_ratio_2 = 0.01 # 加仓保证金比例1%
|
||||||
self.stop_loss_ratio = 0.5 # 止损比例50%
|
self.stop_loss_ratio = 0.5 # 止损比例50%
|
||||||
self.entry_slippage = 0.0002 # 开仓滑点(2bps)
|
self.entry_slippage = 0.0002 # 开仓滑点(2bps)
|
||||||
|
self.rebate_credit_hour = 8 # 次日早上8点返佣到账(上海时间)
|
||||||
|
|
||||||
# 账户状态
|
# 账户状态
|
||||||
self.capital = self.initial_capital
|
self.capital = self.initial_capital
|
||||||
@@ -81,6 +82,9 @@ class BollingerBandBacktest:
|
|||||||
# 交易记录
|
# 交易记录
|
||||||
self.trades = []
|
self.trades = []
|
||||||
self.daily_pnl = []
|
self.daily_pnl = []
|
||||||
|
self.pending_rebates = [] # 待到账返佣队列
|
||||||
|
self.total_rebate_credited = 0.0
|
||||||
|
self.current_run_label = "default"
|
||||||
|
|
||||||
def calculate_bollinger_bands(self, df):
|
def calculate_bollinger_bands(self, df):
|
||||||
"""计算布林带"""
|
"""计算布林带"""
|
||||||
@@ -91,9 +95,54 @@ class BollingerBandBacktest:
|
|||||||
df['middle'] = df['sma']
|
df['middle'] = df['sma']
|
||||||
return df
|
return df
|
||||||
|
|
||||||
def get_net_fee(self, fee):
|
def get_rebate_amount(self, fee):
|
||||||
"""计算扣除返佣后的净手续费"""
|
"""计算返佣金额"""
|
||||||
return fee * (1 - self.rebate_rate)
|
return fee * self.rebate_rate
|
||||||
|
|
||||||
|
def schedule_rebate(self, fee, timestamp):
|
||||||
|
"""登记返佣到账时间(次日08:00,上海时间)"""
|
||||||
|
rebate = self.get_rebate_amount(fee)
|
||||||
|
if rebate <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
trade_utc = pd.Timestamp(timestamp, tz='UTC')
|
||||||
|
trade_local = trade_utc.tz_convert('Asia/Shanghai')
|
||||||
|
credit_local = (trade_local + pd.Timedelta(days=1)).normalize() + pd.Timedelta(hours=self.rebate_credit_hour)
|
||||||
|
credit_utc = credit_local.tz_convert('UTC').tz_localize(None)
|
||||||
|
|
||||||
|
self.pending_rebates.append({
|
||||||
|
'credit_time': credit_utc,
|
||||||
|
'amount': rebate,
|
||||||
|
'trade_time': trade_utc.tz_localize(None),
|
||||||
|
})
|
||||||
|
|
||||||
|
def apply_pending_rebates(self, current_time):
|
||||||
|
"""处理当前时刻前应到账的返佣"""
|
||||||
|
if not self.pending_rebates:
|
||||||
|
return
|
||||||
|
|
||||||
|
remaining = []
|
||||||
|
for item in self.pending_rebates:
|
||||||
|
if item['credit_time'] <= current_time:
|
||||||
|
amount = item['amount']
|
||||||
|
self.capital += amount
|
||||||
|
self.total_rebate_credited += amount
|
||||||
|
self.trades.append({
|
||||||
|
'timestamp': current_time.strftime('%Y-%m-%d %H:%M'),
|
||||||
|
'action': '返佣到账',
|
||||||
|
'price': None,
|
||||||
|
'size': None,
|
||||||
|
'rebate': amount,
|
||||||
|
'capital': self.capital,
|
||||||
|
'reason': f"次日08:00返佣到账({item['trade_time'].strftime('%Y-%m-%d %H:%M')}手续费)"
|
||||||
|
})
|
||||||
|
logger.info(
|
||||||
|
f"[{current_time.strftime('%Y-%m-%d %H:%M')}] 返佣到账: {amount:.4f}U | 可用资金: {self.capital:.4f}U"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
remaining.append(item)
|
||||||
|
|
||||||
|
self.pending_rebates = remaining
|
||||||
|
|
||||||
def apply_entry_slippage(self, price, direction):
|
def apply_entry_slippage(self, price, direction):
|
||||||
"""按方向施加不利滑点"""
|
"""按方向施加不利滑点"""
|
||||||
@@ -305,8 +354,7 @@ class BollingerBandBacktest:
|
|||||||
|
|
||||||
position_size = margin * self.leverage / price
|
position_size = margin * self.leverage / price
|
||||||
fee = position_size * price * self.fee_rate
|
fee = position_size * price * self.fee_rate
|
||||||
net_fee = self.get_net_fee(fee)
|
required = margin + fee
|
||||||
required = margin + net_fee
|
|
||||||
if self.capital < required:
|
if self.capital < required:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"[{timestamp}] 可用资金不足,无法开仓 | 需要: {required:.4f}U | 可用: {self.capital:.4f}U"
|
f"[{timestamp}] 可用资金不足,无法开仓 | 需要: {required:.4f}U | 可用: {self.capital:.4f}U"
|
||||||
@@ -315,6 +363,7 @@ class BollingerBandBacktest:
|
|||||||
|
|
||||||
# 冻结保证金并扣除手续费
|
# 冻结保证金并扣除手续费
|
||||||
self.capital -= required
|
self.capital -= required
|
||||||
|
self.schedule_rebate(fee, timestamp)
|
||||||
|
|
||||||
if self.position_count == 0:
|
if self.position_count == 0:
|
||||||
self.position = position_size if direction == 'long' else -position_size
|
self.position = position_size if direction == 'long' else -position_size
|
||||||
@@ -341,14 +390,16 @@ class BollingerBandBacktest:
|
|||||||
'price': price,
|
'price': price,
|
||||||
'size': position_size,
|
'size': position_size,
|
||||||
'margin': margin,
|
'margin': margin,
|
||||||
'fee': net_fee,
|
'fee': fee,
|
||||||
|
'rebate': self.get_rebate_amount(fee),
|
||||||
'capital': self.capital,
|
'capital': self.capital,
|
||||||
'reason': reason
|
'reason': reason
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[{timestamp}] {action} @ {price:.2f} | 仓位: {position_size:.4f} | "
|
f"[{timestamp}] {action} @ {price:.2f} | 仓位: {position_size:.4f} | "
|
||||||
f"保证金: {margin:.4f}U | 手续费: {net_fee:.4f}U | 可用资金: {self.capital:.4f}U | {reason}"
|
f"保证金: {margin:.4f}U | 手续费: {fee:.4f}U | 返佣待到账: {self.get_rebate_amount(fee):.4f}U | "
|
||||||
|
f"可用资金: {self.capital:.4f}U | {reason}"
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -368,10 +419,10 @@ class BollingerBandBacktest:
|
|||||||
pnl = close_size * (self.entry_price - price)
|
pnl = close_size * (self.entry_price - price)
|
||||||
|
|
||||||
fee = close_size * price * self.fee_rate
|
fee = close_size * price * self.fee_rate
|
||||||
net_fee = self.get_net_fee(fee)
|
|
||||||
|
|
||||||
released_margin = self.total_margin * ratio
|
released_margin = self.total_margin * ratio
|
||||||
self.capital += released_margin + pnl - net_fee
|
self.capital += released_margin + pnl - fee
|
||||||
|
self.schedule_rebate(fee, timestamp)
|
||||||
|
|
||||||
if ratio >= 0.999:
|
if ratio >= 0.999:
|
||||||
self.position = 0
|
self.position = 0
|
||||||
@@ -397,13 +448,15 @@ class BollingerBandBacktest:
|
|||||||
'price': price,
|
'price': price,
|
||||||
'size': close_size,
|
'size': close_size,
|
||||||
'pnl': pnl,
|
'pnl': pnl,
|
||||||
'fee': net_fee,
|
'fee': fee,
|
||||||
|
'rebate': self.get_rebate_amount(fee),
|
||||||
'capital': self.capital,
|
'capital': self.capital,
|
||||||
'reason': reason
|
'reason': reason
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info(f"[{timestamp}] 平仓{int(ratio*100)}% @ {price:.2f} | "
|
logger.info(f"[{timestamp}] 平仓{int(ratio*100)}% @ {price:.2f} | "
|
||||||
f"盈亏: {pnl:.4f}U | 手续费: {net_fee:.4f}U | 可用资金: {self.capital:.4f}U | {reason}")
|
f"盈亏: {pnl:.4f}U | 手续费: {fee:.4f}U | 返佣待到账: {self.get_rebate_amount(fee):.4f}U | "
|
||||||
|
f"可用资金: {self.capital:.4f}U | {reason}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def check_stop_loss(self, high, low, timestamp):
|
def check_stop_loss(self, high, low, timestamp):
|
||||||
@@ -427,14 +480,34 @@ class BollingerBandBacktest:
|
|||||||
|
|
||||||
def run_backtest(self, start_date, end_date):
|
def run_backtest(self, start_date, end_date):
|
||||||
"""运行回测"""
|
"""运行回测"""
|
||||||
|
# 重置状态,支持同一实例重复回测
|
||||||
|
self.capital = self.initial_capital
|
||||||
|
self.position = 0
|
||||||
|
self.position_count = 0
|
||||||
|
self.entry_price = 0
|
||||||
|
self.total_margin = 0
|
||||||
|
self.mid_closed_half = False
|
||||||
|
self.trades = []
|
||||||
|
self.daily_pnl = []
|
||||||
|
self.pending_rebates = []
|
||||||
|
self.total_rebate_credited = 0.0
|
||||||
|
self.clear_delay_reversal()
|
||||||
|
|
||||||
logger.info(f"{'='*80}")
|
logger.info(f"{'='*80}")
|
||||||
logger.info(f"开始回测: {start_date} ~ {end_date}")
|
logger.info(f"开始回测: {start_date} ~ {end_date}")
|
||||||
logger.info(f"初始资金: {self.initial_capital}U | 杠杆: {self.leverage}x | BB({self.bb_period}, {self.bb_std})")
|
logger.info(f"初始资金: {self.initial_capital}U | 杠杆: {self.leverage}x | BB({self.bb_period}, {self.bb_std})")
|
||||||
logger.info(f"{'='*80}")
|
logger.info(f"{'='*80}")
|
||||||
|
|
||||||
# 从数据库加载数据
|
# 从数据库加载数据
|
||||||
start_ts = int(pd.Timestamp(start_date).timestamp() * 1000)
|
start_dt = pd.Timestamp(start_date)
|
||||||
end_ts = int(pd.Timestamp(end_date).timestamp() * 1000)
|
end_dt = pd.Timestamp(end_date)
|
||||||
|
if isinstance(end_date, str) and len(end_date) <= 10:
|
||||||
|
end_dt = end_dt + pd.Timedelta(days=1) - pd.Timedelta(milliseconds=1)
|
||||||
|
|
||||||
|
self.current_run_label = f"{start_dt.strftime('%Y%m%d')}_{end_dt.strftime('%Y%m%d')}"
|
||||||
|
|
||||||
|
start_ts = int(start_dt.timestamp() * 1000)
|
||||||
|
end_ts = int(end_dt.timestamp() * 1000)
|
||||||
|
|
||||||
query = BitMartETH5M.select().where(
|
query = BitMartETH5M.select().where(
|
||||||
(BitMartETH5M.id >= start_ts) & (BitMartETH5M.id <= end_ts)
|
(BitMartETH5M.id >= start_ts) & (BitMartETH5M.id <= end_ts)
|
||||||
@@ -481,6 +554,9 @@ class BollingerBandBacktest:
|
|||||||
lower = row['lower']
|
lower = row['lower']
|
||||||
middle = row['middle']
|
middle = row['middle']
|
||||||
|
|
||||||
|
# 先处理当前时刻返佣到账
|
||||||
|
self.apply_pending_rebates(row['datetime'])
|
||||||
|
|
||||||
# 检查止损
|
# 检查止损
|
||||||
if self.check_stop_loss(high, low, timestamp):
|
if self.check_stop_loss(high, low, timestamp):
|
||||||
continue
|
continue
|
||||||
@@ -579,6 +655,9 @@ class BollingerBandBacktest:
|
|||||||
|
|
||||||
total_pnl = sum([t.get('pnl', 0) for t in self.trades])
|
total_pnl = sum([t.get('pnl', 0) for t in self.trades])
|
||||||
total_fee = sum([t.get('fee', 0) for t in self.trades])
|
total_fee = sum([t.get('fee', 0) for t in self.trades])
|
||||||
|
total_rebate_expected = sum([t.get('rebate', 0) for t in self.trades if t.get('fee', 0) > 0])
|
||||||
|
pending_rebate = sum([x['amount'] for x in self.pending_rebates])
|
||||||
|
realized_net_fee = total_fee - self.total_rebate_credited
|
||||||
|
|
||||||
final_capital = self.capital
|
final_capital = self.capital
|
||||||
roi = (final_capital - self.initial_capital) / self.initial_capital * 100
|
roi = (final_capital - self.initial_capital) / self.initial_capital * 100
|
||||||
@@ -586,7 +665,11 @@ class BollingerBandBacktest:
|
|||||||
logger.info(f"初始资金: {self.initial_capital:.2f}U")
|
logger.info(f"初始资金: {self.initial_capital:.2f}U")
|
||||||
logger.info(f"最终资金: {final_capital:.2f}U")
|
logger.info(f"最终资金: {final_capital:.2f}U")
|
||||||
logger.info(f"总盈亏: {total_pnl:.2f}U")
|
logger.info(f"总盈亏: {total_pnl:.2f}U")
|
||||||
logger.info(f"总手续费: {total_fee:.2f}U")
|
logger.info(f"总手续费(开平全额): {total_fee:.2f}U")
|
||||||
|
logger.info(f"返佣应返总额: {total_rebate_expected:.2f}U")
|
||||||
|
logger.info(f"返佣已到账: {self.total_rebate_credited:.2f}U")
|
||||||
|
logger.info(f"返佣待到账: {pending_rebate:.2f}U")
|
||||||
|
logger.info(f"已实现净手续费: {realized_net_fee:.2f}U")
|
||||||
logger.info(f"净收益: {final_capital - self.initial_capital:.2f}U")
|
logger.info(f"净收益: {final_capital - self.initial_capital:.2f}U")
|
||||||
logger.info(f"收益率: {roi:.2f}%")
|
logger.info(f"收益率: {roi:.2f}%")
|
||||||
logger.info(f"总交易次数: {total_trades}")
|
logger.info(f"总交易次数: {total_trades}")
|
||||||
@@ -597,7 +680,7 @@ class BollingerBandBacktest:
|
|||||||
|
|
||||||
# 保存交易记录
|
# 保存交易记录
|
||||||
trades_df = pd.DataFrame(self.trades)
|
trades_df = pd.DataFrame(self.trades)
|
||||||
output_file = 'bb_backtest_march_2026_trades.csv'
|
output_file = f'bb_backtest_{self.current_run_label}_trades.csv'
|
||||||
trades_df.to_csv(output_file, index=False, encoding='utf-8-sig')
|
trades_df.to_csv(output_file, index=False, encoding='utf-8-sig')
|
||||||
logger.info(f"\n交易记录已保存到: {output_file}")
|
logger.info(f"\n交易记录已保存到: {output_file}")
|
||||||
|
|
||||||
@@ -606,10 +689,15 @@ class BollingerBandBacktest:
|
|||||||
'final_capital': final_capital,
|
'final_capital': final_capital,
|
||||||
'total_pnl': total_pnl,
|
'total_pnl': total_pnl,
|
||||||
'total_fee': total_fee,
|
'total_fee': total_fee,
|
||||||
|
'total_rebate_expected': total_rebate_expected,
|
||||||
|
'total_rebate_credited': self.total_rebate_credited,
|
||||||
|
'pending_rebate': pending_rebate,
|
||||||
|
'realized_net_fee': realized_net_fee,
|
||||||
'roi': roi,
|
'roi': roi,
|
||||||
'total_trades': total_trades,
|
'total_trades': total_trades,
|
||||||
'win_trades': win_trades,
|
'win_trades': win_trades,
|
||||||
'loss_trades': loss_trades,
|
'loss_trades': loss_trades,
|
||||||
|
'trades_file': output_file,
|
||||||
'trades': self.trades
|
'trades': self.trades
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7754
bb_sweep_results.csv
7754
bb_sweep_results.csv
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user