diff --git a/models/database.db b/models/database.db index ceb5482..ea562c1 100644 Binary files a/models/database.db and b/models/database.db differ diff --git a/weex/优化开仓方向版本.py b/weex/优化开仓方向版本.py new file mode 100644 index 0000000..66fd2b7 --- /dev/null +++ b/weex/优化开仓方向版本.py @@ -0,0 +1,398 @@ +""" +量化交易回测系统 - 优化版 +功能:基于包住形态的交易信号识别和回测分析 +作者:量化交易团队 +版本:2.0 +""" + +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-7-1" + end_date: str = "2025-7-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): + """按天获取指定表的数据""" + 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): + """判断是否出现包住形态""" + 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 "long", "bear_bull_engulf" + + # 前涨后跌包住 -> 做空 + if is_bearish(curr) and is_bullish(prev) and c_open >= p_close and c_close <= p_open: + return "short", "bull_bear_engulf" + + return None, None + + +# =============================================================== +# 💹 回测模拟模块(使用 1 分钟数据) +# =============================================================== + +# ---------- 替换后的 simulate_trade ---------- +def simulate_trade(direction, entry_price, entry_time, next_15min_time, tp=8, sl=-1): + """ + 返回 (exit_price, profit_diff, exit_time, exit_reason) + exit_reason: 'tp' (触及止盈并以tp价平仓), + 'sl' (触及止损并以sl价平仓), + 'open_tp' (开盘跳空并以开盘价止盈), + 'open_sl' (开盘跳空并以开盘价止损), + 'timeout' (到分析窗口末尾,用最后一根收盘价平仓) + 注意:sl 参数为负数(你的设定),但在计算止损价时已处理方向 + """ + future_candles = get_future_data_1min(entry_time, next_15min_time) + if not future_candles: + return None, 0.0, None, None + + # 计算目标价位(数值) + tp_price = entry_price + tp if direction == "long" else entry_price - tp + sl_price = entry_price + sl if direction == "long" else entry_price - sl # sl 为负数,long 情况为 entry + (负数) -> 小于 entry + + for candle in future_candles: + open_p = float(candle['open']) + high = float(candle['high']) + low = float(candle['low']) + ts = candle['id'] + + if direction == "long": + # 开盘跳空(以开盘价直接触及止盈/止损) + if open_p >= tp_price: + return open_p, open_p - entry_price, ts, 'open_tp' + if open_p <= sl_price: + return open_p, open_p - entry_price, ts, 'open_sl' + # 盘中触及 + if high >= tp_price: + return tp_price, tp, ts, 'tp' + if low <= sl_price: + return sl_price, sl, ts, 'sl' + else: # short + if open_p <= tp_price: + return open_p, entry_price - open_p, ts, 'open_tp' + if open_p >= sl_price: + return open_p, entry_price - open_p, ts, 'open_sl' + if low <= tp_price: + return tp_price, tp, ts, 'tp' + if high >= sl_price: + return sl_price, sl, ts, 'sl' + + # 未触发止盈止损,用最后一根收盘价平仓(视为 timeout) + 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'], 'timeout' + + +# ---------- 替换后的 backtest_single_position ---------- +def backtest_single_position(dates, tp, sl): + """ + 单笔持仓回测(增强版),加入连续3次止损触发反向开仓(转向单)逻辑 + - 只有当 exit_reason 属于 'sl' 或 'open_sl' 时才算“真实止损” + - 当连续真实止损计数达到 3 时,**下一笔**信号反向开仓(且该转向单不计入连续止损统计) + """ + 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": "涨包跌"}, + "bull_bear_engulf": {"count": 0, "wins": 0, "total_profit": 0, "name": "跌包涨"}, + } + + trades = [] + current_position = None # 当前持仓信息 dict or None + + consec_sl_count = 0 # 连续真实止损计数(只有 exit_reason 为 'sl' 或 'open_sl' 时 +1) + reverse_next_signal = False # 是否需要将下一笔信号取反(由连续3次止损触发) + ignore_next_result = False # 标记下一笔成交的结果是否要被忽略(用于转向单:转向单不计入连续止损计数,也不影响计数) + + 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 + + # 如果需要反向下一笔信号(由之前三次止损触发),则翻转 direction 并标记为转向单(只对这一次有效) + is_reversal_trade = False + if reverse_next_signal: + direction = 'long' if direction == 'short' else 'short' + is_reversal_trade = True + reverse_next_signal = False + ignore_next_result = True # 这笔成交的平仓结果不影响 consec_sl_count + + # 下一个 15 分钟K线的时间范围(用 idx+50 作为 15min 窗口近似) + 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 + # 反向信号 -> 先按当前位置止盈止损平仓,再根据规则决定是否开新仓 + else: + exit_price, diff, exit_time, exit_reason = 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, + "exit_reason": exit_reason, + "is_reversal_trade": current_position.get('is_reversal_trade', False) + }) + # 更新统计(只有非转向单会计入统计?按你原逻辑计入) + stats_key = 'bear_bull_engulf' if current_position['signal'] == '涨包跌' else 'bull_bear_engulf' + stats[stats_key]['count'] += 1 + stats[stats_key]['total_profit'] += diff + if diff > 0: + stats[stats_key]['wins'] += 1 + + # 根据 exit_reason 更新 consec_sl_count(但如果当时该仓为被标记为 ignore_result,则不变) + if not current_position.get('ignore_result', False): + if exit_reason in ('sl', 'open_sl'): + consec_sl_count += 1 + else: + consec_sl_count = 0 + + # 如果累计达到 3 次真实止损 -> 标记下一笔反向 + if consec_sl_count >= 3: + reverse_next_signal = True + consec_sl_count = 0 # 触发后清零(下一笔为转向单) + else: + # 如果这笔被标记为 ignore(通常是前面是转向单),则不影响计数 + pass + + current_position = None # 清空持仓 + + # 开新仓(注意:如果这笔是转向单,我们之前已经取反了 direction,并设置了 is_reversal_trade 和 ignore_next_result) + current_position = { + "direction": direction, + "signal": stats[signal]['name'], + "entry_price": entry_price, + "entry_time": entry_candle['id'], + "is_reversal_trade": is_reversal_trade, + "ignore_result": ignore_next_result # 如果为 True,则本仓平仓结果不计入 consec_sl_count + } + + # 如果我们标记了 ignore_next_result(说明这是转向单),只在刚开仓后清除标记,确保仅忽略这笔的**平仓**结果 + if ignore_next_result: + # 清掉,只有这笔仓的平仓结果需要被忽略(记录在 current_position 中), + # 后续在处理平仓时会读取 current_position['ignore_result'] 并决定是否影响计数 + ignore_next_result = False + + # 最后一笔持仓如果未平仓,用最后收盘价平掉 + if current_position: + exit_price, diff, exit_time, exit_reason = 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, + "exit_reason": exit_reason, + "is_reversal_trade": current_position.get('is_reversal_trade', False) + }) + stats_key = 'bear_bull_engulf' if current_position['signal'] == '涨包跌' else 'bull_bear_engulf' + stats[stats_key]['count'] += 1 + stats[stats_key]['total_profit'] += diff + if diff > 0: + stats[stats_key]['wins'] += 1 + + # 最后一笔是否计入连续止损计数 + if not current_position.get('ignore_result', False): + if exit_reason in ('sl', 'open_sl'): + consec_sl_count += 1 + else: + consec_sl_count = 0 + if consec_sl_count >= 3: + # 可选择记录或告警,这里仅重置计数 + consec_sl_count = 0 + + return trades, stats + + + +# =============================================================== +# 🚀 启动主流程 +# =============================================================== + +if __name__ == '__main__': + dates = [f"2025-9-{i}" for i in range(1, 31)] + + trades, stats = backtest_single_position(dates, tp=50, sl=-10) + + 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) + total_fee = sum(5 + 10000 / t['entry'] * t['exit'] * 0.0005 for t in trades) + + 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 i in range(1, 16): + # for i1 in range(1, 51): + # trades, stats = backtest_single_position(dates, tp=i1, sl=-i) + # + # 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) + # + # if total_profit > total_fee * 0.1: + # print("\n===== 信号统计 =====") + # print(f"止盈:{i1}, 止损:{i}") + # print(f"\n一共交易笔数:{len(trades)}") + # print(f"一共盈利:{total_profit:.2f}") + # print(f"一共手续费:{total_fee:.2f}") + # print(f"净利润:{total_profit - total_fee * 0.1}") + +# 需要优化,目前有两种情况,第一种,同向,不如说上一单开单是涨,上一单还没有结束,当前信号来了,就不开单,等上一单到了上一单的止损位或者止盈位在平仓 +# 第二种,方向,上一单是涨,上一单还没有结束,当前信号来了,是跌,然后就按照现在这个信号要开仓的位置,平掉上一单,然后开一单方向的, +# 一笔中可能有好几次信号,都按照上面的规则去判断,要保证同一时间,只会有一笔持仓, +# 打印每笔的交易详细,如果一笔中同向,输入为一条交易记录