实盘数据统计改用期权历史交易所盈亏;模拟盘仍走本地结算。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -288,6 +288,7 @@ def register_hub_bridge_routes(app: Flask, app_module: Any) -> None:
|
|||||||
from lib.options.options_margin_mode_lib import is_coin_margin_mode
|
from lib.options.options_margin_mode_lib import is_coin_margin_mode
|
||||||
from lib.options.options_positions_lib import sum_options_net_pnl_usdc
|
from lib.options.options_positions_lib import sum_options_net_pnl_usdc
|
||||||
from lib.options.options_stats_lib import (
|
from lib.options.options_stats_lib import (
|
||||||
|
apply_exchange_pnl_freeze,
|
||||||
compute_options_stats_for_window,
|
compute_options_stats_for_window,
|
||||||
history_pnl_to_usdt,
|
history_pnl_to_usdt,
|
||||||
load_local_options_stats_history,
|
load_local_options_stats_history,
|
||||||
@@ -302,11 +303,23 @@ def register_hub_bridge_routes(app: Flask, app_module: Any) -> None:
|
|||||||
raw_live = cfg["fetch_option_positions"](ex)
|
raw_live = cfg["fetch_option_positions"](ex)
|
||||||
if raw_live is None:
|
if raw_live is None:
|
||||||
return jsonify({"ok": False, "msg": "获取期权持仓失败", "version": 1})
|
return jsonify({"ok": False, "msg": "获取期权持仓失败", "version": 1})
|
||||||
# 与实例统计一致:本地已平(扣费+冻结 U),不跟实时指数漂移
|
# 模拟盘本地结算;实盘与期权历史同一交易所盈亏
|
||||||
conn = cfg["get_db"]()
|
conn = cfg["get_db"]()
|
||||||
try:
|
try:
|
||||||
init_options_tables(conn)
|
init_options_tables(conn)
|
||||||
history = load_local_options_stats_history(conn, ex)
|
use_sim = False
|
||||||
|
try:
|
||||||
|
from lib.sim.mode_lib import is_sim_mode
|
||||||
|
|
||||||
|
use_sim = is_sim_mode(cfg["get_db"])
|
||||||
|
except Exception:
|
||||||
|
use_sim = False
|
||||||
|
if use_sim:
|
||||||
|
history = load_local_options_stats_history(conn, ex)
|
||||||
|
else:
|
||||||
|
from lib.options.options_history_lib import load_options_history
|
||||||
|
|
||||||
|
history = apply_exchange_pnl_freeze(conn, load_options_history(ex, cfg), ex)
|
||||||
try:
|
try:
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -95,6 +95,19 @@ def init_options_tables(conn: sqlite3.Connection) -> None:
|
|||||||
ON options_target_monitors(status)
|
ON options_target_monitors(status)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
# 实盘统计:按期权历史 history_key 冻结交易所盈亏折 U,避免跟实时指数漂移
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS options_exchange_pnl_freeze (
|
||||||
|
history_key TEXT PRIMARY KEY,
|
||||||
|
realized_pnl REAL,
|
||||||
|
realized_pnl_usdt REAL,
|
||||||
|
close_index_px REAL,
|
||||||
|
premium_ccy TEXT,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
for ddl in (
|
for ddl in (
|
||||||
"ALTER TABLE options_trades ADD COLUMN wechat_open_sent INTEGER DEFAULT 0",
|
"ALTER TABLE options_trades ADD COLUMN wechat_open_sent INTEGER DEFAULT 0",
|
||||||
"ALTER TABLE options_trades ADD COLUMN wechat_close_sent INTEGER DEFAULT 0",
|
"ALTER TABLE options_trades ADD COLUMN wechat_close_sent INTEGER DEFAULT 0",
|
||||||
|
|||||||
@@ -188,13 +188,14 @@ def settle_closed_pnl(
|
|||||||
) -> dict[str, float | None]:
|
) -> dict[str, float | None]:
|
||||||
"""
|
"""
|
||||||
由「毛权利金」结算.
|
由「毛权利金」结算.
|
||||||
币数量净盈亏 = 收回 − 已付 − 开/平手续费.
|
币数量净盈亏 = 收回 − 已付 − 开/平手续费(只扣一次).
|
||||||
冻结 U(币本位):
|
冻结 U(币本位) = 净盈亏 × 平仓指数.
|
||||||
平仓收回×平仓指数 − 开仓成本U − 手续费U
|
|
||||||
开仓成本优先 budget_usdt,否则 已付×开仓指数(缺省才用平仓指数).
|
注意:不要用 budget_usdt 当开仓成本——它含现货买入缓冲,多余币会卖回,
|
||||||
这样不会把开仓成本也按平仓价重估,避免和余额对不上.
|
拿全额预算当成本会把亏损算大. open_index/budget 参数保留兼容,不参与 U 计算.
|
||||||
不要对 OKX realizedPnl 再调此函数扣费.
|
不要对 OKX realizedPnl 再调此函数扣费.
|
||||||
"""
|
"""
|
||||||
|
del open_index_px, budget_usdt, fee_ccy # 兼容旧调用,不参与结算
|
||||||
paid = float(_safe_float(premium_paid) or 0.0)
|
paid = float(_safe_float(premium_paid) or 0.0)
|
||||||
recv = float(_safe_float(premium_received) or 0.0)
|
recv = float(_safe_float(premium_received) or 0.0)
|
||||||
ofee = float(_safe_float(open_fee) or 0.0)
|
ofee = float(_safe_float(open_fee) or 0.0)
|
||||||
@@ -204,59 +205,13 @@ def settle_closed_pnl(
|
|||||||
if cfee < 0:
|
if cfee < 0:
|
||||||
cfee = abs(cfee)
|
cfee = abs(cfee)
|
||||||
net = round(recv - paid - ofee - cfee, 8)
|
net = round(recv - paid - ofee - cfee, 8)
|
||||||
pc = str(premium_ccy or "").strip().upper()
|
out = freeze_pnl_to_usdt(
|
||||||
close_idx = _safe_float(index_px)
|
net,
|
||||||
open_idx = _safe_float(open_index_px)
|
premium_ccy=premium_ccy,
|
||||||
budget = _safe_float(budget_usdt)
|
index_px=index_px,
|
||||||
fc = str(fee_ccy or pc or "").strip().upper()
|
total_fee=ofee + cfee,
|
||||||
|
|
||||||
if pc not in ("ETH", "BTC"):
|
|
||||||
return freeze_pnl_to_usdt(
|
|
||||||
net,
|
|
||||||
premium_ccy=premium_ccy,
|
|
||||||
index_px=close_idx,
|
|
||||||
total_fee=ofee + cfee,
|
|
||||||
)
|
|
||||||
|
|
||||||
if close_idx is None or close_idx <= 0:
|
|
||||||
return {
|
|
||||||
"realized_pnl": net,
|
|
||||||
"close_index_px": None,
|
|
||||||
"open_index_px": float(open_idx) if open_idx and open_idx > 0 else None,
|
|
||||||
"realized_pnl_usdt": None,
|
|
||||||
"total_fee": round(ofee + cfee, 8),
|
|
||||||
}
|
|
||||||
|
|
||||||
# 开仓成本 U:预算(真实花出去的 USDT) > 已付×开仓指数 > 已付×平仓指数
|
|
||||||
if budget is not None and budget > 0:
|
|
||||||
entry_u = float(budget)
|
|
||||||
if open_idx is None or open_idx <= 0:
|
|
||||||
open_idx = (entry_u / paid) if paid > 1e-12 else close_idx
|
|
||||||
elif open_idx is not None and open_idx > 0:
|
|
||||||
entry_u = paid * float(open_idx)
|
|
||||||
else:
|
|
||||||
entry_u = paid * float(close_idx)
|
|
||||||
open_idx = close_idx
|
|
||||||
|
|
||||||
exit_u = recv * float(close_idx)
|
|
||||||
# 开仓费按开仓指数、平仓费按平仓指数折 U
|
|
||||||
fee_u = fee_to_usdt(ofee, fc or pc, index_px=open_idx) + fee_to_usdt(
|
|
||||||
cfee, fc or pc, index_px=close_idx
|
|
||||||
)
|
)
|
||||||
# 若开仓成本已是 budget(含买币),ETH 开仓费已体现在币变少/卖回变少,不再重复扣 ETH 开仓费
|
return out
|
||||||
if budget is not None and budget > 0 and (fc or pc) in ("ETH", "BTC"):
|
|
||||||
fee_u = fee_to_usdt(cfee, fc or pc, index_px=close_idx)
|
|
||||||
if str(fee_ccy or "").strip().upper() in ("USDT", "USDC", "USD"):
|
|
||||||
fee_u += fee_to_usdt(ofee, fee_ccy, index_px=open_idx)
|
|
||||||
|
|
||||||
usdt = round(exit_u - entry_u - fee_u, 4)
|
|
||||||
return {
|
|
||||||
"realized_pnl": net,
|
|
||||||
"close_index_px": float(close_idx),
|
|
||||||
"open_index_px": float(open_idx) if open_idx and open_idx > 0 else None,
|
|
||||||
"realized_pnl_usdt": usdt,
|
|
||||||
"total_fee": round(ofee + cfee, 8),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def settle_exchange_realized_pnl(
|
def settle_exchange_realized_pnl(
|
||||||
|
|||||||
@@ -1280,13 +1280,13 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
return jsonify({"ok": False, "msg": err})
|
return jsonify({"ok": False, "msg": err})
|
||||||
from lib.options.options_margin_mode_lib import is_coin_margin_mode
|
from lib.options.options_margin_mode_lib import is_coin_margin_mode
|
||||||
from lib.options.options_positions_lib import sum_options_net_pnl_usdc
|
from lib.options.options_positions_lib import sum_options_net_pnl_usdc
|
||||||
from lib.options.options_stats_lib import compute_options_stats
|
from lib.options.options_stats_lib import compute_options_stats_auto
|
||||||
|
|
||||||
raw_live = cfg["fetch_option_positions"](ex)
|
raw_live = cfg["fetch_option_positions"](ex)
|
||||||
if raw_live is None:
|
if raw_live is None:
|
||||||
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
|
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
|
||||||
# 已平仓走本地库:扣手续费 + 平仓时冻结 U,不跟实时指数漂移
|
# 实盘:期权历史交易所原生盈亏;模拟盘:本地结算
|
||||||
stats = compute_options_stats(cfg["get_db"], ex)
|
stats = compute_options_stats_auto(cfg, ex)
|
||||||
open_float = sum_options_net_pnl_usdc(cfg, ex, raw_live)
|
open_float = sum_options_net_pnl_usdc(cfg, ex, raw_live)
|
||||||
net_realized = _safe_float(stats.get("net_realized_pnl")) or 0.0
|
net_realized = _safe_float(stats.get("net_realized_pnl")) or 0.0
|
||||||
total_pnl = None
|
total_pnl = None
|
||||||
|
|||||||
@@ -466,12 +466,8 @@ def repair_double_counted_fee_pnl(conn, ex: Any = None) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def load_local_options_stats_history(conn, ex: Any = None) -> list[dict[str, Any]]:
|
def load_local_options_stats_history(conn, ex: Any = None) -> list[dict[str, Any]]:
|
||||||
"""本地已平(含冻结 U/手续费净额) + 当前 open,供统计/中控窗口复用."""
|
"""本地已平(模拟盘口径) + 当前 open."""
|
||||||
init_options_tables(conn)
|
init_options_tables(conn)
|
||||||
try:
|
|
||||||
recompute_closed_realized_pnl_usdt(conn, ex)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
try:
|
||||||
freeze_missing_realized_pnl_usdt(conn, ex)
|
freeze_missing_realized_pnl_usdt(conn, ex)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -486,7 +482,86 @@ def load_local_options_stats_history(conn, ex: Any = None) -> list[dict[str, Any
|
|||||||
return history
|
return history
|
||||||
|
|
||||||
|
|
||||||
|
def apply_exchange_pnl_freeze(
|
||||||
|
conn,
|
||||||
|
history: list[dict[str, Any]],
|
||||||
|
ex: Any = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""实盘:以期权历史里的交易所 realized_pnl 为准,首次折 U 后按 history_key 冻结."""
|
||||||
|
from lib.options.options_pnl_settle_lib import settle_exchange_realized_pnl
|
||||||
|
from lib.options.options_time_lib import options_app_now_str
|
||||||
|
|
||||||
|
init_options_tables(conn)
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
for row in history or []:
|
||||||
|
r = dict(row)
|
||||||
|
if r.get("status") == "open":
|
||||||
|
out.append(r)
|
||||||
|
continue
|
||||||
|
key = str(r.get("history_key") or "").strip()
|
||||||
|
pnl = _safe_float(r.get("realized_pnl"))
|
||||||
|
if not key or pnl is None:
|
||||||
|
out.append(r)
|
||||||
|
continue
|
||||||
|
frozen = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT realized_pnl_usdt, close_index_px, realized_pnl
|
||||||
|
FROM options_exchange_pnl_freeze WHERE history_key = ?
|
||||||
|
""",
|
||||||
|
(key,),
|
||||||
|
).fetchone()
|
||||||
|
if frozen is not None and _safe_float(_row_get(frozen, "realized_pnl_usdt")) is not None:
|
||||||
|
r["realized_pnl_usdt"] = _row_get(frozen, "realized_pnl_usdt")
|
||||||
|
r["close_index_px"] = _row_get(frozen, "close_index_px")
|
||||||
|
# 币数量仍以当前交易所行为准(防历史修订);U 用冻结值
|
||||||
|
out.append(r)
|
||||||
|
continue
|
||||||
|
ccy = str(r.get("premium_ccy") or "").strip().upper()
|
||||||
|
underly = str(r.get("underlying") or "").strip()
|
||||||
|
inst = str(r.get("inst_id") or "")
|
||||||
|
if not underly and inst:
|
||||||
|
underly = inst.split("-")[0]
|
||||||
|
from lib.options.options_pnl_settle_lib import resolve_close_index_px
|
||||||
|
|
||||||
|
idx = resolve_close_index_px(ex, inst_id=inst, underlying=underly)
|
||||||
|
settled = settle_exchange_realized_pnl(
|
||||||
|
realized_pnl=pnl,
|
||||||
|
premium_ccy=ccy or "ETH",
|
||||||
|
index_px=idx,
|
||||||
|
)
|
||||||
|
usdt = settled.get("realized_pnl_usdt")
|
||||||
|
close_idx = settled.get("close_index_px")
|
||||||
|
if usdt is not None:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO options_exchange_pnl_freeze(
|
||||||
|
history_key, realized_pnl, realized_pnl_usdt, close_index_px,
|
||||||
|
premium_ccy, updated_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(history_key) DO UPDATE SET
|
||||||
|
realized_pnl = excluded.realized_pnl,
|
||||||
|
realized_pnl_usdt = excluded.realized_pnl_usdt,
|
||||||
|
close_index_px = excluded.close_index_px,
|
||||||
|
premium_ccy = excluded.premium_ccy,
|
||||||
|
updated_at = excluded.updated_at
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
key,
|
||||||
|
float(pnl),
|
||||||
|
float(usdt),
|
||||||
|
close_idx,
|
||||||
|
ccy or "",
|
||||||
|
options_app_now_str(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
r["realized_pnl_usdt"] = float(usdt)
|
||||||
|
r["close_index_px"] = close_idx
|
||||||
|
out.append(r)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def compute_options_stats(get_db, ex: Any = None) -> dict[str, Any]:
|
def compute_options_stats(get_db, ex: Any = None) -> dict[str, Any]:
|
||||||
|
"""模拟盘:本地交易结算.实盘请用 compute_options_stats_live_exchange."""
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
try:
|
try:
|
||||||
history = load_local_options_stats_history(conn, ex)
|
history = load_local_options_stats_history(conn, ex)
|
||||||
@@ -494,12 +569,43 @@ def compute_options_stats(get_db, ex: Any = None) -> dict[str, Any]:
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# 优先冻结 U;缺省再按指数折(与 /api/options/stats 一致)
|
|
||||||
return compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
|
return compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def compute_options_stats_live_exchange(cfg: dict[str, Any], ex: Any) -> dict[str, Any]:
|
||||||
|
"""实盘:与「期权历史」同一数据源(交易所 positions-history),盈亏以交易所为准."""
|
||||||
|
from lib.options.options_history_lib import load_options_history
|
||||||
|
|
||||||
|
history = load_options_history(ex, cfg)
|
||||||
|
conn = cfg["get_db"]()
|
||||||
|
try:
|
||||||
|
history = apply_exchange_pnl_freeze(conn, history, ex)
|
||||||
|
try:
|
||||||
|
conn.commit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
return compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
|
||||||
|
|
||||||
|
|
||||||
|
def compute_options_stats_auto(cfg: dict[str, Any], ex: Any = None) -> dict[str, Any]:
|
||||||
|
"""模拟盘走本地结算;实盘走交易所期权历史原生盈亏."""
|
||||||
|
get_db = cfg.get("get_db")
|
||||||
|
try:
|
||||||
|
from lib.sim.mode_lib import is_sim_mode
|
||||||
|
|
||||||
|
if callable(get_db) and is_sim_mode(get_db):
|
||||||
|
return compute_options_stats(get_db, ex)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if ex is None:
|
||||||
|
ex = cfg.get("exchange_options")
|
||||||
|
return compute_options_stats_live_exchange(cfg, ex)
|
||||||
|
|
||||||
|
|
||||||
def _stats_from_option_trade_rows(closed_rows, open_rows, *, ex: Any = None) -> dict[str, Any]:
|
def _stats_from_option_trade_rows(closed_rows, open_rows, *, ex: Any = None) -> dict[str, Any]:
|
||||||
"""兼容旧调用;内部统一折 U 后走 compute_options_stats_from_history."""
|
"""兼容旧调用;内部统一折 U 后走 compute_options_stats_from_history."""
|
||||||
history = _closed_trade_dicts(closed_rows)
|
history = _closed_trade_dicts(closed_rows)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""期权已实现盈亏:扣手续费 + 按开仓成本冻结 U(不跟平仓价重估开仓成本)."""
|
"""期权盈亏结算:模拟盘本地扣费;实盘以交易所历史为准并冻结折 U."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
@@ -9,9 +9,9 @@ from lib.options.options_pnl_settle_lib import (
|
|||||||
settle_exchange_realized_pnl,
|
settle_exchange_realized_pnl,
|
||||||
)
|
)
|
||||||
from lib.options.options_stats_lib import (
|
from lib.options.options_stats_lib import (
|
||||||
|
apply_exchange_pnl_freeze,
|
||||||
compute_options_stats_from_history,
|
compute_options_stats_from_history,
|
||||||
history_pnl_to_usdt,
|
history_pnl_to_usdt,
|
||||||
recompute_closed_realized_pnl_usdt,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -23,17 +23,9 @@ class FeeConvertTests(unittest.TestCase):
|
|||||||
places=8,
|
places=8,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_fee_usdt_to_eth(self):
|
|
||||||
self.assertAlmostEqual(
|
|
||||||
fee_to_premium_ccy(3.0, "USDT", premium_ccy="ETH", index_px=3000),
|
|
||||||
0.001,
|
|
||||||
places=8,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SettleClosedPnlTests(unittest.TestCase):
|
class SettleClosedPnlTests(unittest.TestCase):
|
||||||
def test_same_index_matches_net_times_index(self):
|
def test_sim_net_times_close_index(self):
|
||||||
# 开平指数相同:(recv-paid-fees)*idx
|
|
||||||
s = settle_closed_pnl(
|
s = settle_closed_pnl(
|
||||||
premium_paid=0.01,
|
premium_paid=0.01,
|
||||||
premium_received=0.02,
|
premium_received=0.02,
|
||||||
@@ -41,40 +33,22 @@ class SettleClosedPnlTests(unittest.TestCase):
|
|||||||
close_fee=0.0005,
|
close_fee=0.0005,
|
||||||
premium_ccy="ETH",
|
premium_ccy="ETH",
|
||||||
index_px=3000,
|
index_px=3000,
|
||||||
open_index_px=3000,
|
|
||||||
)
|
)
|
||||||
self.assertAlmostEqual(float(s["realized_pnl"]), 0.009, places=8)
|
self.assertAlmostEqual(float(s["realized_pnl"]), 0.009, places=8)
|
||||||
self.assertAlmostEqual(float(s["realized_pnl_usdt"]), 27.0, places=4)
|
self.assertAlmostEqual(float(s["realized_pnl_usdt"]), 27.0, places=4)
|
||||||
|
|
||||||
def test_open_cost_not_revalued_at_close(self):
|
def test_budget_ignored_for_usdt(self):
|
||||||
# 开仓 2500、平仓 3000:不得把已付也按 3000 计
|
# budget 不得抬高开仓成本
|
||||||
# exit=0.02*3000=60, entry=0.01*2500=25, fee=0.0005*2500+0.0005*3000=2.75
|
|
||||||
# usdt=60-25-2.75=32.25 (旧口径 0.009*3000=27 偏亏)
|
|
||||||
s = settle_closed_pnl(
|
s = settle_closed_pnl(
|
||||||
premium_paid=0.01,
|
premium_paid=0.01,
|
||||||
premium_received=0.02,
|
premium_received=0.02,
|
||||||
open_fee=0.0005,
|
open_fee=0,
|
||||||
close_fee=0.0005,
|
close_fee=0,
|
||||||
premium_ccy="ETH",
|
|
||||||
index_px=3000,
|
|
||||||
open_index_px=2500,
|
|
||||||
)
|
|
||||||
self.assertAlmostEqual(float(s["realized_pnl_usdt"]), 32.25, places=4)
|
|
||||||
|
|
||||||
def test_budget_usdt_as_entry(self):
|
|
||||||
# budget=26 作为真实开仓 USDT 成本;ETH 开仓费不再重复扣
|
|
||||||
s = settle_closed_pnl(
|
|
||||||
premium_paid=0.01,
|
|
||||||
premium_received=0.02,
|
|
||||||
open_fee=0.0005,
|
|
||||||
close_fee=0.0005,
|
|
||||||
premium_ccy="ETH",
|
premium_ccy="ETH",
|
||||||
index_px=3000,
|
index_px=3000,
|
||||||
budget_usdt=26.0,
|
budget_usdt=26.0,
|
||||||
fee_ccy="ETH",
|
|
||||||
)
|
)
|
||||||
# exit=60, entry=26, close_fee_u=1.5 → 32.5
|
self.assertAlmostEqual(float(s["realized_pnl_usdt"]), 30.0, places=4)
|
||||||
self.assertAlmostEqual(float(s["realized_pnl_usdt"]), 32.5, places=4)
|
|
||||||
|
|
||||||
def test_usdc_freeze_is_net(self):
|
def test_usdc_freeze_is_net(self):
|
||||||
s = settle_closed_pnl(
|
s = settle_closed_pnl(
|
||||||
@@ -99,7 +73,65 @@ class SettleClosedPnlTests(unittest.TestCase):
|
|||||||
self.assertAlmostEqual(float(s["realized_pnl_usdt"]), 27.0, places=4)
|
self.assertAlmostEqual(float(s["realized_pnl_usdt"]), 27.0, places=4)
|
||||||
|
|
||||||
|
|
||||||
class FrozenStatsTests(unittest.TestCase):
|
class ExchangeFreezeStatsTests(unittest.TestCase):
|
||||||
|
def test_freeze_uses_exchange_coin_pnl(self):
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
from lib.options.options_db import init_options_tables
|
||||||
|
|
||||||
|
conn = sqlite3.connect(":memory:")
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
init_options_tables(conn)
|
||||||
|
|
||||||
|
class _Ex:
|
||||||
|
def public_get_market_ticker(self, params):
|
||||||
|
inst = (params or {}).get("instId") or ""
|
||||||
|
if inst.startswith("BTC"):
|
||||||
|
return {"data": [{"last": "100000"}]}
|
||||||
|
return {"data": [{"last": "2500"}]}
|
||||||
|
|
||||||
|
history = [
|
||||||
|
{
|
||||||
|
"status": "closed",
|
||||||
|
"source": "exchange",
|
||||||
|
"history_key": "ex|BTC-USD-261009-84000-P|1",
|
||||||
|
"inst_id": "BTC-USD-261009-84000-P",
|
||||||
|
"underlying": "BTC",
|
||||||
|
"premium_ccy": "BTC",
|
||||||
|
"realized_pnl": 0.000138,
|
||||||
|
"created_at": "2026-10-08 10:00:00",
|
||||||
|
"closed_at": "2026-10-08 22:22:56",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "closed",
|
||||||
|
"source": "exchange",
|
||||||
|
"history_key": "ex|ETH-USD-261006-2700-P|2",
|
||||||
|
"inst_id": "ETH-USD-261006-2700-P",
|
||||||
|
"underlying": "ETH",
|
||||||
|
"premium_ccy": "ETH",
|
||||||
|
"realized_pnl": -0.00648,
|
||||||
|
"created_at": "2026-10-05 10:00:00",
|
||||||
|
"closed_at": "2026-10-06 16:00:27",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
frozen = apply_exchange_pnl_freeze(conn, history, _Ex())
|
||||||
|
converted = history_pnl_to_usdt(frozen, _Ex())
|
||||||
|
# 0.000138*100000=13.8; -0.00648*2500=-16.2; net=-2.4
|
||||||
|
self.assertAlmostEqual(float(converted[0]["realized_pnl"]), 13.8, places=4)
|
||||||
|
self.assertAlmostEqual(float(converted[1]["realized_pnl"]), -16.2, places=4)
|
||||||
|
stats = compute_options_stats_from_history(converted)
|
||||||
|
self.assertAlmostEqual(float(stats["net_realized_pnl"]), -2.4, places=4)
|
||||||
|
|
||||||
|
# 指数再变也不改冻结 U
|
||||||
|
class _Ex2:
|
||||||
|
def public_get_market_ticker(self, params):
|
||||||
|
return {"data": [{"last": "999999"}]}
|
||||||
|
|
||||||
|
frozen2 = apply_exchange_pnl_freeze(conn, history, _Ex2())
|
||||||
|
converted2 = history_pnl_to_usdt(frozen2, _Ex2())
|
||||||
|
self.assertAlmostEqual(float(converted2[0]["realized_pnl"]), 13.8, places=4)
|
||||||
|
conn.close()
|
||||||
|
|
||||||
def test_frozen_usdt_ignores_live_index(self):
|
def test_frozen_usdt_ignores_live_index(self):
|
||||||
history = [
|
history = [
|
||||||
{
|
{
|
||||||
@@ -121,42 +153,6 @@ class FrozenStatsTests(unittest.TestCase):
|
|||||||
|
|
||||||
converted = history_pnl_to_usdt(history, _Ex())
|
converted = history_pnl_to_usdt(history, _Ex())
|
||||||
self.assertAlmostEqual(float(converted[0]["realized_pnl"]), 25.0, places=4)
|
self.assertAlmostEqual(float(converted[0]["realized_pnl"]), 25.0, places=4)
|
||||||
stats = compute_options_stats_from_history(converted)
|
|
||||||
self.assertAlmostEqual(float(stats["net_realized_pnl"]), 25.0, places=4)
|
|
||||||
|
|
||||||
|
|
||||||
class RecomputeClosedUsdtTests(unittest.TestCase):
|
|
||||||
def test_recompute_uses_budget_not_close_revalue(self):
|
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
from lib.options.options_db import init_options_tables
|
|
||||||
|
|
||||||
conn = sqlite3.connect(":memory:")
|
|
||||||
conn.row_factory = sqlite3.Row
|
|
||||||
init_options_tables(conn)
|
|
||||||
# 旧错误冻结: (0.02-0.01)*3000=30
|
|
||||||
conn.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO options_trades(
|
|
||||||
inst_id, underlying, opt_type, sheets, eth_amount,
|
|
||||||
premium_paid, premium_received, realized_pnl,
|
|
||||||
premium_ccy, status, budget_usdt, close_index_px, realized_pnl_usdt
|
|
||||||
) VALUES (
|
|
||||||
'ETH-USD-260701-2500-C', 'ETH', 'C', 1, 0.01,
|
|
||||||
0.01, 0.02, 0.01,
|
|
||||||
'ETH', 'closed', 26.0, 3000, 30.0
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
n = recompute_closed_realized_pnl_usdt(conn, None)
|
|
||||||
self.assertEqual(n, 1)
|
|
||||||
row = conn.execute(
|
|
||||||
"SELECT realized_pnl_usdt FROM options_trades WHERE id=1"
|
|
||||||
).fetchone()
|
|
||||||
# exit 60 - budget 26 = 34 (无手续费列)
|
|
||||||
self.assertAlmostEqual(float(row["realized_pnl_usdt"]), 34.0, places=4)
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user