From e990db26a6d4e23f1735c9c51ecb56fcbee8a41b Mon Sep 17 00:00:00 2001 From: blade <8019068@qq.com> Date: Mon, 1 Sep 2025 18:01:21 +0800 Subject: [PATCH] optimize ORB strategy --- config.py | 2 +- .../ma_break_statistics.cpython-312.pyc | Bin 32078 -> 32452 bytes .../__pycache__/orb_trade.cpython-312.pyc | Bin 15826 -> 19872 bytes core/trade/ma_break_statistics.py | 18 +- core/trade/orb_trade.py | 165 ++++++++++++++---- market_data_main.py | 8 +- orb_trade_main.py | 43 +++-- trade_ma_strategy_main.py | 6 +- 8 files changed, 177 insertions(+), 65 deletions(-) diff --git a/config.py b/config.py index 835ce7b..fbf098f 100644 --- a/config.py +++ b/config.py @@ -73,7 +73,7 @@ OKX_MONITOR_CONFIG = { US_STOCK_MONITOR_CONFIG = { "volume_monitor":{ "symbols": ["QQQ", "TQQQ", "MSFT", "AAPL", "GOOG", "NVDA", "META", "AMZN", "TSLA", "AVGO"], - "bars": ["5m"], + "bars": ["5", "15m", "30m", "1H"], "initial_date": "2015-08-31 00:00:00" } } diff --git a/core/trade/__pycache__/ma_break_statistics.cpython-312.pyc b/core/trade/__pycache__/ma_break_statistics.cpython-312.pyc index db4662d9703daadef0121b593432562d6cd204fb..ad1c914e0dddc717bc4feac13fa3e3be07822f46 100644 GIT binary patch delta 3561 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 eiS558P-Le^KdB@w9}pPcp3{g2!(GX3 z999mBwd&gkE_R5=46uM;zY0c zxIwRlxTx30v?*>TB^^uLs>^I?d)%SR_*7Zi8F!{#aaX!LUardwsfx5a?$&7`;2q~eVwP>5gh{q5bTE|Xq3<)6Y)P+xyG7xS+ zp{qa&MDovJ#~#U`m9Z%@d0;(o7@7@6)yRHp z*hEagMGP<{Rw+mW=j0022)AKs*>~Bq|Cn&X!bU5W zke10;E3Wg+3%+ZHL|?8HP8e94A)U<7h)yLldC5W>+4sFCNQ`arwQt&ku6_>yg37fJ zRO_e4rm}DMPCs^F`qRnjBu3R!WHScPDES0d-Yq;v&i(xLXdU~OPX#BI_`g9cWn*@k zA9JXLU2F8XR9>~IcGapnR8cj`PG$(aXmZKra>az3y&7mXdGs9*vki7PdvSx$#(xEuRg zl}w%%dh^M&0)eZy(>*L)T@U-ev3jj>7?g60*=MUiBvF=&9PplfV9NIP!RcP*;bwvPGxNBVYPgI`Am4z}9xO>M! zO2oL)3)nMn3bB7IijXj?Zd}zE9EIC#toj~n;RS2r!ERW<1~n{KF}1NRu+TCJDOT2d zW{50~Mc6ey!k%mTE2(DY#chs;d#$l)X%&gEj>TaZF}}FP)`<4>iLAkiu-8G>v>=n_ zWo?$bT5IPeE@MF`2-0GfYW^l1hv1S8hr3`-^f3e(K|vTm7)0nr`1e7?GdOD5mCp`G zw`Fs=QTM|%JCMvrcPHqel8-jEweFPYeo&hGCzMgw=8PQ84n(^Xg=o{}Y_}xYHPddI zkd>|7vIa`bffOEfO-SZ43Hmtl6yafnM-ecwmvVze$&92GeA{T|WL}{(`(zH1f8r3u z45=A+DeW|=bQ8vkmJT)tiD{W4~>!6LV1zv$nP< z5BKKYwmQ@IfzM?{cBCy$^6Zao=SXpKeA#kBn%GQx0Pt3OO;yur%Wo>f#X#t+<8mNY zY*=>MT3odDw5?d*#8xifB;GP|E+6~M@@4E7%U9MHYh%TzRIF((R@WEnT9Ac*Y2`-& zM|s5^gVRxVtDbePsO(7^^28FnlQD4N$eFFrZ7te^?^SrlW2a(8UvNBmDmk7#l|7r7 z@ioo*Vl%$jWnaq$_td`2Yqm}A+Iz>qyUXr~V%U02c*h*L<6@Z=kBfKgaG|oEmCvxh ztUOhvr_cGf63mn1@Y$fJ&%ZKwSFwZs)oiHatYzVzgBO%nknIx=HoV@;2G_aS3mbgw zlY&RDFV%Wna+&O$aNT`j6OQ$)uNG8pD2Q3c0@tkUp75L(n6I=RSW*5;RhvQy|qWsbogU_x#SyA#!f+~^*N={7d?Eoe%W0;> zp#$itevH{4HaB?TeSaYPq(t;LV-EK{3Q>Ob6Dv%Q0+X{77T7X4blq%w#(La(des$k z-FrZrkDH5j*Q`A}V-L^T>t^hA_ptRd_WEK)U9qC_&lb^Q#qhG@M?kTxVz#VyrmXf{ z!_@MhJpE?_4~<4_v>dlgSF}vkUNNs@XYThtI_-{4@mI`iaVQw}gnfR@lb+MNu9$1) zxi48M25XNb%$-ZX4hz8Ce9H6smfw#a?Y#N-*FL(my|1ei7MWj>mdG5YPszfDA80td z6iC{Gpy#H~AoV)Je*?V6>1JsR1-RupQ~;4%32>BylN+cmdVNKIRWV$5+ZGX5-`+?f z;v?iw8%aQn{Ap!35v{kk@%_BGm)w3ptQOrRlW0vd_b%;CCz6@oUivGv{%eG3gc$%y zD5cmi3g*AH9(r(5*QHY111NqFVH?7Bgv$W1fnUR_Ml%-%l9@!NU(xu&fMy&{WaLDS zZe(vgxVYvbsA#5~lIY9Q4E-^3w*D+tmZq|UkY#C*iQ5W?e+u%_#NR;;dzSX~=H@>} zORM(r1*5ar0k=Y5v%i@;(ph;VrL3nPfDA(__cMT7213ZYocCSM{VrE=m5TszLs>u5v9`xz)epdTtrNvc1YNC=&Na9OA9LHrBK~FQEB<0Sg69TL1t6 diff --git a/core/trade/ma_break_statistics.py b/core/trade/ma_break_statistics.py index 27ffe2f..8721002 100644 --- a/core/trade/ma_break_statistics.py +++ b/core/trade/ma_break_statistics.py @@ -12,7 +12,7 @@ from openpyxl.drawing.image import Image import openpyxl from openpyxl.styles import Font from PIL import Image as PILImage -from config import OKX_MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE +from config import OKX_MONITOR_CONFIG, US_STOCK_MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE from core.db.db_market_data import DBMarketData from core.db.db_huge_volume_data import DBHugeVolumeData from core.utils import timestamp_to_datetime, transform_date_time_to_timestamp @@ -32,7 +32,7 @@ class MaBreakStatistics: 之间的涨跌幅 """ - def __init__(self): + def __init__(self, is_us_stock: bool = False): mysql_user = MYSQL_CONFIG.get("user", "xch") mysql_password = MYSQL_CONFIG.get("password", "") if not mysql_password: @@ -44,10 +44,20 @@ class MaBreakStatistics: self.db_url = f"mysql+pymysql://{mysql_user}:{mysql_password}@{mysql_host}:{mysql_port}/{mysql_database}" self.db_market_data = DBMarketData(self.db_url) self.db_huge_volume_data = DBHugeVolumeData(self.db_url) - self.symbols = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( + if is_us_stock: + self.symbols = US_STOCK_MONITOR_CONFIG.get("volume_monitor", {}).get( + "symbols", ["QQQ"] + ) + else: + self.symbols = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( "symbols", ["XCH-USDT"] ) - self.bars = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( + if is_us_stock: + self.bars = US_STOCK_MONITOR_CONFIG.get("volume_monitor", {}).get( + "bars", ["5m"] + ) + else: + self.bars = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( "bars", ["5m", "15m", "30m", "1H"] ) self.stats_output_dir = "./output/trade_sandbox/ma_strategy/excel/" diff --git a/core/trade/orb_trade.py b/core/trade/orb_trade.py index d4759d0..4d247dc 100644 --- a/core/trade/orb_trade.py +++ b/core/trade/orb_trade.py @@ -1,4 +1,5 @@ import yfinance as yf +import os import pandas as pd import numpy as np import matplotlib.pyplot as plt @@ -22,15 +23,28 @@ class ORBStrategy: max_leverage=4, risk_per_trade=0.01, commission_per_share=0.0005, + is_us_stock=False, ): """ 初始化ORB策略参数 + ORB策略说明: + 1. 每天仅1次交易机会,多头或空头,排除十字星:open1 == close1 + 2. 第一根5分钟K线:确定开盘区间(High1, Low1) + 3. 第二根5分钟K线:根据第一根K线方向生成多空信号,open1close1为空头 + entry_price=第二根K线开盘价,stop_price=第一根K线最低价(多头)或第一根K线最高价(空头) + 4. 多头:跌破止损→止损;突破止盈→止盈 + 5. 空头:突破止损→止损;跌破止盈→止盈 + 6. 止损/止盈:根据$R计算,$R=|entry_price-stop_price| + 7. 盈利目标:10R,即10*$R + 8. 账户净值曲线:账户价值与市场价格 :param initial_capital: 初始账户资金(美元) :param max_leverage: 最大杠杆倍数(默认4倍,符合FINRA规定) :param risk_per_trade: 单次交易风险比例(默认1%) :param commission_per_share: 每股交易佣金(美元,默认0.0005) """ - logger.info(f"初始化ORB策略参数:初始账户资金={initial_capital},最大杠杆倍数={max_leverage},单次交易风险比例={risk_per_trade},每股交易佣金={commission_per_share}") + logger.info( + f"初始化ORB策略参数:初始账户资金={initial_capital},最大杠杆倍数={max_leverage},单次交易风险比例={risk_per_trade},每股交易佣金={commission_per_share}" + ) self.initial_capital = initial_capital self.max_leverage = max_leverage self.risk_per_trade = risk_per_trade @@ -48,6 +62,9 @@ class ORBStrategy: self.db_url = f"mysql+pymysql://{mysql_user}:{mysql_password}@{mysql_host}:{mysql_port}/{mysql_database}" 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/" + os.makedirs(self.output_chart_folder, exist_ok=True) def fetch_intraday_data(self, symbol, start_date, end_date, interval="5m"): """ @@ -72,14 +89,20 @@ class ORBStrategy: data["Low"] = data["low"] data["Close"] = data["close"] data["Volume"] = data["volume"] - # 将data["date_time"]从字符串类型转换为日期 - data["date_time"] = pd.to_datetime(data["date_time"]) + 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"].dt.date + data["Date"] = data[date_time_field].dt.date # 将Date转换为datetime64[ns]类型以确保类型一致 data["Date"] = pd.to_datetime(data["Date"]) - self.data = data[["Date", "date_time", "Open", "High", "Low", "Close", "Volume"]].copy() + 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线") def calculate_shares(self, account_value, entry_price, stop_price): @@ -90,7 +113,9 @@ class ORBStrategy: :param stop_price: 止损价格(多头=第一根K线最低价,空头=第一根K线最高价) :return: 整数股数(Shares) """ - logger.info(f"开始计算交易股数:账户价值={account_value},entry价格={entry_price},止损价格={stop_price}") + logger.info( + f"开始计算交易股数:账户价值={account_value},entry价格={entry_price},止损价格={stop_price}" + ) # 计算单交易风险金额($R) risk_per_trade_dollar = abs(entry_price - stop_price) # 风险金额取绝对值 if risk_per_trade_dollar <= 0: @@ -149,7 +174,7 @@ class ORBStrategy: stop_price = high1 # 空头止损=第一根K线最高价 else: # 十字星→无信号 - signal = "None" + signal = None stop_price = None signals.append( @@ -167,10 +192,12 @@ class ORBStrategy: # 将信号合并到原始数据 signals_df = pd.DataFrame(signals) # 确保Date列类型一致,将Date转换为datetime64[ns]类型 - signals_df['Date'] = pd.to_datetime(signals_df['Date']) + signals_df["Date"] = pd.to_datetime(signals_df["Date"]) # 使用merge而不是join来合并数据,根据signals_df的EntryTime与self.data的date_time进行匹配 # TODO: 这里需要优化 - self.data = self.data.merge(signals_df, left_on="date_time", right_on="EntryTime", how="left") + self.data = self.data.merge( + signals_df, left_on="date_time", right_on="EntryTime", how="left" + ) # 将Date_x和Date_y合并为Date self.data["Date"] = self.data["Date_x"].combine_first(self.data["Date_y"]) # 删除Date_x和Date_y @@ -214,6 +241,7 @@ class ORBStrategy: signal = signal_row["Signal"] if pd.isna(signal): continue + entry_price = signal_row["EntryPrice"] stop_price = signal_row["StopPrice"] high1 = signal_row["High1"] @@ -254,24 +282,24 @@ class ORBStrategy: if low <= stop_price: exit_price = stop_price exit_reason = "Stop Loss" - exit_time = time + exit_time = row["date_time"] break elif high >= profit_target: exit_price = profit_target exit_reason = "Profit Target (10R)" - exit_time = time + exit_time = row["date_time"] break elif signal == "Short": # 空头:突破止损→止损;跌破止盈→止盈 if high >= stop_price: exit_price = stop_price exit_reason = "Stop Loss" - exit_time = time + exit_time = row["date_time"] break elif low <= profit_target: exit_price = profit_target exit_reason = "Profit Target (10R)" - exit_time = time + exit_time = row["date_time"] break # 若未触发止损/止盈,当日收盘平仓 @@ -280,12 +308,16 @@ class ORBStrategy: exit_reason = "End of Day (EoD)" exit_time = daily_prices.iloc[-1].date_time + initial_account_value = account_value # 计算盈亏 if signal == "Long": profit_loss = (exit_price - entry_price) * shares - total_commission else: # Short profit_loss = (entry_price - exit_price) * shares - total_commission + # 计算盈亏百分比,profit_loss除以当期初始资金 + profit_loss_percentage = (profit_loss / initial_account_value) * 100 + # 更新账户价值 account_value += profit_loss account_value = max(account_value, 0) # 账户价值不能为负 @@ -296,14 +328,16 @@ class ORBStrategy: "TradeID": trade_id, "Date": date, "Signal": signal, - "EntryTime": signal_row.date_time, + "EntryTime": signal_row.date_time.strftime("%Y-%m-%d %H:%M:%S"), "EntryPrice": entry_price, - "ExitTime": exit_time, + "ExitTime": exit_time.strftime("%Y-%m-%d %H:%M:%S"), "ExitPrice": exit_price, "Shares": shares, "RiskAssumed": risk_assumed, "ProfitLoss": profit_loss, + "ProfitLossPercentage": profit_loss_percentage, "ExitReason": exit_reason, + "AccountValueInitial": initial_account_value, "AccountValueAfter": account_value, } ) @@ -313,12 +347,7 @@ class ORBStrategy: trade_id += 1 # 生成净值曲线 - self.equity_curve = pd.Series( - equity_history, - index=pd.date_range( - start=self.data.index[0].date(), periods=len(equity_history), freq="D" - ), - ) + self.create_equity_curve() # 输出回测结果 trades_df = pd.DataFrame(self.trades) @@ -330,6 +359,13 @@ class ORBStrategy: if len(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()) + if loss_sum == 0: + profit_loss_ratio = float('inf') + else: + profit_loss_ratio = (profit_sum / loss_sum) * 100 logger.info("\n" + "=" * 50) logger.info("ORB策略回测结果") @@ -338,12 +374,40 @@ class ORBStrategy: 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): + """ + 创建账户净值曲线 + """ + equity_curve_list = [] + # 将self.data.index[0].Date的值转换为字符串,且格式为YYYY-MM-DD + first_date = self.data.iloc[0].date_time.strftime("%Y-%m-%d %H:%M:%S") + first_open = float(self.data.iloc[0].Open) + equity_curve_list.append( + { + "DateTime": first_date, + "AccountValue": self.initial_capital, + "MarketPrice": first_open, + } + ) + for trade in self.trades: + equity_curve_list.append( + { + "DateTime": trade["ExitTime"], + "AccountValue": trade["AccountValueAfter"], + "MarketPrice": trade["ExitPrice"], + } + ) + self.equity_curve = pd.DataFrame(equity_curve_list) + self.equity_curve.sort_values(by="DateTime", inplace=True) + self.equity_curve.reset_index(drop=True, inplace=True) + def plot_equity_curve(self): """ 绘制账户净值曲线 @@ -351,7 +415,7 @@ class ORBStrategy: logger.info("开始绘制账户净值曲线") if self.equity_curve is None: raise ValueError("请先调用backtest进行回测") - + # seaborn风格设置 sns.set_theme(style="whitegrid") # plt.rcParams['font.family'] = "SimHei" @@ -359,22 +423,51 @@ class ORBStrategy: plt.rcParams["font.size"] = 11 # 设置字体大小 plt.rcParams["axes.unicode_minus"] = False # 解决负号显示问题 + symbol = self.data.iloc[0].symbol + bar = self.data.iloc[0].bar + first_account_value = self.equity_curve.iloc[0]["AccountValue"] + first_market_price = self.equity_curve.iloc[0]["MarketPrice"] + 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.index, - self.equity_curve.values, - label="ORB策略净值", - color="blue", - ) - plt.axhline( - y=self.initial_capital, color="red", linestyle="--", label="初始资金" - ) - plt.title("ORB策略账户净值曲线", fontsize=14) - plt.xlabel("日期", fontsize=12) - plt.ylabel("账户价值(美元)", fontsize=12) - plt.legend() + 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.xlabel("时间", fontsize=12) + plt.ylabel("涨跌变化", fontsize=12) + plt.legend(fontsize=11) plt.grid(True, alpha=0.3) - plt.show() + + # 设置x轴标签,避免matplotlib警告 + # 选择合适的时间间隔显示标签,避免过于密集 + 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) + else: + # 如果数据点较少,全部显示 + 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') + plt.close() # ------------------- 策略示例:回测QQQ的ORB策略(2016-2023) ------------------- diff --git a/market_data_main.py b/market_data_main.py index 03c6963..f8c2e3a 100644 --- a/market_data_main.py +++ b/market_data_main.py @@ -109,14 +109,16 @@ class MarketDataMain: # 如果bar为1D, 则end_time_ts与start_time_ts相差超过10天,则按照10天为单位 # 获取数据,直到end_time_ts threshold = None - if bar in ["5m", "15m", "30m"]: + if bar in ["5m", "15m", "30m", "1H"]: if self.is_us_stock: if bar == "5m": threshold = 86400000 * 4 elif bar == "15m": - threshold = 86400000 * 4 * 3 + threshold = 86400000 * 6 elif bar == "30m": - threshold = 86400000 * 4 * 6 + threshold = 86400000 * 12 + elif bar == "1H": + threshold = 86400000 * 24 else: threshold = 86400000 elif bar in ["1H", "4H"]: diff --git a/orb_trade_main.py b/orb_trade_main.py index d14a0d7..5421938 100644 --- a/orb_trade_main.py +++ b/orb_trade_main.py @@ -1,27 +1,34 @@ from core.trade.orb_trade import ORBStrategy +from config import US_STOCK_MONITOR_CONFIG +import core.logger as logging + +logger = logging.logger def main(): - # 初始化ORB策略 - orb_strategy = ORBStrategy( - initial_capital=25000, - max_leverage=4, - risk_per_trade=0.01, - commission_per_share=0.0005, - ) + symbols = US_STOCK_MONITOR_CONFIG.get("volume_monitor", {}).get("symbols", ["QQQ"]) + 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" + ) - # 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() - # 2. 生成ORB策略信号 - orb_strategy.generate_orb_signals() + # 3. 回测策略(盈利目标10R) + orb_strategy.backtest(profit_target_multiple=10) - # 3. 回测策略(盈利目标10R) - orb_strategy.backtest(profit_target_multiple=10) - - # 4. 绘制净值曲线 - orb_strategy.plot_equity_curve() + # 4. 绘制净值曲线 + orb_strategy.plot_equity_curve() if __name__ == "__main__": diff --git a/trade_ma_strategy_main.py b/trade_ma_strategy_main.py index 128e88e..927c85a 100644 --- a/trade_ma_strategy_main.py +++ b/trade_ma_strategy_main.py @@ -25,8 +25,8 @@ from config import ( logger = logging.logger class TradeMaStrategyMain: - def __init__(self): - self.ma_break_statistics = MaBreakStatistics() + def __init__(self, is_us_stock: bool = False): + self.ma_break_statistics = MaBreakStatistics(is_us_stock=is_us_stock) def batch_ma_break_statistics(self): """ @@ -59,5 +59,5 @@ class TradeMaStrategyMain: if __name__ == "__main__": - trade_ma_strategy_main = TradeMaStrategyMain() + trade_ma_strategy_main = TradeMaStrategyMain(is_us_stock=True) trade_ma_strategy_main.batch_ma_break_statistics() \ No newline at end of file