feat: 添加边界防御机制使实盘行为与回测对齐
This commit is contained in:
@@ -236,12 +236,12 @@ if __name__ == "__main__":
|
||||
|
||||
# 这种方式适合获取相对较短或中等长度的历史K线数据。
|
||||
df_if_backtest_daily = collect_and_save_tqsdk_data_stream(
|
||||
symbol="KQ.m@SHFE.br",
|
||||
symbol="KQ.m@CZCE.FG",
|
||||
# symbol='SHFE.rb2510',
|
||||
# symbol='KQ.i@SHFE.bu',
|
||||
freq="min15",
|
||||
start_date_str="2021-01-01",
|
||||
end_date_str="2025-12-01",
|
||||
start_date_str="2020-01-01",
|
||||
end_date_str="2026-03-18",
|
||||
mode="backtest", # 指定为回测模式
|
||||
tq_user=TQ_USER_NAME,
|
||||
tq_pwd=TQ_PASSWORD,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -118,7 +118,8 @@ class PragmaticCyberneticStrategy(Strategy):
|
||||
self._closes.append(prev_bar.close)
|
||||
|
||||
T, FV, FB, ATR = self._calculate_indicators()
|
||||
self.log(f'T: {T} FV: {FV} FB: {FB} ATR: {ATR}')
|
||||
if self.trading:
|
||||
self.log(f'T: {T} FV: {FV} FB: {FB} ATR: {ATR}')
|
||||
if T is None or math.isnan(ATR): return
|
||||
|
||||
self._fbs.append(FB)
|
||||
@@ -134,7 +135,8 @@ class PragmaticCyberneticStrategy(Strategy):
|
||||
|
||||
is_met = self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple())
|
||||
|
||||
self.log(f'is_met: {is_met}, pos: {pos}, current_fb: {current_fb}, prev_fb: {prev_fb}')
|
||||
if self.trading:
|
||||
self.log(f'is_met: {is_met}, pos: {pos}, current_fb: {current_fb}, prev_fb: {prev_fb}')
|
||||
|
||||
# ==========================================
|
||||
# 核心一:经典出场逻辑 (你的原版设计)
|
||||
@@ -192,18 +194,21 @@ class PragmaticCyberneticStrategy(Strategy):
|
||||
# 核心二:经典入场逻辑 (完全不变)
|
||||
# ==========================================
|
||||
elif pos == 0 and is_met:
|
||||
self.log(f'{prev_bar.close}, {T} , {current_fb} , {self.fb_entry_threshold}')
|
||||
self.log(f'prev_bar.close > T: {prev_bar.close > T}, current_fb < -self.fb_entry_threshold : {current_fb < -self.fb_entry_threshold}')
|
||||
self.log(f'prev_bar.close < T: {prev_bar.close < T}, current_fb > self.fb_entry_threshold: {current_fb > self.fb_entry_threshold}')
|
||||
if self.trading:
|
||||
self.log(f'{prev_bar.close}, {T} , {current_fb} , {self.fb_entry_threshold}')
|
||||
self.log(f'prev_bar.close > T: {prev_bar.close > T}, current_fb < -self.fb_entry_threshold : {current_fb < -self.fb_entry_threshold}')
|
||||
self.log(f'prev_bar.close < T: {prev_bar.close < T}, current_fb > self.fb_entry_threshold: {current_fb > self.fb_entry_threshold}')
|
||||
if prev_bar.close > T and current_fb < -self.fb_entry_threshold:
|
||||
if "BUY" in self.order_direction:
|
||||
long_limit = self.round_to_tick(FV - (self.limit_offset_mult * ATR))
|
||||
self.send_limit_order(long_limit, "BUY", self.trade_volume, "OPEN")
|
||||
self.log(f"BUY LIMIT {long_limit}")
|
||||
|
||||
elif prev_bar.close < T and current_fb > self.fb_entry_threshold:
|
||||
if "SELL" in self.order_direction:
|
||||
short_limit = self.round_to_tick(FV + (self.limit_offset_mult * ATR))
|
||||
self.send_limit_order(short_limit, "SELL", self.trade_volume, "OPEN")
|
||||
self.log(f"SELL LIMIT {short_limit}")
|
||||
|
||||
# ==========================================
|
||||
# 彻底隔离历史噪音:原生换月机制
|
||||
|
||||
@@ -118,7 +118,8 @@ class PragmaticCyberneticStrategy(Strategy):
|
||||
self._closes.append(prev_bar.close)
|
||||
|
||||
T, FV, FB, ATR = self._calculate_indicators()
|
||||
self.log(f'T: {T} FV: {FV} FB: {FB} ATR: {ATR}')
|
||||
if self.trading:
|
||||
self.log(f'T: {T} FV: {FV} FB: {FB} ATR: {ATR}')
|
||||
if T is None or math.isnan(ATR): return
|
||||
|
||||
self._fbs.append(FB)
|
||||
@@ -134,7 +135,8 @@ class PragmaticCyberneticStrategy(Strategy):
|
||||
|
||||
is_met = self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple())
|
||||
|
||||
self.log(f'is_met: {is_met}, pos: {pos}, current_fb: {current_fb}, prev_fb: {prev_fb}')
|
||||
if self.trading:
|
||||
self.log(f'is_met: {is_met}, pos: {pos}, current_fb: {current_fb}, prev_fb: {prev_fb}')
|
||||
|
||||
# ==========================================
|
||||
# 核心一:经典出场逻辑 (你的原版设计)
|
||||
@@ -192,18 +194,21 @@ class PragmaticCyberneticStrategy(Strategy):
|
||||
# 核心二:经典入场逻辑 (完全不变)
|
||||
# ==========================================
|
||||
elif pos == 0 and is_met:
|
||||
self.log(f'{prev_bar.close}, {T} , {current_fb} , {self.fb_entry_threshold}')
|
||||
self.log(f'prev_bar.close > T: {prev_bar.close > T}, current_fb < -self.fb_entry_threshold : {current_fb < -self.fb_entry_threshold}')
|
||||
self.log(f'prev_bar.close < T: {prev_bar.close < T}, current_fb > self.fb_entry_threshold: {current_fb > self.fb_entry_threshold}')
|
||||
if self.trading:
|
||||
self.log(f'{prev_bar.close}, {T} , {current_fb} , {self.fb_entry_threshold}')
|
||||
self.log(f'prev_bar.close > T: {prev_bar.close > T}, current_fb < -self.fb_entry_threshold : {current_fb < -self.fb_entry_threshold}')
|
||||
self.log(f'prev_bar.close < T: {prev_bar.close < T}, current_fb > self.fb_entry_threshold: {current_fb > self.fb_entry_threshold}')
|
||||
if prev_bar.close > T and current_fb < -self.fb_entry_threshold:
|
||||
if "BUY" in self.order_direction:
|
||||
long_limit = self.round_to_tick(FV - (self.limit_offset_mult * ATR))
|
||||
self.send_limit_order(long_limit, "BUY", self.trade_volume, "OPEN")
|
||||
self.log(f"BUY LIMIT {long_limit}")
|
||||
|
||||
elif prev_bar.close < T and current_fb > self.fb_entry_threshold:
|
||||
if "SELL" in self.order_direction:
|
||||
short_limit = self.round_to_tick(FV + (self.limit_offset_mult * ATR))
|
||||
self.send_limit_order(short_limit, "SELL", self.trade_volume, "OPEN")
|
||||
self.log(f"SELL LIMIT {short_limit}")
|
||||
|
||||
# ==========================================
|
||||
# 彻底隔离历史噪音:原生换月机制
|
||||
|
||||
@@ -7,7 +7,11 @@ import pandas as pd
|
||||
import time
|
||||
|
||||
# 导入你提供的 core_data 中的类型
|
||||
from src.common_utils import is_bar_pre_close_period, is_futures_trading_time, generate_strategy_identifier
|
||||
from src.common_utils import (
|
||||
is_bar_pre_close_period,
|
||||
is_futures_trading_time,
|
||||
generate_strategy_identifier,
|
||||
)
|
||||
from src.core_data import Bar, Order, Trade, PortfolioSnapshot
|
||||
|
||||
# 导入 Tqsdk 的核心类型
|
||||
@@ -24,6 +28,7 @@ from tqsdk import (
|
||||
)
|
||||
|
||||
from src.state_repo import JsonFileStateRepository
|
||||
|
||||
# 导入 TqsdkContext 和 BaseStrategy
|
||||
from src.tqsdk_real_context import TqsdkContext
|
||||
from src.strategies.base_strategy import Strategy # 假设你的策略基类在此路径
|
||||
@@ -38,15 +43,15 @@ class TqsdkEngine:
|
||||
"""
|
||||
|
||||
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,
|
||||
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 回测引擎。
|
||||
@@ -91,7 +96,9 @@ class TqsdkEngine:
|
||||
|
||||
# 初始化上下文
|
||||
identifier = generate_strategy_identifier(strategy_class, strategy_params)
|
||||
self._context: TqsdkContext = TqsdkContext(api=self._api, state_repository=JsonFileStateRepository(identifier)) # 实例化策略,并将上下文传递给它
|
||||
self._context: TqsdkContext = TqsdkContext(
|
||||
api=self._api, state_repository=JsonFileStateRepository(identifier)
|
||||
) # 实例化策略,并将上下文传递给它
|
||||
self._strategy: Strategy = self.strategy_class(
|
||||
context=self._context, **self.strategy_params
|
||||
)
|
||||
@@ -117,7 +124,9 @@ class TqsdkEngine:
|
||||
self.quote = None
|
||||
self.quote = api.get_quote(symbol)
|
||||
self.klines = api.get_kline_serial(
|
||||
self.quote.underlying_symbol, duration_seconds, data_length=history_length + 2
|
||||
self.quote.underlying_symbol,
|
||||
duration_seconds,
|
||||
data_length=history_length + 2,
|
||||
)
|
||||
self.klines_1min = api.get_kline_serial(self.quote.underlying_symbol, 60)
|
||||
|
||||
@@ -126,6 +135,9 @@ class TqsdkEngine:
|
||||
|
||||
self.target_pos_dict = {}
|
||||
|
||||
# 边界检测状态:记录上一根bar结束时是否有持仓
|
||||
self.prev_bar_had_position: bool = False
|
||||
|
||||
print("TqsdkEngine: 初始化完成。")
|
||||
|
||||
@property
|
||||
@@ -172,16 +184,20 @@ class TqsdkEngine:
|
||||
current_pos_volume = current_positions.get(order_to_send.symbol, 0)
|
||||
|
||||
target_volume = None
|
||||
if order_to_send.direction == 'CLOSE_LONG':
|
||||
if order_to_send.direction == "CLOSE_LONG":
|
||||
target_volume = current_pos_volume - order_to_send.volume
|
||||
elif order_to_send.direction == 'CLOSE_SHORT':
|
||||
elif order_to_send.direction == "CLOSE_SHORT":
|
||||
target_volume = current_pos_volume + order_to_send.volume
|
||||
|
||||
if target_volume is not None:
|
||||
if order_to_send.symbol not in self.target_pos_dict:
|
||||
self.target_pos_dict[order_to_send.symbol] = TargetPosTask(self._api, order_to_send.symbol)
|
||||
self.target_pos_dict[order_to_send.symbol] = TargetPosTask(
|
||||
self._api, order_to_send.symbol
|
||||
)
|
||||
|
||||
self.target_pos_dict[order_to_send.symbol].set_target_volume(target_volume)
|
||||
self.target_pos_dict[order_to_send.symbol].set_target_volume(
|
||||
target_volume
|
||||
)
|
||||
else:
|
||||
try:
|
||||
tq_order = self._api.insert_order(
|
||||
@@ -251,7 +267,7 @@ class TqsdkEngine:
|
||||
price = quote.last_price
|
||||
current_prices[symbol] = price
|
||||
total_market_value += (
|
||||
price * qty * quote.volume_multiple
|
||||
price * qty * quote.volume_multiple
|
||||
) # volume_multiple 乘数
|
||||
else:
|
||||
# 如果没有最新价格,使用最近的K线收盘价作为估算
|
||||
@@ -264,11 +280,11 @@ class TqsdkEngine:
|
||||
price = last_kline.close
|
||||
current_prices[symbol] = price
|
||||
total_market_value += (
|
||||
price * qty * self._api.get_instrument(symbol).volume_multiple
|
||||
price * qty * self._api.get_instrument(symbol).volume_multiple
|
||||
) # 使用 instrument 的乘数
|
||||
|
||||
total_value = (
|
||||
account.available + account.frozen_margin + total_market_value
|
||||
account.available + account.frozen_margin + total_market_value
|
||||
) # Tqsdk 的 balance 已包含持仓市值和冻结资金
|
||||
# Tqsdk 的 total_profit/balance 已经包含了所有盈亏和资金
|
||||
|
||||
@@ -347,6 +363,15 @@ class TqsdkEngine:
|
||||
f"TqsdkEngine: self._last_underlying_symbol:{self._last_underlying_symbol}, is_trading_time:{is_trading_time}"
|
||||
)
|
||||
|
||||
# 初始化边界检测状态:根据实际持仓设置(处理引擎重启情况)
|
||||
current_positions = self._context.get_current_positions()
|
||||
self.prev_bar_had_position = (
|
||||
current_positions.get(self.quote.underlying_symbol, 0) != 0
|
||||
)
|
||||
print(
|
||||
f"TqsdkEngine: 边界检测状态初始化完成,prev_bar_had_position={self.prev_bar_had_position}"
|
||||
)
|
||||
|
||||
# 初始化策略 (如果策略有 on_init 方法)
|
||||
if hasattr(self._strategy, "on_init"):
|
||||
self._strategy.on_init()
|
||||
@@ -362,6 +387,7 @@ class TqsdkEngine:
|
||||
kline_dt = kline_dt.tz_convert(BEIJING_TZ)
|
||||
print(f"TqsdkEngine: 当前是交易时间,处理最新一根k线,datetime:{kline_dt}")
|
||||
|
||||
self._check_boundary_and_close() # 边界检测:上一根bar新开仓且触及边界价则平仓
|
||||
self.main(self.klines.iloc[-1], self.klines.iloc[-2])
|
||||
new_bar = True
|
||||
|
||||
@@ -375,7 +401,9 @@ class TqsdkEngine:
|
||||
for bar in self.all_bars[-5:]:
|
||||
print(bar)
|
||||
|
||||
print(f"TqsdkEngine: 开始等待最新数据, all bars -1:{self.all_bars[-1].datetime}")
|
||||
print(
|
||||
f"TqsdkEngine: 开始等待最新数据, all bars -1:{self.all_bars[-1].datetime}"
|
||||
)
|
||||
|
||||
last_min_k = None
|
||||
while True:
|
||||
@@ -388,11 +416,16 @@ class TqsdkEngine:
|
||||
self._check_roll_over()
|
||||
self.is_checked_rollover = True
|
||||
|
||||
if new_bar and (last_min_k is None or last_min_k.datetime != self.klines_1min.iloc[-1].datetime):
|
||||
if new_bar and (
|
||||
last_min_k is None
|
||||
or last_min_k.datetime != self.klines_1min.iloc[-1].datetime
|
||||
):
|
||||
last_min_k = self.klines_1min.iloc[-1]
|
||||
|
||||
if self.kline_row is not None:
|
||||
kline_dt = pd.to_datetime(self.kline_row.datetime, unit="ns", utc=True)
|
||||
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(
|
||||
@@ -410,8 +443,10 @@ class TqsdkEngine:
|
||||
# if self._api.is_changing(self.klines.iloc[-1], "open"):
|
||||
# print(f"TqsdkEngine: open change!, open:{self.klines.iloc[-1].open}, now: {datetime.now()}")
|
||||
|
||||
if self.kline_row is None or self.kline_row.datetime != self.klines.iloc[-1].datetime:
|
||||
|
||||
if (
|
||||
self.kline_row is None
|
||||
or self.kline_row.datetime != self.klines.iloc[-1].datetime
|
||||
):
|
||||
# 到这里一定满足“整点-00/30 且秒>1”
|
||||
kline_row = self.klines.iloc[-1]
|
||||
kline_dt = pd.to_datetime(kline_row.datetime, unit="ns", utc=True)
|
||||
@@ -433,12 +468,16 @@ class TqsdkEngine:
|
||||
# 小粒度休眠,防止 CPU 空转
|
||||
self._api.wait_update()
|
||||
|
||||
if kline_dt.hour != self.all_bars[-1].datetime.hour or kline_dt.minute != self.all_bars[
|
||||
-1].datetime.minute:
|
||||
if (
|
||||
kline_dt.hour != self.all_bars[-1].datetime.hour
|
||||
or kline_dt.minute != self.all_bars[-1].datetime.minute
|
||||
):
|
||||
print(
|
||||
f"TqsdkEngine: 新k线产生, k line datetime:{kline_dt}, now: {datetime.now()}, open: {self.klines.iloc[-1].open}")
|
||||
f"TqsdkEngine: 新k线产生, k line datetime:{kline_dt}, now: {datetime.now()}, open: {self.klines.iloc[-1].open}"
|
||||
)
|
||||
self.kline_row = self.klines.iloc[-1]
|
||||
|
||||
self._check_boundary_and_close() # 边界检测:上一根bar新开仓且触及边界价则平仓
|
||||
self.main(self.klines.iloc[-1], self.klines.iloc[-2])
|
||||
|
||||
new_bar = True
|
||||
@@ -468,7 +507,6 @@ class TqsdkEngine:
|
||||
# 处理订单和取消请求
|
||||
self._process_queued_requests()
|
||||
|
||||
|
||||
def _check_roll_over(self, timeout_seconds: int = 120):
|
||||
"""
|
||||
[最安全版] 检查并处理实盘持仓换月,此函数会阻塞直到换月成功或超时。
|
||||
@@ -500,9 +538,11 @@ class TqsdkEngine:
|
||||
# 条件一: 是本引擎负责的品种 (e.g., "CZCE.FG605".startswith("CZCE.FG"))
|
||||
# 条件二: 不是当前最新的主力合约
|
||||
# 条件三: 有实际持仓
|
||||
if (pos_symbol.startswith(self.product_id) and
|
||||
pos_symbol != current_dominant_symbol and
|
||||
quantity != 0):
|
||||
if (
|
||||
pos_symbol.startswith(self.product_id)
|
||||
and pos_symbol != current_dominant_symbol
|
||||
and quantity != 0
|
||||
):
|
||||
old_contracts_to_rollover[pos_symbol] = quantity
|
||||
|
||||
# 如果没有需要处理的旧合约,直接返回
|
||||
@@ -515,7 +555,7 @@ class TqsdkEngine:
|
||||
print("=" * 70)
|
||||
print(f"TqsdkEngine ({self.product_id}): [换月开始] 检测到需要移仓的旧合约!")
|
||||
for old_symbol, qty in old_contracts_to_rollover.items():
|
||||
print(f" - 待平仓旧合约: {old_symbol} ({qty} 手)")
|
||||
print(f" - 待平仓旧合约: {old_symbol} ({qty} 手)")
|
||||
print(f" - 目标新合约: {current_dominant_symbol}")
|
||||
print(f" - 新合约目标总持仓: {total_target_quantity} 手")
|
||||
print(f" - 开始执行阻塞式移仓,超时时间: {timeout_seconds} 秒...")
|
||||
@@ -532,10 +572,16 @@ class TqsdkEngine:
|
||||
|
||||
# 5.2 在新合约上建立合并后的总目标仓位
|
||||
if current_dominant_symbol not in self.target_pos_dict:
|
||||
self.target_pos_dict[current_dominant_symbol] = TargetPosTask(self._api, current_dominant_symbol)
|
||||
self.target_pos_dict[current_dominant_symbol].set_target_volume(total_target_quantity)
|
||||
self.target_pos_dict[current_dominant_symbol] = TargetPosTask(
|
||||
self._api, current_dominant_symbol
|
||||
)
|
||||
self.target_pos_dict[current_dominant_symbol].set_target_volume(
|
||||
total_target_quantity
|
||||
)
|
||||
|
||||
print(f" - [移仓指令已发送] 正在处理 {len(old_contracts_to_rollover)} 个旧合约的平仓...")
|
||||
print(
|
||||
f" - [移仓指令已发送] 正在处理 {len(old_contracts_to_rollover)} 个旧合约的平仓..."
|
||||
)
|
||||
|
||||
# 6. 进入等待循环,直到所有换月操作完成或超时
|
||||
while True:
|
||||
@@ -557,26 +603,120 @@ class TqsdkEngine:
|
||||
latest_positions = self._context.get_current_positions()
|
||||
|
||||
# 检查所有旧合约仓位是否已归零
|
||||
all_old_cleared = all(latest_positions.get(s, 0) == 0 for s in old_contracts_to_rollover)
|
||||
all_old_cleared = all(
|
||||
latest_positions.get(s, 0) == 0 for s in old_contracts_to_rollover
|
||||
)
|
||||
|
||||
# 检查新合约仓位是否已达到目标
|
||||
new_pos_correct = latest_positions.get(current_dominant_symbol, 0) == total_target_quantity
|
||||
new_pos_correct = (
|
||||
latest_positions.get(current_dominant_symbol, 0)
|
||||
== total_target_quantity
|
||||
)
|
||||
|
||||
if all_old_cleared and new_pos_correct:
|
||||
print("-" * 70)
|
||||
print(f"TqsdkEngine ({self.product_id}): [换月成功] 移仓操作已确认完成。")
|
||||
print(
|
||||
f"TqsdkEngine ({self.product_id}): [换月成功] 移仓操作已确认完成。"
|
||||
)
|
||||
print(f" - 所有旧合约持仓已清零。")
|
||||
print(f" - 新合约 {current_dominant_symbol} 持仓: {total_target_quantity}")
|
||||
print(
|
||||
f" - 新合约 {current_dominant_symbol} 持仓: {total_target_quantity}"
|
||||
)
|
||||
print("-" * 70)
|
||||
|
||||
# 6.3 通知策略层 (只需通知一次)
|
||||
if hasattr(self._strategy, "on_rollover"):
|
||||
# 传递第一个旧合约符号作为代表
|
||||
representative_old_symbol = list(old_contracts_to_rollover.keys())[0]
|
||||
self._strategy.on_rollover(representative_old_symbol, current_dominant_symbol)
|
||||
representative_old_symbol = list(old_contracts_to_rollover.keys())[
|
||||
0
|
||||
]
|
||||
self._strategy.on_rollover(
|
||||
representative_old_symbol, current_dominant_symbol
|
||||
)
|
||||
|
||||
break # 成功,跳出循环
|
||||
break # 成功,跳出循环
|
||||
|
||||
def _check_boundary_and_close(self):
|
||||
"""
|
||||
检查上一根bar是否新开仓且触及边界价,如果是则平仓。
|
||||
只在实盘循环中调用,避免预热阶段误平仓。
|
||||
使用 self.klines.iloc[-2] 获取上一根K线数据。
|
||||
"""
|
||||
import math
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
# 获取上一根K线数据
|
||||
prev_kline = self.klines.iloc[-2]
|
||||
|
||||
current_positions = self._context.get_current_positions()
|
||||
current_qty = current_positions.get(self._last_underlying_symbol, 0)
|
||||
|
||||
# 检测是否是上一根bar新开仓:上一根bar没有持仓,但现在有持仓
|
||||
if not self.prev_bar_had_position and current_qty != 0:
|
||||
avg_price = self._context.get_average_position_price(
|
||||
self._last_underlying_symbol
|
||||
)
|
||||
|
||||
if avg_price is not None:
|
||||
# 修复1:使用 math.isclose 解决浮点数精度问题
|
||||
is_long_boundary = (current_qty > 0) and math.isclose(
|
||||
avg_price, prev_kline.low, abs_tol=1e-5
|
||||
)
|
||||
is_short_boundary = (current_qty < 0) and math.isclose(
|
||||
avg_price, prev_kline.high, abs_tol=1e-5
|
||||
)
|
||||
|
||||
if is_long_boundary or is_short_boundary:
|
||||
direction = "CLOSE_LONG" if current_qty > 0 else "CLOSE_SHORT"
|
||||
boundary_price = (
|
||||
prev_kline.low if current_qty > 0 else prev_kline.high
|
||||
)
|
||||
|
||||
print("=" * 60)
|
||||
print(f"🚨 [务实对齐] 触发边界防御机制!")
|
||||
print(
|
||||
f" - 持仓方向: {'多仓' if current_qty > 0 else '空仓'} ({current_qty}手)"
|
||||
)
|
||||
print(f" - 物理均价: {avg_price} == 上根K线极值: {boundary_price}")
|
||||
print(
|
||||
f" - 说明: 极大概率在回测中不会成交,立即物理对齐以保护策略状态!"
|
||||
)
|
||||
print("=" * 60)
|
||||
|
||||
# 修复2:补全 Order 必需参数,防止框架报错
|
||||
close_order = Order(
|
||||
id=f"SYS_ALIGN_CLOSE_{int(time.time() * 1000)}",
|
||||
symbol=self._last_underlying_symbol,
|
||||
direction=direction,
|
||||
volume=abs(current_qty),
|
||||
price_type="MARKET",
|
||||
offset="CLOSE",
|
||||
submitted_time=datetime.now(),
|
||||
)
|
||||
|
||||
self._context.send_order(close_order)
|
||||
self._process_queued_requests()
|
||||
|
||||
# 修复3:阻塞等待 TargetPosTask 执行完毕,防止策略复读
|
||||
wait_timeout = time.monotonic() + 10 # 最大等待 10 秒
|
||||
while True:
|
||||
self._api.wait_update()
|
||||
temp_qty = self._context.get_current_positions().get(
|
||||
self._last_underlying_symbol, 0
|
||||
)
|
||||
if temp_qty == 0:
|
||||
print("✅ [务实对齐] 平仓确认完成,账户已归零。")
|
||||
current_qty = 0 # 同步本地状态
|
||||
break
|
||||
if time.monotonic() > wait_timeout:
|
||||
print(
|
||||
"⚠️ [务实对齐] 警告: 平仓确认超时,策略可能陷入混乱状态!"
|
||||
)
|
||||
break
|
||||
|
||||
# 更新状态:记录本根bar开始时的持仓情况(供下一根bar检测使用)
|
||||
self.prev_bar_had_position = current_qty != 0
|
||||
|
||||
def main(self, kline_row, prev_kline_row):
|
||||
kline_dt = pd.to_datetime(kline_row.datetime, unit="ns", utc=True)
|
||||
@@ -584,7 +724,9 @@ class TqsdkEngine:
|
||||
|
||||
if self.partial_bar is not None:
|
||||
last_bar = Bar(
|
||||
datetime=pd.to_datetime(prev_kline_row.datetime, unit="ns", utc=True).tz_convert(BEIJING_TZ),
|
||||
datetime=pd.to_datetime(
|
||||
prev_kline_row.datetime, unit="ns", utc=True
|
||||
).tz_convert(BEIJING_TZ),
|
||||
symbol=self.partial_bar.symbol,
|
||||
open=prev_kline_row.open,
|
||||
high=prev_kline_row.high,
|
||||
@@ -605,7 +747,6 @@ class TqsdkEngine:
|
||||
|
||||
self.last_processed_bar = last_bar
|
||||
|
||||
|
||||
self._strategy.on_open_bar(kline_row.open, self._last_underlying_symbol)
|
||||
# 处理订单和取消请求
|
||||
if self._strategy.trading is True:
|
||||
|
||||
Reference in New Issue
Block a user