mirror of
https://ghfast.top/https://github.com/aeroxw/easy-tdx.git
synced 2026-09-12 16:54:17 +08:00
162 lines
5.8 KiB
Python
162 lines
5.8 KiB
Python
"""滑点模型单元测试。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
|
||
from easy_tdx.backtest.slippage import (
|
||
FixedSlippage,
|
||
PercentSlippage,
|
||
SlippageModel,
|
||
SquareRootSlippage,
|
||
VolumeSlippage,
|
||
)
|
||
|
||
|
||
class TestSlippageBase:
|
||
"""基类验证。"""
|
||
|
||
def test_cannot_instantiate_abc(self) -> None:
|
||
"""不能直接实例化 ABC。"""
|
||
with pytest.raises(TypeError):
|
||
SlippageModel() # type: ignore[abstract]
|
||
|
||
def test_subclass_must_implement_compute(self) -> None:
|
||
"""子类必须实现 compute。"""
|
||
|
||
class BadModel(SlippageModel):
|
||
pass
|
||
|
||
with pytest.raises(TypeError):
|
||
BadModel() # type: ignore[abstract]
|
||
|
||
|
||
class TestFixedSlippage:
|
||
"""固定每股滑点。"""
|
||
|
||
def test_zero_per_share(self) -> None:
|
||
"""per_share=0 时无滑点。"""
|
||
model = FixedSlippage(per_share=0.0)
|
||
cost = model.compute(price=10.0, size=100, volume=10000, volatility=0.3, direction="BUY")
|
||
assert cost == 0.0
|
||
|
||
def test_basic(self) -> None:
|
||
"""基本计算:100 股 × 0.01 元/股 = 1.0。"""
|
||
model = FixedSlippage(per_share=0.01)
|
||
cost = model.compute(price=10.0, size=100, volume=10000, volatility=0.3, direction="BUY")
|
||
assert cost == pytest.approx(1.0)
|
||
|
||
def test_large_size(self) -> None:
|
||
"""大单。"""
|
||
model = FixedSlippage(per_share=0.05)
|
||
cost = model.compute(
|
||
price=50.0, size=10000, volume=500000, volatility=0.2, direction="SELL"
|
||
)
|
||
assert cost == pytest.approx(500.0)
|
||
|
||
def test_direction_irrelevant(self) -> None:
|
||
"""方向不影响固定滑点。"""
|
||
model = FixedSlippage(per_share=0.01)
|
||
buy_cost = model.compute(
|
||
price=10.0, size=100, volume=10000, volatility=0.3, direction="BUY"
|
||
)
|
||
sell_cost = model.compute(
|
||
price=10.0, size=100, volume=10000, volatility=0.3, direction="SELL"
|
||
)
|
||
assert buy_cost == sell_cost
|
||
|
||
|
||
class TestPercentSlippage:
|
||
"""按成交金额百分比滑点。"""
|
||
|
||
def test_zero_rate(self) -> None:
|
||
"""rate=0 时无滑点。"""
|
||
model = PercentSlippage(rate=0.0)
|
||
cost = model.compute(price=10.0, size=100, volume=10000, volatility=0.3, direction="BUY")
|
||
assert cost == 0.0
|
||
|
||
def test_basic(self) -> None:
|
||
"""10元 × 100股 × 0.001 = 1.0。"""
|
||
model = PercentSlippage(rate=0.001)
|
||
cost = model.compute(price=10.0, size=100, volume=10000, volatility=0.3, direction="BUY")
|
||
assert cost == pytest.approx(1.0)
|
||
|
||
def test_high_price(self) -> None:
|
||
"""高价股。"""
|
||
model = PercentSlippage(rate=0.002)
|
||
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)
|