From f79834647dc559c18acb34c1fb952abc4ace2f2a Mon Sep 17 00:00:00 2001 From: blade <8019068@qq.com> Date: Tue, 2 Sep 2025 18:42:32 +0800 Subject: [PATCH] statistics orb strategy --- .../__pycache__/orb_trade.cpython-312.pyc | Bin 19872 -> 32869 bytes core/trade/orb_trade.py | 405 ++++++++++++++---- orb_trade_main.py | 352 ++++++++++++++- 3 files changed, 640 insertions(+), 117 deletions(-) diff --git a/core/trade/__pycache__/orb_trade.cpython-312.pyc b/core/trade/__pycache__/orb_trade.cpython-312.pyc index 59f02252cc71730a3ac63446c0a698fac69006e3..be7de178b996535321b1f24132610146b0ddb928 100644 GIT binary patch literal 32869 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%0AS+$na_x2!~RGeIH6~0oN69eE~NVaAN^C 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 E2S&WD-v9sr diff --git a/core/trade/orb_trade.py b/core/trade/orb_trade.py index 1ecf580..733e458 100644 --- a/core/trade/orb_trade.py +++ b/core/trade/orb_trade.py @@ -4,6 +4,12 @@ import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns +from openpyxl import Workbook +from openpyxl.drawing.image import Image +import openpyxl +from openpyxl.styles import Font +from PIL import Image as PILImage + import core.logger as logging from config import OKX_MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE from core.db.db_market_data import DBMarketData @@ -19,11 +25,19 @@ logger = logging.logger class ORBStrategy: def __init__( self, + symbol: str, + bar: str, + start_date: str, + end_date: str, initial_capital=25000, max_leverage=4, risk_per_trade=0.01, commission_per_share=0.0005, + profit_target_multiple=10, is_us_stock=False, + direction=None, + by_sar=False, + symbol_bar_data=None, ): """ 初始化ORB策略参数 @@ -37,18 +51,31 @@ class ORBStrategy: 6. 止损/止盈:根据$R计算,$R=|entry_price-stop_price| 7. 盈利目标:10R,即10*$R 8. 账户净值曲线:账户价值与市场价格 + :param symbol: 股票代码 + :param bar: K线周期 + :param start_date: 开始日期 + :param end_date: 结束日期 :param initial_capital: 初始账户资金(美元) :param max_leverage: 最大杠杆倍数(默认4倍,符合FINRA规定) :param risk_per_trade: 单次交易风险比例(默认1%) :param commission_per_share: 每股交易佣金(美元,默认0.0005) + :param profit_target_multiple: 盈利目标倍数(默认10倍$R,即10R) + :param is_us_stock: 是否是美股 + :param direction: 方向,None=自动,Long=多头,Short=空头 + :param by_sar: 是否根据SAR指标生成信号,True=是,False=否 """ logger.info( - f"初始化ORB策略参数:初始账户资金={initial_capital},最大杠杆倍数={max_leverage},单次交易风险比例={risk_per_trade},每股交易佣金={commission_per_share}" + f"初始化ORB策略参数:股票代码={symbol},K线周期={bar},开始日期={start_date},结束日期={end_date},初始账户资金={initial_capital},最大杠杆倍数={max_leverage},单次交易风险比例={risk_per_trade},每股交易佣金={commission_per_share}" ) + self.symbol = symbol + self.bar = bar + self.start_date = start_date + self.end_date = end_date self.initial_capital = initial_capital self.max_leverage = max_leverage self.risk_per_trade = risk_per_trade self.commission_per_share = commission_per_share + self.profit_target_multiple = profit_target_multiple self.data = None # 存储K线数据 self.trades = [] # 存储交易记录 self.equity_curve = None # 存储账户净值曲线 @@ -64,9 +91,35 @@ class ORBStrategy: self.db_market_data = DBMarketData(self.db_url) self.is_us_stock = is_us_stock self.output_chart_folder = r"./output/trade_sandbox/orb_strategy/chart/" + self.output_excel_folder = r"./output/trade_sandbox/orb_strategy/excel/" os.makedirs(self.output_chart_folder, exist_ok=True) + os.makedirs(self.output_excel_folder, exist_ok=True) + self.direction = direction + self.by_sar = by_sar + self.direction_desc = "既做多又做空" + if self.direction == "Long": + self.direction_desc = "做多" + elif self.direction == "Short": + self.direction_desc = "做空" + + self.sar_desc = "不考虑SAR" + if self.by_sar: + self.sar_desc = "考虑SAR" + self.symbol_bar_data = symbol_bar_data + + def run(self): + """ + 运行ORB策略 + """ + self.fetch_intraday_data() + self.generate_orb_signals() + self.backtest() + if len(self.trades) > 0: + self.plot_equity_curve() + self.output_trade_summary() + return self.symbol_bar_data, self.trades_df, self.trades_summary_df - def fetch_intraday_data(self, symbol, start_date, end_date, interval="5m"): + def fetch_intraday_data(self): """ 获取日内5分钟K线数据(需yfinance支持,部分数据可能有延迟) :param ticker: 股票代码(如QQQ、TQQQ) @@ -74,36 +127,69 @@ class ORBStrategy: :param end_date: 结束日期(格式:YYYY-MM-DD) :param interval: K线周期(默认5分钟) """ - logger.info(f"开始获取{symbol}数据:{start_date}至{end_date},间隔{interval}") - # data = yf.download( - # symbol, start=start_date, end=end_date, interval=interval, progress=False - # ) - data = self.db_market_data.query_market_data_by_symbol_bar( - symbol, interval, start=start_date, end=end_date - ) - data = pd.DataFrame(data) - data.sort_values(by="date_time", inplace=True) - # 保留核心列:开盘价、最高价、最低价、收盘价、成交量 - data["Open"] = data["open"] - data["High"] = data["high"] - data["Low"] = data["low"] - data["Close"] = data["close"] - data["Volume"] = data["volume"] - if self.is_us_stock: - date_time_field = "date_time_us" - else: - date_time_field = "date_time" - data[date_time_field] = pd.to_datetime(data[date_time_field]) - # data["Date"]为日期,不包括时分秒,即date_time如果是2025-01-01 10:00:00,则Date为2025-01-01 - data["Date"] = data[date_time_field].dt.date - # 将Date转换为datetime64[ns]类型以确保类型一致 - data["Date"] = pd.to_datetime(data["Date"]) + logger.info(f"开始获取{self.symbol}数据:{self.start_date}至{self.end_date},间隔{self.bar}") + if self.symbol_bar_data is None or len(self.symbol_bar_data) == 0: + data = self.db_market_data.query_market_data_by_symbol_bar( + self.symbol, self.bar, start=self.start_date, end=self.end_date + ) + data = pd.DataFrame(data) + data.sort_values(by="date_time", inplace=True) + # 保留核心列:开盘价、最高价、最低价、收盘价、成交量 + data["Open"] = data["open"] + data["High"] = data["high"] + data["Low"] = data["low"] + data["Close"] = data["close"] + data["Volume"] = data["volume"] + if self.is_us_stock: + date_time_field = "date_time_us" + else: + date_time_field = "date_time" + data[date_time_field] = pd.to_datetime(data[date_time_field]) + # data["Date"]为日期,不包括时分秒,即date_time如果是2025-01-01 10:00:00,则Date为2025-01-01 + data["Date"] = data[date_time_field].dt.date + # 将Date转换为datetime64[ns]类型以确保类型一致 + data["Date"] = pd.to_datetime(data["Date"]) + # 最小data["Date"] + self.start_date = data["Date"].min().strftime("%Y-%m-%d") + # 最大data["Date"] + self.end_date = data["Date"].max().strftime("%Y-%m-%d") - self.data = data[ - ["symbol", "bar", "Date", date_time_field, "Open", "High", "Low", "Close", "Volume"] - ].copy() - self.data.rename(columns={date_time_field: "date_time"}, inplace=True) - logger.info(f"成功获取{symbol}数据:{len(self.data)}根{interval}K线") + self.data = data[ + [ + "symbol", + "bar", + "Date", + date_time_field, + "Open", + "High", + "Low", + "Close", + "Volume", + "sar_signal", + ] + ].copy() + self.data.rename(columns={date_time_field: "date_time"}, inplace=True) + self.symbol_bar_data = self.data.copy() + else: + self.data = self.symbol_bar_data.copy() + + # 获取Close的mean + self.close_mean = self.data["Close"].mean() + if self.close_mean > 10000: + self.initial_capital = self.initial_capital * 10000 + elif self.close_mean > 5000: + self.initial_capital = self.initial_capital * 5000 + elif self.close_mean > 1000: + self.initial_capital = self.initial_capital * 1000 + elif self.close_mean > 500: + self.initial_capital = self.initial_capital * 500 + elif self.close_mean > 100: + self.initial_capital = self.initial_capital * 100 + else: + pass + logger.info(f"收盘价均值:{self.close_mean}") + logger.info(f"初始资金调整为:{self.initial_capital}") + logger.info(f"成功获取{self.symbol}数据:{len(self.data)}根{self.bar}K线,开始日期={self.start_date},结束日期={self.end_date}") def calculate_shares(self, account_value, entry_price, stop_price): """ @@ -134,20 +220,15 @@ class ORBStrategy: return int(max_shares) # 股数取整 - def generate_orb_signals(self, direction: str = None, by_sar: bool = False): + def generate_orb_signals(self): """ 生成ORB策略信号(每日仅1次交易机会) - 第一根5分钟K线:确定开盘区间(High1, Low1) - 第二根5分钟K线:根据第一根K线方向生成多空信号 - :param direction: 方向,None=自动,Long=多头,Short=空头 - :param by_sar: 是否根据SAR指标生成信号,True=是,False=否 """ - direction_desc = "既做多又做空" - if direction == "Long": - direction_desc = "做多" - elif direction == "Short": - direction_desc = "做空" - logger.info(f"开始生成ORB策略信号:{direction_desc},根据SAR指标:{by_sar}") + logger.info( + f"开始生成ORB策略信号:{self.direction_desc},根据SAR指标:{self.by_sar}" + ) if self.data is None: raise ValueError("请先调用fetch_intraday_data获取数据") @@ -164,6 +245,7 @@ class ORBStrategy: low1 = first_candle["Low"] open1 = first_candle["Open"] close1 = first_candle["Close"] + sar_signal = first_candle["sar_signal"] # 第二根5分钟K线(entry信号) second_candle = daily_data.iloc[1] @@ -171,11 +253,19 @@ class ORBStrategy: entry_time = second_candle.date_time # entry时间 # 生成信号:第一根K线方向决定多空(排除十字星:open1 == close1) - if open1 < close1 and (direction == "Long" or direction is None): + if ( + open1 < close1 + and (self.direction == "Long" or self.direction is None) + and ((self.by_sar and sar_signal == "SAR多头") or not self.by_sar) + ): # 第一根K线收涨→多头信号 signal = "Long" stop_price = low1 # 多头止损=第一根K线最低价 - elif open1 > close1 and (direction == "Short" or direction is None): + elif ( + open1 > close1 + and (self.direction == "Short" or self.direction is None) + and ((self.by_sar and sar_signal == "SAR空头") or not self.by_sar) + ): # 第一根K线收跌→空头信号 signal = "Short" stop_price = high1 # 空头止损=第一根K线最高价 @@ -213,12 +303,12 @@ class ORBStrategy: f"生成信号完成:共{len(signals_df)}个交易日,其中多头{sum(signals_df['Signal']=='Long')}次,空头{sum(signals_df['Signal']=='Short')}次" ) - def backtest(self, profit_target_multiple=10): + def backtest(self): """ 回测ORB策略 :param profit_target_multiple: 盈利目标倍数(默认10倍$R,即10R) """ - logger.info(f"开始回测ORB策略:盈利目标倍数={profit_target_multiple}") + logger.info(f"开始回测ORB策略:盈利目标倍数={self.profit_target_multiple}") if "Signal" not in self.data.columns: raise ValueError("请先调用generate_orb_signals生成策略信号") @@ -255,9 +345,9 @@ class ORBStrategy: low1 = signal_row["Low1"] risk_assumed = abs(entry_price - stop_price) # 计算$R profit_target = ( - entry_price + (risk_assumed * profit_target_multiple) + entry_price + (risk_assumed * self.profit_target_multiple) if signal == "Long" - else entry_price - (risk_assumed * profit_target_multiple) + else entry_price - (risk_assumed * self.profit_target_multiple) ) # 计算交易股数 @@ -293,7 +383,7 @@ class ORBStrategy: break elif high >= profit_target: exit_price = profit_target - exit_reason = "Profit Target (10R)" + exit_reason = f"Profit Target ({self.profit_target_multiple}R)" exit_time = row["date_time"] break elif signal == "Short": @@ -305,7 +395,7 @@ class ORBStrategy: break elif low <= profit_target: exit_price = profit_target - exit_reason = "Profit Target (10R)" + exit_reason = f"Profit Target ({self.profit_target_multiple}R)" exit_time = row["date_time"] break @@ -333,6 +423,10 @@ class ORBStrategy: self.trades.append( { "TradeID": trade_id, + "Direction": self.direction_desc, + "BySar": self.sar_desc, + "Symbol": self.symbol, + "Bar": self.bar, "Date": date, "Signal": signal, "EntryTime": signal_row.date_time.strftime("%Y-%m-%d %H:%M:%S"), @@ -353,40 +447,91 @@ class ORBStrategy: equity_history.append(account_value) trade_id += 1 + if len(self.trades) == 0: + logger.info("没有交易") + self.trades_df = pd.DataFrame() + self.initial_trade_summary() + return # 生成净值曲线 self.create_equity_curve() # 输出回测结果 - trades_df = pd.DataFrame(self.trades) + self.trades_df = pd.DataFrame(self.trades) + self.trades_df.sort_values(by="ExitTime", inplace=True) total_return = ( (account_value - self.initial_capital) / self.initial_capital * 100 ) win_rate = ( - (trades_df["ProfitLoss"] > 0).sum() / len(trades_df) * 100 - if len(trades_df) > 0 + (self.trades_df["ProfitLoss"] > 0).sum() / len(self.trades_df) * 100 + if len(self.trades_df) > 0 else 0 ) # 计算盈亏比 - profit_sum = trades_df[trades_df["ProfitLoss"] > 0]["ProfitLoss"].sum() - loss_sum = abs(trades_df[trades_df["ProfitLoss"] < 0]["ProfitLoss"].sum()) + profit_sum = self.trades_df[self.trades_df["ProfitLoss"] > 0]["ProfitLoss"].sum() + loss_sum = abs(self.trades_df[self.trades_df["ProfitLoss"] < 0]["ProfitLoss"].sum()) if loss_sum == 0: - profit_loss_ratio = float('inf') + profit_loss_ratio = float("inf") else: profit_loss_ratio = (profit_sum / loss_sum) * 100 + first_entry_price = self.trades_df.iloc[0]["EntryPrice"] + last_exit_price = self.trades_df.iloc[-1]["ExitPrice"] + natural_return = (last_exit_price - first_entry_price) / first_entry_price * 100 + self.initial_trade_summary() + if len(self.trades_df) > 0: + logger.info("\n" + "=" * 50) + logger.info("ORB策略回测结果") + logger.info("=" * 50) + logger.info(f"股票代码:{self.symbol}") + logger.info(f"K线周期:{self.bar}") + logger.info(f"开始日期:{self.start_date}") + logger.info(f"结束日期:{self.end_date}") + logger.info(f"盈利目标倍数:{self.profit_target_multiple}") + logger.info(f"初始资金:${self.initial_capital:,.2f}") + logger.info(f"最终资金:${account_value:,.2f}") + self.trades_summary["最终资金$"] = account_value + logger.info(f"总收益率:{total_return:.2f}%") + self.trades_summary["总收益率%"] = total_return + logger.info(f"自然收益率:{natural_return:.2f}%") + self.trades_summary["自然收益率%"] = natural_return + logger.info(f"总交易次数:{len(self.trades_df)}") + self.trades_summary["总交易次数"] = len(self.trades_df) + logger.info(f"盈亏比:{profit_loss_ratio:.2f}%") + self.trades_summary["盈亏比%"] = profit_loss_ratio + logger.info(f"胜率:{win_rate:.2f}%") + self.trades_summary["胜率%"] = win_rate + logger.info(f"平均每笔盈亏:${self.trades_df['ProfitLoss'].mean():.2f}") + self.trades_summary["平均每笔盈亏$"] = self.trades_df["ProfitLoss"].mean() + logger.info(f"最大单笔盈利:${self.trades_df['ProfitLoss'].max():.2f}") + self.trades_summary["最大单笔盈利$"] = self.trades_df["ProfitLoss"].max() + logger.info(f"最大单笔亏损:${abs(self.trades_df['ProfitLoss'].min()):.2f}") + self.trades_summary["最大单笔亏损$"] = abs(self.trades_df["ProfitLoss"].min()) + else: + logger.info("没有交易") + self.trades_summary_df = pd.DataFrame([self.trades_summary]) + + def initial_trade_summary(self): + """ + 初始化交易总结 + """ + self.trades_summary = {} + self.trades_summary["方向"] = self.direction_desc + self.trades_summary["根据SAR"] = self.sar_desc + self.trades_summary["股票代码"] = self.symbol + self.trades_summary["K线周期"] = self.bar + self.trades_summary["开始日期"] = self.start_date + self.trades_summary["结束日期"] = self.end_date + self.trades_summary["盈利目标倍数"] = self.profit_target_multiple + self.trades_summary["初始资金$"] = self.initial_capital + self.trades_summary["最终资金$"] = self.initial_capital + self.trades_summary["总收益率%"] = 0 + self.trades_summary["自然收益率%"] = 0 + self.trades_summary["总交易次数"] = 0 + self.trades_summary["盈亏比%"] = 0 + self.trades_summary["胜率%"] = 0 + self.trades_summary["平均每笔盈亏$"] = 0 + self.trades_summary["最大单笔盈利$"] = 0 + self.trades_summary["最大单笔亏损$"] = 0 - logger.info("\n" + "=" * 50) - logger.info("ORB策略回测结果") - logger.info("=" * 50) - logger.info(f"初始资金:${self.initial_capital:,.2f}") - logger.info(f"最终资金:${account_value:,.2f}") - logger.info(f"总收益率:{total_return:.2f}%") - logger.info(f"总交易次数:{len(trades_df)}") - logger.info(f"盈亏比:{profit_loss_ratio:.2f}%") - logger.info(f"胜率:{win_rate:.2f}%") - if len(trades_df) > 0: - logger.info(f"平均每笔盈亏:${trades_df['ProfitLoss'].mean():.2f}") - logger.info(f"最大单笔盈利:${trades_df['ProfitLoss'].max():.2f}") - logger.info(f"最大单笔亏损:${trades_df['ProfitLoss'].min():.2f}") def create_equity_curve(self): """ @@ -437,9 +582,25 @@ class ORBStrategy: account_value_to_1 = self.equity_curve["AccountValue"] / first_account_value market_price_to_1 = self.equity_curve["MarketPrice"] / first_market_price plt.figure(figsize=(12, 6)) - plt.plot(self.equity_curve["DateTime"], account_value_to_1, label="账户价值", color='blue', linewidth=2, marker='o', markersize=4) - plt.plot(self.equity_curve["DateTime"], market_price_to_1, label="市场价格", color='green', linewidth=2, marker='s', markersize=4) - plt.title(f"ORB策略账户净值曲线 {symbol} {bar}", fontsize=14, fontweight='bold') + plt.plot( + self.equity_curve["DateTime"], + account_value_to_1, + label="账户价值", + color="blue", + linewidth=2, + marker="o", + markersize=4, + ) + plt.plot( + self.equity_curve["DateTime"], + market_price_to_1, + label="市场价格", + color="green", + linewidth=2, + marker="s", + markersize=4, + ) + plt.title(f"ORB曲线 {symbol} {bar} {self.direction_desc} {self.sar_desc}", fontsize=14, fontweight="bold") plt.xlabel("时间", fontsize=12) plt.ylabel("涨跌变化", fontsize=12) plt.legend(fontsize=11) @@ -450,54 +611,108 @@ class ORBStrategy: if len(self.equity_curve) > 30: # 如果数据点较多,选择间隔显示,但确保第一条和最后一条始终显示 step = max(1, len(self.equity_curve) // 30) - + # 创建标签索引列表,确保包含首尾数据 label_indices = [0] # 第一条 - + # 添加中间间隔的标签 for i in range(step, len(self.equity_curve) - 1, step): label_indices.append(i) - + # 添加最后一条(如果还没有包含的话) if len(self.equity_curve) - 1 not in label_indices: label_indices.append(len(self.equity_curve) - 1) - + # 设置x轴标签 - plt.xticks(self.equity_curve["DateTime"].iloc[label_indices], - self.equity_curve["DateTime"].iloc[label_indices], - rotation=45, ha='right', fontsize=10) + plt.xticks( + self.equity_curve["DateTime"].iloc[label_indices], + self.equity_curve["DateTime"].iloc[label_indices], + rotation=45, + ha="right", + fontsize=10, + ) else: # 如果数据点较少,全部显示 - plt.xticks(self.equity_curve["DateTime"], - self.equity_curve["DateTime"], - rotation=45, ha='right', fontsize=10) + plt.xticks( + self.equity_curve["DateTime"], + self.equity_curve["DateTime"], + rotation=45, + ha="right", + fontsize=10, + ) plt.tight_layout() - save_path = f"{self.output_chart_folder}/{symbol}_{bar}_orb_strategy_equity_curve.png" - plt.savefig(save_path, dpi=150, bbox_inches='tight') + self.chart_save_path = ( + f"{self.output_chart_folder}/{symbol}_{bar}_{self.direction_desc}_{self.sar_desc}_orb_strategy_equity_curve.png" + ) + plt.savefig(self.chart_save_path, dpi=150, bbox_inches="tight") plt.close() + + def output_trade_summary(self): + """ + 输出交易明细,交易总结与Chart图片到Excel + """ + start_date = self.start_date.replace("-", "") + end_date = self.end_date.replace("-", "") + output_file_name = f"orb_{self.symbol}_{self.bar}_{start_date}_{end_date}_{self.direction_desc}_{self.sar_desc}.xlsx" + output_file_path = os.path.join(self.output_excel_folder, output_file_name) + logger.info(f"导出{output_file_path}") + with pd.ExcelWriter(output_file_path) as writer: + self.trades_df.to_excel(writer, sheet_name="交易明细", index=False) + self.trades_summary_df.to_excel(writer, sheet_name="交易总结", index=False) + if os.path.exists(self.chart_save_path): + charts_dict = { + "账户净值曲线": self.chart_save_path + } + self.output_chart_to_excel(output_file_path, charts_dict) + + def output_chart_to_excel(self, excel_file_path: str, charts_dict: dict): + """ + 输出Excel文件,包含所有图表 + charts_dict: 图表数据字典,格式为: + { + "sheet_name": { + "chart_name": "chart_path" + } + } + """ + logger.info(f"将图表输出到{excel_file_path}") + # 打开已经存在的Excel文件 + wb = openpyxl.load_workbook(excel_file_path) + + for sheet_name, chart_path in charts_dict.items(): + try: + ws = wb.create_sheet(title=sheet_name) + row_offset = 1 + # Insert chart image + img = Image(chart_path) + ws.add_image(img, f"A{row_offset}") + + except Exception as e: + logger.error(f"输出Excel Sheet {sheet_name} 失败: {e}") + continue + # Save Excel file + wb.save(excel_file_path) + logger.info(f"图表已输出到{excel_file_path}") + # ------------------- 策略示例:回测QQQ的ORB策略(2016-2023) ------------------- if __name__ == "__main__": # 初始化ORB策略 orb_strategy = ORBStrategy( + symbol="ETH-USDT", + bar="5m", + start_date="2025-05-15", + end_date="2025-08-20", initial_capital=25000, max_leverage=4, risk_per_trade=0.01, commission_per_share=0.0005, + profit_target_multiple=10, + is_us_stock=False, + direction=None, + by_sar=False, ) - # 1. 获取QQQ的5分钟日内数据(2024-2025,注意:yfinance免费版可能限制历史日内数据,建议用专业数据源) - orb_strategy.fetch_intraday_data( - symbol="ETH-USDT", start_date="2025-05-15", end_date="2025-08-20", interval="5m" - ) - - # 2. 生成ORB策略信号 - orb_strategy.generate_orb_signals() - - # 3. 回测策略(盈利目标10R) - orb_strategy.backtest(profit_target_multiple=10) - - # 4. 绘制净值曲线 - orb_strategy.plot_equity_curve() + orb_strategy.run() diff --git a/orb_trade_main.py b/orb_trade_main.py index 5421938..0f53e19 100644 --- a/orb_trade_main.py +++ b/orb_trade_main.py @@ -1,35 +1,343 @@ from core.trade.orb_trade import ORBStrategy -from config import US_STOCK_MONITOR_CONFIG +from config import US_STOCK_MONITOR_CONFIG, OKX_MONITOR_CONFIG import core.logger as logging +from datetime import datetime +from openpyxl import Workbook +from openpyxl.drawing.image import Image +import openpyxl +import pandas as pd +import os logger = logging.logger + def main(): - symbols = US_STOCK_MONITOR_CONFIG.get("volume_monitor", {}).get("symbols", ["QQQ"]) + is_us_stock_list = [True, False] + bar = "5m" + direction_list = [None, "Long", "Short"] + by_sar_list = [False, True] + start_date = "2024-01-01" + end_date = datetime.now().strftime("%Y-%m-%d") + profit_target_multiple = 10 + initial_capital = 25000 + max_leverage = 4 + risk_per_trade = 0.01 + commission_per_share = 0.0005 + + trades_df_list = [] + trades_summary_df_list = [] + symbol_data_cache = [] + for is_us_stock in is_us_stock_list: + for direction in direction_list: + for by_sar in by_sar_list: + if is_us_stock: + symbols = US_STOCK_MONITOR_CONFIG.get("volume_monitor", {}).get( + "symbols", ["QQQ"] + ) + else: + symbols = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( + "symbols", ["BTC-USDT"] + ) + for symbol in symbols: + logger.info( + f"开始回测 {symbol}, 交易周期:{bar}, 开始日期:{start_date}, 结束日期:{end_date}, 是否是美股:{is_us_stock}, 交易方向:{direction}, 是否使用SAR:{by_sar}" + ) + symbol_bar_data = None + found_symbol_bar_data = False + for symbol_data_dict in symbol_data_cache: + if ( + symbol_data_dict["symbol"] == symbol + and symbol_data_dict["bar"] == bar + ): + symbol_bar_data = symbol_data_dict["data"] + found_symbol_bar_data = True + break + + orb_strategy = ORBStrategy( + symbol=symbol, + bar=bar, + start_date=start_date, + end_date=end_date, + is_us_stock=is_us_stock, + direction=direction, + by_sar=by_sar, + profit_target_multiple=profit_target_multiple, + initial_capital=initial_capital, + max_leverage=max_leverage, + risk_per_trade=risk_per_trade, + commission_per_share=commission_per_share, + symbol_bar_data=symbol_bar_data, + ) + symbol_bar_data, trades_df, trades_summary_df = orb_strategy.run() + if symbol_bar_data is None or len(symbol_bar_data) == 0: + continue + if not found_symbol_bar_data: + symbol_data_cache.append( + {"symbol": symbol, "bar": bar, "data": symbol_bar_data} + ) + if trades_summary_df is None or len(trades_summary_df) == 0: + continue + trades_summary_df_list.append(trades_summary_df) + trades_df_list.append(trades_df) + total_trades_df = pd.concat(trades_df_list) + total_trades_summary_df = pd.concat(trades_summary_df_list) + statitics_dict = statistics_summary(total_trades_summary_df) + output_excel_folder = r"./output/trade_sandbox/orb_strategy/excel/summary/" + os.makedirs(output_excel_folder, exist_ok=True) + now_str = datetime.now().strftime("%Y%m%d%H%M%S") + excel_file_name = f"orb_strategy_summary_{now_str}.xlsx" + output_file_path = os.path.join(output_excel_folder, excel_file_name) + with pd.ExcelWriter(output_file_path) as writer: + total_trades_df.to_excel(writer, sheet_name="交易详情", index=False) + total_trades_summary_df.to_excel(writer, sheet_name="交易总结", index=False) + statitics_dict["statistics_summary_df"].to_excel( + writer, sheet_name="统计总结", index=False + ) + statitics_dict["max_total_return_record_df"].to_excel( + writer, sheet_name="最大总收益率记录", index=False + ) + statitics_dict["max_total_return_record_df_grouped_count"].to_excel( + writer, sheet_name="最大总收益率记录_方向和根据SAR的组合", index=False + ) + statitics_dict["max_total_return_record_df_direction_count"].to_excel( + writer, sheet_name="最大总收益率记录_方向", index=False + ) + statitics_dict["max_total_return_record_df_sar_count"].to_excel( + writer, sheet_name="最大总收益率记录_根据SAR", index=False + ) + chart_path = r"./output/trade_sandbox/orb_strategy/chart/" + os.makedirs(chart_path, exist_ok=True) + copy_chart_to_excel(chart_path, output_file_path) + logger.info(f"交易总结已输出到{output_file_path}") + + +def statistics_summary(trades_summary_df: pd.DataFrame): + statistics_summary_list = [] + summary = {} + # 1. 统计总收益率% > 0 的占比 + total_return_gt_0 = trades_summary_df[trades_summary_df["总收益率%"] > 0].shape[0] + total_return_gt_0_ratio = round((total_return_gt_0 / trades_summary_df.shape[0]) * 100, 2) + summary["总收益率%>0占比"] = total_return_gt_0_ratio + logger.info(f"总收益率% > 0 的占比:{total_return_gt_0_ratio:.2f}%") + # 2. 统计总收益率% > 自然收益率% 的占比 + total_return_gt_natural_return = trades_summary_df[ + trades_summary_df["总收益率%"] > trades_summary_df["自然收益率%"] + ].shape[0] + total_return_gt_natural_return_ratio = ( + round((total_return_gt_natural_return / trades_summary_df.shape[0]) * 100, 2) + ) + summary["总收益率%>自然收益率%占比"] = total_return_gt_natural_return_ratio + logger.info( + f"总收益率% > 自然收益率% 的占比:{total_return_gt_natural_return_ratio:.2f}%" + ) + statistics_summary_list.append(summary) + statistics_summary_df = pd.DataFrame(statistics_summary_list) + + symbol_list = trades_summary_df["股票代码"].unique() + max_total_return_record_list = [] + for symbol in symbol_list: + trades_summary_df_copy = trades_summary_df.copy() + symbol_trades_summary_df = trades_summary_df_copy[ + trades_summary_df_copy["股票代码"] == symbol + ] + symbol_trades_summary_df.reset_index(drop=True, inplace=True) + if symbol_trades_summary_df.empty: + continue + # 过滤掉NaN,避免idxmax报错 + valid_df = symbol_trades_summary_df[ + symbol_trades_summary_df["总收益率%"].notna() + ] + if valid_df.empty: + continue + # 获得总收益率%最大的记录 + max_idx = valid_df["总收益率%"].idxmax() + max_total_return_record = symbol_trades_summary_df.loc[max_idx] + summary = {} + summary["股票代码"] = symbol + summary["方向"] = max_total_return_record["方向"] + summary["根据SAR"] = max_total_return_record["根据SAR"] + summary["总收益率%"] = max_total_return_record["总收益率%"] + summary["自然收益率%"] = max_total_return_record["自然收益率%"] + max_total_return_record_list.append(summary) + max_total_return_record_df = pd.DataFrame(max_total_return_record_list) + # 统计max_total_return_record_df中方向和根据SAR的组合(使用size更稳健,支持空分组与缺失值) + # 强制将分组键转为可哈希的标量类型,避免单元格为Series/列表导致的unhashable错误 + if len(max_total_return_record_df) > 0: + + def _to_hashable_scalar(v): + # 标量或None直接返回 + if isinstance(v, (str, int, float, bool)) or v is None: + return v + try: + import numpy as _np + + if _np.isscalar(v): + return v + except Exception: + pass + # 其它(如Series、list、dict、ndarray等)转字符串 + return str(v) + + for key_col in ["方向", "根据SAR"]: + if key_col in max_total_return_record_df.columns: + max_total_return_record_df[key_col] = max_total_return_record_df[ + key_col + ].apply(_to_hashable_scalar) + # 分组统计 + max_total_return_record_df_grouped_count = ( + max_total_return_record_df.groupby(["方向", "根据SAR"], dropna=False) + .size() + .reset_index(name="数量") + ) + max_total_return_record_df_grouped_count.sort_values( + by="数量", ascending=False, inplace=True + ) + max_total_return_record_df_grouped_count.reset_index(drop=True, inplace=True) + + # 统计方向的记录数目 + max_total_return_record_df_direction_count = ( + max_total_return_record_df.groupby(["方向"], dropna=False) + .size() + .reset_index(name="数量") + ) + max_total_return_record_df_direction_count.sort_values( + by="数量", ascending=False, inplace=True + ) + max_total_return_record_df_direction_count.reset_index(drop=True, inplace=True) + + # 统计根据SAR的记录数目 + max_total_return_record_df_sar_count = ( + max_total_return_record_df.groupby(["根据SAR"], dropna=False) + .size() + .reset_index(name="数量") + ) + max_total_return_record_df_sar_count.sort_values( + by="数量", ascending=False, inplace=True + ) + max_total_return_record_df_sar_count.reset_index(drop=True, inplace=True) + else: + # 构造空结果,保证下游写入Excel不报错 + max_total_return_record_df_grouped_count = pd.DataFrame( + columns=["方向", "根据SAR", "数量"] + ) + max_total_return_record_df_direction_count = pd.DataFrame( + columns=["方向", "数量"] + ) + max_total_return_record_df_sar_count = pd.DataFrame(columns=["根据SAR", "数量"]) + + result = { + "statistics_summary_df": statistics_summary_df, + "max_total_return_record_df": max_total_return_record_df, + "max_total_return_record_df_grouped_count": max_total_return_record_df_grouped_count, + "max_total_return_record_df_direction_count": max_total_return_record_df_direction_count, + "max_total_return_record_df_sar_count": max_total_return_record_df_sar_count, + } + return result + + +def copy_chart_to_excel(chart_path: str, excel_file_path: str): + f""" + 将chart图片复制到excel中 + 算法: + 1. 读取chart_path + 2. chart文件名开头是symbol,结尾是.png + 3. 每个symbol创建一个Excel Sheet,Sheet名称为symbol_chart + 4. 将chart图片插入到Sheet中 + 5. 要求每张图片大小为800x400 + 6. 要求两张图片左右并列显示 + 7. 要求上下图片间距为20px + """ + # 收集所有图片 + if not os.path.isdir(chart_path): + return + chart_files = [f for f in os.listdir(chart_path) if f.lower().endswith(".png")] + if len(chart_files) == 0: + return + + # 汇总需要处理的symbol列表(去重) + symbols = set(US_STOCK_MONITOR_CONFIG.get("volume_monitor", {}).get("symbols", ["QQQ"])) + symbols.update(OKX_MONITOR_CONFIG.get("volume_monitor", {}).get("symbols", ["BTC-USDT"])) + symbols = list(symbols) + symbols.sort() + # 每个symbol创建一个sheet并插图 for symbol in symbols: - logger.info(f"开始回测 {symbol}") - # 初始化ORB策略 - orb_strategy = ORBStrategy( - initial_capital=25000, - max_leverage=4, - risk_per_trade=0.01, - commission_per_share=0.0005, - is_us_stock=True, - ) - # 1. 获取QQQ的5分钟日内数据(2024-2025,注意:yfinance免费版可能限制历史日内数据,建议用专业数据源) - orb_strategy.fetch_intraday_data( - symbol=symbol, start_date="2024-11-30", end_date="2025-08-30", interval="5m" - ) + logger.info(f"开始保存{symbol}的图表") + symbol_files = [f for f in chart_files if f.startswith(symbol)] + if len(symbol_files) == 0: + continue + # 排序以稳定显示顺序 + symbol_files.sort() + copy_chart_to_excel_sheet(chart_path, symbol_files, excel_file_path, symbol) - # 2. 生成ORB策略信号 - orb_strategy.generate_orb_signals() - # 3. 回测策略(盈利目标10R) - orb_strategy.backtest(profit_target_multiple=10) +def copy_chart_to_excel_sheet( + chart_path: str, chart_files: list, excel_file_path: str, symbol: str +): + """ + 将chart图片复制到excel中 + 算法: + 1. 读取chart_files + 2. 创建一个Excel Sheet,Sheet名称为{symbol}_chart + 3. 将chart_files中的图片插入到Sheet中 + 4. 要求每张图片大小为800x400 + 5. 要求两张图片左右并列显示, 如6张图片则图片行数为3,列数为2 + 6. 要求上下图片间距为20px + """ + # 打开已经存在的Excel文件 + wb = openpyxl.load_workbook(excel_file_path) + # 如果sheet已存在,先删除,避免重复插入 + sheet_name = f"{symbol}_chart" + if sheet_name in wb.sheetnames: + del wb[sheet_name] + ws = wb.create_sheet(title=sheet_name) - # 4. 绘制净值曲线 - orb_strategy.plot_equity_curve() + # 两列布局:左列A,右列L;行间距通过起始行步进控制 + left_col = "A" + right_col = "L" + row_step = 26 # 行步进,控制上下间距 + + for idx, chart_file in enumerate(chart_files): + try: + img_path = os.path.join(chart_path, chart_file) + img = Image(img_path) + # 设置图片尺寸 800x400 像素 + img.width = 800 + img.height = 400 + + row_block = idx // 2 + col_block = idx % 2 + anchor_col = left_col if col_block == 0 else right_col + anchor_cell = f"{anchor_col}{1 + row_block * row_step}" + ws.add_image(img, anchor_cell) + except Exception: + continue + + wb.save(excel_file_path) + logger.info(f"{symbol}的图表已输出到{excel_file_path}") + + +def test(): + orb_strategy = ORBStrategy( + symbol="BTC-USDT", + bar="5m", + start_date="2024-01-01", + end_date="2025-09-02", + is_us_stock=False, + direction=None, + by_sar=True, + profit_target_multiple=10, + initial_capital=25000, + max_leverage=4, + risk_per_trade=0.01, + commission_per_share=0.0005, + ) + orb_strategy.run() if __name__ == "__main__": - main() \ No newline at end of file + # main() + + chart_path = r"./output/trade_sandbox/orb_strategy/chart/" + excel_file_path = r"./output/trade_sandbox/orb_strategy/excel/summary/orb_strategy_summary_20250902174203.xlsx" + copy_chart_to_excel(chart_path, excel_file_path) + # test()