From 644df3a42f5c79fe0e11172792bbeb12a45bf7f9 Mon Sep 17 00:00:00 2001 From: blade <8019068@qq.com> Date: Wed, 3 Sep 2025 18:26:27 +0800 Subject: [PATCH] optimize orb strategy --- .gitignore | 1 + core/biz/market_data_from_csv.py | 0 .../__pycache__/orb_trade.cpython-312.pyc | Bin 32869 -> 35498 bytes core/trade/orb_trade.py | 109 +++++++++--- orb_trade_main.py | 156 +++++++++++------- 5 files changed, 181 insertions(+), 85 deletions(-) create mode 100644 core/biz/market_data_from_csv.py diff --git a/.gitignore b/.gitignore index e6f8a5f..43f58ef 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ core/db/__pycache__/*.pyc core/biz/__pycache__/*.pyc core/statistics/__pycache__/*.pyc +/data diff --git a/core/biz/market_data_from_csv.py b/core/biz/market_data_from_csv.py new file mode 100644 index 0000000..e69de29 diff --git a/core/trade/__pycache__/orb_trade.cpython-312.pyc b/core/trade/__pycache__/orb_trade.cpython-312.pyc index be7de178b996535321b1f24132610146b0ddb928..1c695d07facbbcdfcdb3650a4159d2658ff55238 100644 GIT binary patch delta 8596 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 diff --git a/core/trade/orb_trade.py b/core/trade/orb_trade.py index 733e458..2d60af7 100644 --- a/core/trade/orb_trade.py +++ b/core/trade/orb_trade.py @@ -38,6 +38,8 @@ class ORBStrategy: direction=None, by_sar=False, symbol_bar_data=None, + price_range_mean_as_R=False, + by_big_k=False, ): """ 初始化ORB策略参数 @@ -63,6 +65,8 @@ class ORBStrategy: :param is_us_stock: 是否是美股 :param direction: 方向,None=自动,Long=多头,Short=空头 :param by_sar: 是否根据SAR指标生成信号,True=是,False=否 + :param price_range_mean_as_R: 是否将价格振幅均值作为$R,True=是,False=否 + :param by_big_k: 是否根据K线实体部分,亦即abs(open-close)超过high-low的50%,True=是,False=否 """ logger.info( f"初始化ORB策略参数:股票代码={symbol},K线周期={bar},开始日期={start_date},结束日期={end_date},初始账户资金={initial_capital},最大杠杆倍数={max_leverage},单次交易风险比例={risk_per_trade},每股交易佣金={commission_per_share}" @@ -101,12 +105,23 @@ class ORBStrategy: 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 - + self.price_range_mean_as_R = price_range_mean_as_R + if self.price_range_mean_as_R: + self.price_range_mean_as_R_desc = "R为振幅均值" + else: + self.price_range_mean_as_R_desc = "R为entry减stop" + + self.by_big_k = by_big_k + if self.by_big_k: + self.by_big_k_desc = "K线实体过50%" + else: + self.by_big_k_desc = "无K线要求" + def run(self): """ 运行ORB策略 @@ -127,7 +142,9 @@ class ORBStrategy: :param end_date: 结束日期(格式:YYYY-MM-DD) :param interval: K线周期(默认5分钟) """ - logger.info(f"开始获取{self.symbol}数据:{self.start_date}至{self.end_date},间隔{self.bar}") + 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 @@ -153,7 +170,6 @@ class ORBStrategy: 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", @@ -169,6 +185,7 @@ class ORBStrategy: ] ].copy() self.data.rename(columns={date_time_field: "date_time"}, inplace=True) + self.calculate_price_range_mean() self.symbol_bar_data = self.data.copy() else: self.data = self.symbol_bar_data.copy() @@ -189,21 +206,24 @@ class ORBStrategy: 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}") + 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): + def calculate_shares(self, account_value, entry_price, stop_price, risk_assumed): """ 根据ORB公式计算交易股数 :param account_value: 当前账户价值(美元) :param entry_price: 交易entry价格(第二根5分钟K线开盘价) :param stop_price: 止损价格(多头=第一根K线最低价,空头=第一根K线最高价) + :param risk_assumed: 风险金额($R),根据price_range_mean_as_R决定 :return: 整数股数(Shares) """ logger.info( f"开始计算交易股数:账户价值={account_value},entry价格={entry_price},止损价格={stop_price}" ) # 计算单交易风险金额($R) - risk_per_trade_dollar = abs(entry_price - stop_price) # 风险金额取绝对值 + risk_per_trade_dollar = risk_assumed # 风险金额取绝对值 if risk_per_trade_dollar <= 0: return 0 # 无风险时不交易 @@ -247,6 +267,12 @@ class ORBStrategy: close1 = first_candle["Close"] sar_signal = first_candle["sar_signal"] + if high1 == low1: + continue + if self.by_big_k: + if (abs(open1 - close1) / (high1 - low1)) < 0.5: + continue + # 第二根5分钟K线(entry信号) second_candle = daily_data.iloc[1] entry_price = second_candle["Open"] # entry价格=第二根K线开盘价 @@ -303,6 +329,17 @@ class ORBStrategy: f"生成信号完成:共{len(signals_df)}个交易日,其中多头{sum(signals_df['Signal']=='Long')}次,空头{sum(signals_df['Signal']=='Short')}次" ) + def calculate_price_range_mean(self): + """ + 计算价格振幅均值,振幅为最高价与最低价之差 + 计算价格振幅标准差 + 要求用滑动窗口: window_size=100计算均值,每次计算都包含当前行 + 返回一个新列,列名为"PriceRangeMean" + """ + self.data["PriceRange"] = self.data["High"] - self.data["Low"] + self.data["PriceRangeMean"] = self.data["PriceRange"].rolling(window=100).mean() + self.data["PriceRangeStd"] = self.data["PriceRange"].rolling(window=100).std() + def backtest(self): """ 回测ORB策略 @@ -343,7 +380,18 @@ class ORBStrategy: stop_price = signal_row["StopPrice"] high1 = signal_row["High1"] low1 = signal_row["Low1"] - risk_assumed = abs(entry_price - stop_price) # 计算$R + price_range = signal_row["PriceRange"] + price_range_mean = signal_row["PriceRangeMean"] + price_range_std = signal_row["PriceRangeStd"] + # 计算$R + if ( + self.price_range_mean_as_R + and price_range_mean is not None + and price_range_mean > 0 + ): + risk_assumed = price_range_mean + else: + risk_assumed = abs(entry_price - stop_price) profit_target = ( entry_price + (risk_assumed * self.profit_target_multiple) if signal == "Long" @@ -351,7 +399,9 @@ class ORBStrategy: ) # 计算交易股数 - shares = self.calculate_shares(account_value, entry_price, stop_price) + shares = self.calculate_shares( + account_value, entry_price, stop_price, risk_assumed + ) if shares == 0: # 股数为0→不交易 equity_history.append(account_value) @@ -425,6 +475,8 @@ class ORBStrategy: "TradeID": trade_id, "Direction": self.direction_desc, "BySar": self.sar_desc, + "PriceRangeMeanAsR": self.price_range_mean_as_R_desc, + "ByBigK": self.by_big_k_desc, "Symbol": self.symbol, "Bar": self.bar, "Date": date, @@ -467,8 +519,12 @@ class ORBStrategy: else 0 ) # 计算盈亏比 - 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()) + 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") else: @@ -504,11 +560,13 @@ class ORBStrategy: 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()) + 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): """ 初始化交易总结 @@ -516,6 +574,8 @@ class ORBStrategy: self.trades_summary = {} self.trades_summary["方向"] = self.direction_desc self.trades_summary["根据SAR"] = self.sar_desc + self.trades_summary["R算法"] = self.price_range_mean_as_R_desc + self.trades_summary["K线条件"] = self.by_big_k_desc self.trades_summary["股票代码"] = self.symbol self.trades_summary["K线周期"] = self.bar self.trades_summary["开始日期"] = self.start_date @@ -532,7 +592,6 @@ class ORBStrategy: self.trades_summary["最大单笔盈利$"] = 0 self.trades_summary["最大单笔亏损$"] = 0 - def create_equity_curve(self): """ 创建账户净值曲线 @@ -600,7 +659,11 @@ class ORBStrategy: marker="s", markersize=4, ) - plt.title(f"ORB曲线 {symbol} {bar} {self.direction_desc} {self.sar_desc}", fontsize=14, fontweight="bold") + plt.title( + f"{symbol} {bar} {self.direction_desc} {self.sar_desc} {self.price_range_mean_as_R_desc} {self.by_big_k_desc}", + fontsize=14, + fontweight="bold", + ) plt.xlabel("时间", fontsize=12) plt.ylabel("涨跌变化", fontsize=12) plt.legend(fontsize=11) @@ -641,30 +704,26 @@ class ORBStrategy: fontsize=10, ) plt.tight_layout() - self.chart_save_path = ( - f"{self.output_chart_folder}/{symbol}_{bar}_{self.direction_desc}_{self.sar_desc}_orb_strategy_equity_curve.png" - ) + self.chart_save_path = f"{self.output_chart_folder}/{symbol}_{bar}_{self.direction_desc}_{self.sar_desc}_{self.price_range_mean_as_R_desc}_{self.by_big_k_desc}_orb.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_name = f"orb_{self.symbol}_{self.bar}_{start_date}_{end_date}_{self.direction_desc}_{self.sar_desc}_{self.price_range_mean_as_R_desc}_{self.by_big_k_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 - } + 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文件,包含所有图表 @@ -694,7 +753,7 @@ class ORBStrategy: # Save Excel file wb.save(excel_file_path) logger.info(f"图表已输出到{excel_file_path}") - + # ------------------- 策略示例:回测QQQ的ORB策略(2016-2023) ------------------- if __name__ == "__main__": diff --git a/orb_trade_main.py b/orb_trade_main.py index 0f53e19..aa2ea03 100644 --- a/orb_trade_main.py +++ b/orb_trade_main.py @@ -16,6 +16,8 @@ def main(): bar = "5m" direction_list = [None, "Long", "Short"] by_sar_list = [False, True] + price_range_mean_as_R_list = [False, True] + by_big_k_list = [False, True] start_date = "2024-01-01" end_date = datetime.now().strftime("%Y-%m-%d") profit_target_multiple = 10 @@ -30,55 +32,59 @@ def main(): 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 + for price_range_mean_as_R in price_range_mean_as_R_list: + for by_big_k in by_big_k_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}, 是否使用R为entry减stop:{price_range_mean_as_R}, 是否使用K线实体过50%:{by_big_k}" + ) + 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) + 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, + price_range_mean_as_R=price_range_mean_as_R, + by_big_k=by_big_k, + ) + 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) @@ -97,13 +103,19 @@ def main(): writer, sheet_name="最大总收益率记录", index=False ) statitics_dict["max_total_return_record_df_grouped_count"].to_excel( - writer, sheet_name="最大总收益率记录_方向和根据SAR的组合", index=False + writer, sheet_name="最大总收益率_所有条件组合", index=False ) statitics_dict["max_total_return_record_df_direction_count"].to_excel( - writer, sheet_name="最大总收益率记录_方向", index=False + writer, sheet_name="最大总收益率_方向", index=False ) statitics_dict["max_total_return_record_df_sar_count"].to_excel( - writer, sheet_name="最大总收益率记录_根据SAR", index=False + writer, sheet_name="最大总收益率_根据SAR", index=False + ) + statitics_dict["max_total_return_record_df_R_count"].to_excel( + writer, sheet_name="最大总收益率_R算法", index=False + ) + statitics_dict["max_total_return_record_df_K_count"].to_excel( + writer, sheet_name="最大总收益率_K线条件", index=False ) chart_path = r"./output/trade_sandbox/orb_strategy/chart/" os.makedirs(chart_path, exist_ok=True) @@ -156,6 +168,8 @@ def statistics_summary(trades_summary_df: pd.DataFrame): summary["股票代码"] = symbol summary["方向"] = max_total_return_record["方向"] summary["根据SAR"] = max_total_return_record["根据SAR"] + summary["R算法"] = max_total_return_record["R算法"] + summary["K线条件"] = max_total_return_record["K线条件"] summary["总收益率%"] = max_total_return_record["总收益率%"] summary["自然收益率%"] = max_total_return_record["自然收益率%"] max_total_return_record_list.append(summary) @@ -178,14 +192,14 @@ def statistics_summary(trades_summary_df: pd.DataFrame): # 其它(如Series、list、dict、ndarray等)转字符串 return str(v) - for key_col in ["方向", "根据SAR"]: + for key_col in ["方向", "根据SAR", "R算法", "K线条件"]: 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) + max_total_return_record_df.groupby(["方向", "根据SAR", "R算法", "K线条件"], dropna=False) .size() .reset_index(name="数量") ) @@ -215,15 +229,39 @@ def statistics_summary(trades_summary_df: pd.DataFrame): by="数量", ascending=False, inplace=True ) max_total_return_record_df_sar_count.reset_index(drop=True, inplace=True) + + # 统计R算法的记录数目 + max_total_return_record_df_R_count = ( + max_total_return_record_df.groupby(["R算法"], dropna=False) + .size() + .reset_index(name="数量") + ) + max_total_return_record_df_R_count.sort_values( + by="数量", ascending=False, inplace=True + ) + max_total_return_record_df_R_count.reset_index(drop=True, inplace=True) + + # 统计K线条件的记录数目 + max_total_return_record_df_K_count = ( + max_total_return_record_df.groupby(["K线条件"], dropna=False) + .size() + .reset_index(name="数量") + ) + max_total_return_record_df_K_count.sort_values( + by="数量", ascending=False, inplace=True + ) + max_total_return_record_df_K_count.reset_index(drop=True, inplace=True) else: # 构造空结果,保证下游写入Excel不报错 max_total_return_record_df_grouped_count = pd.DataFrame( - columns=["方向", "根据SAR", "数量"] - ) + columns=["方向", "根据SAR", "R算法", "K线条件", "数量"] + ) max_total_return_record_df_direction_count = pd.DataFrame( columns=["方向", "数量"] ) max_total_return_record_df_sar_count = pd.DataFrame(columns=["根据SAR", "数量"]) + max_total_return_record_df_R_count = pd.DataFrame(columns=["R算法", "数量"]) + max_total_return_record_df_K_count = pd.DataFrame(columns=["K线条件", "数量"]) result = { "statistics_summary_df": statistics_summary_df, @@ -231,6 +269,8 @@ def statistics_summary(trades_summary_df: pd.DataFrame): "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, + "max_total_return_record_df_R_count": max_total_return_record_df_R_count, + "max_total_return_record_df_K_count": max_total_return_record_df_K_count, } return result @@ -335,9 +375,5 @@ def test(): if __name__ == "__main__": - # 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) + main() # test()