加入精准回测数据

This commit is contained in:
27942
2026-02-02 00:34:04 +08:00
parent 03e209f28a
commit 251f794ea8
3 changed files with 82547 additions and 85066 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -215,15 +215,128 @@ def determine_trigger_order_by_1m(
return None
def check_reverse_signal_in_first_minute(
bars_1m: List[Dict],
long_trigger: float,
short_trigger: float,
current_direction: str
) -> bool:
"""
检查反手信号是否在第一分钟触发
:param bars_1m: 3根1分钟K线数据
:param long_trigger: 做多触发价格
:param short_trigger: 做空触发价格
:param current_direction: 当前持仓方向 ('long''short')
:return: True 表示反手信号在第一分钟触发False 表示不是
"""
if not bars_1m:
return False
# 只检查第一分钟K线
first_bar = bars_1m[0]
high = float(first_bar['high'])
low = float(first_bar['low'])
# 如果当前是多仓,检查空信号是否在第一分钟触发
if current_direction == 'long':
return low <= short_trigger
# 如果当前是空仓,检查多信号是否在第一分钟触发
if current_direction == 'short':
return high >= long_trigger
return False
def get_body_percent(candle) -> float:
"""
计算K线实体占价格的百分比
:param candle: K线数据
:return: 实体百分比如0.1表示0.1%
"""
body = abs(float(candle['open']) - float(candle['close']))
price = (float(candle['open']) + float(candle['close'])) / 2
if price == 0:
return 0
return (body / price) * 100
def check_breakout_reverse_signal(
all_data_3m: List[Dict],
current_idx: int,
current_position_direction: str,
min_body_percent: float = 0.1
) -> tuple:
"""
检查"突破上一根K线高低点"的反手信号
规则:
- 持多单时当前K线跌破上一根K线最低点 → 反手开空
条件上一根K线是阴线且实体>0.1%波动
- 持空单时当前K线涨破上一根K线最高点 → 反手开多
条件上一根K线是阳线且实体>0.1%波动
:param all_data_3m: 3分钟K线数据
:param current_idx: 当前K线索引
:param current_position_direction: 当前持仓方向 ('long''short')
:param min_body_percent: 最小实体百分比默认0.1%
:return: (方向, 触发价格, 信号类型) 或 (None, None, None)
"""
if current_idx <= 0 or current_position_direction is None:
return None, None, None
curr = all_data_3m[current_idx]
prev = all_data_3m[current_idx - 1]
c_high = float(curr['high'])
c_low = float(curr['low'])
prev_high = float(prev['high'])
prev_low = float(prev['low'])
# 计算上一根K线的实体百分比
body_percent = get_body_percent(prev)
# 持多单时检查是否跌破上一根K线最低点
if current_position_direction == 'long':
# 条件上一根K线是阴线且实体>min_body_percent%
if is_bearish(prev) and body_percent >= min_body_percent:
if c_low < prev_low:
# 触发反手开空信号
logger.debug(f"突破反手信号持多单当前K线低点{c_low:.2f}跌破上一根阴线低点{prev_low:.2f},实体{body_percent:.3f}%")
return 'short', prev_low, 'breakout'
# 持空单时检查是否涨破上一根K线最高点
elif current_position_direction == 'short':
# 条件上一根K线是阳线且实体>min_body_percent%
if is_bullish(prev) and body_percent >= min_body_percent:
if c_high > prev_high:
# 触发反手开多信号
logger.debug(f"突破反手信号持空单当前K线高点{c_high:.2f}涨破上一根阳线高点{prev_high:.2f},实体{body_percent:.3f}%")
return 'long', prev_high, 'breakout'
return None, None, None
def check_trigger_with_1m(
all_data_3m: List[Dict],
current_idx: int,
min_body_size: float = 0.1
min_body_size: float = 0.1,
current_position_direction: str = None,
first_minute_only: bool = True
) -> tuple:
"""
检查当前3分钟K线是否触发了交易信号
如果同时触发两个方向使用1分钟K线精确判断顺序
新增逻辑:如果有持仓且 first_minute_only=True反手信号必须在第一分钟触发才有效
:param all_data_3m: 3分钟K线数据
:param current_idx: 当前K线索引
:param min_body_size: 最小实体大小
:param current_position_direction: 当前持仓方向 ('long', 'short', 或 None)
:param first_minute_only: 是否只在第一分钟触发反手信号才有效
返回:(方向, 触发价格, 有效前一根K线索引, 1分钟数据是否使用)
"""
if current_idx <= 0:
@@ -257,6 +370,15 @@ def check_trigger_with_1m(
direction = determine_trigger_order_by_1m(bars_1m, long_trigger, short_trigger)
if direction:
trigger_price = long_trigger if direction == 'long' else short_trigger
# 检查反手信号是否需要在第一分钟触发
if first_minute_only and current_position_direction and direction != current_position_direction:
# 这是一个反手信号,检查是否在第一分钟触发
if not check_reverse_signal_in_first_minute(bars_1m, long_trigger, short_trigger, current_position_direction):
# 反手信号不是在第一分钟触发,忽略
logger.debug(f"反手信号 {direction} 不在第一分钟触发,忽略")
return None, None, None, False
return direction, trigger_price, valid_prev_idx, True
# 如果没有1分钟数据使用开盘价距离判断
@@ -269,9 +391,21 @@ def check_trigger_with_1m(
return 'long', long_trigger, valid_prev_idx, False
if short_triggered:
# 检查是否是反手信号且需要第一分钟触发
if first_minute_only and current_position_direction == 'long':
bars_1m = get_1m_data_for_3m_bar(curr)
if bars_1m and not check_reverse_signal_in_first_minute(bars_1m, long_trigger, short_trigger, 'long'):
logger.debug(f"空信号不在第一分钟触发,忽略(当前持多仓)")
return None, None, None, False
return 'short', short_trigger, valid_prev_idx, False
if long_triggered:
# 检查是否是反手信号且需要第一分钟触发
if first_minute_only and current_position_direction == 'short':
bars_1m = get_1m_data_for_3m_bar(curr)
if bars_1m and not check_reverse_signal_in_first_minute(bars_1m, long_trigger, short_trigger, 'short'):
logger.debug(f"多信号不在第一分钟触发,忽略(当前持空仓)")
return None, None, None, False
return 'long', long_trigger, valid_prev_idx, False
return None, None, None, False
@@ -279,12 +413,21 @@ def check_trigger_with_1m(
# ========================= 回测逻辑 =========================
def backtest_one_third_strategy(dates: List[str], min_body_size: float = 0.1):
def backtest_one_third_strategy(
dates: List[str],
min_body_size: float = 0.1,
first_minute_only: bool = True,
enable_breakout_reverse: bool = True,
breakout_min_body_percent: float = 0.1
):
"""
三分之一策略回测(精准版)
:param dates: 日期列表
:param min_body_size: 最小实体大小
:param min_body_size: 最小实体大小(绝对值)
:param first_minute_only: 是否只在第一分钟触发反手信号才有效默认True
:param enable_breakout_reverse: 是否启用"突破上一根K线高低点"反手信号默认True
:param breakout_min_body_percent: 突破反手信号的最小实体百分比默认0.1%
:return: (trades, stats)
"""
# 获取所有3分钟K线数据
@@ -297,6 +440,8 @@ def backtest_one_third_strategy(dates: List[str], min_body_size: float = 0.1):
total_queried += len(day_data)
logger.info(f"总共查询了 {len(dates)} 天,获取到 {total_queried} 条3分钟K线数据")
logger.info(f"反手信号仅第一分钟有效: {first_minute_only}")
logger.info(f"突破反手信号启用: {enable_breakout_reverse},最小实体百分比: {breakout_min_body_percent}%")
if not all_data:
logger.warning("未获取到任何数据,请检查数据库")
@@ -325,24 +470,47 @@ def backtest_one_third_strategy(dates: List[str], min_body_size: float = 0.1):
# 统计使用1分钟数据精准判断的次数
precise_count = 0
fallback_count = 0
# 统计突破反手信号触发次数
breakout_reverse_count = 0
# 记录每根K线是否已经执行过突破反手当前K线只执行一次
last_breakout_bar_id = None
idx = 1
while idx < len(all_data):
curr = all_data[idx]
curr_bar_id = curr['id']
# 检测信号使用1分钟K线精准判断
# 获取当前持仓方向(用于判断反手信号是否在第一分钟触发
current_pos_dir = current_position['direction'] if current_position else None
# 检测信号使用1分钟K线精准判断并考虑反手信号第一分钟限制
direction, trigger_price, valid_prev_idx, used_1m = check_trigger_with_1m(
all_data, idx, min_body_size
all_data, idx, min_body_size,
current_position_direction=current_pos_dir,
first_minute_only=first_minute_only
)
# 如果没有五分之一信号,且有持仓,检查突破反手信号
signal_type = 'one_fifth' # 信号类型one_fifth五分之一或 breakout突破
if direction is None and current_position is not None and enable_breakout_reverse:
# 检查当前K线是否已经执行过突破反手
if last_breakout_bar_id != curr_bar_id:
breakout_dir, breakout_price, breakout_type = check_breakout_reverse_signal(
all_data, idx, current_pos_dir, breakout_min_body_percent
)
if breakout_dir:
direction = breakout_dir
trigger_price = breakout_price
signal_type = 'breakout'
if used_1m:
precise_count += 1
elif direction:
elif direction and signal_type == 'one_fifth':
fallback_count += 1
# 无持仓 -> 开仓
# 无持仓 -> 开仓(只用五分之一信号开仓,突破信号不用于开仓)
if current_position is None:
if direction:
if direction and signal_type == 'one_fifth':
current_position = {
'direction': direction,
'entry_price': trigger_price,
@@ -369,19 +537,28 @@ def backtest_one_third_strategy(dates: List[str], min_body_size: float = 0.1):
else:
diff = current_position['entry_price'] - exit_price
# 记录信号类型
signal_type_str = '突破' if signal_type == 'breakout' else '五分之一'
trades.append({
'entry_time': datetime.datetime.fromtimestamp(current_position['entry_time'] / 1000),
'exit_time': datetime.datetime.fromtimestamp(curr['id'] / 1000),
'direction': '做多' if pos_dir == 'long' else '做空',
'entry': current_position['entry_price'],
'exit': exit_price,
'diff': diff
'diff': diff,
'signal_type': signal_type_str
})
stats[pos_dir]['total_profit'] += diff
if diff > 0:
stats[pos_dir]['wins'] += 1
# 如果是突破反手信号记录当前K线ID防止重复执行
if signal_type == 'breakout':
last_breakout_bar_id = curr_bar_id
breakout_reverse_count += 1
# 反手开仓
current_position = {
'direction': direction,
@@ -392,7 +569,7 @@ def backtest_one_third_strategy(dates: List[str], min_body_size: float = 0.1):
stats[direction]['count'] += 1
time_str = datetime.datetime.fromtimestamp(curr['id'] / 1000).strftime('%Y-%m-%d %H:%M')
logger.debug(f"[{time_str}] 平{pos_dir}反手{direction} @ {trigger_price:.2f} 盈亏={diff:.2f}")
logger.debug(f"[{time_str}] 平{pos_dir}反手{direction}({signal_type_str}) @ {trigger_price:.2f} 盈亏={diff:.2f}")
idx += 1
@@ -420,7 +597,7 @@ def backtest_one_third_strategy(dates: List[str], min_body_size: float = 0.1):
if diff > 0:
stats[pos_dir]['wins'] += 1
logger.info(f"回测完成使用1分钟精准判断 {precise_count} 次,使用开盘价距离判断 {fallback_count}")
logger.info(f"回测完成使用1分钟精准判断 {precise_count} 次,使用开盘价距离判断 {fallback_count} 次,突破反手信号 {breakout_reverse_count}")
return trades, stats
@@ -431,7 +608,12 @@ if __name__ == '__main__':
# ==================== 配置参数 ====================
START_DATE = "2025-01-01"
END_DATE = "2025-12-31"
MIN_BODY_SIZE = 0.1 # 最小实体大小
MIN_BODY_SIZE = 0.1 # 最小实体大小(绝对值)
FIRST_MINUTE_ONLY = True # 反手信号仅在3分钟K线的第一分钟触发才有效
# 突破反手信号配置
ENABLE_BREAKOUT_REVERSE = True # 是否启用"突破上一根K线高低点"反手信号
BREAKOUT_MIN_BODY_PERCENT = 0.1 # 突破反手信号的最小实体百分比0.1表示0.1%
# ==================== 生成查询日期列表 ====================
dates = []
@@ -455,7 +637,13 @@ if __name__ == '__main__':
exit(1)
# ==================== 执行回测 ====================
trades, stats = backtest_one_third_strategy(dates, MIN_BODY_SIZE)
trades, stats = backtest_one_third_strategy(
dates,
MIN_BODY_SIZE,
FIRST_MINUTE_ONLY,
ENABLE_BREAKOUT_REVERSE,
BREAKOUT_MIN_BODY_PERCENT
)
# ==================== 输出交易详情 ====================
logger.info("===== 每笔交易详情 =====")
@@ -511,6 +699,8 @@ if __name__ == '__main__':
print(f"{'='*60}")
print(f"回测周期:{START_DATE}{END_DATE}")
print(f"最小实体要求:{MIN_BODY_SIZE}")
print(f"反手信号仅第一分钟有效:{'' if FIRST_MINUTE_ONLY else ''}")
print(f"突破反手信号:{'启用' if ENABLE_BREAKOUT_REVERSE else '禁用'}(最小实体{BREAKOUT_MIN_BODY_PERCENT}%")
print(f"{'='*60}")
print(f"总交易笔数:{len(trades)}")
print(f"总点差:{total_points_profit:.2f}")
@@ -536,7 +726,7 @@ if __name__ == '__main__':
with open(csv_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=[
'entry_time', 'exit_time', 'direction', 'entry', 'exit',
'point_diff', 'raw_profit', 'fee', 'net_profit'
'point_diff', 'raw_profit', 'fee', 'net_profit', 'signal_type'
])
writer.writeheader()
for t in trades:
@@ -549,6 +739,7 @@ if __name__ == '__main__':
'point_diff': t['point_diff'],
'raw_profit': t['raw_profit'],
'fee': t['fee'],
'net_profit': t['net_profit']
'net_profit': t['net_profit'],
'signal_type': t.get('signal_type', '五分之一')
})
print(f"\n交易记录已保存到:{csv_path}")

View File

@@ -71,6 +71,11 @@ class BitmartOneFifthStrategy:
self.last_trigger_kline_id = None
self.last_trigger_direction = None
self.last_trade_kline_id = None
# 反手信号当前价格容差(美元)
# 当检测到反手信号时,当前价格必须在触发价附近才执行
# 避免"先涨后跌"或"先跌后涨"的情况下错误开仓
self.reverse_price_tolerance = 2.0 # 2美元容差
# ========================= 五分之一策略核心 =========================
@@ -169,6 +174,8 @@ class BitmartOneFifthStrategy:
逻辑优化:
- 当已有持仓时,只关心反向信号(有多仓只看空信号,有空仓只看多信号)
- 无仓位时用1分钟K线判断先触发的方向
- 【重要】反手信号不仅要求K线高/低点触及触发价,还要求当前价格在触发价附近
避免"先涨后跌""先跌后涨"的情况下错误开仓
返回:(方向, 触发价格, 有效前一根K线, 当前K线) 或 (None, None, None, None)
"""
@@ -189,6 +196,8 @@ class BitmartOneFifthStrategy:
c_high = float(curr['high'])
c_low = float(curr['low'])
c_close = float(curr['close']) # 当前价格(用于检查价格是否仍在触发价附近)
long_triggered = c_high >= long_trigger
short_triggered = c_low <= short_trigger
@@ -199,13 +208,24 @@ class BitmartOneFifthStrategy:
if current_position == 1:
# 当前是多仓,只关心空信号(用于平多开空)
if short_triggered:
direction = 'short'
trigger_price = short_trigger
# 【重要】额外检查:当前价格必须在做空触发价附近或下方
# 如果价格已经涨回去了(当前价格远高于做空触发价),则不触发
if c_close <= short_trigger + self.reverse_price_tolerance:
direction = 'short'
trigger_price = short_trigger
else:
logger.debug(f"空信号被过滤:当前价格{c_close:.2f}已远离做空触发价{short_trigger:.2f}(容差{self.reverse_price_tolerance}")
elif current_position == -1:
# 当前是空仓,只关心多信号(用于平空开多)
if long_triggered:
direction = 'long'
trigger_price = long_trigger
# 【重要】额外检查:当前价格必须在做多触发价附近或上方
# 如果价格已经跌回去了(当前价格远低于做多触发价),则不触发
if c_close >= long_trigger - self.reverse_price_tolerance:
direction = 'long'
trigger_price = long_trigger
else:
logger.debug(f"多信号被过滤:当前价格{c_close:.2f}已远离做多触发价{long_trigger:.2f}(容差{self.reverse_price_tolerance}")
else:
# 无仓位,按原逻辑判断先触发的方向
if long_triggered and short_triggered: