fix模拟盘期权手续费按OKX币本位公式计费,并纠正复盘旧单展示。
费率作用在标的币数量上并以权利金7%封顶,不再误用权利金×费率。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user