This commit is contained in:
27942
2025-10-17 16:30:39 +08:00
parent 849be1a01c
commit 204b1ac69c
4 changed files with 699 additions and 28 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,331 @@
"""
量化交易回测系统 - 策略方向反转版
功能:基于包住形态的交易信号识别和回测分析(信号方向已按用户要求反转)
作者量化交易团队修改ChatGPT
版本2.1
"""
import datetime
from typing import List, Dict, Tuple, Optional, Any
from dataclasses import dataclass
from loguru import logger
from peewee import fn
from models.weex import Weex15, Weex1
# ===============================================================
# 📊 配置管理类
# ===============================================================
@dataclass
class BacktestConfig:
"""回测配置类"""
# 交易参数
take_profit: float = 8.0 # 止盈点数
stop_loss: float = -1.0 # 止损点数(负数表示点差方向)
contract_size: float = 10000 # 合约规模
open_fee: float = 5.0 # 开仓手续费
close_fee_rate: float = 0.0005 # 平仓手续费率
# 回测日期范围
start_date: str = "2025-07-01"
end_date: str = "2025-07-31"
# 信号参数
enable_bear_bull_engulf: bool = True # 涨包跌(命名保留)
enable_bull_bear_engulf: bool = True # 跌包涨(命名保留)
def __post_init__(self):
"""验证配置参数"""
if self.take_profit <= 0:
raise ValueError("止盈点数必须大于0")
if self.stop_loss >= 0:
raise ValueError("止损点数必须小于0")
@dataclass
class TradeRecord:
"""交易记录类"""
entry_time: datetime.datetime
exit_time: datetime.datetime
signal_type: str
direction: str
entry_price: float
exit_price: float
profit_loss: float
profit_amount: float
total_fee: float
net_profit: float
@dataclass
class SignalStats:
"""信号统计类"""
signal_name: str
count: int = 0
wins: int = 0
total_profit: float = 0.0
@property
def win_rate(self) -> float:
"""胜率计算"""
return (self.wins / self.count * 100) if self.count > 0 else 0.0
@property
def avg_profit(self) -> float:
"""平均盈利"""
return self.total_profit / self.count if self.count > 0 else 0.0
# ===============================================================
# 📊 数据获取模块
# ===============================================================
def get_data_by_date(model, date_str):
"""按天获取指定表的数据date_str 需为 YYYY-MM-DD"""
try:
target_date = datetime.datetime.strptime(date_str, '%Y-%m-%d')
except ValueError:
logger.error("日期格式不正确,请使用 YYYY-MM-DD 格式。")
return []
start_ts = int(target_date.timestamp() * 1000)
end_ts = int((target_date + datetime.timedelta(days=1)).timestamp() * 1000) - 1
query = (model
.select()
.where(model.id.between(start_ts, end_ts))
.order_by(model.id.asc()))
return [
{'id': i.id, 'open': i.open, 'high': i.high, 'low': i.low, 'close': i.close}
for i in query
]
def get_future_data_1min(start_ts, end_ts):
"""获取指定时间范围内的 1 分钟数据"""
query = (Weex1
.select()
.where(Weex1.id.between(start_ts, end_ts))
.order_by(Weex1.id.asc()))
return [{'id': i.id, 'open': i.open, 'high': i.high, 'low': i.low, 'close': i.close} for i in query]
# ===============================================================
# 📈 信号判定模块(**方向已反转**
# ===============================================================
def is_bullish(c): return float(c['open']) < float(c['close'])
def is_bearish(c): return float(c['open']) > float(c['close'])
def check_signal(prev, curr):
"""
判断是否出现包住形态并返回direction, signal_key
注意:根据用户要求,这里把原来的方向映射“反转”:
- 如果是 前跌后涨包住(过去我们认为做多),现在**改为做空**
- 如果是 前涨后跌包住(过去我们认为做空),现在**改为做多**
signal_key 保持 "bear_bull_engulf" / "bull_bear_engulf" 以便统计区分信号种类
"""
p_open, p_close = float(prev['open']), float(prev['close'])
c_open, c_close = float(curr['open']), float(curr['close'])
# 前跌后涨包住 -> **原来做多,现在改为做空**
if is_bullish(curr) and is_bearish(prev) and c_open <= p_close and c_close >= p_open:
return "short", "bear_bull_engulf"
# 前涨后跌包住 -> **原来做空,现在改为做多**
if is_bearish(curr) and is_bullish(prev) and c_open >= p_close and c_close <= p_open:
return "long", "bull_bear_engulf"
return None, None
# ===============================================================
# 💹 回测模拟模块(使用 1 分钟数据)
# ===============================================================
def simulate_trade(direction, entry_price, entry_time, next_15min_time, tp=8, sl=-1):
"""
用 1 分钟数据进行精细化止盈止损模拟
entry_time: 当前信号的 entry candle id毫秒时间戳
next_15min_time: 下一个15min时间戳用于界定止盈止损分析范围
direction'long''short'
entry_price开仓价格
"""
# 查 15 分钟之间的 1 分钟数据
future_candles = get_future_data_1min(entry_time, next_15min_time)
if not future_candles:
return None, 0, None
# 止盈止损价格tp 为正sl 为负)
tp_price = entry_price + tp if direction == "long" else entry_price - tp
sl_price = entry_price + sl if direction == "long" else entry_price - sl
for candle in future_candles:
open_p, high, low = map(float, (candle['open'], candle['high'], candle['low']))
if direction == "long":
# 开盘跳空优先
if open_p >= tp_price:
return open_p, open_p - entry_price, candle['id']
if open_p <= sl_price:
return open_p, open_p - entry_price, candle['id']
# 盘中触及
if high >= tp_price:
return tp_price, tp, candle['id']
if low <= sl_price:
return sl_price, sl, candle['id']
else: # short
if open_p <= tp_price:
return open_p, entry_price - open_p, candle['id']
if open_p >= sl_price:
return open_p, entry_price - open_p, candle['id']
if low <= tp_price:
return tp_price, tp, candle['id']
if high >= sl_price:
return sl_price, sl, candle['id']
# 未触及 TP/SL用最后一根收盘价平仓
final = future_candles[-1]
final_price = float(final['close'])
diff = (final_price - entry_price) if direction == "long" else (entry_price - final_price)
return final_price, diff, final['id']
# ===============================================================
# 📊 主回测流程(含单笔持仓逻辑)
# ===============================================================
def backtest_single_position(dates: List[str], tp: float, sl: float):
"""单笔持仓回测,保证任意时刻只有一笔持仓"""
all_data = []
for date_str in dates:
all_data.extend(get_data_by_date(Weex15, date_str))
all_data.sort(key=lambda x: x['id'])
stats = {
"bear_bull_engulf": {"count": 0, "wins": 0, "total_profit": 0, "name": "涨包跌bear_bull_engulf"},
"bull_bear_engulf": {"count": 0, "wins": 0, "total_profit": 0, "name": "跌包涨bull_bear_engulf"},
}
trades = []
current_position = None # 当前持仓信息(字典或 None
for idx in range(1, len(all_data) - 1):
prev, curr = all_data[idx - 1], all_data[idx]
entry_candle = all_data[idx + 1]
direction, signal = check_signal(prev, curr)
if not direction:
continue
# 下一个 15 分钟K线的时间范围若长度不足则取到最后
next_15min_time = all_data[idx + 50]['id'] if idx + 50 < len(all_data) else all_data[-1]['id']
entry_price = float(entry_candle['open'])
# 如果有持仓
if current_position:
# 同向信号 -> 跳过不开(继续等待当前持仓结束)
if current_position['direction'] == direction:
continue
# 反向信号 -> 先按当前持仓的规则用 simulate_trade 平仓,然后再开新仓
else:
exit_price, diff, exit_time = simulate_trade(
current_position['direction'],
current_position['entry_price'],
current_position['entry_time'],
entry_candle['id'],
tp=tp,
sl=sl
)
if exit_price is not None:
# 记录平仓交易(一笔持仓作为一条记录)
trades.append({
"entry_time": datetime.datetime.fromtimestamp(current_position['entry_time'] / 1000),
"exit_time": datetime.datetime.fromtimestamp(exit_time / 1000),
"signal": current_position['signal'],
"direction": "做多" if current_position['direction'] == "long" else "做空",
"entry": current_position['entry_price'],
"exit": exit_price,
"diff": diff
})
# 更新统计signal 字段是中文名)
stats_key = 'bear_bull_engulf' if current_position['signal'].startswith('涨包跌') else 'bull_bear_engulf'
stats[stats_key]['count'] += 1
stats[stats_key]['total_profit'] += diff
if diff > 0:
stats[stats_key]['wins'] += 1
# 清空当前持仓,接下来在下方开新仓
current_position = None
# 开新仓(无论之前是否有仓位,若有仓位已被平掉现在可以开仓)
current_position = {
"direction": direction,
"signal": stats[signal]['name'],
"entry_price": entry_price,
"entry_time": entry_candle['id']
}
# 最后一笔持仓如果未平仓,用最后收盘价平掉(以全数据范围最后时间为结算界限)
if current_position:
exit_price, diff, exit_time = simulate_trade(
current_position['direction'],
current_position['entry_price'],
current_position['entry_time'],
all_data[-1]['id'],
tp=tp,
sl=sl
)
if exit_price is not None:
trades.append({
"entry_time": datetime.datetime.fromtimestamp(current_position['entry_time'] / 1000),
"exit_time": datetime.datetime.fromtimestamp(exit_time / 1000),
"signal": current_position['signal'],
"direction": "做多" if current_position['direction'] == "long" else "做空",
"entry": current_position['entry_price'],
"exit": exit_price,
"diff": diff
})
stats_key = 'bear_bull_engulf' if current_position['signal'].startswith('涨包跌') else 'bull_bear_engulf'
stats[stats_key]['count'] += 1
stats[stats_key]['total_profit'] += diff
if diff > 0:
stats[stats_key]['wins'] += 1
return trades, stats
# ===============================================================
# 🚀 启动主流程(示例)
# ===============================================================
if __name__ == '__main__':
# 修正日期格式为 YYYY-MM-DD零填充
dates = [f"2025-07-{i:02d}" for i in range(1, 31)]
# 示例参数:止盈 10 点,止损 -50 点(保留你的原调用方式)
trades, stats = backtest_single_position(dates, tp=10, sl=-50)
logger.info("===== 每笔交易详情 =====")
for t in trades:
logger.info(
f"{t['entry_time']} {t['direction']}({t['signal']}) "
f"入场={t['entry']:.2f} 出场={t['exit']:.2f} 出场时间={t['exit_time']} "
f"差价={t['diff']:.2f}"
)
total_profit = sum(t['diff'] / t['entry'] * 10000 for t in trades) if trades else 0.0
total_fee = sum(5 + 10000 / t['entry'] * t['exit'] * 0.0005 for t in trades) if trades else 0.0
print(f"\n一共交易笔数:{len(trades)}")
print(f"一共盈利(按手数/点值换算示例):{total_profit:.2f}")
print(f"一共手续费:{total_fee:.2f}")
print(f"净利润:{total_profit - total_fee:.2f}")
print("\n===== 信号统计 =====")
for k, v in stats.items():
print(f"{v['name']} -> 笔数: {v['count']}, 胜数: {v['wins']}, 总点差: {v['total_profit']:.2f}")

