oilverse-api / core /hedging.py
孙家明
deploy: OilVerse for HuggingFace (Node.js 18 fix)
fab9847
"""
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