Files
eth_hedge_sim/backend/app/exchange/candles.py
T
dekun ec244c63c6 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>
2026-08-07 16:01:16 +08:00

186 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""指数/永续 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,
)