stock-ai-agent/backend/tests/test_position_sizing_regression.py
2026-04-28 15:48:42 +08:00

186 lines
6.1 KiB
Python

"""
仓位 sizing 回归测试
覆盖重点:
- 中线信号默认仓位降到 light
- 总杠杆限制按“名义仓位空间 -> 保证金”正确换算
- 模拟盘回退仓位计算不再按可用保证金倍数放大
"""
import importlib.util
import sys
import types
from pathlib import Path
from unittest.mock import MagicMock
import pytest
def load_position_sizing_module():
module_path = Path(__file__).resolve().parents[1] / "app" / "services" / "position_sizing.py"
if "app" not in sys.modules:
app_pkg = types.ModuleType("app")
app_pkg.__path__ = [str(module_path.parents[2] / "app")]
sys.modules["app"] = app_pkg
if "app.services" not in sys.modules:
services_pkg = types.ModuleType("app.services")
services_pkg.__path__ = [str(module_path.parent)]
sys.modules["app.services"] = services_pkg
if "app.utils" not in sys.modules:
utils_pkg = types.ModuleType("app.utils")
utils_pkg.__path__ = [str(module_path.parents[1] / "utils")]
sys.modules["app.utils"] = utils_pkg
logger_module = types.ModuleType("app.utils.logger")
logger_module.logger = MagicMock()
sys.modules["app.utils.logger"] = logger_module
module_name = "app.services.position_sizing_test"
spec = importlib.util.spec_from_file_location(module_name, module_path)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module
def load_paper_trading_service_class():
service_path = Path(__file__).resolve().parents[1] / "app" / "services" / "paper_trading_service.py"
position_sizing_module = load_position_sizing_module()
sys.modules["app.services.position_sizing"] = position_sizing_module
if "app.models" not in sys.modules:
models_pkg = types.ModuleType("app.models")
models_pkg.__path__ = [str(service_path.parents[1] / "models")]
sys.modules["app.models"] = models_pkg
config_module = types.ModuleType("app.config")
config_module.get_settings = MagicMock(return_value=MagicMock())
sys.modules["app.config"] = config_module
datetime_module = types.ModuleType("app.utils.datetime_utils")
datetime_module.get_beijing_time = MagicMock()
sys.modules["app.utils.datetime_utils"] = datetime_module
db_module = types.ModuleType("app.services.db_service")
db_module.db_service = MagicMock()
sys.modules["app.services.db_service"] = db_module
paper_models_module = types.ModuleType("app.models.paper_trading")
paper_models_module.PaperOrder = object
paper_models_module.OrderStatus = types.SimpleNamespace(
PENDING="pending",
OPEN="open",
CLOSED_TP="closed_tp",
CLOSED_SL="closed_sl",
CLOSED_BE="closed_be",
CLOSED_TS="closed_ts",
CLOSED_MANUAL="closed_manual",
CANCELLED="cancelled",
)
paper_models_module.OrderSide = types.SimpleNamespace(LONG="long", SHORT="short")
paper_models_module.SignalGrade = lambda value: value
paper_models_module.EntryType = types.SimpleNamespace(MARKET="market", LIMIT="limit")
sys.modules["app.models.paper_trading"] = paper_models_module
module_name = "app.services.paper_trading_service_test"
spec = importlib.util.spec_from_file_location(module_name, service_path)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module.PaperTradingService
def test_medium_term_defaults_to_light_margin_budget():
module = load_position_sizing_module()
target_pct, _, position_size, grade = module.resolve_target_margin_pct(
position_size=None,
signal_type="medium_term",
confidence=75,
)
assert position_size == "light"
assert grade == "B"
assert target_pct == pytest.approx(0.08)
def test_total_leverage_cap_is_converted_to_margin_cap():
module = load_position_sizing_module()
margin, position_value, _ = module.calculate_margin_and_position_value(
balance=20000,
available_margin=12000,
current_total_leverage=9.5,
max_total_leverage=10,
order_leverage=10,
target_margin_pct=0.18,
max_margin_pct=0.25,
)
assert margin == pytest.approx(1000.0)
assert position_value == pytest.approx(10000.0)
def test_paper_dynamic_position_uses_equity_pct_instead_of_margin_multiple():
PaperTradingService = load_paper_trading_service_class()
service = PaperTradingService.__new__(PaperTradingService)
service.leverage = 10
service.max_total_leverage = 10
service.get_account_status = MagicMock(
return_value={
"current_balance": 20000,
"used_margin": 0,
"current_total_leverage": 0,
"max_total_leverage": 10,
}
)
margin, position_value = service._calculate_dynamic_position(
position_size="medium",
symbol="ETHUSDT",
signal_type="medium_term",
confidence=75,
grade="B",
)
assert margin == pytest.approx(2400.0)
assert position_value == pytest.approx(24000.0)
def test_setup_profile_can_cap_margin_budget_more_conservatively():
module = load_position_sizing_module()
margin, position_value, _ = module.calculate_margin_and_position_value(
balance=20000,
available_margin=19000,
current_total_leverage=0,
max_total_leverage=10,
order_leverage=10,
target_margin_pct=0.12 * 0.55,
max_margin_pct=0.08,
)
assert margin == pytest.approx(1320.0)
assert position_value == pytest.approx(13200.0)
def test_min_position_value_floor_can_raise_too_small_contract_position():
module = load_position_sizing_module()
margin, position_value, _ = module.calculate_margin_and_position_value(
balance=1400,
available_margin=1400,
current_total_leverage=0,
max_total_leverage=10,
order_leverage=10,
target_margin_pct=0.028, # 原本只会开到约 392U 名义仓位
max_margin_pct=0.25,
min_position_value=1400,
)
assert margin == pytest.approx(140.0)
assert position_value == pytest.approx(1400.0)