Align option display precision with OKX and use exchange position history.
Format prices by tickSz, move bid depth/recovery to card end with plain styling, and load option history from OKX positions-history instead of local DB. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -116,6 +116,53 @@ def format_option_px(px: float, tick_sz: Any) -> str:
|
||||
return f"{px:.{decimals}f}".rstrip("0").rstrip(".") or "0"
|
||||
|
||||
|
||||
def format_usdc_amount(v: float | None) -> str | None:
|
||||
"""USDC 金额展示(与交易所持仓/历史一致,最多 4 位小数)."""
|
||||
if v is None:
|
||||
return None
|
||||
return f"{float(v):.4f}".rstrip("0").rstrip(".") or "0"
|
||||
|
||||
|
||||
def _ms_to_iso(ms: Any) -> str | None:
|
||||
val = _safe_float(ms)
|
||||
if val is None or val <= 0:
|
||||
return None
|
||||
try:
|
||||
from datetime import datetime, timezone
|
||||
|
||||
dt = datetime.fromtimestamp(int(val) / 1000.0, tz=timezone.utc).astimezone()
|
||||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||
except (TypeError, ValueError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def option_instrument_meta_cached(
|
||||
ex: ccxt.okx,
|
||||
inst_id: str,
|
||||
cache: dict[str, dict[str, Any] | None] | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
inst_id = (inst_id or "").strip()
|
||||
if not inst_id:
|
||||
return None
|
||||
if cache is not None and inst_id in cache:
|
||||
return cache[inst_id]
|
||||
meta = fetch_option_instrument_meta(ex, inst_id)
|
||||
if cache is not None:
|
||||
cache[inst_id] = meta
|
||||
return meta
|
||||
|
||||
|
||||
def tick_sz_and_ct_mult(
|
||||
ex: ccxt.okx,
|
||||
inst_id: str,
|
||||
cache: dict[str, dict[str, Any] | None] | None = None,
|
||||
) -> tuple[Any, float]:
|
||||
meta = option_instrument_meta_cached(ex, inst_id, cache)
|
||||
tick_sz = meta.get("tickSz") if meta else None
|
||||
ct_mult = _safe_float(meta.get("ctMult")) if meta else None
|
||||
return tick_sz, ct_mult or 0.01
|
||||
|
||||
|
||||
def _intrinsic_px_per_unit(opt_type: str, strike: float, index_px: float) -> float | None:
|
||||
o = (opt_type or "").upper()
|
||||
if o == "C" and index_px > strike:
|
||||
@@ -732,6 +779,144 @@ def fetch_option_position_history(
|
||||
return []
|
||||
|
||||
|
||||
def fetch_all_option_positions_history(
|
||||
ex: ccxt.okx,
|
||||
*,
|
||||
limit: int = 200,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""拉取 OKX 期权全部历史仓位(分页,按平仓时间倒序)."""
|
||||
cap = max(1, min(int(limit), 500))
|
||||
out: list[dict[str, Any]] = []
|
||||
after: str | None = None
|
||||
while len(out) < cap:
|
||||
page_limit = min(100, cap - len(out))
|
||||
params: dict[str, Any] = {
|
||||
"instType": "OPTION",
|
||||
"limit": str(page_limit),
|
||||
}
|
||||
if after is not None:
|
||||
params["after"] = after
|
||||
try:
|
||||
resp = ex.private_get_account_positions_history(params)
|
||||
except Exception:
|
||||
break
|
||||
rows = (resp or {}).get("data") or []
|
||||
batch = [r for r in rows if isinstance(r, dict)]
|
||||
if not batch:
|
||||
break
|
||||
out.extend(batch)
|
||||
if len(batch) < page_limit:
|
||||
break
|
||||
utimes = [_safe_float(r.get("uTime")) for r in batch]
|
||||
utimes = [int(u) for u in utimes if u is not None and u > 0]
|
||||
if not utimes:
|
||||
break
|
||||
oldest = min(utimes)
|
||||
if after is not None and str(oldest) == after:
|
||||
break
|
||||
after = str(oldest)
|
||||
out.sort(key=lambda r: int(_safe_float(r.get("uTime")) or 0), reverse=True)
|
||||
return out[:cap]
|
||||
|
||||
|
||||
def format_option_history_row(
|
||||
raw: dict[str, Any],
|
||||
*,
|
||||
tick_sz: Any = None,
|
||||
ct_mult: float = 0.01,
|
||||
) -> dict[str, Any]:
|
||||
"""标准化 OKX positions-history 单条记录供前端展示."""
|
||||
from lib.options.options_pricing_lib import total_premium
|
||||
|
||||
inst_id = str(raw.get("instId") or "").strip()
|
||||
open_avg = _safe_float(raw.get("openAvgPx"))
|
||||
close_avg = _safe_float(raw.get("closeAvgPx"))
|
||||
sheets = _safe_float(raw.get("closeTotalPos"))
|
||||
if sheets is None or sheets <= 0:
|
||||
sheets = _safe_float(raw.get("openMaxPos"))
|
||||
sheets_i = int(abs(sheets or 0))
|
||||
eth_amount = round(abs(sheets or 0) * ct_mult, 8) if sheets else 0.0
|
||||
premium_paid = (
|
||||
round(total_premium(open_avg, eth_amount), 8)
|
||||
if open_avg is not None and eth_amount > 0
|
||||
else None
|
||||
)
|
||||
realized = _safe_float(raw.get("realizedPnl"))
|
||||
if realized is None:
|
||||
realized = _safe_float(raw.get("pnl"))
|
||||
pnl_ratio = _safe_float(raw.get("pnlRatio"))
|
||||
close_type = str(raw.get("type") or "").strip()
|
||||
utime = _safe_float(raw.get("uTime"))
|
||||
ctime = _safe_float(raw.get("cTime"))
|
||||
opt_type, strike = option_fields_from_inst_id(inst_id)
|
||||
uly = str(raw.get("uly") or inst_id.split("-")[0] or "").replace("-USD_UM", "").replace("-USD", "")
|
||||
if close_type in ("3", "4"):
|
||||
status_label = "强平"
|
||||
else:
|
||||
status_label = "已平"
|
||||
return {
|
||||
"source": "exchange",
|
||||
"pos_id": str(raw.get("posId") or "").strip() or None,
|
||||
"inst_id": inst_id,
|
||||
"underlying": uly,
|
||||
"opt_type": opt_type,
|
||||
"strike": strike,
|
||||
"sheets": sheets_i,
|
||||
"eth_amount": eth_amount,
|
||||
"open_avg_px": open_avg,
|
||||
"open_avg_px_fmt": format_option_px(open_avg, tick_sz) if open_avg is not None else None,
|
||||
"close_avg_px": close_avg,
|
||||
"close_avg_px_fmt": format_option_px(close_avg, tick_sz) if close_avg is not None else None,
|
||||
"premium_paid": premium_paid,
|
||||
"premium_paid_fmt": format_usdc_amount(premium_paid),
|
||||
"realized_pnl": realized,
|
||||
"pnl_ratio_pct": round(pnl_ratio * 100, 2) if pnl_ratio is not None else None,
|
||||
"status": "closed",
|
||||
"status_label": status_label,
|
||||
"close_type": close_type,
|
||||
"created_at": _ms_to_iso(ctime),
|
||||
"closed_at": _ms_to_iso(utime),
|
||||
"close_ms": int(utime) if utime is not None else None,
|
||||
"tick_sz": tick_sz,
|
||||
"raw": raw,
|
||||
}
|
||||
|
||||
|
||||
def format_live_option_history_row(
|
||||
row: dict[str, Any],
|
||||
*,
|
||||
open_ms: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""将当前持仓格式化为历史列表中的「持仓中」行."""
|
||||
inst_id = str(row.get("inst_id") or "").strip()
|
||||
return {
|
||||
"source": "live",
|
||||
"pos_id": str((row.get("raw") or {}).get("posId") or "").strip() or None,
|
||||
"inst_id": inst_id,
|
||||
"underlying": str(row.get("underlying") or inst_id.split("-")[0] or ""),
|
||||
"opt_type": row.get("opt_type"),
|
||||
"strike": row.get("strike"),
|
||||
"sheets": int(abs(_safe_float(row.get("pos")) or 0)),
|
||||
"eth_amount": row.get("eth_amount"),
|
||||
"open_avg_px": row.get("avg_px"),
|
||||
"open_avg_px_fmt": row.get("avg_px_fmt"),
|
||||
"close_avg_px": None,
|
||||
"close_avg_px_fmt": None,
|
||||
"premium_paid": row.get("premium_paid"),
|
||||
"premium_paid_fmt": row.get("premium_paid_fmt"),
|
||||
"realized_pnl": row.get("upl"),
|
||||
"pnl_ratio_pct": row.get("upl_ratio_pct"),
|
||||
"status": "open",
|
||||
"status_label": "持仓中",
|
||||
"close_type": None,
|
||||
"created_at": _ms_to_iso(open_ms),
|
||||
"closed_at": None,
|
||||
"close_ms": open_ms,
|
||||
"tick_sz": row.get("tick_sz"),
|
||||
"raw": row.get("raw"),
|
||||
}
|
||||
|
||||
|
||||
def resolve_option_close_from_history(
|
||||
hist_rows: list[dict[str, Any]],
|
||||
*,
|
||||
@@ -947,7 +1132,12 @@ def transfer_main_sub_account(
|
||||
return {"ok": False, "msg": str(e)}
|
||||
|
||||
|
||||
def format_position_row(pos: dict[str, Any], ct_mult: float = 0.01) -> dict[str, Any]:
|
||||
def format_position_row(
|
||||
pos: dict[str, Any],
|
||||
ct_mult: float = 0.01,
|
||||
*,
|
||||
tick_sz: Any = None,
|
||||
) -> dict[str, Any]:
|
||||
from lib.options.options_pricing_lib import (
|
||||
close_breakeven_idx,
|
||||
expiry_breakeven_px,
|
||||
@@ -971,7 +1161,7 @@ def format_position_row(pos: dict[str, Any], ct_mult: float = 0.01) -> dict[str,
|
||||
strike = parsed_strike
|
||||
eth_amount = round(abs(sheets) * ct_mult, 8)
|
||||
premium_paid = (
|
||||
round(total_premium(avg, eth_amount), 4) if avg is not None and eth_amount > 0 else None
|
||||
round(total_premium(avg, eth_amount), 8) if avg is not None and eth_amount > 0 else None
|
||||
)
|
||||
delta_pa = _safe_float(pos.get("deltaPA"))
|
||||
expiry_be = expiry_breakeven_px(
|
||||
@@ -996,6 +1186,11 @@ def format_position_row(pos: dict[str, Any], ct_mult: float = 0.01) -> dict[str,
|
||||
"eth_amount": eth_amount,
|
||||
"avg_px": avg,
|
||||
"mark_px": mark,
|
||||
"avg_px_fmt": format_option_px(avg, tick_sz) if avg is not None else None,
|
||||
"mark_px_fmt": format_option_px(mark, tick_sz) if mark is not None else None,
|
||||
"premium_paid_fmt": format_usdc_amount(premium_paid),
|
||||
"tick_sz": tick_sz,
|
||||
"ct_mult": ct_mult,
|
||||
"idx_px": idx_px,
|
||||
"premium_paid": premium_paid,
|
||||
"upl": upl,
|
||||
|
||||
Reference in New Issue
Block a user