Files
group_fqcd_jr/tests/unit/service/test_asset_allocation_service.py
T

154 lines
5.5 KiB
Python

from decimal import Decimal
import pytest
from app.core.contracts import RequestContext
from app.model.product import ProductMarketQuoteSnapshot
from app.service.asset_allocation_service import AssetAllocationService
from app.service.dynamic_allocation_optimizer import AssetClassMarketMetric
def test_short_horizon_and_daily_liquidity_increase_cash_allocation() -> None:
weights = AssetAllocationService._weights("C4", 12, "daily", Decimal("30"))
assert weights == {"cash_management_etf": 35, "bond_etf": 20, "equity_etf": 45}
assert sum(weights.values()) == 100
def test_drawdown_cap_limits_equity_and_preserves_total_weight() -> None:
weights = AssetAllocationService._weights("C5", 60, "within_30_days", Decimal("10"))
assert weights["equity_etf"] == 20
assert sum(weights.values()) == 100
@pytest.mark.asyncio
async def test_allocation_uses_internal_profile_without_returning_risk_level(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def profile(_self: object, _context: RequestContext) -> dict[str, object]:
return {"risk_level": "C3"}
async def goal(_self: object, _context: RequestContext) -> dict[str, object]:
return {
"status": "confirmed",
"annualized_return_lower_pct": "6",
"investment_horizon_months": 36,
"liquidity_requirement": "within_30_days",
"max_drawdown_pct": "15",
"benchmark_name": "test benchmark",
}
monkeypatch.setattr(
"app.service.asset_allocation_service.CustomerProfileService.current_for_agent", profile
)
monkeypatch.setattr(
"app.service.asset_allocation_service.InvestmentGoalService.current_for_agent", goal
)
result = await AssetAllocationService().generate_for_agent(RequestContext(
user_id="7",
trace_id="allocation-test",
roles=("customer",),
permissions=(
"asset-allocation:generate:self",
"customer-profile:read:self",
"investment-goal:read:self",
),
))
assert result["status"] == "ready"
assert "risk_level" not in result
assert sum(item["target_pct"] for item in result["allocation"]) == 100
@pytest.mark.asyncio
async def test_allocation_applies_dynamic_metrics_to_the_strategic_baseline(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def profile(_self: object, _context: RequestContext) -> dict[str, object]:
return {"risk_level": "C3"}
async def goal(_self: object, _context: RequestContext) -> dict[str, object]:
return {
"status": "confirmed",
"annualized_return_lower_pct": "6",
"investment_horizon_months": 36,
"liquidity_requirement": "within_30_days",
"max_drawdown_pct": "15",
"benchmark_name": "test benchmark",
}
async def metrics(_risk_level: str) -> list[AssetClassMarketMetric]:
return [
AssetClassMarketMetric("cash_management_etf", Decimal("1"), Decimal("0"),
Decimal("50000000"), 1),
AssetClassMarketMetric("bond_etf", Decimal("5"), Decimal("-3"),
Decimal("8000000"), 2),
AssetClassMarketMetric("equity_etf", Decimal("15"), Decimal("-20"),
Decimal("10000000"), 3),
]
monkeypatch.setattr(
"app.service.asset_allocation_service.CustomerProfileService.current_for_agent", profile
)
monkeypatch.setattr(
"app.service.asset_allocation_service.InvestmentGoalService.current_for_agent", goal
)
result = await AssetAllocationService(market_metric_loader=metrics).generate_for_agent(
RequestContext(
user_id="7",
trace_id="dynamic-allocation-test",
roles=("customer",),
permissions=(
"asset-allocation:generate:self",
"customer-profile:read:self",
"investment-goal:read:self",
),
)
)
assert result["optimization"]["dynamic"] is True
assert result["optimization"]["metric_coverage_pct"] == "100.00"
assert result["optimization"]["factor_evidence"]
assert result["optimization"]["strategic_allocation"] != {
item["asset_class"]: item["target_pct"] for item in result["allocation"]
}
def test_liquidity_prefers_history_then_uses_quote_evidence() -> None:
quote = ProductMarketQuoteSnapshot(
id=1,
product_id=1,
observed_at=None,
source="test",
last_price=Decimal("1.25"),
volume=Decimal("400"),
turnover_amount=Decimal("60000"),
quote_status="active",
created_at=None,
)
assert AssetAllocationService._liquidity(Decimal("70000"), quote) == (
Decimal("70000"),
"historical_turnover",
)
assert AssetAllocationService._liquidity(None, quote) == (
Decimal("60000"),
"latest_quote_turnover",
)
def test_liquidity_estimates_from_quote_volume_or_reports_unavailable() -> None:
quote = ProductMarketQuoteSnapshot(
id=1,
product_id=1,
observed_at=None,
source="test",
last_price=Decimal("1.25"),
volume=Decimal("400"),
turnover_amount=None,
quote_status="active",
created_at=None,
)
assert AssetAllocationService._liquidity(None, quote) == (
Decimal("50000"),
"estimated_from_quote_volume",
)
assert AssetAllocationService._liquidity(None, None) == (None, "unavailable")