feat(risk): 实现风控层与回调钩子机制(融合Freqtrade设计)

核心组件:
- RiskControl: 风控抽象基类
- StopLossControl: 止损控制(固定止损/跟踪止损)
- PositionLimitControl: 仓位限制控制
- PremiumControl: 溢价控制(filter/penalize模式)

回调钩子机制:
- CallbackHook: 回调管理器(注册/触发)
- 5个核心回调:before_entry, after_entry, before_exit, after_exit, dynamic_stoploss, custom_exit

便捷回调函数:
- premium_filter_callback: 溢价过滤回调
- crash_filter_callback: 崩盘检测回调
- holding_time_stoploss_callback: 持仓时间动态止损

测试覆盖:13个测试全部通过
This commit is contained in:
2026-05-11 22:18:41 +08:00
parent f5e6202eee
commit 512b73ac04
2 changed files with 644 additions and 0 deletions

View File

@@ -0,0 +1,293 @@
"""
风控层测试
测试RiskControl、StopLossControl、PositionLimitControl、CallbackHook
"""
import pandas as pd
import pytest
from datetime import datetime, timedelta
from framework.risk import (
RiskControl, StopLossControl, PositionLimitControl, PremiumControl,
CallbackHook, Position, Trade,
premium_filter_callback, crash_filter_callback, holding_time_stoploss_callback
)
class TestPosition:
"""测试持仓信息"""
def test_position_profit(self):
"""测试盈亏计算"""
position = Position(
code='code1',
entry_price=100.0,
entry_date=datetime(2020, 1, 1),
current_price=110.0,
current_date=datetime(2020, 1, 10),
quantity=100,
weight=0.33
)
assert position.profit_ratio == 0.10
assert position.is_profit == True
assert position.holding_days == 9
def test_position_loss(self):
"""测试亏损计算"""
position = Position(
code='code1',
entry_price=100.0,
entry_date=datetime(2020, 1, 1),
current_price=95.0,
current_date=datetime(2020, 1, 5),
quantity=100,
weight=0.33
)
assert position.profit_ratio == -0.05
assert position.is_profit == False
assert position.holding_days == 4
class TestStopLossControl:
"""测试止损控制"""
def test_fixed_stoploss_check(self):
"""测试固定止损检查"""
control = StopLossControl(threshold=-0.05)
# 未触发止损
position = Position(
code='code1',
entry_price=100.0,
entry_date=datetime(2020, 1, 1),
current_price=96.0,
current_date=datetime(2020, 1, 5),
quantity=100,
weight=0.33
)
assert control.check(position) == True
# 触发止损
position.current_price = 94.0
assert control.check(position) == False
def test_fixed_stoploss_apply(self):
"""测试固定止损应用"""
control = StopLossControl(threshold=-0.05)
position = Position(
code='code1',
entry_price=100.0,
entry_date=datetime(2020, 1, 1),
current_price=95.0,
current_date=datetime(2020, 1, 5),
quantity=100,
weight=0.33
)
stop_price = control.apply(position)
assert stop_price == 95.0 # 100 * (1 - 0.05)
def test_trailing_stoploss(self):
"""测试跟踪止损"""
control = StopLossControl(trailing=True, trailing_percent=0.03)
position = Position(
code='code1',
entry_price=100.0,
entry_date=datetime(2020, 1, 1),
current_price=105.0,
current_date=datetime(2020, 1, 5),
quantity=100,
weight=0.33
)
# 最高价更新为105
control.check(position)
assert control._highest_price['code1'] == 105.0
# 当前价回撤到101从105回撤4%超过3%阈值
position.current_price = 101.0
assert control.check(position) == False
# 止损价格应为 105 * (1 - 0.03) = 101.85
stop_price = control.apply(position)
assert abs(stop_price - 101.85) < 0.01
class TestPositionLimitControl:
"""测试仓位限制控制"""
def test_position_limit_check(self):
"""测试仓位限制检查"""
control = PositionLimitControl(max_position=0.33)
# 仓位未超限
position = Position(
code='code1',
entry_price=100.0,
entry_date=datetime(2020, 1, 1),
current_price=105.0,
current_date=datetime(2020, 1, 5),
quantity=100,
weight=0.30
)
assert control.check(position) == True
# 仓位超限
position.weight = 0.40
assert control.check(position) == False
def test_position_limit_apply(self):
"""测试仓位限制应用"""
control = PositionLimitControl(max_position=0.33)
position = Position(
code='code1',
entry_price=100.0,
entry_date=datetime(2020, 1, 1),
current_price=105.0,
current_date=datetime(2020, 1, 5),
quantity=100,
weight=0.50
)
suggested_weight = control.apply(position)
assert suggested_weight == 0.33
class TestPremiumControl:
"""测试溢价控制"""
def test_premium_filter(self):
"""测试溢价过滤"""
control = PremiumControl(threshold=0.10, mode='filter')
# 溢价未超限
assert control.check(None, premium=0.05) == True
# 溢价超限
assert control.check(None, premium=0.15) == False
def test_premium_penalize(self):
"""测试溢价降权"""
control = PremiumControl(threshold=0.10, mode='penalize')
# 降权模式下允许通过
assert control.check(None, premium=0.15) == True
# 返回降权系数
position = Position(
code='code1',
entry_price=100.0,
entry_date=datetime(2020, 1, 1),
current_price=105.0,
current_date=datetime(2020, 1, 5),
quantity=100,
weight=0.33
)
penalty = control.apply(position)
assert penalty == 0.5
class TestCallbackHook:
"""测试回调钩子"""
def test_register_hook(self):
"""测试注册回调"""
hook = CallbackHook()
def dummy_callback(code, price):
return True
hook.register('before_entry', dummy_callback)
assert len(hook._hooks['before_entry']) == 1
def test_trigger_before_entry(self):
"""测试触发入场前回调"""
hook = CallbackHook()
# 注册溢价过滤回调
hook.register('before_entry', premium_filter_callback(threshold=0.10))
# 溢价正常,允许入场
result = hook.trigger('before_entry', 'code1', 100.0, premium=0.05)
assert result == True
# 溢价过高,拒绝入场
result = hook.trigger('before_entry', 'code1', 100.0, premium=0.15)
assert result == False
def test_trigger_dynamic_stoploss(self):
"""测试触发动态止损回调"""
hook = CallbackHook()
# 注册持仓时间止损回调
hook.register('dynamic_stoploss', holding_time_stoploss_callback())
# 持仓5天止损-5%
position = Position(
code='code1',
entry_price=100.0,
entry_date=datetime(2020, 1, 1),
current_price=95.0,
current_date=datetime(2020, 1, 6),
quantity=100,
weight=0.33
)
stoploss = hook.trigger('dynamic_stoploss', position)
# holding_days=5返回-0.05
assert stoploss == -0.05
def test_trigger_custom_exit(self):
"""测试触发自定义出场回调"""
hook = CallbackHook()
def reversal_exit_callback(position):
# 反转信号触发出场
return position.profit_ratio < -0.02
hook.register('custom_exit', reversal_exit_callback)
# 未触发出场
position = Position(
code='code1',
entry_price=100.0,
entry_date=datetime(2020, 1, 1),
current_price=99.0,
current_date=datetime(2020, 1, 5),
quantity=100,
weight=0.33
)
result = hook.trigger('custom_exit', position)
assert result == False
# 触发出场
position.current_price = 97.0
result = hook.trigger('custom_exit', position)
assert result == True
def test_multiple_callbacks(self):
"""测试多个回调组合"""
hook = CallbackHook()
# 注册多个入场前回调
hook.register('before_entry', premium_filter_callback(0.10))
hook.register('before_entry', lambda code, price, **kwargs: price > 50)
# 溢价正常 + 价格>50允许入场
result = hook.trigger('before_entry', 'code1', 100.0, premium=0.05)
assert result == True
# 溢价过高拒绝入场任一回调返回False
result = hook.trigger('before_entry', 'code1', 100.0, premium=0.15)
assert result == False
# 价格过低,拒绝入场
result = hook.trigger('before_entry', 'code1', 40.0, premium=0.05)
assert result == False
if __name__ == '__main__':
pytest.main([__file__, '-v'])