diff --git a/backend/app/api/predict.py b/backend/app/api/predict.py index a2489357..06789ff4 100644 --- a/backend/app/api/predict.py +++ b/backend/app/api/predict.py @@ -152,3 +152,182 @@ def api_cvp_detailed(data: dict): } except Exception as e: raise HTTPException(400, f"CVP详细分析失败: {str(e)}") + + +# ── 实物期权计算器 ───────────────────────────────────────────── +import math + +def _norm_cdf(x: float) -> float: + """标准正态分布CDF — Abramowitz & Stegun 近似 (max error ≈ 1.5×10⁻⁷)""" + a1, a2, a3, a4, a5 = 0.254829592, -0.284496736, 1.421413741, -1.453152027, 1.061405429 + p = 0.3275911 + sign = 1.0 + if x < 0: + sign = -1.0 + x_abs = abs(x) / math.sqrt(2.0) + t = 1.0 / (1.0 + p * x_abs) + y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * math.exp(-x_abs * x_abs) + return 0.5 * (1.0 + sign * y) + +def _black_scholes_call(S0: float, X: float, t: float, r: float, sigma: float) -> dict: + """BSM看涨期权定价(扩张期权/延迟期权)""" + sqrt_t = math.sqrt(t) + d1 = (math.log(S0 / X) + (r + 0.5 * sigma ** 2) * t) / (sigma * sqrt_t) + d2 = d1 - sigma * sqrt_t + nd1 = _norm_cdf(d1) + nd2 = _norm_cdf(d2) + call_value = max(S0 * nd1 - X * math.exp(-r * t) * nd2, 0.0) + return {"value": round(call_value, 4), "d1": round(d1, 4), "d2": round(d2, 4), "Nd1": round(nd1, 4), "Nd2": round(nd2, 4)} + +def _black_scholes_put(S0: float, X: float, t: float, r: float, sigma: float) -> dict: + """BSM看跌期权定价(放弃期权/收缩期权)""" + sqrt_t = math.sqrt(t) + d1 = (math.log(S0 / X) + (r + 0.5 * sigma ** 2) * t) / (sigma * sqrt_t) + d2 = d1 - sigma * sqrt_t + nd1 = _norm_cdf(-d1) + nd2 = _norm_cdf(-d2) + put_value = max(X * math.exp(-r * t) * nd2 - S0 * nd1, 0.0) + return {"value": round(put_value, 4), "d1": round(d1, 4), "d2": round(d2, 4), "N(-d1)": round(nd1, 4), "N(-d2)": round(nd2, 4)} + +def _binomial_tree_call(S0: float, X: float, t: float, r: float, sigma: float, n: int = 100) -> float: + """二叉树欧式看涨期权定价(延迟期权)""" + dt = t / n + u = math.exp(sigma * math.sqrt(dt)) + d = 1.0 / u + p = (math.exp(r * dt) - d) / (u - d) + discount = math.exp(-r * dt) + prices = [S0 * (u ** (n - j)) * (d ** j) for j in range(n + 1)] + values = [max(p - X, 0.0) for p in prices] + for i in range(n - 1, -1, -1): + for j in range(i + 1): + values[j] = discount * (p * values[j] + (1 - p) * values[j + 1]) + return max(values[0], 0.0) + +def _binomial_tree_american_put(S0: float, X: float, t: float, r: float, sigma: float, n: int = 100) -> float: + """二叉树美式看跌期权定价(可随时放弃的放弃期权)""" + dt = t / n + u = math.exp(sigma * math.sqrt(dt)) + d = 1.0 / u + p = (math.exp(r * dt) - d) / (u - d) + discount = math.exp(-r * dt) + prices = [S0 * (u ** (n - j)) * (d ** j) for j in range(n + 1)] + values = [max(X - p, 0.0) for p in prices] + for i in range(n - 1, -1, -1): + for j in range(i + 1): + hold = discount * (p * values[j] + (1 - p) * values[j + 1]) + exercise = X - (S0 * (u ** (i - j)) * (d ** j)) + values[j] = max(hold, exercise) + return max(values[0], 0.0) + +@router.post("/real-option") +def api_real_option(data: dict): + """实物期权计算器""" + try: + opt_type = data.get("opt_type", "expansion") # expansion|abandon|delay|shrink + model = data.get("model", "bs") # bs|binomial + S0 = float(data.get("S0", 100.0)) + X = float(data.get("X", 80.0)) + t = float(data.get("t", 3.0)) + r = float(data.get("r", 0.0174)) + sigma = float(data.get("sigma", 0.30)) + expansion_factor = float(data.get("expansion_factor", 1.5)) + salvage_value = float(data.get("salvage_value", S0 * 0.3)) + n_steps = int(data.get("n_steps", 100)) + + # 输入校验 + if S0 <= 0 or X <= 0 or t <= 0 or sigma <= 0: + raise HTTPException(400, "参数必须为正数") + if sigma > 2.0: + raise HTTPException(400, "波动率σ不能超过200%") + + result = {"option_type": opt_type, "model": model, "S0": S0, "X": X, "t": t, "r": r, "sigma": sigma} + + # 计算期权价值 + if opt_type in ("expansion", "delay") and model == "bs": + bs = _black_scholes_call(S0, X, t, r, sigma) + result["option_value"] = bs["value"] + result["intermediate"] = {k: v for k, v in bs.items() if k != "value"} + elif opt_type == "expansion" and model == "binomial": + adj_X = X / expansion_factor + bt_val = _binomial_tree_call(S0, adj_X, t, r, sigma, n_steps) + option_value = max(bt_val * expansion_factor, 0.0) + result["option_value"] = round(option_value, 4) + result["intermediate"] = {"expansion_factor": expansion_factor, "adjusted_X": round(adj_X, 4), "tree_value": round(bt_val, 4)} + elif opt_type == "delay" and model == "binomial": + option_value = _binomial_tree_call(S0, X, t, r, sigma, n_steps) + result["option_value"] = round(option_value, 4) + # Also compute BS for reference + bs = _black_scholes_call(S0, X, t, r, sigma) + result["intermediate"] = {"n_steps": n_steps, "bs_reference": round(bs["value"], 4)} + elif opt_type in ("abandon", "shrink") and model == "bs": + effective_X = salvage_value if opt_type == "abandon" else X + bs = _black_scholes_put(S0, effective_X, t, r, sigma) + result["option_value"] = bs["value"] + result["intermediate"] = {k: v for k, v in bs.items() if k != "value"} + if opt_type == "abandon": + result["intermediate"]["salvage_value"] = effective_X + elif opt_type == "abandon" and model == "binomial": + bt_val = _binomial_tree_american_put(S0, salvage_value, t, r, sigma, n_steps) + result["option_value"] = round(bt_val, 4) + result["intermediate"] = {"n_steps": n_steps, "salvage_value": salvage_value} + else: + raise HTTPException(400, f"不支持的组合: {opt_type} + {model}") + + # 决策建议 + val = result["option_value"] + if val > 0: + result["suggestion"] = "期权价值 > 0,管理弹性有价值,建议保留决策弹性,在有利时机行权" + result["suggestion_type"] = "positive" + else: + result["suggestion"] = "期权价值 ≈ 0,弹性无明显价值,建议按传统NPV决策,无需等待" + result["suggestion_type"] = "neutral" + + # 扩展NPV(假设传统NPV = S0 - X) + npv_without = S0 - X + expanded_npv = npv_without + val + result["npv_without_flexibility"] = round(npv_without, 4) + result["expanded_npv"] = round(expanded_npv, 4) + + if expanded_npv > 0: + result["decision"] = "✅ 扩展NPV > 0,含弹性后项目整体值得投资" + else: + result["decision"] = "❌ 扩展NPV ≤ 0,含弹性后项目仍不值得投资" + + # 敏感性分析数据(σ从10%~90%变化) + sensitivity = [] + for s_pct in range(5, 96, 5): + s = s_pct / 100.0 + if opt_type in ("expansion", "delay"): + if model == "bs": + v = _black_scholes_call(S0, X, t, r, s)["value"] + else: + bt = _binomial_tree_call(S0, X, t, r, s, n_steps) + v = bt * expansion_factor if opt_type == "expansion" else bt + else: + eff_X = salvage_value if opt_type == "abandon" else X + if model == "bs": + v = _black_scholes_put(S0, eff_X, t, r, s)["value"] + else: + v = _binomial_tree_american_put(S0, eff_X, t, r, s, n_steps) + sensitivity.append({"sigma": s_pct, "option_value": round(v, 4)}) + result["sensitivity"] = sensitivity + + # 警告提示 + warnings = [] + if t * sigma * sigma * 0.5 > r: + warnings.append("高波动+长时间,延迟价值显著") + if S0 < X: + warnings.append("价外期权,期权价值较低") + if S0 > X * 1.5: + warnings.append("深度价内,几乎确定行权") + if sigma < 0.10: + warnings.append("波动率过低,期权价值趋近于0") + if t > 10: + warnings.append("长期期权,贴现因子影响大") + result["warnings"] = warnings + + return result + except HTTPException: + raise + except Exception as e: + raise HTTPException(400, f"实物期权计算失败: {str(e)}") diff --git a/frontend/src/permission.ts b/frontend/src/permission.ts index dacd6069..ae54b140 100644 --- a/frontend/src/permission.ts +++ b/frontend/src/permission.ts @@ -61,6 +61,7 @@ export const MENU_ITEMS: MenuItem[] = [ // ══════════════════════════════════════════════════════════════ { path: '/maps-review', label: '战略回顾会', icon: 'TrendCharts', roles: ['ceo', 'finance'], group: '复盘与改进' }, { path: '/predict', label: '预测模拟', icon: 'DataLine', roles: ['ceo', 'finance', 'it'], group: '复盘与改进' }, + { path: '/real-options', label: '实物期权计算器', icon: 'TrendCharts', roles: ['ceo', 'finance'], group: '复盘与改进' }, // ══════════════════════════════════════════════════════════════ // GROUP 5: 系统与支持(Infra) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index f2263438..d53daf8f 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -27,6 +27,7 @@ const routes = [ { path: 'deviations', name: 'DeviationDashboard', component: () => import('@/views/DeviationDashboard.vue'), meta: { title: '差异分析', roles: ['ceo', 'finance', 'business', 'it'] } }, { path: 'cost', name: 'CostDashboard', component: () => import('@/views/CostDashboard.vue'), meta: { title: '成本分析', roles: ['ceo', 'finance', 'it'] } }, { path: 'predict', name: 'PredictDashboard', component: () => import('@/views/PredictDashboard.vue'), meta: { title: '预测模拟', roles: ['ceo', 'finance', 'it'] } }, + { path: 'real-options', name: 'RealOptions', component: () => import('@/views/RealOptions.vue'), meta: { title: '实物期权计算器', roles: ['ceo', 'finance'] } }, { path: 'action-plans', name: 'ActionPlans', component: () => import('@/views/ActionPlanLibrary.vue'), meta: { title: '改善行动', roles: ['ceo', 'finance', 'business', 'it'] } }, { path: 'reports', name: 'ReportCenter', component: () => import('@/views/ReportCenter.vue'), meta: { title: '管理报表', roles: ['ceo', 'finance', 'business'] } }, { path: 'alignment', name: 'KPIAlignment', component: () => import('@/views/KPIAlignment.vue'), meta: { title: '战略执行看板', roles: ['ceo', 'finance', 'business', 'it'] } }, diff --git a/frontend/src/views/RealOptions.vue b/frontend/src/views/RealOptions.vue new file mode 100644 index 00000000..fcbe587f --- /dev/null +++ b/frontend/src/views/RealOptions.vue @@ -0,0 +1,147 @@ + + + + 实物期权计算器 + + + + + 📋 参数设置 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 计算 + + + 重置默认值 + + + + + + + 📊 计算结果 + + 期权价值 + + {{ result.option_value.toFixed(2) }} 万元 + + + + {{ result.d1?.toFixed(4) || '-' }} + {{ result.d2?.toFixed(4) || '-' }} + {{ result.Nd1?.toFixed(4) || '-' }} + {{ result.Nd2?.toFixed(4) || '-' }} + {{ form.model === 'bs' ? 'Black-Scholes' : '二叉树' }} + + + {{ result.suggestion }} + + + + + + + 📈 波动率敏感性 + + + + + + + + + +