mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 15:44:18 +08:00
fix(backtest): _find_bar_index 用 to_numpy().argmax() 取真实位置
idxmax() 返回 index label,后续 iloc[] 按位置取行;当 df.index 非默认 RangeIndex 时 label != position,撮合会取错 K 线。两处分支统一改为位置索引。 新增 2 例非连续 index 回归测试。
This commit is contained in:
@@ -144,19 +144,22 @@ class OrderSimulator:
|
|||||||
dt_col = self.df["datetime"]
|
dt_col = self.df["datetime"]
|
||||||
|
|
||||||
# 尝试直接比较(如果是 int 类型)
|
# 尝试直接比较(如果是 int 类型)
|
||||||
|
# 注意:用 to_numpy().argmax() 取位置索引,而非 idxmax()(返回 label),
|
||||||
|
# 因为后续 self.df.iloc[...] 按位置取行;若 df.index 非默认 RangeIndex,
|
||||||
|
# label != position 会导致撮合取错 bar。
|
||||||
try:
|
try:
|
||||||
idx = (dt_col == datetime_val).idxmax() if (dt_col == datetime_val).any() else None
|
mask = (dt_col == datetime_val).to_numpy()
|
||||||
if idx is not None:
|
if mask.any():
|
||||||
return int(idx)
|
return int(mask.argmax())
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 如果是 datetime 对象,转为 int 比较
|
# 如果是 datetime 对象,转为 int 比较
|
||||||
if pd.api.types.is_datetime64_any_dtype(dt_col):
|
if pd.api.types.is_datetime64_any_dtype(dt_col):
|
||||||
dt_ints = dt_col.dt.strftime("%Y%m%d").astype(int)
|
dt_ints = dt_col.dt.strftime("%Y%m%d").astype(int)
|
||||||
mask = dt_ints == datetime_val
|
mask_arr = (dt_ints == datetime_val).to_numpy()
|
||||||
if mask.any():
|
if mask_arr.any():
|
||||||
return int(mask.idxmax())
|
return int(mask_arr.argmax())
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -464,3 +464,41 @@ class TestSlippageModelIntegration:
|
|||||||
trades = sim.simulate(signals, cash=20000, position=0)
|
trades = sim.simulate(signals, cash=20000, position=0)
|
||||||
assert len(trades) == 1
|
assert len(trades) == 1
|
||||||
assert trades[0].slippage == pytest.approx(5.0)
|
assert trades[0].slippage == pytest.approx(5.0)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Test Non-Continuous Index ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class TestNonContinuousIndex:
|
||||||
|
"""df.index 非默认 RangeIndex 时,撮合应按位置(iloc)而非 label 取 bar。
|
||||||
|
|
||||||
|
回归 _find_bar_index 旧实现在非连续 index 下用 idxmax() 返回 label 当位置用,
|
||||||
|
导致 iloc 取错 bar / 越界。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_next_open_with_non_continuous_index(self) -> None:
|
||||||
|
"""信号在 bar 0(label=10),应在 bar 1(position)open 成交。"""
|
||||||
|
df = _make_df(10)
|
||||||
|
df.index = [10 * (i + 1) for i in range(len(df))] # [10,20,...,100]
|
||||||
|
sim = OrderSimulator(df, execution="next_open")
|
||||||
|
|
||||||
|
signals = [_buy_signal(0, size=100)]
|
||||||
|
trades = sim.simulate(signals, cash=20000, position=0)
|
||||||
|
|
||||||
|
assert len(trades) == 1
|
||||||
|
# position 1 的 open = 101.0;旧代码会用 label 10 当位置 → iloc[10] 越界
|
||||||
|
assert trades[0].price == 101.0
|
||||||
|
assert trades[0].rejected is False
|
||||||
|
|
||||||
|
def test_this_close_with_non_continuous_index(self) -> None:
|
||||||
|
"""this_close 模式下信号在 bar 2(label=30),应在同根 close 成交。"""
|
||||||
|
df = _make_df(10)
|
||||||
|
df.index = [10 * (i + 1) for i in range(len(df))]
|
||||||
|
sim = OrderSimulator(df, execution="this_close")
|
||||||
|
|
||||||
|
signals = [_buy_signal(2, size=100)]
|
||||||
|
trades = sim.simulate(signals, cash=20000, position=0)
|
||||||
|
|
||||||
|
assert len(trades) == 1
|
||||||
|
# position 2 的 close = 103.0
|
||||||
|
assert trades[0].price == 103.0
|
||||||
|
|||||||
Reference in New Issue
Block a user