Files
Mutual_Fund/agent/advisor_agent/intent/rebalance.py
T

108 lines
3.6 KiB
Python

"""组合偏离度与调仓建议计算。"""
from __future__ import annotations
from collections import defaultdict
from decimal import Decimal, ROUND_HALF_UP
from typing import Iterable
from common.common_const import (
CUSTOMER_REL_STATUS_SIGNED,
ERR_CODE_NOT_SIGNED_REBALANCE,
)
from common.suitability import check_suitability
from utils.exceptions import ApiError
_MONEY = Decimal("0.01")
_PERCENT = Decimal("100")
def _money(value: Decimal) -> Decimal:
return value.quantize(_MONEY, rounding=ROUND_HALF_UP)
def build_rebalance_plan(
*,
relation_status: str,
customer_risk: str,
holdings: Iterable[dict],
target_allocation: dict[str, int | float | Decimal],
threshold: Decimal,
candidates: Iterable[dict],
) -> dict | None:
if relation_status != CUSTOMER_REL_STATUS_SIGNED:
raise ApiError(ERR_CODE_NOT_SIGNED_REBALANCE, "客户尚未签约,禁止生成调仓草稿")
values: dict[str, Decimal] = defaultdict(Decimal)
holdings_by_class: dict[str, list[dict]] = defaultdict(list)
for holding in holdings:
asset_class = str(holding.get("asset_class", ""))
value = Decimal(str(holding.get("market_value", 0) or 0))
values[asset_class] += value
holdings_by_class[asset_class].append(holding)
total = sum(values.values(), Decimal("0"))
if total <= 0:
return None
target = {
asset_class: Decimal(str(weight)) for asset_class, weight in target_allocation.items()
}
deviation: dict[str, Decimal] = {}
for asset_class in target:
actual = values.get(asset_class, Decimal("0")) / total * _PERCENT
deviation[asset_class] = (actual - target[asset_class]).quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
)
if not any(abs(value) > threshold for value in deviation.values()):
return None
sell: list[dict] = []
buy: list[dict] = []
for asset_class, drift in deviation.items():
if drift > threshold:
target_value = total * target[asset_class] / _PERCENT
excess = _money(values.get(asset_class, Decimal("0")) - target_value)
remaining = excess
for holding in holdings_by_class.get(asset_class, []):
amount = min(
remaining,
_money(Decimal(str(holding.get("market_value", 0) or 0))),
)
if amount > 0:
sell.append(
{
"product_code": holding.get("product_code"),
"asset_class": asset_class,
"amount": amount,
}
)
remaining -= amount
if remaining <= 0:
break
elif drift < -threshold:
target_value = total * target[asset_class] / _PERCENT
amount = _money(target_value - values.get(asset_class, Decimal("0")))
for candidate in candidates:
if candidate.get("asset_class") != asset_class:
continue
if not check_suitability(
customer_risk, candidate.get("risk_level", "")
).ok:
continue
buy.append(
{
"product_code": candidate.get("product_code"),
"asset_class": asset_class,
"amount": amount,
}
)
break
return {
"deviation": deviation,
"sell": sell,
"buy": buy,
}