97 lines
3.2 KiB
Python
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",
|
|
)
|