284 lines
12 KiB
Python
284 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
💰 动态定价与核保页面
|
||
保险产品选择 → 费率计算器 → 核保报告生成
|
||
"""
|
||
import sys
|
||
import os
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
|
||
import streamlit as st
|
||
import plotly.graph_objects as go
|
||
|
||
from collectors.financial_collector import get_all_companies, get_company_by_code
|
||
from risk_engine.risk_scorer import calculate_six_dimension_scores
|
||
from risk_engine.dynamic_pricing import (
|
||
calculate_premium, calculate_all_products,
|
||
get_enterprise_scale, INDUSTRY_RISK_FACTORS,
|
||
)
|
||
from risk_engine.report_generator import generate_report, format_report_markdown
|
||
from config import INSURANCE_PRODUCTS
|
||
|
||
st.set_page_config(page_title="动态定价与核保", page_icon="💰", layout="wide")
|
||
|
||
st.markdown("# 💰 科创特有风险综合险 · 动态定价引擎")
|
||
st.markdown("基于六维风险评分的“千企千面”精准核保与定价。")
|
||
|
||
from utils.session_helper import render_company_selector, render_sidebar_global_company_selector
|
||
|
||
with st.sidebar:
|
||
render_sidebar_global_company_selector()
|
||
st.markdown("---")
|
||
|
||
# 企业选择(全局同步)
|
||
company = render_company_selector("🏢 选择目标企业", key_suffix="pricing_page")
|
||
stock_code = company["stock_code"] if company else "688256"
|
||
|
||
if company:
|
||
# 计算风险评分
|
||
risk_result = calculate_six_dimension_scores(company)
|
||
scores = risk_result["scores"]
|
||
comprehensive = risk_result["comprehensive_score"]
|
||
level_info = risk_result["risk_level"]
|
||
|
||
revenue = company.get("financials", {}).get("revenue_2024", 0)
|
||
sector = company.get("sector", "")
|
||
scale = get_enterprise_scale(revenue)
|
||
|
||
st.markdown("---")
|
||
|
||
# ============================================================
|
||
# 风险概要
|
||
# ============================================================
|
||
col_info, col_score = st.columns([2, 1])
|
||
|
||
with col_info:
|
||
st.markdown(f"### {company['short_name']}")
|
||
st.markdown(f"**行业**: {company['industry']} | **领域**: {sector} | **规模**: {scale}")
|
||
st.markdown(f"**营收**: ¥{revenue/1e8:.1f}亿 | **行业风险系数**: {INDUSTRY_RISK_FACTORS.get(sector, 1.0):.2f}")
|
||
|
||
with col_score:
|
||
color = level_info["color"]
|
||
st.markdown(
|
||
f"<div style='background:{color}22; padding:20px; border-radius:12px; text-align:center; "
|
||
f"border:2px solid {color};'>"
|
||
f"<div style='font-size:2.5em; font-weight:bold; color:{color};'>{comprehensive}</div>"
|
||
f"<div style='color:{color};'>综合风险评分 {level_info['emoji']} {level_info['level']}</div>"
|
||
f"</div>",
|
||
unsafe_allow_html=True,
|
||
)
|
||
|
||
st.markdown("---")
|
||
|
||
# ============================================================
|
||
# 保险产品费率计算
|
||
# ============================================================
|
||
st.markdown("### 📊 保险产品费率方案")
|
||
|
||
pricing_results = calculate_all_products(
|
||
comprehensive, scores, sector, revenue
|
||
)
|
||
|
||
# 三列展示三个险种
|
||
cols = st.columns(3)
|
||
for i, pricing in enumerate(pricing_results):
|
||
with cols[i]:
|
||
product = INSURANCE_PRODUCTS[pricing["product_key"]]
|
||
is_insurable = pricing["is_insurable"]
|
||
|
||
if is_insurable:
|
||
border_color = "#4CAF50" if comprehensive < 40 else ("#FF9800" if comprehensive < 70 else "#F44336")
|
||
else:
|
||
border_color = "#B71C1C"
|
||
|
||
st.markdown(
|
||
f"<div style='background:#16213e; padding:20px; border-radius:12px; "
|
||
f"border:2px solid {border_color}; min-height: 300px;'>"
|
||
f"<h4 style='text-align:center; color:white;'>{product['name']}</h4>"
|
||
f"<p style='color:#a8a8b3; font-size:0.85em; text-align:center;'>{product['description']}</p>"
|
||
f"</div>",
|
||
unsafe_allow_html=True,
|
||
)
|
||
|
||
if is_insurable:
|
||
st.metric("基础保费", f"¥{pricing['base_premium']:,.0f}")
|
||
st.metric("最终保费", f"¥{pricing['final_premium']:,.0f}",
|
||
delta=f"×{pricing['risk_multiplier']:.2f}",
|
||
delta_color="inverse" if pricing['risk_multiplier'] > 1.2 else "normal")
|
||
st.metric("保额", f"¥{pricing['adjusted_coverage']:,.0f}")
|
||
st.metric("免赔率", f"{pricing['deductible_rate']:.0%}")
|
||
|
||
with st.expander("📐 定价明细"):
|
||
st.markdown(f"- 风险系数: {pricing['risk_multiplier']:.3f}")
|
||
st.markdown(f"- 行业调整: {pricing['industry_factor']:.3f}")
|
||
st.markdown(f"- 规模折扣: {pricing['scale_factor']:.3f}")
|
||
st.markdown(f"- 维度调整: {pricing['dimension_adjustment']:.3f}")
|
||
st.markdown(f"- **计算公式**: {pricing['pricing_breakdown']}")
|
||
else:
|
||
st.error("⛔ 风险过高,建议拒保")
|
||
|
||
# ============================================================
|
||
# 费率对比图
|
||
# ============================================================
|
||
st.markdown("---")
|
||
st.markdown("### 📈 费率构成分析")
|
||
|
||
col_chart1, col_chart2 = st.columns(2)
|
||
|
||
with col_chart1:
|
||
# 基础保费 vs 最终保费对比
|
||
product_names = [p["product_name"] for p in pricing_results if p["is_insurable"]]
|
||
base_premiums = [p["base_premium"] for p in pricing_results if p["is_insurable"]]
|
||
final_premiums = [p["final_premium"] for p in pricing_results if p["is_insurable"]]
|
||
|
||
max_val = max(max(base_premiums, default=100000), max(final_premiums, default=100000))
|
||
|
||
fig = go.Figure(data=[
|
||
go.Bar(
|
||
name="基础保费",
|
||
x=product_names,
|
||
y=base_premiums,
|
||
marker_color="#3B82F6",
|
||
text=[f"¥{v:,.0f}" for v in base_premiums],
|
||
textposition="outside",
|
||
textfont=dict(size=11, color="#93C5FD")
|
||
),
|
||
go.Bar(
|
||
name="调整后保费",
|
||
x=product_names,
|
||
y=final_premiums,
|
||
marker_color="#EF4444",
|
||
text=[f"¥{v:,.0f}" for v in final_premiums],
|
||
textposition="outside",
|
||
textfont=dict(size=11, color="#FCA5A5")
|
||
),
|
||
])
|
||
fig.update_layout(
|
||
title=dict(
|
||
text="<b>📊 基础保费 vs 调整后保费对比</b>",
|
||
font=dict(size=15, color="#F8FAFC"),
|
||
x=0.02,
|
||
y=0.96
|
||
),
|
||
barmode="group",
|
||
height=380,
|
||
margin=dict(l=20, r=20, t=75, b=30),
|
||
paper_bgcolor="rgba(0,0,0,0)",
|
||
plot_bgcolor="rgba(0,0,0,0)",
|
||
font=dict(color="white"),
|
||
legend=dict(
|
||
x=0.02,
|
||
y=0.88,
|
||
orientation="h",
|
||
bgcolor="rgba(0,0,0,0)",
|
||
font=dict(color="#CBD5E1", size=12)
|
||
),
|
||
xaxis=dict(gridcolor="rgba(255,255,255,0.05)"),
|
||
yaxis=dict(
|
||
gridcolor="rgba(255,255,255,0.08)",
|
||
range=[0, max_val * 1.3]
|
||
),
|
||
)
|
||
st.plotly_chart(fig, use_container_width=True)
|
||
|
||
with col_chart2:
|
||
# 定价因子贡献瀑布图(选第一个可投保的产品)
|
||
insurable = [p for p in pricing_results if p["is_insurable"]]
|
||
if insurable:
|
||
p = insurable[0]
|
||
factors = ["基础保费", "风险系数", "行业调整", "规模折扣", "维度调整", "最终保费"]
|
||
values = [
|
||
p["base_premium"],
|
||
p["base_premium"] * (p["risk_multiplier"] - 1),
|
||
p["base_premium"] * p["risk_multiplier"] * (p["industry_factor"] - 1),
|
||
p["base_premium"] * p["risk_multiplier"] * p["industry_factor"] * (p["scale_factor"] - 1),
|
||
p["base_premium"] * p["risk_multiplier"] * p["industry_factor"] * p["scale_factor"] * (p["dimension_adjustment"] - 1),
|
||
p["final_premium"],
|
||
]
|
||
measures = ["absolute", "relative", "relative", "relative", "relative", "total"]
|
||
|
||
fig2 = go.Figure(go.Waterfall(
|
||
name=p["product_name"],
|
||
orientation="v",
|
||
measure=measures,
|
||
x=factors,
|
||
y=values,
|
||
text=[f"¥{v:,.0f}" for v in values],
|
||
textposition="outside",
|
||
connector={"line": {"color": "rgba(255,255,255,0.3)"}},
|
||
increasing={"marker": {"color": "#EF4444"}},
|
||
decreasing={"marker": {"color": "#10B981"}},
|
||
totals={"marker": {"color": "#3B82F6"}},
|
||
))
|
||
fig2.update_layout(
|
||
title=dict(
|
||
text=f"<b>📉 {p['product_name']} — 定价因子分解</b>",
|
||
font=dict(size=15, color="#F8FAFC"),
|
||
x=0.02,
|
||
y=0.96
|
||
),
|
||
height=380,
|
||
margin=dict(l=20, r=20, t=75, b=30),
|
||
paper_bgcolor="rgba(0,0,0,0)",
|
||
plot_bgcolor="rgba(0,0,0,0)",
|
||
font=dict(color="white"),
|
||
xaxis=dict(gridcolor="rgba(255,255,255,0.05)"),
|
||
yaxis=dict(gridcolor="rgba(255,255,255,0.08)"),
|
||
)
|
||
st.plotly_chart(fig2, use_container_width=True)
|
||
|
||
# ============================================================
|
||
# 核保报告生成
|
||
# ============================================================
|
||
st.markdown("---")
|
||
st.markdown("### 📋 核保决策报告")
|
||
|
||
# 检查 session_state 中是否有该企业的真实大模型辩论结果
|
||
has_real_debate = False
|
||
debate_res_in_session = st.session_state.get("debate_results")
|
||
|
||
if debate_res_in_session and debate_res_in_session.get("company", {}).get("stock_code") == stock_code:
|
||
has_real_debate = True
|
||
st.success("🤖 **已成功链接页面 3 的【大模型多智能体辩论】真实研判与穿透凭据!**")
|
||
else:
|
||
st.info("💡 **提示**:建议先至 **【⚖️ 多智能体辩论诊断】** 页面为该企业发起大模型辩论,本报告将自动整合最深度的 AI 审查凭据。")
|
||
|
||
if st.button("📄 生成完整核保报告", type="primary", use_container_width=True):
|
||
if has_real_debate:
|
||
debate_result = debate_res_in_session
|
||
else:
|
||
# 若尚未发起辩论,则回退到离线规则引擎评估
|
||
from agents.law_agent import LawAgent
|
||
from agents.tech_agent import TechAgent
|
||
from agents.finance_agent import FinanceAgent
|
||
from agents.judge_agent import JudgeAgent
|
||
|
||
law_result = LawAgent()._rule_based_evaluation(company)
|
||
tech_result = TechAgent()._rule_based_evaluation(company)
|
||
fin_result = FinanceAgent()._rule_based_evaluation(company)
|
||
judge_result = JudgeAgent()._rule_based_evaluation(
|
||
company, law_result, tech_result, fin_result
|
||
)
|
||
|
||
debate_result = {
|
||
"law_result": law_result,
|
||
"tech_result": tech_result,
|
||
"finance_result": fin_result,
|
||
"judge_result": judge_result,
|
||
"conflicts": [],
|
||
}
|
||
|
||
report = generate_report(company, debate_result, pricing_results)
|
||
markdown_report = format_report_markdown(report)
|
||
|
||
st.markdown(markdown_report)
|
||
|
||
# 下载按钮
|
||
st.download_button(
|
||
label="📥 下载核保报告 (Markdown 格式)",
|
||
data=markdown_report,
|
||
file_name=f"核保报告_{company['short_name']}_{report['report_id']}.md",
|
||
mime="text/markdown",
|
||
use_container_width=True,
|
||
)
|