""" hedging.py — 对冲决策计算器 ============================== 核心功能: 1. 给定企业月度燃油/原料消耗金额 2. 根据当前风险预测(区间+因子+regime) 3. 计算不同对冲比例下的成本-收益矩阵 4. 输出推荐对冲比例和工具建议 对冲逻辑: - 不对冲:完全暴露在油价波动中 - 部分对冲:锁定一部分成本,保留一部分上行/下行暴露 - 完全对冲:完全锁定成本,放弃上行收益但消除下行风险 - 对冲成本 = 远期升水(contango)+ 期权时间价值(简化为波动率函数) """ import numpy as np from config import INDUSTRIES, INDUSTRY_ZH # ═══════════════════════════════════════════════════════════ # INDUSTRY COST SENSITIVITY (油价弹性系数,基于公开研究) # ═══════════════════════════════════════════════════════════ # 油价变动1%对行业成本/利润的影响(弹性系数,文献值+经验校准) COST_ELASTICITY = { 'Aviation': 0.35, # 航空燃油占运营成本 25-40% 'Logistics': 0.22, # 柴油占物流成本 15-25% 'Chemicals': 0.28, # 原油是石脑油/乙烯原料 'Manufacturing': 0.12, # 能源占制造成本 8-15% 'Upstream_OG': -0.60, # 上游:油价上涨 = 收入增加 } # 典型企业月度油品相关支出(百万美元,用于示例计算) TYPICAL_EXPOSURE = { 'Aviation': 50.0, # 大型航司月均燃油 $50M 'Logistics': 15.0, # 大型物流公司 $15M 'Chemicals': 30.0, # 大型化工企业 $30M 'Manufacturing': 8.0, # 中型制造企业 $8M 'Upstream_OG': 80.0, # 油气公司产量对应营收 } # 对冲成本系数(占名义价值的百分比/月,含远期升水+交易成本) HEDGE_COST_RATES = { 'futures': 0.002, # 期货锁价:~0.2%/月(远期升水+保证金机会成本) 'put': 0.008, # 看跌期权(保护性):~0.8%/月(时间价值衰减) 'collar': 0.003, # 零成本领:~0.3%/月(放弃部分上行) } # ═══════════════════════════════════════════════════════════ # HEDGING DECISION ENGINE # ═══════════════════════════════════════════════════════════ def compute_hedge_matrix(pred_q10, pred_q50, pred_q90, pred_vol, risk_level, risk_bias, industry, monthly_exposure=None): """ 计算不同对冲比例下的成本-收益矩阵。 Parameters ---------- pred_q10, pred_q50, pred_q90 : float 1M 预测区间(收益率,如 -0.11 表示 -11%) pred_vol : float 预测波动率 risk_level : str 'Low' / 'Medium' / 'High' risk_bias : str 'Upward' / 'Balanced' / 'Downward' industry : str 行业标识 monthly_exposure : float or None 月度油品暴露金额(百万美元),None则使用典型值 Returns ------- dict with keys: 'recommended_ratio': 推荐对冲比例 'recommended_tool': 推荐工具 'rationale': 推荐理由 'matrix': 对冲比例 × 情景 的成本矩阵 """ exposure = monthly_exposure or TYPICAL_EXPOSURE.get(industry, 20.0) elasticity = COST_ELASTICITY.get(industry, 0.20) is_upstream = industry == 'Upstream_OG' # 情景定义 scenarios = { 'downside': pred_q10, # 下行风险(10%分位) 'base': pred_q50, # 基准 'upside': pred_q90, # 上行风险(90%分位) } # 对冲比例选项 hedge_ratios = [0.0, 0.25, 0.50, 0.75, 1.0] # 计算矩阵 matrix = [] for ratio in hedge_ratios: row = {'hedge_ratio': ratio, 'hedge_ratio_pct': f'{ratio*100:.0f}%'} for scen_name, price_change in scenarios.items(): # 未对冲部分的损益 unhedged_impact = exposure * price_change * elasticity * (1 - ratio) # 对冲部分:锁定成本,不受价格影响,但有对冲成本 hedge_cost = exposure * ratio * HEDGE_COST_RATES['futures'] # 总净影响 = 未对冲损益 - 对冲成本 net_impact = unhedged_impact - hedge_cost # 上游油气反向:油价涨=收入增 if is_upstream: net_impact = -net_impact # 对冲是锁定收入 row[f'{scen_name}_impact'] = round(net_impact, 2) # VaR: 最大损失 row['worst_case'] = min(row['downside_impact'], row['upside_impact']) row['best_case'] = max(row['downside_impact'], row['upside_impact']) row['range'] = round(row['best_case'] - row['worst_case'], 2) matrix.append(row) # ── 推荐逻辑 ── recommended_ratio, recommended_tool, rationale = _recommend( risk_level, risk_bias, pred_vol, elasticity, is_upstream, pred_q10, pred_q90 ) # ── 各工具成本比较 ── tool_comparison = [] for tool, rate in HEDGE_COST_RATES.items(): monthly_cost = exposure * recommended_ratio * rate tool_comparison.append({ 'tool': tool, 'tool_zh': {'futures': '期货锁价', 'put': '看跌期权', 'collar': '零成本领'}[tool], 'monthly_cost': round(monthly_cost, 2), 'annualized_cost': round(monthly_cost * 12, 2), 'cost_pct': round(rate * 100, 2), }) return { 'industry': industry, 'industry_zh': INDUSTRY_ZH.get(industry, industry), 'exposure': exposure, 'elasticity': elasticity, 'recommended_ratio': recommended_ratio, 'recommended_ratio_pct': f'{recommended_ratio*100:.0f}%', 'recommended_tool': recommended_tool, 'rationale': rationale, 'matrix': matrix, 'tool_comparison': tool_comparison, } def _recommend(risk_level, risk_bias, pred_vol, elasticity, is_upstream, q10, q90): """推荐对冲比例和工具。""" # 基础比例由风险等级决定 base_ratio = {'Low': 0.25, 'Medium': 0.50, 'High': 0.75}.get(risk_level, 0.50) # 偏置调整 if is_upstream: # 上游:下行=收入减少=需要对冲 if risk_bias == 'Downward': base_ratio += 0.15 elif risk_bias == 'Upward': base_ratio -= 0.10 else: # 下游/成本端:上行=成本增加=需要对冲 if risk_bias == 'Upward': base_ratio += 0.15 elif risk_bias == 'Downward': base_ratio -= 0.10 # 波动率调整 if pred_vol > 0.08: base_ratio += 0.10 # 高波动 → 多对冲 # 弹性调整:暴露越大越应该对冲 if abs(elasticity) > 0.30: base_ratio += 0.05 # 尾部风险调整 tail_risk = abs(q10) if not is_upstream else abs(q90) if tail_risk > 0.15: # 尾部超过15% base_ratio += 0.10 base_ratio = max(0.0, min(1.0, round(base_ratio / 0.05) * 0.05)) # 5%步进 # 工具推荐 if risk_level == 'High' and pred_vol > 0.06: tool = 'collar' reason = f'高风险+高波动环境,零成本领策略平衡保护与成本' elif risk_bias == 'Upward' and not is_upstream: tool = 'futures' reason = f'上行偏置明显,期货锁价直接锁定成本' elif risk_bias == 'Downward' and is_upstream: tool = 'put' reason = f'下行风险突出,看跌期权保留上行收益空间' elif pred_vol < 0.04: tool = 'futures' reason = f'低波动环境,简单期货锁价成本最低' else: tool = 'collar' reason = f'均衡环境下零成本领提供灵活保护' risk_zh = {'Low': '低', 'Medium': '中等', 'High': '高'}[risk_level] bias_zh = {'Upward': '上行', 'Downward': '下行', 'Balanced': '均衡'}[risk_bias] rationale = ( f"当前风险{risk_zh}、偏置{bias_zh}、预测波动率{pred_vol*100:.1f}%。" f"建议对冲{base_ratio*100:.0f}%暴露。{reason}。" ) return base_ratio, tool, rationale def compute_all_industry_hedges(row): """为所有行业计算对冲建议。""" results = {} for ind in INDUSTRIES: results[ind] = compute_hedge_matrix( pred_q10=row.get('pred_q10_1m', -0.10), pred_q50=row.get('pred_q50_1m', 0.0), pred_q90=row.get('pred_q90_1m', 0.10), pred_vol=row.get('pred_vol', 0.05), risk_level=row.get('risk_level', 'Medium'), risk_bias=row.get('risk_bias', 'Balanced'), industry=ind, ) return results # ═══════════════════════════════════════════════════════════ # HEDGING BACKTEST # ═══════════════════════════════════════════════════════════ def backtest_hedging(results_df, lookback=60): """ 回测对冲策略:逐月计算 "按推荐比例对冲" vs "完全不对冲" 的累计成本差异。 Parameters ---------- results_df : DataFrame walk-forward 预测结果(含 risk_level, risk_bias, pred_vol, actual_ret_1m 等) lookback : int 回测月数(默认 60 个月) Returns ------- dict: 各行业的月度时间序列 + 累计节省金额 """ import pandas as pd df = results_df.tail(lookback).copy() backtest = {} for ind in INDUSTRIES: exposure = TYPICAL_EXPOSURE.get(ind, 20.0) elasticity = COST_ELASTICITY.get(ind, 0.20) is_upstream = ind == 'Upstream_OG' tool_rate = HEDGE_COST_RATES['futures'] monthly = [] cum_unhedged = 0.0 cum_hedged = 0.0 for _, row in df.iterrows(): actual_ret = row.get('actual_ret_1m', 0) if np.isnan(actual_ret): continue # Determine recommended hedge ratio for this month rl = row.get('risk_level', 'Medium') rb = row.get('risk_bias', 'Balanced') pv = row.get('pred_vol', 0.05) q10 = row.get('pred_q10_1m', -0.10) q90 = row.get('pred_q90_1m', 0.10) ratio, _, _ = _recommend(rl, rb, pv, elasticity, is_upstream, q10, q90) # Unhedged P&L: full exposure to price change price_impact = actual_ret * elasticity if is_upstream: # Upstream: revenue = price * volume. Price up = good. unhedged_pnl = exposure * actual_ret # Simplified: revenue change hedged_pnl = exposure * actual_ret * (1 - ratio) - exposure * ratio * tool_rate else: # Downstream: cost = price * consumption. Price up = bad. unhedged_pnl = -exposure * actual_ret * elasticity hedged_pnl = -exposure * actual_ret * elasticity * (1 - ratio) - exposure * ratio * tool_rate cum_unhedged += unhedged_pnl cum_hedged += hedged_pnl saving = cum_unhedged - cum_hedged # Positive = hedging saved money monthly.append({ 'date': str(row.get('test_date', '')), 'actual_ret': round(float(actual_ret) * 100, 2), 'hedge_ratio': round(ratio, 2), 'risk_level': rl, 'unhedged_pnl': round(unhedged_pnl, 2), 'hedged_pnl': round(hedged_pnl, 2), 'cum_unhedged': round(cum_unhedged, 2), 'cum_hedged': round(cum_hedged, 2), 'cum_saving': round(saving, 2), }) # Summary stats total_saving = cum_unhedged - cum_hedged # Volatility reduction unhedged_vol = np.std([m['unhedged_pnl'] for m in monthly]) if monthly else 0 hedged_vol = np.std([m['hedged_pnl'] for m in monthly]) if monthly else 0 vol_reduction = 1 - hedged_vol / unhedged_vol if unhedged_vol > 0 else 0 # Max drawdown max_dd_unhedged = min(m['cum_unhedged'] for m in monthly) if monthly else 0 max_dd_hedged = min(m['cum_hedged'] for m in monthly) if monthly else 0 backtest[ind] = { 'industry_zh': INDUSTRY_ZH.get(ind, ind), 'months': len(monthly), 'total_saving': round(total_saving, 2), 'vol_reduction': round(vol_reduction * 100, 1), 'max_dd_unhedged': round(max_dd_unhedged, 2), 'max_dd_hedged': round(max_dd_hedged, 2), 'dd_improvement': round(max_dd_hedged - max_dd_unhedged, 2), 'monthly': monthly, } return backtest