Files
crypto_monitor/tests/test_options_review_lib.py
dekun 72dddc5106 Fix options review screenshots missing after journal upload hijack.
Stop journal_upload_slots from binding options slots; resolve journal_* files from static/images root so existing reviews display again.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-19 16:23:37 +08:00

409 lines
15 KiB
Python

"""期权复盘(含对冲)单元测试:导入去重、双计防护、复盘不被覆盖、统计."""
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_hide_trade_persists_across_local_sync(self):
conn = _conn()
from lib.options.options_db import init_options_tables
from lib.options.options_review_lib import (
hide_review_trade,
sync_options_from_local_trades,
)
init_options_tables(conn)
conn.execute(
"""
INSERT INTO options_trades
(inst_id, underlying, opt_type, strike, sheets, eth_amount,
open_quote, premium_paid, status, realized_pnl, created_at, closed_at)
VALUES ('ETH-USD-1-C','ETH','C',2000,1,0.01,0.01,0.2,'closed',1.0,
'2026-03-01 10:00:00','2026-03-01 11:00:00')
"""
)
sync_options_from_local_trades(conn)
tid = conn.execute("SELECT id FROM options_review_trades").fetchone()["id"]
out = hide_review_trade(conn, tid)
self.assertTrue(out["ok"])
self.assertEqual(
conn.execute("SELECT COUNT(*) AS c FROM options_review_trades").fetchone()["c"],
0,
)
sync_options_from_local_trades(conn)
self.assertEqual(
conn.execute("SELECT COUNT(*) AS c FROM options_review_trades").fetchone()["c"],
0,
)
def test_local_options_trades_import(self):
conn = _conn()
from lib.options.options_db import init_options_tables
init_options_tables(conn)
conn.execute(
"""
INSERT INTO options_trades
(inst_id, underlying, opt_type, strike, sheets, eth_amount,
open_quote, premium_paid, status, realized_pnl, created_at, closed_at)
VALUES ('ETH-USD-260328-2000-C','ETH','C',2000,2,0.02,0.01,0.5,'closed',3.2,
'2026-03-01 10:00:00','2026-03-01 12:00:00')
"""
)
from lib.options.options_review_lib import sync_options_from_local_trades
out = sync_options_from_local_trades(conn)
self.assertTrue(out["ok"])
self.assertEqual(out["inserted"], 1)
row = conn.execute(
"SELECT history_key, realized_pnl_total, source_type FROM options_review_trades"
).fetchone()
self.assertTrue(str(row["history_key"]).startswith("local_opt:"))
self.assertEqual(float(row["realized_pnl_total"]), 3.2)
self.assertEqual(row["source_type"], SOURCE_OPTION)
def test_image_paths_resolve_legacy_journal_root(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
# 误存到 UPLOAD 根目录的 journal_*
legacy = root / "journal_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_5m.png"
legacy.write_bytes(b"img")
# 正常 options_journal 子目录
sub = root / "options_journal"
sub.mkdir()
modern = sub / "options_journal_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_5m.png"
modern.write_bytes(b"img2")
from lib.options.options_review_images_lib import options_review_image_paths
class Row:
images_json = (
'[{"tf":"5m","file":"journal_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_5m.png"},'
'{"tf":"5m","file":"options_journal_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb_5m.png"}]'
)
image = None
paths = options_review_image_paths(Row(), str(root))
self.assertEqual(len(paths), 2)
self.assertTrue(any(p.endswith(legacy.name) for p in paths))
self.assertTrue(any(p.endswith(modern.name) for p in paths))
def test_image_namespace(self):
with tempfile.TemporaryDirectory() as tmp:
folder = options_review_upload_dir(tmp)
fname = build_options_review_slot_filename(
"a" * 32, "5m", ".png", secure_filename_fn=lambda x: x
)
self.assertTrue(fname.startswith("options_journal_"))
self.assertTrue(is_valid_options_review_file(fname, "a" * 32, "5m"))
item = save_options_review_slot_file(
_FakeFile("x.png"),
"a" * 32,
"5m",
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)
def test_q_search_btcusdt_matches_btc_pending(self):
from lib.options.options_review_lib import count_review_trades
conn = _conn()
upsert_option_history_row(
conn,
{
"history_key": "ex:btc1",
"inst_id": "BTC-USD-260328-90000-C",
"underlying": "BTC",
"opt_type": "C",
"realized_pnl": 1.2,
"created_at": "2026-01-01 00:00:00",
"closed_at": "2026-01-01 02:00:00",
},
)
upsert_option_history_row(
conn,
{
"history_key": "ex:eth1",
"inst_id": "ETH-USD-260328-2000-C",
"underlying": "ETH",
"opt_type": "C",
"realized_pnl": 2.0,
"created_at": "2026-01-01 00:00:00",
"closed_at": "2026-01-01 03:00:00",
},
)
# 旧精确 strategy_tag 会把待复盘滤成空
self.assertEqual(
count_review_trades(conn, strategy_tag="BTCUSDT", reviewed="0"),
0,
)
self.assertEqual(
count_review_trades(conn, q="BTCUSDT", reviewed="0"),
1,
)
rows = list_review_trades(conn, q="BTCUSDT", reviewed="0")
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0]["underlying"], "BTC")
if __name__ == "__main__":
unittest.main()