Add OKX options review module with hedge plan entries.

Import closed OKX option history and closed hedge plans into one list for journaling, images, and stats without mixing contract reviews.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-17 09:35:19 +08:00
parent eb5c6a658b
commit 10128d18bc
17 changed files with 2133 additions and 3 deletions
+284
View File
@@ -0,0 +1,284 @@
"""期权复盘(含对冲)单元测试:导入去重、双计防护、复盘不被覆盖、统计."""
from __future__ import annotations
import sqlite3
import tempfile
import unittest
from pathlib import Path
from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables, insert_leg, insert_plan
from lib.options.options_review_db import SOURCE_OPTION, SOURCE_PERP_OPTIONS, init_options_review_tables
from lib.options.options_review_images_lib import (
build_options_review_slot_filename,
is_valid_options_review_file,
options_review_upload_dir,
save_options_review_slot_file,
)
from lib.options.options_review_lib import (
compute_review_stats,
list_review_trades,
save_review_entry,
sync_hedge_plans_closed,
sync_options_from_exchange,
upsert_option_history_row,
)
def _conn() -> sqlite3.Connection:
c = sqlite3.connect(":memory:")
c.row_factory = sqlite3.Row
init_options_review_tables(c)
init_hedge_plan_tables(c)
return c
class _FakeFile:
def __init__(self, name: str, data: bytes = b"img"):
self.filename = name
self._data = data
def save(self, path: str) -> None:
Path(path).write_bytes(self._data)
class OptionsReviewTests(unittest.TestCase):
def test_option_upsert_idempotent(self):
conn = _conn()
row = {
"history_key": "ex:pos1",
"pos_id": "pos1",
"inst_id": "ETH-USD-260328-2000-C",
"underlying": "ETH",
"opt_type": "C",
"strike": 2000,
"sheets": 10,
"open_avg_px": 0.01,
"close_avg_px": 0.02,
"premium_paid": 1.0,
"realized_pnl": 5.5,
"created_at": "2026-03-01 10:00:00",
"closed_at": "2026-03-01 12:00:00",
"status_label": "已平",
}
self.assertEqual(upsert_option_history_row(conn, row), "inserted")
row["realized_pnl"] = 6.0
self.assertEqual(upsert_option_history_row(conn, row), "updated")
n = conn.execute("SELECT COUNT(*) AS c FROM options_review_trades").fetchone()["c"]
self.assertEqual(n, 1)
pnl = conn.execute(
"SELECT realized_pnl_total FROM options_review_trades WHERE history_key='ex:pos1'"
).fetchone()["realized_pnl_total"]
self.assertEqual(float(pnl), 6.0)
def test_entry_not_overwritten_by_resync(self):
conn = _conn()
upsert_option_history_row(
conn,
{
"history_key": "ex:p2",
"inst_id": "ETH-USD-260328-1800-P",
"underlying": "ETH",
"opt_type": "P",
"realized_pnl": 1.0,
"created_at": "2026-03-02 10:00:00",
"closed_at": "2026-03-02 11:00:00",
},
)
tid = conn.execute("SELECT id FROM options_review_trades").fetchone()["id"]
save_review_entry(
conn,
tid,
{"strategy_tag": "突破追涨", "note": "keep-me", "images": []},
)
upsert_option_history_row(
conn,
{
"history_key": "ex:p2",
"inst_id": "ETH-USD-260328-1800-P",
"underlying": "ETH",
"opt_type": "P",
"realized_pnl": 2.0,
"created_at": "2026-03-02 10:00:00",
"closed_at": "2026-03-02 11:00:00",
},
)
note = conn.execute(
"SELECT note, strategy_tag FROM options_review_entries WHERE trade_id=?",
(tid,),
).fetchone()
self.assertEqual(note["note"], "keep-me")
self.assertEqual(note["strategy_tag"], "突破追涨")
pnl = conn.execute(
"SELECT realized_pnl_total FROM options_review_trades WHERE id=?", (tid,)
).fetchone()["realized_pnl_total"]
self.assertEqual(float(pnl), 2.0)
def test_hedge_import_and_double_count_guard(self):
conn = _conn()
upsert_option_history_row(
conn,
{
"history_key": "ex:leg1",
"inst_id": "ETH-USD-260328-2000-C",
"underlying": "ETH",
"opt_type": "C",
"realized_pnl": -3.0,
"created_at": "2026-03-03 09:00:00",
"closed_at": "2026-03-03 18:00:00",
},
)
plan_id = insert_plan(
conn,
{
"plan_type": SOURCE_PERP_OPTIONS,
"status": "closed",
"underlying": "ETH",
"direction": "long",
"realized_pnl_perp": 20.0,
"realized_pnl_options": -3.0,
"realized_pnl_total": 17.0,
"close_reason": "tp",
"opened_at": "2026-03-03 09:00:00",
"closed_at": "2026-03-03 18:00:00",
"premium_total": 3.0,
},
)
insert_leg(
conn,
{
"plan_id": plan_id,
"leg_role": "perp",
"symbol": "ETH-USDT-SWAP",
"status": "closed",
"realized_pnl": 20.0,
},
)
insert_leg(
conn,
{
"plan_id": plan_id,
"leg_role": "option_hedge",
"inst_id": "ETH-USD-260328-2000-C",
"opt_type": "C",
"status": "closed",
"realized_pnl": -3.0,
},
)
out = sync_hedge_plans_closed(conn)
self.assertTrue(out["ok"])
self.assertEqual(out["inserted"], 1)
listed = list_review_trades(conn, include_hedge_legs=False)
types = {r["source_type"] for r in listed}
self.assertIn(SOURCE_PERP_OPTIONS, types)
self.assertNotIn(SOURCE_OPTION, types)
listed_all = list_review_trades(conn, include_hedge_legs=True)
self.assertEqual(len(listed_all), 2)
stats = compute_review_stats(conn, include_hedge_legs=False)
self.assertEqual(stats["kpi"]["total"], 1)
self.assertEqual(stats["kpi"]["pnl_sum"], 17.0)
def test_sync_options_from_mock_exchange(self):
conn = _conn()
def fetch(_ex, limit=500):
return [
{
"instId": "ETH-USD-260328-2100-C",
"posId": "mock1",
"openAvgPx": "0.01",
"closeAvgPx": "0.02",
"closeTotalPos": "5",
"realizedPnl": "1.23",
"type": "2",
"cTime": "1700000000000",
"uTime": "1700003600000",
"uly": "ETH-USD",
}
]
def fmt(raw, tick_sz=None, ct_mult=0.01):
return {
"history_key": f"ex:{raw['posId']}",
"pos_id": raw["posId"],
"inst_id": raw["instId"],
"underlying": "ETH",
"opt_type": "C",
"sheets": 5,
"open_avg_px": 0.01,
"close_avg_px": 0.02,
"premium_paid": 0.5,
"realized_pnl": float(raw["realizedPnl"]),
"created_at": "2026-01-01 00:00:00",
"closed_at": "2026-01-01 01:00:00",
"status_label": "已平",
}
result = sync_options_from_exchange(
conn, object(), limit=10, fetch_fn=fetch, format_fn=fmt
)
self.assertTrue(result["ok"])
self.assertEqual(result["inserted"], 1)
row = conn.execute(
"SELECT realized_pnl_total FROM options_review_trades WHERE history_key='ex:mock1'"
).fetchone()
self.assertEqual(float(row["realized_pnl_total"]), 1.23)
def test_image_namespace(self):
with tempfile.TemporaryDirectory() as tmp:
folder = options_review_upload_dir(tmp)
fname = build_options_review_slot_filename(
"a" * 32, "chart", ".png", secure_filename_fn=lambda x: x
)
self.assertTrue(fname.startswith("options_journal_"))
self.assertTrue(is_valid_options_review_file(fname, "a" * 32, "chart"))
item = save_options_review_slot_file(
_FakeFile("x.png"),
"a" * 32,
"chart",
folder,
secure_filename_fn=lambda x: x,
)
self.assertIsNotNone(item)
self.assertTrue((Path(folder) / item["file"]).is_file())
def test_strategy_stats_only_tagged(self):
conn = _conn()
upsert_option_history_row(
conn,
{
"history_key": "ex:a",
"inst_id": "ETH-USD-1-C",
"underlying": "ETH",
"opt_type": "C",
"realized_pnl": 10,
"created_at": "2026-01-01 00:00:00",
"closed_at": "2026-01-01 02:00:00",
},
)
upsert_option_history_row(
conn,
{
"history_key": "ex:b",
"inst_id": "ETH-USD-2-P",
"underlying": "ETH",
"opt_type": "P",
"realized_pnl": -4,
"created_at": "2026-01-01 00:00:00",
"closed_at": "2026-01-01 05:00:00",
},
)
tid = conn.execute(
"SELECT id FROM options_review_trades WHERE history_key='ex:a'"
).fetchone()["id"]
save_review_entry(conn, tid, {"strategy_tag": "假破", "images": []})
stats = compute_review_stats(conn)
self.assertEqual(len(stats["by_strategy"]), 1)
self.assertEqual(stats["by_strategy"][0]["key"], "假破")
self.assertEqual(stats["kpi"]["total"], 2)
if __name__ == "__main__":
unittest.main()