Files
Mutual_Fund/service/advisor_agent/context.py

178 lines
6.0 KiB
Python

"""从现有业务表聚合投顾意图所需的本地上下文。"""
from __future__ import annotations
from decimal import Decimal, InvalidOperation
from common.common_const import (
CUSTOMER_REL_STATUS_SIGNED,
SYS_KEY_REBALANCE_DEVIATION_THRESHOLD,
)
from repositories.fin_customer_profile import FinCustomerProfileRepo
from repositories.fin_holdings import FinHoldingsRepo
from repositories.fin_product import FinProductRepo
from repositories.portfolio_benchmark import PortfolioBenchmarkRepo
from repositories.risk_assessment import RiskAssessmentRepo
from repositories.sys_config import SysConfigRepo
from model.fin_product import FinProduct
from service.advisor_agent.data import to_holding_input, to_product_candidate
from service.advisor_agent.data import to_fund_performance_row
async def _load_customer_risk(db, customer_id: int, profile_repo_cls, risk_repo_cls):
assessment = await risk_repo_cls(db).get_current_by_customer(customer_id)
if assessment is not None and assessment.risk_level:
return assessment.risk_level
profile = await profile_repo_cls(db).get_by_customer_id(customer_id)
return profile.risk_level if profile is not None else None
async def load_customer_risk(
db,
*,
customer_id: int,
profile_repo_cls=FinCustomerProfileRepo,
risk_repo_cls=RiskAssessmentRepo,
) -> str | None:
return await _load_customer_risk(db, customer_id, profile_repo_cls, risk_repo_cls)
async def load_rebalance_context(
db,
*,
customer_id: int,
profile_repo_cls=FinCustomerProfileRepo,
risk_repo_cls=RiskAssessmentRepo,
holdings_repo_cls=FinHoldingsRepo,
product_repo_cls=FinProductRepo,
benchmark_repo_cls=PortfolioBenchmarkRepo,
sys_config_repo_cls=SysConfigRepo,
) -> dict | None:
"""聚合画像、持仓、在售产品和组合基准,供调仓引擎使用。
关系签约状态由调用方读取并校验,本函数只负责客户投资数据。
"""
customer_risk = await _load_customer_risk(
db, customer_id, profile_repo_cls, risk_repo_cls
)
if not customer_risk:
return None
benchmark = await benchmark_repo_cls(db).get_active_by_risk(customer_risk)
if benchmark is None:
return None
threshold = benchmark.drift_threshold
if threshold is None:
raw_threshold = await sys_config_repo_cls(db).get_value(
SYS_KEY_REBALANCE_DEVIATION_THRESHOLD
)
try:
threshold = Decimal(str(raw_threshold))
except (InvalidOperation, TypeError, ValueError):
return None
if not threshold.is_finite() or threshold < 0:
return None
product_repo = product_repo_cls(db)
products = await product_repo.list(
where=[FinProduct.status == "在售"],
limit=1000,
)
products_by_id = {product.id: product for product in products}
holdings = await holdings_repo_cls(db).list_by_customer(customer_id, status="持有中")
holding_inputs = []
for holding in holdings:
product = products_by_id.get(holding.product_id)
if product is None:
product = await product_repo.get(holding.product_id)
if product is not None:
holding_inputs.append(to_holding_input(holding, product))
return {
"customer_id": customer_id,
"customer_risk": customer_risk,
"relation_status": CUSTOMER_REL_STATUS_SIGNED,
"holdings": holding_inputs,
"target_allocation": benchmark.target_allocation,
"threshold": threshold,
"candidates": [to_product_candidate(product) for product in products],
}
async def load_fund_analysis_context(
db,
*,
fund_codes: list[str],
product_repo_cls=FinProductRepo,
performance_repo_cls=None,
) -> list[dict]:
if performance_repo_cls is None:
from repositories.fund_performance import FundPerformanceRepo
performance_repo_cls = FundPerformanceRepo
product_repo = product_repo_cls(db)
performance_repo = performance_repo_cls(db)
result = []
for code in fund_codes:
product = await product_repo.get_by_code(code)
if product is None:
continue
performance = await performance_repo.list_for_product(product.id)
result.append(
{
"fund": {
"fund_code": product.product_code,
"fund_name": product.product_name,
"risk_level": product.risk_level,
},
"performance": [to_fund_performance_row(row) for row in performance],
}
)
return result
async def load_recommendation_context(
db,
*,
customer_id: int,
profile_repo_cls=FinCustomerProfileRepo,
product_repo_cls=FinProductRepo,
performance_repo_cls=None,
risk_repo_cls=RiskAssessmentRepo,
) -> dict | None:
customer_risk = await _load_customer_risk(
db, customer_id, profile_repo_cls, risk_repo_cls
)
if not customer_risk:
return None
products = await product_repo_cls(db).list(
where=[FinProduct.status == "在售"],
limit=1000,
)
if performance_repo_cls is None:
from repositories.fund_performance import FundPerformanceRepo
performance_repo_cls = FundPerformanceRepo
performance_repo = performance_repo_cls(db)
candidates = []
for product in products:
candidate = to_product_candidate(product)
latest = None
if hasattr(performance_repo, "get_latest_for_product"):
latest = await performance_repo.get_latest_for_product(product.id)
else:
rows = await performance_repo.list_for_product(product.id)
latest = rows[-1] if rows else None
return_rate = getattr(latest, "return_rate", None)
if return_rate is not None:
candidate["performance_score"] = float(return_rate)
candidates.append(candidate)
return {
"customer_id": customer_id,
"customer_risk": customer_risk,
"candidates": candidates,
}