tqsdk实盘
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from tqsdk import TqApi, TqAuth, TqBacktest, TargetPosTask
|
import pandas as pd
|
||||||
|
from tqsdk import TqApi, TqAuth, TqBacktest, TargetPosTask, TqKq, TqSim
|
||||||
|
|
||||||
'''
|
'''
|
||||||
如果当前价格大于5分钟K线的MA15则开多仓
|
如果当前价格大于5分钟K线的MA15则开多仓
|
||||||
@@ -7,13 +8,26 @@ from tqsdk import TqApi, TqAuth, TqBacktest, TargetPosTask
|
|||||||
回测从 2018-05-01 到 2018-10-01
|
回测从 2018-05-01 到 2018-10-01
|
||||||
'''
|
'''
|
||||||
# 在创建 api 实例时传入 TqBacktest 就会进入回测模式
|
# 在创建 api 实例时传入 TqBacktest 就会进入回测模式
|
||||||
api = TqApi(backtest=TqBacktest(start_dt=date(2018, 5, 1), end_dt=date(2018, 10, 1)), auth=TqAuth("emanresu", "dfgvfgdfgg"))
|
api = TqApi(auth=TqAuth("emanresu", "dfgvfgdfgg"))
|
||||||
# 获得 m1901 5分钟K线的引用
|
# 获得 m1901 5分钟K线的引用
|
||||||
klines = api.get_kline_serial("DCE.m1901", 60 * 60, data_length=15)
|
klines = api.get_kline_serial("KQ.m@CZCE.MA", 60 * 60)
|
||||||
# 创建 m1901 的目标持仓 task,该 task 负责调整 m1901 的仓位到指定的目标仓位
|
# 创建 m1901 的目标持仓 task,该 task 负责调整 m1901 的仓位到指定的目标仓位
|
||||||
target_pos = TargetPosTask(api, "DCE.m1901")
|
target_pos = TargetPosTask(api, "KQ.m@CZCE.MA")
|
||||||
|
|
||||||
|
BEIJING_TZ = "Asia/Shanghai"
|
||||||
|
print('-----------------')
|
||||||
|
print(pd.to_datetime(klines.iloc[-1].datetime, unit="ns", utc=True).tz_convert(BEIJING_TZ))
|
||||||
|
print(pd.to_datetime(klines.iloc[-2].datetime, unit="ns", utc=True).tz_convert(BEIJING_TZ))
|
||||||
|
|
||||||
|
print(klines.iloc[-1])
|
||||||
|
print(klines.iloc[-2])
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
api.wait_update()
|
api.wait_update()
|
||||||
if api.is_changing(klines) and len(klines) > 2:
|
|
||||||
target_pos.set_target_volume(5)
|
if api.is_changing(klines.iloc[-1], 'datetime'):
|
||||||
|
|
||||||
|
print(f'---------{pd.to_datetime(klines.iloc[-1].datetime, unit="ns", utc=True).tz_convert(BEIJING_TZ)}--------')
|
||||||
|
print(klines.iloc[-1])
|
||||||
|
print(klines.iloc[-2])
|
||||||
|
# target_pos.set_target_volume(5)
|
||||||
@@ -236,12 +236,12 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# 这种方式适合获取相对较短或中等长度的历史K线数据。
|
# 这种方式适合获取相对较短或中等长度的历史K线数据。
|
||||||
df_if_backtest_daily = collect_and_save_tqsdk_data_stream(
|
df_if_backtest_daily = collect_and_save_tqsdk_data_stream(
|
||||||
symbol="KQ.m@SHFE.rb",
|
symbol="KQ.m@CZCE.MA",
|
||||||
# symbol='SHFE.rb2510',
|
# symbol='SHFE.rb2510',
|
||||||
# symbol='KQ.i@SHFE.bu',
|
# symbol='KQ.i@SHFE.bu',
|
||||||
freq="min60",
|
freq="min60",
|
||||||
start_date_str="2022-01-01",
|
start_date_str="2022-01-01",
|
||||||
end_date_str="2025-06-22",
|
end_date_str="2025-07-11",
|
||||||
mode="backtest", # 指定为回测模式
|
mode="backtest", # 指定为回测模式
|
||||||
tq_user=TQ_USER_NAME,
|
tq_user=TQ_USER_NAME,
|
||||||
tq_pwd=TQ_PASSWORD,
|
tq_pwd=TQ_PASSWORD,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
555
grid_search_multi_process2.ipynb
Normal file
555
grid_search_multi_process2.ipynb
Normal file
File diff suppressed because one or more lines are too long
17456
main.ipynb
17456
main.ipynb
File diff suppressed because one or more lines are too long
443
main2.ipynb
Normal file
443
main2.ipynb
Normal file
File diff suppressed because one or more lines are too long
51
real_trading.py
Normal file
51
real_trading.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from src.analysis.result_analyzer import ResultAnalyzer
|
||||||
|
|
||||||
|
# 导入 TqsdkEngine,而不是原来的 BacktestEngine
|
||||||
|
from src.tqsdk_real_engine import TqsdkEngine
|
||||||
|
|
||||||
|
# 导入你的策略类
|
||||||
|
from src.strategies.OpenTwoFactorStrategy import SimpleLimitBuyStrategyLong, SimpleLimitBuyStrategyShort, SimpleLimitBuyStrategy
|
||||||
|
|
||||||
|
from tqsdk import TqApi, TqBacktest, TqAuth, TqKq
|
||||||
|
|
||||||
|
|
||||||
|
# --- 配置参数 ---
|
||||||
|
# Tqsdk 的本地数据文件路径,注意 Tqsdk 要求文件名为 KQ_m@交易所_品种名_周期.csv
|
||||||
|
# 例如: KQ_m@SHFE_rb_min60.csv
|
||||||
|
initial_capital = 100000.0
|
||||||
|
slippage_rate = 0.000 # 在 Tqsdk 模拟中,滑点通常由 TqSim 处理或在策略中手动模拟
|
||||||
|
commission_rate = 0.0001 # 同上
|
||||||
|
# 主力合约的 symbol
|
||||||
|
main_symbol = "KQ.m@CZCE.MA"
|
||||||
|
strategy_parameters = {
|
||||||
|
'symbol': main_symbol, # 根据您的数据文件中的品种名称调整
|
||||||
|
'trade_volume': 1,
|
||||||
|
'range_factor': 1.8, # 示例值,需要通过网格搜索优化
|
||||||
|
'profit_factor': 2.8, # 示例值
|
||||||
|
# 'range_factor': 0.7, # 示例值,需要通过网格搜索优化
|
||||||
|
# 'profit_factor': 71, # 示例值
|
||||||
|
# 'range_factor_l': 2, # 示例值,需要通过网格搜索优化
|
||||||
|
# 'profit_factor_l': 3, # 示例值
|
||||||
|
# 'range_factor_s': 1.6, # 示例值,需要通过网格搜索优化
|
||||||
|
# 'profit_factor_s': 5.6, # 示例值
|
||||||
|
'max_position': 10,
|
||||||
|
'enable_log': True,
|
||||||
|
'stop_loss_points': 20,
|
||||||
|
'use_indicator': True
|
||||||
|
}
|
||||||
|
|
||||||
|
api = TqApi(TqKq(), auth=TqAuth("emanresu", "dfgvfgdfgg"))
|
||||||
|
# --- 1. 初始化回测引擎并运行 ---
|
||||||
|
print("\n初始化 Tqsdk 回测引擎...")
|
||||||
|
engine = TqsdkEngine(
|
||||||
|
strategy_class=SimpleLimitBuyStrategyLong,
|
||||||
|
strategy_params=strategy_parameters,
|
||||||
|
api=api,
|
||||||
|
symbol=main_symbol,
|
||||||
|
duration_seconds=60 * 60,
|
||||||
|
roll_over_mode=True, # 启用换月模式检测
|
||||||
|
history_length=50,
|
||||||
|
close_bar_delta=timedelta(minutes=58)
|
||||||
|
)
|
||||||
|
engine.run() # 这是一个同步方法,内部会运行 asyncio 循环
|
||||||
@@ -2,8 +2,12 @@
|
|||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
|
from src.indicators.base_indicators import Indicator
|
||||||
|
|
||||||
# 导入纯函数 (注意相对导入路径的变化)
|
# 导入纯函数 (注意相对导入路径的变化)
|
||||||
from .analysis_utils import (
|
from .analysis_utils import (
|
||||||
calculate_metrics,
|
calculate_metrics,
|
||||||
@@ -26,6 +30,7 @@ class ResultAnalyzer:
|
|||||||
trade_history: List[Trade],
|
trade_history: List[Trade],
|
||||||
bars: List[Bar],
|
bars: List[Bar],
|
||||||
initial_capital: float,
|
initial_capital: float,
|
||||||
|
indicator_list: List[Indicator] = [],
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
@@ -37,8 +42,9 @@ class ResultAnalyzer:
|
|||||||
self.portfolio_snapshots = portfolio_snapshots
|
self.portfolio_snapshots = portfolio_snapshots
|
||||||
self.trade_history = trade_history
|
self.trade_history = trade_history
|
||||||
self.initial_capital = initial_capital
|
self.initial_capital = initial_capital
|
||||||
self.bars = bars # 接收所有K线数据
|
self.bars = bars # 接收所有K线数据
|
||||||
self._metrics_cache: Optional[Dict[str, Any]] = None
|
self._metrics_cache: Optional[Dict[str, Any]] = None
|
||||||
|
self.indicator_list = indicator_list
|
||||||
|
|
||||||
print("\n--- 结果分析器初始化完成 ---")
|
print("\n--- 结果分析器初始化完成 ---")
|
||||||
|
|
||||||
@@ -63,14 +69,17 @@ class ResultAnalyzer:
|
|||||||
print("\n--- 交易明细 ---")
|
print("\n--- 交易明细 ---")
|
||||||
for trade in self.trade_history:
|
for trade in self.trade_history:
|
||||||
# 调整输出格式,显示实现盈亏
|
# 调整输出格式,显示实现盈亏
|
||||||
pnl_display = f" | PnL: {trade.realized_pnl:.2f}" if trade.is_close_trade else ""
|
pnl_display = (
|
||||||
|
f" | Indicators:{trade.indicator_dict} | PnL: {trade.realized_pnl:.2f}"
|
||||||
|
if trade.is_close_trade
|
||||||
|
else ""
|
||||||
|
)
|
||||||
print(
|
print(
|
||||||
f" {trade.fill_time} | {trade.direction:<10} | {trade.symbol} | Vol: {trade.volume} | Price: {trade.price:.2f} | Comm: {trade.commission:.2f}{pnl_display}"
|
f" {trade.fill_time} | {trade.direction:<10} | {trade.symbol} | Vol: {trade.volume} | Price: {trade.price:.2f} | Comm: {trade.commission:.2f}{pnl_display}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print("\n没有交易记录。")
|
print("\n没有交易记录。")
|
||||||
|
|
||||||
|
|
||||||
metrics = self.calculate_all_metrics()
|
metrics = self.calculate_all_metrics()
|
||||||
|
|
||||||
print("\n--- 回测绩效报告 ---")
|
print("\n--- 回测绩效报告 ---")
|
||||||
@@ -82,7 +91,7 @@ class ResultAnalyzer:
|
|||||||
print(f"{'夏普比率':<15}: {metrics['夏普比率']:.2f}")
|
print(f"{'夏普比率':<15}: {metrics['夏普比率']:.2f}")
|
||||||
print(f"{'卡玛比率':<15}: {metrics['卡玛比率']:.2f}")
|
print(f"{'卡玛比率':<15}: {metrics['卡玛比率']:.2f}")
|
||||||
print(f"{'总交易次数':<15}: {metrics['总交易次数']}")
|
print(f"{'总交易次数':<15}: {metrics['总交易次数']}")
|
||||||
print(f"{'总实现盈亏':<15}: {metrics['总实现盈亏']:.2f}") # 新增
|
print(f"{'总实现盈亏':<15}: {metrics['总实现盈亏']:.2f}") # 新增
|
||||||
print(f"{'交易成本':<15}: {metrics['交易成本']:.2f}")
|
print(f"{'交易成本':<15}: {metrics['交易成本']:.2f}")
|
||||||
|
|
||||||
# 新增交易相关详细指标,以适应更全面的交易分析需求
|
# 新增交易相关详细指标,以适应更全面的交易分析需求
|
||||||
@@ -94,7 +103,6 @@ class ResultAnalyzer:
|
|||||||
print(f"{'平均每次盈利':<15}: {metrics['平均每次盈利']:.2f}")
|
print(f"{'平均每次盈利':<15}: {metrics['平均每次盈利']:.2f}")
|
||||||
print(f"{'平均每次亏损':<15}: {metrics['平均每次亏损']:.2f}")
|
print(f"{'平均每次亏损':<15}: {metrics['平均每次亏损']:.2f}")
|
||||||
|
|
||||||
|
|
||||||
def plot_performance(self) -> None:
|
def plot_performance(self) -> None:
|
||||||
"""
|
"""
|
||||||
绘制投资组合净值和回撤曲线,以及所有合约的收盘价曲线。
|
绘制投资组合净值和回撤曲线,以及所有合约的收盘价曲线。
|
||||||
@@ -103,9 +111,200 @@ class ResultAnalyzer:
|
|||||||
plot_equity_and_drawdown_chart(
|
plot_equity_and_drawdown_chart(
|
||||||
self.portfolio_snapshots,
|
self.portfolio_snapshots,
|
||||||
self.initial_capital,
|
self.initial_capital,
|
||||||
title="Portfolio Equity and Drawdown Curve (All Contracts)" # 明确标题,表明是整体曲线
|
title="Portfolio Equity and Drawdown Curve (All Contracts)", # 明确标题,表明是整体曲线
|
||||||
)
|
)
|
||||||
|
|
||||||
# 绘制所有处理过的K线收盘价曲线
|
# 绘制所有处理过的K线收盘价曲线
|
||||||
plot_close_price_chart(self.bars, title="Underlying Asset Close Price (Concatenated Bars)") # 明确标题
|
plot_close_price_chart(
|
||||||
print("图表绘制完成。")
|
self.bars, title="Underlying Asset Close Price (Concatenated Bars)"
|
||||||
|
) # 明确标题
|
||||||
|
print("图表绘制完成。")
|
||||||
|
|
||||||
|
def analyze_indicators(self):
|
||||||
|
"""
|
||||||
|
分析所有平仓交易的指标值与实现盈亏的关系,并绘制累积盈亏曲线图。
|
||||||
|
图表将展示指标值区间与对应累积盈亏的关系,帮助找出具有概率优势的指标区间。
|
||||||
|
同时会标记出最大和最小累积盈亏对应的指标值,并优化标注位置以避免重叠。
|
||||||
|
"""
|
||||||
|
close_trades = [trade for trade in self.trade_history if trade.is_close_trade]
|
||||||
|
|
||||||
|
if not close_trades:
|
||||||
|
print(
|
||||||
|
"没有平仓交易可供分析。请确保 trade_history 中有 is_close_trade 为 True 的交易。"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
for indicator in self.indicator_list:
|
||||||
|
# 假设每个 indicator 对象都有一个 get_name() 方法
|
||||||
|
indicator_name = indicator.get_name()
|
||||||
|
|
||||||
|
# 收集该指标的所有值和对应的实现盈亏
|
||||||
|
indi_values = []
|
||||||
|
pnls = []
|
||||||
|
for trade in close_trades:
|
||||||
|
# 确保 trade.indicator_dict 中包含当前指标的值
|
||||||
|
# 并且这个值是可用的(非None或NaN)
|
||||||
|
if (
|
||||||
|
indicator_name in trade.indicator_dict
|
||||||
|
and trade.indicator_dict[indicator_name] is not None
|
||||||
|
):
|
||||||
|
# 检查是否为 NaN,如果使用 np.nan,则需要 isinstance(value, float) and np.isnan(value)
|
||||||
|
# 为了简化,这里假设非 None 即为有效数值
|
||||||
|
if not (
|
||||||
|
isinstance(trade.indicator_dict[indicator_name], float)
|
||||||
|
and np.isnan(trade.indicator_dict[indicator_name])
|
||||||
|
):
|
||||||
|
indi_values.append(trade.indicator_dict[indicator_name])
|
||||||
|
pnls.append(trade.realized_pnl)
|
||||||
|
|
||||||
|
if not indi_values:
|
||||||
|
print(f"指标 '{indicator_name}' 没有对应的有效平仓交易数据。跳过绘图。")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 将收集到的数据转换为 Pandas DataFrame 进行更便捷的处理
|
||||||
|
# DataFrame 的结构为:['indicator_value', 'realized_pnl']
|
||||||
|
df = pd.DataFrame({"indicator_value": indi_values, "realized_pnl": pnls})
|
||||||
|
|
||||||
|
# 确保数据框不为空
|
||||||
|
if df.empty:
|
||||||
|
print(f"指标 '{indicator_name}' 的数据框为空,跳过绘图。")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 按照指标值进行排序,这是计算累积和的关键步骤
|
||||||
|
df = df.sort_values(by="indicator_value").reset_index(drop=True)
|
||||||
|
|
||||||
|
# --- 绘制累积收益曲线 ---
|
||||||
|
plt.figure(figsize=(12, 7)) # 创建一个新的图表
|
||||||
|
|
||||||
|
# 获取指标值的范围,用于生成X轴的等距点
|
||||||
|
min_val = df["indicator_value"].min()
|
||||||
|
max_val = df["indicator_value"].max()
|
||||||
|
|
||||||
|
# 特殊处理:如果所有指标值都相同
|
||||||
|
if min_val == max_val:
|
||||||
|
total_pnl = df["realized_pnl"].sum()
|
||||||
|
print(
|
||||||
|
f"指标 '{indicator_name}' 的所有值都相同 ({min_val:.2f}),无法创建区间图,绘制一个点表示总收益。"
|
||||||
|
)
|
||||||
|
plt.plot(min_val, total_pnl, "ro", markersize=8) # 绘制一个点
|
||||||
|
plt.title(
|
||||||
|
f"{indicator_name} Value vs. Cumulative PnL (All values are {min_val:.2f})"
|
||||||
|
)
|
||||||
|
plt.xlabel(f"{indicator_name} Value")
|
||||||
|
plt.ylabel("Cumulative Realized PnL")
|
||||||
|
plt.grid(True)
|
||||||
|
plt.text(
|
||||||
|
min_val,
|
||||||
|
total_pnl,
|
||||||
|
f" Total PnL: {total_pnl:.2f}",
|
||||||
|
ha="center",
|
||||||
|
va="bottom",
|
||||||
|
)
|
||||||
|
plt.show()
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 生成X轴上的100个等距点,这些点代表了指标值的不同阈值
|
||||||
|
x_points = np.linspace(min_val, max_val, 100)
|
||||||
|
|
||||||
|
# 计算Y轴的值:对于每个X轴点 xp,Y轴值是所有 'indicator_value' <= xp 的 'realized_pnl' 之和
|
||||||
|
y_cumulative_pnl = []
|
||||||
|
for xp in x_points:
|
||||||
|
# 筛选出指标值小于等于当前x_point的所有交易,并求和它们的 realized_pnl
|
||||||
|
cumulative_pnl = df[df["indicator_value"] <= xp]["realized_pnl"].sum()
|
||||||
|
y_cumulative_pnl.append(cumulative_pnl)
|
||||||
|
|
||||||
|
# 绘制累积盈亏曲线
|
||||||
|
plt.plot(
|
||||||
|
x_points,
|
||||||
|
y_cumulative_pnl,
|
||||||
|
marker="o",
|
||||||
|
linestyle="-",
|
||||||
|
markersize=3,
|
||||||
|
label=f"Cumulative PnL for {indicator_name}",
|
||||||
|
alpha=0.8,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 标记累积盈亏的最大值点
|
||||||
|
optimal_index = np.argmax(y_cumulative_pnl)
|
||||||
|
optimal_indi_value = x_points[optimal_index]
|
||||||
|
max_cumulative_pnl = y_cumulative_pnl[optimal_index]
|
||||||
|
|
||||||
|
# 标记累积盈亏的最小值点
|
||||||
|
min_pnl_index = np.argmin(y_cumulative_pnl[:optimal_index]) if len(y_cumulative_pnl[:optimal_index]) > 0 else 0
|
||||||
|
min_indi_value_at_pnl = x_points[min_pnl_index]
|
||||||
|
min_cumulative_pnl = y_cumulative_pnl[min_pnl_index]
|
||||||
|
|
||||||
|
# 动态调整标注位置以避免重叠
|
||||||
|
offset_x = (max_val - min_val) * 0.05 # 水平偏移量
|
||||||
|
|
||||||
|
# 默认标注为右侧对齐,文本在点的左侧
|
||||||
|
max_ha = "right"
|
||||||
|
max_xytext_x = optimal_indi_value - offset_x
|
||||||
|
min_ha = "right"
|
||||||
|
min_xytext_x = min_indi_value_at_pnl - offset_x
|
||||||
|
|
||||||
|
# 如果最大值点在最小值点右侧,则最大值标注放左侧,最小值标注放右侧
|
||||||
|
# 这样可以避免标注文本重叠
|
||||||
|
if optimal_indi_value > min_indi_value_at_pnl:
|
||||||
|
max_ha = "left"
|
||||||
|
max_xytext_x = optimal_indi_value + offset_x
|
||||||
|
min_ha = "right"
|
||||||
|
min_xytext_x = min_indi_value_at_pnl - offset_x
|
||||||
|
else: # 如果最大值点在最小值点左侧或重合
|
||||||
|
max_ha = "right"
|
||||||
|
max_xytext_x = optimal_indi_value - offset_x
|
||||||
|
min_ha = "left"
|
||||||
|
min_xytext_x = min_indi_value_at_pnl + offset_x
|
||||||
|
|
||||||
|
# 绘制最大值垂直线和标注
|
||||||
|
plt.axvline(
|
||||||
|
optimal_indi_value,
|
||||||
|
color="red",
|
||||||
|
linestyle="--",
|
||||||
|
label=f"Max PnL Threshold: {optimal_indi_value:.2f}",
|
||||||
|
alpha=0.7,
|
||||||
|
)
|
||||||
|
plt.annotate(
|
||||||
|
f"Max Cum. PnL: {max_cumulative_pnl:.2f}",
|
||||||
|
xy=(optimal_indi_value, max_cumulative_pnl),
|
||||||
|
xytext=(max_xytext_x, max_cumulative_pnl),
|
||||||
|
arrowprops=dict(facecolor="red", shrink=0.05),
|
||||||
|
horizontalalignment=max_ha,
|
||||||
|
verticalalignment="bottom",
|
||||||
|
color="red",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 绘制最小值垂直线和标注
|
||||||
|
plt.axvline(
|
||||||
|
min_indi_value_at_pnl,
|
||||||
|
color="blue",
|
||||||
|
linestyle=":",
|
||||||
|
label=f"Min PnL Threshold: {min_indi_value_at_pnl:.2f}",
|
||||||
|
alpha=0.7,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 垂直偏移最小值标注,避免与曲线重叠
|
||||||
|
min_text_y_offset = (
|
||||||
|
-(max_cumulative_pnl - min_cumulative_pnl) * 0.1
|
||||||
|
if max_cumulative_pnl != min_cumulative_pnl
|
||||||
|
else -0.05
|
||||||
|
)
|
||||||
|
plt.annotate(
|
||||||
|
f"Min Cum. PnL: {min_cumulative_pnl:.2f}",
|
||||||
|
xy=(min_indi_value_at_pnl, min_cumulative_pnl),
|
||||||
|
xytext=(min_xytext_x, min_cumulative_pnl + min_text_y_offset),
|
||||||
|
arrowprops=dict(facecolor="blue", shrink=0.05),
|
||||||
|
horizontalalignment=min_ha,
|
||||||
|
verticalalignment="top",
|
||||||
|
color="blue",
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.title(f"{indicator_name} Value vs. Cumulative Realized PnL")
|
||||||
|
plt.xlabel(f"{indicator_name} Value")
|
||||||
|
plt.ylabel("Cumulative Realized PnL")
|
||||||
|
plt.grid(True)
|
||||||
|
plt.legend()
|
||||||
|
plt.tight_layout() # 自动调整图表参数,使之更紧凑
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
print("\n所有指标的分析图表已生成。")
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ class BacktestContext:
|
|||||||
|
|
||||||
def get_bar_history(self):
|
def get_bar_history(self):
|
||||||
return self._engine.get_bar_history()
|
return self._engine.get_bar_history()
|
||||||
|
|
||||||
|
def get_price_history(self, key: str):
|
||||||
|
return self._engine.get_price_history(key)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_rollover_bar(self) -> bool:
|
def is_rollover_bar(self) -> bool:
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
# src/backtest_engine.py
|
# src/backtest_engine.py
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Type, Dict, Any, List, Optional
|
from typing import Type, Dict, Any, List, Optional
|
||||||
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
|
from src.indicators.base_indicators import Indicator
|
||||||
|
|
||||||
# 导入所有需要协调的模块
|
# 导入所有需要协调的模块
|
||||||
from .core_data import Bar, Order, Trade, PortfolioSnapshot
|
from .core_data import Bar, Order, Trade, PortfolioSnapshot
|
||||||
from .data_manager import DataManager
|
from .data_manager import DataManager
|
||||||
@@ -24,7 +27,8 @@ class BacktestEngine:
|
|||||||
commission_rate: float = 0.0002,
|
commission_rate: float = 0.0002,
|
||||||
roll_over_mode: bool = False,
|
roll_over_mode: bool = False,
|
||||||
start_time: Optional[datetime] = None, # 新增开始时间
|
start_time: Optional[datetime] = None, # 新增开始时间
|
||||||
end_time: Optional[datetime] = None # 新增结束时间
|
end_time: Optional[datetime] = None, # 新增结束时间
|
||||||
|
indicators: List[Indicator] = [],
|
||||||
): # 新增换月模式参数
|
): # 新增换月模式参数
|
||||||
"""
|
"""
|
||||||
初始化回测引擎。
|
初始化回测引擎。
|
||||||
@@ -54,10 +58,18 @@ class BacktestEngine:
|
|||||||
# 实例化策略。初始 symbol 会在 run_backtest 中根据第一根 Bar 动态设置。
|
# 实例化策略。初始 symbol 会在 run_backtest 中根据第一根 Bar 动态设置。
|
||||||
self.strategy = strategy_class(self.context, symbol="INITIAL_PLACEHOLDER_SYMBOL", **strategy_params)
|
self.strategy = strategy_class(self.context, symbol="INITIAL_PLACEHOLDER_SYMBOL", **strategy_params)
|
||||||
|
|
||||||
|
self.indicators = indicators
|
||||||
|
|
||||||
self.portfolio_snapshots: List[PortfolioSnapshot] = []
|
self.portfolio_snapshots: List[PortfolioSnapshot] = []
|
||||||
self.trade_history: List[Trade] = []
|
self.trade_history: List[Trade] = []
|
||||||
self.all_bars: List[Bar] = []
|
self.all_bars: List[Bar] = []
|
||||||
|
|
||||||
|
self.close_list: List[float] = []
|
||||||
|
self.open_list: List[float] = []
|
||||||
|
self.high_list: List[float] = []
|
||||||
|
self.low_list: List[float] = []
|
||||||
|
self.volume_list: List[float] = []
|
||||||
|
|
||||||
self._history_bars: List[Bar] = [] # 引擎层面保留的历史 Bar,通常供策略在 on_bar 中使用
|
self._history_bars: List[Bar] = [] # 引擎层面保留的历史 Bar,通常供策略在 on_bar 中使用
|
||||||
self._max_history_bars: int = strategy_params.get('history_bars_limit', 200)
|
self._max_history_bars: int = strategy_params.get('history_bars_limit', 200)
|
||||||
|
|
||||||
@@ -84,6 +96,8 @@ class BacktestEngine:
|
|||||||
# 调用策略的初始化方法
|
# 调用策略的初始化方法
|
||||||
self.strategy.on_init()
|
self.strategy.on_init()
|
||||||
|
|
||||||
|
self.strategy.trading = True
|
||||||
|
|
||||||
last_processed_bar: Optional[Bar] = None # 用于在换月时引用旧合约的最后一根 K 线
|
last_processed_bar: Optional[Bar] = None # 用于在换月时引用旧合约的最后一根 K 线
|
||||||
|
|
||||||
# 主回测循环
|
# 主回测循环
|
||||||
@@ -94,6 +108,11 @@ class BacktestEngine:
|
|||||||
break # 没有更多数据,回测结束
|
break # 没有更多数据,回测结束
|
||||||
|
|
||||||
self.all_bars.append(current_bar)
|
self.all_bars.append(current_bar)
|
||||||
|
self.close_list.append(current_bar.close)
|
||||||
|
self.open_list.append(current_bar.open)
|
||||||
|
self.high_list.append(current_bar.high)
|
||||||
|
self.low_list.append(current_bar.low)
|
||||||
|
self.volume_list.append(current_bar.volume)
|
||||||
|
|
||||||
if self.start_time and current_bar.datetime < self.start_time:
|
if self.start_time and current_bar.datetime < self.start_time:
|
||||||
continue
|
continue
|
||||||
@@ -153,12 +172,28 @@ class BacktestEngine:
|
|||||||
# self.simulator.process_pending_orders(current_bar)
|
# self.simulator.process_pending_orders(current_bar)
|
||||||
self.strategy.on_open_bar(current_bar)
|
self.strategy.on_open_bar(current_bar)
|
||||||
|
|
||||||
|
current_indicator_dict = {}
|
||||||
|
close_array = np.array(self.close_list)
|
||||||
|
open_array = np.array(self.open_list)
|
||||||
|
high_array = np.array(self.high_list)
|
||||||
|
low_array = np.array(self.low_list)
|
||||||
|
volume_array = np.array(self.volume_list)
|
||||||
|
|
||||||
|
for indicator in self.indicators:
|
||||||
|
current_indicator_dict[indicator.get_name()] = indicator.get_latest_value(
|
||||||
|
close_array,
|
||||||
|
open_array,
|
||||||
|
high_array,
|
||||||
|
low_array,
|
||||||
|
volume_array
|
||||||
|
)
|
||||||
|
|
||||||
# 7. 调用策略的 on_bar 方法
|
# 7. 调用策略的 on_bar 方法
|
||||||
# self.strategy.on_bar(current_bar)
|
# self.strategy.on_bar(current_bar)
|
||||||
self.simulator.process_pending_orders(current_bar)
|
self.simulator.process_pending_orders(current_bar, current_indicator_dict)
|
||||||
|
|
||||||
self.strategy.on_close_bar(current_bar)
|
self.strategy.on_close_bar(current_bar)
|
||||||
self.simulator.process_pending_orders(current_bar)
|
self.simulator.process_pending_orders(current_bar, current_indicator_dict)
|
||||||
|
|
||||||
|
|
||||||
# 8. 记录投资组合快照
|
# 8. 记录投资组合快照
|
||||||
@@ -230,3 +265,16 @@ class BacktestEngine:
|
|||||||
def get_bar_history(self):
|
def get_bar_history(self):
|
||||||
return self.all_bars
|
return self.all_bars
|
||||||
|
|
||||||
|
|
||||||
|
def get_price_history(self, key: str):
|
||||||
|
if key == 'close':
|
||||||
|
return self.close_list
|
||||||
|
elif key == 'open':
|
||||||
|
return self.open_list
|
||||||
|
elif key == 'high':
|
||||||
|
return self.high_list
|
||||||
|
elif key == 'low':
|
||||||
|
return self.low_list
|
||||||
|
elif key == 'volume':
|
||||||
|
return self.volume_list
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
from typing import List, Union
|
from typing import List, Union
|
||||||
|
|
||||||
|
BEIJING_TZ = "Asia/Shanghai"
|
||||||
|
|
||||||
|
|
||||||
def generate_parameter_range(start: Union[int, float], end: Union[int, float], step: Union[int, float]) -> List[Union[int, float]]:
|
def generate_parameter_range(start: Union[int, float], end: Union[int, float], step: Union[int, float]) -> List[Union[int, float]]:
|
||||||
"""
|
"""
|
||||||
根据开始、结束和步长生成一个参数值的列表。
|
根据开始、结束和步长生成一个参数值的列表。
|
||||||
@@ -31,6 +34,82 @@ def generate_parameter_range(start: Union[int, float], end: Union[int, float], s
|
|||||||
|
|
||||||
return param_range
|
return param_range
|
||||||
|
|
||||||
# 示例:
|
from datetime import datetime, time, timedelta
|
||||||
# print(generate_parameter_range(0.99, 1.01, 0.005)) # [0.99, 0.995, 1.0, 1.005, 1.01]
|
from typing import Optional
|
||||||
# print(generate_parameter_range(5, 20, 5)) # [5, 10, 15, 20]
|
|
||||||
|
def is_futures_trading_time(current_dt: Optional[datetime] = None) -> bool:
|
||||||
|
"""
|
||||||
|
判断当前时间(或指定时间)是否是中国期货的开盘时间。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_dt (Optional[datetime]): 要检查的 datetime 对象。如果为 None,
|
||||||
|
则默认使用当前系统时间(datetime.now())。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 如果是期货开盘时间则返回 True,否则返回 False。
|
||||||
|
"""
|
||||||
|
if current_dt is None:
|
||||||
|
current_dt = datetime.now()
|
||||||
|
|
||||||
|
current_time = current_dt.time() # 获取时间部分 (小时、分钟、秒)
|
||||||
|
|
||||||
|
# 定义白盘交易时段 (使用 time 对象进行精确比较)
|
||||||
|
day_sessions = [
|
||||||
|
(time(9, 0), time(10, 15)), # 09:00 - 10:15
|
||||||
|
(time(10, 30), time(11, 30)), # 10:30 - 11:30
|
||||||
|
(time(13, 30), time(15, 0)) # 13:30 - 15:00
|
||||||
|
]
|
||||||
|
|
||||||
|
# 定义夜盘交易时段 (简化为 21:00 - 23:00)
|
||||||
|
# 注意:这里的 23:00 是结束时刻的开始,意味着实际交易到 22:59:59。
|
||||||
|
night_session = (time(21, 0), time(23, 0)) # 21:00 - 23:00 (不包含 23:00)
|
||||||
|
|
||||||
|
# 检查当前时间是否在任何一个日盘交易时段内
|
||||||
|
for start_t, end_t in day_sessions:
|
||||||
|
if start_t <= current_time < end_t:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 检查当前时间是否在夜盘交易时段内
|
||||||
|
if night_session[0] <= current_time < night_session[1]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 如果不在任何交易时段内,则不是开盘时间
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_bar_pre_close_period(
|
||||||
|
bar_start_time: datetime,
|
||||||
|
bar_duration_seconds: int,
|
||||||
|
pre_close_minutes: int,
|
||||||
|
current_system_time: Optional[datetime] = None
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
判断当前系统时间是否在一根K线的即将结束时间段内。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bar_start_time (datetime): 当前K线的开始时间(例如:bar.datetime)。
|
||||||
|
可以是 datetime.datetime 对象或 pandas.Timestamp 对象。
|
||||||
|
bar_duration_seconds (int): 该K线的持续时间,以秒为单位。
|
||||||
|
对于1小时K线,传入 3600。
|
||||||
|
pre_close_minutes (int): K线结束前多少分钟被认为是“即将结束”状态。例如,传入 3 表示结束前3分钟。
|
||||||
|
current_system_time (Optional[datetime]): 用于判断的当前时间。
|
||||||
|
如果为 None,则默认使用 `datetime.now()`。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 如果当前时间在K线结束前 `pre_close_minutes` 的窗口内,则返回 True,否则返回 False。
|
||||||
|
"""
|
||||||
|
if current_system_time is None:
|
||||||
|
current_system_time = datetime.now(BEIJING_TZ)
|
||||||
|
|
||||||
|
# 1. 计算K线的精确结束时间
|
||||||
|
# K线结束时间 = K线开始时间 + K线持续时间
|
||||||
|
bar_end_time = bar_start_time + timedelta(seconds=bar_duration_seconds)
|
||||||
|
|
||||||
|
# 2. 计算“即将结束”窗口的开始时间
|
||||||
|
# 这个窗口从 (K线结束时间 - pre_close_minutes) 开始,到 K线结束时间 结束
|
||||||
|
pre_close_window_start_time = bar_end_time - timedelta(minutes=pre_close_minutes)
|
||||||
|
|
||||||
|
# 3. 判断当前系统时间是否在这个窗口内
|
||||||
|
# 窗口定义为 [pre_close_window_start_time, bar_end_time),即包含开始时间,不包含结束时间
|
||||||
|
print(pre_close_window_start_time, current_system_time, bar_end_time)
|
||||||
|
return pre_close_window_start_time <= current_system_time < bar_end_time
|
||||||
@@ -82,6 +82,7 @@ class Trade:
|
|||||||
realized_pnl: float = 0.0 # <--- 新增字段:这笔交易带来的实现盈亏
|
realized_pnl: float = 0.0 # <--- 新增字段:这笔交易带来的实现盈亏
|
||||||
is_open_trade: bool = False # <--- 新增字段:是否是开仓交易(用于跟踪成本)
|
is_open_trade: bool = False # <--- 新增字段:是否是开仓交易(用于跟踪成本)
|
||||||
is_close_trade: bool = False # <--- 新增字段:是否是平仓交易 (用于计算盈亏)
|
is_close_trade: bool = False # <--- 新增字段:是否是平仓交易 (用于计算盈亏)
|
||||||
|
indicator_dict: Dict[str, float] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass()
|
@dataclass()
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class ExecutionSimulator:
|
|||||||
self.trade_log: List[Trade] = []
|
self.trade_log: List[Trade] = []
|
||||||
self.pending_orders: Dict[str, Order] = {}
|
self.pending_orders: Dict[str, Order] = {}
|
||||||
self._current_time: Optional[datetime] = None
|
self._current_time: Optional[datetime] = None
|
||||||
|
self.indicator_dict = {}
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"模拟器初始化:初始资金={self.initial_capital:.2f}, 滑点率={self.slippage_rate}, 佣金率={self.commission_rate}"
|
f"模拟器初始化:初始资金={self.initial_capital:.2f}, 滑点率={self.slippage_rate}, 佣金率={self.commission_rate}"
|
||||||
@@ -117,7 +118,7 @@ class ExecutionSimulator:
|
|||||||
self.pending_orders[order.id] = order
|
self.pending_orders[order.id] = order
|
||||||
return order
|
return order
|
||||||
|
|
||||||
def process_pending_orders(self, current_bar: Bar):
|
def process_pending_orders(self, current_bar: Bar, indicator_dict: Dict[str, float]):
|
||||||
"""
|
"""
|
||||||
处理所有待撮合的订单。在每个K线数据到来时调用。
|
处理所有待撮合的订单。在每个K线数据到来时调用。
|
||||||
"""
|
"""
|
||||||
@@ -132,7 +133,16 @@ class ExecutionSimulator:
|
|||||||
if order.symbol != current_bar.symbol:
|
if order.symbol != current_bar.symbol:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self._execute_single_order(order, current_bar)
|
trade = self._execute_single_order(order, current_bar)
|
||||||
|
if trade:
|
||||||
|
self.trade_log.append(trade)
|
||||||
|
|
||||||
|
if trade.is_open_trade:
|
||||||
|
self.indicator_dict = indicator_dict
|
||||||
|
elif trade.is_close_trade:
|
||||||
|
trade.indicator_dict = self.indicator_dict.copy()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _execute_single_order(self, order: Order, current_bar: Bar) -> Optional[Trade]:
|
def _execute_single_order(self, order: Order, current_bar: Bar) -> Optional[Trade]:
|
||||||
"""
|
"""
|
||||||
@@ -295,7 +305,6 @@ class ExecutionSimulator:
|
|||||||
is_open_trade=is_trade_an_open_operation,
|
is_open_trade=is_trade_an_open_operation,
|
||||||
is_close_trade=is_trade_a_close_operation,
|
is_close_trade=is_trade_a_close_operation,
|
||||||
)
|
)
|
||||||
self.trade_log.append(executed_trade)
|
|
||||||
|
|
||||||
if order.id in self.pending_orders:
|
if order.id in self.pending_orders:
|
||||||
del self.pending_orders[order.id]
|
del self.pending_orders[order.id]
|
||||||
|
|||||||
0
src/indicators/__init__.py
Normal file
0
src/indicators/__init__.py
Normal file
26
src/indicators/base_indicators.py
Normal file
26
src/indicators/base_indicators.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from src.core_data import Bar
|
||||||
|
|
||||||
|
|
||||||
|
class Indicator(ABC):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_values(self, close: np.array, open: np.array, high: np.array, low: np.array, volume: np.array):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_value(self, close: np.array, open: np.array, high: np.array, low: np.array, volume: np.array):
|
||||||
|
return self.get_values(close, open, high, low, volume)[-1].item()
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_name(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
17
src/indicators/indicator_list.py
Normal file
17
src/indicators/indicator_list.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from src.indicators.indicators import RSI, HistoricalRange
|
||||||
|
|
||||||
|
|
||||||
|
INDICATOR_LIST = [
|
||||||
|
RSI(5),
|
||||||
|
RSI(10),
|
||||||
|
RSI(15),
|
||||||
|
RSI(20),
|
||||||
|
RSI(25),
|
||||||
|
RSI(30),
|
||||||
|
RSI(35),
|
||||||
|
RSI(40),
|
||||||
|
HistoricalRange(1),
|
||||||
|
HistoricalRange(8),
|
||||||
|
HistoricalRange(15),
|
||||||
|
HistoricalRange(21),
|
||||||
|
]
|
||||||
88
src/indicators/indicators.py
Normal file
88
src/indicators/indicators.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import talib
|
||||||
|
from src.indicators.base_indicators import Indicator
|
||||||
|
|
||||||
|
|
||||||
|
class RSI(Indicator):
|
||||||
|
"""
|
||||||
|
相对强弱指数 (RSI) 指标实现,使用 TA-Lib 简化计算。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, window: int = 14):
|
||||||
|
"""
|
||||||
|
初始化RSI指标。
|
||||||
|
Args:
|
||||||
|
window (int): RSI的计算周期,默认为14。
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
self.window = window
|
||||||
|
|
||||||
|
def get_values(self,
|
||||||
|
close: np.array,
|
||||||
|
open: np.array, # 不使用
|
||||||
|
high: np.array, # 不使用
|
||||||
|
low: np.array, # 不使用
|
||||||
|
volume: np.array) -> np.array: # 不使用
|
||||||
|
"""
|
||||||
|
根据收盘价列表计算RSI值,使用 TA-Lib。
|
||||||
|
Args:
|
||||||
|
close (np.array): 收盘价列表。
|
||||||
|
其他 OHLCV 参数在此指标中不使用。
|
||||||
|
Returns:
|
||||||
|
np.array: RSI值列表。如果数据不足,则列表开头为NaN。
|
||||||
|
"""
|
||||||
|
# 使用 talib.RSI 直接计算
|
||||||
|
# 注意:TA-Lib 会在数据不足时自动填充 NaN
|
||||||
|
rsi_values = talib.RSI(close, timeperiod=self.window)
|
||||||
|
|
||||||
|
# 将 numpy 数组转换为 list 并返回
|
||||||
|
return rsi_values
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return f'rsi_{self.window}'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class HistoricalRange(Indicator):
|
||||||
|
"""
|
||||||
|
历史波动幅度指标:计算过去 N 日的 (最高价 - 最低价) 的简单移动平均。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, window: int = 20):
|
||||||
|
"""
|
||||||
|
初始化历史波动幅度指标。
|
||||||
|
Args:
|
||||||
|
window (int): 计算范围平均值的周期,默认为20。
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
self.window = window
|
||||||
|
|
||||||
|
def get_values(self,
|
||||||
|
close: np.array, # 不使用
|
||||||
|
open: np.array, # 不使用
|
||||||
|
high: np.array,
|
||||||
|
low: np.array,
|
||||||
|
volume: np.array) -> np.array: # 不使用
|
||||||
|
"""
|
||||||
|
根据最高价和最低价列表计算过去 N 日的 (high - low) 值的简单移动平均。
|
||||||
|
Args:
|
||||||
|
high (np.array): 最高价列表。
|
||||||
|
low (np.array): 最低价列表。
|
||||||
|
其他 OHLCV 参数在此指标中不使用。
|
||||||
|
Returns:
|
||||||
|
np.array: 历史波动幅度指标值列表。如果数据不足,则列表开头为NaN。
|
||||||
|
"""
|
||||||
|
# if not high or not low or len(high) != len(low):
|
||||||
|
# print(high, low, len(high), len(low))
|
||||||
|
# return []
|
||||||
|
|
||||||
|
# 计算每日的 (high - low) 范围
|
||||||
|
daily_ranges = high - low
|
||||||
|
|
||||||
|
# 将 numpy 数组转换为 list 并返回
|
||||||
|
return np.roll(daily_ranges, self.window)
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return f'range_{self.window}'
|
||||||
829
src/strategies/OpenTwoFactorStrategy.py
Normal file
829
src/strategies/OpenTwoFactorStrategy.py
Normal file
@@ -0,0 +1,829 @@
|
|||||||
|
# src/strategies/simple_limit_buy_strategy.py
|
||||||
|
|
||||||
|
from tkinter import N
|
||||||
|
import numpy as np
|
||||||
|
from src.indicators.indicators import RSI, HistoricalRange
|
||||||
|
from .base_strategy import Strategy
|
||||||
|
from ..core_data import Bar, Order
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleLimitBuyStrategyLong(Strategy):
|
||||||
|
"""
|
||||||
|
一个基于当前K线Open、前1根和前7根K线Range计算优势价格进行限价买入的策略。
|
||||||
|
具备以下特点:
|
||||||
|
- 每根K线开始时取消上一根K线未成交的订单。
|
||||||
|
- 最多只能有一个开仓挂单和一个持仓。
|
||||||
|
- 包含简单的止损和止盈逻辑。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
context: Any,
|
||||||
|
symbol: str,
|
||||||
|
enable_log: bool,
|
||||||
|
trade_volume: int,
|
||||||
|
range_factor: float,
|
||||||
|
profit_factor: float,
|
||||||
|
max_position: int,
|
||||||
|
stop_loss_points: float = 10, # 新增:止损点数
|
||||||
|
take_profit_points: float = 10,
|
||||||
|
use_indicator: bool = False,
|
||||||
|
): # 新增:止盈点数
|
||||||
|
"""
|
||||||
|
初始化策略。
|
||||||
|
Args:
|
||||||
|
context: 模拟器实例。
|
||||||
|
symbol (str): 交易合约代码。
|
||||||
|
trade_volume (int): 单笔交易量。
|
||||||
|
range_factor (float): 前1根K线Range的权重因子,用于从Open价向下偏移。
|
||||||
|
profit_factor (float): 前7根K线Range的权重因子,用于从Open价向下偏移。
|
||||||
|
max_position (int): 最大持仓量(此处为1,因为只允许一个持仓)。
|
||||||
|
stop_loss_points (float): 止损点数(例如,亏损达到此点数则止损)。
|
||||||
|
take_profit_points (float): 止盈点数(例如,盈利达到此点数则止盈)。
|
||||||
|
"""
|
||||||
|
super().__init__(context, symbol, enable_log)
|
||||||
|
self.trade_volume = trade_volume
|
||||||
|
self.range_factor = range_factor
|
||||||
|
self.profit_factor = profit_factor
|
||||||
|
self.max_position = max_position # 理论上这里应为1
|
||||||
|
self.stop_loss_points = stop_loss_points
|
||||||
|
self.take_profit_points = take_profit_points
|
||||||
|
self.use_indicator = use_indicator
|
||||||
|
|
||||||
|
self.order_id_counter = 0
|
||||||
|
|
||||||
|
self._last_order_id: Optional[str] = None # 用于跟踪上一根K线发出的订单ID
|
||||||
|
|
||||||
|
self.log(
|
||||||
|
f"策略初始化: symbol={self.symbol}, trade_volume={self.trade_volume}, "
|
||||||
|
f"range_factor={self.range_factor}, "
|
||||||
|
f"profit_factor={self.profit_factor}, "
|
||||||
|
f"max_position={self.max_position}, "
|
||||||
|
f"止损点={self.stop_loss_points}, 止盈点={self.take_profit_points}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_init(self):
|
||||||
|
super().on_init()
|
||||||
|
count = self.cancel_all_pending_orders()
|
||||||
|
self.log(f'取消{count}笔订单')
|
||||||
|
|
||||||
|
|
||||||
|
def on_open_bar(self, bar: Bar, next_bar_open: Optional[float] = None):
|
||||||
|
"""
|
||||||
|
每当新的K线数据到来时调用。
|
||||||
|
Args:
|
||||||
|
bar (Bar): 当前的K线数据对象。
|
||||||
|
next_bar_open (Optional[float]): 下一根K线的开盘价,此处策略未使用。
|
||||||
|
"""
|
||||||
|
current_datetime = bar.datetime # 获取当前K线时间
|
||||||
|
self.symbol = bar.symbol
|
||||||
|
|
||||||
|
# --- 1. 撤销上一根K线未成交的订单 ---
|
||||||
|
# 检查是否记录了上一笔订单ID,并且该订单仍然在待处理列表中
|
||||||
|
if self._last_order_id:
|
||||||
|
pending_orders = self.get_pending_orders()
|
||||||
|
if self._last_order_id in pending_orders:
|
||||||
|
success = self.cancel_order(
|
||||||
|
self._last_order_id
|
||||||
|
) # 直接调用基类的取消方法
|
||||||
|
if success:
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 策略: 成功撤销上一根K线未成交订单 {self._last_order_id}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 策略: 尝试撤销订单 {self._last_order_id} 失败(可能已成交或不存在)"
|
||||||
|
)
|
||||||
|
# 无论撤销成功与否,既然我们尝试了撤销,就清除记录
|
||||||
|
self._last_order_id = None
|
||||||
|
# else:
|
||||||
|
# self.log(f"[{current_datetime}] 策略: 无上一根K线未成交订单需要撤销。")
|
||||||
|
|
||||||
|
# 2. 更新K线历史
|
||||||
|
trade_volume = self.trade_volume
|
||||||
|
|
||||||
|
# 获取当前持仓和未决订单(在取消之后获取,确保是最新的状态)
|
||||||
|
current_positions = self.get_current_positions()
|
||||||
|
current_pos_volume = current_positions.get(self.symbol, 0)
|
||||||
|
pending_orders_after_cancel = (
|
||||||
|
self.get_pending_orders()
|
||||||
|
) # 再次获取,此时应已取消旧订单
|
||||||
|
|
||||||
|
range_1_ago = None
|
||||||
|
range_7_ago = None
|
||||||
|
|
||||||
|
bar_history = self.get_bar_history()
|
||||||
|
if len(bar_history) > 16:
|
||||||
|
|
||||||
|
# 获取前1根K线 (倒数第二根) 和前7根K线 (队列中最老的一根)
|
||||||
|
bar_1_ago = bar_history[-8]
|
||||||
|
bar_7_ago = bar_history[-15]
|
||||||
|
|
||||||
|
# 计算历史 K 线的 Range
|
||||||
|
range_1_ago = bar_1_ago.high - bar_1_ago.low
|
||||||
|
range_7_ago = bar_7_ago.high - bar_7_ago.low
|
||||||
|
|
||||||
|
# for i in range(1, 9, 1):
|
||||||
|
# print(bar_history[-i].datetime)
|
||||||
|
|
||||||
|
# --- 3. 平仓逻辑 (止损/止盈) ---
|
||||||
|
# 只有当有持仓时才考虑平仓
|
||||||
|
if (
|
||||||
|
current_pos_volume > 0 and range_1_ago is not None
|
||||||
|
): # 假设只做多,所以持仓量 > 0
|
||||||
|
avg_entry_price = self.get_average_position_price(self.symbol)
|
||||||
|
if avg_entry_price is not None:
|
||||||
|
pnl_per_unit = (
|
||||||
|
bar.open - avg_entry_price
|
||||||
|
) # 当前浮动盈亏(以收盘价计算)
|
||||||
|
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 止盈信号 - PnL per unit: {pnl_per_unit:.2f}, 目标: {self.take_profit_points:.2f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 止盈条件
|
||||||
|
if pnl_per_unit >= range_1_ago * self.profit_factor:
|
||||||
|
|
||||||
|
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||||
|
self.order_id_counter += 1
|
||||||
|
|
||||||
|
# 创建一个限价多单
|
||||||
|
order = Order(
|
||||||
|
id=order_id,
|
||||||
|
symbol=self.symbol,
|
||||||
|
direction="CLOSE_LONG",
|
||||||
|
volume=trade_volume,
|
||||||
|
price_type="MARKET",
|
||||||
|
# limit_price=limit_price,
|
||||||
|
submitted_time=bar.datetime,
|
||||||
|
offset="CLOSE",
|
||||||
|
)
|
||||||
|
trade = self.send_order(order)
|
||||||
|
return # 平仓后本K线不再进行开仓判断
|
||||||
|
|
||||||
|
# 止损条件
|
||||||
|
elif pnl_per_unit <= -self.stop_loss_points:
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 止损信号 - PnL per unit: {pnl_per_unit:.2f}, 目标: {-self.stop_loss_points:.2f}"
|
||||||
|
)
|
||||||
|
# 发送市价卖出订单平仓,确保立即成交
|
||||||
|
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||||
|
self.order_id_counter += 1
|
||||||
|
|
||||||
|
# 创建一个限价多单
|
||||||
|
order = Order(
|
||||||
|
id=order_id,
|
||||||
|
symbol=self.symbol,
|
||||||
|
direction="CLOSE_LONG",
|
||||||
|
volume=trade_volume,
|
||||||
|
price_type="MARKET",
|
||||||
|
# limit_price=limit_price,
|
||||||
|
submitted_time=bar.datetime,
|
||||||
|
offset="CLOSE",
|
||||||
|
)
|
||||||
|
trade = self.send_order(order)
|
||||||
|
return # 平仓后本K线不再进行开仓判断
|
||||||
|
|
||||||
|
# --- 4. 开仓逻辑 (只考虑做多 BUY 方向) ---
|
||||||
|
# 只有在没有持仓 (current_pos_volume == 0) 且没有待处理订单 (not pending_orders_after_cancel)
|
||||||
|
# 且K线历史足够长时才考虑开仓
|
||||||
|
|
||||||
|
# rsi = RSI(5).get_latest_value(np.array(self.get_price_history('close')), None, None, None, None)
|
||||||
|
indicator_value = HistoricalRange(21).get_latest_value(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
np.array(self.get_price_history("high")),
|
||||||
|
np.array(self.get_price_history("low")),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
current_pos_volume == 0
|
||||||
|
and range_1_ago is not None
|
||||||
|
and (not self.use_indicator or 10 < indicator_value < 25)
|
||||||
|
):
|
||||||
|
# if current_pos_volume == 0 and range_1_ago is not None:
|
||||||
|
|
||||||
|
# 根据策略逻辑计算目标买入价格
|
||||||
|
# 目标买入价 = 当前K线Open - (前1根Range * 因子1 + 前7根Range * 因子2)
|
||||||
|
self.log(bar.open, range_1_ago * self.range_factor)
|
||||||
|
target_buy_price = bar.open - (range_1_ago * self.range_factor)
|
||||||
|
|
||||||
|
# 确保目标买入价格有效,例如不能是负数
|
||||||
|
target_buy_price = max(0.01, target_buy_price)
|
||||||
|
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 开多仓信号 - 当前Open={bar.open:.2f}, "
|
||||||
|
f"前1Range={range_1_ago:.2f}, "
|
||||||
|
f"计算目标买入价={target_buy_price:.2f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||||
|
self.order_id_counter += 1
|
||||||
|
|
||||||
|
# 创建一个限价多单
|
||||||
|
order = Order(
|
||||||
|
id=order_id,
|
||||||
|
symbol=self.symbol,
|
||||||
|
direction="BUY",
|
||||||
|
volume=trade_volume,
|
||||||
|
price_type="LIMIT",
|
||||||
|
limit_price=target_buy_price,
|
||||||
|
submitted_time=bar.datetime,
|
||||||
|
)
|
||||||
|
new_order = self.send_order(order)
|
||||||
|
# 记录下这个订单的ID,以便在下一根K线开始时进行撤销
|
||||||
|
if new_order:
|
||||||
|
self._last_order_id = new_order.id
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 策略: 发送限价买入订单 {self._last_order_id} @ {target_buy_price:.2f}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.log(f"[{current_datetime}] 策略: 发送订单失败。")
|
||||||
|
|
||||||
|
# else:
|
||||||
|
# self.log(f"[{current_datetime}] 不满足开仓条件:持仓={current_pos_volume}, 待处理订单={len(pending_orders_after_cancel)}, K线历史长度={len(bar_history)}")
|
||||||
|
|
||||||
|
def on_close_bar(self, bar: Bar, next_bar_open: Optional[float] = None):
|
||||||
|
self.cancel_all_pending_orders()
|
||||||
|
|
||||||
|
def on_rollover(self, old_symbol: str, new_symbol: str):
|
||||||
|
"""
|
||||||
|
在合约换月时清空历史K线数据和上次订单ID,避免使用旧合约数据进行计算。
|
||||||
|
"""
|
||||||
|
super().on_rollover(old_symbol, new_symbol) # 调用基类方法打印日志
|
||||||
|
self._last_order_id = None # 清空上次订单ID,因为旧合约订单已取消
|
||||||
|
|
||||||
|
self.log(f"换月完成,清空历史K线数据和上次订单ID,准备新合约交易。")
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleLimitBuyStrategyShort(Strategy):
|
||||||
|
"""
|
||||||
|
一个基于当前K线Open、前1根和前7根K线Range计算优势价格进行限价买入的策略。
|
||||||
|
具备以下特点:
|
||||||
|
- 每根K线开始时取消上一根K线未成交的订单。
|
||||||
|
- 最多只能有一个开仓挂单和一个持仓。
|
||||||
|
- 包含简单的止损和止盈逻辑。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
context: Any,
|
||||||
|
symbol: str,
|
||||||
|
enable_log: bool,
|
||||||
|
trade_volume: int,
|
||||||
|
range_factor: float,
|
||||||
|
profit_factor: float,
|
||||||
|
max_position: int,
|
||||||
|
stop_loss_points: float = 10, # 新增:止损点数
|
||||||
|
take_profit_points: float = 10,
|
||||||
|
use_indicator: bool = False,
|
||||||
|
): # 新增:止盈点数
|
||||||
|
"""
|
||||||
|
初始化策略。
|
||||||
|
Args:
|
||||||
|
context: 模拟器实例。
|
||||||
|
symbol (str): 交易合约代码。
|
||||||
|
trade_volume (int): 单笔交易量。
|
||||||
|
range_factor (float): 前1根K线Range的权重因子,用于从Open价向下偏移。
|
||||||
|
profit_factor (float): 前7根K线Range的权重因子,用于从Open价向下偏移。
|
||||||
|
max_position (int): 最大持仓量(此处为1,因为只允许一个持仓)。
|
||||||
|
stop_loss_points (float): 止损点数(例如,亏损达到此点数则止损)。
|
||||||
|
take_profit_points (float): 止盈点数(例如,盈利达到此点数则止盈)。
|
||||||
|
"""
|
||||||
|
super().__init__(context, symbol, enable_log)
|
||||||
|
self.trade_volume = trade_volume
|
||||||
|
self.range_factor = range_factor
|
||||||
|
self.profit_factor = profit_factor
|
||||||
|
self.max_position = max_position # 理论上这里应为1
|
||||||
|
self.stop_loss_points = stop_loss_points
|
||||||
|
self.take_profit_points = take_profit_points
|
||||||
|
self.use_indicator = use_indicator
|
||||||
|
|
||||||
|
self.order_id_counter = 0
|
||||||
|
|
||||||
|
self._last_order_id: Optional[str] = None # 用于跟踪上一根K线发出的订单ID
|
||||||
|
|
||||||
|
self.log(
|
||||||
|
f"策略初始化: symbol={self.symbol}, trade_volume={self.trade_volume}, "
|
||||||
|
f"range_factor={self.range_factor}, "
|
||||||
|
f"profit_factor={self.profit_factor}, "
|
||||||
|
f"max_position={self.max_position}, "
|
||||||
|
f"止损点={self.stop_loss_points}, 止盈点={self.take_profit_points}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_open_bar(self, bar: Bar, next_bar_open: Optional[float] = None):
|
||||||
|
"""
|
||||||
|
每当新的K线数据到来时调用。
|
||||||
|
Args:
|
||||||
|
bar (Bar): 当前的K线数据对象。
|
||||||
|
next_bar_open (Optional[float]): 下一根K线的开盘价,此处策略未使用。
|
||||||
|
"""
|
||||||
|
current_datetime = bar.datetime # 获取当前K线时间
|
||||||
|
self.symbol = bar.symbol
|
||||||
|
|
||||||
|
# --- 1. 撤销上一根K线未成交的订单 ---
|
||||||
|
# 检查是否记录了上一笔订单ID,并且该订单仍然在待处理列表中
|
||||||
|
if self._last_order_id:
|
||||||
|
pending_orders = self.get_pending_orders()
|
||||||
|
if self._last_order_id in pending_orders:
|
||||||
|
success = self.cancel_order(
|
||||||
|
self._last_order_id
|
||||||
|
) # 直接调用基类的取消方法
|
||||||
|
if success:
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 策略: 成功撤销上一根K线未成交订单 {self._last_order_id}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 策略: 尝试撤销订单 {self._last_order_id} 失败(可能已成交或不存在)"
|
||||||
|
)
|
||||||
|
# 无论撤销成功与否,既然我们尝试了撤销,就清除记录
|
||||||
|
self._last_order_id = None
|
||||||
|
# else:
|
||||||
|
# self.log(f"[{current_datetime}] 策略: 无上一根K线未成交订单需要撤销。")
|
||||||
|
|
||||||
|
# 2. 更新K线历史
|
||||||
|
trade_volume = self.trade_volume
|
||||||
|
|
||||||
|
# 获取当前持仓和未决订单(在取消之后获取,确保是最新的状态)
|
||||||
|
current_positions = self.get_current_positions()
|
||||||
|
current_pos_volume = current_positions.get(self.symbol, 0)
|
||||||
|
pending_orders_after_cancel = (
|
||||||
|
self.get_pending_orders()
|
||||||
|
) # 再次获取,此时应已取消旧订单
|
||||||
|
|
||||||
|
range_1_ago = None
|
||||||
|
range_7_ago = None
|
||||||
|
|
||||||
|
bar_history = self.get_bar_history()
|
||||||
|
if len(bar_history) > 16:
|
||||||
|
|
||||||
|
# 获取前1根K线 (倒数第二根) 和前7根K线 (队列中最老的一根)
|
||||||
|
bar_1_ago = bar_history[-8]
|
||||||
|
bar_7_ago = bar_history[-15]
|
||||||
|
|
||||||
|
# 计算历史 K 线的 Range
|
||||||
|
range_1_ago = bar_1_ago.high - bar_1_ago.low
|
||||||
|
range_7_ago = bar_7_ago.high - bar_7_ago.low
|
||||||
|
|
||||||
|
# --- 3. 平仓逻辑 (止损/止盈) ---
|
||||||
|
# 只有当有持仓时才考虑平仓
|
||||||
|
if (
|
||||||
|
current_pos_volume < 0 and range_1_ago is not None
|
||||||
|
): # 假设只做多,所以持仓量 > 0
|
||||||
|
avg_entry_price = self.get_average_position_price(self.symbol)
|
||||||
|
if avg_entry_price is not None:
|
||||||
|
pnl_per_unit = (
|
||||||
|
avg_entry_price - bar.open
|
||||||
|
) # 当前浮动盈亏(以收盘价计算)
|
||||||
|
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 止盈信号 - PnL per unit: {pnl_per_unit:.2f}, 目标: {self.take_profit_points:.2f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 止盈条件
|
||||||
|
if pnl_per_unit >= range_1_ago * self.profit_factor:
|
||||||
|
|
||||||
|
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||||
|
self.order_id_counter += 1
|
||||||
|
|
||||||
|
# 创建一个限价多单
|
||||||
|
order = Order(
|
||||||
|
id=order_id,
|
||||||
|
symbol=self.symbol,
|
||||||
|
direction="CLOSE_SHORT",
|
||||||
|
volume=trade_volume,
|
||||||
|
price_type="MARKET",
|
||||||
|
# limit_price=limit_price,
|
||||||
|
submitted_time=bar.datetime,
|
||||||
|
offset="CLOSE",
|
||||||
|
)
|
||||||
|
trade = self.send_order(order)
|
||||||
|
return # 平仓后本K线不再进行开仓判断
|
||||||
|
|
||||||
|
# 止损条件
|
||||||
|
elif pnl_per_unit <= -self.stop_loss_points:
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 止损信号 - PnL per unit: {pnl_per_unit:.2f}, 目标: {-self.stop_loss_points:.2f}"
|
||||||
|
)
|
||||||
|
# 发送市价卖出订单平仓,确保立即成交
|
||||||
|
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||||
|
self.order_id_counter += 1
|
||||||
|
|
||||||
|
# 创建一个限价多单
|
||||||
|
order = Order(
|
||||||
|
id=order_id,
|
||||||
|
symbol=self.symbol,
|
||||||
|
direction="CLOSE_SHORT",
|
||||||
|
volume=trade_volume,
|
||||||
|
price_type="MARKET",
|
||||||
|
# limit_price=limit_price,
|
||||||
|
submitted_time=bar.datetime,
|
||||||
|
offset="CLOSE",
|
||||||
|
)
|
||||||
|
trade = self.send_order(order)
|
||||||
|
return # 平仓后本K线不再进行开仓判断
|
||||||
|
|
||||||
|
# --- 4. 开仓逻辑 (只考虑做多 BUY 方向) ---
|
||||||
|
# 只有在没有持仓 (current_pos_volume == 0) 且没有待处理订单 (not pending_orders_after_cancel)
|
||||||
|
# 且K线历史足够长时才考虑开仓
|
||||||
|
# rsi = RSI(5).get_latest_value(np.array(self.get_price_history('close')), None, None, None, None)
|
||||||
|
indicator_value = RSI(5).get_latest_value(np.array(self.get_price_history('close')), None, None, None, None)
|
||||||
|
if (
|
||||||
|
current_pos_volume == 0
|
||||||
|
and range_1_ago is not None
|
||||||
|
and (not self.use_indicator or 20 < indicator_value < 60)
|
||||||
|
):
|
||||||
|
|
||||||
|
# 根据策略逻辑计算目标买入价格
|
||||||
|
# 目标买入价 = 当前K线Open - (前1根Range * 因子1 + 前7根Range * 因子2)
|
||||||
|
self.log(bar.open, range_1_ago * self.range_factor)
|
||||||
|
target_buy_price = bar.open + (range_1_ago * self.range_factor)
|
||||||
|
|
||||||
|
# 确保目标买入价格有效,例如不能是负数
|
||||||
|
target_buy_price = max(0.01, target_buy_price)
|
||||||
|
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 开多仓信号 - 当前Open={bar.open:.2f}, "
|
||||||
|
f"前1Range={range_1_ago:.2f}, "
|
||||||
|
f"计算目标买入价={target_buy_price:.2f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||||
|
self.order_id_counter += 1
|
||||||
|
|
||||||
|
# 创建一个限价多单
|
||||||
|
order = Order(
|
||||||
|
id=order_id,
|
||||||
|
symbol=self.symbol,
|
||||||
|
direction="SELL",
|
||||||
|
volume=trade_volume,
|
||||||
|
price_type="LIMIT",
|
||||||
|
limit_price=target_buy_price,
|
||||||
|
submitted_time=bar.datetime,
|
||||||
|
)
|
||||||
|
new_order = self.send_order(order)
|
||||||
|
# 记录下这个订单的ID,以便在下一根K线开始时进行撤销
|
||||||
|
if new_order:
|
||||||
|
self._last_order_id = new_order.id
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 策略: 发送限价买入订单 {self._last_order_id} @ {target_buy_price:.2f}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.log(f"[{current_datetime}] 策略: 发送订单失败。")
|
||||||
|
|
||||||
|
# else:
|
||||||
|
# self.log(f"[{current_datetime}] 不满足开仓条件:持仓={current_pos_volume}, 待处理订单={len(pending_orders_after_cancel)}, K线历史长度={len(bar_history)}")
|
||||||
|
|
||||||
|
def on_close_bar(self, bar: Bar, next_bar_open: Optional[float] = None):
|
||||||
|
self.cancel_all_pending_orders()
|
||||||
|
|
||||||
|
def on_rollover(self, old_symbol: str, new_symbol: str):
|
||||||
|
"""
|
||||||
|
在合约换月时清空历史K线数据和上次订单ID,避免使用旧合约数据进行计算。
|
||||||
|
"""
|
||||||
|
super().on_rollover(old_symbol, new_symbol) # 调用基类方法打印日志
|
||||||
|
self._last_order_id = None # 清空上次订单ID,因为旧合约订单已取消
|
||||||
|
|
||||||
|
self.log(f"换月完成,清空历史K线数据和上次订单ID,准备新合约交易。")
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleLimitBuyStrategy(Strategy):
|
||||||
|
"""
|
||||||
|
一个基于当前K线Open、前1根和前7根K线Range计算优势价格进行限价买入的策略。
|
||||||
|
具备以下特点:
|
||||||
|
- 每根K线开始时取消上一根K线未成交的订单。
|
||||||
|
- 最多只能有一个开仓挂单和一个持仓。
|
||||||
|
- 包含简单的止损和止盈逻辑。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
context: Any,
|
||||||
|
symbol: str,
|
||||||
|
enable_log: bool,
|
||||||
|
trade_volume: int,
|
||||||
|
range_factor_l: float,
|
||||||
|
profit_factor_l: float,
|
||||||
|
range_factor_s: float,
|
||||||
|
profit_factor_s: float,
|
||||||
|
max_position: int,
|
||||||
|
stop_loss_points: float = 10, # 新增:止损点数
|
||||||
|
take_profit_points: float = 10,
|
||||||
|
use_indicator: bool = False,
|
||||||
|
): # 新增:止盈点数
|
||||||
|
"""
|
||||||
|
初始化策略。
|
||||||
|
Args:
|
||||||
|
context: 模拟器实例。
|
||||||
|
symbol (str): 交易合约代码。
|
||||||
|
trade_volume (int): 单笔交易量。
|
||||||
|
range_factor (float): 前1根K线Range的权重因子,用于从Open价向下偏移。
|
||||||
|
profit_factor (float): 前7根K线Range的权重因子,用于从Open价向下偏移。
|
||||||
|
max_position (int): 最大持仓量(此处为1,因为只允许一个持仓)。
|
||||||
|
stop_loss_points (float): 止损点数(例如,亏损达到此点数则止损)。
|
||||||
|
take_profit_points (float): 止盈点数(例如,盈利达到此点数则止盈)。
|
||||||
|
"""
|
||||||
|
super().__init__(context, symbol, enable_log)
|
||||||
|
self.trade_volume = trade_volume
|
||||||
|
self.range_factor_l = range_factor_l
|
||||||
|
self.profit_factor_l = profit_factor_l
|
||||||
|
self.range_factor_s = range_factor_s
|
||||||
|
self.profit_factor_s = profit_factor_s
|
||||||
|
self.max_position = max_position # 理论上这里应为1
|
||||||
|
self.stop_loss_points = stop_loss_points
|
||||||
|
self.take_profit_points = take_profit_points
|
||||||
|
self.use_indicator = use_indicator
|
||||||
|
|
||||||
|
self.order_id_counter = 0
|
||||||
|
|
||||||
|
self._last_order_id: Optional[str] = None # 用于跟踪上一根K线发出的订单ID
|
||||||
|
|
||||||
|
self.log(
|
||||||
|
f"策略初始化: symbol={self.symbol}, trade_volume={self.trade_volume}, "
|
||||||
|
f"range_factor_l={self.range_factor_l}, "
|
||||||
|
f"profit_factor_l={self.profit_factor_l}, "
|
||||||
|
f"range_factor_s={self.range_factor_s}, "
|
||||||
|
f"profit_factor_s={self.profit_factor_s}, "
|
||||||
|
f"max_position={self.max_position}, "
|
||||||
|
f"止损点={self.stop_loss_points}, 止盈点={self.take_profit_points}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_open_bar(self, bar: Bar, next_bar_open: Optional[float] = None):
|
||||||
|
"""
|
||||||
|
每当新的K线数据到来时调用。
|
||||||
|
Args:
|
||||||
|
bar (Bar): 当前的K线数据对象。
|
||||||
|
next_bar_open (Optional[float]): 下一根K线的开盘价,此处策略未使用。
|
||||||
|
"""
|
||||||
|
current_datetime = bar.datetime # 获取当前K线时间
|
||||||
|
self.symbol = bar.symbol
|
||||||
|
|
||||||
|
# --- 1. 撤销上一根K线未成交的订单 ---
|
||||||
|
# 检查是否记录了上一笔订单ID,并且该订单仍然在待处理列表中
|
||||||
|
if self._last_order_id:
|
||||||
|
pending_orders = self.get_pending_orders()
|
||||||
|
if self._last_order_id in pending_orders:
|
||||||
|
success = self.cancel_order(
|
||||||
|
self._last_order_id
|
||||||
|
) # 直接调用基类的取消方法
|
||||||
|
if success:
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 策略: 成功撤销上一根K线未成交订单 {self._last_order_id}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 策略: 尝试撤销订单 {self._last_order_id} 失败(可能已成交或不存在)"
|
||||||
|
)
|
||||||
|
# 无论撤销成功与否,既然我们尝试了撤销,就清除记录
|
||||||
|
self._last_order_id = None
|
||||||
|
# else:
|
||||||
|
# self.log(f"[{current_datetime}] 策略: 无上一根K线未成交订单需要撤销。")
|
||||||
|
|
||||||
|
# 2. 更新K线历史
|
||||||
|
trade_volume = self.trade_volume
|
||||||
|
|
||||||
|
# 获取当前持仓和未决订单(在取消之后获取,确保是最新的状态)
|
||||||
|
current_positions = self.get_current_positions()
|
||||||
|
current_pos_volume = current_positions.get(self.symbol, 0)
|
||||||
|
pending_orders_after_cancel = (
|
||||||
|
self.get_pending_orders()
|
||||||
|
) # 再次获取,此时应已取消旧订单
|
||||||
|
|
||||||
|
range_1_ago = None
|
||||||
|
range_7_ago = None
|
||||||
|
|
||||||
|
bar_history = self.get_bar_history()
|
||||||
|
if len(bar_history) > 16:
|
||||||
|
|
||||||
|
# 获取前1根K线 (倒数第二根) 和前7根K线 (队列中最老的一根)
|
||||||
|
bar_1_ago = bar_history[-8]
|
||||||
|
bar_7_ago = bar_history[-15]
|
||||||
|
|
||||||
|
# 计算历史 K 线的 Range
|
||||||
|
range_1_ago = bar_1_ago.high - bar_1_ago.low
|
||||||
|
range_7_ago = bar_7_ago.high - bar_7_ago.low
|
||||||
|
|
||||||
|
# --- 3. 平仓逻辑 (止损/止盈) ---
|
||||||
|
# 只有当有持仓时才考虑平仓
|
||||||
|
if (
|
||||||
|
current_pos_volume < 0 and range_1_ago is not None
|
||||||
|
): # 假设只做多,所以持仓量 > 0
|
||||||
|
avg_entry_price = self.get_average_position_price(self.symbol)
|
||||||
|
if avg_entry_price is not None:
|
||||||
|
pnl_per_unit = (
|
||||||
|
avg_entry_price - bar.open
|
||||||
|
) # 当前浮动盈亏(以收盘价计算)
|
||||||
|
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 止盈信号 - PnL per unit: {pnl_per_unit:.2f}, 目标: {self.take_profit_points:.2f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 止盈条件
|
||||||
|
if pnl_per_unit >= range_1_ago * self.profit_factor_s:
|
||||||
|
|
||||||
|
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||||
|
self.order_id_counter += 1
|
||||||
|
|
||||||
|
# 创建一个限价多单
|
||||||
|
order = Order(
|
||||||
|
id=order_id,
|
||||||
|
symbol=self.symbol,
|
||||||
|
direction="CLOSE_SHORT",
|
||||||
|
volume=trade_volume,
|
||||||
|
price_type="MARKET",
|
||||||
|
# limit_price=limit_price,
|
||||||
|
submitted_time=bar.datetime,
|
||||||
|
offset="CLOSE",
|
||||||
|
)
|
||||||
|
trade = self.send_order(order)
|
||||||
|
return # 平仓后本K线不再进行开仓判断
|
||||||
|
|
||||||
|
# 止损条件
|
||||||
|
elif pnl_per_unit <= -self.stop_loss_points:
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 止损信号 - PnL per unit: {pnl_per_unit:.2f}, 目标: {-self.stop_loss_points:.2f}"
|
||||||
|
)
|
||||||
|
# 发送市价卖出订单平仓,确保立即成交
|
||||||
|
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||||
|
self.order_id_counter += 1
|
||||||
|
|
||||||
|
# 创建一个限价多单
|
||||||
|
order = Order(
|
||||||
|
id=order_id,
|
||||||
|
symbol=self.symbol,
|
||||||
|
direction="CLOSE_SHORT",
|
||||||
|
volume=trade_volume,
|
||||||
|
price_type="MARKET",
|
||||||
|
# limit_price=limit_price,
|
||||||
|
submitted_time=bar.datetime,
|
||||||
|
offset="CLOSE",
|
||||||
|
)
|
||||||
|
trade = self.send_order(order)
|
||||||
|
return # 平仓后本K线不再进行开仓判断
|
||||||
|
|
||||||
|
if (
|
||||||
|
current_pos_volume > 0 and range_1_ago is not None
|
||||||
|
): # 假设只做多,所以持仓量 > 0
|
||||||
|
avg_entry_price = self.get_average_position_price(self.symbol)
|
||||||
|
if avg_entry_price is not None:
|
||||||
|
pnl_per_unit = (
|
||||||
|
bar.open - avg_entry_price
|
||||||
|
) # 当前浮动盈亏(以收盘价计算)
|
||||||
|
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 止盈信号 - PnL per unit: {pnl_per_unit:.2f}, 目标: {self.take_profit_points:.2f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 止盈条件
|
||||||
|
if pnl_per_unit >= range_1_ago * self.profit_factor_l:
|
||||||
|
|
||||||
|
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||||
|
self.order_id_counter += 1
|
||||||
|
|
||||||
|
# 创建一个限价多单
|
||||||
|
order = Order(
|
||||||
|
id=order_id,
|
||||||
|
symbol=self.symbol,
|
||||||
|
direction="CLOSE_LONG",
|
||||||
|
volume=trade_volume,
|
||||||
|
price_type="MARKET",
|
||||||
|
# limit_price=limit_price,
|
||||||
|
submitted_time=bar.datetime,
|
||||||
|
offset="CLOSE",
|
||||||
|
)
|
||||||
|
trade = self.send_order(order)
|
||||||
|
return # 平仓后本K线不再进行开仓判断
|
||||||
|
|
||||||
|
# 止损条件
|
||||||
|
elif pnl_per_unit <= -self.stop_loss_points:
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 止损信号 - PnL per unit: {pnl_per_unit:.2f}, 目标: {-self.stop_loss_points:.2f}"
|
||||||
|
)
|
||||||
|
# 发送市价卖出订单平仓,确保立即成交
|
||||||
|
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||||
|
self.order_id_counter += 1
|
||||||
|
|
||||||
|
# 创建一个限价多单
|
||||||
|
order = Order(
|
||||||
|
id=order_id,
|
||||||
|
symbol=self.symbol,
|
||||||
|
direction="CLOSE_LONG",
|
||||||
|
volume=trade_volume,
|
||||||
|
price_type="MARKET",
|
||||||
|
# limit_price=limit_price,
|
||||||
|
submitted_time=bar.datetime,
|
||||||
|
offset="CLOSE",
|
||||||
|
)
|
||||||
|
trade = self.send_order(order)
|
||||||
|
return # 平仓后本K线不再进行开仓判断
|
||||||
|
|
||||||
|
# --- 4. 开仓逻辑 (只考虑做多 BUY 方向) ---
|
||||||
|
# 只有在没有持仓 (current_pos_volume == 0) 且没有待处理订单 (not pending_orders_after_cancel)
|
||||||
|
# 且K线历史足够长时才考虑开仓
|
||||||
|
|
||||||
|
if (
|
||||||
|
current_pos_volume == 0
|
||||||
|
and range_1_ago is not None
|
||||||
|
):
|
||||||
|
|
||||||
|
indicator_value = HistoricalRange(21).get_latest_value(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
np.array(self.get_price_history("high")),
|
||||||
|
np.array(self.get_price_history("low")),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if (not self.use_indicator or 10 < indicator_value < 25):
|
||||||
|
# 根据策略逻辑计算目标买入价格
|
||||||
|
# 目标买入价 = 当前K线Open - (前1根Range * 因子1 + 前7根Range * 因子2)
|
||||||
|
self.log(bar.open, range_1_ago * self.range_factor_l)
|
||||||
|
target_buy_price = bar.open - (range_1_ago * self.range_factor_l)
|
||||||
|
|
||||||
|
# 确保目标买入价格有效,例如不能是负数
|
||||||
|
target_buy_price = max(0.01, target_buy_price)
|
||||||
|
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 开多仓信号 - 当前Open={bar.open:.2f}, "
|
||||||
|
f"前1Range={range_1_ago:.2f}, "
|
||||||
|
f"计算目标买入价={target_buy_price:.2f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||||
|
self.order_id_counter += 1
|
||||||
|
|
||||||
|
# 创建一个限价多单
|
||||||
|
order = Order(
|
||||||
|
id=order_id,
|
||||||
|
symbol=self.symbol,
|
||||||
|
direction="BUY",
|
||||||
|
volume=trade_volume,
|
||||||
|
price_type="LIMIT",
|
||||||
|
limit_price=target_buy_price,
|
||||||
|
submitted_time=bar.datetime,
|
||||||
|
)
|
||||||
|
new_order = self.send_order(order)
|
||||||
|
# 记录下这个订单的ID,以便在下一根K线开始时进行撤销
|
||||||
|
if new_order:
|
||||||
|
self._last_order_id = new_order.id
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 策略: 发送限价买入订单 {self._last_order_id} @ {target_buy_price:.2f}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.log(f"[{current_datetime}] 策略: 发送订单失败。")
|
||||||
|
|
||||||
|
|
||||||
|
indicator_value = RSI(5).get_latest_value(np.array(self.get_price_history('close')), None, None, None, None)
|
||||||
|
if (not self.use_indicator or 20 < indicator_value < 60):
|
||||||
|
# 根据策略逻辑计算目标买入价格
|
||||||
|
# 目标买入价 = 当前K线Open - (前1根Range * 因子1 + 前7根Range * 因子2)
|
||||||
|
self.log(bar.open, range_1_ago * self.range_factor_s)
|
||||||
|
target_buy_price = bar.open + (range_1_ago * self.range_factor_s)
|
||||||
|
|
||||||
|
# 确保目标买入价格有效,例如不能是负数
|
||||||
|
target_buy_price = max(0.01, target_buy_price)
|
||||||
|
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 开多仓信号 - 当前Open={bar.open:.2f}, "
|
||||||
|
f"前1Range={range_1_ago:.2f}, "
|
||||||
|
f"计算目标买入价={target_buy_price:.2f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||||
|
self.order_id_counter += 1
|
||||||
|
|
||||||
|
# 创建一个限价多单
|
||||||
|
order = Order(
|
||||||
|
id=order_id,
|
||||||
|
symbol=self.symbol,
|
||||||
|
direction="SELL",
|
||||||
|
volume=trade_volume,
|
||||||
|
price_type="LIMIT",
|
||||||
|
limit_price=target_buy_price,
|
||||||
|
submitted_time=bar.datetime,
|
||||||
|
)
|
||||||
|
new_order = self.send_order(order)
|
||||||
|
# 记录下这个订单的ID,以便在下一根K线开始时进行撤销
|
||||||
|
if new_order:
|
||||||
|
self._last_order_id = new_order.id
|
||||||
|
self.log(
|
||||||
|
f"[{current_datetime}] 策略: 发送限价买入订单 {self._last_order_id} @ {target_buy_price:.2f}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.log(f"[{current_datetime}] 策略: 发送订单失败。")
|
||||||
|
# else:
|
||||||
|
# self.log(f"[{current_datetime}] 不满足开仓条件:持仓={current_pos_volume}, 待处理订单={len(pending_orders_after_cancel)}, K线历史长度={len(bar_history)}")
|
||||||
|
|
||||||
|
def on_close_bar(self, bar: Bar, next_bar_open: Optional[float] = None):
|
||||||
|
self.cancel_all_pending_orders()
|
||||||
|
|
||||||
|
def on_rollover(self, old_symbol: str, new_symbol: str):
|
||||||
|
"""
|
||||||
|
在合约换月时清空历史K线数据和上次订单ID,避免使用旧合约数据进行计算。
|
||||||
|
"""
|
||||||
|
super().on_rollover(old_symbol, new_symbol) # 调用基类方法打印日志
|
||||||
|
self._last_order_id = None # 清空上次订单ID,因为旧合约订单已取消
|
||||||
|
|
||||||
|
self.log(f"换月完成,清空历史K线数据和上次订单ID,准备新合约交易。")
|
||||||
@@ -159,11 +159,11 @@ class SimpleLimitBuyStrategyLong(Strategy):
|
|||||||
# 只有在没有持仓 (current_pos_volume == 0) 且没有待处理订单 (not pending_orders_after_cancel)
|
# 只有在没有持仓 (current_pos_volume == 0) 且没有待处理订单 (not pending_orders_after_cancel)
|
||||||
# 且K线历史足够长时才考虑开仓
|
# 且K线历史足够长时才考虑开仓
|
||||||
bar_history = self.get_bar_history()
|
bar_history = self.get_bar_history()
|
||||||
if current_pos_volume == 0 and len(bar_history) > 10:
|
if current_pos_volume == 0 and len(bar_history) > 16:
|
||||||
|
|
||||||
# 获取前1根K线 (倒数第二根) 和前7根K线 (队列中最老的一根)
|
# 获取前1根K线 (倒数第二根) 和前7根K线 (队列中最老的一根)
|
||||||
bar_1_ago = bar_history[-2]
|
bar_1_ago = bar_history[-8]
|
||||||
bar_7_ago = bar_history[-8]
|
bar_7_ago = bar_history[-15]
|
||||||
|
|
||||||
# 计算历史 K 线的 Range
|
# 计算历史 K 线的 Range
|
||||||
range_1_ago = bar_1_ago.high - bar_1_ago.low
|
range_1_ago = bar_1_ago.high - bar_1_ago.low
|
||||||
@@ -387,11 +387,11 @@ class SimpleLimitBuyStrategyShort(Strategy):
|
|||||||
# 只有在没有持仓 (current_pos_volume == 0) 且没有待处理订单 (not pending_orders_after_cancel)
|
# 只有在没有持仓 (current_pos_volume == 0) 且没有待处理订单 (not pending_orders_after_cancel)
|
||||||
# 且K线历史足够长时才考虑开仓
|
# 且K线历史足够长时才考虑开仓
|
||||||
bar_history = self.get_bar_history()
|
bar_history = self.get_bar_history()
|
||||||
if current_pos_volume == 0 and len(bar_history) > 10:
|
if current_pos_volume == 0 and len(bar_history) > 16:
|
||||||
|
|
||||||
# 获取前1根K线 (倒数第二根) 和前7根K线 (队列中最老的一根)
|
# 获取前1根K线 (倒数第二根) 和前7根K线 (队列中最老的一根)
|
||||||
bar_1_ago = bar_history[-2]
|
bar_1_ago = bar_history[-8]
|
||||||
bar_7_ago = bar_history[-8]
|
bar_7_ago = bar_history[-15]
|
||||||
|
|
||||||
# 计算历史 K 线的 Range
|
# 计算历史 K 线的 Range
|
||||||
range_1_ago = bar_1_ago.high - bar_1_ago.low
|
range_1_ago = bar_1_ago.high - bar_1_ago.low
|
||||||
@@ -660,11 +660,11 @@ class SimpleLimitBuyStrategy(Strategy):
|
|||||||
return # 平仓后本K线不再进行开仓判断
|
return # 平仓后本K线不再进行开仓判断
|
||||||
|
|
||||||
bar_history = self.get_bar_history()
|
bar_history = self.get_bar_history()
|
||||||
if current_pos_volume == 0 and len(bar_history) > 10:
|
if current_pos_volume == 0 and len(bar_history) > 16:
|
||||||
|
|
||||||
# 获取前1根K线 (倒数第二根) 和前7根K线 (队列中最老的一根)
|
# 获取前1根K线 (倒数第二根) 和前7根K线 (队列中最老的一根)
|
||||||
bar_1_ago = bar_history[-2]
|
bar_1_ago = bar_history[-8]
|
||||||
bar_7_ago = bar_history[-8]
|
bar_7_ago = bar_history[-15]
|
||||||
|
|
||||||
print(bar_1_ago, bar_7_ago)
|
print(bar_1_ago, bar_7_ago)
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class Strategy(ABC):
|
|||||||
self.symbol = symbol # 策略操作的合约Symbol
|
self.symbol = symbol # 策略操作的合约Symbol
|
||||||
self.params = params
|
self.params = params
|
||||||
self.enable_log = enable_log
|
self.enable_log = enable_log
|
||||||
|
self.trading = False
|
||||||
|
|
||||||
def on_init(self):
|
def on_init(self):
|
||||||
"""
|
"""
|
||||||
@@ -78,6 +79,9 @@ class Strategy(ABC):
|
|||||||
发送订单的辅助方法。
|
发送订单的辅助方法。
|
||||||
会在 BaseStrategy 内部构建 Order 对象,并通过 context 转发给模拟器。
|
会在 BaseStrategy 内部构建 Order 对象,并通过 context 转发给模拟器。
|
||||||
"""
|
"""
|
||||||
|
if not self.trading:
|
||||||
|
return None
|
||||||
|
|
||||||
if self.context.is_rollover_bar:
|
if self.context.is_rollover_bar:
|
||||||
self.log(f"当前是换月K线,禁止开仓订单")
|
self.log(f"当前是换月K线,禁止开仓订单")
|
||||||
return None
|
return None
|
||||||
@@ -103,16 +107,24 @@ class Strategy(ABC):
|
|||||||
取消指定ID的订单。
|
取消指定ID的订单。
|
||||||
通过 context 调用模拟器的 cancel_order 方法。
|
通过 context 调用模拟器的 cancel_order 方法。
|
||||||
"""
|
"""
|
||||||
|
if not self.trading:
|
||||||
|
return False
|
||||||
|
|
||||||
return self.context.cancel_order(order_id)
|
return self.context.cancel_order(order_id)
|
||||||
|
|
||||||
def cancel_all_pending_orders(self) -> int:
|
def cancel_all_pending_orders(self) -> int:
|
||||||
"""取消当前策略的未决订单,仅限于当前策略关注的Symbol。"""
|
"""取消当前策略的未决订单,仅限于当前策略关注的Symbol。"""
|
||||||
# 注意:在换月模式下,引擎会自动取消旧合约的挂单,这里是策略主动取消
|
# 注意:在换月模式下,引擎会自动取消旧合约的挂单,这里是策略主动取消
|
||||||
|
if not self.trading:
|
||||||
|
return 0
|
||||||
|
|
||||||
pending_orders = self.get_pending_orders()
|
pending_orders = self.get_pending_orders()
|
||||||
cancelled_count = 0
|
cancelled_count = 0
|
||||||
|
# orders_to_cancel = [
|
||||||
|
# order.id for order in pending_orders.values() if order.symbol == self.symbol
|
||||||
|
# ]
|
||||||
orders_to_cancel = [
|
orders_to_cancel = [
|
||||||
order.id for order in pending_orders.values() if order.symbol == self.symbol
|
order.id for order in pending_orders.values()
|
||||||
]
|
]
|
||||||
for order_id in orders_to_cancel:
|
for order_id in orders_to_cancel:
|
||||||
if self.cancel_order(order_id):
|
if self.cancel_order(order_id):
|
||||||
@@ -179,3 +191,7 @@ class Strategy(ABC):
|
|||||||
|
|
||||||
def get_bar_history(self):
|
def get_bar_history(self):
|
||||||
return self.context.get_bar_history()
|
return self.context.get_bar_history()
|
||||||
|
|
||||||
|
|
||||||
|
def get_price_history(self, key: str):
|
||||||
|
return self.context.get_price_history(key)
|
||||||
|
|||||||
@@ -199,4 +199,7 @@ class TqsdkContext:
|
|||||||
return False # 默认返回 False
|
return False # 默认返回 False
|
||||||
|
|
||||||
def get_bar_history(self):
|
def get_bar_history(self):
|
||||||
return self._engine.get_bar_history()
|
return self._engine.get_bar_history()
|
||||||
|
|
||||||
|
def get_price_history(self, key: str):
|
||||||
|
return self._engine.get_price_history(key)
|
||||||
@@ -96,6 +96,12 @@ class TqsdkEngine:
|
|||||||
self.trade_history: List[Trade] = []
|
self.trade_history: List[Trade] = []
|
||||||
self.all_bars: List[Bar] = [] # 收集所有处理过的Bar
|
self.all_bars: List[Bar] = [] # 收集所有处理过的Bar
|
||||||
|
|
||||||
|
self.close_list: List[float] = []
|
||||||
|
self.open_list: List[float] = []
|
||||||
|
self.high_list: List[float] = []
|
||||||
|
self.low_list: List[float] = []
|
||||||
|
self.volume_list: List[float] = []
|
||||||
|
|
||||||
self.last_processed_bar: Optional[Bar] = None
|
self.last_processed_bar: Optional[Bar] = None
|
||||||
self._is_rollover_bar: bool = False # 换月信号
|
self._is_rollover_bar: bool = False # 换月信号
|
||||||
self._last_underlying_symbol = self.symbol # 用于检测主力合约换月
|
self._last_underlying_symbol = self.symbol # 用于检测主力合约换月
|
||||||
@@ -316,6 +322,8 @@ class TqsdkEngine:
|
|||||||
"""
|
"""
|
||||||
print(f"TqsdkEngine: 开始运行回测,从 {self.start_time} 到 {self.end_time}")
|
print(f"TqsdkEngine: 开始运行回测,从 {self.start_time} 到 {self.end_time}")
|
||||||
|
|
||||||
|
self._strategy.trading = True
|
||||||
|
|
||||||
# 初始化策略 (如果策略有 on_init 方法)
|
# 初始化策略 (如果策略有 on_init 方法)
|
||||||
if hasattr(self._strategy, "on_init"):
|
if hasattr(self._strategy, "on_init"):
|
||||||
self._strategy.on_init()
|
self._strategy.on_init()
|
||||||
@@ -416,6 +424,13 @@ class TqsdkEngine:
|
|||||||
self._is_rollover_bar = False
|
self._is_rollover_bar = False
|
||||||
|
|
||||||
self.all_bars.append(current_bar)
|
self.all_bars.append(current_bar)
|
||||||
|
|
||||||
|
self.close_list.append(current_bar.close)
|
||||||
|
self.open_list.append(current_bar.open)
|
||||||
|
self.high_list.append(current_bar.high)
|
||||||
|
self.low_list.append(current_bar.low)
|
||||||
|
self.volume_list.append(current_bar.volume)
|
||||||
|
|
||||||
self.last_processed_bar = current_bar
|
self.last_processed_bar = current_bar
|
||||||
|
|
||||||
|
|
||||||
@@ -441,6 +456,13 @@ class TqsdkEngine:
|
|||||||
close_oi=kline_row.close_oi,
|
close_oi=kline_row.close_oi,
|
||||||
)
|
)
|
||||||
self.all_bars[-1] = current_bar
|
self.all_bars[-1] = current_bar
|
||||||
|
|
||||||
|
self.close_list[-1] = current_bar.close
|
||||||
|
self.open_list[-1] = current_bar.open
|
||||||
|
self.high_list[-1] = current_bar.high
|
||||||
|
self.low_list[-1] = current_bar.low
|
||||||
|
self.volume_list[-1] = current_bar.volume
|
||||||
|
|
||||||
self.last_processed_bar = current_bar
|
self.last_processed_bar = current_bar
|
||||||
|
|
||||||
# 设置当前 Bar 到 Context
|
# 设置当前 Bar 到 Context
|
||||||
@@ -489,3 +511,15 @@ class TqsdkEngine:
|
|||||||
|
|
||||||
def get_bar_history(self):
|
def get_bar_history(self):
|
||||||
return self.all_bars
|
return self.all_bars
|
||||||
|
|
||||||
|
def get_price_history(self, key: str):
|
||||||
|
if key == 'close':
|
||||||
|
return self.close_list
|
||||||
|
elif key == 'open':
|
||||||
|
return self.open_list
|
||||||
|
elif key == 'high':
|
||||||
|
return self.high_list
|
||||||
|
elif key == 'low':
|
||||||
|
return self.low_list
|
||||||
|
elif key == 'volume':
|
||||||
|
return self.volume_list
|
||||||
|
|||||||
205
src/tqsdk_real_context.py
Normal file
205
src/tqsdk_real_context.py
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# filename: tqsdk_context.py
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, Any, Dict, List, Literal, Deque, TYPE_CHECKING
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
# 导入你提供的 core_data 中的类型
|
||||||
|
from src.core_data import Bar, Order, Trade, PortfolioSnapshot # 确保此路径正确,如果core_data不在同级目录,需要调整
|
||||||
|
|
||||||
|
# 导入 Tqsdk 的核心类型
|
||||||
|
import tqsdk
|
||||||
|
from tqsdk import TqApi, TqAccount, tafunc
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
# 使用 TYPE_CHECKING 避免循环导入,只在类型检查时导入 TqsdkEngine
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from src.tqsdk_engine import TqsdkEngine # 假设 TqsdkEngine 在 tqsdk_engine.py 中
|
||||||
|
|
||||||
|
|
||||||
|
class TqsdkContext:
|
||||||
|
"""
|
||||||
|
Tqsdk 回测上下文,适配原有 BacktestContext 接口。
|
||||||
|
策略通过此上下文与 Tqsdk 进行交互。
|
||||||
|
"""
|
||||||
|
def __init__(self, api: TqApi):
|
||||||
|
"""
|
||||||
|
初始化 Tqsdk 回测上下文。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api (TqApi): Tqsdk 的 TqApi 实例。
|
||||||
|
"""
|
||||||
|
self._api = api
|
||||||
|
self._current_bar: Optional[Bar] = None
|
||||||
|
self._engine: Optional['TqsdkEngine'] = None # 添加对引擎的引用,用于访问其状态或触发事件
|
||||||
|
|
||||||
|
# 用于缓存 Tqsdk 的 K 线序列,避免每次都 get_kline_serial
|
||||||
|
self._kline_serial: Dict[str, object] = {}
|
||||||
|
|
||||||
|
# 订单/取消请求队列,TqsdkEngine 会在异步循环中处理它们
|
||||||
|
self.order_queue: Deque[Order] = deque()
|
||||||
|
self.cancel_queue: Deque[str] = deque() # 存储 order_id
|
||||||
|
|
||||||
|
print("TqsdkContext: 初始化完成。")
|
||||||
|
|
||||||
|
def set_current_bar(self, bar: Bar):
|
||||||
|
"""
|
||||||
|
设置当前正在处理的 K 线数据。
|
||||||
|
由 TqsdkEngine 调用。
|
||||||
|
"""
|
||||||
|
self._current_bar = bar
|
||||||
|
|
||||||
|
def get_current_bar(self) -> Optional[Bar]:
|
||||||
|
"""
|
||||||
|
获取当前正在处理的 K 线数据。
|
||||||
|
策略可以通过此方法获取最新 K 线。
|
||||||
|
"""
|
||||||
|
return self._current_bar
|
||||||
|
|
||||||
|
def get_kline_data(self, symbol: str, duration_seconds: int, data_length: int = 10):
|
||||||
|
"""
|
||||||
|
获取指定合约的 K 线数据。
|
||||||
|
返回 Tqsdk 的 DataFrame 格式 K 线序列(TqKLine 对象),可以直接用于计算指标。
|
||||||
|
如果需要转换为你自己的 Bar 对象列表,则需要在此方法内部进行转换。
|
||||||
|
"""
|
||||||
|
if symbol not in self._kline_serial:
|
||||||
|
# 这里的 get_kline_serial 并不是实时获取,而是在 TqApi 启动时就已经加载
|
||||||
|
# 所以在 Context 中直接调用是安全的,TqApi 会返回已加载的数据引用
|
||||||
|
self._kline_serial[symbol] = self._api.get_kline_serial(symbol, duration_seconds, data_length=data_length)
|
||||||
|
return self._kline_serial[symbol]
|
||||||
|
|
||||||
|
def get_current_time(self) -> datetime:
|
||||||
|
"""
|
||||||
|
获取当前模拟时间(Tqsdk 的数据时间)。
|
||||||
|
"""
|
||||||
|
# Tqsdk 的 get_tick_timestamp() 返回微秒时间戳
|
||||||
|
return datetime.now()
|
||||||
|
|
||||||
|
def get_current_positions(self) -> Dict[str, int]:
|
||||||
|
"""
|
||||||
|
获取当前所有持仓。返回 {symbol: quantity} 的字典,quantity 为净持仓量(多头-空头)。
|
||||||
|
"""
|
||||||
|
tq_positions: Dict[str] = self._api.get_position()
|
||||||
|
converted_positions: Dict[str, int] = {}
|
||||||
|
for symbol, pos in tq_positions.items():
|
||||||
|
net_pos = pos.pos_long - pos.pos_short
|
||||||
|
if net_pos != 0:
|
||||||
|
converted_positions[symbol] = net_pos
|
||||||
|
return converted_positions
|
||||||
|
|
||||||
|
def get_pending_orders(self) -> Dict[str, Order]:
|
||||||
|
"""
|
||||||
|
获取当前所有待处理(未成交)订单。
|
||||||
|
返回 {order_id: Order} 的字典。
|
||||||
|
"""
|
||||||
|
tq_orders: Dict[str] = self._api.get_order()
|
||||||
|
pending_orders: Dict[str, Order] = {}
|
||||||
|
for order_id, tq_order in tq_orders.items():
|
||||||
|
if tq_order.status == "ALIVE": # 正在进行中的订单
|
||||||
|
# 将 TqOrder 转换为你自己的 core_data.Order 类型
|
||||||
|
# 注意:core_data.Order 的 direction 有 "CLOSE_LONG", "CLOSE_SHORT" 等,需要映射
|
||||||
|
# Tqsdk 的 direction 只有 "BUY", "SELL"
|
||||||
|
# Tqsdk 的 offset 决定了是开仓还是平仓
|
||||||
|
core_direction: Literal["BUY", "SELL", "CLOSE_LONG", "CLOSE_SHORT"]
|
||||||
|
if tq_order.offset == "OPEN":
|
||||||
|
core_direction = tq_order.direction # 开仓时方向直接对应买卖
|
||||||
|
elif tq_order.offset in ["CLOSE", "CLOSETODAY", "CLOSEYESTERDAY"]:
|
||||||
|
# 平仓时,买入平空,卖出平多
|
||||||
|
core_direction = "CLOSE_SHORT" if tq_order.direction == "BUY" else "CLOSE_LONG"
|
||||||
|
else: # 默认为 BUY/SELL
|
||||||
|
core_direction = tq_order.direction
|
||||||
|
|
||||||
|
|
||||||
|
converted_order = Order(
|
||||||
|
id=tq_order.order_id, # 将 Tqsdk 的 order_id 赋值给你的 Order 类的 id
|
||||||
|
symbol=tq_order.exchange_id + "." + tq_order.instrument_id, # 例如 "SHFE.rb2401"
|
||||||
|
direction=core_direction,
|
||||||
|
volume=tq_order.volume_orign,
|
||||||
|
price_type="LIMIT" if tq_order.limit_price is not None else "MARKET", # Tqsdk 市价单类型为 "ANY"
|
||||||
|
limit_price=tq_order.limit_price,
|
||||||
|
offset=tq_order.offset, # Tqsdk 原生 offset
|
||||||
|
# order_id=tq_order.order_id, # 存储 Tqsdk 的 order_id
|
||||||
|
submitted_time=pd.to_datetime(tq_order.insert_date_time, unit="ns", utc=True),
|
||||||
|
# status=tq_order.status # 保持 Tqsdk 的状态字符串
|
||||||
|
)
|
||||||
|
pending_orders[order_id] = converted_order
|
||||||
|
return pending_orders
|
||||||
|
|
||||||
|
def get_account_cash(self) -> float:
|
||||||
|
"""
|
||||||
|
获取当前可用现金。
|
||||||
|
"""
|
||||||
|
account: TqAccount = self._api.get_account()
|
||||||
|
return account.available_cash if account else 0.0
|
||||||
|
|
||||||
|
def get_average_position_price(self, symbol: str) -> Optional[float]:
|
||||||
|
"""
|
||||||
|
获取指定合约的平均持仓成本。
|
||||||
|
注意: Tqsdk 的 TqPosition 对象中包含了 open_price_long 和 open_price_short。
|
||||||
|
这里需要根据多头或空头持仓返回对应的平均成本。
|
||||||
|
"""
|
||||||
|
position = self._api.get_position(symbol)
|
||||||
|
# if position:
|
||||||
|
# return avg_cost
|
||||||
|
if position:
|
||||||
|
if position.pos_long > 0:
|
||||||
|
return position.open_price_long
|
||||||
|
elif position.pos_short > 0:
|
||||||
|
return position.open_price_short
|
||||||
|
return None
|
||||||
|
|
||||||
|
def send_order(self, order: Order) -> Optional[Order]:
|
||||||
|
"""
|
||||||
|
策略通过此方法发送订单。
|
||||||
|
将订单放入队列,等待 TqsdkEngine 在其异步循环中处理。
|
||||||
|
"""
|
||||||
|
# 为订单分配一个临时ID,便于在队列中追踪,实际ID由Tqsdk返回后更新
|
||||||
|
if not order.id: # 使用 order.id 属性
|
||||||
|
order.id = f"LOCAL_{id(order)}_{datetime.now().strftime('%f')}"
|
||||||
|
order.order_id = order.id # 保持 Tqsdk 风格的 order_id 也一致
|
||||||
|
self.order_queue.append(order)
|
||||||
|
print(f"Context: 订单已加入队列: {order}")
|
||||||
|
return order # 返回传入的订单,待引擎更新其状态和ID
|
||||||
|
|
||||||
|
def cancel_order(self, order_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
策略通过此方法取消指定ID的订单。
|
||||||
|
将取消请求放入队列,等待 TqsdkEngine 在其异步循环中处理。
|
||||||
|
"""
|
||||||
|
# 检查订单是否处于待处理状态
|
||||||
|
if order_id in self.get_pending_orders():
|
||||||
|
self.cancel_queue.append(order_id)
|
||||||
|
print(f"Context: 取消订单请求已加入队列: {order_id}")
|
||||||
|
return True
|
||||||
|
print(f"Context: 订单 {order_id} 不在待处理队列中,无法取消。")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_engine(self, engine: 'TqsdkEngine'): # 使用 TYPE_CHECKING 中的 TqsdkEngine 类型提示
|
||||||
|
"""
|
||||||
|
设置对 TqsdkEngine 实例的引用。
|
||||||
|
由 TqsdkEngine 在初始化时调用,用于允许 Context 访问 Engine 的状态。
|
||||||
|
"""
|
||||||
|
self._engine = engine
|
||||||
|
print("TqsdkContext: 已设置引擎引用。")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_rollover_bar(self) -> bool:
|
||||||
|
"""
|
||||||
|
属性:判断当前 K 线是否为换月 K 线(即新合约的第一根 K 线)。
|
||||||
|
用于在换月时禁止策略开仓。
|
||||||
|
Tqsdk 的回测模式下,通常通过主力连续合约或多合约同时回测来处理换月。
|
||||||
|
此处为适配原有接口的简化实现。如果你需要 Tqsdk 的换月逻辑,
|
||||||
|
可能需要在 TqsdkEngine 中实现更复杂的判断,并通过 Context 暴露此状态。
|
||||||
|
对于 Tqsdk 的主力连续合约,通常不需要策略层面关心具体的换月 K 线。
|
||||||
|
"""
|
||||||
|
# 如果引擎设置了 is_rollover_bar 属性,则使用引擎的判断
|
||||||
|
if self._engine and hasattr(self._engine, 'is_rollover_bar'):
|
||||||
|
return self._engine.is_rollover_bar
|
||||||
|
return False # 默认返回 False
|
||||||
|
|
||||||
|
def get_bar_history(self):
|
||||||
|
return self._engine.get_bar_history()
|
||||||
|
|
||||||
|
def get_price_history(self, key: str):
|
||||||
|
return self._engine.get_price_history(key)
|
||||||
580
src/tqsdk_real_engine.py
Normal file
580
src/tqsdk_real_engine.py
Normal file
@@ -0,0 +1,580 @@
|
|||||||
|
# filename: tqsdk_engine.py
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from typing import Literal, Type, Dict, Any, List, Optional
|
||||||
|
import pandas as pd
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
# 导入你提供的 core_data 中的类型
|
||||||
|
from src.common_utils import is_bar_pre_close_period, is_futures_trading_time
|
||||||
|
from src.core_data import Bar, Order, Trade, PortfolioSnapshot
|
||||||
|
|
||||||
|
# 导入 Tqsdk 的核心类型
|
||||||
|
import tqsdk
|
||||||
|
from tqsdk import (
|
||||||
|
TqApi,
|
||||||
|
TqAccount,
|
||||||
|
tafunc,
|
||||||
|
TqSim,
|
||||||
|
TqBacktest,
|
||||||
|
TqAuth,
|
||||||
|
TargetPosTask,
|
||||||
|
BacktestFinished,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 导入 TqsdkContext 和 BaseStrategy
|
||||||
|
from src.tqsdk_real_context import TqsdkContext
|
||||||
|
from src.strategies.base_strategy import Strategy # 假设你的策略基类在此路径
|
||||||
|
|
||||||
|
BEIJING_TZ = "Asia/Shanghai"
|
||||||
|
|
||||||
|
|
||||||
|
class TqsdkEngine:
|
||||||
|
"""
|
||||||
|
Tqsdk 回测引擎:协调 Tqsdk 数据流、策略执行、订单模拟和结果记录。
|
||||||
|
替代原有的 BacktestEngine。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
strategy_class: Type[Strategy],
|
||||||
|
strategy_params: Dict[str, Any],
|
||||||
|
api: TqApi,
|
||||||
|
roll_over_mode: bool = False, # 是否开启换月模式检测
|
||||||
|
symbol: str = None,
|
||||||
|
duration_seconds: int = 1,
|
||||||
|
history_length: int = 50,
|
||||||
|
close_bar_delta: timedelta = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
初始化 Tqsdk 回测引擎。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
strategy_class (Type[Strategy]): 策略类。
|
||||||
|
strategy_params (Dict[str, Any]): 传递给策略的参数字典。
|
||||||
|
data_path (str): 本地 K 线数据文件路径,用于 TqSim 加载。
|
||||||
|
initial_capital (float): 初始资金。
|
||||||
|
slippage_rate (float): 交易滑点率(在 Tqsdk 中通常需要手动实现或通过费用设置)。
|
||||||
|
commission_rate (float): 交易佣金率(在 Tqsdk 中通常需要手动实现或通过费用设置)。
|
||||||
|
roll_over_mode (bool): 是否启用换月检测。
|
||||||
|
start_time (Optional[datetime]): 回测开始时间。
|
||||||
|
end_time (Optional[datetime]): 回测结束时间。
|
||||||
|
"""
|
||||||
|
self.strategy_class = strategy_class
|
||||||
|
self.strategy_params = strategy_params
|
||||||
|
self.roll_over_mode = roll_over_mode
|
||||||
|
self.history_length = history_length
|
||||||
|
self.close_bar_delta = close_bar_delta
|
||||||
|
|
||||||
|
self.next_close_time = None
|
||||||
|
|
||||||
|
# Tqsdk API 和模拟器
|
||||||
|
# 这里使用 file_path 参数指定本地数据文件
|
||||||
|
self._api: TqApi = api
|
||||||
|
|
||||||
|
# 从策略参数中获取主symbol,TqsdkContext 需要知道它
|
||||||
|
self.symbol: str = strategy_params.get("symbol")
|
||||||
|
if not self.symbol:
|
||||||
|
raise ValueError("strategy_params 必须包含 'symbol' 字段")
|
||||||
|
|
||||||
|
# 获取 K 线数据(Tqsdk 自动处理)
|
||||||
|
# 这里假设策略所需 K 线周期在 strategy_params 中,否则默认60秒(1分钟K线)
|
||||||
|
self.bar_duration_seconds: int = strategy_params.get("bar_duration_seconds", 60)
|
||||||
|
# self._main_kline_serial = self._api.get_kline_serial(
|
||||||
|
# self.symbol, self.bar_duration_seconds
|
||||||
|
# )
|
||||||
|
|
||||||
|
# 初始化上下文
|
||||||
|
self._context: TqsdkContext = TqsdkContext(api=self._api)
|
||||||
|
# 实例化策略,并将上下文传递给它
|
||||||
|
self._strategy: Strategy = self.strategy_class(
|
||||||
|
context=self._context, **self.strategy_params
|
||||||
|
)
|
||||||
|
self._context.set_engine(
|
||||||
|
self
|
||||||
|
) # 将引擎自身传递给上下文,以便 Context 可以访问引擎属性
|
||||||
|
|
||||||
|
self.portfolio_snapshots: List[PortfolioSnapshot] = []
|
||||||
|
self.trade_history: List[Trade] = []
|
||||||
|
self.all_bars: List[Bar] = [] # 收集所有处理过的Bar
|
||||||
|
|
||||||
|
self.close_list: List[float] = []
|
||||||
|
self.open_list: List[float] = []
|
||||||
|
self.high_list: List[float] = []
|
||||||
|
self.low_list: List[float] = []
|
||||||
|
self.volume_list: List[float] = []
|
||||||
|
|
||||||
|
self.last_processed_bar: Optional[Bar] = None
|
||||||
|
self._is_rollover_bar: bool = False # 换月信号
|
||||||
|
self._last_underlying_symbol = self.symbol # 用于检测主力合约换月
|
||||||
|
|
||||||
|
self.klines = api.get_kline_serial(
|
||||||
|
symbol, duration_seconds, data_length=history_length + 2
|
||||||
|
)
|
||||||
|
self.klines_1min = api.get_kline_serial(
|
||||||
|
symbol, 60
|
||||||
|
)
|
||||||
|
self.now = None
|
||||||
|
self.quote = None
|
||||||
|
if roll_over_mode:
|
||||||
|
self.quote = api.get_quote(symbol)
|
||||||
|
|
||||||
|
self.kline_row = None
|
||||||
|
|
||||||
|
print("TqsdkEngine: 初始化完成。")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_rollover_bar(self) -> bool:
|
||||||
|
"""
|
||||||
|
属性:判断当前 K 线是否为换月 K 线(即检测到主力合约切换)。
|
||||||
|
"""
|
||||||
|
return self._is_rollover_bar
|
||||||
|
|
||||||
|
def _process_queued_requests(self):
|
||||||
|
"""
|
||||||
|
异步处理 Context 中排队的订单和取消请求。
|
||||||
|
"""
|
||||||
|
# 处理订单
|
||||||
|
while self._context.order_queue:
|
||||||
|
order_to_send: Order = self._context.order_queue.popleft()
|
||||||
|
print(f"Engine: 处理订单请求: {order_to_send}")
|
||||||
|
|
||||||
|
# 映射 core_data.Order 到 Tqsdk 的订单参数
|
||||||
|
tqsdk_direction = ""
|
||||||
|
tqsdk_offset = ""
|
||||||
|
|
||||||
|
if order_to_send.direction == "BUY":
|
||||||
|
tqsdk_direction = "BUY"
|
||||||
|
tqsdk_offset = order_to_send.offset or "OPEN" # 默认开仓
|
||||||
|
elif order_to_send.direction == "SELL":
|
||||||
|
tqsdk_direction = "SELL"
|
||||||
|
tqsdk_offset = order_to_send.offset or "OPEN" # 默认开仓
|
||||||
|
elif order_to_send.direction == "CLOSE_LONG":
|
||||||
|
tqsdk_direction = "SELL"
|
||||||
|
tqsdk_offset = order_to_send.offset or "CLOSE" # 平多,默认平仓
|
||||||
|
elif order_to_send.direction == "CLOSE_SHORT":
|
||||||
|
tqsdk_direction = "BUY"
|
||||||
|
tqsdk_offset = order_to_send.offset or "CLOSE" # 平空,默认平仓
|
||||||
|
else:
|
||||||
|
print(f"Engine: 未知订单方向: {order_to_send.direction}")
|
||||||
|
continue # 跳过此订单
|
||||||
|
|
||||||
|
if "SHFE" in order_to_send.symbol:
|
||||||
|
tqsdk_offset = "OPEN"
|
||||||
|
|
||||||
|
try:
|
||||||
|
tq_order = self._api.insert_order(
|
||||||
|
symbol=order_to_send.symbol,
|
||||||
|
direction=tqsdk_direction,
|
||||||
|
offset=tqsdk_offset,
|
||||||
|
volume=order_to_send.volume,
|
||||||
|
# Tqsdk 市价单 limit_price 设为 None,限价单则传递价格
|
||||||
|
limit_price=(
|
||||||
|
order_to_send.limit_price
|
||||||
|
if order_to_send.price_type == "LIMIT"
|
||||||
|
# else self.quote.bid_price1 + (1 if tqsdk_direction == "BUY" else -1)
|
||||||
|
else (
|
||||||
|
self.quote.bid_price1
|
||||||
|
if tqsdk_direction == "SELL"
|
||||||
|
else self.quote.ask_price1
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# 更新原始 Order 对象与 Tqsdk 的订单ID和状态
|
||||||
|
order_to_send.id = tq_order.order_id
|
||||||
|
# order_to_send.order_id = tq_order.order_id
|
||||||
|
# order_to_send.status = tq_order.status
|
||||||
|
order_to_send.submitted_time = pd.to_datetime(
|
||||||
|
tq_order.insert_date_time, unit="ns", utc=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 等待订单状态更新(成交/撤销/报错)
|
||||||
|
# 在 Tqsdk 中,订单和成交是独立的,通常在 wait_update() 循环中通过 api.is_changing() 检查
|
||||||
|
# 这里为了模拟同步处理,直接等待订单状态最终确定
|
||||||
|
# 注意:实际回测中,不应在这里长时间阻塞,而应在主循环中持续 wait_update
|
||||||
|
# 为了简化适配,这里模拟即时处理,但可能与真实异步行为有差异。
|
||||||
|
# 更健壮的方式是在主循环中通过订单状态回调更新
|
||||||
|
# 这里我们假设订单会很快更新状态,或者在下一个 wait_update() 周期中被检测到
|
||||||
|
self._api.wait_update() # 等待一次更新
|
||||||
|
|
||||||
|
# # 检查最终订单状态和成交
|
||||||
|
# if tq_order.status == "FINISHED":
|
||||||
|
# # 查找对应的成交记录
|
||||||
|
# for trade_id, tq_trade in self._api.get_trade().items():
|
||||||
|
# if tq_trade.order_id == tq_order.order_id and tq_trade.volume > 0: # 确保是实际成交
|
||||||
|
# # 创建 core_data.Trade 对象
|
||||||
|
# trade = Trade(
|
||||||
|
# order_id=tq_trade.order_id,
|
||||||
|
# fill_time=tafunc.get_datetime_from_timestamp(tq_trade.trade_date_time) if tq_trade.trade_date_time else datetime.now(),
|
||||||
|
# symbol=order_to_send.symbol, # 使用 Context 中的 symbol
|
||||||
|
# direction=tq_trade.direction, # 实际成交方向
|
||||||
|
# volume=tq_trade.volume,
|
||||||
|
# price=tq_trade.price,
|
||||||
|
# commission=tq_trade.commission,
|
||||||
|
# cash_after_trade=self._api.get_account().available,
|
||||||
|
# positions_after_trade=self._context.get_current_positions(),
|
||||||
|
# realized_pnl=tq_trade.realized_pnl, # Tqsdk TqTrade 对象有 realized_pnl
|
||||||
|
# is_open_trade=tq_trade.offset == "OPEN",
|
||||||
|
# is_close_trade=tq_trade.offset in ["CLOSE", "CLOSETODAY", "CLOSEYESTERDAY"]
|
||||||
|
# )
|
||||||
|
# self.trade_history.append(trade)
|
||||||
|
# print(f"Engine: 成交记录: {trade}")
|
||||||
|
# break # 找到成交就跳出
|
||||||
|
# order_to_send.status = tq_order.status # 更新最终状态
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Engine: 发送订单 {order_to_send.id} 失败: {e}")
|
||||||
|
# order_to_send.status = "ERROR"
|
||||||
|
|
||||||
|
# 处理取消请求
|
||||||
|
while self._context.cancel_queue:
|
||||||
|
order_id_to_cancel = self._context.cancel_queue.popleft()
|
||||||
|
print(f"Engine: 处理取消请求: {order_id_to_cancel}")
|
||||||
|
tq_order_to_cancel = self._api.get_order(order_id_to_cancel)
|
||||||
|
if tq_order_to_cancel and tq_order_to_cancel.status == "ALIVE":
|
||||||
|
try:
|
||||||
|
self._api.cancel_order(tq_order_to_cancel)
|
||||||
|
self._api.wait_update() # 等待取消确认
|
||||||
|
print(
|
||||||
|
f"Engine: 订单 {order_id_to_cancel} 已尝试取消。当前状态: {tq_order_to_cancel.status}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Engine: 取消订单 {order_id_to_cancel} 失败: {e}")
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"Engine: 订单 {order_id_to_cancel} 不存在或已非活动状态,无法取消。"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _record_portfolio_snapshot(self, current_time: datetime):
|
||||||
|
"""
|
||||||
|
记录当前投资组合的快照。
|
||||||
|
"""
|
||||||
|
account: TqAccount = self._api.get_account()
|
||||||
|
current_positions = self._context.get_current_positions()
|
||||||
|
|
||||||
|
# 计算当前持仓市值
|
||||||
|
total_market_value = 0.0
|
||||||
|
current_prices: Dict[str, float] = {}
|
||||||
|
for symbol, qty in current_positions.items():
|
||||||
|
# 获取当前合约的最新价格
|
||||||
|
quote = self._api.get_quote(symbol)
|
||||||
|
if quote.last_price: # 确保价格是最近的
|
||||||
|
price = quote.last_price
|
||||||
|
current_prices[symbol] = price
|
||||||
|
total_market_value += (
|
||||||
|
price * qty * quote.volume_multiple
|
||||||
|
) # volume_multiple 乘数
|
||||||
|
else:
|
||||||
|
# 如果没有最新价格,使用最近的K线收盘价作为估算
|
||||||
|
# 在实盘或连续回测中,通常会有最新的行情
|
||||||
|
print(f"警告: 未获取到 {symbol} 最新价格,可能影响净值计算。")
|
||||||
|
# 可以尝试从 K 线获取最近价格
|
||||||
|
kline = self._api.get_kline_serial(symbol, self.bar_duration_seconds)
|
||||||
|
if not kline.empty:
|
||||||
|
last_kline = kline.iloc[-2]
|
||||||
|
price = last_kline.close
|
||||||
|
current_prices[symbol] = price
|
||||||
|
total_market_value += (
|
||||||
|
price * qty * self._api.get_instrument(symbol).volume_multiple
|
||||||
|
) # 使用 instrument 的乘数
|
||||||
|
|
||||||
|
total_value = (
|
||||||
|
account.available + account.frozen_margin + total_market_value
|
||||||
|
) # Tqsdk 的 balance 已包含持仓市值和冻结资金
|
||||||
|
# Tqsdk 的 total_profit/balance 已经包含了所有盈亏和资金
|
||||||
|
|
||||||
|
snapshot = PortfolioSnapshot(
|
||||||
|
datetime=current_time,
|
||||||
|
total_value=account.balance, # Tqsdk 的 balance 包含了可用资金、冻结保证金和持仓市值
|
||||||
|
cash=account.available,
|
||||||
|
positions=current_positions,
|
||||||
|
price_at_snapshot=current_prices,
|
||||||
|
)
|
||||||
|
self.portfolio_snapshots.append(snapshot)
|
||||||
|
|
||||||
|
def _close_all_positions_at_end(self):
|
||||||
|
"""
|
||||||
|
回测结束时,平掉所有剩余持仓。
|
||||||
|
"""
|
||||||
|
current_positions = self._context.get_current_positions()
|
||||||
|
if not current_positions:
|
||||||
|
print("回测结束:没有需要平仓的持仓。")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("回测结束:开始平仓所有剩余持仓...")
|
||||||
|
for symbol, qty in current_positions.items():
|
||||||
|
order_direction: Literal["BUY", "SELL"]
|
||||||
|
if qty > 0: # 多头持仓,卖出平仓
|
||||||
|
order_direction = "SELL"
|
||||||
|
else: # 空头持仓,买入平仓
|
||||||
|
order_direction = "BUY"
|
||||||
|
|
||||||
|
TargetPosTask(self._api, symbol).set_target_volume(0)
|
||||||
|
|
||||||
|
# # 使用市价单快速平仓
|
||||||
|
# tq_order = self._api.insert_order(
|
||||||
|
# symbol=symbol,
|
||||||
|
# direction=order_direction,
|
||||||
|
# offset="CLOSE", # 平仓
|
||||||
|
# volume=abs(qty),
|
||||||
|
# limit_price=self
|
||||||
|
# )
|
||||||
|
# print(f"平仓订单已发送: {symbol} {order_direction} {abs(qty)} 手")
|
||||||
|
# 等待订单完成
|
||||||
|
# while tq_order.status == "ALIVE":
|
||||||
|
# self._api.wait_update()
|
||||||
|
|
||||||
|
# if tq_order.status == "FINISHED":
|
||||||
|
# print(f"订单 {tq_order.order_id} 平仓完成。")
|
||||||
|
# else:
|
||||||
|
# print(f"订单 {tq_order.order_id} 平仓失败或未完成,状态: {tq_order.status}")
|
||||||
|
|
||||||
|
def _run_async(self):
|
||||||
|
"""
|
||||||
|
异步运行回测的主循环。
|
||||||
|
"""
|
||||||
|
print(f"TqsdkEngine: 开始加载历史数据,加载k线数量{self.history_length}")
|
||||||
|
|
||||||
|
self._strategy.trading = False
|
||||||
|
|
||||||
|
is_trading_time = is_futures_trading_time()
|
||||||
|
|
||||||
|
for i in range(self.history_length + 1, 0 if not is_trading_time else 1, -1):
|
||||||
|
kline_row = self.klines.iloc[-i]
|
||||||
|
kline_dt = pd.to_datetime(kline_row.datetime, unit="ns", utc=True)
|
||||||
|
kline_dt = kline_dt.tz_convert(BEIJING_TZ)
|
||||||
|
self.main(kline_row, self.klines.iloc[-i - 1])
|
||||||
|
|
||||||
|
print(f"TqsdkEngine: 加载历史k线完成, bars数量:{len(self.all_bars)},last bar datetime:{self.all_bars[-1].datetime}")
|
||||||
|
|
||||||
|
self._strategy.trading = True
|
||||||
|
self._last_underlying_symbol = self.quote.underlying_symbol
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"TqsdkEngine: self._last_underlying_symbol:{self._last_underlying_symbol}, is_trading_time:{is_trading_time}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 初始化策略 (如果策略有 on_init 方法)
|
||||||
|
if hasattr(self._strategy, "on_init"):
|
||||||
|
self._strategy.on_init()
|
||||||
|
|
||||||
|
if is_trading_time:
|
||||||
|
print(f"TqsdkEngine: 当前是交易时间,处理最新一根k线")
|
||||||
|
|
||||||
|
kline_row = self.klines.iloc[-1]
|
||||||
|
kline_dt = pd.to_datetime(kline_row.datetime, unit="ns", utc=True)
|
||||||
|
kline_dt = kline_dt.tz_convert(BEIJING_TZ)
|
||||||
|
self.kline_row = kline_row
|
||||||
|
|
||||||
|
self.main(self.klines.iloc[-1], self.klines.iloc[-2])
|
||||||
|
|
||||||
|
# 迭代 K 线数据
|
||||||
|
# 使用 self._api.get_kline_serial 获取到的 K 线是 Pandas DataFrame,
|
||||||
|
# 直接迭代其行(Bar)更符合回测逻辑
|
||||||
|
print(f"TqsdkEngine: 开始等待最新数据")
|
||||||
|
while True:
|
||||||
|
# Tqsdk API 的 wait_update() 确保数据更新
|
||||||
|
self._api.wait_update()
|
||||||
|
|
||||||
|
if self.roll_over_mode and (
|
||||||
|
self._api.is_changing(self.quote, "underlying_symbol")
|
||||||
|
or self._last_underlying_symbol != self.quote.underlying_symbol
|
||||||
|
):
|
||||||
|
self._last_underlying_symbol = self.quote.underlying_symbol
|
||||||
|
|
||||||
|
if self._api.is_changing(self.klines_1min.iloc[-1], "datetime"):
|
||||||
|
kline_dt = pd.to_datetime(self.kline_row.datetime, unit="ns", utc=True)
|
||||||
|
kline_dt = kline_dt.tz_convert(BEIJING_TZ)
|
||||||
|
|
||||||
|
is_close_bar = is_bar_pre_close_period(kline_dt, int(self.kline_row.duration), pre_close_minutes=3)
|
||||||
|
|
||||||
|
if is_close_bar:
|
||||||
|
print(f'TqsdkEngine: close bar, kline_dt:{kline_dt}, now: {datetime.now()}')
|
||||||
|
self.close_bar(kline_row)
|
||||||
|
|
||||||
|
if self._api.is_changing(self.klines.iloc[-1], "datetime"):
|
||||||
|
kline_row = self.klines.iloc[-1]
|
||||||
|
kline_dt = pd.to_datetime(kline_row.datetime, unit="ns", utc=True)
|
||||||
|
kline_dt = kline_dt.tz_convert(BEIJING_TZ)
|
||||||
|
self.kline_row = kline_row
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"TqsdkEngine: 新k线产生,k line datetime:{kline_dt}, now: {datetime.now()}"
|
||||||
|
)
|
||||||
|
self.main(kline_row, self.klines.iloc[-2])
|
||||||
|
|
||||||
|
def close_bar(self, kline_row):
|
||||||
|
kline_dt = pd.to_datetime(kline_row.datetime, unit="ns", utc=True)
|
||||||
|
kline_dt = kline_dt.tz_convert(BEIJING_TZ)
|
||||||
|
if len(self.all_bars) > 0:
|
||||||
|
# 创建 core_data.Bar 对象
|
||||||
|
current_bar = Bar(
|
||||||
|
datetime=kline_dt,
|
||||||
|
symbol=self._last_underlying_symbol,
|
||||||
|
open=kline_row.open,
|
||||||
|
high=kline_row.high,
|
||||||
|
low=kline_row.low,
|
||||||
|
close=kline_row.close,
|
||||||
|
volume=kline_row.volume,
|
||||||
|
open_oi=kline_row.open_oi,
|
||||||
|
close_oi=kline_row.close_oi,
|
||||||
|
)
|
||||||
|
self.all_bars[-1] = current_bar
|
||||||
|
|
||||||
|
self.close_list[-1] = current_bar.close
|
||||||
|
self.open_list[-1] = current_bar.open
|
||||||
|
self.high_list[-1] = current_bar.high
|
||||||
|
self.low_list[-1] = current_bar.low
|
||||||
|
self.volume_list[-1] = current_bar.volume
|
||||||
|
|
||||||
|
self.last_processed_bar = current_bar
|
||||||
|
|
||||||
|
if self._strategy.trading is True:
|
||||||
|
self._strategy.on_close_bar(current_bar)
|
||||||
|
|
||||||
|
# 处理订单和取消请求
|
||||||
|
self._process_queued_requests()
|
||||||
|
|
||||||
|
def main(self, kline_row, prev_kline_row):
|
||||||
|
if True:
|
||||||
|
kline_dt = pd.to_datetime(prev_kline_row.datetime, unit="ns", utc=True)
|
||||||
|
kline_dt = kline_dt.tz_convert(BEIJING_TZ)
|
||||||
|
if len(self.all_bars) > 0:
|
||||||
|
# 创建 core_data.Bar 对象
|
||||||
|
current_bar = Bar(
|
||||||
|
datetime=kline_dt,
|
||||||
|
symbol=self._last_underlying_symbol,
|
||||||
|
open=prev_kline_row.open,
|
||||||
|
high=prev_kline_row.high,
|
||||||
|
low=prev_kline_row.low,
|
||||||
|
close=prev_kline_row.close,
|
||||||
|
volume=prev_kline_row.volume,
|
||||||
|
open_oi=prev_kline_row.open_oi,
|
||||||
|
close_oi=prev_kline_row.close_oi,
|
||||||
|
)
|
||||||
|
self.all_bars[-1] = current_bar
|
||||||
|
|
||||||
|
self.close_list[-1] = current_bar.close
|
||||||
|
self.open_list[-1] = current_bar.open
|
||||||
|
self.high_list[-1] = current_bar.high
|
||||||
|
self.low_list[-1] = current_bar.low
|
||||||
|
self.volume_list[-1] = current_bar.volume
|
||||||
|
|
||||||
|
self.last_processed_bar = current_bar
|
||||||
|
|
||||||
|
# if self._strategy.trading is True:
|
||||||
|
# self._strategy.on_close_bar(current_bar)
|
||||||
|
|
||||||
|
# # 处理订单和取消请求
|
||||||
|
# self._process_queued_requests()
|
||||||
|
|
||||||
|
# on open bar --------------------------------------
|
||||||
|
# 创建 core_data.Bar 对象
|
||||||
|
kline_dt = pd.to_datetime(kline_row.datetime, unit="ns", utc=True)
|
||||||
|
kline_dt = kline_dt.tz_convert(BEIJING_TZ)
|
||||||
|
current_bar = Bar(
|
||||||
|
datetime=kline_dt,
|
||||||
|
symbol=self._last_underlying_symbol,
|
||||||
|
open=kline_row.open,
|
||||||
|
high=kline_row.high,
|
||||||
|
low=kline_row.low,
|
||||||
|
close=kline_row.close,
|
||||||
|
volume=kline_row.volume,
|
||||||
|
open_oi=kline_row.open_oi,
|
||||||
|
close_oi=kline_row.close_oi,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 设置当前 Bar 到 Context
|
||||||
|
self._context.set_current_bar(current_bar)
|
||||||
|
|
||||||
|
# Tqsdk 的 is_changing 用于判断数据是否有变化,对于回测遍历 K 线,每次迭代都算作新 Bar
|
||||||
|
# 如果 kline_row.datetime 与上次不同,则认为是新 Bar
|
||||||
|
if (
|
||||||
|
self.roll_over_mode
|
||||||
|
and self.last_processed_bar is not None
|
||||||
|
and self._last_underlying_symbol != self.last_processed_bar.symbol
|
||||||
|
and self._strategy.trading is True
|
||||||
|
):
|
||||||
|
self._is_rollover_bar = True
|
||||||
|
print(
|
||||||
|
f"TqsdkEngine: 检测到换月信号!从 {self._last_underlying_symbol} 切换到 {self.quote.underlying_symbol}"
|
||||||
|
)
|
||||||
|
self._close_all_positions_at_end()
|
||||||
|
|
||||||
|
self._strategy.cancel_all_pending_orders()
|
||||||
|
|
||||||
|
self._strategy.on_rollover(
|
||||||
|
self.last_processed_bar.symbol, self._last_underlying_symbol
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._is_rollover_bar = False
|
||||||
|
|
||||||
|
self.all_bars.append(current_bar)
|
||||||
|
|
||||||
|
self.close_list.append(current_bar.close)
|
||||||
|
self.open_list.append(current_bar.open)
|
||||||
|
self.high_list.append(current_bar.high)
|
||||||
|
self.low_list.append(current_bar.low)
|
||||||
|
self.volume_list.append(current_bar.volume)
|
||||||
|
|
||||||
|
self.last_processed_bar = current_bar
|
||||||
|
|
||||||
|
# 调用策略的 on_bar 方法
|
||||||
|
self._strategy.on_open_bar(current_bar)
|
||||||
|
|
||||||
|
# 处理订单和取消请求
|
||||||
|
if self._strategy.trading is True:
|
||||||
|
self._process_queued_requests()
|
||||||
|
|
||||||
|
# 记录投资组合快照
|
||||||
|
self._record_portfolio_snapshot(current_bar.datetime)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
同步调用异步回测主循环。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._run_async()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n回测被用户中断。")
|
||||||
|
finally:
|
||||||
|
self._api.close()
|
||||||
|
print("TqsdkEngine: API 已关闭。")
|
||||||
|
|
||||||
|
def get_results(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
返回回测结果数据,供结果分析模块使用。
|
||||||
|
"""
|
||||||
|
final_portfolio_value = 0.0
|
||||||
|
if self.portfolio_snapshots:
|
||||||
|
final_portfolio_value = self.portfolio_snapshots[-1].total_value
|
||||||
|
# else:
|
||||||
|
# final_portfolio_value = self.initial_capital # 如果没有快照,则净值是初始资金
|
||||||
|
|
||||||
|
# total_return_percentage = (
|
||||||
|
# (final_portfolio_value - self.initial_capital) / self.initial_capital
|
||||||
|
# ) * 100 if self.initial_capital != 0 else 0.0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"portfolio_snapshots": self.portfolio_snapshots,
|
||||||
|
"trade_history": self.trade_history,
|
||||||
|
# "initial_capital": self.initial_capital,
|
||||||
|
"all_bars": self.all_bars,
|
||||||
|
"final_portfolio_value": final_portfolio_value,
|
||||||
|
# "total_return_percentage": total_return_percentage,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_bar_history(self):
|
||||||
|
return self.all_bars
|
||||||
|
|
||||||
|
def get_price_history(self, key: str):
|
||||||
|
if key == "close":
|
||||||
|
return self.close_list
|
||||||
|
elif key == "open":
|
||||||
|
return self.open_list
|
||||||
|
elif key == "high":
|
||||||
|
return self.high_list
|
||||||
|
elif key == "low":
|
||||||
|
return self.low_list
|
||||||
|
elif key == "volume":
|
||||||
|
return self.volume_list
|
||||||
8686
tqsdk_main2.ipynb
Normal file
8686
tqsdk_main2.ipynb
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user