"""资产配置与组合诊断(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", )