fix模拟盘期权手续费按OKX币本位公式计费,并纠正复盘旧单展示。

费率作用在标的币数量上并以权利金7%封顶,不再误用权利金×费率。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-13 09:37:14 +08:00
parent bdc9eb532a
commit 78f18e731a
3 changed files with 249 additions and 9 deletions
+154 -4
View File
@@ -280,6 +280,148 @@ def hide_review_trade(conn: sqlite3.Connection, trade_id: int) -> dict[str, Any]
return {"ok": True, "entry": _row_to_dict(entry) if entry else None, "history_key": d.get("history_key")} return {"ok": True, "entry": _row_to_dict(entry) if entry else None, "history_key": d.get("history_key")}
def _sum_sim_order_fees(conn: sqlite3.Connection, ord_ids: list[str]) -> float | None:
"""从 sim_option_orders 汇总手续费(权利金币种,正数)."""
ids = [str(x).strip() for x in (ord_ids or []) if str(x).strip()]
if not ids:
return None
try:
conn.execute("SELECT 1 FROM sim_option_orders LIMIT 1").fetchone()
except Exception:
return None
total = 0.0
n = 0
for oid in ids:
try:
row = conn.execute(
"SELECT fee FROM sim_option_orders WHERE ord_id=?",
(oid,),
).fetchone()
except Exception:
continue
if not row:
continue
fee = _safe_float(row["fee"] if hasattr(row, "keys") else row[0])
if fee is None:
continue
total += abs(float(fee))
n += 1
return round(total, 8) if n else None
def _estimate_okx_option_fee_coin(
*,
underlying_coin: float | None,
premium_coin: float | None,
) -> float:
"""按 OKX 币本位公式估算期权手续费(权利金币种)."""
try:
from lib.sim.pricing_lib import option_fee_coin
return float(
option_fee_coin(
underlying_coin=float(underlying_coin or 0.0),
premium_coin=float(premium_coin or 0.0),
)
)
except Exception:
return 0.0
def _fee_looks_like_legacy_premium_rate(
fee_coin: float,
*,
premium_coin: float | None,
underlying_coin: float | None,
) -> bool:
"""旧模拟盘把手续费算成权利金×费率,约为正确值的 premium/underlying 倍."""
if fee_coin <= 0:
return True
prem = abs(float(premium_coin or 0.0))
uly = abs(float(underlying_coin or 0.0))
if prem <= 0 or uly <= 0:
return False
# 正确量级 ~ uly * fee_rate; 错误量级 ~ prem * fee_rate
# 若 fee << uly*1e-4 且接近 prem*5e-4,视为旧算法
try:
from lib.sim.pricing_lib import sim_fee_rate
fr = float(sim_fee_rate())
except Exception:
fr = 0.0005
expect_okx = uly * fr
expect_legacy = prem * fr
if expect_okx <= 0:
return False
# 比 OKX 估算小一个数量级以上,且靠近旧公式
if fee_coin < expect_okx * 0.25 and abs(fee_coin - expect_legacy) <= max(expect_legacy * 0.5, 1e-10):
return True
if fee_coin < expect_okx * 0.05:
return True
return False
def _resolve_option_fees_coin(
conn: sqlite3.Connection,
row: Any,
*,
open_fee: float,
close_fee: float,
premium_ccy: str,
) -> tuple[float, float]:
"""解析期权开/平手续费(权利金币种);修正模拟盘旧算法偏小值."""
ccy = (premium_ccy or "").strip().upper()
paid = _safe_float(row["premium_paid"] if hasattr(row, "keys") else None)
recv = _safe_float(row["premium_received"] if hasattr(row, "keys") else None)
uly = _safe_float(row["eth_amount"] if hasattr(row, "keys") else None)
if uly is None or uly <= 0:
sheets = _safe_float(row["sheets"] if hasattr(row, "keys") else None) or 0.0
# BTC 期权约 0.01,ETH 约 0.1
ct = 0.01 if ccy == "BTC" else 0.1
uly = float(sheets) * ct if sheets > 0 else None
open_oids = [
p.strip()
for p in str(row["exchange_ord_id"] if hasattr(row, "keys") else "").split(",")
if p.strip()
]
close_oids = [
p.strip()
for p in str(row["close_ord_id"] if hasattr(row, "keys") else "").split(",")
if p.strip()
]
ofee = abs(float(open_fee or 0.0))
cfee = abs(float(close_fee or 0.0))
# 优先用订单表(若订单本身仍是旧算法,下面会再纠正)
sim_open = _sum_sim_order_fees(conn, open_oids)
sim_close = _sum_sim_order_fees(conn, close_oids)
if sim_open is not None and sim_open > 0:
ofee = sim_open
if sim_close is not None and sim_close > 0:
cfee = sim_close
if ccy in ("ETH", "BTC"):
if _fee_looks_like_legacy_premium_rate(ofee, premium_coin=paid, underlying_coin=uly):
est = _estimate_okx_option_fee_coin(underlying_coin=uly, premium_coin=paid)
if est > 0:
ofee = est
if _fee_looks_like_legacy_premium_rate(cfee, premium_coin=recv, underlying_coin=uly):
est = _estimate_okx_option_fee_coin(underlying_coin=uly, premium_coin=recv)
if est > 0:
cfee = est
elif cfee <= 0 and recv is not None and recv > 0:
est = _estimate_okx_option_fee_coin(underlying_coin=uly, premium_coin=recv)
if est > 0:
cfee = est
if ofee <= 0 and paid is not None and paid > 0:
est = _estimate_okx_option_fee_coin(underlying_coin=uly, premium_coin=paid)
if est > 0:
ofee = est
return round(ofee, 8), round(cfee, 8)
def sync_options_from_local_trades( def sync_options_from_local_trades(
conn: sqlite3.Connection, conn: sqlite3.Connection,
@@ -317,11 +459,11 @@ def sync_options_from_local_trades(
rows = conn.execute( rows = conn.execute(
""" """
SELECT id, inst_id, underlying, opt_type, strike, exp_time, sheets, SELECT id, inst_id, underlying, opt_type, strike, exp_time, sheets,
open_quote, close_quote, premium_paid, premium_received, realized_pnl, eth_amount, open_quote, close_quote, premium_paid, premium_received,
realized_pnl_usdt, close_index_px, open_index_px, budget_usdt, realized_pnl, realized_pnl_usdt, close_index_px, open_index_px,
premium_ccy, created_at, closed_at, signal_note, status, budget_usdt, premium_ccy, created_at, closed_at, signal_note, status,
trading_usdt_before, pnl_settled, initial_premium_usdt, trading_usdt_before, pnl_settled, initial_premium_usdt,
open_fee, close_fee open_fee, close_fee, exchange_ord_id, close_ord_id
FROM options_trades FROM options_trades
WHERE status = 'closed' WHERE status = 'closed'
ORDER BY id DESC ORDER BY id DESC
@@ -445,6 +587,14 @@ def sync_options_from_local_trades(
# 仅期权开/平手续费折 U(不含现货) # 仅期权开/平手续费折 U(不含现货)
ofee = abs(_safe_float(r["open_fee"]) or 0.0) ofee = abs(_safe_float(r["open_fee"]) or 0.0)
cfee = abs(_safe_float(r["close_fee"]) or 0.0) cfee = abs(_safe_float(r["close_fee"]) or 0.0)
# 模拟盘旧单曾按「权利金×费率」计费偏小:按 OKX 公式/订单回填
ofee, cfee = _resolve_option_fees_coin(
conn,
r,
open_fee=ofee,
close_fee=cfee,
premium_ccy=ccy,
)
fee_u = None fee_u = None
if ccy in ("ETH", "BTC"): if ccy in ("ETH", "BTC"):
fee_parts = 0.0 fee_parts = 0.0
+43 -5
View File
@@ -1,4 +1,9 @@
"""成交价与手续费: 滑点 = 1×fee_rate.""" """成交价与手续费.
永续:滑点=1×fee_rate,手续费=名义×fee_rate.
期权(币本位):滑点仍按权利金价×fee_rate;手续费对齐 OKX=
Min(fee_rate×标的币数量, 权利金×7%).
"""
from __future__ import annotations from __future__ import annotations
@@ -57,6 +62,30 @@ def perp_fill(
return PriceResult(base_px=base, fill_px=fill, fee=fee, slip=slip, notional=notional) return PriceResult(base_px=base, fill_px=fill, fee=fee, slip=slip, notional=notional)
def option_fee_coin(
*,
underlying_coin: float,
premium_coin: float,
fee_rate: float | None = None,
premium_fee_cap: float = 0.07,
) -> float:
"""OKX 币本位期权手续费(权利金币种).
Min(费率 × 张数对应标的币数量, 权利金×封顶比例).
注意:费率作用在标的币面值上,不是权利金名义上.
"""
f = sim_fee_rate(fee_rate)
qty = abs(float(underlying_coin or 0.0))
prem = abs(float(premium_coin or 0.0))
if qty <= 0 and prem <= 0:
return 0.0
by_rate = qty * f
cap_ratio = abs(float(premium_fee_cap or 0.07))
if cap_ratio <= 0 or prem <= 0:
return round(by_rate, 8)
return round(min(by_rate, prem * cap_ratio), 8)
def option_fill( def option_fill(
*, *,
action: str, action: str,
@@ -65,7 +94,11 @@ def option_fill(
qty: float, qty: float,
fee_rate: float, fee_rate: float,
) -> PriceResult: ) -> PriceResult:
"""开仓买入吃卖一; 平仓卖出吃买一. qty = sheets × ct_mult.""" """开仓买入吃卖一; 平仓卖出吃买一.
qty = sheets × ct_mult(标的币数量).
滑点仍按权利金价×费率;手续费按 OKX 币本位公式(标的币×费率,权利金7%封顶).
"""
f = float(fee_rate) f = float(fee_rate)
if action == "open": if action == "open":
base = float(ask) base = float(ask)
@@ -73,9 +106,14 @@ def option_fill(
else: else:
base = float(bid) base = float(bid)
fill = base * (1.0 - f) fill = base * (1.0 - f)
notional = abs(fill * float(qty)) qty_f = abs(float(qty))
fee = notional * f notional = abs(fill * qty_f)
slip = abs(fill - base) * float(qty) fee = option_fee_coin(
underlying_coin=qty_f,
premium_coin=notional,
fee_rate=f,
)
slip = abs(fill - base) * qty_f
return PriceResult(base_px=base, fill_px=fill, fee=fee, slip=slip, notional=notional) return PriceResult(base_px=base, fill_px=fill, fee=fee, slip=slip, notional=notional)
+52
View File
@@ -0,0 +1,52 @@
"""模拟盘期权手续费:对齐 OKX 币本位公式."""
from __future__ import annotations
import unittest
from lib.sim.pricing_lib import option_fee_coin, option_fill
class OptionFeeCoinTests(unittest.TestCase):
def test_fee_on_underlying_not_premium(self):
# 90 张 BTC×0.01=0.9 币;权利金 0.01125
fee = option_fee_coin(
underlying_coin=0.9,
premium_coin=0.01125,
fee_rate=0.0005,
)
# min(0.9*0.0005, 0.01125*0.07) = min(0.00045, 0.0007875)
self.assertAlmostEqual(fee, 0.00045, places=8)
def test_premium_cap(self):
fee = option_fee_coin(
underlying_coin=1.0,
premium_coin=0.001,
fee_rate=0.0005,
)
# min(0.0005, 0.00007) = 0.00007
self.assertAlmostEqual(fee, 0.00007, places=8)
def test_option_fill_uses_okx_fee(self):
pr = option_fill(
action="open",
bid=0.012,
ask=0.0125,
qty=0.9,
fee_rate=0.0005,
)
self.assertGreater(pr.fee, 1e-6)
# 旧算法 fee≈notional*rate≈5.6e-6;新算法应为 4.5e-4 量级
self.assertGreater(pr.fee, 1e-4)
self.assertAlmostEqual(
pr.fee,
option_fee_coin(
underlying_coin=0.9,
premium_coin=pr.notional,
fee_rate=0.0005,
),
places=8,
)
if __name__ == "__main__":
unittest.main()