statistics orb strategy

This commit is contained in:
blade 2025-09-02 18:42:32 +08:00
parent 6ee64abaf5
commit f79834647d
3 changed files with 640 additions and 117 deletions

View File

@ -4,6 +4,12 @@ import pandas as pd
import numpy as np import numpy as np
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import seaborn as sns 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 import core.logger as logging
from config import OKX_MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE from config import OKX_MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE
from core.db.db_market_data import DBMarketData from core.db.db_market_data import DBMarketData
@ -19,11 +25,19 @@ logger = logging.logger
class ORBStrategy: class ORBStrategy:
def __init__( def __init__(
self, self,
symbol: str,
bar: str,
start_date: str,
end_date: str,
initial_capital=25000, initial_capital=25000,
max_leverage=4, max_leverage=4,
risk_per_trade=0.01, risk_per_trade=0.01,
commission_per_share=0.0005, commission_per_share=0.0005,
profit_target_multiple=10,
is_us_stock=False, is_us_stock=False,
direction=None,
by_sar=False,
symbol_bar_data=None,
): ):
""" """
初始化ORB策略参数 初始化ORB策略参数
@ -37,18 +51,31 @@ class ORBStrategy:
6. 止损/止盈根据$R计算$R=|entry_price-stop_price| 6. 止损/止盈根据$R计算$R=|entry_price-stop_price|
7. 盈利目标10R即10*$R 7. 盈利目标10R即10*$R
8. 账户净值曲线账户价值与市场价格 8. 账户净值曲线账户价值与市场价格
:param symbol: 股票代码
:param bar: K线周期
:param start_date: 开始日期
:param end_date: 结束日期
:param initial_capital: 初始账户资金美元 :param initial_capital: 初始账户资金美元
:param max_leverage: 最大杠杆倍数默认4倍符合FINRA规定 :param max_leverage: 最大杠杆倍数默认4倍符合FINRA规定
:param risk_per_trade: 单次交易风险比例默认1% :param risk_per_trade: 单次交易风险比例默认1%
:param commission_per_share: 每股交易佣金美元默认0.0005 :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( 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.initial_capital = initial_capital
self.max_leverage = max_leverage self.max_leverage = max_leverage
self.risk_per_trade = risk_per_trade self.risk_per_trade = risk_per_trade
self.commission_per_share = commission_per_share self.commission_per_share = commission_per_share
self.profit_target_multiple = profit_target_multiple
self.data = None # 存储K线数据 self.data = None # 存储K线数据
self.trades = [] # 存储交易记录 self.trades = [] # 存储交易记录
self.equity_curve = None # 存储账户净值曲线 self.equity_curve = None # 存储账户净值曲线
@ -64,9 +91,35 @@ class ORBStrategy:
self.db_market_data = DBMarketData(self.db_url) self.db_market_data = DBMarketData(self.db_url)
self.is_us_stock = is_us_stock self.is_us_stock = is_us_stock
self.output_chart_folder = r"./output/trade_sandbox/orb_strategy/chart/" 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_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 = "做空"
def fetch_intraday_data(self, symbol, start_date, end_date, interval="5m"): 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):
""" """
获取日内5分钟K线数据需yfinance支持部分数据可能有延迟 获取日内5分钟K线数据需yfinance支持部分数据可能有延迟
:param ticker: 股票代码如QQQTQQQ :param ticker: 股票代码如QQQTQQQ
@ -74,12 +127,10 @@ class ORBStrategy:
:param end_date: 结束日期格式YYYY-MM-DD :param end_date: 结束日期格式YYYY-MM-DD
:param interval: K线周期默认5分钟 :param interval: K线周期默认5分钟
""" """
logger.info(f"开始获取{symbol}数据:{start_date}{end_date},间隔{interval}") logger.info(f"开始获取{self.symbol}数据:{self.start_date}{self.end_date},间隔{self.bar}")
# data = yf.download( if self.symbol_bar_data is None or len(self.symbol_bar_data) == 0:
# symbol, start=start_date, end=end_date, interval=interval, progress=False
# )
data = self.db_market_data.query_market_data_by_symbol_bar( data = self.db_market_data.query_market_data_by_symbol_bar(
symbol, interval, start=start_date, end=end_date self.symbol, self.bar, start=self.start_date, end=self.end_date
) )
data = pd.DataFrame(data) data = pd.DataFrame(data)
data.sort_values(by="date_time", inplace=True) data.sort_values(by="date_time", inplace=True)
@ -98,12 +149,47 @@ class ORBStrategy:
data["Date"] = data[date_time_field].dt.date data["Date"] = data[date_time_field].dt.date
# 将Date转换为datetime64[ns]类型以确保类型一致 # 将Date转换为datetime64[ns]类型以确保类型一致
data["Date"] = pd.to_datetime(data["Date"]) 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[ self.data = data[
["symbol", "bar", "Date", date_time_field, "Open", "High", "Low", "Close", "Volume"] [
"symbol",
"bar",
"Date",
date_time_field,
"Open",
"High",
"Low",
"Close",
"Volume",
"sar_signal",
]
].copy() ].copy()
self.data.rename(columns={date_time_field: "date_time"}, inplace=True) self.data.rename(columns={date_time_field: "date_time"}, inplace=True)
logger.info(f"成功获取{symbol}数据:{len(self.data)}{interval}K线") 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): def calculate_shares(self, account_value, entry_price, stop_price):
""" """
@ -134,20 +220,15 @@ class ORBStrategy:
return int(max_shares) # 股数取整 return int(max_shares) # 股数取整
def generate_orb_signals(self, direction: str = None, by_sar: bool = False): def generate_orb_signals(self):
""" """
生成ORB策略信号每日仅1次交易机会 生成ORB策略信号每日仅1次交易机会
- 第一根5分钟K线确定开盘区间High1, Low1 - 第一根5分钟K线确定开盘区间High1, Low1
- 第二根5分钟K线根据第一根K线方向生成多空信号 - 第二根5分钟K线根据第一根K线方向生成多空信号
:param direction: 方向None=自动Long=多头Short=空头
:param by_sar: 是否根据SAR指标生成信号True=False=
""" """
direction_desc = "既做多又做空" logger.info(
if direction == "Long": f"开始生成ORB策略信号{self.direction_desc}根据SAR指标{self.by_sar}"
direction_desc = "做多" )
elif direction == "Short":
direction_desc = "做空"
logger.info(f"开始生成ORB策略信号{direction_desc}根据SAR指标{by_sar}")
if self.data is None: if self.data is None:
raise ValueError("请先调用fetch_intraday_data获取数据") raise ValueError("请先调用fetch_intraday_data获取数据")
@ -164,6 +245,7 @@ class ORBStrategy:
low1 = first_candle["Low"] low1 = first_candle["Low"]
open1 = first_candle["Open"] open1 = first_candle["Open"]
close1 = first_candle["Close"] close1 = first_candle["Close"]
sar_signal = first_candle["sar_signal"]
# 第二根5分钟K线entry信号 # 第二根5分钟K线entry信号
second_candle = daily_data.iloc[1] second_candle = daily_data.iloc[1]
@ -171,11 +253,19 @@ class ORBStrategy:
entry_time = second_candle.date_time # entry时间 entry_time = second_candle.date_time # entry时间
# 生成信号第一根K线方向决定多空排除十字星open1 == close1 # 生成信号第一根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线收涨→多头信号 # 第一根K线收涨→多头信号
signal = "Long" signal = "Long"
stop_price = low1 # 多头止损=第一根K线最低价 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线收跌→空头信号 # 第一根K线收跌→空头信号
signal = "Short" signal = "Short"
stop_price = high1 # 空头止损=第一根K线最高价 stop_price = high1 # 空头止损=第一根K线最高价
@ -213,12 +303,12 @@ class ORBStrategy:
f"生成信号完成:共{len(signals_df)}个交易日,其中多头{sum(signals_df['Signal']=='Long')}次,空头{sum(signals_df['Signal']=='Short')}" 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策略 回测ORB策略
:param profit_target_multiple: 盈利目标倍数默认10倍$R即10R :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: if "Signal" not in self.data.columns:
raise ValueError("请先调用generate_orb_signals生成策略信号") raise ValueError("请先调用generate_orb_signals生成策略信号")
@ -255,9 +345,9 @@ class ORBStrategy:
low1 = signal_row["Low1"] low1 = signal_row["Low1"]
risk_assumed = abs(entry_price - stop_price) # 计算$R risk_assumed = abs(entry_price - stop_price) # 计算$R
profit_target = ( profit_target = (
entry_price + (risk_assumed * profit_target_multiple) entry_price + (risk_assumed * self.profit_target_multiple)
if signal == "Long" 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 break
elif high >= profit_target: elif high >= profit_target:
exit_price = 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"] exit_time = row["date_time"]
break break
elif signal == "Short": elif signal == "Short":
@ -305,7 +395,7 @@ class ORBStrategy:
break break
elif low <= profit_target: elif low <= profit_target:
exit_price = 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"] exit_time = row["date_time"]
break break
@ -333,6 +423,10 @@ class ORBStrategy:
self.trades.append( self.trades.append(
{ {
"TradeID": trade_id, "TradeID": trade_id,
"Direction": self.direction_desc,
"BySar": self.sar_desc,
"Symbol": self.symbol,
"Bar": self.bar,
"Date": date, "Date": date,
"Signal": signal, "Signal": signal,
"EntryTime": signal_row.date_time.strftime("%Y-%m-%d %H:%M:%S"), "EntryTime": signal_row.date_time.strftime("%Y-%m-%d %H:%M:%S"),
@ -353,40 +447,91 @@ class ORBStrategy:
equity_history.append(account_value) equity_history.append(account_value)
trade_id += 1 trade_id += 1
if len(self.trades) == 0:
logger.info("没有交易")
self.trades_df = pd.DataFrame()
self.initial_trade_summary()
return
# 生成净值曲线 # 生成净值曲线
self.create_equity_curve() 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 = ( total_return = (
(account_value - self.initial_capital) / self.initial_capital * 100 (account_value - self.initial_capital) / self.initial_capital * 100
) )
win_rate = ( win_rate = (
(trades_df["ProfitLoss"] > 0).sum() / len(trades_df) * 100 (self.trades_df["ProfitLoss"] > 0).sum() / len(self.trades_df) * 100
if len(trades_df) > 0 if len(self.trades_df) > 0
else 0 else 0
) )
# 计算盈亏比 # 计算盈亏比
profit_sum = trades_df[trades_df["ProfitLoss"] > 0]["ProfitLoss"].sum() profit_sum = self.trades_df[self.trades_df["ProfitLoss"] > 0]["ProfitLoss"].sum()
loss_sum = abs(trades_df[trades_df["ProfitLoss"] < 0]["ProfitLoss"].sum()) loss_sum = abs(self.trades_df[self.trades_df["ProfitLoss"] < 0]["ProfitLoss"].sum())
if loss_sum == 0: if loss_sum == 0:
profit_loss_ratio = float('inf') profit_loss_ratio = float("inf")
else: else:
profit_loss_ratio = (profit_sum / loss_sum) * 100 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("\n" + "=" * 50)
logger.info("ORB策略回测结果") logger.info("ORB策略回测结果")
logger.info("=" * 50) 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"初始资金:${self.initial_capital:,.2f}")
logger.info(f"最终资金:${account_value:,.2f}") logger.info(f"最终资金:${account_value:,.2f}")
self.trades_summary["最终资金$"] = account_value
logger.info(f"总收益率:{total_return:.2f}%") logger.info(f"总收益率:{total_return:.2f}%")
logger.info(f"总交易次数:{len(trades_df)}") 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}%") logger.info(f"盈亏比:{profit_loss_ratio:.2f}%")
self.trades_summary["盈亏比%"] = profit_loss_ratio
logger.info(f"胜率:{win_rate:.2f}%") logger.info(f"胜率:{win_rate:.2f}%")
if len(trades_df) > 0: self.trades_summary["胜率%"] = win_rate
logger.info(f"平均每笔盈亏:${trades_df['ProfitLoss'].mean():.2f}") logger.info(f"平均每笔盈亏:${self.trades_df['ProfitLoss'].mean():.2f}")
logger.info(f"最大单笔盈利:${trades_df['ProfitLoss'].max():.2f}") self.trades_summary["平均每笔盈亏$"] = self.trades_df["ProfitLoss"].mean()
logger.info(f"最大单笔亏损:${trades_df['ProfitLoss'].min():.2f}") 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
def create_equity_curve(self): def create_equity_curve(self):
""" """
@ -437,9 +582,25 @@ class ORBStrategy:
account_value_to_1 = self.equity_curve["AccountValue"] / first_account_value account_value_to_1 = self.equity_curve["AccountValue"] / first_account_value
market_price_to_1 = self.equity_curve["MarketPrice"] / first_market_price market_price_to_1 = self.equity_curve["MarketPrice"] / first_market_price
plt.figure(figsize=(12, 6)) 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(
plt.plot(self.equity_curve["DateTime"], market_price_to_1, label="市场价格", color='green', linewidth=2, marker='s', markersize=4) self.equity_curve["DateTime"],
plt.title(f"ORB策略账户净值曲线 {symbol} {bar}", fontsize=14, fontweight='bold') 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.xlabel("时间", fontsize=12)
plt.ylabel("涨跌变化", fontsize=12) plt.ylabel("涨跌变化", fontsize=12)
plt.legend(fontsize=11) plt.legend(fontsize=11)
@ -463,41 +624,95 @@ class ORBStrategy:
label_indices.append(len(self.equity_curve) - 1) label_indices.append(len(self.equity_curve) - 1)
# 设置x轴标签 # 设置x轴标签
plt.xticks(self.equity_curve["DateTime"].iloc[label_indices], plt.xticks(
self.equity_curve["DateTime"].iloc[label_indices], self.equity_curve["DateTime"].iloc[label_indices],
rotation=45, ha='right', fontsize=10) self.equity_curve["DateTime"].iloc[label_indices],
rotation=45,
ha="right",
fontsize=10,
)
else: else:
# 如果数据点较少,全部显示 # 如果数据点较少,全部显示
plt.xticks(self.equity_curve["DateTime"], plt.xticks(
self.equity_curve["DateTime"], self.equity_curve["DateTime"],
rotation=45, ha='right', fontsize=10) self.equity_curve["DateTime"],
rotation=45,
ha="right",
fontsize=10,
)
plt.tight_layout() plt.tight_layout()
save_path = f"{self.output_chart_folder}/{symbol}_{bar}_orb_strategy_equity_curve.png" self.chart_save_path = (
plt.savefig(save_path, dpi=150, bbox_inches='tight') 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() 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 ------------------- # ------------------- 策略示例回测QQQ的ORB策略2016-2023 -------------------
if __name__ == "__main__": if __name__ == "__main__":
# 初始化ORB策略 # 初始化ORB策略
orb_strategy = ORBStrategy( orb_strategy = ORBStrategy(
symbol="ETH-USDT",
bar="5m",
start_date="2025-05-15",
end_date="2025-08-20",
initial_capital=25000, initial_capital=25000,
max_leverage=4, max_leverage=4,
risk_per_trade=0.01, risk_per_trade=0.01,
commission_per_share=0.0005, 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.run()
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()

View File

@ -1,35 +1,343 @@
from core.trade.orb_trade import ORBStrategy 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 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 logger = logging.logger
def main(): 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: for symbol in symbols:
logger.info(f"开始回测 {symbol}") logger.info(
# 初始化ORB策略 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( 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 SheetSheet名称为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}的图表")
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)
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 SheetSheet名称为{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)
# 两列布局左列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, initial_capital=25000,
max_leverage=4, max_leverage=4,
risk_per_trade=0.01, risk_per_trade=0.01,
commission_per_share=0.0005, commission_per_share=0.0005,
is_us_stock=True,
) )
# 1. 获取QQQ的5分钟日内数据2024-2025注意yfinance免费版可能限制历史日内数据建议用专业数据源 orb_strategy.run()
orb_strategy.fetch_intraday_data(
symbol=symbol, start_date="2024-11-30", end_date="2025-08-30", interval="5m"
)
# 2. 生成ORB策略信号
orb_strategy.generate_orb_signals()
# 3. 回测策略盈利目标10R
orb_strategy.backtest(profit_target_multiple=10)
# 4. 绘制净值曲线
orb_strategy.plot_equity_curve()
if __name__ == "__main__": if __name__ == "__main__":
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)
# test()