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:
@@ -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 线回看 hours;inst 可用指数 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,
|
||||
)
|
||||
Reference in New Issue
Block a user