加入精准回测数据
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user