Files
eth_hedge_sim/backend/app/strategy/auto_usdc.py
T

246 lines
7.6 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.
"""OKX 开仓前:交易账户 USDC 不足时市价 USDT→USDC(目标=期权所需×2)。
仅 OKX SIM/LIVE。资金账户不参与;期权已可开则跳过。
空仓等待开仓时也会按盘口刷新以损定仓名义后再检测,不依赖已选中合格期权。
"""
from __future__ import annotations
import logging
import math
import time
from typing import Any
from ..config import get_settings
from ..models.db import Database, get_db
from .open_capacity import assess_open_capacity, invalidate_live_balance_cache
logger = logging.getLogger(__name__)
# 目标持仓 = 期权开仓所需 USDC × 倍数
_TARGET_MULTIPLE = 2.0
# 过小不兑(避免粉尘单)
_MIN_CONVERT_USDT = 1.0
# 不足时重试间隔,避免每秒砸单
_RETRY_COOLDOWN_SEC = 45.0
_last_attempt_ts: float = 0.0
def _is_okx(exchange: str | None = None) -> bool:
from ..exchange.runtime import load_runtime_settings, normalize_exchange_name
ex = exchange
if not ex:
try:
ex = load_runtime_settings().exchange
except Exception:
ex = get_settings().exchange
return normalize_exchange_name(ex) == "okx"
def _round_down(n: float, nd: int = 2) -> float:
if n <= 0:
return 0.0
f = 10**nd
return math.floor(n * f + 1e-12) / f
def refresh_risk_sizing_from_market(db: Database | None = None) -> dict[str, Any]:
"""空仓时用当前指数/卖一刷新以损定仓名义,便于资金门与自动兑对齐展示。"""
database = db or get_db()
out: dict[str, Any] = {"ok": True, "detail": "skip"}
try:
from ..sim.ledger import Ledger
from .open_capacity import _index_and_option_ask
from .risk_sizing import apply_risk_sizing_to_ledger, is_risk_based
if not is_risk_based(Ledger(database)):
out["detail"] = "manual_sizing"
return out
idx, ask = _index_and_option_ask()
if idx is None or ask is None or float(idx) <= 0 or float(ask) <= 0:
out["ok"] = False
out["detail"] = "暂无指数或期权卖一"
return out
r = apply_risk_sizing_to_ledger(
index_px=float(idx), option_ask=float(ask), db=database
)
out["ok"] = bool(r.ok)
out["detail"] = r.detail
out["k"] = r.k
out["option_qty_eth"] = r.option_qty_eth
return out
except Exception as e:
logger.exception("refresh risk sizing for auto_usdc failed")
return {"ok": False, "detail": str(e)}
def prepare_okx_trading_usdc(db: Database | None = None) -> dict[str, Any]:
"""选约前也可调用:先刷新名义,再按资金门自动兑 USDC。"""
database = db or get_db()
sized = refresh_risk_sizing_from_market(database)
top = ensure_okx_trading_usdc(database)
return {"sizing": sized, "convert": top}
def ensure_okx_trading_usdc(
db: Database | None = None,
*,
cap: dict[str, Any] | None = None,
force: bool = False,
) -> dict[str, Any]:
"""
开仓资金门前调用:
- 非 OKX → 跳过
- 期权可开(USDC≥需)→ 跳过
- 否则在**交易账户**市价 USDT→USDC,尽量补到 需×2(并预留永续保证金)
"""
global _last_attempt_ts
db = db or get_db()
out: dict[str, Any] = {
"ok": True,
"acted": False,
"skipped": True,
"detail": "skip",
}
if not _is_okx():
out["detail"] = "非 OKX,跳过自动兑 USDC"
return out
cap = cap or assess_open_capacity(db)
need = cap.get("option_need_usdc")
have = cap.get("option_have_usdc")
if need is None or have is None:
out["detail"] = "期权所需/持有未知,跳过兑换"
out["capacity"] = cap
return out
need_f = float(need)
have_f = float(have)
if need_f <= 0:
out["detail"] = "期权所需为 0,跳过"
return out
# 可开仓:不兑换(即使低于 2 倍目标)
if have_f + 1e-9 >= need_f:
out["detail"] = (
f"交易账户 USDC 已够开仓(有 {have_f:.2f} ≥ 需 {need_f:.2f}),不兑换"
)
out["capacity"] = cap
return out
now = time.time()
if (
not force
and _last_attempt_ts > 0
and now - _last_attempt_ts < _RETRY_COOLDOWN_SEC
):
left = _RETRY_COOLDOWN_SEC - (now - _last_attempt_ts)
out["detail"] = f"USDC 不足,自动兑换冷却中({left:.0f}s"
out["capacity"] = cap
out["need_usdc"] = round(need_f, 2)
out["have_usdc"] = round(have_f, 2)
return out
target = need_f * _TARGET_MULTIPLE
gap_usdc = target - have_f
if gap_usdc <= 1e-6:
out["detail"] = "无需补足"
return out
from ..live.okx_funds import usdc_usdt_mid_rate
rate = float(usdc_usdt_mid_rate() or 1.0)
if rate <= 0:
rate = 1.0
# usdt_to_usdcamount = 花费的 USDT(与 OkxFundsClient / SIM 一致)
want_usdt = gap_usdc * rate
perp_need = float(cap.get("perp_need_usdt") or 0)
trading_usdt = float(cap.get("perp_have_usdt") or 0)
# 预留永续保证金,避免兑光导致永续不可开
spendable = max(0.0, trading_usdt - max(0.0, perp_need))
spend_usdt = _round_down(min(want_usdt, spendable), 2)
out.update(
{
"need_usdc": round(need_f, 2),
"have_usdc": round(have_f, 2),
"target_usdc": round(target, 2),
"want_usdt": round(want_usdt, 2),
"spend_usdt": spend_usdt,
"rate": rate,
"spendable_usdt": round(spendable, 2),
}
)
if spend_usdt < _MIN_CONVERT_USDT:
out["ok"] = False
out["skipped"] = True
out["detail"] = (
f"交易账户可兑 USDT 不足(可兑 {spendable:.2f}"
f"目标补约 {want_usdt:.2f},门槛 {_MIN_CONVERT_USDT}"
)
out["capacity"] = cap
return out
_last_attempt_ts = now
s = get_settings()
try:
if s.is_sim:
from ..sim.funds_wallets import SimFundsWallets
r = SimFundsWallets(db).convert(
direction="usdt_to_usdc",
amount=spend_usdt,
rate=rate,
account="trading",
)
else:
from ..live.okx_funds import OkxFundsClient
client = OkxFundsClient()
try:
r = client.spot_swap_usdt_usdc(
direction="usdt_to_usdc",
amount=spend_usdt,
)
finally:
client.close()
invalidate_live_balance_cache()
except Exception as e:
logger.exception("auto USDC convert failed")
out["ok"] = False
out["skipped"] = False
out["acted"] = False
out["detail"] = f"自动兑换异常:{e}"
return out
if not r.get("ok"):
out["ok"] = False
out["skipped"] = False
out["acted"] = False
out["detail"] = f"自动兑换失败:{r.get('detail') or r}"
out["raw"] = r
logger.warning("auto_usdc failed: %s", out["detail"])
return out
invalidate_live_balance_cache()
cap2 = assess_open_capacity(db)
out.update(
{
"ok": True,
"acted": True,
"skipped": False,
"detail": (
f"交易账户市价兑 USDC:花 {spend_usdt:.2f} USDT"
f"(目标持仓≈{target:.2f}=需{need_f:.2f}×{_TARGET_MULTIPLE:g}"
),
"capacity_before": cap,
"capacity_after": cap2,
"raw": r,
}
)
logger.info("auto_usdc: %s", out["detail"])
return out