Files

97 lines
3.2 KiB
Python

"""资产配置与组合诊断(PRD §4.3,只读)。
边界:持仓诊断仅做「持仓分布 + 集中度」只读概览;偏离度/调仓建议以投顾Agent 草稿为准,
工作台不重复实现调仓逻辑。标准策略库=portfolio_benchmark 只读;白名单基金=fin_product 筛选。
"""
from __future__ import annotations
from decimal import Decimal
from sqlalchemy.ext.asyncio import AsyncSession
from model.sys_user import SysUser
from repositories.fin_holdings import FinHoldingsRepo
from repositories.fin_product import FinProductRepo
from repositories.portfolio_benchmark import PortfolioBenchmarkRepo
from service.advisor.permissions import ensure_customer_owned
from service.product import list_products
# 持仓中状态(与 service/holdings.py 口径一致)
_HOLDING_STATUS = "持有中"
async def diagnose(db: AsyncSession, user: SysUser, customer_id: int) -> dict:
await ensure_customer_owned(db, user.id, customer_id)
holdings = await FinHoldingsRepo(db).list_by_customer(customer_id, _HOLDING_STATUS)
product_repo = FinProductRepo(db)
by_type: dict[str, Decimal] = {}
total = Decimal("0")
items: list[dict] = []
for h in holdings:
product = await product_repo.get(h.product_id)
ptype = product.product_type if product else "未知"
total += h.current_value
by_type[ptype] = by_type.get(ptype, Decimal("0")) + h.current_value
items.append(
{
"product_id": h.product_id,
"product_code": product.product_code if product else None,
"product_name": product.product_name if product else None,
"product_type": ptype,
"current_value": f"{h.current_value:.2f}",
}
)
# 集中度:单一产品市值占比最高者
items.sort(key=lambda x: float(x["current_value"]), reverse=True)
top_ratio = 0.0
if total and items:
top_ratio = float(items[0]["current_value"]) / float(total)
return {
"total_value": f"{total:.2f}",
"allocation": {k: f"{v:.2f}" for k, v in by_type.items()},
"concentration": {
"top_product_ratio": round(top_ratio, 4),
"top_product": items[0] if items else None,
},
"holdings": items,
}
async def list_strategies(db: AsyncSession) -> list[dict]:
"""标准策略库(portfolio_benchmark 只读,投顾不可私自新建策略)。"""
rows = await PortfolioBenchmarkRepo(db).list_enabled()
return [
{
"risk_level": b.risk_level,
"target_allocation": b.target_allocation,
"drift_threshold": float(b.drift_threshold),
}
for b in rows
]
async def list_funds(
db: AsyncSession,
*,
page: int = 1,
page_size: int = 10,
keyword: str | None = None,
product_type: str | None = None,
risk_level: str | None = None,
) -> dict:
"""白名单产品筛选(复用 service/product.list_products,仅在售)。"""
return await list_products(
db,
page=page,
page_size=page_size,
keyword=keyword,
product_type=product_type,
risk_level=risk_level,
status="在售",
sort_by="create_time",
sort_order="desc",
)