oilverse-api / core /hedging.py
ๅญ™ๅฎถๆ˜Ž
deploy: OilVerse for HuggingFace (Node.js 18 fix)
fab9847
Raw
History Blame Contribute Delete
13.2 kB
"""
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