Add option-option hedge mode with SIM/LIVE parity.

Mutual hedge_mode, amplitude OTM selection, 1:1 risk sizing, win-leg/full close, dual audits and docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-07 16:01:16 +08:00
parent 15fe2f72dc
commit ec244c63c6
22 changed files with 2640 additions and 83 deletions
+185
View File
@@ -0,0 +1,185 @@
"""指数/永续 K 线高低点:期期对冲振幅回看。"""
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Any
from .okx.parse import safe_float as okx_safe_float
from .binance.parse import safe_float as bn_safe_float
logger = logging.getLogger(__name__)
@dataclass(frozen=True, slots=True)
class AmplitudeHL:
high: float
low: float
mid: float
hours: float
bar_count: int
@property
def range_pct(self) -> float:
if self.mid <= 0:
return 0.0
return (self.high - self.low) / self.mid * 100.0
def _hl_from_okx_candles(rows: list[Any]) -> tuple[float, float] | None:
"""OKX candle row: [ts, o, h, l, c, ...] newest first."""
highs: list[float] = []
lows: list[float] = []
for row in rows:
if not isinstance(row, (list, tuple)) or len(row) < 5:
continue
h = okx_safe_float(row[2])
lo = okx_safe_float(row[3])
if h is None or lo is None or h <= 0 or lo <= 0:
continue
highs.append(float(h))
lows.append(float(lo))
if not highs or not lows:
return None
return max(highs), min(lows)
def _hl_from_binance_klines(rows: list[Any]) -> tuple[float, float] | None:
"""Binance kline: [openTime, o, h, l, c, ...] oldest first."""
highs: list[float] = []
lows: list[float] = []
for row in rows:
if not isinstance(row, (list, tuple)) or len(row) < 5:
continue
h = bn_safe_float(row[2])
lo = bn_safe_float(row[3])
if h is None or lo is None or h <= 0 or lo <= 0:
continue
highs.append(float(h))
lows.append(float(lo))
if not highs or not lows:
return None
return max(highs), min(lows)
def fetch_okx_amplitude_hl(
*,
inst_id: str,
hours: float,
base_url: str = "https://www.okx.com",
proxy: str | None = None,
) -> AmplitudeHL | None:
"""用 1H K 线回看 hoursinst 可用指数 ETH-USD 或永续 ETH-USDT-SWAP。"""
import math
import httpx
hrs = max(1.0, float(hours))
limit = int(min(300, max(2, math.ceil(hrs) + 1)))
try:
with httpx.Client(
base_url=base_url.rstrip("/"),
timeout=15.0,
proxy=(proxy or "").strip() or None,
headers={"Accept": "application/json", "User-Agent": "eth-hedge-sim/0.1"},
) as client:
r = client.get(
"/api/v5/market/candles",
params={"instId": inst_id, "bar": "1H", "limit": str(limit)},
)
r.raise_for_status()
body = r.json()
if str(body.get("code")) != "0":
logger.warning("OKX candles error: %s", body.get("msg"))
return None
data = body.get("data") or []
except Exception as e:
logger.warning("OKX candles fetch failed: %s", e)
return None
hl = _hl_from_okx_candles(data)
if hl is None:
return None
high, low = hl
mid = (high + low) / 2.0
return AmplitudeHL(
high=high, low=low, mid=mid, hours=hrs, bar_count=len(data)
)
def fetch_binance_amplitude_hl(
*,
symbol: str,
hours: float,
fapi_base: str = "https://fapi.binance.com",
proxy: str | None = None,
) -> AmplitudeHL | None:
"""USDT 永续 1h klines。"""
import math
import httpx
hrs = max(1.0, float(hours))
limit = int(min(500, max(2, math.ceil(hrs) + 1)))
sym = str(symbol or "ETHUSDT").upper().replace("-", "")
try:
with httpx.Client(
base_url=fapi_base.rstrip("/"),
timeout=15.0,
proxy=(proxy or "").strip() or None,
headers={"Accept": "application/json", "User-Agent": "eth-hedge-sim/0.1"},
trust_env=False,
) as client:
r = client.get(
"/fapi/v1/klines",
params={"symbol": sym, "interval": "1h", "limit": limit},
)
r.raise_for_status()
data = r.json()
if not isinstance(data, list):
return None
except Exception as e:
logger.warning("Binance klines fetch failed: %s", e)
return None
hl = _hl_from_binance_klines(data)
if hl is None:
return None
high, low = hl
mid = (high + low) / 2.0
return AmplitudeHL(
high=high, low=low, mid=mid, hours=hrs, bar_count=len(data)
)
def fetch_amplitude_hl_for_runtime(hours: float) -> AmplitudeHL | None:
"""按当前交易所 runtime 拉振幅高低点。"""
from ..config import get_settings
from .runtime import load_runtime_settings
s = get_settings()
rt = load_runtime_settings()
ex = str(rt.exchange or "okx").strip().lower()
hrs = float(hours)
if ex in ("binance", "bn"):
return fetch_binance_amplitude_hl(
symbol=str(rt.perp_inst_id or "ETHUSDT"),
hours=hrs,
fapi_base=s.binance_fapi_base,
proxy=s.binance_http_proxy or None,
)
# OKX:优先指数,失败再试永续
idx = str(rt.index_inst_id or "ETH-USD")
amp = fetch_okx_amplitude_hl(
inst_id=idx,
hours=hrs,
base_url=s.okx_rest_base,
proxy=s.okx_http_proxy or None,
)
if amp is not None:
return amp
return fetch_okx_amplitude_hl(
inst_id=str(rt.perp_inst_id or "ETH-USDT-SWAP"),
hours=hrs,
base_url=s.okx_rest_base,
proxy=s.okx_http_proxy or None,
)