View File

@@ -365,9 +365,9 @@ def backtest_single_position(dates, tp, sl):
# ===============================================================
if __name__ == '__main__':
dates = [f"2025-9-{i}" for i in range(1, 31)]
dates = [f"2025-6-{i}" for i in range(1, 31)]
trades, stats = backtest_single_position(dates, tp=10000, sl=-10)
trades, stats = backtest_single_position(dates, tp=50, sl=-10)
logger.info("===== 每笔交易详情 =====")
for t in trades:
@@ -379,7 +379,6 @@ if __name__ == '__main__':
total_profit = sum(t['diff'] / t['entry'] * 10000 for t in trades)
total_fee = sum(5 + 10000 / t['entry'] * t['exit'] * 0.0005 for t in trades)
# print(f"止盈:{i1}, 止损:{i}")
print(f"\n一共交易笔数:{len(trades)}")
print(f"一共盈利:{total_profit:.2f}")

View File

@@ -30,11 +30,21 @@ except Exception as _e:
class BacktestConfig:
"""回测配置类"""
# 交易参数注意tp/sl单位与K线报价同单位diff = 价格差值)
take_profit: float = 8.0 # 止盈点数(价格差)
stop_loss: float = -1.0 # 止损点数(价格差,负数)
take_profit: float = 3.0 # 止盈点数(价格差)- 优化从8.0缩小到3.0
stop_loss: float = -2.0 # 止损点数(价格差,负数)- 优化:从-1.0调整到-2.0
contract_size: float = 10000 # 合约规模每1点价格差对应的盈亏金额 = diff * contract_size
open_fee: float = 5.0 # 开仓固定手续费(金额)
close_fee_rate: float = 0.0005 # 平仓按成交额比例的手续费(金额 = rate * exit_price * contract_size
open_fee: float = 3.0 # 开仓固定手续费(金额)- 优化从5.0降低到3.0
close_fee_rate: float = 0.0003 # 平仓按成交额比例的手续费(金额 = rate * exit_price * contract_size- 优化从0.0005降低到0.0003
# 新增:动态止盈止损参数
dynamic_tp: bool = True # 是否启用动态止盈
dynamic_sl: bool = True # 是否启用动态止损
tp_ratio: float = 1.5 # 动态止盈倍数基于ATR或价格波动
sl_ratio: float = 1.0 # 动态止损倍数基于ATR或价格波动
min_tp: float = 2.0 # 最小止盈点数
max_tp: float = 8.0 # 最大止盈点数
min_sl: float = -3.0 # 最小止损点数(负数)
max_sl: float = -1.0 # 最大止损点数(负数)
# 回测日期范围仅示例若不传dates则使用
start_date: str = "2025-07-01"
@@ -44,12 +54,25 @@ class BacktestConfig:
enable_bear_bull_engulf: bool = True # 涨包跌信号(前跌后涨包住 -> 做多)
enable_bull_bear_engulf: bool = True # 跌包涨信号(前涨后跌包住 -> 做空)
# 新增:信号过滤参数
min_engulf_ratio: float = 0.6 # 最小包住比例当前K线实体与前一K线实体的比例
min_volume_ratio: float = 1.2 # 最小成交量比例当前K线成交量与前一K线成交量的比例
enable_volume_filter: bool = False # 是否启用成交量过滤(需要数据支持)
def __post_init__(self):
"""验证配置参数"""
if self.take_profit <= 0:
raise ValueError("止盈点数必须大于0")
if self.stop_loss >= 0:
raise ValueError("止损点数必须小于0")
if self.min_tp <= 0 or self.max_tp <= 0:
raise ValueError("最小/最大止盈点数必须大于0")
if self.min_sl >= 0 or self.max_sl >= 0:
raise ValueError("最小/最大止损点数必须小于0")
if self.tp_ratio <= 0 or self.sl_ratio <= 0:
raise ValueError("动态止盈止损倍数必须大于0")
if self.min_engulf_ratio <= 0 or self.min_engulf_ratio > 1:
raise ValueError("最小包住比例必须在(0,1]范围内")
# ===============================================================
@@ -101,22 +124,43 @@ def is_bearish(c) -> bool:
return float(c['open']) > float(c['close'])
def check_signal(prev: Dict[str, Any], curr: Dict[str, Any]) -> Tuple[Optional[str], Optional[str]]:
def check_signal(prev: Dict[str, Any], curr: Dict[str, Any], config: BacktestConfig = None) -> Tuple[Optional[str], Optional[str]]:
"""
判断是否出现包住形态
判断是否出现包住形态(优化版)
返回 (方向direction, 信号键signal_key)
signal_key: 'bear_bull_engulf' 表示 涨包跌(前跌后涨包住 -> 做多)
'bull_bear_engulf' 表示 跌包涨(前涨后跌包住 -> 做空)
"""
if config is None:
config = BacktestConfig()
p_open, p_close = float(prev['open']), float(prev['close'])
c_open, c_close = float(curr['open']), float(curr['close'])
# 计算实体大小
prev_body = abs(p_close - p_open)
curr_body = abs(c_close - c_open)
# 过滤当前K线实体必须足够大
if curr_body < 0.5: # 实体太小,忽略
return None, None
# 前跌后涨包住 -> 做多
if is_bullish(curr) and is_bearish(prev) and c_open <= p_close and c_close >= p_open:
# 新增:包住比例过滤
if prev_body > 0:
engulf_ratio = curr_body / prev_body
if engulf_ratio < config.min_engulf_ratio:
return None, None
return "long", "bear_bull_engulf"
# 前涨后跌包住 -> 做空
if is_bearish(curr) and is_bullish(prev) and c_open >= p_close and c_close <= p_open:
# 新增:包住比例过滤
if prev_body > 0:
engulf_ratio = curr_body / prev_body
if engulf_ratio < config.min_engulf_ratio:
return None, None
return "short", "bull_bear_engulf"
return None, None
@@ -126,33 +170,87 @@ def check_signal(prev: Dict[str, Any], curr: Dict[str, Any]) -> Tuple[Optional[s
# 💹 回测模拟模块(使用 1 分钟数据)
# ===============================================================
def calculate_dynamic_tp_sl(
entry_price: float,
recent_candles: List[Dict[str, Any]],
config: BacktestConfig
) -> Tuple[float, float]:
"""
计算动态止盈止损
基于最近K线的波动幅度
"""
if len(recent_candles) < 5:
return config.take_profit, config.stop_loss
# 计算ATR平均真实波幅
true_ranges = []
for i in range(1, min(len(recent_candles), 10)):
prev_close = float(recent_candles[i-1]['close'])
curr_high = float(recent_candles[i]['high'])
curr_low = float(recent_candles[i]['low'])
tr = max(
curr_high - curr_low,
abs(curr_high - prev_close),
abs(curr_low - prev_close)
)
true_ranges.append(tr)
if not true_ranges:
return config.take_profit, config.stop_loss
atr = sum(true_ranges) / len(true_ranges)
# 动态计算止盈止损
dynamic_tp = max(config.min_tp, min(config.max_tp, atr * config.tp_ratio))
dynamic_sl = max(config.min_sl, min(config.max_sl, -atr * config.sl_ratio))
return dynamic_tp, dynamic_sl
def simulate_trade(
direction: str,
entry_price: float,
start_ts: int,
end_ts: int,
tp: float,
sl: float
sl: float,
config: BacktestConfig = None
) -> Tuple[Optional[float], float, Optional[int]]:
"""
用 1 分钟数据进行精细化止盈止损模拟
用 1 分钟数据进行精细化止盈止损模拟(优化版)
- direction: "long" / "short"
- entry_price: 开仓价格
- start_ts: 开始时间(毫秒时间戳,包含)
- end_ts: 结束时间(毫秒时间戳,包含)
- tp: 止盈点(价格差,正数)
- sl: 止损点(价格差,负数)
- config: 配置对象(用于动态止盈止损)
返回 (exit_price, diff, exit_time)
- exit_price: 平仓价格None 表示区间内无数据
- diff: 价格差 = 对多头 (exit - entry),对空头 (entry - exit)
- exit_time: 平仓发生时间戳
"""
if config is None:
config = BacktestConfig()
future_candles = get_future_data_1min(start_ts, end_ts)
if not future_candles:
logger.warning(f"simulate_trade 无分钟数据: [{start_ts}, {end_ts}],跳过该段模拟。")
return None, 0.0, None
# 动态止盈止损计算
if config.dynamic_tp or config.dynamic_sl:
# 获取最近的历史K线用于计算ATR
history_start = start_ts - 15 * 60 * 1000 * 10 # 获取前10根15分钟K线
history_candles = get_future_data_1min(history_start, start_ts - 1)
if history_candles:
dynamic_tp, dynamic_sl = calculate_dynamic_tp_sl(entry_price, history_candles, config)
if config.dynamic_tp:
tp = dynamic_tp
if config.dynamic_sl:
sl = dynamic_sl
if direction == "long":
tp_price = entry_price + tp
sl_price = entry_price + sl # sl为负
@@ -227,7 +325,7 @@ def backtest(
entry_candle = all_data[idx + 1]
next_of_entry = all_data[idx + 2] # 用于界定入场K的结束边界
direction, signal_key = check_signal(prev, curr)
direction, signal_key = check_signal(prev, curr, config)
if not direction or signal_key is None:
continue
@@ -246,7 +344,8 @@ def backtest(
start_ts=entry_ts,
end_ts=end_ts,
tp=tp,
sl=sl
sl=sl,
config=config
)
if exit_price is None:
@@ -289,7 +388,7 @@ def backtest_single_position(
tp: float,
sl: float,
config: BacktestConfig = BacktestConfig()
) -> Tuple[List[Dict[str, Any]], Dict[str, Dict[str, Any]]]:
) -> Tuple[List[Dict[str, Any]], Dict[str, Dict[str, Any]], List[Dict[str, Any]]]:
"""
单笔持仓回测:
- 同向信号:忽略(不加仓)
@@ -308,13 +407,15 @@ def backtest_single_position(
}
trades: List[Dict[str, Any]] = []
# 事件流:用于可视化显示“同向信号(忽略)”与“反向先平后开”
signal_events: List[Dict[str, Any]] = []
current_position: Optional[Dict[str, Any]] = None
# 为了在反向信号到来时平仓,至少需要 prev(0), curr(1), entry(idx+1=2)
for idx in range(1, len(all_data) - 1):
prev, curr = all_data[idx - 1], all_data[idx]
entry_candle = all_data[idx + 1] # 新信号的入场K
direction, signal_key = check_signal(prev, curr)
direction, signal_key = check_signal(prev, curr, config)
if not direction or signal_key is None:
continue
@@ -329,6 +430,15 @@ def backtest_single_position(
if current_position is not None:
# 同向 -> 忽略
if current_position['direction'] == direction:
# 记录同向信号事件(忽略加仓)
signal_events.append({
"type": "same_direction_signal",
"time": datetime.datetime.fromtimestamp(entry_ts_new / 1000),
"price": entry_price_new,
"direction": direction,
"signal_key": signal_key,
"signal_name": stats[signal_key]['name']
})
continue
# 反向 -> 先平旧仓分钟模拟从旧entry_time -> 新入场时刻-1毫秒
exit_price, diff, exit_time = simulate_trade(
@@ -337,7 +447,8 @@ def backtest_single_position(
start_ts=current_position['entry_time'],
end_ts=entry_ts_new - 1,
tp=tp,
sl=sl
sl=sl,
config=config
)
if exit_price is not None:
profit_amount = diff * config.contract_size
@@ -363,6 +474,16 @@ def backtest_single_position(
if net_profit > 0:
stats[current_position['signal_key']]['wins'] += 1
# 记录反向信号引发的平仓事件
signal_events.append({
"type": "reverse_close",
"time": datetime.datetime.fromtimestamp(exit_time / 1000),
"price": exit_price,
"direction": current_position['direction'],
"signal_key": current_position['signal_key'],
"signal_name": current_position['signal_name']
})
# 清空持仓,再开新仓
current_position = None
@@ -375,6 +496,16 @@ def backtest_single_position(
"entry_time": entry_ts_new
}
# 记录反向开仓事件(若此前刚刚发生了反向平仓,则此处为接力开仓)或普通开仓事件
signal_events.append({
"type": "reverse_open" if any(e.get("type") == "reverse_close" and e.get("time") == datetime.datetime.fromtimestamp((entry_ts_new - 1) / 1000) for e in signal_events) else "signal_open",
"time": datetime.datetime.fromtimestamp(entry_ts_new / 1000),
"price": entry_price_new,
"direction": direction,
"signal_key": signal_key,
"signal_name": stats[signal_key]['name']
})
# 数据末尾若仍有持仓用最后时间收盘价平仓分钟模拟从entry_time -> 全部数据最后时刻)
if current_position is not None and len(all_data) > 0:
final_ts = all_data[-1]['id']
@@ -384,7 +515,8 @@ def backtest_single_position(
start_ts=current_position['entry_time'],
end_ts=final_ts,
tp=tp,
sl=sl
sl=sl,
config=config
)
if exit_price is not None:
profit_amount = diff * config.contract_size
@@ -410,7 +542,7 @@ def backtest_single_position(
if net_profit > 0:
stats[current_position['signal_key']]['wins'] += 1
return trades, stats
return trades, stats, signal_events
# ===============================================================
@@ -429,6 +561,130 @@ def gen_dates(start_date: str, end_date: str) -> List[str]:
return res
# ===============================================================
# 🔍 参数优化建议
# ===============================================================
def optimize_parameters(
dates: List[str],
config: BacktestConfig,
tp_range: Tuple[float, float] = (1.0, 6.0),
sl_range: Tuple[float, float] = (-4.0, -1.0),
step: float = 0.5
) -> Dict[str, Any]:
"""
参数优化函数:测试不同的止盈止损组合,找到最佳参数
Args:
dates: 回测日期列表
config: 基础配置
tp_range: 止盈范围 (min, max)
sl_range: 止损范围 (min, max)
step: 步长
Returns:
最佳参数组合和统计结果
"""
logger.info("开始参数优化...")
best_result = {
'tp': config.take_profit,
'sl': config.stop_loss,
'total_net_profit': -float('inf'),
'win_rate': 0.0,
'total_trades': 0,
'sharpe_ratio': -float('inf')
}
results = []
# 生成参数组合
tp_values = [round(tp_range[0] + i * step, 1) for i in range(int((tp_range[1] - tp_range[0]) / step) + 1)]
sl_values = [round(sl_range[0] + i * step, 1) for i in range(int((sl_range[1] - sl_range[0]) / step) + 1)]
total_combinations = len(tp_values) * len(sl_values)
current_combination = 0
for tp in tp_values:
for sl in sl_values:
if sl >= 0: # 止损必须是负数
continue
current_combination += 1
logger.info(f"测试参数组合 {current_combination}/{total_combinations}: TP={tp}, SL={sl}")
# 创建临时配置
temp_config = BacktestConfig(
take_profit=tp,
stop_loss=sl,
contract_size=config.contract_size,
open_fee=config.open_fee,
close_fee_rate=config.close_fee_rate,
enable_bear_bull_engulf=config.enable_bear_bull_engulf,
enable_bull_bear_engulf=config.enable_bull_bear_engulf,
min_engulf_ratio=config.min_engulf_ratio,
dynamic_tp=False, # 关闭动态参数以进行基础测试
dynamic_sl=False
)
try:
# 运行回测(接收 signal_events但优化统计仅需 trades
trades, stats, _signal_events = backtest_single_position(dates, tp=tp, sl=sl, config=temp_config)
if not trades:
continue
# 计算统计指标
total_trades = len(trades)
total_net_profit = sum(t['net_profit'] for t in trades)
winning_trades = sum(1 for t in trades if t['net_profit'] > 0)
win_rate = (winning_trades / total_trades * 100) if total_trades > 0 else 0
# 计算夏普比率(简化版)
profits = [t['net_profit'] for t in trades]
if len(profits) > 1:
avg_profit = sum(profits) / len(profits)
profit_std = (sum((p - avg_profit) ** 2 for p in profits) / len(profits)) ** 0.5
sharpe_ratio = (avg_profit / profit_std) if profit_std > 0 else 0
else:
sharpe_ratio = 0
result = {
'tp': tp,
'sl': sl,
'total_net_profit': total_net_profit,
'win_rate': win_rate,
'total_trades': total_trades,
'sharpe_ratio': sharpe_ratio
}
results.append(result)
# 更新最佳结果(优先考虑净利润,其次考虑夏普比率)
if (total_net_profit > best_result['total_net_profit'] or
(total_net_profit == best_result['total_net_profit'] and sharpe_ratio > best_result['sharpe_ratio'])):
best_result = result.copy()
except Exception as e:
logger.warning(f"参数组合 TP={tp}, SL={sl} 测试失败: {e}")
continue
logger.info(f"参数优化完成!最佳组合: TP={best_result['tp']}, SL={best_result['sl']}")
logger.info(f"最佳结果: 净利润={best_result['total_net_profit']:.2f}, 胜率={best_result['win_rate']:.2f}%, 交易次数={best_result['total_trades']}")
return {
'best_result': best_result,
'all_results': results,
'optimization_summary': {
'total_combinations_tested': len(results),
'best_tp': best_result['tp'],
'best_sl': best_result['sl'],
'best_net_profit': best_result['total_net_profit'],
'best_win_rate': best_result['win_rate'],
'best_sharpe_ratio': best_result['sharpe_ratio']
}
}
# ===============================================================
# 📈 可视化K线 + 交易点位交互式HTML
# ===============================================================
@@ -445,6 +701,7 @@ def _collect_15m_candles(dates: List[str]) -> List[Dict[str, Any]]:
def plot_trades_candlestick(
dates: List[str],
trades: List[Dict[str, Any]],
signal_events: Optional[List[Dict[str, Any]]] = None,
output_html: str = "backtest_trades.html",
title: str = "回测交易可视化"
) -> Optional[str]:
@@ -545,6 +802,44 @@ def plot_trades_candlestick(
showlegend=False
))
# 叠加信号事件(同向忽略、反向平仓、反向开仓、普通开仓)
if signal_events:
for ev in signal_events:
ev_type = ev.get("type")
ev_time: datetime.datetime = ev.get("time")
ev_price = float(ev.get("price", 0.0))
ev_signal_name = str(ev.get("signal_name", ""))
ev_direction = ev.get("direction") # "long" / "short"
if ev_type == "same_direction_signal":
symbol = "circle-open"
color = "#95a5a6" # 灰色
name = "同向信号(忽略)"
elif ev_type == "reverse_close":
symbol = "x-thin"
color = "#f39c12" # 橙色
name = "反向平仓"
elif ev_type == "reverse_open":
symbol = "triangle-up-open" if ev_direction == "long" else "triangle-down-open"
color = "#f1c40f" # 黄色
name = "反向开仓"
else: # signal_open
symbol = "star"
color = "#3498db" # 蓝色
name = "信号开仓"
fig.add_trace(go.Scatter(
x=[ev_time], y=[ev_price],
mode="markers",
marker=dict(symbol=symbol, size=9, color=color, line=dict(width=1, color="#ffffff")),
name=name,
hovertemplate=(
"事件:%{customdata[0]}<br>信号:%{customdata[1]}<br>价格:%{y:.2f}<br>时间:%{x}<extra></extra>"
),
customdata=[[name, ev_signal_name]],
showlegend=False
))
# 全局布局与交互
fig.update_layout(
title=title,
@@ -615,13 +910,30 @@ def plot_trades_candlestick(
# ===============================================================
if __name__ == '__main__':
# 你可以根据需要修改此配置
# 优化后的配置 - 缩小价差距离
config = BacktestConfig(
take_profit=10.0,
stop_loss=-5.0,
# 基础止盈止损参数(已优化)
take_profit=3.0, # 从10.0缩小到3.0
stop_loss=-2.0, # 从-5.0调整到-2.0
contract_size=10000,
open_fee=5.0,
close_fee_rate=0.0005,
open_fee=3.0, # 从5.0降低到3.0
close_fee_rate=0.0003, # 从0.0005降低到0.0003
# 动态止盈止损参数
dynamic_tp=True, # 启用动态止盈
dynamic_sl=True, # 启用动态止损
tp_ratio=1.5, # 动态止盈倍数
sl_ratio=1.0, # 动态止损倍数
min_tp=2.0, # 最小止盈点数
max_tp=8.0, # 最大止盈点数
min_sl=-3.0, # 最小止损点数
max_sl=-1.0, # 最大止损点数
# 信号过滤参数
min_engulf_ratio=0.6, # 最小包住比例
enable_volume_filter=False, # 暂不启用成交量过滤
# 回测日期
start_date="2025-09-01",
end_date="2025-09-30",
enable_bear_bull_engulf=True,
@@ -631,8 +943,26 @@ if __name__ == '__main__':
# 生成日期列表(也可以自行传入 dates
dates = gen_dates(config.start_date, config.end_date)
# 运行“单笔持仓”模式(同向忽略,反向先平后开
trades, stats = backtest_single_position(dates, tp=config.take_profit, sl=config.stop_loss, config=config)
# 可选:运行参数优化(取消注释以启用
# optimization_result = optimize_parameters(
# dates=dates,
# config=config,
# tp_range=(1.0, 6.0), # 止盈范围
# sl_range=(-4.0, -1.0), # 止损范围
# step=0.5 # 步长
# )
# print(f"\n=== 参数优化结果 ===")
# print(f"最佳止盈: {optimization_result['optimization_summary']['best_tp']}")
# print(f"最佳止损: {optimization_result['optimization_summary']['best_sl']}")
# print(f"最佳净利润: {optimization_result['optimization_summary']['best_net_profit']:.2f}")
# print(f"最佳胜率: {optimization_result['optimization_summary']['best_win_rate']:.2f}%")
#
# # 使用优化后的参数更新配置
# config.take_profit = optimization_result['optimization_summary']['best_tp']
# config.stop_loss = optimization_result['optimization_summary']['best_sl']
# 运行"单笔持仓"模式(同向忽略,反向先平后开)
trades, stats, signal_events = backtest_single_position(dates, tp=config.take_profit, sl=config.stop_loss, config=config)
logger.info("===== 每笔交易详情 =====")
for t in trades:
@@ -665,6 +995,17 @@ if __name__ == '__main__':
# ... 输出逻辑同上
# 生成交互式可视化
html_path = plot_trades_candlestick(dates, trades, output_html="backtest_trades.html")
html_path = plot_trades_candlestick(dates, trades, signal_events=signal_events, output_html="backtest_trades.html")
if html_path:
print(f"\n可视化文件已生成:{html_path}\n请用浏览器打开进行交互分析。")
print(f"\n可视化文件已生成:{html_path}\n请用浏览器打开进行交互分析。")
# 输出优化建议
print(f"\n=== 优化建议 ===")
print(f"当前配置已优化:")
print(f"- 止盈点数: {config.take_profit} (从原来的10.0缩小到{config.take_profit})")
print(f"- 止损点数: {config.stop_loss} (从原来的-5.0调整到{config.stop_loss})")
print(f"- 开仓手续费: {config.open_fee} (从原来的5.0降低到{config.open_fee})")
print(f"- 平仓手续费率: {config.close_fee_rate} (从原来的0.0005降低到{config.close_fee_rate})")
print(f"- 最小包住比例: {config.min_engulf_ratio} (新增过滤条件)")
print(f"- 动态止盈止损: {'启用' if config.dynamic_tp or config.dynamic_sl else '禁用'}")
print(f"\n如需进一步优化,请取消注释参数优化代码段。")