2025-08-31 03:20:59 +00:00
|
|
|
|
import yfinance as yf
|
2025-09-01 10:01:21 +00:00
|
|
|
|
import os
|
2025-08-31 03:20:59 +00:00
|
|
|
|
import pandas as pd
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
|
import seaborn as sns
|
2025-09-02 10:42:32 +00:00
|
|
|
|
from openpyxl import Workbook
|
|
|
|
|
|
from openpyxl.drawing.image import Image
|
|
|
|
|
|
import openpyxl
|
|
|
|
|
|
from openpyxl.styles import Font
|
|
|
|
|
|
from PIL import Image as PILImage
|
2025-09-09 06:15:29 +00:00
|
|
|
|
from datetime import datetime, timedelta
|
2025-09-02 10:42:32 +00:00
|
|
|
|
|
2025-08-31 03:20:59 +00:00
|
|
|
|
import core.logger as logging
|
2025-09-25 04:28:43 +00:00
|
|
|
|
from config import OKX_MONITOR_CONFIG, COIN_MYSQL_CONFIG, WINDOW_SIZE
|
2025-08-31 03:20:59 +00:00
|
|
|
|
from core.db.db_market_data import DBMarketData
|
2025-09-09 06:15:29 +00:00
|
|
|
|
from core.db.db_binance_data import DBBinanceData
|
2025-08-31 03:20:59 +00:00
|
|
|
|
from core.db.db_huge_volume_data import DBHugeVolumeData
|
|
|
|
|
|
from core.utils import timestamp_to_datetime, transform_date_time_to_timestamp
|
|
|
|
|
|
|
|
|
|
|
|
# seaborn支持中文
|
|
|
|
|
|
plt.rcParams["font.family"] = ["SimHei"]
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.logger
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ORBStrategy:
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
2025-09-02 10:42:32 +00:00
|
|
|
|
symbol: str,
|
|
|
|
|
|
bar: str,
|
|
|
|
|
|
start_date: str,
|
|
|
|
|
|
end_date: str,
|
2025-08-31 03:20:59 +00:00
|
|
|
|
initial_capital=25000,
|
|
|
|
|
|
max_leverage=4,
|
|
|
|
|
|
risk_per_trade=0.01,
|
|
|
|
|
|
commission_per_share=0.0005,
|
2025-09-02 10:42:32 +00:00
|
|
|
|
profit_target_multiple=10,
|
2025-09-01 10:01:21 +00:00
|
|
|
|
is_us_stock=False,
|
2025-09-09 06:15:29 +00:00
|
|
|
|
is_binance=False,
|
2025-09-02 10:42:32 +00:00
|
|
|
|
direction=None,
|
|
|
|
|
|
by_sar=False,
|
|
|
|
|
|
symbol_bar_data=None,
|
2025-09-09 06:15:29 +00:00
|
|
|
|
symbol_1h_data=None,
|
2025-09-03 10:26:27 +00:00
|
|
|
|
price_range_mean_as_R=False,
|
|
|
|
|
|
by_big_k=False,
|
2025-09-09 06:15:29 +00:00
|
|
|
|
by_1h_k=False,
|
2025-08-31 03:20:59 +00:00
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
初始化ORB策略参数
|
2025-09-01 10:01:21 +00:00
|
|
|
|
ORB策略说明:
|
|
|
|
|
|
1. 每天仅1次交易机会,多头或空头,排除十字星:open1 == close1
|
|
|
|
|
|
2. 第一根5分钟K线:确定开盘区间(High1, Low1)
|
|
|
|
|
|
3. 第二根5分钟K线:根据第一根K线方向生成多空信号,open1<close1为多头,open1>close1为空头
|
|
|
|
|
|
entry_price=第二根K线开盘价,stop_price=第一根K线最低价(多头)或第一根K线最高价(空头)
|
|
|
|
|
|
4. 多头:跌破止损→止损;突破止盈→止盈
|
|
|
|
|
|
5. 空头:突破止损→止损;跌破止盈→止盈
|
|
|
|
|
|
6. 止损/止盈:根据$R计算,$R=|entry_price-stop_price|
|
|
|
|
|
|
7. 盈利目标:10R,即10*$R
|
|
|
|
|
|
8. 账户净值曲线:账户价值与市场价格
|
2025-09-02 10:42:32 +00:00
|
|
|
|
:param symbol: 股票代码
|
|
|
|
|
|
:param bar: K线周期
|
|
|
|
|
|
:param start_date: 开始日期
|
|
|
|
|
|
:param end_date: 结束日期
|
2025-08-31 03:20:59 +00:00
|
|
|
|
:param initial_capital: 初始账户资金(美元)
|
|
|
|
|
|
:param max_leverage: 最大杠杆倍数(默认4倍,符合FINRA规定)
|
|
|
|
|
|
:param risk_per_trade: 单次交易风险比例(默认1%)
|
|
|
|
|
|
:param commission_per_share: 每股交易佣金(美元,默认0.0005)
|
2025-09-02 10:42:32 +00:00
|
|
|
|
:param profit_target_multiple: 盈利目标倍数(默认10倍$R,即10R)
|
|
|
|
|
|
:param is_us_stock: 是否是美股
|
2025-09-09 06:15:29 +00:00
|
|
|
|
:param is_binance: 是否是Binance
|
2025-09-02 10:42:32 +00:00
|
|
|
|
:param direction: 方向,None=自动,Long=多头,Short=空头
|
|
|
|
|
|
:param by_sar: 是否根据SAR指标生成信号,True=是,False=否
|
2025-09-09 06:15:29 +00:00
|
|
|
|
:param symbol_bar_data: 5分钟K线数据
|
|
|
|
|
|
:param symbol_1h_data: 1小时K线数据
|
2025-09-03 10:26:27 +00:00
|
|
|
|
:param price_range_mean_as_R: 是否将价格振幅均值作为$R,True=是,False=否
|
|
|
|
|
|
:param by_big_k: 是否根据K线实体部分,亦即abs(open-close)超过high-low的50%,True=是,False=否
|
2025-09-09 06:15:29 +00:00
|
|
|
|
:param by_1h_k: 是否根据1小时K线,True=是,False=否
|
2025-08-31 03:20:59 +00:00
|
|
|
|
"""
|
2025-09-01 10:01:21 +00:00
|
|
|
|
logger.info(
|
2025-09-02 10:42:32 +00:00
|
|
|
|
f"初始化ORB策略参数:股票代码={symbol},K线周期={bar},开始日期={start_date},结束日期={end_date},初始账户资金={initial_capital},最大杠杆倍数={max_leverage},单次交易风险比例={risk_per_trade},每股交易佣金={commission_per_share}"
|
2025-09-01 10:01:21 +00:00
|
|
|
|
)
|
2025-09-02 10:42:32 +00:00
|
|
|
|
self.symbol = symbol
|
|
|
|
|
|
self.bar = bar
|
|
|
|
|
|
self.start_date = start_date
|
|
|
|
|
|
self.end_date = end_date
|
2025-08-31 03:20:59 +00:00
|
|
|
|
self.initial_capital = initial_capital
|
|
|
|
|
|
self.max_leverage = max_leverage
|
|
|
|
|
|
self.risk_per_trade = risk_per_trade
|
|
|
|
|
|
self.commission_per_share = commission_per_share
|
2025-09-02 10:42:32 +00:00
|
|
|
|
self.profit_target_multiple = profit_target_multiple
|
2025-08-31 03:20:59 +00:00
|
|
|
|
self.data = None # 存储K线数据
|
|
|
|
|
|
self.trades = [] # 存储交易记录
|
|
|
|
|
|
self.equity_curve = None # 存储账户净值曲线
|
2025-09-25 04:28:43 +00:00
|
|
|
|
mysql_user = COIN_MYSQL_CONFIG.get("user", "xch")
|
|
|
|
|
|
mysql_password = COIN_MYSQL_CONFIG.get("password", "")
|
2025-08-31 03:20:59 +00:00
|
|
|
|
if not mysql_password:
|
|
|
|
|
|
raise ValueError("MySQL password is not set")
|
2025-09-25 04:28:43 +00:00
|
|
|
|
mysql_host = COIN_MYSQL_CONFIG.get("host", "localhost")
|
|
|
|
|
|
mysql_port = COIN_MYSQL_CONFIG.get("port", 3306)
|
|
|
|
|
|
mysql_database = COIN_MYSQL_CONFIG.get("database", "okx")
|
2025-09-09 06:15:29 +00:00
|
|
|
|
self.is_us_stock = is_us_stock
|
|
|
|
|
|
self.is_binance = is_binance
|
2025-08-31 03:20:59 +00:00
|
|
|
|
|
|
|
|
|
|
self.db_url = f"mysql+pymysql://{mysql_user}:{mysql_password}@{mysql_host}:{mysql_port}/{mysql_database}"
|
2025-09-09 06:15:29 +00:00
|
|
|
|
if self.is_binance:
|
|
|
|
|
|
self.db_market_data = DBBinanceData(self.db_url)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.db_market_data = DBMarketData(self.db_url)
|
|
|
|
|
|
|
2025-09-01 10:01:21 +00:00
|
|
|
|
self.output_chart_folder = r"./output/trade_sandbox/orb_strategy/chart/"
|
2025-09-02 10:42:32 +00:00
|
|
|
|
self.output_excel_folder = r"./output/trade_sandbox/orb_strategy/excel/"
|
2025-09-01 10:01:21 +00:00
|
|
|
|
os.makedirs(self.output_chart_folder, exist_ok=True)
|
2025-09-02 10:42:32 +00:00
|
|
|
|
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 = "做空"
|
2025-09-03 10:26:27 +00:00
|
|
|
|
|
2025-09-02 10:42:32 +00:00
|
|
|
|
self.sar_desc = "不考虑SAR"
|
|
|
|
|
|
if self.by_sar:
|
|
|
|
|
|
self.sar_desc = "考虑SAR"
|
|
|
|
|
|
self.symbol_bar_data = symbol_bar_data
|
2025-09-09 06:15:29 +00:00
|
|
|
|
self.symbol_1h_data = symbol_1h_data
|
2025-09-03 10:26:27 +00:00
|
|
|
|
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线要求"
|
2025-09-09 06:15:29 +00:00
|
|
|
|
self.by_1h_k = by_1h_k
|
|
|
|
|
|
if self.by_1h_k:
|
|
|
|
|
|
self.by_1h_k_desc = "参照1小时K线"
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.by_1h_k_desc = "不参照1小时K线"
|
2025-09-03 10:26:27 +00:00
|
|
|
|
|
2025-09-02 10:42:32 +00:00
|
|
|
|
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()
|
2025-09-09 06:15:29 +00:00
|
|
|
|
return (
|
|
|
|
|
|
self.symbol_bar_data,
|
|
|
|
|
|
self.symbol_1h_data,
|
|
|
|
|
|
self.trades_df,
|
|
|
|
|
|
self.trades_summary_df,
|
|
|
|
|
|
)
|
2025-09-02 10:42:32 +00:00
|
|
|
|
|
|
|
|
|
|
def fetch_intraday_data(self):
|
2025-08-31 03:20:59 +00:00
|
|
|
|
"""
|
|
|
|
|
|
获取日内5分钟K线数据(需yfinance支持,部分数据可能有延迟)
|
|
|
|
|
|
:param ticker: 股票代码(如QQQ、TQQQ)
|
|
|
|
|
|
:param start_date: 起始日期(格式:YYYY-MM-DD)
|
|
|
|
|
|
:param end_date: 结束日期(格式:YYYY-MM-DD)
|
|
|
|
|
|
:param interval: K线周期(默认5分钟)
|
|
|
|
|
|
"""
|
2025-09-03 10:26:27 +00:00
|
|
|
|
logger.info(
|
|
|
|
|
|
f"开始获取{self.symbol}数据:{self.start_date}至{self.end_date},间隔{self.bar}"
|
|
|
|
|
|
)
|
2025-09-02 10:42:32 +00:00
|
|
|
|
if self.symbol_bar_data is None or len(self.symbol_bar_data) == 0:
|
2025-09-09 06:15:29 +00:00
|
|
|
|
self.data = self.get_full_data(bar=self.bar)
|
2025-09-03 10:26:27 +00:00
|
|
|
|
self.calculate_price_range_mean()
|
2025-09-02 10:42:32 +00:00
|
|
|
|
self.symbol_bar_data = self.data.copy()
|
2025-09-09 06:15:29 +00:00
|
|
|
|
self.symbol_1h_data = self.get_full_data(bar="1H")
|
2025-09-02 10:42:32 +00:00
|
|
|
|
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
|
2025-09-01 10:01:21 +00:00
|
|
|
|
else:
|
2025-09-02 10:42:32 +00:00
|
|
|
|
pass
|
|
|
|
|
|
logger.info(f"收盘价均值:{self.close_mean}")
|
|
|
|
|
|
logger.info(f"初始资金调整为:{self.initial_capital}")
|
2025-09-03 10:26:27 +00:00
|
|
|
|
logger.info(
|
|
|
|
|
|
f"成功获取{self.symbol}数据:{len(self.data)}根{self.bar}K线,开始日期={self.start_date},结束日期={self.end_date}"
|
|
|
|
|
|
)
|
2025-08-31 03:20:59 +00:00
|
|
|
|
|
2025-09-09 06:15:29 +00:00
|
|
|
|
def get_full_data(self, bar: str = "5m"):
|
|
|
|
|
|
"""
|
|
|
|
|
|
分段获取数据,并将数据合并为完整数据
|
|
|
|
|
|
分段依据:如果end_date与start_date相差超过一年,则每次取一年数据
|
|
|
|
|
|
"""
|
|
|
|
|
|
data = pd.DataFrame()
|
|
|
|
|
|
start_date = datetime.strptime(self.start_date, "%Y-%m-%d")
|
|
|
|
|
|
end_date = datetime.strptime(self.end_date, "%Y-%m-%d") + timedelta(days=1)
|
|
|
|
|
|
fields = [
|
|
|
|
|
|
"symbol",
|
|
|
|
|
|
"bar",
|
|
|
|
|
|
"date_time",
|
|
|
|
|
|
"date_time_us",
|
|
|
|
|
|
"open",
|
|
|
|
|
|
"high",
|
|
|
|
|
|
"low",
|
|
|
|
|
|
"close",
|
|
|
|
|
|
"volume",
|
|
|
|
|
|
"sar_signal",
|
|
|
|
|
|
"ma5",
|
|
|
|
|
|
"ma10",
|
|
|
|
|
|
"ma20",
|
|
|
|
|
|
"ma30",
|
|
|
|
|
|
"dif",
|
|
|
|
|
|
"macd",
|
|
|
|
|
|
]
|
|
|
|
|
|
while start_date < end_date:
|
|
|
|
|
|
current_end_date = min(start_date + timedelta(days=180), end_date)
|
|
|
|
|
|
start_date_str = start_date.strftime("%Y-%m-%d")
|
|
|
|
|
|
current_end_date_str = current_end_date.strftime("%Y-%m-%d")
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
f"获取{self.symbol}数据:{start_date_str}至{current_end_date_str}"
|
|
|
|
|
|
)
|
|
|
|
|
|
current_data = self.db_market_data.query_market_data_by_symbol_bar(
|
|
|
|
|
|
self.symbol, bar, fields, start=start_date_str, end=current_end_date_str
|
|
|
|
|
|
)
|
|
|
|
|
|
if current_data is not None and len(current_data) > 0:
|
|
|
|
|
|
current_data = pd.DataFrame(current_data)
|
|
|
|
|
|
data = pd.concat([data, current_data])
|
|
|
|
|
|
start_date = current_end_date
|
|
|
|
|
|
data.drop_duplicates(inplace=True)
|
|
|
|
|
|
if self.is_us_stock:
|
|
|
|
|
|
date_time_field = "date_time_us"
|
|
|
|
|
|
else:
|
|
|
|
|
|
date_time_field = "date_time"
|
|
|
|
|
|
data.sort_values(by=date_time_field, inplace=True)
|
|
|
|
|
|
data.reset_index(drop=True, inplace=True)
|
|
|
|
|
|
# 保留核心列:开盘价、最高价、最低价、收盘价、成交量
|
|
|
|
|
|
data["Open"] = data["open"]
|
|
|
|
|
|
data["High"] = data["high"]
|
|
|
|
|
|
data["Low"] = data["low"]
|
|
|
|
|
|
data["Close"] = data["close"]
|
|
|
|
|
|
data["Volume"] = data["volume"]
|
|
|
|
|
|
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")
|
|
|
|
|
|
data = data[
|
|
|
|
|
|
[
|
|
|
|
|
|
"symbol",
|
|
|
|
|
|
"bar",
|
|
|
|
|
|
"Date",
|
|
|
|
|
|
date_time_field,
|
|
|
|
|
|
"Open",
|
|
|
|
|
|
"High",
|
|
|
|
|
|
"Low",
|
|
|
|
|
|
"Close",
|
|
|
|
|
|
"Volume",
|
|
|
|
|
|
"sar_signal",
|
|
|
|
|
|
"ma5",
|
|
|
|
|
|
"ma10",
|
|
|
|
|
|
"ma20",
|
|
|
|
|
|
"ma30",
|
|
|
|
|
|
"dif",
|
|
|
|
|
|
"macd",
|
|
|
|
|
|
]
|
|
|
|
|
|
]
|
|
|
|
|
|
data.rename(columns={date_time_field: "date_time"}, inplace=True)
|
|
|
|
|
|
return data
|
|
|
|
|
|
|
2025-09-03 10:26:27 +00:00
|
|
|
|
def calculate_shares(self, account_value, entry_price, stop_price, risk_assumed):
|
2025-08-31 03:20:59 +00:00
|
|
|
|
"""
|
|
|
|
|
|
根据ORB公式计算交易股数
|
|
|
|
|
|
:param account_value: 当前账户价值(美元)
|
|
|
|
|
|
:param entry_price: 交易entry价格(第二根5分钟K线开盘价)
|
|
|
|
|
|
:param stop_price: 止损价格(多头=第一根K线最低价,空头=第一根K线最高价)
|
2025-09-03 10:26:27 +00:00
|
|
|
|
:param risk_assumed: 风险金额($R),根据price_range_mean_as_R决定
|
2025-08-31 03:20:59 +00:00
|
|
|
|
:return: 整数股数(Shares)
|
|
|
|
|
|
"""
|
2025-09-01 10:01:21 +00:00
|
|
|
|
logger.info(
|
|
|
|
|
|
f"开始计算交易股数:账户价值={account_value},entry价格={entry_price},止损价格={stop_price}"
|
|
|
|
|
|
)
|
2025-08-31 03:20:59 +00:00
|
|
|
|
# 计算单交易风险金额($R)
|
2025-09-03 10:26:27 +00:00
|
|
|
|
risk_per_trade_dollar = risk_assumed # 风险金额取绝对值
|
2025-08-31 03:20:59 +00:00
|
|
|
|
if risk_per_trade_dollar <= 0:
|
|
|
|
|
|
return 0 # 无风险时不交易
|
|
|
|
|
|
|
|
|
|
|
|
# 公式1:基于风险预算的最大股数(风险控制优先)
|
|
|
|
|
|
shares_risk = (account_value * self.risk_per_trade) / risk_per_trade_dollar
|
|
|
|
|
|
# 公式2:基于杠杆限制的最大股数(杠杆约束)
|
|
|
|
|
|
shares_leverage = (self.max_leverage * account_value) / entry_price
|
|
|
|
|
|
# 取两者最小值(满足风险和杠杆双重约束)
|
|
|
|
|
|
max_shares = min(shares_risk, shares_leverage)
|
|
|
|
|
|
# 扣除佣金影响(简化计算:假设佣金从可用资金中扣除)
|
|
|
|
|
|
commission_cost = max_shares * self.commission_per_share
|
|
|
|
|
|
if (account_value - commission_cost) < 0:
|
|
|
|
|
|
return 0 # 扣除佣金后资金不足,不交易
|
|
|
|
|
|
|
|
|
|
|
|
return int(max_shares) # 股数取整
|
|
|
|
|
|
|
2025-09-02 10:42:32 +00:00
|
|
|
|
def generate_orb_signals(self):
|
2025-08-31 03:20:59 +00:00
|
|
|
|
"""
|
|
|
|
|
|
生成ORB策略信号(每日仅1次交易机会)
|
|
|
|
|
|
- 第一根5分钟K线:确定开盘区间(High1, Low1)
|
|
|
|
|
|
- 第二根5分钟K线:根据第一根K线方向生成多空信号
|
|
|
|
|
|
"""
|
2025-09-02 10:42:32 +00:00
|
|
|
|
logger.info(
|
2025-09-09 06:15:29 +00:00
|
|
|
|
f"开始生成ORB策略信号:{self.direction_desc},根据SAR指标:{self.by_sar},{self.by_1h_k_desc}"
|
2025-09-02 10:42:32 +00:00
|
|
|
|
)
|
2025-08-31 03:20:59 +00:00
|
|
|
|
if self.data is None:
|
|
|
|
|
|
raise ValueError("请先调用fetch_intraday_data获取数据")
|
|
|
|
|
|
|
|
|
|
|
|
signals = []
|
|
|
|
|
|
# 按日期分组处理每日数据
|
|
|
|
|
|
for date, daily_data in self.data.groupby("Date"):
|
|
|
|
|
|
daily_data = daily_data.sort_index() # 按时间排序
|
|
|
|
|
|
if len(daily_data) < 2:
|
|
|
|
|
|
continue # 当日K线不足2根,跳过
|
|
|
|
|
|
|
|
|
|
|
|
# 第一根5分钟K线(开盘区间)
|
|
|
|
|
|
first_candle = daily_data.iloc[0]
|
2025-09-09 06:15:29 +00:00
|
|
|
|
current_date = first_candle["Date"]
|
2025-08-31 03:20:59 +00:00
|
|
|
|
high1 = first_candle["High"]
|
|
|
|
|
|
low1 = first_candle["Low"]
|
|
|
|
|
|
open1 = first_candle["Open"]
|
|
|
|
|
|
close1 = first_candle["Close"]
|
2025-09-02 10:42:32 +00:00
|
|
|
|
sar_signal = first_candle["sar_signal"]
|
2025-08-31 03:20:59 +00:00
|
|
|
|
|
2025-09-03 10:26:27 +00:00
|
|
|
|
if high1 == low1:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if self.by_big_k:
|
|
|
|
|
|
if (abs(open1 - close1) / (high1 - low1)) < 0.5:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2025-09-09 06:15:29 +00:00
|
|
|
|
ma5_1h = None
|
|
|
|
|
|
ma10_1h = None
|
|
|
|
|
|
dif_1h = None
|
|
|
|
|
|
macd_1h = None
|
|
|
|
|
|
if self.by_1h_k:
|
|
|
|
|
|
if self.symbol_1h_data is None or len(self.symbol_1h_data) == 0:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if len(self.symbol_1h_data) < 2:
|
|
|
|
|
|
continue
|
|
|
|
|
|
symbol_1h_date_data = self.symbol_1h_data[
|
|
|
|
|
|
self.symbol_1h_data["Date"] == current_date
|
|
|
|
|
|
]
|
|
|
|
|
|
if len(symbol_1h_date_data) > 0:
|
|
|
|
|
|
first_candle_1h = symbol_1h_date_data.iloc[0]
|
|
|
|
|
|
ma5_1h = first_candle_1h["ma5"]
|
|
|
|
|
|
ma10_1h = first_candle_1h["ma10"]
|
|
|
|
|
|
dif_1h = first_candle_1h["dif"]
|
|
|
|
|
|
macd_1h = first_candle_1h["macd"]
|
|
|
|
|
|
|
2025-08-31 03:20:59 +00:00
|
|
|
|
# 第二根5分钟K线(entry信号)
|
|
|
|
|
|
second_candle = daily_data.iloc[1]
|
|
|
|
|
|
entry_price = second_candle["Open"] # entry价格=第二根K线开盘价
|
|
|
|
|
|
entry_time = second_candle.date_time # entry时间
|
|
|
|
|
|
|
|
|
|
|
|
# 生成信号:第一根K线方向决定多空(排除十字星:open1 == close1)
|
2025-09-02 10:42:32 +00:00
|
|
|
|
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)
|
2025-09-09 06:15:29 +00:00
|
|
|
|
and (
|
|
|
|
|
|
(
|
|
|
|
|
|
self.by_1h_k
|
|
|
|
|
|
and (
|
|
|
|
|
|
ma5_1h is not None
|
|
|
|
|
|
and ma10_1h is not None
|
|
|
|
|
|
and ma5_1h > ma10_1h
|
|
|
|
|
|
)
|
|
|
|
|
|
and (
|
|
|
|
|
|
dif_1h is not None
|
|
|
|
|
|
and macd_1h is not None
|
|
|
|
|
|
and (dif_1h > 0 or macd_1h > 0)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
or not self.by_1h_k
|
|
|
|
|
|
)
|
2025-09-02 10:42:32 +00:00
|
|
|
|
):
|
2025-08-31 03:20:59 +00:00
|
|
|
|
# 第一根K线收涨→多头信号
|
|
|
|
|
|
signal = "Long"
|
|
|
|
|
|
stop_price = low1 # 多头止损=第一根K线最低价
|
2025-09-02 10:42:32 +00:00
|
|
|
|
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)
|
2025-09-09 06:15:29 +00:00
|
|
|
|
and (
|
|
|
|
|
|
(
|
|
|
|
|
|
self.by_1h_k
|
|
|
|
|
|
and (
|
|
|
|
|
|
ma5_1h is not None
|
|
|
|
|
|
and ma10_1h is not None
|
|
|
|
|
|
and ma5_1h < ma10_1h
|
|
|
|
|
|
)
|
|
|
|
|
|
and (
|
|
|
|
|
|
dif_1h is not None
|
|
|
|
|
|
and macd_1h is not None
|
|
|
|
|
|
and (dif_1h < 0 or macd_1h < 0)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
or not self.by_1h_k
|
|
|
|
|
|
)
|
2025-09-02 10:42:32 +00:00
|
|
|
|
):
|
2025-08-31 03:20:59 +00:00
|
|
|
|
# 第一根K线收跌→空头信号
|
|
|
|
|
|
signal = "Short"
|
|
|
|
|
|
stop_price = high1 # 空头止损=第一根K线最高价
|
|
|
|
|
|
else:
|
2025-09-02 04:44:34 +00:00
|
|
|
|
# 与direction不一致或十字星→无信号
|
2025-09-01 10:01:21 +00:00
|
|
|
|
signal = None
|
2025-08-31 03:20:59 +00:00
|
|
|
|
stop_price = None
|
|
|
|
|
|
|
|
|
|
|
|
signals.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"Date": date,
|
|
|
|
|
|
"EntryTime": entry_time,
|
|
|
|
|
|
"Signal": signal,
|
|
|
|
|
|
"EntryPrice": entry_price,
|
|
|
|
|
|
"StopPrice": stop_price,
|
|
|
|
|
|
"High1": high1,
|
|
|
|
|
|
"Low1": low1,
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 将信号合并到原始数据
|
|
|
|
|
|
signals_df = pd.DataFrame(signals)
|
|
|
|
|
|
# 确保Date列类型一致,将Date转换为datetime64[ns]类型
|
2025-09-01 10:01:21 +00:00
|
|
|
|
signals_df["Date"] = pd.to_datetime(signals_df["Date"])
|
2025-08-31 03:20:59 +00:00
|
|
|
|
# 使用merge而不是join来合并数据,根据signals_df的EntryTime与self.data的date_time进行匹配
|
|
|
|
|
|
# TODO: 这里需要优化
|
2025-09-01 10:01:21 +00:00
|
|
|
|
self.data = self.data.merge(
|
|
|
|
|
|
signals_df, left_on="date_time", right_on="EntryTime", how="left"
|
|
|
|
|
|
)
|
2025-08-31 03:20:59 +00:00
|
|
|
|
# 将Date_x和Date_y合并为Date
|
|
|
|
|
|
self.data["Date"] = self.data["Date_x"].combine_first(self.data["Date_y"])
|
|
|
|
|
|
# 删除Date_x和Date_y
|
|
|
|
|
|
self.data.drop(columns=["Date_x", "Date_y"], inplace=True)
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
f"生成信号完成:共{len(signals_df)}个交易日,其中多头{sum(signals_df['Signal']=='Long')}次,空头{sum(signals_df['Signal']=='Short')}次"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-09-03 10:26:27 +00:00
|
|
|
|
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()
|
|
|
|
|
|
|
2025-09-02 10:42:32 +00:00
|
|
|
|
def backtest(self):
|
2025-08-31 03:20:59 +00:00
|
|
|
|
"""
|
|
|
|
|
|
回测ORB策略
|
|
|
|
|
|
:param profit_target_multiple: 盈利目标倍数(默认10倍$R,即10R)
|
|
|
|
|
|
"""
|
2025-09-02 10:42:32 +00:00
|
|
|
|
logger.info(f"开始回测ORB策略:盈利目标倍数={self.profit_target_multiple}")
|
2025-08-31 03:20:59 +00:00
|
|
|
|
if "Signal" not in self.data.columns:
|
|
|
|
|
|
raise ValueError("请先调用generate_orb_signals生成策略信号")
|
|
|
|
|
|
|
|
|
|
|
|
account_value = self.initial_capital # 初始账户价值
|
|
|
|
|
|
current_position = None # 当前持仓(None=空仓,Long/Short=持仓)
|
|
|
|
|
|
equity_history = [account_value] # 净值历史
|
|
|
|
|
|
trade_id = 0 # 交易ID
|
|
|
|
|
|
|
|
|
|
|
|
# 按时间遍历数据(每日仅处理第二根K线后的信号)
|
|
|
|
|
|
for date, daily_data in self.data.groupby("Date"):
|
|
|
|
|
|
daily_data = daily_data.sort_index()
|
|
|
|
|
|
if len(daily_data) < 2:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 获取当日信号(第二根K线的信号)
|
|
|
|
|
|
signal_row = (
|
|
|
|
|
|
daily_data[~pd.isna(daily_data["Signal"])].iloc[0]
|
|
|
|
|
|
if sum(~pd.isna(daily_data["Signal"])) > 0
|
|
|
|
|
|
else None
|
|
|
|
|
|
)
|
|
|
|
|
|
if signal_row is None:
|
|
|
|
|
|
# 无信号→当日不交易,净值保持不变
|
|
|
|
|
|
equity_history.append(account_value)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 提取信号参数
|
|
|
|
|
|
signal = signal_row["Signal"]
|
|
|
|
|
|
if pd.isna(signal):
|
|
|
|
|
|
continue
|
2025-09-01 10:01:21 +00:00
|
|
|
|
|
2025-08-31 03:20:59 +00:00
|
|
|
|
entry_price = signal_row["EntryPrice"]
|
|
|
|
|
|
stop_price = signal_row["StopPrice"]
|
|
|
|
|
|
high1 = signal_row["High1"]
|
|
|
|
|
|
low1 = signal_row["Low1"]
|
2025-09-03 10:26:27 +00:00
|
|
|
|
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)
|
2025-08-31 03:20:59 +00:00
|
|
|
|
profit_target = (
|
2025-09-02 10:42:32 +00:00
|
|
|
|
entry_price + (risk_assumed * self.profit_target_multiple)
|
2025-08-31 03:20:59 +00:00
|
|
|
|
if signal == "Long"
|
2025-09-02 10:42:32 +00:00
|
|
|
|
else entry_price - (risk_assumed * self.profit_target_multiple)
|
2025-08-31 03:20:59 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 计算交易股数
|
2025-09-03 10:26:27 +00:00
|
|
|
|
shares = self.calculate_shares(
|
|
|
|
|
|
account_value, entry_price, stop_price, risk_assumed
|
|
|
|
|
|
)
|
2025-08-31 03:20:59 +00:00
|
|
|
|
if shares == 0:
|
|
|
|
|
|
# 股数为0→不交易
|
|
|
|
|
|
equity_history.append(account_value)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 计算佣金(买入/卖出各收一次)
|
|
|
|
|
|
total_commission = shares * self.commission_per_share * 2 # 往返佣金
|
|
|
|
|
|
|
|
|
|
|
|
# 模拟日内持仓:寻找止损/止盈触发点,或当日收盘平仓
|
|
|
|
|
|
daily_prices = daily_data[
|
|
|
|
|
|
daily_data.date_time > signal_row.date_time
|
|
|
|
|
|
] # 从entry时间开始遍历
|
|
|
|
|
|
exit_price = None
|
|
|
|
|
|
exit_time = None
|
|
|
|
|
|
exit_reason = None
|
|
|
|
|
|
|
|
|
|
|
|
for idx, (time, row) in enumerate(daily_prices.iterrows()):
|
|
|
|
|
|
high = row["High"]
|
|
|
|
|
|
low = row["Low"]
|
|
|
|
|
|
close = row["Close"]
|
|
|
|
|
|
|
|
|
|
|
|
# 检查止损/止盈条件
|
|
|
|
|
|
if signal == "Long":
|
|
|
|
|
|
# 多头:跌破止损→止损;突破止盈→止盈
|
|
|
|
|
|
if low <= stop_price:
|
|
|
|
|
|
exit_price = stop_price
|
|
|
|
|
|
exit_reason = "Stop Loss"
|
2025-09-01 10:01:21 +00:00
|
|
|
|
exit_time = row["date_time"]
|
2025-08-31 03:20:59 +00:00
|
|
|
|
break
|
|
|
|
|
|
elif high >= profit_target:
|
|
|
|
|
|
exit_price = profit_target
|
2025-09-02 10:42:32 +00:00
|
|
|
|
exit_reason = f"Profit Target ({self.profit_target_multiple}R)"
|
2025-09-01 10:01:21 +00:00
|
|
|
|
exit_time = row["date_time"]
|
2025-08-31 03:20:59 +00:00
|
|
|
|
break
|
|
|
|
|
|
elif signal == "Short":
|
|
|
|
|
|
# 空头:突破止损→止损;跌破止盈→止盈
|
|
|
|
|
|
if high >= stop_price:
|
|
|
|
|
|
exit_price = stop_price
|
|
|
|
|
|
exit_reason = "Stop Loss"
|
2025-09-01 10:01:21 +00:00
|
|
|
|
exit_time = row["date_time"]
|
2025-08-31 03:20:59 +00:00
|
|
|
|
break
|
|
|
|
|
|
elif low <= profit_target:
|
|
|
|
|
|
exit_price = profit_target
|
2025-09-02 10:42:32 +00:00
|
|
|
|
exit_reason = f"Profit Target ({self.profit_target_multiple}R)"
|
2025-09-01 10:01:21 +00:00
|
|
|
|
exit_time = row["date_time"]
|
2025-08-31 03:20:59 +00:00
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
# 若未触发止损/止盈,当日收盘平仓
|
|
|
|
|
|
if exit_price is None:
|
|
|
|
|
|
exit_price = daily_prices.iloc[-1]["Close"]
|
|
|
|
|
|
exit_reason = "End of Day (EoD)"
|
|
|
|
|
|
exit_time = daily_prices.iloc[-1].date_time
|
|
|
|
|
|
|
2025-09-01 10:01:21 +00:00
|
|
|
|
initial_account_value = account_value
|
2025-08-31 03:20:59 +00:00
|
|
|
|
# 计算盈亏
|
|
|
|
|
|
if signal == "Long":
|
|
|
|
|
|
profit_loss = (exit_price - entry_price) * shares - total_commission
|
|
|
|
|
|
else: # Short
|
|
|
|
|
|
profit_loss = (entry_price - exit_price) * shares - total_commission
|
|
|
|
|
|
|
2025-09-01 10:01:21 +00:00
|
|
|
|
# 计算盈亏百分比,profit_loss除以当期初始资金
|
|
|
|
|
|
profit_loss_percentage = (profit_loss / initial_account_value) * 100
|
|
|
|
|
|
|
2025-08-31 03:20:59 +00:00
|
|
|
|
# 更新账户价值
|
|
|
|
|
|
account_value += profit_loss
|
|
|
|
|
|
account_value = max(account_value, 0) # 账户价值不能为负
|
|
|
|
|
|
|
|
|
|
|
|
# 记录交易
|
|
|
|
|
|
self.trades.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"TradeID": trade_id,
|
2025-09-02 10:42:32 +00:00
|
|
|
|
"Direction": self.direction_desc,
|
|
|
|
|
|
"BySar": self.sar_desc,
|
2025-09-03 10:26:27 +00:00
|
|
|
|
"PriceRangeMeanAsR": self.price_range_mean_as_R_desc,
|
|
|
|
|
|
"ByBigK": self.by_big_k_desc,
|
2025-09-09 06:15:29 +00:00
|
|
|
|
"By1hK": self.by_1h_k_desc,
|
2025-09-02 10:42:32 +00:00
|
|
|
|
"Symbol": self.symbol,
|
|
|
|
|
|
"Bar": self.bar,
|
2025-08-31 03:20:59 +00:00
|
|
|
|
"Date": date,
|
|
|
|
|
|
"Signal": signal,
|
2025-09-01 10:01:21 +00:00
|
|
|
|
"EntryTime": signal_row.date_time.strftime("%Y-%m-%d %H:%M:%S"),
|
2025-08-31 03:20:59 +00:00
|
|
|
|
"EntryPrice": entry_price,
|
2025-09-01 10:01:21 +00:00
|
|
|
|
"ExitTime": exit_time.strftime("%Y-%m-%d %H:%M:%S"),
|
2025-08-31 03:20:59 +00:00
|
|
|
|
"ExitPrice": exit_price,
|
|
|
|
|
|
"Shares": shares,
|
|
|
|
|
|
"RiskAssumed": risk_assumed,
|
|
|
|
|
|
"ProfitLoss": profit_loss,
|
2025-09-01 10:01:21 +00:00
|
|
|
|
"ProfitLossPercentage": profit_loss_percentage,
|
2025-08-31 03:20:59 +00:00
|
|
|
|
"ExitReason": exit_reason,
|
2025-09-01 10:01:21 +00:00
|
|
|
|
"AccountValueInitial": initial_account_value,
|
2025-08-31 03:20:59 +00:00
|
|
|
|
"AccountValueAfter": account_value,
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 记录净值
|
|
|
|
|
|
equity_history.append(account_value)
|
|
|
|
|
|
trade_id += 1
|
|
|
|
|
|
|
2025-09-02 10:42:32 +00:00
|
|
|
|
if len(self.trades) == 0:
|
|
|
|
|
|
logger.info("没有交易")
|
|
|
|
|
|
self.trades_df = pd.DataFrame()
|
|
|
|
|
|
self.initial_trade_summary()
|
|
|
|
|
|
return
|
2025-08-31 03:20:59 +00:00
|
|
|
|
# 生成净值曲线
|
2025-09-01 10:01:21 +00:00
|
|
|
|
self.create_equity_curve()
|
2025-08-31 03:20:59 +00:00
|
|
|
|
|
|
|
|
|
|
# 输出回测结果
|
2025-09-02 10:42:32 +00:00
|
|
|
|
self.trades_df = pd.DataFrame(self.trades)
|
|
|
|
|
|
self.trades_df.sort_values(by="ExitTime", inplace=True)
|
2025-08-31 03:20:59 +00:00
|
|
|
|
total_return = (
|
|
|
|
|
|
(account_value - self.initial_capital) / self.initial_capital * 100
|
|
|
|
|
|
)
|
|
|
|
|
|
win_rate = (
|
2025-09-02 10:42:32 +00:00
|
|
|
|
(self.trades_df["ProfitLoss"] > 0).sum() / len(self.trades_df) * 100
|
|
|
|
|
|
if len(self.trades_df) > 0
|
2025-08-31 03:20:59 +00:00
|
|
|
|
else 0
|
|
|
|
|
|
)
|
2025-09-01 10:01:21 +00:00
|
|
|
|
# 计算盈亏比
|
2025-09-03 10:26:27 +00:00
|
|
|
|
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()
|
|
|
|
|
|
)
|
2025-09-01 10:01:21 +00:00
|
|
|
|
if loss_sum == 0:
|
2025-09-02 10:42:32 +00:00
|
|
|
|
profit_loss_ratio = float("inf")
|
2025-09-01 10:01:21 +00:00
|
|
|
|
else:
|
|
|
|
|
|
profit_loss_ratio = (profit_sum / loss_sum) * 100
|
2025-09-02 10:42:32 +00:00
|
|
|
|
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}")
|
2025-09-03 10:26:27 +00:00
|
|
|
|
self.trades_summary["最大单笔亏损$"] = abs(
|
|
|
|
|
|
self.trades_df["ProfitLoss"].min()
|
|
|
|
|
|
)
|
2025-09-02 10:42:32 +00:00
|
|
|
|
else:
|
|
|
|
|
|
logger.info("没有交易")
|
|
|
|
|
|
self.trades_summary_df = pd.DataFrame([self.trades_summary])
|
2025-09-03 10:26:27 +00:00
|
|
|
|
|
2025-09-02 10:42:32 +00:00
|
|
|
|
def initial_trade_summary(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
初始化交易总结
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.trades_summary = {}
|
|
|
|
|
|
self.trades_summary["方向"] = self.direction_desc
|
|
|
|
|
|
self.trades_summary["根据SAR"] = self.sar_desc
|
2025-09-03 10:26:27 +00:00
|
|
|
|
self.trades_summary["R算法"] = self.price_range_mean_as_R_desc
|
|
|
|
|
|
self.trades_summary["K线条件"] = self.by_big_k_desc
|
2025-09-09 06:15:29 +00:00
|
|
|
|
self.trades_summary["1小时K线条件"] = self.by_1h_k_desc
|
2025-09-02 10:42:32 +00:00
|
|
|
|
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
|
2025-08-31 03:20:59 +00:00
|
|
|
|
|
2025-09-01 10:01:21 +00:00
|
|
|
|
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)
|
|
|
|
|
|
|
2025-08-31 03:20:59 +00:00
|
|
|
|
def plot_equity_curve(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
绘制账户净值曲线
|
|
|
|
|
|
"""
|
|
|
|
|
|
logger.info("开始绘制账户净值曲线")
|
|
|
|
|
|
if self.equity_curve is None:
|
|
|
|
|
|
raise ValueError("请先调用backtest进行回测")
|
2025-09-01 10:01:21 +00:00
|
|
|
|
|
2025-08-31 03:20:59 +00:00
|
|
|
|
# seaborn风格设置
|
|
|
|
|
|
sns.set_theme(style="whitegrid")
|
|
|
|
|
|
# plt.rcParams['font.family'] = "SimHei"
|
|
|
|
|
|
plt.rcParams["font.sans-serif"] = ["SimHei"] # 也可直接用字体名
|
|
|
|
|
|
plt.rcParams["font.size"] = 11 # 设置字体大小
|
|
|
|
|
|
plt.rcParams["axes.unicode_minus"] = False # 解决负号显示问题
|
|
|
|
|
|
|
2025-09-01 10:01:21 +00:00
|
|
|
|
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
|
2025-08-31 03:20:59 +00:00
|
|
|
|
plt.figure(figsize=(12, 6))
|
2025-09-02 10:42:32 +00:00
|
|
|
|
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,
|
|
|
|
|
|
)
|
2025-09-03 10:26:27 +00:00
|
|
|
|
plt.title(
|
2025-09-09 06:15:29 +00:00
|
|
|
|
f"{symbol} {bar} {self.direction_desc} {self.sar_desc} {self.price_range_mean_as_R_desc} {self.by_big_k_desc} {self.by_1h_k_desc}",
|
2025-09-03 10:26:27 +00:00
|
|
|
|
fontsize=14,
|
|
|
|
|
|
fontweight="bold",
|
|
|
|
|
|
)
|
2025-09-01 10:01:21 +00:00
|
|
|
|
plt.xlabel("时间", fontsize=12)
|
|
|
|
|
|
plt.ylabel("涨跌变化", fontsize=12)
|
|
|
|
|
|
plt.legend(fontsize=11)
|
2025-08-31 03:20:59 +00:00
|
|
|
|
plt.grid(True, alpha=0.3)
|
2025-09-01 10:01:21 +00:00
|
|
|
|
|
|
|
|
|
|
# 设置x轴标签,避免matplotlib警告
|
|
|
|
|
|
# 选择合适的时间间隔显示标签,避免过于密集
|
|
|
|
|
|
if len(self.equity_curve) > 30:
|
|
|
|
|
|
# 如果数据点较多,选择间隔显示,但确保第一条和最后一条始终显示
|
|
|
|
|
|
step = max(1, len(self.equity_curve) // 30)
|
2025-09-02 10:42:32 +00:00
|
|
|
|
|
2025-09-01 10:01:21 +00:00
|
|
|
|
# 创建标签索引列表,确保包含首尾数据
|
|
|
|
|
|
label_indices = [0] # 第一条
|
2025-09-02 10:42:32 +00:00
|
|
|
|
|
2025-09-01 10:01:21 +00:00
|
|
|
|
# 添加中间间隔的标签
|
|
|
|
|
|
for i in range(step, len(self.equity_curve) - 1, step):
|
|
|
|
|
|
label_indices.append(i)
|
2025-09-02 10:42:32 +00:00
|
|
|
|
|
2025-09-01 10:01:21 +00:00
|
|
|
|
# 添加最后一条(如果还没有包含的话)
|
|
|
|
|
|
if len(self.equity_curve) - 1 not in label_indices:
|
|
|
|
|
|
label_indices.append(len(self.equity_curve) - 1)
|
2025-09-02 10:42:32 +00:00
|
|
|
|
|
2025-09-01 10:01:21 +00:00
|
|
|
|
# 设置x轴标签
|
2025-09-02 10:42:32 +00:00
|
|
|
|
plt.xticks(
|
|
|
|
|
|
self.equity_curve["DateTime"].iloc[label_indices],
|
|
|
|
|
|
self.equity_curve["DateTime"].iloc[label_indices],
|
|
|
|
|
|
rotation=45,
|
|
|
|
|
|
ha="right",
|
|
|
|
|
|
fontsize=10,
|
|
|
|
|
|
)
|
2025-09-01 10:01:21 +00:00
|
|
|
|
else:
|
|
|
|
|
|
# 如果数据点较少,全部显示
|
2025-09-02 10:42:32 +00:00
|
|
|
|
plt.xticks(
|
|
|
|
|
|
self.equity_curve["DateTime"],
|
|
|
|
|
|
self.equity_curve["DateTime"],
|
|
|
|
|
|
rotation=45,
|
|
|
|
|
|
ha="right",
|
|
|
|
|
|
fontsize=10,
|
|
|
|
|
|
)
|
2025-09-01 10:01:21 +00:00
|
|
|
|
plt.tight_layout()
|
2025-09-09 06:15:29 +00:00
|
|
|
|
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}_{self.by_1h_k_desc}_orb.png"
|
2025-09-02 10:42:32 +00:00
|
|
|
|
plt.savefig(self.chart_save_path, dpi=150, bbox_inches="tight")
|
2025-09-01 10:01:21 +00:00
|
|
|
|
plt.close()
|
2025-09-03 10:26:27 +00:00
|
|
|
|
|
2025-09-02 10:42:32 +00:00
|
|
|
|
def output_trade_summary(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
输出交易明细,交易总结与Chart图片到Excel
|
|
|
|
|
|
"""
|
|
|
|
|
|
start_date = self.start_date.replace("-", "")
|
|
|
|
|
|
end_date = self.end_date.replace("-", "")
|
2025-09-09 06:15:29 +00:00
|
|
|
|
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}_{self.by_1h_k_desc}.xlsx"
|
2025-09-02 10:42:32 +00:00
|
|
|
|
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):
|
2025-09-03 10:26:27 +00:00
|
|
|
|
charts_dict = {"账户净值曲线": self.chart_save_path}
|
2025-09-02 10:42:32 +00:00
|
|
|
|
self.output_chart_to_excel(output_file_path, charts_dict)
|
2025-09-03 10:26:27 +00:00
|
|
|
|
|
2025-09-02 10:42:32 +00:00
|
|
|
|
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)
|
2025-08-31 03:20:59 +00:00
|
|
|
|
|
2025-09-02 10:42:32 +00:00
|
|
|
|
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}")
|
2025-09-03 10:26:27 +00:00
|
|
|
|
|
2025-08-31 03:20:59 +00:00
|
|
|
|
|
|
|
|
|
|
# ------------------- 策略示例:回测QQQ的ORB策略(2016-2023) -------------------
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
|
|
|
# 初始化ORB策略
|
|
|
|
|
|
orb_strategy = ORBStrategy(
|
2025-09-02 10:42:32 +00:00
|
|
|
|
symbol="ETH-USDT",
|
|
|
|
|
|
bar="5m",
|
|
|
|
|
|
start_date="2025-05-15",
|
|
|
|
|
|
end_date="2025-08-20",
|
2025-08-31 03:20:59 +00:00
|
|
|
|
initial_capital=25000,
|
|
|
|
|
|
max_leverage=4,
|
|
|
|
|
|
risk_per_trade=0.01,
|
|
|
|
|
|
commission_per_share=0.0005,
|
2025-09-02 10:42:32 +00:00
|
|
|
|
profit_target_multiple=10,
|
|
|
|
|
|
is_us_stock=False,
|
|
|
|
|
|
direction=None,
|
|
|
|
|
|
by_sar=False,
|
2025-08-31 03:20:59 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-09-02 10:42:32 +00:00
|
|
|
|
orb_strategy.run()
|