加入一个回测,

This commit is contained in:
ddrwode
2026-03-04 18:02:02 +08:00
parent de22d1f3ae
commit 4b5a66d588
5 changed files with 60414 additions and 16 deletions

2710
1.html Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -56,9 +56,10 @@ class BollingerBandBacktest:
self.fee_rate = 0.0005 # 万五手续费
self.rebate_rate = 0.9 # 90%返佣
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.entry_slippage = 0.0002 # 开仓滑点2bps
self.rebate_credit_hour = 8 # 次日早上8点返佣到账上海时间
# 账户状态
self.capital = self.initial_capital
@@ -81,6 +82,9 @@ class BollingerBandBacktest:
# 交易记录
self.trades = []
self.daily_pnl = []
self.pending_rebates = [] # 待到账返佣队列
self.total_rebate_credited = 0.0
self.current_run_label = "default"
def calculate_bollinger_bands(self, df):
"""计算布林带"""
@@ -91,9 +95,54 @@ class BollingerBandBacktest:
df['middle'] = df['sma']
return df
def get_net_fee(self, fee):
"""计算扣除返佣后的净手续费"""
return fee * (1 - self.rebate_rate)
def get_rebate_amount(self, fee):
"""计算返佣金额"""
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):
"""按方向施加不利滑点"""
@@ -305,8 +354,7 @@ class BollingerBandBacktest:
position_size = margin * self.leverage / price
fee = position_size * price * self.fee_rate
net_fee = self.get_net_fee(fee)
required = margin + net_fee
required = margin + fee
if self.capital < required:
logger.warning(
f"[{timestamp}] 可用资金不足,无法开仓 | 需要: {required:.4f}U | 可用: {self.capital:.4f}U"
@@ -315,6 +363,7 @@ class BollingerBandBacktest:
# 冻结保证金并扣除手续费
self.capital -= required
self.schedule_rebate(fee, timestamp)
if self.position_count == 0:
self.position = position_size if direction == 'long' else -position_size
@@ -341,14 +390,16 @@ class BollingerBandBacktest:
'price': price,
'size': position_size,
'margin': margin,
'fee': net_fee,
'fee': fee,
'rebate': self.get_rebate_amount(fee),
'capital': self.capital,
'reason': reason
})
logger.info(
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
@@ -368,10 +419,10 @@ class BollingerBandBacktest:
pnl = close_size * (self.entry_price - price)
fee = close_size * price * self.fee_rate
net_fee = self.get_net_fee(fee)
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:
self.position = 0
@@ -397,13 +448,15 @@ class BollingerBandBacktest:
'price': price,
'size': close_size,
'pnl': pnl,
'fee': net_fee,
'fee': fee,
'rebate': self.get_rebate_amount(fee),
'capital': self.capital,
'reason': reason
})
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
def check_stop_loss(self, high, low, timestamp):
@@ -427,14 +480,34 @@ class BollingerBandBacktest:
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"开始回测: {start_date} ~ {end_date}")
logger.info(f"初始资金: {self.initial_capital}U | 杠杆: {self.leverage}x | BB({self.bb_period}, {self.bb_std})")
logger.info(f"{'='*80}")
# 从数据库加载数据
start_ts = int(pd.Timestamp(start_date).timestamp() * 1000)
end_ts = int(pd.Timestamp(end_date).timestamp() * 1000)
start_dt = pd.Timestamp(start_date)
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(
(BitMartETH5M.id >= start_ts) & (BitMartETH5M.id <= end_ts)
@@ -481,6 +554,9 @@ class BollingerBandBacktest:
lower = row['lower']
middle = row['middle']
# 先处理当前时刻返佣到账
self.apply_pending_rebates(row['datetime'])
# 检查止损
if self.check_stop_loss(high, low, timestamp):
continue
@@ -579,6 +655,9 @@ class BollingerBandBacktest:
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_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
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"最终资金: {final_capital:.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"收益率: {roi:.2f}%")
logger.info(f"总交易次数: {total_trades}")
@@ -597,7 +680,7 @@ class BollingerBandBacktest:
# 保存交易记录
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')
logger.info(f"\n交易记录已保存到: {output_file}")
@@ -606,10 +689,15 @@ class BollingerBandBacktest:
'final_capital': final_capital,
'total_pnl': total_pnl,
'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,
'total_trades': total_trades,
'win_trades': win_trades,
'loss_trades': loss_trades,
'trades_file': output_file,
'trades': self.trades
}

File diff suppressed because it is too large Load Diff