加入一个回测,
This commit is contained in:
632
bb_backtest_march_2026.py
Normal file
632
bb_backtest_march_2026.py
Normal file
@@ -0,0 +1,632 @@
|
|||||||
|
"""
|
||||||
|
布林带延迟反转策略回测 - 2026年3月
|
||||||
|
策略规则:
|
||||||
|
1. 5分钟K线,BB(10, 2.5)
|
||||||
|
2. 空仓触上轨开空,触下轨开多
|
||||||
|
3. 同向加仓最多1次,保证金递增(1%->2%)
|
||||||
|
4. 延迟反转:触轨不立刻平仓,记录价格,回调到该价再平仓+反向开仓
|
||||||
|
5. 中轨平半+回开仓价全平
|
||||||
|
6. 止损:亏损达保证金50%
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from pathlib import Path
|
||||||
|
from peewee import *
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
# 数据库配置
|
||||||
|
DB_PATH = Path(__file__).parent / 'models' / 'database.db'
|
||||||
|
db = SqliteDatabase(str(DB_PATH))
|
||||||
|
|
||||||
|
class BitMartETH5M(Model):
|
||||||
|
"""5分钟K线模型"""
|
||||||
|
id = BigIntegerField(primary_key=True)
|
||||||
|
open = FloatField(null=True)
|
||||||
|
high = FloatField(null=True)
|
||||||
|
low = FloatField(null=True)
|
||||||
|
close = FloatField(null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
database = db
|
||||||
|
table_name = 'bitmart_eth_5m'
|
||||||
|
|
||||||
|
|
||||||
|
class BitMartETH1M(Model):
|
||||||
|
"""1分钟K线模型"""
|
||||||
|
id = BigIntegerField(primary_key=True)
|
||||||
|
open = FloatField(null=True)
|
||||||
|
high = FloatField(null=True)
|
||||||
|
low = FloatField(null=True)
|
||||||
|
close = FloatField(null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
database = db
|
||||||
|
table_name = 'bitmart_eth_1m'
|
||||||
|
|
||||||
|
|
||||||
|
class BollingerBandBacktest:
|
||||||
|
"""布林带延迟反转策略回测"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# 策略参数
|
||||||
|
self.bb_period = 10
|
||||||
|
self.bb_std = 2.5
|
||||||
|
self.initial_capital = 100 # 初始本金100U
|
||||||
|
self.leverage = 50 # 50倍杠杆
|
||||||
|
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.stop_loss_ratio = 0.5 # 止损比例50%
|
||||||
|
self.entry_slippage = 0.0002 # 开仓滑点(2bps)
|
||||||
|
|
||||||
|
# 账户状态
|
||||||
|
self.capital = self.initial_capital
|
||||||
|
self.position = 0 # 持仓量(正=多,负=空)
|
||||||
|
self.position_count = 0 # 持仓次数(0=空仓,1=首次,2=加仓)
|
||||||
|
self.entry_price = 0 # 开仓均价
|
||||||
|
self.total_margin = 0 # 总保证金
|
||||||
|
|
||||||
|
# 延迟反转状态
|
||||||
|
self.delay_reverse_price = None # 记录触碰轨道时的轨道价格
|
||||||
|
self.delay_reverse_type = None # 'long_to_short' 或 'short_to_long'
|
||||||
|
self.delay_reverse_kline_index = None # 触发延迟反转的K线索引
|
||||||
|
|
||||||
|
# 中轨平仓记录
|
||||||
|
self.mid_closed_half = False # 是否已平50%
|
||||||
|
|
||||||
|
# 1分钟K线缓存(key=5分钟起始时间戳, value=该5分钟内的1m列表)
|
||||||
|
self.one_minute_by_5m = {}
|
||||||
|
|
||||||
|
# 交易记录
|
||||||
|
self.trades = []
|
||||||
|
self.daily_pnl = []
|
||||||
|
|
||||||
|
def calculate_bollinger_bands(self, df):
|
||||||
|
"""计算布林带"""
|
||||||
|
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']
|
||||||
|
return df
|
||||||
|
|
||||||
|
def get_net_fee(self, fee):
|
||||||
|
"""计算扣除返佣后的净手续费"""
|
||||||
|
return fee * (1 - self.rebate_rate)
|
||||||
|
|
||||||
|
def apply_entry_slippage(self, price, direction):
|
||||||
|
"""按方向施加不利滑点"""
|
||||||
|
if direction == 'long':
|
||||||
|
return price * (1 + self.entry_slippage)
|
||||||
|
return price * (1 - self.entry_slippage)
|
||||||
|
|
||||||
|
def load_one_minute_cache(self, start_ts, end_ts):
|
||||||
|
"""加载并缓存1分钟K线,用于5分钟内走势确认"""
|
||||||
|
query = BitMartETH1M.select().where(
|
||||||
|
(BitMartETH1M.id >= start_ts) & (BitMartETH1M.id <= end_ts)
|
||||||
|
).order_by(BitMartETH1M.id)
|
||||||
|
|
||||||
|
bucket = {}
|
||||||
|
for row in query:
|
||||||
|
five_min_start = int(row.id - (row.id % 300000))
|
||||||
|
bucket.setdefault(five_min_start, []).append({
|
||||||
|
'timestamp': int(row.id),
|
||||||
|
'open': float(row.open) if row.open is not None else None,
|
||||||
|
'high': float(row.high) if row.high is not None else None,
|
||||||
|
'low': float(row.low) if row.low is not None else None,
|
||||||
|
'close': float(row.close) if row.close is not None else None,
|
||||||
|
})
|
||||||
|
|
||||||
|
self.one_minute_by_5m = bucket
|
||||||
|
total_rows = sum(len(v) for v in bucket.values())
|
||||||
|
logger.info(f"加载1分钟数据: {total_rows} 根 | 5分钟桶: {len(bucket)}")
|
||||||
|
|
||||||
|
def should_close_half_by_1m_trend(self, five_min_ts, middle_price, side):
|
||||||
|
"""
|
||||||
|
1分钟顺序确认中轨平半:
|
||||||
|
- 多仓:先到中轨上方,再回踩中轨
|
||||||
|
- 空仓:先到中轨下方,再反抽中轨
|
||||||
|
"""
|
||||||
|
minute_rows = self.one_minute_by_5m.get(five_min_ts, [])
|
||||||
|
if not minute_rows:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
seen_above_middle = False
|
||||||
|
seen_below_middle = False
|
||||||
|
|
||||||
|
for minute in minute_rows:
|
||||||
|
m_open = minute['open']
|
||||||
|
m_high = minute['high']
|
||||||
|
m_low = minute['low']
|
||||||
|
m_close = minute['close']
|
||||||
|
|
||||||
|
if None in (m_open, m_high, m_low, m_close):
|
||||||
|
continue
|
||||||
|
|
||||||
|
minute_time = pd.to_datetime(minute['timestamp'], unit='ms').strftime('%H:%M')
|
||||||
|
|
||||||
|
if side == 'long':
|
||||||
|
# 当前1m开在中轨上方且回踩到中轨,视为有效回踩
|
||||||
|
if m_open >= middle_price and m_low <= middle_price:
|
||||||
|
return True, f"1m({minute_time})回踩中轨"
|
||||||
|
|
||||||
|
# 已经到过中轨上方后,再次触及中轨
|
||||||
|
if seen_above_middle and m_low <= middle_price:
|
||||||
|
return True, f"1m({minute_time})回踩中轨"
|
||||||
|
|
||||||
|
if m_high >= middle_price:
|
||||||
|
seen_above_middle = True
|
||||||
|
|
||||||
|
else: # short
|
||||||
|
# 当前1m开在中轨下方且反抽到中轨,视为有效反抽
|
||||||
|
if m_open <= middle_price and m_high >= middle_price:
|
||||||
|
return True, f"1m({minute_time})反抽中轨"
|
||||||
|
|
||||||
|
# 已经到过中轨下方后,再次触及中轨
|
||||||
|
if seen_below_middle and m_high >= middle_price:
|
||||||
|
return True, f"1m({minute_time})反抽中轨"
|
||||||
|
|
||||||
|
if m_low <= middle_price:
|
||||||
|
seen_below_middle = True
|
||||||
|
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
def get_touch_entry_price(self, five_min_ts, direction, touch_price):
|
||||||
|
"""
|
||||||
|
触轨开仓的保守成交价:
|
||||||
|
- 在该5分钟内找到首个触轨1m
|
||||||
|
- 用该1m收盘价作为基准
|
||||||
|
- 为避免“过于理想”,不允许优于触轨价,再叠加不利滑点
|
||||||
|
"""
|
||||||
|
minute_rows = self.one_minute_by_5m.get(five_min_ts, [])
|
||||||
|
if not minute_rows:
|
||||||
|
return self.apply_entry_slippage(touch_price, direction)
|
||||||
|
|
||||||
|
trigger_close = None
|
||||||
|
for minute in minute_rows:
|
||||||
|
m_high = minute['high']
|
||||||
|
m_low = minute['low']
|
||||||
|
m_close = minute['close']
|
||||||
|
if None in (m_high, m_low, m_close):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if direction == 'short' and m_high >= touch_price:
|
||||||
|
trigger_close = m_close
|
||||||
|
break
|
||||||
|
if direction == 'long' and m_low <= touch_price:
|
||||||
|
trigger_close = m_close
|
||||||
|
break
|
||||||
|
|
||||||
|
if trigger_close is None:
|
||||||
|
base_price = touch_price
|
||||||
|
elif direction == 'long':
|
||||||
|
base_price = max(touch_price, trigger_close)
|
||||||
|
else:
|
||||||
|
base_price = min(touch_price, trigger_close)
|
||||||
|
|
||||||
|
return self.apply_entry_slippage(base_price, direction)
|
||||||
|
|
||||||
|
def clear_delay_reversal(self):
|
||||||
|
"""清理延迟反转状态"""
|
||||||
|
self.delay_reverse_price = None
|
||||||
|
self.delay_reverse_type = None
|
||||||
|
self.delay_reverse_kline_index = None
|
||||||
|
|
||||||
|
def mark_delay_reversal(self, reverse_type, trigger_price, kline_index, timestamp):
|
||||||
|
"""记录延迟反转触发信息"""
|
||||||
|
self.delay_reverse_type = reverse_type
|
||||||
|
self.delay_reverse_price = trigger_price
|
||||||
|
self.delay_reverse_kline_index = kline_index
|
||||||
|
|
||||||
|
if reverse_type == 'long_to_short':
|
||||||
|
logger.info(f"[{timestamp}] 多仓触上轨 @ {trigger_price:.2f},进入延迟反转")
|
||||||
|
else:
|
||||||
|
logger.info(f"[{timestamp}] 空仓触下轨 @ {trigger_price:.2f},进入延迟反转")
|
||||||
|
|
||||||
|
def reverse_position(self, price, new_direction, timestamp, reason):
|
||||||
|
"""全平后反向开仓"""
|
||||||
|
if self.position == 0:
|
||||||
|
self.clear_delay_reversal()
|
||||||
|
return False
|
||||||
|
|
||||||
|
close_side = "多" if self.position > 0 else "空"
|
||||||
|
open_side = "多" if new_direction == "long" else "空"
|
||||||
|
self.close_position(price, 1.0, timestamp, f"{reason}-平{close_side}")
|
||||||
|
open_price = self.apply_entry_slippage(price, new_direction)
|
||||||
|
self.open_position(open_price, new_direction, timestamp, f"{reason}-开{open_side}")
|
||||||
|
self.mid_closed_half = False
|
||||||
|
self.clear_delay_reversal()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_and_execute_delay_reversal(self, i, row, prev_row, timestamp):
|
||||||
|
"""执行延迟反转三段判断逻辑"""
|
||||||
|
if self.position == 0 or self.delay_reverse_price is None or self.delay_reverse_kline_index is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
offset = i - self.delay_reverse_kline_index
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# 情况3:持续等待,动态追踪上一根K线条件
|
||||||
|
if offset >= 2 and prev_row is not None:
|
||||||
|
prev_touch_upper = prev_row['high'] >= prev_row['upper']
|
||||||
|
if prev_touch_upper:
|
||||||
|
ref_price = prev_row['upper']
|
||||||
|
if low <= ref_price:
|
||||||
|
return self.reverse_position(ref_price, 'short', timestamp, "延迟反转-上一根触上轨后回调确认")
|
||||||
|
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, "延迟反转-跌破上一根实体确认")
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# 情况3:持续等待,动态追踪上一根K线条件
|
||||||
|
if offset >= 2 and prev_row is not None:
|
||||||
|
prev_touch_lower = prev_row['low'] <= prev_row['lower']
|
||||||
|
if prev_touch_lower:
|
||||||
|
ref_price = prev_row['lower']
|
||||||
|
if high >= ref_price:
|
||||||
|
return self.reverse_position(ref_price, 'long', timestamp, "延迟反转-上一根触下轨后反弹确认")
|
||||||
|
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 False
|
||||||
|
|
||||||
|
def open_position(self, price, direction, timestamp, reason):
|
||||||
|
"""开仓或加仓"""
|
||||||
|
if self.position_count not in (0, 1):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.position_count == 1:
|
||||||
|
current_direction = 'long' if self.position > 0 else 'short'
|
||||||
|
if direction != current_direction:
|
||||||
|
logger.warning("加仓方向不一致,跳过")
|
||||||
|
return False
|
||||||
|
|
||||||
|
margin_ratio = self.margin_ratio_1 if self.position_count == 0 else self.margin_ratio_2
|
||||||
|
margin = self.capital * margin_ratio
|
||||||
|
if margin <= 0:
|
||||||
|
logger.warning(f"[{timestamp}] 资金不足,无法开仓 | 可用资金: {self.capital:.4f}U")
|
||||||
|
return False
|
||||||
|
|
||||||
|
position_size = margin * self.leverage / price
|
||||||
|
fee = position_size * price * self.fee_rate
|
||||||
|
net_fee = self.get_net_fee(fee)
|
||||||
|
required = margin + net_fee
|
||||||
|
if self.capital < required:
|
||||||
|
logger.warning(
|
||||||
|
f"[{timestamp}] 可用资金不足,无法开仓 | 需要: {required:.4f}U | 可用: {self.capital:.4f}U"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 冻结保证金并扣除手续费
|
||||||
|
self.capital -= required
|
||||||
|
|
||||||
|
if self.position_count == 0:
|
||||||
|
self.position = position_size if direction == 'long' else -position_size
|
||||||
|
self.entry_price = price
|
||||||
|
self.total_margin = margin
|
||||||
|
self.position_count = 1
|
||||||
|
action = f'开{direction}'
|
||||||
|
else:
|
||||||
|
old_size = abs(self.position)
|
||||||
|
new_size = old_size + position_size
|
||||||
|
old_value = old_size * self.entry_price
|
||||||
|
new_value = position_size * price
|
||||||
|
self.entry_price = (old_value + new_value) / new_size
|
||||||
|
self.position = new_size if direction == 'long' else -new_size
|
||||||
|
self.total_margin += margin
|
||||||
|
self.position_count = 2
|
||||||
|
action = f'加{direction}'
|
||||||
|
|
||||||
|
self.mid_closed_half = False
|
||||||
|
|
||||||
|
self.trades.append({
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'action': action,
|
||||||
|
'price': price,
|
||||||
|
'size': position_size,
|
||||||
|
'margin': margin,
|
||||||
|
'fee': net_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}"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def close_position(self, price, ratio, timestamp, reason):
|
||||||
|
"""平仓"""
|
||||||
|
if self.position == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
ratio = min(max(ratio, 0.0), 1.0)
|
||||||
|
if ratio == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
close_size = abs(self.position) * ratio
|
||||||
|
if self.position > 0:
|
||||||
|
pnl = close_size * (price - self.entry_price)
|
||||||
|
else:
|
||||||
|
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
|
||||||
|
|
||||||
|
if ratio >= 0.999:
|
||||||
|
self.position = 0
|
||||||
|
self.position_count = 0
|
||||||
|
self.total_margin = 0
|
||||||
|
self.entry_price = 0
|
||||||
|
self.mid_closed_half = False
|
||||||
|
self.clear_delay_reversal()
|
||||||
|
else:
|
||||||
|
self.position *= (1 - ratio)
|
||||||
|
self.total_margin *= (1 - ratio)
|
||||||
|
if abs(self.position) < 1e-12:
|
||||||
|
self.position = 0
|
||||||
|
self.position_count = 0
|
||||||
|
self.total_margin = 0
|
||||||
|
self.entry_price = 0
|
||||||
|
self.mid_closed_half = False
|
||||||
|
self.clear_delay_reversal()
|
||||||
|
|
||||||
|
self.trades.append({
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'action': f'平仓{int(ratio*100)}%',
|
||||||
|
'price': price,
|
||||||
|
'size': close_size,
|
||||||
|
'pnl': pnl,
|
||||||
|
'fee': net_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}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_stop_loss(self, high, low, timestamp):
|
||||||
|
"""检查止损"""
|
||||||
|
if self.position == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
stop_price = low if self.position > 0 else high
|
||||||
|
if self.position > 0:
|
||||||
|
unrealized_pnl = abs(self.position) * (stop_price - self.entry_price)
|
||||||
|
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
|
||||||
|
|
||||||
|
def run_backtest(self, start_date, end_date):
|
||||||
|
"""运行回测"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
query = BitMartETH5M.select().where(
|
||||||
|
(BitMartETH5M.id >= start_ts) & (BitMartETH5M.id <= end_ts)
|
||||||
|
).order_by(BitMartETH5M.id)
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for row in query:
|
||||||
|
data.append({
|
||||||
|
'timestamp': row.id,
|
||||||
|
'open': row.open,
|
||||||
|
'high': row.high,
|
||||||
|
'low': row.low,
|
||||||
|
'close': row.close
|
||||||
|
})
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
logger.error("没有找到数据!")
|
||||||
|
return None
|
||||||
|
|
||||||
|
df = pd.DataFrame(data)
|
||||||
|
df['datetime'] = pd.to_datetime(df['timestamp'], unit='ms')
|
||||||
|
|
||||||
|
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)):
|
||||||
|
row = df.iloc[i]
|
||||||
|
prev_row = df.iloc[i-1] if i > 0 else None
|
||||||
|
|
||||||
|
timestamp = row['datetime'].strftime('%Y-%m-%d %H:%M')
|
||||||
|
five_min_ts = int(row['timestamp'])
|
||||||
|
high = row['high']
|
||||||
|
low = row['low']
|
||||||
|
upper = row['upper']
|
||||||
|
lower = row['lower']
|
||||||
|
middle = row['middle']
|
||||||
|
|
||||||
|
# 检查止损
|
||||||
|
if self.check_stop_loss(high, low, timestamp):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 已处于延迟反转状态时,先执行确认逻辑
|
||||||
|
if self.delay_reverse_price is not None:
|
||||||
|
if self.check_and_execute_delay_reversal(i, row, prev_row, timestamp):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# === 中轨平仓逻辑 ===
|
||||||
|
if self.position != 0:
|
||||||
|
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, "回开仓价全平")
|
||||||
|
|
||||||
|
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 self.position == 0: # 空仓
|
||||||
|
self.clear_delay_reversal()
|
||||||
|
|
||||||
|
# 触上轨开空
|
||||||
|
if high >= upper:
|
||||||
|
entry_price = self.get_touch_entry_price(five_min_ts, 'short', upper)
|
||||||
|
self.open_position(entry_price, 'short', timestamp, "触上轨开空")
|
||||||
|
|
||||||
|
# 触下轨开多
|
||||||
|
elif low <= lower:
|
||||||
|
entry_price = self.get_touch_entry_price(five_min_ts, 'long', lower)
|
||||||
|
self.open_position(entry_price, 'long', timestamp, "触下轨开多")
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 进入延迟反转等待后,不再执行加仓
|
||||||
|
if self.delay_reverse_price is not None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 同向加仓最多1次
|
||||||
|
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, "触下轨加多")
|
||||||
|
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, "触上轨加空")
|
||||||
|
|
||||||
|
# 最后平仓
|
||||||
|
if self.position != 0:
|
||||||
|
final_price = df.iloc[-1]['close']
|
||||||
|
final_time = df.iloc[-1]['datetime'].strftime('%Y-%m-%d %H:%M')
|
||||||
|
self.close_position(final_price, 1.0, final_time, "回测结束平仓")
|
||||||
|
|
||||||
|
# 生成报告
|
||||||
|
return self.generate_report(df)
|
||||||
|
|
||||||
|
def generate_report(self, df):
|
||||||
|
"""生成回测报告"""
|
||||||
|
logger.info(f"\n{'='*80}")
|
||||||
|
logger.info("回测报告")
|
||||||
|
logger.info(f"{'='*80}")
|
||||||
|
|
||||||
|
# 基本统计
|
||||||
|
total_trades = len([t for t in self.trades if '开' in t['action']])
|
||||||
|
win_trades = len([t for t in self.trades if '平' in t['action'] and t.get('pnl', 0) > 0])
|
||||||
|
loss_trades = len([t for t in self.trades if '平' in t['action'] and t.get('pnl', 0) < 0])
|
||||||
|
|
||||||
|
total_pnl = sum([t.get('pnl', 0) for t in self.trades])
|
||||||
|
total_fee = sum([t.get('fee', 0) for t in self.trades])
|
||||||
|
|
||||||
|
final_capital = self.capital
|
||||||
|
roi = (final_capital - self.initial_capital) / self.initial_capital * 100
|
||||||
|
|
||||||
|
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"净收益: {final_capital - self.initial_capital:.2f}U")
|
||||||
|
logger.info(f"收益率: {roi:.2f}%")
|
||||||
|
logger.info(f"总交易次数: {total_trades}")
|
||||||
|
logger.info(f"盈利次数: {win_trades}")
|
||||||
|
logger.info(f"亏损次数: {loss_trades}")
|
||||||
|
if win_trades + loss_trades > 0:
|
||||||
|
logger.info(f"胜率: {win_trades/(win_trades+loss_trades)*100:.2f}%")
|
||||||
|
|
||||||
|
# 保存交易记录
|
||||||
|
trades_df = pd.DataFrame(self.trades)
|
||||||
|
output_file = 'bb_backtest_march_2026_trades.csv'
|
||||||
|
trades_df.to_csv(output_file, index=False, encoding='utf-8-sig')
|
||||||
|
logger.info(f"\n交易记录已保存到: {output_file}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'initial_capital': self.initial_capital,
|
||||||
|
'final_capital': final_capital,
|
||||||
|
'total_pnl': total_pnl,
|
||||||
|
'total_fee': total_fee,
|
||||||
|
'roi': roi,
|
||||||
|
'total_trades': total_trades,
|
||||||
|
'win_trades': win_trades,
|
||||||
|
'loss_trades': loss_trades,
|
||||||
|
'trades': self.trades
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 连接数据库
|
||||||
|
db.connect(reuse_if_open=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 创建回测实例
|
||||||
|
backtest = BollingerBandBacktest()
|
||||||
|
|
||||||
|
# 运行回测(2026年3月)
|
||||||
|
result = backtest.run_backtest('2026-03-01', '2026-03-31')
|
||||||
|
|
||||||
|
if result:
|
||||||
|
logger.success(f"\n回测完成!最终收益率: {result['roi']:.2f}%")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
157
bb_backtest_march_2026_trades.csv
Normal file
157
bb_backtest_march_2026_trades.csv
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
timestamp,action,price,size,margin,fee,capital,reason,pnl
|
||||||
|
2026-03-01 01:35,开short,1970.895742,0.02536917551471376,1.0,0.0024999999999999996,98.9975,触上轨开空,
|
||||||
|
2026-03-01 01:50,平仓100%,2022.33,0.02536917551471376,,0.0025652422359335528,98.690090039093,止损,-1.3048447186710703
|
||||||
|
2026-03-01 03:50,开long,2019.4038,0.02443545219611179,0.9869009003909299,0.0024672522509773245,97.70072188645109,触下轨开多,
|
||||||
|
2026-03-01 03:55,加long,2018.383596,0.04840542802670058,1.9540144377290218,0.004885036094322554,95.74182241262774,触下轨加多,
|
||||||
|
2026-03-01 04:00,平仓50%,2028.348,0.03642044011140619,,0.0036936663429545246,97.55902982143648,触中轨平50%-1m(04:02)回踩中轨,0.35044340609171903
|
||||||
|
2026-03-01 04:05,平仓100%,2018.7258371425564,0.03642044011140619,,0.003676144172649939,99.0258113463238,回开仓价全平,0.0
|
||||||
|
2026-03-01 04:15,开long,1999.6626196004338,0.02476062971215385,0.9902581134632381,0.0024756452836580947,98.03307758757691,触下轨开多,
|
||||||
|
2026-03-01 04:35,平仓50%,2014.6299999999999,0.012380314856076925,,0.0012470876859249124,98.71226043853991,触中轨平50%-1m(04:39)回踩中轨,0.18530088191730254
|
||||||
|
2026-03-01 05:10,平仓100%,2017.8250206638015,0.012380314856076925,,0.0012490654540143892,99.43099667352436,延迟反转-同K回调确认-平多,0.22485624370683877
|
||||||
|
2026-03-01 05:10,开short,2017.4214556596687,0.02464308991920873,0.9943099667352436,0.0024857749168381085,98.43420093187228,延迟反转-同K回调确认-开空,
|
||||||
|
2026-03-01 05:15,平仓50%,2014.157,0.012321544959604365,,0.0012408763015600921,98.9703381761176,触中轨平50%-1m(05:16)反抽中轨,0.04022313717924384
|
||||||
|
2026-03-01 05:20,平仓100%,2017.4214556596687,0.012321544959604365,,0.0012428874584190543,99.4662502720268,回开仓价全平,0.0
|
||||||
|
2026-03-01 05:20,开short,2020.9165042134857,0.0246091934190863,0.994662502720268,0.0024866562568006696,98.46910111304973,触上轨开空,
|
||||||
|
2026-03-01 05:35,平仓50%,2016.4740000000002,0.01230459670954315,,0.0012405949672639653,99.01985499216998,触中轨平50%-1m(05:38)反抽中轨,0.05466322272738604
|
||||||
|
2026-03-01 06:10,平仓100%,1993.0582785593572,0.01230459670954315,,0.0012261889168144599,99.8587442863308,延迟反转-同K反弹确认-平空,0.34278423171750066
|
||||||
|
2026-03-01 06:10,开long,1993.456890215069,0.02504662748828175,0.998587442863308,0.00249646860715827,98.85766037486034,延迟反转-同K反弹确认-开多,
|
||||||
|
2026-03-01 07:00,平仓50%,1996.611,0.012523313744140875,,0.0012502092989001429,99.39520379341326,触中轨平50%-1m(07:01)回踩中轨,0.03949990642015626
|
||||||
|
2026-03-01 07:05,平仓100%,1993.456890215069,0.012523313744140875,,0.001248234303579135,99.89324928054134,回开仓价全平,0.0
|
||||||
|
2026-03-01 07:10,开short,2002.49942,0.024942141876011462,0.9989324928054134,0.002497331232013533,98.89181945650391,触上轨开空,
|
||||||
|
2026-03-01 07:25,平仓50%,1997.67,0.012471070938005731,,0.0012456542140362953,99.450268088102,触中轨平50%-1m(07:26)反抽中轨,0.06022803940942398
|
||||||
|
2026-03-01 07:35,平仓100%,2002.49942,0.012471070938005731,,0.0012486656160067666,99.94848566888871,回开仓价全平,0.0
|
||||||
|
2026-03-01 08:50,开long,1983.9752370436452,0.02518894485241248,0.9994848566888871,0.0024987121417222175,98.9465021000581,触下轨开多,
|
||||||
|
2026-03-01 09:00,加long,1984.6668539999998,0.049855471662982744,1.978930042001162,0.004947325105002905,96.96262473295194,触下轨加多,
|
||||||
|
2026-03-01 09:15,平仓50%,1992.7069999999999,0.037522208257697615,,0.003738538352528591,98.75848822726503,触中轨平50%-1m(09:18)回踩中轨,0.31039458332059994
|
||||||
|
2026-03-01 09:30,平仓100%,1984.4347101286562,0.037522208257697615,,0.003723018623362561,100.24397265798669,回开仓价全平,0.0
|
||||||
|
2026-03-01 09:35,开long,1977.5454880734026,0.025345554188907188,1.0024397265798668,0.0025060993164496663,99.23902683209037,触下轨开多,
|
||||||
|
2026-03-01 10:15,平仓50%,1979.625,0.012672777094453594,,0.0012543673177803846,99.76534551917355,触中轨平50%-1m(10:17)回踩中轨,0.02635319111102702
|
||||||
|
2026-03-01 10:15,平仓100%,1977.5454880734026,0.012672777094453594,,0.0012530496582248331,100.26531233280527,回开仓价全平,0.0
|
||||||
|
2026-03-01 10:25,开short,1983.593202,0.02527365798383223,1.0026531233280527,0.0025066328083201313,99.26015257666889,触上轨开空,
|
||||||
|
2026-03-01 10:35,加short,1987.0210368714647,0.049954253495449914,1.9852030515333778,0.004963007628833443,97.26998651750668,触上轨加空,
|
||||||
|
2026-03-01 10:40,平仓50%,1980.9359999999997,0.037613955739641075,,0.0037255419513530803,98.94575440746019,触中轨平50%-1m(10:41)反抽中轨,0.18556534447413794
|
||||||
|
2026-03-01 11:00,平仓100%,1985.8694174198158,0.037613955739641075,,0.0037348202185767876,100.43594767467232,回开仓价全平,0.0
|
||||||
|
2026-03-01 13:20,开long,1971.0811861410878,0.02547737464617128,1.0043594767467232,0.0025108986918668075,99.42907729923373,触下轨开多,
|
||||||
|
2026-03-01 13:25,平仓50%,1977.221,0.01273868732308564,,0.0012593600043819353,100.00821084657333,触中轨平50%-1m(13:27)回踩中轨,0.07821316897063098
|
||||||
|
2026-03-01 13:50,平仓100%,2012.7953226137352,0.01273868732308564,,0.0012820185130072825,101.04049190791126,延迟反转-同K回调确认-平多,0.5313833414775777
|
||||||
|
2026-03-01 13:50,开short,2012.3927635492125,0.025104565504824317,1.0104049190791127,0.002526012297697781,100.02756097653445,延迟反转-同K回调确认-开空,
|
||||||
|
2026-03-01 14:25,平仓50%,2004.753,0.012552282752412158,,0.0012582113252373262,100.62740169698006,触中轨平50%-1m(14:28)反抽中轨,0.09589647223128844
|
||||||
|
2026-03-01 14:40,平仓100%,1994.9544227315625,0.012552282752412158,,0.0012520615996150872,101.35024307959607,延迟反转-同K反弹确认-平空,0.21889098467607288
|
||||||
|
2026-03-01 14:40,开long,1995.3534136161088,0.025396564435150013,1.0135024307959608,0.0025337560769899017,100.33420689272312,延迟反转-同K反弹确认-开多,
|
||||||
|
2026-03-01 15:00,平仓50%,2004.636,0.012698282217575007,,0.0012727716835755343,100.95755823804919,触中轨平50%-1m(15:03)回踩中轨,0.11787290161166897
|
||||||
|
2026-03-01 15:20,平仓100%,2012.645228772288,0.012698282217575007,,0.0012778568559403158,101.68260794549853,延迟反转-同K回调确认-平多,0.21957634890730374
|
||||||
|
2026-03-01 15:20,开short,2012.2426997265336,0.02526599002180933,1.0168260794549853,0.002542065198637463,100.6632398008449,延迟反转-同K回调确认-开空,
|
||||||
|
2026-03-01 15:50,平仓50%,2008.9260000000002,0.012632995010904665,,0.0012689376067638331,101.2122837540636,触中轨平50%-1m(15:52)反抽中轨,0.04189985109796585
|
||||||
|
2026-03-01 15:50,平仓100%,2012.2426997265336,0.012632995010904665,,0.0012710325993187314,101.71942576119179,回开仓价全平,0.0
|
||||||
|
2026-03-01 15:55,开long,1992.6786162417027,0.025523289338307855,1.017194257611918,0.0025429856440297945,100.69968851793584,触下轨开多,
|
||||||
|
2026-03-01 16:00,加long,1991.1565538891698,0.050573466120103445,2.013993770358717,0.005034984425896792,98.68065976315123,触下轨加多,
|
||||||
|
2026-03-01 16:25,平仓50%,2002.4759999999999,0.03804837772920565,,0.00380954816208344,100.60370677137739,触中轨平50%-1m(16:26)回踩中轨,0.41126254240293536
|
||||||
|
2026-03-01 16:45,平仓100%,1967.62,0.03804837772920565,,0.00374323744937698,101.20060583618708,止损,-0.9149517117262566
|
||||||
|
2026-03-01 16:50,开long,1965.2229659999998,0.02574786871185666,1.0120060583618709,0.0025300151459046764,100.18606976267931,触下轨开多,
|
||||||
|
2026-03-01 17:35,平仓50%,1967.5260000000003,0.01287393435592833,,0.001266490028379112,100.72045541036735,触中轨平50%-1m(17:36)回踩中轨,0.029649108535477225
|
||||||
|
2026-03-01 17:45,平仓100%,1965.2229659999998,0.01287393435592833,,0.0012650075729523382,101.22519343197533,回开仓价全平,0.0
|
||||||
|
2026-03-01 18:00,开short,1982.393442,0.025531055361505614,1.0122519343197534,0.002530629835799383,100.21041086781977,触上轨开空,
|
||||||
|
2026-03-01 18:05,加short,1991.233970955805,0.05032578407635248,2.0042082173563953,0.005010520543390987,98.20119212991999,触上轨加空,
|
||||||
|
2026-03-01 18:30,平仓50%,1977.5280000000002,0.037928419718929046,,0.0037502255994967156,100.11266378212795,触中轨平50%-1m(18:33)反抽中轨,0.4069918019693819
|
||||||
|
2026-03-01 18:55,平仓100%,1972.9829470185493,0.037928419718929046,,0.0037416062656404534,102.196530730795,延迟反转-同K反弹确认-平空,0.5793784790946218
|
||||||
|
2026-03-01 18:55,开long,1973.377543607953,0.025893811111265538,1.02196530730795,0.0025549132682698744,101.17201051021878,延迟反转-同K反弹确认-开多,
|
||||||
|
2026-03-01 19:05,加long,1968.973716,0.051383118874614164,2.0234402102043756,0.0050586005255109385,99.1435116994889,触下轨加多,
|
||||||
|
2026-03-01 19:40,平仓100%,1946.27,0.0772769299858797,,0.007520088526180904,100.31289177647686,止损,-1.8685053519981858
|
||||||
|
2026-03-01 20:10,开long,1933.526628,0.025940395731771856,1.0031289177647686,0.002507822294411921,99.30725503641769,触下轨开多,
|
||||||
|
2026-03-01 20:35,平仓100%,1911.05,0.025940395731771856,,0.00247866966316013,99.72485265948347,止损,-0.5830526250358269
|
||||||
|
2026-03-01 22:10,开short,1943.1708021370632,0.025660341476366342,0.9972485265948348,0.002493121316487086,98.72511101157215,触上轨开空,
|
||||||
|
2026-03-01 22:25,平仓50%,1933.2939999999999,0.012830170738183171,,0.0012402246053552545,99.34921610802999,触中轨平50%-1m(22:28)反抽中轨,0.12672105776577522
|
||||||
|
2026-03-01 22:25,平仓100%,1943.1708021370632,0.012830170738183171,,0.001246560658243543,99.84659381066916,回开仓价全平,0.0
|
||||||
|
2026-03-02 00:10,开short,1947.900342,0.025629286996313172,0.9984659381066916,0.0024961648452667285,98.8456317077172,触上轨开空,
|
||||||
|
2026-03-02 00:15,平仓50%,1940.6370000000002,0.012814643498156586,,0.001243428565716605,99.43669838654002,触中轨平50%-1m(00:17)反抽中轨,0.0930771383351843
|
||||||
|
2026-03-02 00:20,平仓100%,1947.900342,0.012814643498156586,,0.0012480824226333643,99.93468327317073,回开仓价全平,0.0
|
||||||
|
2026-03-02 00:40,开short,1961.187684,0.02547810290888272,0.9993468327317073,0.002498367081829268,98.9328380733572,触上轨开空,
|
||||||
|
2026-03-02 00:45,加short,1959.7479720000001,0.050482429111735394,1.9786567614671442,0.004946641903667859,96.9492346699864,触上轨加空,
|
||||||
|
2026-03-02 01:10,平仓100%,1988.21,0.07596053202061812,,0.007551274468435656,97.79437733094622,止损,-2.1253096587705884
|
||||||
|
2026-03-02 02:05,开long,1949.6071946200707,0.02508053355588993,0.9779437733094622,0.002444859433273655,96.81398869820349,触下轨开多,
|
||||||
|
2026-03-02 02:10,加long,1940.8287934851146,0.049882807295102104,1.9362797739640698,0.004840699434910173,94.8728682248045,触下轨加多,
|
||||||
|
2026-03-02 02:30,平仓50%,1953.676,0.037481670425496015,,0.0036613519975100665,96.69776991480681,触中轨平50%-1m(02:34)回踩中轨,0.371451268363048
|
||||||
|
2026-03-02 03:10,平仓100%,1966.7001436494575,0.037481670425496015,,0.0036857603305022308,99.0108138563194,延迟反转-同K回调确认-平多,0.859617928206332
|
||||||
|
2026-03-02 03:10,开short,1966.3068036207276,0.025176847700979926,0.990108138563194,0.0024752703464079847,98.0182304474098,延迟反转-同K回调确认-开空,
|
||||||
|
2026-03-02 03:55,平仓50%,1971.86,0.012588423850489963,,0.0012411304726913566,98.44213739647142,触中轨平50%-1m(03:57)反抽中轨,-0.0699059897472862
|
||||||
|
2026-03-02 03:55,平仓100%,1966.3068036207276,0.012588423850489963,,0.0012376351732039923,98.93595383057982,回开仓价全平,0.0
|
||||||
|
2026-03-02 04:15,开long,1969.413804,0.025118122364541886,0.9893595383057983,0.002473398845764495,97.94412089342826,触下轨开多,
|
||||||
|
2026-03-02 04:20,平仓50%,1971.578,0.012559061182270943,,0.001238058436380969,98.46474287411921,触中轨平50%-1m(04:22)回踩中轨,0.02718026997442538
|
||||||
|
2026-03-02 04:20,平仓100%,1969.413804,0.012559061182270943,,0.0012366994228822474,98.95818594384923,回开仓价全平,0.0
|
||||||
|
2026-03-02 05:05,开long,1964.19276,0.025190548493786637,0.9895818594384923,0.0024739546485962305,97.96613012976213,触下轨开多,
|
||||||
|
2026-03-02 05:15,平仓50%,1970.2719999999997,0.012595274246893318,,0.0012408058090487492,98.53624994868501,触中轨平50%-1m(05:17)回踩中轨,0.07656969501268121
|
||||||
|
2026-03-02 05:20,平仓100%,1964.19276,0.012595274246893318,,0.0012369773242981153,99.02980390107996,回开仓价全平,0.0
|
||||||
|
2026-03-02 05:40,开short,1979.524016,0.025013539391451352,0.9902980390107996,0.0024757450975269983,98.03703011697164,触上轨开空,
|
||||||
|
2026-03-02 07:20,平仓50%,1937.3039999999996,0.012506769695725676,,0.0012114707479304063,99.05900368239097,触中轨平50%-1m(07:24)反抽中轨,0.5280360166618587
|
||||||
|
2026-03-02 08:10,平仓100%,1928.936186384675,0.012506769695725676,,0.0012062380320432252,100.1856367982698,延迟反转-同K反弹确认-平空,0.6326903344054814
|
||||||
|
2026-03-02 08:10,开long,1929.3219736219519,0.025963949555342865,1.001856367982698,0.0025046409199567447,99.18127578936715,延迟反转-同K反弹确认-开多,
|
||||||
|
2026-03-02 08:15,加long,1930.6160459999999,0.051372864114984755,1.983625515787343,0.004959063789468357,97.19269120979034,触下轨加多,
|
||||||
|
2026-03-02 08:50,平仓100%,1948.7595882760102,0.07733681367032762,,0.007535542858338308,101.60740052896617,延迟反转-同K回调确认-平多,1.4367629782641276
|
||||||
|
2026-03-02 08:50,开short,1948.369836358355,0.026074977818091708,1.0160740052896617,0.0025401850132241535,100.58878633866328,延迟反转-同K回调确认-开空,
|
||||||
|
2026-03-02 09:40,平仓50%,1955.359,0.013037488909045854,,0.0012746485637851492,101.00442754928288,触中轨平50%-1m(09:43)反抽中轨,-0.09112114346145109
|
||||||
|
2026-03-02 09:40,平仓100%,1948.369836358355,0.013037488909045854,,0.0012700925066120767,101.5111944594211,回开仓价全平,0.0
|
||||||
|
2026-03-02 11:05,开long,1939.03773,0.02617566251777399,1.0151119445942112,0.0025377798614855274,100.49354473496541,触下轨开多,
|
||||||
|
2026-03-02 12:00,平仓50%,1943.6330000000003,0.013087831258886995,,0.0012718970366602154,101.05997092857488,触中轨平50%-1m(12:02)回踩中轨,0.06014211834902898
|
||||||
|
2026-03-02 12:20,平仓100%,1939.03773,0.013087831258886995,,0.0012688899307427637,101.56625801094124,回开仓价全平,0.0
|
||||||
|
2026-03-02 12:20,开long,1936.8878482318667,0.026218931081543613,1.0156625801094123,0.0025391564502735306,100.54805627438155,触下轨开多,
|
||||||
|
2026-03-02 12:35,加long,1930.6260479999999,0.05208054474274946,2.010961125487631,0.005027402813719077,98.5320677460802,触下轨加多,
|
||||||
|
2026-03-02 12:50,平仓50%,1939.3809999999999,0.039149737912146536,,0.003796312893089832,100.30224850785619,触中轨平50%-1m(12:51)回踩中轨,0.26066522187056274
|
||||||
|
2026-03-02 13:05,平仓100%,1932.7228399260932,0.039149737912146536,,0.0037832796319963034,101.81177708102271,回开仓价全平,0.0
|
||||||
|
2026-03-02 13:05,开long,1930.4860199999998,0.026369467591643766,1.0181177708102271,0.0025452944270255673,100.79111401578545,触下轨开多,
|
||||||
|
2026-03-02 13:15,平仓50%,1935.7879999999998,0.013184733795821883,,0.001276142473257322,101.36880195360808,触中轨平50%-1m(13:16)回踩中轨,0.06990519489077116
|
||||||
|
2026-03-02 13:25,平仓100%,1930.4860199999998,0.013184733795821883,,0.0012726472135127836,101.87658819179968,回开仓价全平,0.0
|
||||||
|
2026-03-02 14:25,开long,1922.6183440503523,0.02649423077311791,1.0187658819179968,0.002546914704794992,100.85527539517689,触下轨开多,
|
||||||
|
2026-03-02 14:30,平仓100%,1950.3106548105818,0.02649423077311791,,0.0025835990283911126,102.60514414998892,延迟反转-同K回调确认-平多,0.733686471922416
|
||||||
|
2026-03-02 14:30,开short,1949.9205926796196,0.02631008271187774,1.0260514414998891,0.0025651286037497224,101.57652757988528,延迟反转-同K回调确认-开空,
|
||||||
|
2026-03-02 14:45,平仓100%,1977.81,0.02631008271187774,,0.002601817234418945,101.8662045907663,止损,-0.7337726133844545
|
||||||
|
2026-03-02 17:40,开long,2025.7860592669192,0.025142389573859815,1.018662045907663,0.002546655114769157,100.84499588974387,触下轨开多,
|
||||||
|
2026-03-02 18:15,平仓50%,2037.0170000000003,0.012571194786929907,,0.00128038687456438,101.49423286941916,触中轨平50%-1m(18:15)回踩中轨,0.1411863435960275
|
||||||
|
2026-03-02 18:40,加long,2027.0190691607734,0.05007068478711442,2.0298846573883833,0.005074711643470957,99.45927350038731,触下轨加多,
|
||||||
|
2026-03-02 18:45,平仓50%,2033.3019999999997,0.03132093978702216,,0.0031842464755415857,100.93023459635924,触中轨平50%-1m(18:47)回踩中轨,0.20453750227635803
|
||||||
|
2026-03-02 18:50,平仓100%,2026.7716243577877,0.03132093978702216,,0.003174019600427768,102.19666841692992,回开仓价全平,0.0
|
||||||
|
2026-03-02 18:50,开long,2028.895698,0.02518529378264026,1.0219666841692994,0.002554916710423248,101.1721468160502,触下轨开多,
|
||||||
|
2026-03-02 18:55,加long,2027.7254639999999,0.049894400702781824,2.023442936321004,0.005058607340802509,99.1436452723884,触下轨加多,
|
||||||
|
2026-03-02 19:05,平仓50%,2031.3199999999997,0.03753984724271104,,0.003812772125053188,100.78273929931468,触中轨平50%-1m(19:05)回踩中轨,0.1202019888061939
|
||||||
|
2026-03-02 19:05,平仓100%,2028.1180160380236,0.03753984724271104,,0.003806762025612878,102.30163734753422,回开仓价全平,0.0
|
||||||
|
2026-03-02 19:05,开long,2026.35519,0.025242770332760428,1.0230163734753424,0.0025575409336883554,101.27606343312519,触下轨开多,
|
||||||
|
2026-03-02 19:10,加long,2023.6443782356102,0.05004637401825834,2.0255212686625037,0.005063803171656258,99.24547836129103,触下轨加多,
|
||||||
|
2026-03-02 19:15,平仓50%,2030.7600000000002,0.037644572175509386,,0.0038223545695568717,100.99957516548169,触中轨平50%-1m(19:18)回踩中轨,0.23365033769128954
|
||||||
|
2026-03-02 19:35,平仓100%,2038.953388071054,0.037644572175509386,,0.003837776398987009,103.06209313644565,延迟反转-同K回调确认-平多,0.5420869262940294
|
||||||
|
2026-03-02 19:35,开short,2038.5455973934397,0.025278338946213584,1.0306209313644565,0.002576552328411141,102.02889565275278,延迟反转-同K回调确认-开空,
|
||||||
|
2026-03-02 19:45,平仓50%,2031.934,0.012639169473106792,,0.0012840979092083886,102.62648712046943,触中轨平50%-1m(19:45)反抽中轨,0.08356509994363615
|
||||||
|
2026-03-02 19:50,平仓100%,2038.5455973934397,0.012639169473106792,,0.0012882761642055705,103.14050930998745,回开仓价全平,0.0
|
||||||
|
2026-03-02 19:50,开short,2043.2612660000002,0.025239187720692457,1.0314050930998746,0.0025785127327496863,102.10652570415482,触上轨开空,
|
||||||
|
2026-03-02 19:55,加short,2042.021514,0.05000266892592338,2.0421305140830963,0.00510532628520774,100.05928986378652,触上轨加空,
|
||||||
|
2026-03-02 20:05,平仓50%,2037.4699999999998,0.03762092832330792,,0.003832575641544508,101.77910244042056,触中轨平50%-1m(20:09)反抽中轨,0.18687734868409472
|
||||||
|
2026-03-02 20:05,平仓100%,2042.4373773884072,0.03762092832330792,,0.003841919508978713,103.31202832450306,回开仓价全平,0.0
|
||||||
|
2026-03-02 20:50,开short,2045.370844,0.02525508482424204,1.0331202832450306,0.002582800708112576,102.27632524054992,触上轨开空,
|
||||||
|
2026-03-02 20:55,平仓50%,2038.2310000000002,0.01262754241212102,,0.0012868924199099917,102.88175717267845,触中轨平50%-1m(20:59)反抽中轨,0.09015868292592541
|
||||||
|
2026-03-02 21:45,平仓100%,2045.370844,0.01262754241212102,,0.001291400354056288,103.3970259139469,回开仓价全平,0.0
|
||||||
|
2026-03-02 22:05,开short,2047.470424,0.025249943711506063,1.033970259139469,0.002584925647848672,102.36047072915959,触上轨开空,
|
||||||
|
2026-03-02 22:15,平仓50%,2043.7830000000001,0.012624971855753031,,0.0012901351427133249,102.92271934780683,触中轨平50%-1m(22:15)反抽中轨,0.04655362422022782
|
||||||
|
2026-03-02 22:25,平仓100%,2047.470424,0.012624971855753031,,0.001292462823924336,103.43841201455264,回开仓价全平,0.0
|
||||||
|
2026-03-02 22:30,开short,2051.5995980000002,0.025209210441303816,1.0343841201455264,0.0025859603003638154,102.40144193410674,触上轨开空,
|
||||||
|
2026-03-02 22:50,平仓50%,2049.2699999999995,0.012604605220651908,,0.001291511967026266,102.94670614532531,触中轨平50%-1m(22:54)反抽中轨,0.029363663112829255
|
||||||
|
2026-03-02 22:50,平仓100%,2051.5995980000002,0.012604605220651908,,0.0012929801501819077,103.46260522524788,回开仓价全平,0.0
|
||||||
|
2026-03-03 00:30,开long,2020.584036,0.02560215348183813,1.0346260522524788,0.0025865651306311967,102.42539260786478,触下轨开多,
|
||||||
|
2026-03-03 00:45,平仓50%,2029.438,0.012801076740919065,,0.001298949578946865,103.05474695703741,触中轨平50%-1m(00:47)回踩中轨,0.11334027262533643
|
||||||
|
2026-03-03 01:30,加long,2025.1515544797203,0.05088742456290541,2.061094939140748,0.005152737347851869,100.98849928054881,触下轨加多,
|
||||||
|
2026-03-03 02:15,平仓100%,2002.4,0.06368850130382447,,0.0063764927505389046,102.16998750049402,止损,-1.390543252571244
|
||||||
|
2026-03-03 02:45,开short,2019.206078,0.025299544363914604,1.0216998750049402,0.00255424968751235,101.14573337580157,触上轨开空,
|
||||||
|
2026-03-03 03:10,平仓50%,2012.1740000000002,0.012649772181957302,,0.0012726771345228875,101.74426482083527,触中轨平50%-1m(03:13)反抽中轨,0.0889541846657505
|
||||||
|
2026-03-03 04:35,平仓100%,2003.7637516585983,0.012649772181957302,,0.0012673577482472664,102.44918931076766,延迟反转-同K反弹确认-平空,0.1953419101781693
|
||||||
|
2026-03-03 04:35,开long,2004.16450440893,0.025559076883507146,1.0244918931076767,0.0025612297327691916,101.42213618792722,延迟反转-同K反弹确认-开多,
|
||||||
|
2026-03-03 04:40,加long,2002.570434,0.05064597702331163,2.0284427237585443,0.0050711068093963595,99.38862235735928,触下轨加多,
|
||||||
|
2026-03-03 05:05,平仓50%,2003.5459999999998,0.03810252695340939,,0.0038170082733697775,100.92807270325906,触中轨平50%-1m(05:09)回踩中轨,0.016800045740044088
|
||||||
|
2026-03-03 05:05,平仓100%,2003.1050831610567,0.03810252695340939,,0.003816168271082775,102.45072384342109,回开仓价全平,0.0
|
||||||
|
2026-03-03 05:15,开short,2013.5872020000002,0.02543985275176105,1.0245072384342109,0.0025612680960855265,101.42365533689079,触上轨开空,
|
||||||
|
2026-03-03 05:20,平仓50%,2003.975,0.012719926375880526,,0.0012745207229552585,102.05690093713504,触中轨平50%-1m(05:24)反抽中轨,0.12226650175009475
|
||||||
|
2026-03-03 06:35,加short,1993.821156,0.0511865874379031,2.041138018742701,0.005102845046856751,100.01066007334548,触上轨加空,
|
||||||
|
2026-03-03 06:40,平仓50%,1992.6649999999997,0.03195325690689181,,0.0031836068337185775,101.44682656011526,触中轨平50%-1m(06:41)反抽中轨,0.1626542746235937
|
||||||
|
2026-03-03 06:45,平仓100%,1988.3703092642115,0.03195325690689181,,0.003176745365897763,103.02022926476914,延迟反转-同K反弹确认-平空,0.2998836310398825
|
||||||
|
2026-03-03 06:45,开long,1988.7679833260643,0.025900514823372103,1.0302022926476915,0.002575505731619228,101.98745146638983,延迟反转-同K反弹确认-开多,
|
||||||
|
2026-03-03 06:50,平仓50%,1992.611,0.012950257411686051,,0.0012902412685678573,102.55103042660998,触中轨平50%-1m(06:51)回踩中轨,0.04976805516487063
|
||||||
|
2026-03-03 06:55,平仓100%,1999.2593560774471,0.012950257411686051,,0.0012945461646962318,103.20070300450149,延迟反转-同K回调确认-平多,0.1358659777323571
|
||||||
|
2026-03-03 06:55,开short,1998.8595042062318,0.025814896641643555,1.032007030045015,0.002580017575112537,102.16611595688137,延迟反转-同K回调确认-开空,
|
||||||
|
2026-03-03 07:05,平仓50%,1993.9159999999997,0.012907448320821777,,0.0012868183863029834,102.74464067858328,触中轨平50%-1m(07:08)反抽中轨,0.0638080250657058
|
||||||
|
2026-03-03 07:05,平仓100%,1998.8595042062318,0.012907448320821777,,0.0012900087875562685,103.25935418481824,回开仓价全平,0.0
|
||||||
|
310
bb_backtest_visualization.html
Normal file
310
bb_backtest_visualization.html
Normal file
File diff suppressed because one or more lines are too long
21851
bb_sweep_results.csv
Normal file
21851
bb_sweep_results.csv
Normal file
File diff suppressed because it is too large
Load Diff
523
generate_backtest_chart.py
Normal file
523
generate_backtest_chart.py
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
"""
|
||||||
|
生成回测可视化图表
|
||||||
|
显示K线、布林带、开仓/平仓位置
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from peewee import *
|
||||||
|
import time
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
# 数据库配置
|
||||||
|
DB_PATH = Path(__file__).parent / 'models' / 'database.db'
|
||||||
|
db = SqliteDatabase(str(DB_PATH))
|
||||||
|
|
||||||
|
class BitMartETH5M(Model):
|
||||||
|
"""5分钟K线模型"""
|
||||||
|
id = BigIntegerField(primary_key=True)
|
||||||
|
open = FloatField(null=True)
|
||||||
|
high = FloatField(null=True)
|
||||||
|
low = FloatField(null=True)
|
||||||
|
close = FloatField(null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
database = db
|
||||||
|
table_name = 'bitmart_eth_5m'
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_bollinger_bands(df, period=10, std_dev=2.5):
|
||||||
|
"""计算布林带"""
|
||||||
|
df['sma'] = df['close'].rolling(window=period).mean()
|
||||||
|
df['std'] = df['close'].rolling(window=period).std()
|
||||||
|
df['bb_upper'] = df['sma'] + std_dev * df['std']
|
||||||
|
df['bb_mid'] = df['sma']
|
||||||
|
df['bb_lower'] = df['sma'] - std_dev * df['std']
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def generate_chart_data(start_date, end_date, trades_file):
|
||||||
|
"""生成图表数据"""
|
||||||
|
|
||||||
|
# 1. 加载K线数据
|
||||||
|
start_ts = int(pd.Timestamp(start_date).timestamp() * 1000)
|
||||||
|
end_ts = int(pd.Timestamp(end_date).timestamp() * 1000) + 86400000 # 加一天确保包含end_date当天的数据
|
||||||
|
|
||||||
|
query = BitMartETH5M.select().where(
|
||||||
|
(BitMartETH5M.id >= start_ts) & (BitMartETH5M.id <= end_ts)
|
||||||
|
).order_by(BitMartETH5M.id)
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for row in query:
|
||||||
|
data.append({
|
||||||
|
'timestamp': row.id,
|
||||||
|
'open': row.open,
|
||||||
|
'high': row.high,
|
||||||
|
'low': row.low,
|
||||||
|
'close': row.close
|
||||||
|
})
|
||||||
|
|
||||||
|
df = pd.DataFrame(data)
|
||||||
|
df['datetime'] = pd.to_datetime(df['timestamp'], unit='ms')
|
||||||
|
|
||||||
|
# 2. 计算布林带
|
||||||
|
df = calculate_bollinger_bands(df)
|
||||||
|
|
||||||
|
# 3. 加载交易记录
|
||||||
|
trades_df = pd.read_csv(trades_file)
|
||||||
|
trades_df['datetime'] = pd.to_datetime(trades_df['timestamp'])
|
||||||
|
|
||||||
|
# 4. 准备图表数据
|
||||||
|
chart_data = []
|
||||||
|
for idx, row in df.iterrows():
|
||||||
|
chart_data.append({
|
||||||
|
'timestamp': int(row['timestamp']),
|
||||||
|
'datetime': row['datetime'].strftime('%Y-%m-%d %H:%M'),
|
||||||
|
'open': float(row['open']) if pd.notna(row['open']) else None,
|
||||||
|
'high': float(row['high']) if pd.notna(row['high']) else None,
|
||||||
|
'low': float(row['low']) if pd.notna(row['low']) else None,
|
||||||
|
'close': float(row['close']) if pd.notna(row['close']) else None,
|
||||||
|
'bb_upper': float(row['bb_upper']) if pd.notna(row['bb_upper']) else None,
|
||||||
|
'bb_mid': float(row['bb_mid']) if pd.notna(row['bb_mid']) else None,
|
||||||
|
'bb_lower': float(row['bb_lower']) if pd.notna(row['bb_lower']) else None,
|
||||||
|
})
|
||||||
|
|
||||||
|
# 5. 准备交易标记数据
|
||||||
|
trades_markers = []
|
||||||
|
for idx, trade in trades_df.iterrows():
|
||||||
|
action = trade['action']
|
||||||
|
price = trade['price']
|
||||||
|
timestamp = trade['datetime']
|
||||||
|
reason = trade['reason']
|
||||||
|
|
||||||
|
# 找到对应的K线索引(使用最近的K线)
|
||||||
|
kline_idx = df[df['datetime'] == timestamp].index
|
||||||
|
if len(kline_idx) == 0:
|
||||||
|
# 如果找不到完全匹配的,找最近的K线
|
||||||
|
time_diff = abs(df['datetime'] - timestamp)
|
||||||
|
min_diff = time_diff.min()
|
||||||
|
# 如果时间差超过10分钟,跳过这个标记
|
||||||
|
if min_diff > pd.Timedelta(minutes=10):
|
||||||
|
print(f"跳过标记 {timestamp},找不到匹配的K线(最小时间差: {min_diff})")
|
||||||
|
continue
|
||||||
|
kline_idx = time_diff.idxmin()
|
||||||
|
else:
|
||||||
|
kline_idx = kline_idx[0]
|
||||||
|
|
||||||
|
# 图上显示真实成交价,避免“总在最高/最低点成交”的错觉
|
||||||
|
kline = df.loc[kline_idx]
|
||||||
|
if pd.notna(price):
|
||||||
|
display_price = float(price)
|
||||||
|
else:
|
||||||
|
display_price = float(kline['close'])
|
||||||
|
|
||||||
|
marker = {
|
||||||
|
'timestamp': timestamp.strftime('%Y-%m-%d %H:%M'),
|
||||||
|
'price': display_price, # 使用K线实际价格
|
||||||
|
'action': action,
|
||||||
|
'reason': reason,
|
||||||
|
'index': int(kline_idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 关键操作直接显示在图上,便于快速理解“为什么做这笔交易”
|
||||||
|
# 新增:开仓/加仓也展示标签
|
||||||
|
marker['show_reason_label'] = (
|
||||||
|
('延迟反转' in reason)
|
||||||
|
or ('止损' in reason)
|
||||||
|
or ('开long' in action)
|
||||||
|
or ('开short' in action)
|
||||||
|
or ('加long' in action)
|
||||||
|
or ('加short' in action)
|
||||||
|
)
|
||||||
|
if '止损' in reason:
|
||||||
|
marker['short_reason'] = '止损'
|
||||||
|
elif '延迟反转' in reason:
|
||||||
|
marker['short_reason'] = '延迟反转'
|
||||||
|
elif '触中轨平50%' in reason:
|
||||||
|
marker['short_reason'] = '中轨平半'
|
||||||
|
elif '回开仓价全平' in reason:
|
||||||
|
marker['short_reason'] = '回本全平'
|
||||||
|
elif '触上轨开空' in reason:
|
||||||
|
marker['short_reason'] = '上轨开空'
|
||||||
|
elif '触下轨开多' in reason:
|
||||||
|
marker['short_reason'] = '下轨开多'
|
||||||
|
elif '触上轨加空' in reason:
|
||||||
|
marker['short_reason'] = '上轨加空'
|
||||||
|
elif '触下轨加多' in reason:
|
||||||
|
marker['short_reason'] = '下轨加多'
|
||||||
|
else:
|
||||||
|
marker['short_reason'] = reason
|
||||||
|
|
||||||
|
# 分类标记
|
||||||
|
if '开long' in action or '加long' in action:
|
||||||
|
marker['type'] = 'open_long'
|
||||||
|
marker['color'] = '#00ff00'
|
||||||
|
marker['symbol'] = 'triangle'
|
||||||
|
elif '开short' in action or '加short' in action:
|
||||||
|
marker['type'] = 'open_short'
|
||||||
|
marker['color'] = '#ff0000'
|
||||||
|
marker['symbol'] = 'triangle'
|
||||||
|
elif '平仓' in action:
|
||||||
|
if '50%' in action:
|
||||||
|
marker['type'] = 'close_half'
|
||||||
|
marker['color'] = '#ffff00'
|
||||||
|
marker['symbol'] = 'diamond'
|
||||||
|
else:
|
||||||
|
marker['type'] = 'close_all'
|
||||||
|
marker['color'] = '#ff00ff'
|
||||||
|
marker['symbol'] = 'circle'
|
||||||
|
else:
|
||||||
|
marker['type'] = 'other'
|
||||||
|
marker['color'] = '#ffffff'
|
||||||
|
marker['symbol'] = 'circle'
|
||||||
|
|
||||||
|
trades_markers.append(marker)
|
||||||
|
|
||||||
|
return chart_data, trades_markers
|
||||||
|
|
||||||
|
|
||||||
|
def generate_html(chart_data, trades_markers, output_file):
|
||||||
|
"""生成HTML文件"""
|
||||||
|
|
||||||
|
html_content = f"""<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>布林带策略回测可视化 - 2026年3月</title>
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #0a0e27;
|
||||||
|
color: #e0e6ed;
|
||||||
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||||
|
}}
|
||||||
|
#chart {{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}}
|
||||||
|
.legend {{
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: rgba(15, 23, 42, 0.95);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.8;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
z-index: 1000;
|
||||||
|
}}
|
||||||
|
.legend-title {{
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #f1f5f9;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}}
|
||||||
|
.legend-item {{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 8px 0;
|
||||||
|
}}
|
||||||
|
.legend-marker {{
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
margin-right: 10px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="chart"></div>
|
||||||
|
<div class="legend">
|
||||||
|
<div class="legend-title">📊 交易标记说明</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<div class="legend-marker" style="background: #00ff00;">▲</div>
|
||||||
|
<span>开多/加多</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<div class="legend-marker" style="background: #ff0000;">▼</div>
|
||||||
|
<span>开空/加空</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<div class="legend-marker" style="background: #ffff00;">◆</div>
|
||||||
|
<span>平仓50%</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<div class="legend-marker" style="background: #ff00ff;">●</div>
|
||||||
|
<span>平仓100%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const chartData = {json.dumps(chart_data, ensure_ascii=False)};
|
||||||
|
const tradesMarkers = {json.dumps(trades_markers, ensure_ascii=False)};
|
||||||
|
|
||||||
|
function main() {{
|
||||||
|
const categoryData = [];
|
||||||
|
const klineData = [];
|
||||||
|
const upper = [];
|
||||||
|
const mid = [];
|
||||||
|
const lower = [];
|
||||||
|
|
||||||
|
for (const k of chartData) {{
|
||||||
|
const d = new Date(k.timestamp);
|
||||||
|
const label = `${{d.getMonth()+1}}/${{d.getDate()}} ${{d.getHours().toString().padStart(2, "0")}}:${{d
|
||||||
|
.getMinutes()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}}`;
|
||||||
|
categoryData.push(label);
|
||||||
|
klineData.push([k.open, k.close, k.low, k.high]);
|
||||||
|
upper.push(k.bb_upper);
|
||||||
|
mid.push(k.bb_mid);
|
||||||
|
lower.push(k.bb_lower);
|
||||||
|
}}
|
||||||
|
|
||||||
|
// 准备交易标记数据
|
||||||
|
const openLongData = [];
|
||||||
|
const openShortData = [];
|
||||||
|
const closeHalfData = [];
|
||||||
|
const closeAllData = [];
|
||||||
|
|
||||||
|
for (const marker of tradesMarkers) {{
|
||||||
|
const point = {{
|
||||||
|
value: [marker.index, marker.price],
|
||||||
|
reason: marker.reason,
|
||||||
|
action: marker.action,
|
||||||
|
shortReason: marker.short_reason,
|
||||||
|
label: marker.show_reason_label
|
||||||
|
? {{
|
||||||
|
show: true,
|
||||||
|
formatter: marker.short_reason,
|
||||||
|
color: "#f8fafc",
|
||||||
|
backgroundColor: "rgba(15,23,42,0.85)",
|
||||||
|
borderColor: "#475569",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: [2, 4],
|
||||||
|
fontSize: 10,
|
||||||
|
}}
|
||||||
|
: {{ show: false }},
|
||||||
|
itemStyle: {{ color: marker.color }},
|
||||||
|
}};
|
||||||
|
|
||||||
|
if (marker.type === 'open_long') {{
|
||||||
|
openLongData.push(point);
|
||||||
|
}} else if (marker.type === 'open_short') {{
|
||||||
|
openShortData.push(point);
|
||||||
|
}} else if (marker.type === 'close_half') {{
|
||||||
|
closeHalfData.push(point);
|
||||||
|
}} else if (marker.type === 'close_all') {{
|
||||||
|
closeAllData.push(point);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
const chartDom = document.getElementById("chart");
|
||||||
|
const chart = echarts.init(chartDom, null, {{ renderer: "canvas" }});
|
||||||
|
|
||||||
|
const option = {{
|
||||||
|
backgroundColor: "#0a0e27",
|
||||||
|
tooltip: {{
|
||||||
|
trigger: "axis",
|
||||||
|
axisPointer: {{ type: "cross" }},
|
||||||
|
backgroundColor: "rgba(15, 23, 42, 0.95)",
|
||||||
|
borderColor: "#334155",
|
||||||
|
textStyle: {{ color: "#e0e6ed" }},
|
||||||
|
}},
|
||||||
|
axisPointer: {{
|
||||||
|
link: [{{ xAxisIndex: "all" }}],
|
||||||
|
}},
|
||||||
|
grid: {{
|
||||||
|
left: "3%",
|
||||||
|
right: "200px",
|
||||||
|
top: "6%",
|
||||||
|
bottom: "8%",
|
||||||
|
containLabel: true,
|
||||||
|
}},
|
||||||
|
xAxis: {{
|
||||||
|
type: "category",
|
||||||
|
data: categoryData,
|
||||||
|
scale: true,
|
||||||
|
boundaryGap: true,
|
||||||
|
axisLine: {{ lineStyle: {{ color: "#475569" }} }},
|
||||||
|
axisLabel: {{
|
||||||
|
color: "#94a3b8",
|
||||||
|
rotate: 45,
|
||||||
|
fontSize: 11
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
yAxis: {{
|
||||||
|
scale: true,
|
||||||
|
axisLine: {{ lineStyle: {{ color: "#475569" }} }},
|
||||||
|
splitLine: {{ lineStyle: {{ color: "#1e293b" }} }},
|
||||||
|
axisLabel: {{ color: "#94a3b8" }},
|
||||||
|
}},
|
||||||
|
dataZoom: [
|
||||||
|
{{
|
||||||
|
type: "inside",
|
||||||
|
start: 0,
|
||||||
|
end: 100,
|
||||||
|
}},
|
||||||
|
{{
|
||||||
|
type: "slider",
|
||||||
|
start: 0,
|
||||||
|
end: 100,
|
||||||
|
height: 30,
|
||||||
|
backgroundColor: "#1e293b",
|
||||||
|
fillerColor: "rgba(100, 116, 139, 0.3)",
|
||||||
|
borderColor: "#334155",
|
||||||
|
handleStyle: {{
|
||||||
|
color: "#64748b",
|
||||||
|
borderColor: "#94a3b8"
|
||||||
|
}},
|
||||||
|
textStyle: {{ color: "#94a3b8" }},
|
||||||
|
}},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{{
|
||||||
|
name: "K线",
|
||||||
|
type: "candlestick",
|
||||||
|
data: klineData,
|
||||||
|
itemStyle: {{
|
||||||
|
color: "#10b981",
|
||||||
|
color0: "#ef4444",
|
||||||
|
borderColor: "#10b981",
|
||||||
|
borderColor0: "#ef4444",
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
{{
|
||||||
|
name: "BB上轨",
|
||||||
|
type: "line",
|
||||||
|
data: upper,
|
||||||
|
symbol: "none",
|
||||||
|
lineStyle: {{ color: "#f59e0b", width: 2 }},
|
||||||
|
z: 1,
|
||||||
|
}},
|
||||||
|
{{
|
||||||
|
name: "BB中轨",
|
||||||
|
type: "line",
|
||||||
|
data: mid,
|
||||||
|
symbol: "none",
|
||||||
|
lineStyle: {{ color: "#8b5cf6", width: 2, type: "dashed" }},
|
||||||
|
z: 1,
|
||||||
|
}},
|
||||||
|
{{
|
||||||
|
name: "BB下轨",
|
||||||
|
type: "line",
|
||||||
|
data: lower,
|
||||||
|
symbol: "none",
|
||||||
|
lineStyle: {{ color: "#f59e0b", width: 2 }},
|
||||||
|
z: 1,
|
||||||
|
}},
|
||||||
|
{{
|
||||||
|
name: "开多/加多",
|
||||||
|
type: "scatter",
|
||||||
|
symbol: "triangle",
|
||||||
|
symbolSize: 12,
|
||||||
|
symbolOffset: [0, 8], // 向下偏移,使三角形底部对齐价格
|
||||||
|
data: openLongData,
|
||||||
|
itemStyle: {{ color: "#00ff00" }},
|
||||||
|
z: 10,
|
||||||
|
tooltip: {{
|
||||||
|
formatter: (params) => {{
|
||||||
|
const marker = params.data;
|
||||||
|
return `开多/加多<br/>价格: ${{marker.value[1].toFixed(2)}}<br/>原因: ${{marker.reason}}`;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
{{
|
||||||
|
name: "开空/加空",
|
||||||
|
type: "scatter",
|
||||||
|
symbol: "triangle",
|
||||||
|
symbolSize: 12,
|
||||||
|
symbolRotate: 180,
|
||||||
|
symbolOffset: [0, -8], // 向上偏移,使三角形底部对齐价格
|
||||||
|
data: openShortData,
|
||||||
|
itemStyle: {{ color: "#ff0000" }},
|
||||||
|
z: 10,
|
||||||
|
tooltip: {{
|
||||||
|
formatter: (params) => {{
|
||||||
|
const marker = params.data;
|
||||||
|
return `开空/加空<br/>价格: ${{marker.value[1].toFixed(2)}}<br/>原因: ${{marker.reason}}`;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
{{
|
||||||
|
name: "平仓50%",
|
||||||
|
type: "scatter",
|
||||||
|
symbol: "diamond",
|
||||||
|
symbolSize: 10,
|
||||||
|
data: closeHalfData,
|
||||||
|
itemStyle: {{ color: "#ffff00" }},
|
||||||
|
z: 10,
|
||||||
|
tooltip: {{
|
||||||
|
formatter: (params) => {{
|
||||||
|
const marker = params.data;
|
||||||
|
return `平仓50%<br/>价格: ${{marker.value[1].toFixed(2)}}<br/>原因: ${{marker.reason}}`;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
{{
|
||||||
|
name: "平仓100%",
|
||||||
|
type: "scatter",
|
||||||
|
symbol: "circle",
|
||||||
|
symbolSize: 10,
|
||||||
|
data: closeAllData,
|
||||||
|
itemStyle: {{ color: "#ff00ff" }},
|
||||||
|
z: 10,
|
||||||
|
tooltip: {{
|
||||||
|
formatter: (params) => {{
|
||||||
|
const marker = params.data;
|
||||||
|
return `平仓100%<br/>价格: ${{marker.value[1].toFixed(2)}}<br/>原因: ${{marker.reason}}`;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
],
|
||||||
|
}};
|
||||||
|
|
||||||
|
chart.setOption(option);
|
||||||
|
window.addEventListener("resize", () => chart.resize());
|
||||||
|
}}
|
||||||
|
|
||||||
|
main();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open(output_file, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(html_content)
|
||||||
|
|
||||||
|
print(f"✅ 图表已生成: {output_file}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
db.connect(reuse_if_open=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("正在生成回测可视化图表...")
|
||||||
|
|
||||||
|
# 生成图表数据
|
||||||
|
chart_data, trades_markers = generate_chart_data(
|
||||||
|
start_date='2026-03-01',
|
||||||
|
end_date='2026-03-03',
|
||||||
|
trades_file='bb_backtest_march_2026_trades.csv'
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"📊 K线数据: {len(chart_data)} 根")
|
||||||
|
print(f"📍 交易标记: {len(trades_markers)} 个")
|
||||||
|
|
||||||
|
# 生成HTML
|
||||||
|
generate_html(chart_data, trades_markers, 'bb_backtest_visualization.html')
|
||||||
|
|
||||||
|
print("\n🎉 完成!请在浏览器中打开 bb_backtest_visualization.html 查看图表")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
Reference in New Issue
Block a user