tqsdk实盘
This commit is contained in:
@@ -2,8 +2,12 @@
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from src.indicators.base_indicators import Indicator
|
||||
|
||||
# 导入纯函数 (注意相对导入路径的变化)
|
||||
from .analysis_utils import (
|
||||
calculate_metrics,
|
||||
@@ -26,6 +30,7 @@ class ResultAnalyzer:
|
||||
trade_history: List[Trade],
|
||||
bars: List[Bar],
|
||||
initial_capital: float,
|
||||
indicator_list: List[Indicator] = [],
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
@@ -37,8 +42,9 @@ class ResultAnalyzer:
|
||||
self.portfolio_snapshots = portfolio_snapshots
|
||||
self.trade_history = trade_history
|
||||
self.initial_capital = initial_capital
|
||||
self.bars = bars # 接收所有K线数据
|
||||
self.bars = bars # 接收所有K线数据
|
||||
self._metrics_cache: Optional[Dict[str, Any]] = None
|
||||
self.indicator_list = indicator_list
|
||||
|
||||
print("\n--- 结果分析器初始化完成 ---")
|
||||
|
||||
@@ -63,14 +69,17 @@ class ResultAnalyzer:
|
||||
print("\n--- 交易明细 ---")
|
||||
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(
|
||||
f" {trade.fill_time} | {trade.direction:<10} | {trade.symbol} | Vol: {trade.volume} | Price: {trade.price:.2f} | Comm: {trade.commission:.2f}{pnl_display}"
|
||||
)
|
||||
else:
|
||||
print("\n没有交易记录。")
|
||||
|
||||
|
||||
metrics = self.calculate_all_metrics()
|
||||
|
||||
print("\n--- 回测绩效报告 ---")
|
||||
@@ -82,7 +91,7 @@ class ResultAnalyzer:
|
||||
print(f"{'夏普比率':<15}: {metrics['夏普比率']:.2f}")
|
||||
print(f"{'卡玛比率':<15}: {metrics['卡玛比率']:.2f}")
|
||||
print(f"{'总交易次数':<15}: {metrics['总交易次数']}")
|
||||
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}")
|
||||
|
||||
|
||||
def plot_performance(self) -> None:
|
||||
"""
|
||||
绘制投资组合净值和回撤曲线,以及所有合约的收盘价曲线。
|
||||
@@ -103,9 +111,200 @@ class ResultAnalyzer:
|
||||
plot_equity_and_drawdown_chart(
|
||||
self.portfolio_snapshots,
|
||||
self.initial_capital,
|
||||
title="Portfolio Equity and Drawdown Curve (All Contracts)" # 明确标题,表明是整体曲线
|
||||
title="Portfolio Equity and Drawdown Curve (All Contracts)", # 明确标题,表明是整体曲线
|
||||
)
|
||||
|
||||
# 绘制所有处理过的K线收盘价曲线
|
||||
plot_close_price_chart(self.bars, title="Underlying Asset Close Price (Concatenated Bars)") # 明确标题
|
||||
print("图表绘制完成。")
|
||||
plot_close_price_chart(
|
||||
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):
|
||||
return self._engine.get_bar_history()
|
||||
|
||||
def get_price_history(self, key: str):
|
||||
return self._engine.get_price_history(key)
|
||||
|
||||
@property
|
||||
def is_rollover_bar(self) -> bool:
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
# src/backtest_engine.py
|
||||
from datetime import datetime
|
||||
from typing import Type, Dict, Any, List, Optional
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.indicators.base_indicators import Indicator
|
||||
|
||||
# 导入所有需要协调的模块
|
||||
from .core_data import Bar, Order, Trade, PortfolioSnapshot
|
||||
from .data_manager import DataManager
|
||||
@@ -24,7 +27,8 @@ class BacktestEngine:
|
||||
commission_rate: float = 0.0002,
|
||||
roll_over_mode: bool = False,
|
||||
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 动态设置。
|
||||
self.strategy = strategy_class(self.context, symbol="INITIAL_PLACEHOLDER_SYMBOL", **strategy_params)
|
||||
|
||||
self.indicators = indicators
|
||||
|
||||
self.portfolio_snapshots: List[PortfolioSnapshot] = []
|
||||
self.trade_history: List[Trade] = []
|
||||
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._max_history_bars: int = strategy_params.get('history_bars_limit', 200)
|
||||
|
||||
@@ -84,6 +96,8 @@ class BacktestEngine:
|
||||
# 调用策略的初始化方法
|
||||
self.strategy.on_init()
|
||||
|
||||
self.strategy.trading = True
|
||||
|
||||
last_processed_bar: Optional[Bar] = None # 用于在换月时引用旧合约的最后一根 K 线
|
||||
|
||||
# 主回测循环
|
||||
@@ -94,6 +108,11 @@ class BacktestEngine:
|
||||
break # 没有更多数据,回测结束
|
||||
|
||||
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:
|
||||
continue
|
||||
@@ -153,12 +172,28 @@ class BacktestEngine:
|
||||
# self.simulator.process_pending_orders(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 方法
|
||||
# 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.simulator.process_pending_orders(current_bar)
|
||||
self.simulator.process_pending_orders(current_bar, current_indicator_dict)
|
||||
|
||||
|
||||
# 8. 记录投资组合快照
|
||||
@@ -230,3 +265,16 @@ class BacktestEngine:
|
||||
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
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
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]]:
|
||||
"""
|
||||
根据开始、结束和步长生成一个参数值的列表。
|
||||
@@ -31,6 +34,82 @@ def generate_parameter_range(start: Union[int, float], end: Union[int, float], s
|
||||
|
||||
return param_range
|
||||
|
||||
# 示例:
|
||||
# print(generate_parameter_range(0.99, 1.01, 0.005)) # [0.99, 0.995, 1.0, 1.005, 1.01]
|
||||
# print(generate_parameter_range(5, 20, 5)) # [5, 10, 15, 20]
|
||||
from datetime import datetime, time, timedelta
|
||||
from typing import Optional
|
||||
|
||||
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 # <--- 新增字段:这笔交易带来的实现盈亏
|
||||
is_open_trade: bool = False # <--- 新增字段:是否是开仓交易(用于跟踪成本)
|
||||
is_close_trade: bool = False # <--- 新增字段:是否是平仓交易 (用于计算盈亏)
|
||||
indicator_dict: Dict[str, float] = None
|
||||
|
||||
|
||||
@dataclass()
|
||||
|
||||
@@ -42,6 +42,7 @@ class ExecutionSimulator:
|
||||
self.trade_log: List[Trade] = []
|
||||
self.pending_orders: Dict[str, Order] = {}
|
||||
self._current_time: Optional[datetime] = None
|
||||
self.indicator_dict = {}
|
||||
|
||||
print(
|
||||
f"模拟器初始化:初始资金={self.initial_capital:.2f}, 滑点率={self.slippage_rate}, 佣金率={self.commission_rate}"
|
||||
@@ -117,7 +118,7 @@ class ExecutionSimulator:
|
||||
self.pending_orders[order.id] = 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线数据到来时调用。
|
||||
"""
|
||||
@@ -132,7 +133,16 @@ class ExecutionSimulator:
|
||||
if order.symbol != current_bar.symbol:
|
||||
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]:
|
||||
"""
|
||||
@@ -295,7 +305,6 @@ class ExecutionSimulator:
|
||||
is_open_trade=is_trade_an_open_operation,
|
||||
is_close_trade=is_trade_a_close_operation,
|
||||
)
|
||||
self.trade_log.append(executed_trade)
|
||||
|
||||
if order.id in self.pending_orders:
|
||||
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)
|
||||
# 且K线历史足够长时才考虑开仓
|
||||
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线 (队列中最老的一根)
|
||||
bar_1_ago = bar_history[-2]
|
||||
bar_7_ago = bar_history[-8]
|
||||
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
|
||||
@@ -387,11 +387,11 @@ class SimpleLimitBuyStrategyShort(Strategy):
|
||||
# 只有在没有持仓 (current_pos_volume == 0) 且没有待处理订单 (not pending_orders_after_cancel)
|
||||
# 且K线历史足够长时才考虑开仓
|
||||
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线 (队列中最老的一根)
|
||||
bar_1_ago = bar_history[-2]
|
||||
bar_7_ago = bar_history[-8]
|
||||
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
|
||||
@@ -660,11 +660,11 @@ class SimpleLimitBuyStrategy(Strategy):
|
||||
return # 平仓后本K线不再进行开仓判断
|
||||
|
||||
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线 (队列中最老的一根)
|
||||
bar_1_ago = bar_history[-2]
|
||||
bar_7_ago = bar_history[-8]
|
||||
bar_1_ago = bar_history[-8]
|
||||
bar_7_ago = bar_history[-15]
|
||||
|
||||
print(bar_1_ago, bar_7_ago)
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ class Strategy(ABC):
|
||||
self.symbol = symbol # 策略操作的合约Symbol
|
||||
self.params = params
|
||||
self.enable_log = enable_log
|
||||
self.trading = False
|
||||
|
||||
def on_init(self):
|
||||
"""
|
||||
@@ -78,6 +79,9 @@ class Strategy(ABC):
|
||||
发送订单的辅助方法。
|
||||
会在 BaseStrategy 内部构建 Order 对象,并通过 context 转发给模拟器。
|
||||
"""
|
||||
if not self.trading:
|
||||
return None
|
||||
|
||||
if self.context.is_rollover_bar:
|
||||
self.log(f"当前是换月K线,禁止开仓订单")
|
||||
return None
|
||||
@@ -103,16 +107,24 @@ class Strategy(ABC):
|
||||
取消指定ID的订单。
|
||||
通过 context 调用模拟器的 cancel_order 方法。
|
||||
"""
|
||||
if not self.trading:
|
||||
return False
|
||||
|
||||
return self.context.cancel_order(order_id)
|
||||
|
||||
def cancel_all_pending_orders(self) -> int:
|
||||
"""取消当前策略的未决订单,仅限于当前策略关注的Symbol。"""
|
||||
# 注意:在换月模式下,引擎会自动取消旧合约的挂单,这里是策略主动取消
|
||||
if not self.trading:
|
||||
return 0
|
||||
|
||||
pending_orders = self.get_pending_orders()
|
||||
cancelled_count = 0
|
||||
# orders_to_cancel = [
|
||||
# order.id for order in pending_orders.values() if order.symbol == self.symbol
|
||||
# ]
|
||||
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:
|
||||
if self.cancel_order(order_id):
|
||||
@@ -179,3 +191,7 @@ class Strategy(ABC):
|
||||
|
||||
def get_bar_history(self):
|
||||
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
|
||||
|
||||
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.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 # 用于检测主力合约换月
|
||||
@@ -316,6 +322,8 @@ class TqsdkEngine:
|
||||
"""
|
||||
print(f"TqsdkEngine: 开始运行回测,从 {self.start_time} 到 {self.end_time}")
|
||||
|
||||
self._strategy.trading = True
|
||||
|
||||
# 初始化策略 (如果策略有 on_init 方法)
|
||||
if hasattr(self._strategy, "on_init"):
|
||||
self._strategy.on_init()
|
||||
@@ -416,6 +424,13 @@ class TqsdkEngine:
|
||||
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
|
||||
|
||||
|
||||
@@ -441,6 +456,13 @@ class TqsdkEngine:
|
||||
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
|
||||
|
||||
# 设置当前 Bar 到 Context
|
||||
@@ -489,3 +511,15 @@ class TqsdkEngine:
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user