e0e2a94322
Co-authored-by: Cursor <cursoragent@cursor.com>
1309 lines
49 KiB
Python
1309 lines
49 KiB
Python
"""模拟撮合: 用公开行情 bid/ask 成交, 结算到本地钱包."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import uuid
|
||
from datetime import datetime
|
||
from typing import Any, Callable
|
||
|
||
from lib.sim.pricing_lib import option_fill, perp_fill, sim_fee_rate
|
||
from lib.sim.wallets_lib import InsufficientFunds, SimWallets
|
||
|
||
|
||
def _now() -> str:
|
||
"""业务时区墙钟(默认 Asia/Shanghai);勿用 utcnow,否则复盘K线标记会偏 8 小时."""
|
||
try:
|
||
from lib.options.options_time_lib import options_app_now_str
|
||
|
||
return options_app_now_str()
|
||
except Exception:
|
||
from zoneinfo import ZoneInfo
|
||
|
||
tz = ZoneInfo(
|
||
(os.getenv("APP_TIMEZONE") or os.getenv("TZ") or "Asia/Shanghai").strip()
|
||
or "Asia/Shanghai"
|
||
)
|
||
return datetime.now(tz).strftime("%Y-%m-%d %H:%M:%S")
|
||
|
||
|
||
def _ticker_bid_ask(exchange: Any, symbol: str) -> tuple[float, float]:
|
||
"""取买卖一.优先 public ticker,避免私钥无效时 ccxt 带坏 Key 导致 50111."""
|
||
inst = symbol.replace("/", "-") if symbol and "/" in symbol else (symbol or "")
|
||
if inst and exchange is not None and hasattr(exchange, "public_get_market_ticker"):
|
||
try:
|
||
rows = (exchange.public_get_market_ticker({"instId": inst}) or {}).get("data") or []
|
||
if rows:
|
||
t = rows[0]
|
||
bid = t.get("bidPx") or t.get("last") or t.get("lastPx")
|
||
ask = t.get("askPx") or t.get("last") or t.get("lastPx")
|
||
if bid is not None and ask is not None and float(bid) > 0 and float(ask) > 0:
|
||
return float(bid), float(ask)
|
||
except Exception:
|
||
pass
|
||
t = exchange.fetch_ticker(symbol)
|
||
last = t.get("last")
|
||
bid = t.get("bid")
|
||
ask = t.get("ask")
|
||
if bid is None or float(bid) <= 0:
|
||
bid = last
|
||
if ask is None or float(ask) <= 0:
|
||
ask = last
|
||
if bid is None or ask is None:
|
||
raise RuntimeError(f"无法获取 {symbol} 行情 bid/ask")
|
||
return float(bid), float(ask)
|
||
|
||
|
||
def _option_bid_ask(exchange_options: Any, inst_id: str) -> tuple[float, float, float]:
|
||
"""返回 bid, ask, ct_mult."""
|
||
ct_mult = 0.01
|
||
bid = ask = None
|
||
try:
|
||
from lib.exchange.okx_options_lib import quote_option_contract
|
||
|
||
q = quote_option_contract(exchange_options, inst_id)
|
||
if q.get("ok"):
|
||
bid = q.get("bid") or q.get("mark")
|
||
ask = q.get("ask") or q.get("book_ask") or q.get("ref_ask") or q.get("mark")
|
||
ct_mult = float(q.get("ct_mult") or 0.01)
|
||
except Exception:
|
||
pass
|
||
if bid is None or ask is None:
|
||
rows = exchange_options.public_get_market_ticker({"instId": inst_id}).get("data") or []
|
||
if not rows:
|
||
raise RuntimeError(f"无法获取期权行情 {inst_id}")
|
||
t = rows[0]
|
||
bid = bid or t.get("bidPx") or t.get("markPx") or t.get("last")
|
||
ask = ask or t.get("askPx") or t.get("markPx") or t.get("last")
|
||
if bid is None or ask is None:
|
||
raise RuntimeError(f"期权 {inst_id} 缺少 bid/ask")
|
||
return float(bid), float(ask), float(ct_mult)
|
||
|
||
|
||
def _contract_size(exchange: Any, symbol: str) -> float:
|
||
try:
|
||
if hasattr(exchange, "market"):
|
||
m = exchange.market(symbol)
|
||
return float(m.get("contractSize") or 1)
|
||
except Exception:
|
||
pass
|
||
return 1.0
|
||
|
||
|
||
def _premium_ccy_for_inst(inst_id: str) -> str:
|
||
try:
|
||
from lib.options.options_margin_mode_lib import (
|
||
margin_mode_from_inst_id,
|
||
premium_ccy_for_mode,
|
||
)
|
||
|
||
underly = (str(inst_id or "").split("-")[0] or "ETH").upper()
|
||
return premium_ccy_for_mode(margin_mode_from_inst_id(inst_id), underly)
|
||
except Exception:
|
||
return "USDC"
|
||
|
||
|
||
class SimBroker:
|
||
def __init__(self, get_db: Callable) -> None:
|
||
self.get_db = get_db
|
||
self.wallets = SimWallets(get_db)
|
||
|
||
def balances_header(self) -> dict[str, float]:
|
||
return self.wallets.view()
|
||
|
||
def list_perp_positions(self) -> list[dict[str, Any]]:
|
||
conn = self.get_db()
|
||
try:
|
||
rows = conn.execute(
|
||
"SELECT * FROM sim_perp_positions WHERE contracts > 1e-12 ORDER BY id"
|
||
).fetchall()
|
||
out = []
|
||
for r in rows:
|
||
out.append(
|
||
{
|
||
"symbol": r["symbol"],
|
||
"direction": r["direction"],
|
||
"contracts": float(r["contracts"]),
|
||
"entry_px": float(r["entry_px"]),
|
||
"leverage": int(r["leverage"] or 1),
|
||
"margin_usdt": float(r["margin_usdt"] or 0),
|
||
"contract_size": float(r["contract_size"] or 1),
|
||
}
|
||
)
|
||
return out
|
||
finally:
|
||
conn.close()
|
||
|
||
def list_option_positions(self) -> list[dict[str, Any]]:
|
||
conn = self.get_db()
|
||
try:
|
||
rows = conn.execute(
|
||
"SELECT * FROM sim_option_positions WHERE sheets > 1e-12 ORDER BY id"
|
||
).fetchall()
|
||
out = []
|
||
for r in rows:
|
||
out.append(
|
||
{
|
||
"inst_id": r["inst_id"],
|
||
"side": r["side"],
|
||
"sheets": float(r["sheets"]),
|
||
"entry_px": float(r["entry_px"]),
|
||
"ct_mult": float(r["ct_mult"] or 0.01),
|
||
"premium_paid_usdc": float(r["premium_paid_usdc"] or 0),
|
||
}
|
||
)
|
||
return out
|
||
finally:
|
||
conn.close()
|
||
|
||
def get_perp_contracts(self, symbol: str, direction: str) -> float:
|
||
conn = self.get_db()
|
||
try:
|
||
row = conn.execute(
|
||
"""
|
||
SELECT contracts FROM sim_perp_positions
|
||
WHERE symbol=? AND direction=? AND contracts > 1e-12
|
||
""",
|
||
(symbol, direction),
|
||
).fetchone()
|
||
if not row:
|
||
return 0.0
|
||
return float(row["contracts"] if hasattr(row, "keys") else row[0])
|
||
finally:
|
||
conn.close()
|
||
|
||
def place_perp_market(
|
||
self,
|
||
exchange: Any,
|
||
*,
|
||
symbol: str,
|
||
direction: str,
|
||
contracts: float,
|
||
leverage: int,
|
||
fee_rate: float | None = None,
|
||
stop_loss: Any = None,
|
||
take_profit: Any = None,
|
||
) -> dict[str, Any]:
|
||
_ = (stop_loss, take_profit) # sim: ignore attachAlgoOrds / tpsl
|
||
side = (direction or "").lower().strip()
|
||
if side not in ("long", "short"):
|
||
raise ValueError("direction 须为 long 或 short")
|
||
qty_c = float(contracts)
|
||
if qty_c <= 0:
|
||
raise ValueError("张数须大于 0")
|
||
lev = max(1, int(leverage or 1))
|
||
fr = sim_fee_rate(fee_rate)
|
||
bid, ask = _ticker_bid_ask(exchange, symbol)
|
||
ct_sz = _contract_size(exchange, symbol)
|
||
qty = qty_c * ct_sz
|
||
pr = perp_fill(side=side, action="open", bid=bid, ask=ask, qty=qty, fee_rate=fr)
|
||
margin = pr.notional / lev
|
||
need = margin + pr.fee
|
||
try:
|
||
self.wallets.debit_trading(
|
||
"USDT",
|
||
need,
|
||
kind="perp_open",
|
||
note=f"open {side} {symbol} {qty_c}@{pr.fill_px:.4f}",
|
||
)
|
||
except InsufficientFunds as e:
|
||
raise RuntimeError(str(e)) from e
|
||
|
||
conn = self.get_db()
|
||
try:
|
||
existing = conn.execute(
|
||
"SELECT * FROM sim_perp_positions WHERE symbol=? AND direction=?",
|
||
(symbol, side),
|
||
).fetchone()
|
||
if existing and float(existing["contracts"] or 0) > 1e-12:
|
||
old_c = float(existing["contracts"])
|
||
old_px = float(existing["entry_px"])
|
||
old_m = float(existing["margin_usdt"] or 0)
|
||
new_c = old_c + qty_c
|
||
entry = (old_px * old_c + pr.fill_px * qty_c) / new_c
|
||
conn.execute(
|
||
"""
|
||
UPDATE sim_perp_positions
|
||
SET contracts=?, entry_px=?, leverage=?, margin_usdt=?, contract_size=?, updated_at=?
|
||
WHERE symbol=? AND direction=?
|
||
""",
|
||
(new_c, entry, lev, old_m + margin, ct_sz, _now(), symbol, side),
|
||
)
|
||
else:
|
||
conn.execute(
|
||
"""
|
||
INSERT INTO sim_perp_positions(
|
||
symbol, direction, contracts, entry_px, leverage, margin_usdt, contract_size, updated_at
|
||
) VALUES (?,?,?,?,?,?,?,?)
|
||
ON CONFLICT(symbol, direction) DO UPDATE SET
|
||
contracts=excluded.contracts,
|
||
entry_px=excluded.entry_px,
|
||
leverage=excluded.leverage,
|
||
margin_usdt=excluded.margin_usdt,
|
||
contract_size=excluded.contract_size,
|
||
updated_at=excluded.updated_at
|
||
""",
|
||
(symbol, side, qty_c, pr.fill_px, lev, margin, ct_sz, _now()),
|
||
)
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
|
||
oid = f"sim-perp-{uuid.uuid4().hex[:16]}"
|
||
return {
|
||
"id": oid,
|
||
"symbol": symbol,
|
||
"side": "buy" if side == "long" else "sell",
|
||
"amount": qty_c,
|
||
"average": pr.fill_px,
|
||
"status": "closed",
|
||
"info": {"sim": True, "fee": pr.fee, "margin": margin, "fill": pr.to_dict()},
|
||
"tpsl_attached": False,
|
||
}
|
||
|
||
def close_perp_market(
|
||
self,
|
||
exchange: Any,
|
||
*,
|
||
symbol: str,
|
||
direction: str,
|
||
contracts: float | None = None,
|
||
fee_rate: float | None = None,
|
||
) -> dict[str, Any]:
|
||
side = (direction or "").lower().strip()
|
||
if side not in ("long", "short"):
|
||
raise ValueError("direction 须为 long 或 short")
|
||
# 行情先拉,再开库,避免握锁等网络
|
||
bid, ask = _ticker_bid_ask(exchange, symbol)
|
||
conn = self.get_db()
|
||
try:
|
||
row = conn.execute(
|
||
"SELECT * FROM sim_perp_positions WHERE symbol=? AND direction=?",
|
||
(symbol, side),
|
||
).fetchone()
|
||
if not row or float(row["contracts"] or 0) <= 1e-12:
|
||
raise ValueError("模拟永续无对应持仓")
|
||
pos_c = float(row["contracts"])
|
||
entry = float(row["entry_px"])
|
||
margin_all = float(row["margin_usdt"] or 0)
|
||
ct_sz = float(row["contract_size"] or 1)
|
||
close_c = pos_c if contracts is None else min(pos_c, float(contracts))
|
||
if close_c <= 0:
|
||
raise ValueError("平仓张数无效")
|
||
fr = sim_fee_rate(fee_rate)
|
||
qty = close_c * ct_sz
|
||
pr = perp_fill(side=side, action="close", bid=bid, ask=ask, qty=qty, fee_rate=fr)
|
||
if side == "long":
|
||
pnl = (pr.fill_px - entry) * qty
|
||
else:
|
||
pnl = (entry - pr.fill_px) * qty
|
||
release = margin_all * (close_c / pos_c)
|
||
credit = release + pnl - pr.fee
|
||
remain = pos_c - close_c
|
||
if remain <= 1e-12:
|
||
conn.execute(
|
||
"DELETE FROM sim_perp_positions WHERE symbol=? AND direction=?",
|
||
(symbol, side),
|
||
)
|
||
else:
|
||
conn.execute(
|
||
"""
|
||
UPDATE sim_perp_positions
|
||
SET contracts=?, margin_usdt=?, updated_at=?
|
||
WHERE symbol=? AND direction=?
|
||
""",
|
||
(remain, margin_all - release, _now(), symbol, side),
|
||
)
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
|
||
if credit >= 0:
|
||
self.wallets.credit_trading(
|
||
"USDT",
|
||
credit,
|
||
kind="perp_close",
|
||
note=f"close {side} {symbol} pnl={pnl:.4f}",
|
||
)
|
||
else:
|
||
self.wallets.debit_trading(
|
||
"USDT",
|
||
abs(credit),
|
||
kind="perp_close",
|
||
note=f"close {side} {symbol} pnl={pnl:.4f}",
|
||
)
|
||
|
||
oid = f"sim-perp-close-{uuid.uuid4().hex[:16]}"
|
||
return {
|
||
"id": oid,
|
||
"symbol": symbol,
|
||
"side": "sell" if side == "long" else "buy",
|
||
"amount": close_c,
|
||
"average": pr.fill_px,
|
||
"status": "closed",
|
||
"info": {
|
||
"sim": True,
|
||
"fee": pr.fee,
|
||
"pnl": pnl,
|
||
"released_margin": release,
|
||
"fill": pr.to_dict(),
|
||
},
|
||
"tpsl_attached": False,
|
||
}
|
||
|
||
def _store_option_order(
|
||
self,
|
||
*,
|
||
ord_id: str,
|
||
inst_id: str,
|
||
side: str,
|
||
sheets: float,
|
||
avg_px: float,
|
||
fee: float = 0.0,
|
||
fee_ccy: str | None = None,
|
||
) -> None:
|
||
conn = self.get_db()
|
||
try:
|
||
ccy = str(fee_ccy or _premium_ccy_for_inst(inst_id) or "").strip().upper()
|
||
conn.execute(
|
||
"""
|
||
INSERT OR REPLACE INTO sim_option_orders(
|
||
ord_id, inst_id, side, sheets, avg_px, state, acc_fill_sz,
|
||
fee, fee_ccy, created_at
|
||
) VALUES (?,?,?,?,?,'filled',?,?,?,?)
|
||
""",
|
||
(
|
||
ord_id,
|
||
inst_id,
|
||
side,
|
||
float(sheets),
|
||
float(avg_px),
|
||
float(sheets),
|
||
float(fee or 0),
|
||
ccy,
|
||
_now(),
|
||
),
|
||
)
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
|
||
def get_option_order(self, ord_id: str) -> dict[str, Any] | None:
|
||
conn = self.get_db()
|
||
try:
|
||
row = conn.execute(
|
||
"SELECT * FROM sim_option_orders WHERE ord_id=?",
|
||
(ord_id,),
|
||
).fetchone()
|
||
if not row:
|
||
return None
|
||
keys = set(row.keys()) if hasattr(row, "keys") else set()
|
||
fee = float(row["fee"]) if "fee" in keys and row["fee"] is not None else 0.0
|
||
fee_ccy = str(row["fee_ccy"] or "").strip().upper() if "fee_ccy" in keys else ""
|
||
if not fee_ccy:
|
||
fee_ccy = _premium_ccy_for_inst(row["inst_id"])
|
||
return {
|
||
"ok": True,
|
||
"ord_id": row["ord_id"],
|
||
"inst_id": row["inst_id"],
|
||
"side": row["side"],
|
||
"state": row["state"],
|
||
"acc_fill_sz": float(row["acc_fill_sz"]),
|
||
"avg_px": float(row["avg_px"]),
|
||
"fee": -abs(fee) if fee else 0.0,
|
||
"fee_ccy": fee_ccy,
|
||
"sim": True,
|
||
"raw": {"fee": -abs(fee) if fee else 0.0, "feeCcy": fee_ccy, "sim": True},
|
||
}
|
||
finally:
|
||
conn.close()
|
||
|
||
def place_option_buy(
|
||
self,
|
||
exchange_options: Any,
|
||
*,
|
||
inst_id: str,
|
||
sheets: int,
|
||
price: float | None = None,
|
||
fee_rate: float | None = None,
|
||
**_kwargs: Any,
|
||
) -> dict[str, Any]:
|
||
n = int(sheets)
|
||
if n < 1:
|
||
return {"ok": False, "msg": "张数至少为 1"}
|
||
fr = sim_fee_rate(fee_rate)
|
||
bid, ask, ct_mult = _option_bid_ask(exchange_options, inst_id)
|
||
if price is not None and float(price) > 0:
|
||
# 限价: 用 min(limit, ask) 作为基准卖一近似, 仍走 option_fill 滑点
|
||
ask = min(float(ask), float(price)) if float(price) > 0 else float(ask)
|
||
qty = n * ct_mult
|
||
pr = option_fill(action="open", bid=bid, ask=ask, qty=qty, fee_rate=fr)
|
||
cost = pr.notional + pr.fee
|
||
prem_ccy = _premium_ccy_for_inst(inst_id)
|
||
try:
|
||
self.wallets.debit_trading(
|
||
prem_ccy,
|
||
cost,
|
||
kind="option_open",
|
||
note=f"buy {inst_id} x{n}@{pr.fill_px}",
|
||
)
|
||
except InsufficientFunds as e:
|
||
return {"ok": False, "msg": str(e)}
|
||
|
||
conn = self.get_db()
|
||
try:
|
||
existing = conn.execute(
|
||
"SELECT * FROM sim_option_positions WHERE inst_id=?",
|
||
(inst_id,),
|
||
).fetchone()
|
||
if existing and float(existing["sheets"] or 0) > 1e-12:
|
||
old_s = float(existing["sheets"])
|
||
old_px = float(existing["entry_px"])
|
||
old_prem = float(existing["premium_paid_usdc"] or 0)
|
||
new_s = old_s + n
|
||
entry = (old_px * old_s + pr.fill_px * n) / new_s
|
||
conn.execute(
|
||
"""
|
||
UPDATE sim_option_positions
|
||
SET sheets=?, entry_px=?, ct_mult=?, premium_paid_usdc=?, updated_at=?
|
||
WHERE inst_id=?
|
||
""",
|
||
(new_s, entry, ct_mult, old_prem + pr.notional, _now(), inst_id),
|
||
)
|
||
else:
|
||
conn.execute(
|
||
"""
|
||
INSERT INTO sim_option_positions(
|
||
inst_id, side, sheets, entry_px, ct_mult, premium_paid_usdc, updated_at
|
||
) VALUES (?,?,?,?,?,?,?)
|
||
ON CONFLICT(inst_id) DO UPDATE SET
|
||
sheets=excluded.sheets,
|
||
entry_px=excluded.entry_px,
|
||
ct_mult=excluded.ct_mult,
|
||
premium_paid_usdc=excluded.premium_paid_usdc,
|
||
updated_at=excluded.updated_at
|
||
""",
|
||
(inst_id, "buy", float(n), pr.fill_px, ct_mult, pr.notional, _now()),
|
||
)
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
|
||
ord_id = f"sim-opt-{uuid.uuid4().hex[:16]}"
|
||
self._store_option_order(
|
||
ord_id=ord_id,
|
||
inst_id=inst_id,
|
||
side="buy",
|
||
sheets=n,
|
||
avg_px=pr.fill_px,
|
||
fee=float(pr.fee or 0),
|
||
fee_ccy=prem_ccy,
|
||
)
|
||
return {
|
||
"ok": True,
|
||
"data": {"ordId": ord_id, "sCode": "0", "sMsg": "sim filled"},
|
||
"raw": {"sim": True, "fee": pr.fee, "feeCcy": prem_ccy},
|
||
"px": pr.fill_px,
|
||
"ord_type": "ioc",
|
||
"fee": float(pr.fee or 0),
|
||
"fee_ccy": prem_ccy,
|
||
"info": {"sim": True, "fee": pr.fee, "fee_ccy": prem_ccy, "fill": pr.to_dict()},
|
||
}
|
||
|
||
def sell_option_close(
|
||
self,
|
||
exchange_options: Any,
|
||
*,
|
||
inst_id: str,
|
||
sheets: int,
|
||
price: float | None = None,
|
||
fee_rate: float | None = None,
|
||
**_kwargs: Any,
|
||
) -> dict[str, Any]:
|
||
n = int(sheets)
|
||
if n < 1:
|
||
return {"ok": False, "msg": "张数至少为 1"}
|
||
# 行情先拉,再开库,避免握锁等网络/嵌套写钱包
|
||
bid, ask, _ = _option_bid_ask(exchange_options, inst_id)
|
||
if price is not None and float(price) > 0:
|
||
bid = max(float(bid), float(price)) if float(price) > 0 else float(bid)
|
||
conn = self.get_db()
|
||
try:
|
||
row = conn.execute(
|
||
"SELECT * FROM sim_option_positions WHERE inst_id=?",
|
||
(inst_id,),
|
||
).fetchone()
|
||
if not row or float(row["sheets"] or 0) <= 1e-12:
|
||
return {"ok": False, "msg": "模拟期权无对应持仓"}
|
||
pos_s = float(row["sheets"])
|
||
ct_mult = float(row["ct_mult"] or 0.01)
|
||
close_n = min(pos_s, float(n))
|
||
if close_n <= 0:
|
||
return {"ok": False, "msg": "平仓张数无效"}
|
||
fr = sim_fee_rate(fee_rate)
|
||
qty = close_n * ct_mult
|
||
pr = option_fill(action="close", bid=bid, ask=ask, qty=qty, fee_rate=fr)
|
||
credit = pr.notional - pr.fee
|
||
remain = pos_s - close_n
|
||
if remain <= 1e-12:
|
||
conn.execute("DELETE FROM sim_option_positions WHERE inst_id=?", (inst_id,))
|
||
else:
|
||
prem = float(row["premium_paid_usdc"] or 0) * (remain / pos_s)
|
||
conn.execute(
|
||
"""
|
||
UPDATE sim_option_positions
|
||
SET sheets=?, premium_paid_usdc=?, updated_at=?
|
||
WHERE inst_id=?
|
||
""",
|
||
(remain, prem, _now(), inst_id),
|
||
)
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
|
||
if credit > 0:
|
||
prem_ccy = _premium_ccy_for_inst(inst_id)
|
||
self.wallets.credit_trading(
|
||
prem_ccy,
|
||
credit,
|
||
kind="option_close",
|
||
note=f"sell {inst_id} x{close_n}@{pr.fill_px}",
|
||
)
|
||
ord_id = f"sim-opt-close-{uuid.uuid4().hex[:16]}"
|
||
prem_ccy = _premium_ccy_for_inst(inst_id)
|
||
self._store_option_order(
|
||
ord_id=ord_id,
|
||
inst_id=inst_id,
|
||
side="sell",
|
||
sheets=close_n,
|
||
avg_px=pr.fill_px,
|
||
fee=float(pr.fee or 0),
|
||
fee_ccy=prem_ccy,
|
||
)
|
||
return {
|
||
"ok": True,
|
||
"data": {"ordId": ord_id, "sCode": "0", "sMsg": "sim filled"},
|
||
"raw": {"sim": True, "fee": pr.fee, "feeCcy": prem_ccy},
|
||
"px": pr.fill_px,
|
||
"ord_type": "ioc",
|
||
"fee": float(pr.fee or 0),
|
||
"fee_ccy": prem_ccy,
|
||
"info": {"sim": True, "fee": pr.fee, "fee_ccy": prem_ccy, "fill": pr.to_dict()},
|
||
}
|
||
|
||
def convert_usdt_usdc(
|
||
self,
|
||
exchange: Any,
|
||
*,
|
||
direction: str,
|
||
amount: float,
|
||
fee_rate: float | None = None,
|
||
account: str = "trading",
|
||
symbol: str = "USDC/USDT",
|
||
) -> dict[str, Any]:
|
||
"""模拟 USDC-USDT 现货市价兑换, 默认扣交易账户."""
|
||
from lib.sim.pricing_lib import spot_usdc_usdt_fill
|
||
|
||
fr = sim_fee_rate(fee_rate)
|
||
bid, ask = _ticker_bid_ask(exchange, symbol)
|
||
fill = spot_usdc_usdt_fill(
|
||
direction=direction, amount=float(amount), bid=bid, ask=ask, fee_rate=fr
|
||
)
|
||
result = SimWallets(self.get_db).convert(
|
||
from_ccy=fill.from_ccy,
|
||
to_ccy=fill.to_ccy,
|
||
amount=fill.from_amount,
|
||
account=account or "trading",
|
||
to_amount=fill.to_amount,
|
||
rate=fill.fill_px,
|
||
fee=fill.fee,
|
||
note=f"USDC/USDT mkt {fill.fill_px:.6f} (bid {bid:.6f}/ask {ask:.6f})",
|
||
)
|
||
if not result.get("ok"):
|
||
return result
|
||
result.update(
|
||
{
|
||
"direction": fill.direction,
|
||
"bid": bid,
|
||
"ask": ask,
|
||
"base_px": fill.base_px,
|
||
"fill_px": fill.fill_px,
|
||
"fee_rate": fr,
|
||
"symbol": symbol,
|
||
}
|
||
)
|
||
return result
|
||
|
||
def convert_usdt_coin(
|
||
self,
|
||
exchange: Any,
|
||
*,
|
||
underlying: str,
|
||
direction: str,
|
||
amount: float,
|
||
fee_rate: float | None = None,
|
||
account: str = "trading",
|
||
) -> dict[str, Any]:
|
||
"""模拟 ETH/BTC-USDT 现货市价兑换(交易账户)."""
|
||
from lib.options.options_margin_mode_lib import spot_quote_inst_id
|
||
from lib.sim.pricing_lib import spot_coin_usdt_fill
|
||
|
||
coin = (underlying or "ETH").strip().upper() or "ETH"
|
||
if coin not in ("ETH", "BTC"):
|
||
return {"ok": False, "msg": f"不支持标的 {coin}"}
|
||
# spot_quote_inst_id → ETH-USDT;ccxt 常用 ETH/USDT
|
||
inst = spot_quote_inst_id(coin)
|
||
symbol = inst.replace("-", "/") if inst else f"{coin}/USDT"
|
||
fr = sim_fee_rate(fee_rate)
|
||
bid, ask = _ticker_bid_ask(exchange, symbol)
|
||
fill = spot_coin_usdt_fill(
|
||
direction=direction,
|
||
amount=float(amount),
|
||
bid=bid,
|
||
ask=ask,
|
||
fee_rate=fr,
|
||
coin=coin,
|
||
)
|
||
result = SimWallets(self.get_db).convert(
|
||
from_ccy=fill.from_ccy,
|
||
to_ccy=fill.to_ccy,
|
||
amount=fill.from_amount,
|
||
account=account or "trading",
|
||
to_amount=fill.to_amount,
|
||
rate=fill.fill_px,
|
||
fee=fill.fee,
|
||
note=f"{symbol} mkt {fill.fill_px:.4f} (bid {bid:.4f}/ask {ask:.4f})",
|
||
)
|
||
if not result.get("ok"):
|
||
return result
|
||
result.update(
|
||
{
|
||
"direction": fill.direction,
|
||
"underlying": coin,
|
||
"bid": bid,
|
||
"ask": ask,
|
||
"base_px": fill.base_px,
|
||
"fill_px": fill.fill_px,
|
||
"fee_rate": fr,
|
||
"symbol": symbol,
|
||
"inst_id": inst,
|
||
"coin_bought": fill.to_amount if fill.direction == "usdt_to_coin" else None,
|
||
"coin_sold": fill.from_amount if fill.direction == "coin_to_usdt" else None,
|
||
"usdt_spent": fill.from_amount if fill.direction == "usdt_to_coin" else None,
|
||
"usdt_recovered": fill.to_amount if fill.direction == "coin_to_usdt" else None,
|
||
}
|
||
)
|
||
return result
|
||
|
||
def _index_px_for_option(
|
||
self,
|
||
exchange: Any,
|
||
inst_id: str,
|
||
*,
|
||
idx_cache: dict[str, float | None] | None = None,
|
||
) -> float | None:
|
||
"""到期结算/持仓展示用指数价:优先合约行情,否则 family 指数."""
|
||
from lib.exchange.okx_options_lib import (
|
||
fetch_index_price,
|
||
inst_family_from_inst_id,
|
||
quote_option_contract,
|
||
)
|
||
|
||
cache = idx_cache if idx_cache is not None else {}
|
||
if exchange is None or not inst_id:
|
||
return None
|
||
try:
|
||
q = quote_option_contract(exchange, inst_id)
|
||
if q.get("ok") and q.get("index_px") is not None:
|
||
return float(q["index_px"])
|
||
except Exception:
|
||
pass
|
||
family = inst_family_from_inst_id(inst_id) or ""
|
||
uly = family.replace("_UM", "") if family else ""
|
||
if not uly:
|
||
return None
|
||
if uly not in cache:
|
||
try:
|
||
cache[uly] = fetch_index_price(exchange, uly)
|
||
except Exception:
|
||
cache[uly] = None
|
||
return cache.get(uly)
|
||
|
||
def settle_expired_option_positions(
|
||
self,
|
||
exchange: Any = None,
|
||
*,
|
||
now_ms: int | None = None,
|
||
) -> list[dict[str, Any]]:
|
||
"""模拟盘到期结算:按指数内在价值兑付后删除本地仓(无实盘交割).
|
||
|
||
虚值兑付 0;实值 credit 交易账户 USDC.同时回写 options_trades 为 closed.
|
||
"""
|
||
import time
|
||
|
||
from lib.exchange.okx_options_lib import (
|
||
expiry_ms_from_inst_id,
|
||
option_fields_from_inst_id,
|
||
)
|
||
from lib.options.options_pricing_lib import option_expiry_pnl
|
||
|
||
now = int(now_ms if now_ms is not None else time.time() * 1000)
|
||
settled: list[dict[str, Any]] = []
|
||
idx_cache: dict[str, float | None] = {}
|
||
|
||
for p in self.list_option_positions():
|
||
inst_id = str(p.get("inst_id") or "").strip()
|
||
if not inst_id:
|
||
continue
|
||
exp_ms = expiry_ms_from_inst_id(inst_id)
|
||
if exp_ms is None or now < int(exp_ms):
|
||
continue
|
||
opt_type, strike = option_fields_from_inst_id(inst_id)
|
||
if strike is None:
|
||
continue
|
||
spot = self._index_px_for_option(exchange, inst_id, idx_cache=idx_cache)
|
||
if spot is None:
|
||
# 无指数则本轮跳过,避免实值误按 0 结算
|
||
continue
|
||
|
||
sheets = float(p.get("sheets") or 0)
|
||
ct_mult = float(p.get("ct_mult") or 0.01)
|
||
prem = float(p.get("premium_paid_usdc") or 0)
|
||
pnl = float(
|
||
option_expiry_pnl(
|
||
opt_type=str(opt_type or "P"),
|
||
strike=float(strike),
|
||
spot=float(spot),
|
||
sheets=sheets,
|
||
ct_mult=ct_mult,
|
||
premium_paid=prem,
|
||
)
|
||
)
|
||
settle_recv = round(max(0.0, prem + pnl), 4)
|
||
o = (opt_type or "").strip().upper()
|
||
if o in ("C", "CALL"):
|
||
intrinsic_u = max(0.0, float(spot) - float(strike))
|
||
elif o in ("P", "PUT"):
|
||
intrinsic_u = max(0.0, float(strike) - float(spot))
|
||
else:
|
||
intrinsic_u = 0.0
|
||
|
||
prem_ccy = _premium_ccy_for_inst(inst_id)
|
||
# 币本位到期兑付用币数量: 实值/指数 × 张数 × 乘数
|
||
if prem_ccy in ("ETH", "BTC") and float(spot) > 0:
|
||
settle_recv = round(
|
||
max(0.0, (intrinsic_u / float(spot)) * sheets * ct_mult),
|
||
8,
|
||
)
|
||
|
||
conn = self.get_db()
|
||
try:
|
||
conn.execute("DELETE FROM sim_option_positions WHERE inst_id=?", (inst_id,))
|
||
try:
|
||
closed_at = _now()
|
||
open_ids = [
|
||
int(r["id"])
|
||
for r in conn.execute(
|
||
"SELECT id FROM options_trades WHERE inst_id=? AND status='open'",
|
||
(inst_id,),
|
||
).fetchall()
|
||
]
|
||
conn.execute(
|
||
"""
|
||
UPDATE options_trades
|
||
SET status = 'closed',
|
||
close_quote = ?,
|
||
premium_received = ?,
|
||
realized_pnl = ?,
|
||
closed_at = COALESCE(closed_at, ?),
|
||
signal_note = CASE
|
||
WHEN signal_note IS NULL OR TRIM(signal_note) = ''
|
||
THEN '到期结算'
|
||
ELSE signal_note
|
||
END
|
||
WHERE inst_id = ? AND status = 'open'
|
||
""",
|
||
(
|
||
round(intrinsic_u, 6),
|
||
settle_recv,
|
||
round(pnl, 4),
|
||
closed_at,
|
||
inst_id,
|
||
),
|
||
)
|
||
if open_ids:
|
||
from lib.options.options_notify_lib import (
|
||
active_options_notify_cfg,
|
||
notify_options_close_trade_ids,
|
||
)
|
||
|
||
notify_options_close_trade_ids(
|
||
active_options_notify_cfg(),
|
||
conn,
|
||
open_ids,
|
||
reason="到期结算",
|
||
)
|
||
except Exception:
|
||
pass
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
|
||
if settle_recv > 1e-12:
|
||
self.wallets.credit_trading(
|
||
prem_ccy,
|
||
settle_recv,
|
||
kind="option_expiry",
|
||
note=f"expiry settle {inst_id} @{spot:g} recv={settle_recv}",
|
||
)
|
||
ord_id = f"sim-opt-exp-{uuid.uuid4().hex[:16]}"
|
||
self._store_option_order(
|
||
ord_id=ord_id,
|
||
inst_id=inst_id,
|
||
side="settle",
|
||
sheets=sheets,
|
||
avg_px=intrinsic_u,
|
||
)
|
||
try:
|
||
from lib.options.options_positions_lib import forget_close_gate_for_inst
|
||
|
||
forget_close_gate_for_inst(inst_id)
|
||
except Exception:
|
||
pass
|
||
try:
|
||
from lib.options.options_margin_mode_lib import margin_mode_from_inst_id
|
||
from lib.options.options_spot_bridge_lib import sell_residual_after_option_flat
|
||
|
||
if margin_mode_from_inst_id(inst_id) == "coin":
|
||
uly = inst_id.split("-")[0].upper() if "-" in inst_id else "ETH"
|
||
sell_residual_after_option_flat(
|
||
self.get_db,
|
||
exchange,
|
||
underlying=uly,
|
||
inst_id=inst_id,
|
||
)
|
||
except Exception as e:
|
||
print(f"[sim_expiry_sell] {inst_id}: {e}", flush=True)
|
||
settled.append(
|
||
{
|
||
"inst_id": inst_id,
|
||
"spot": spot,
|
||
"intrinsic": intrinsic_u,
|
||
"premium_received": settle_recv,
|
||
"realized_pnl": round(pnl, 4),
|
||
"ord_id": ord_id,
|
||
}
|
||
)
|
||
return settled
|
||
|
||
def option_positions_okx_rows(self, exchange: Any = None) -> list[dict[str, Any]]:
|
||
"""对齐 OKX positions 行字段, 供 format_position_row 使用.
|
||
|
||
模拟盘补充公开行情的 idxPx / markPx, 否则指数价与平掉回本均为空.
|
||
拉取前先结算已到期仓,避免虚值到期后一直挂在当前持仓.
|
||
"""
|
||
from lib.exchange.okx_options_lib import (
|
||
expiry_ms_from_inst_id,
|
||
option_fields_from_inst_id,
|
||
quote_option_contract,
|
||
)
|
||
|
||
try:
|
||
self.settle_expired_option_positions(exchange)
|
||
except Exception:
|
||
pass
|
||
|
||
rows: list[dict[str, Any]] = []
|
||
idx_cache: dict[str, float | None] = {}
|
||
for p in self.list_option_positions():
|
||
inst_id = str(p["inst_id"] or "")
|
||
sheets = float(p["sheets"])
|
||
entry = float(p["entry_px"])
|
||
ct_mult = float(p.get("ct_mult") or 0.01)
|
||
opt_type, strike = option_fields_from_inst_id(inst_id)
|
||
mark = entry
|
||
idx = None
|
||
exp_ms = expiry_ms_from_inst_id(inst_id)
|
||
|
||
if exchange is not None and inst_id:
|
||
try:
|
||
q = quote_option_contract(exchange, inst_id)
|
||
if q.get("ok"):
|
||
raw_mark = q.get("mark")
|
||
if raw_mark is None:
|
||
raw_mark = q.get("bid")
|
||
if raw_mark is not None and float(raw_mark) > 0:
|
||
mark = float(raw_mark)
|
||
if q.get("index_px") is not None:
|
||
idx = float(q["index_px"])
|
||
if q.get("opt_type"):
|
||
opt_type = str(q["opt_type"])
|
||
if q.get("strike") is not None:
|
||
strike = float(q["strike"])
|
||
if q.get("exp_time") is not None:
|
||
try:
|
||
exp_ms = int(float(q["exp_time"]))
|
||
except (TypeError, ValueError):
|
||
pass
|
||
except Exception:
|
||
pass
|
||
if idx is None:
|
||
idx = self._index_px_for_option(exchange, inst_id, idx_cache=idx_cache)
|
||
|
||
eth = abs(sheets) * ct_mult
|
||
upl = (mark - entry) * eth
|
||
prem = float(p.get("premium_paid_usdc") or 0) or (entry * eth)
|
||
upl_ratio = (upl / prem) if prem > 1e-12 else 0.0
|
||
row: dict[str, Any] = {
|
||
"instId": inst_id,
|
||
"pos": str(sheets),
|
||
"availPos": str(sheets),
|
||
"avgPx": str(entry),
|
||
"markPx": str(mark),
|
||
"upl": str(round(upl, 4)),
|
||
"uplRatio": str(round(upl_ratio, 6)),
|
||
"posSide": "long",
|
||
"mgnMode": "isolated",
|
||
}
|
||
if idx is not None:
|
||
row["idxPx"] = str(idx)
|
||
if opt_type:
|
||
row["optType"] = opt_type
|
||
if strike is not None:
|
||
row["stk"] = str(strike)
|
||
if exp_ms:
|
||
row["expTime"] = str(exp_ms)
|
||
rows.append(row)
|
||
return rows
|
||
|
||
@staticmethod
|
||
def _sells_from_ledger(
|
||
conn: Any,
|
||
*,
|
||
inst: str,
|
||
opened: str,
|
||
closed: str,
|
||
) -> list[Any]:
|
||
"""从 sim_ledger_entries 的 option_close 备注解析卖平成交."""
|
||
import re
|
||
|
||
try:
|
||
params: list[Any] = [f"%sell {inst} %"]
|
||
sql = """
|
||
SELECT note, ts AS created_at
|
||
FROM sim_ledger_entries
|
||
WHERE kind = 'option_close' AND note LIKE ?
|
||
"""
|
||
if opened:
|
||
sql += " AND ts >= ?"
|
||
params.append(opened[:19])
|
||
if closed:
|
||
sql += " AND ts <= ?"
|
||
params.append((closed[:16] + ":59") if len(closed) >= 16 else closed)
|
||
sql += " ORDER BY ts ASC, id ASC"
|
||
rows = conn.execute(sql, params).fetchall()
|
||
except Exception:
|
||
return []
|
||
out: list[dict[str, Any]] = []
|
||
pat = re.compile(
|
||
rf"sell\s+{re.escape(inst)}\s+x(?P<sheets>[0-9.]+)@(?P<px>[0-9.]+)",
|
||
re.I,
|
||
)
|
||
for i, r in enumerate(rows):
|
||
note = str(r["note"] or "")
|
||
m = pat.search(note)
|
||
if not m:
|
||
continue
|
||
try:
|
||
sh = float(m.group("sheets"))
|
||
px = float(m.group("px"))
|
||
except (TypeError, ValueError):
|
||
continue
|
||
if sh <= 0 or px <= 0:
|
||
continue
|
||
out.append(
|
||
{
|
||
"ord_id": f"ledger-{i}",
|
||
"sheets": sh,
|
||
"avg_px": px,
|
||
"created_at": r["created_at"],
|
||
}
|
||
)
|
||
return out
|
||
|
||
def repair_closed_options_pnl_from_sim_orders(self, *, limit: int = 300) -> int:
|
||
"""用 sim_option_orders 卖平成交重算已平仓收回(修复分批平仓只记最后一笔).
|
||
|
||
返回更新条数.历史页/复盘合成前调用,使旧数据也能自愈.
|
||
"""
|
||
from lib.options.options_db import init_options_tables
|
||
from lib.options.options_pricing_lib import total_premium
|
||
from lib.sim.db_lib import init_sim_tables
|
||
|
||
conn = self.get_db()
|
||
updated = 0
|
||
try:
|
||
init_options_tables(conn)
|
||
init_sim_tables(conn)
|
||
# 表不存在时跳过(极旧库)
|
||
try:
|
||
conn.execute("SELECT 1 FROM sim_option_orders LIMIT 1").fetchone()
|
||
except Exception:
|
||
return 0
|
||
|
||
trades = conn.execute(
|
||
"""
|
||
SELECT id, inst_id, sheets, eth_amount, premium_paid, premium_received,
|
||
realized_pnl, close_quote, created_at, closed_at, close_ord_id
|
||
FROM options_trades
|
||
WHERE status = 'closed'
|
||
ORDER BY id DESC
|
||
LIMIT ?
|
||
""",
|
||
(int(max(1, min(int(limit), 500))),),
|
||
).fetchall()
|
||
for tr in trades:
|
||
inst = str(tr["inst_id"] or "").strip()
|
||
if not inst:
|
||
continue
|
||
want_sheets = float(tr["sheets"] or 0)
|
||
if want_sheets <= 0:
|
||
continue
|
||
eth_amt = float(tr["eth_amount"] or 0)
|
||
ct_mult = (eth_amt / want_sheets) if eth_amt > 0 else 0.01
|
||
if ct_mult <= 0:
|
||
ct_mult = 0.01
|
||
opened = str(tr["created_at"] or "").strip()
|
||
closed = str(tr["closed_at"] or "").strip()
|
||
# 时间窗:开仓后、平仓后 2 分钟内(含下单延迟)
|
||
params: list[Any] = [inst]
|
||
sql = """
|
||
SELECT ord_id, sheets, avg_px, created_at
|
||
FROM sim_option_orders
|
||
WHERE inst_id = ? AND lower(side) = 'sell'
|
||
"""
|
||
if opened:
|
||
sql += " AND created_at >= ?"
|
||
params.append(opened[:19])
|
||
if closed:
|
||
# 字符串时间比较;闭区间放宽到同分钟及之后的短尾
|
||
sql += " AND created_at <= ?"
|
||
params.append((closed[:16] + ":59") if len(closed) >= 16 else closed)
|
||
sql += " ORDER BY created_at ASC, ord_id ASC"
|
||
sells = conn.execute(sql, params).fetchall()
|
||
if not sells:
|
||
# 回退:从钱包流水 note 解析 sell INST x张数@价
|
||
sells = self._sells_from_ledger(
|
||
conn, inst=inst, opened=opened, closed=closed
|
||
)
|
||
if not sells:
|
||
continue
|
||
selected: list[Any] = []
|
||
sheets_sum = 0.0
|
||
for o in sells:
|
||
selected.append(o)
|
||
sheets_sum += float(o["sheets"] or 0)
|
||
if sheets_sum + 1e-9 >= want_sheets:
|
||
break
|
||
if sheets_sum + 1e-6 < want_sheets * 0.5:
|
||
# 成交张数明显不足,不瞎改
|
||
continue
|
||
recv = 0.0
|
||
notional = 0.0
|
||
oids: list[str] = []
|
||
for o in selected:
|
||
sh = float(o["sheets"] or 0)
|
||
px = float(o["avg_px"] or 0)
|
||
if sh <= 0 or px <= 0:
|
||
continue
|
||
recv += total_premium(px, sh * ct_mult)
|
||
notional += px * sh
|
||
oid = str(o["ord_id"] or "").strip()
|
||
if oid and oid not in oids:
|
||
oids.append(oid)
|
||
if not oids:
|
||
continue
|
||
total_recv = round(recv, 8)
|
||
paid = float(tr["premium_paid"] or 0) if tr["premium_paid"] is not None else None
|
||
if paid is None:
|
||
continue
|
||
new_pnl = round(total_recv - paid, 8)
|
||
close_px = round(notional / sheets_sum, 8) if sheets_sum > 0 else None
|
||
old_recv = (
|
||
float(tr["premium_received"])
|
||
if tr["premium_received"] is not None
|
||
else None
|
||
)
|
||
old_pnl = float(tr["realized_pnl"]) if tr["realized_pnl"] is not None else None
|
||
# 已与合计一致则跳过
|
||
if (
|
||
old_recv is not None
|
||
and old_pnl is not None
|
||
and abs(old_recv - total_recv) < 1e-8
|
||
and abs(old_pnl - new_pnl) < 1e-8
|
||
):
|
||
continue
|
||
# 仅当卖笔数≥2 或合计收回明显高于本地(漏记)时覆盖
|
||
if len(selected) < 2 and old_recv is not None and total_recv <= old_recv + 1e-10:
|
||
continue
|
||
conn.execute(
|
||
"""
|
||
UPDATE options_trades
|
||
SET premium_received = ?,
|
||
realized_pnl = ?,
|
||
close_quote = COALESCE(?, close_quote),
|
||
close_ord_id = ?
|
||
WHERE id = ?
|
||
""",
|
||
(
|
||
total_recv,
|
||
new_pnl,
|
||
close_px,
|
||
",".join(oids) if oids else tr["close_ord_id"],
|
||
int(tr["id"]),
|
||
),
|
||
)
|
||
updated += 1
|
||
if updated:
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
return updated
|
||
|
||
def option_positions_history_okx_rows(
|
||
self,
|
||
exchange: Any = None,
|
||
*,
|
||
inst_id: str | None = None,
|
||
limit: int = 200,
|
||
) -> list[dict[str, Any]]:
|
||
"""模拟盘历史仓位:从本地已平 options_trades 合成 OKX positions-history 字段.
|
||
|
||
实盘 positions-history 在模拟模式不可用,期权历史/复盘依赖此合成数据.
|
||
"""
|
||
# 先按卖平成交修复分批漏计,再合成展示
|
||
try:
|
||
self.repair_closed_options_pnl_from_sim_orders(limit=max(int(limit), 200))
|
||
except Exception:
|
||
pass
|
||
|
||
from datetime import datetime
|
||
from zoneinfo import ZoneInfo
|
||
|
||
from lib.exchange.okx_options_lib import option_fields_from_inst_id
|
||
from lib.options.options_db import init_options_tables
|
||
from lib.options.options_margin_mode_lib import margin_mode_from_inst_id, premium_ccy_for_mode
|
||
|
||
tz = ZoneInfo(
|
||
(os.getenv("APP_TIMEZONE") or os.getenv("TZ") or "Asia/Shanghai").strip()
|
||
or "Asia/Shanghai"
|
||
)
|
||
|
||
def _to_ms(ts: Any) -> int | None:
|
||
if ts is None:
|
||
return None
|
||
raw = str(ts).strip()
|
||
if not raw:
|
||
return None
|
||
for fmt, ln in (
|
||
("%Y-%m-%d %H:%M:%S", 19),
|
||
("%Y-%m-%d %H:%M:%S.%f", 26),
|
||
("%Y-%m-%d %H:%M", 16),
|
||
):
|
||
try:
|
||
dt = datetime.strptime(raw[:ln], fmt).replace(tzinfo=tz)
|
||
return int(dt.timestamp() * 1000)
|
||
except ValueError:
|
||
continue
|
||
return None
|
||
|
||
conn = self.get_db()
|
||
try:
|
||
init_options_tables(conn)
|
||
try:
|
||
from lib.options.options_stats_lib import sync_sim_cash_realized_pnl_usdt
|
||
|
||
sync_sim_cash_realized_pnl_usdt(conn, exchange)
|
||
conn.commit()
|
||
except Exception:
|
||
pass
|
||
sql = """
|
||
SELECT id, inst_id, underlying, sheets, open_quote, close_quote,
|
||
premium_paid, premium_received, realized_pnl, realized_pnl_usdt,
|
||
premium_ccy, created_at, closed_at
|
||
FROM options_trades
|
||
WHERE status = 'closed'
|
||
"""
|
||
params: list[Any] = []
|
||
if inst_id:
|
||
sql += " AND inst_id = ?"
|
||
params.append(str(inst_id).strip())
|
||
sql += " ORDER BY id DESC LIMIT ?"
|
||
params.append(int(max(1, min(int(limit), 500))))
|
||
rows = conn.execute(sql, params).fetchall()
|
||
finally:
|
||
conn.close()
|
||
|
||
out: list[dict[str, Any]] = []
|
||
for r in rows:
|
||
iid = str(r["inst_id"] or "").strip()
|
||
if not iid:
|
||
continue
|
||
sheets = float(r["sheets"] or 0)
|
||
if sheets <= 0:
|
||
continue
|
||
open_px = float(r["open_quote"] or 0) or None
|
||
close_px = float(r["close_quote"] or 0) or None
|
||
paid = float(r["premium_paid"] or 0) if r["premium_paid"] is not None else None
|
||
recv = float(r["premium_received"] or 0) if r["premium_received"] is not None else None
|
||
pnl = float(r["realized_pnl"]) if r["realized_pnl"] is not None else None
|
||
pnl_usdt = float(r["realized_pnl_usdt"]) if r["realized_pnl_usdt"] is not None else None
|
||
if pnl is None and paid is not None and recv is not None:
|
||
pnl = round(recv - paid, 8)
|
||
if pnl is None:
|
||
pnl = 0.0
|
||
if paid is None and open_px is not None:
|
||
paid = round(open_px * sheets * 0.01, 8)
|
||
ccy = str(r["premium_ccy"] or "").strip().upper()
|
||
if not ccy:
|
||
u = str(r["underlying"] or iid.split("-")[0] or "ETH")
|
||
ccy = premium_ccy_for_mode(margin_mode_from_inst_id(iid), u)
|
||
if close_px is None and exchange is not None:
|
||
try:
|
||
bid, _ask, _ = _option_bid_ask(exchange, iid)
|
||
if bid and float(bid) > 0:
|
||
close_px = float(bid)
|
||
except Exception:
|
||
pass
|
||
open_ms = _to_ms(r["created_at"])
|
||
close_ms = _to_ms(r["closed_at"]) or open_ms
|
||
opt_type, strike = option_fields_from_inst_id(iid)
|
||
uly = str(r["underlying"] or iid.split("-")[0] or "").upper()
|
||
pnl_ratio = None
|
||
if paid is not None and abs(float(paid)) > 1e-12:
|
||
pnl_ratio = float(pnl) / float(paid)
|
||
out.append(
|
||
{
|
||
"instId": iid,
|
||
"uly": f"{uly}-USD" if margin_mode_from_inst_id(iid) == "coin" else f"{uly}-USD_UM",
|
||
"posId": f"sim-{int(r['id'])}",
|
||
"openAvgPx": str(open_px) if open_px is not None else "",
|
||
"closeAvgPx": str(close_px) if close_px is not None else "",
|
||
"closeTotalPos": str(sheets),
|
||
"openMaxPos": str(sheets),
|
||
# 期权历史展示币数量盈亏;U 口径只写 _sim_realized_pnl_usdt 供统计
|
||
"realizedPnl": str(round(pnl, 8)),
|
||
"pnl": str(round(pnl, 8)),
|
||
"pnlRatio": str(round(pnl_ratio, 8)) if pnl_ratio is not None else "",
|
||
"cTime": str(open_ms or ""),
|
||
"uTime": str(close_ms or ""),
|
||
"type": "2",
|
||
"optType": opt_type or "",
|
||
"stk": str(strike) if strike is not None else "",
|
||
"_sim_premium_paid": paid,
|
||
"_sim_premium_ccy": ccy,
|
||
"_sim_realized_pnl_usdt": pnl_usdt,
|
||
}
|
||
)
|
||
return out
|