"""从现有业务表聚合投顾意图所需的本地上下文。""" 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, }