From d081eeb265d3bee213994eebff54c96b2246916a Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 12 Jun 2026 20:47:03 +0800 Subject: [PATCH] feat(backtest): add SquareRootSlippage + VolumeSlippage --- src/easy_tdx/backtest/slippage.py | 60 ++++++++++++++++++++++ tests/unit/test_backtest_slippage.py | 74 ++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/src/easy_tdx/backtest/slippage.py b/src/easy_tdx/backtest/slippage.py index 7ac581d..70a0dd8 100644 --- a/src/easy_tdx/backtest/slippage.py +++ b/src/easy_tdx/backtest/slippage.py @@ -4,6 +4,8 @@ from __future__ import annotations from abc import ABC, abstractmethod +import numpy as np + class SlippageModel(ABC): """滑点模型基类。 @@ -67,3 +69,61 @@ class PercentSlippage(SlippageModel): direction: str, ) -> float: return price * size * self._rate + + +class SquareRootSlippage(SlippageModel): + """方根市场冲击模型(Almgren-Chriss 简化版)。 + + impact = σ × √(participation_rate) × price × size × impact_coeff + + 当 volume=0 或 volatility=0 时退化为 PercentSlippage(rate=0.001)。 + """ + + def __init__(self, impact_coeff: float = 0.1) -> None: + self._impact_coeff = impact_coeff + self._fallback = PercentSlippage(rate=0.001) + + def compute( + self, + price: float, + size: float, + volume: float, + volatility: float, + direction: str, + ) -> float: + if size <= 0: + return 0.0 + if volume <= 0 or volatility <= 0: + return self._fallback.compute(price, size, volume, volatility, direction) + participation_rate = min(size / volume, 1.0) + impact = volatility * np.sqrt(participation_rate) * price * size * self._impact_coeff + return float(impact) + + +class VolumeSlippage(SlippageModel): + """成交量比例滑点。 + + cost = (base_bps / 10000) × (size / volume) × price × size + + 当 volume=0 时退化为 PercentSlippage(rate=base_bps/10000)。 + """ + + def __init__(self, base_bps: float = 10.0) -> None: + self._base_bps = base_bps + self._fallback = PercentSlippage(rate=base_bps / 10000.0) + + def compute( + self, + price: float, + size: float, + volume: float, + volatility: float, + direction: str, + ) -> float: + if size <= 0: + return 0.0 + if volume <= 0: + return self._fallback.compute(price, size, volume, volatility, direction) + rate = self._base_bps / 10000.0 + participation = min(size / volume, 1.0) + return rate * participation * price * size diff --git a/tests/unit/test_backtest_slippage.py b/tests/unit/test_backtest_slippage.py index 241ef8f..b2b2e2e 100644 --- a/tests/unit/test_backtest_slippage.py +++ b/tests/unit/test_backtest_slippage.py @@ -8,6 +8,8 @@ from easy_tdx.backtest.slippage import ( FixedSlippage, PercentSlippage, SlippageModel, + SquareRootSlippage, + VolumeSlippage, ) @@ -85,3 +87,75 @@ class TestPercentSlippage: cost = model.compute(price=100.0, size=500, volume=20000, volatility=0.25, direction="BUY") # 100 × 500 × 0.002 = 100.0 assert cost == pytest.approx(100.0) + + +class TestSquareRootSlippage: + """方根市场冲击模型。""" + + def test_zero_size(self) -> None: + """size=0 时无冲击。""" + model = SquareRootSlippage(impact_coeff=0.1) + cost = model.compute(price=10.0, size=0, volume=10000, volatility=0.3, direction="BUY") + assert cost == 0.0 + + def test_small_participation_rate(self) -> None: + """低参与率(小单),冲击成本低。""" + model = SquareRootSlippage(impact_coeff=0.1) + # size=100, volume=1000000, participation_rate=0.0001 + cost = model.compute( + price=10.0, size=100, volume=1_000_000, volatility=0.3, direction="BUY" + ) + # σ=0.3, √(0.0001)=0.01, impact = 0.3 × 0.01 × 10 × 100 × 0.1 = 0.3 + assert cost == pytest.approx(0.3) + + def test_high_participation_rate(self) -> None: + """高参与率(大单),冲击成本高。""" + model = SquareRootSlippage(impact_coeff=0.1) + cost = model.compute( + price=10.0, size=100_000, volume=200_000, volatility=0.3, direction="BUY" + ) + small_cost = model.compute( + price=10.0, size=100, volume=1_000_000, volatility=0.3, direction="BUY" + ) + assert cost > small_cost * 10 + + def test_zero_volume_fallback(self) -> None: + """volume=0 时退化为 PercentSlippage(rate=0.001)。""" + model = SquareRootSlippage(impact_coeff=0.1) + cost = model.compute(price=10.0, size=100, volume=0, volatility=0.3, direction="BUY") + assert cost == pytest.approx(1.0) + + def test_zero_volatility_fallback(self) -> None: + """volatility=0 时退化为 PercentSlippage(rate=0.001)。""" + model = SquareRootSlippage(impact_coeff=0.1) + cost = model.compute(price=10.0, size=100, volume=10000, volatility=0.0, direction="BUY") + assert cost == pytest.approx(1.0) + + +class TestVolumeSlippage: + """成交量比例滑点。""" + + def test_zero_size(self) -> None: + model = VolumeSlippage(base_bps=10.0) + cost = model.compute(price=10.0, size=0, volume=10000, volatility=0.3, direction="BUY") + assert cost == 0.0 + + def test_basic(self) -> None: + model = VolumeSlippage(base_bps=10.0) + cost = model.compute(price=10.0, size=100, volume=10000, volatility=0.3, direction="BUY") + assert cost == pytest.approx(0.01) + + def test_high_participation(self) -> None: + model = VolumeSlippage(base_bps=10.0) + cost_high = model.compute( + price=10.0, size=5000, volume=10000, volatility=0.3, direction="BUY" + ) + cost_low = model.compute( + price=10.0, size=100, volume=10000, volatility=0.3, direction="BUY" + ) + assert cost_high > cost_low + + def test_zero_volume_fallback(self) -> None: + model = VolumeSlippage(base_bps=10.0) + cost = model.compute(price=10.0, size=100, volume=0, volatility=0.3, direction="BUY") + assert cost == pytest.approx(1.0)