178 lines
6.0 KiB
Python
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,
|
|
}
|