feat:新增投顾agent和nl2sqlagent
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
"""组合偏离度与调仓建议计算。"""
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user