移植期权复盘自动K线、时区修复与保存后轮询展示,与今日 crypto_monitor 改动对齐。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
"""期权复盘自动 K 线:单元测试."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from lib.options.options_review_chart_async_lib import (
|
||||
OPTIONS_REVIEW_AUTO_CHART_TF,
|
||||
build_options_review_chart_filename,
|
||||
merge_auto_chart_into_images,
|
||||
schedule_options_review_exchange_chart,
|
||||
underlying_to_okx_perp_symbol,
|
||||
)
|
||||
from lib.options.options_review_db import init_options_review_tables
|
||||
from lib.options.options_review_lib import save_review_entry, upsert_option_history_row
|
||||
|
||||
|
||||
def _conn() -> sqlite3.Connection:
|
||||
c = sqlite3.connect(":memory:")
|
||||
c.row_factory = sqlite3.Row
|
||||
init_options_review_tables(c)
|
||||
return c
|
||||
|
||||
|
||||
class OptionsReviewChartAsyncTests(unittest.TestCase):
|
||||
def test_underlying_to_okx_perp_symbol(self):
|
||||
self.assertEqual(
|
||||
underlying_to_okx_perp_symbol(
|
||||
"ETH-USD",
|
||||
normalize_symbol_input_fn=lambda s: f"{s}/USDT",
|
||||
normalize_okx_symbol_fn=lambda s: f"{s.split('/')[0]}/USDT:USDT"
|
||||
if "/" in s
|
||||
else s,
|
||||
),
|
||||
"ETH/USDT:USDT",
|
||||
)
|
||||
|
||||
def test_merge_auto_chart_keeps_manual_slots(self):
|
||||
merged = merge_auto_chart_into_images(
|
||||
[
|
||||
{"tf": "auto", "file": "old.png"},
|
||||
{"tf": "15m", "file": "m15.png"},
|
||||
],
|
||||
"new_auto.png",
|
||||
)
|
||||
self.assertEqual(merged[0]["tf"], OPTIONS_REVIEW_AUTO_CHART_TF)
|
||||
self.assertEqual(merged[0]["file"], "new_auto.png")
|
||||
self.assertEqual(merged[1], {"tf": "15m", "file": "m15.png"})
|
||||
|
||||
def test_chart_filename(self):
|
||||
self.assertEqual(
|
||||
build_options_review_chart_filename(12), "options_journal_chart_12.png"
|
||||
)
|
||||
|
||||
@patch("lib.options.options_review_chart_async_lib.threading.Thread")
|
||||
def test_schedule_starts_daemon_thread(self, mock_thread_cls):
|
||||
thread = MagicMock()
|
||||
mock_thread_cls.return_value = thread
|
||||
schedule_options_review_exchange_chart(
|
||||
trade_id=7,
|
||||
exchange_symbol="ETH/USDT:USDT",
|
||||
title_prefix="t",
|
||||
journal_tfs=["15m", "1h"],
|
||||
journal_limit=100,
|
||||
marker_payload={},
|
||||
upload_folder="/tmp",
|
||||
generate_chart_fn=MagicMock(),
|
||||
get_db_fn=MagicMock(),
|
||||
now_str_fn=lambda: "2026-09-15 12:00:00",
|
||||
)
|
||||
mock_thread_cls.assert_called_once()
|
||||
self.assertTrue(mock_thread_cls.call_args.kwargs.get("daemon"))
|
||||
thread.start.assert_called_once()
|
||||
|
||||
def test_writeback_merges_auto_image(self):
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from lib.options.options_review_images_lib import parse_options_review_images_json
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
db_path = Path(td) / "t.db"
|
||||
|
||||
def get_db():
|
||||
c = sqlite3.connect(str(db_path))
|
||||
c.row_factory = sqlite3.Row
|
||||
return c
|
||||
|
||||
conn = get_db()
|
||||
init_options_review_tables(conn)
|
||||
upsert_option_history_row(
|
||||
conn,
|
||||
{
|
||||
"history_key": "ex:chart1",
|
||||
"inst_id": "ETH-USD-260328-2000-C",
|
||||
"underlying": "ETH",
|
||||
"opt_type": "C",
|
||||
"realized_pnl": 1.0,
|
||||
"created_at": "2026-09-01 10:00:00",
|
||||
"closed_at": "2026-09-01 12:00:00",
|
||||
},
|
||||
)
|
||||
trade_id = conn.execute(
|
||||
"SELECT id FROM options_review_trades WHERE history_key='ex:chart1'"
|
||||
).fetchone()["id"]
|
||||
save_review_entry(
|
||||
conn,
|
||||
trade_id,
|
||||
{
|
||||
"strategy_tag": "测试",
|
||||
"images": [{"tf": "15m", "file": "options_journal_x_15m.png"}],
|
||||
},
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
chart_name = build_options_review_chart_filename(trade_id)
|
||||
generate = MagicMock(return_value=chart_name)
|
||||
|
||||
with patch(
|
||||
"lib.options.options_review_chart_async_lib.threading.Thread"
|
||||
) as mock_thread_cls:
|
||||
thread = MagicMock()
|
||||
mock_thread_cls.return_value = thread
|
||||
schedule_options_review_exchange_chart(
|
||||
trade_id=trade_id,
|
||||
exchange_symbol="ETH/USDT:USDT",
|
||||
title_prefix="ETH review",
|
||||
journal_tfs=["15m", "1h"],
|
||||
journal_limit=200,
|
||||
marker_payload={"entry_ts_ms": 1, "exit_ts_ms": 2},
|
||||
upload_folder=td,
|
||||
generate_chart_fn=generate,
|
||||
get_db_fn=get_db,
|
||||
now_str_fn=lambda: "2026-09-15 12:00:00",
|
||||
)
|
||||
target = mock_thread_cls.call_args.kwargs["target"]
|
||||
target()
|
||||
|
||||
conn = get_db()
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT images_json, image FROM options_review_entries WHERE trade_id=?",
|
||||
(trade_id,),
|
||||
).fetchone()
|
||||
self.assertEqual(row["image"], chart_name)
|
||||
images = parse_options_review_images_json(row["images_json"])
|
||||
self.assertEqual(images[0]["tf"], "auto")
|
||||
self.assertEqual(images[0]["file"], chart_name)
|
||||
self.assertTrue(any(x.get("tf") == "15m" for x in images))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,92 @@
|
||||
"""期权业务时区与 UTC 墙钟迁移单测."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
import unittest
|
||||
from datetime import datetime, timezone
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from lib.options.options_db import init_options_tables
|
||||
from lib.options.options_review_db import init_options_review_tables
|
||||
from lib.options.options_review_lib import get_sync_state, upsert_option_history_row
|
||||
from lib.options.options_time_lib import (
|
||||
migrate_options_utc_wall_clock_to_app,
|
||||
ms_to_options_app_str,
|
||||
options_wall_clock_to_ms,
|
||||
utc_wall_clock_to_app_str,
|
||||
)
|
||||
|
||||
|
||||
class OptionsTimeLibTests(unittest.TestCase):
|
||||
def test_ms_to_shanghai_wall_clock(self):
|
||||
# 2026-09-13 21:41:57 UTC → 2026-09-14 05:41:57 Asia/Shanghai
|
||||
ms = int(
|
||||
datetime(2026, 9, 13, 21, 41, 57, tzinfo=timezone.utc).timestamp() * 1000
|
||||
)
|
||||
self.assertEqual(ms_to_options_app_str(ms), "2026-09-14 05:41:57")
|
||||
|
||||
def test_utc_wall_to_app(self):
|
||||
self.assertEqual(
|
||||
utc_wall_clock_to_app_str("2026-09-13 21:41:57"),
|
||||
"2026-09-14 05:41:57",
|
||||
)
|
||||
|
||||
def test_wall_clock_to_ms_roundtrip(self):
|
||||
raw = "2026-09-14 05:41:57"
|
||||
ms = options_wall_clock_to_ms(raw)
|
||||
self.assertIsNotNone(ms)
|
||||
dt = datetime.fromtimestamp(ms / 1000.0, tz=ZoneInfo("Asia/Shanghai"))
|
||||
self.assertEqual(dt.strftime("%Y-%m-%d %H:%M:%S"), raw)
|
||||
|
||||
def test_migrate_once(self):
|
||||
conn = sqlite3.connect(":memory:")
|
||||
conn.row_factory = sqlite3.Row
|
||||
init_options_tables(conn)
|
||||
init_options_review_tables(conn)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO options_trades(
|
||||
inst_id, underlying, opt_type, sheets, eth_amount, premium_paid, status,
|
||||
created_at, closed_at
|
||||
) VALUES ('ETH-USD-260917-2525-P', 'ETH', 'P', 1, 0.01, 1, 'closed',
|
||||
'2026-09-13 21:41:57', '2026-09-14 02:31:00')
|
||||
"""
|
||||
)
|
||||
upsert_option_history_row(
|
||||
conn,
|
||||
{
|
||||
"history_key": "local_opt:1",
|
||||
"inst_id": "ETH-USD-260917-2525-P",
|
||||
"underlying": "ETH",
|
||||
"opt_type": "P",
|
||||
"realized_pnl": 0.5,
|
||||
"created_at": "2026-09-13 21:41:57",
|
||||
"closed_at": "2026-09-14 02:31:00",
|
||||
},
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
first = migrate_options_utc_wall_clock_to_app(conn)
|
||||
self.assertEqual(first["skipped"], 0)
|
||||
self.assertGreaterEqual(first["trades"], 1)
|
||||
self.assertGreaterEqual(first["review"], 1)
|
||||
|
||||
row = conn.execute(
|
||||
"SELECT created_at, closed_at FROM options_trades WHERE id=1"
|
||||
).fetchone()
|
||||
self.assertEqual(row["created_at"], "2026-09-14 05:41:57")
|
||||
self.assertEqual(row["closed_at"], "2026-09-14 10:31:00")
|
||||
|
||||
rev = conn.execute(
|
||||
"SELECT opened_at, closed_at FROM options_review_trades WHERE history_key='local_opt:1'"
|
||||
).fetchone()
|
||||
self.assertEqual(rev["opened_at"], "2026-09-14 05:41:57")
|
||||
self.assertEqual(rev["closed_at"], "2026-09-14 10:31:00")
|
||||
self.assertEqual(get_sync_state(conn, "options_ts_utc_wall_to_app_done"), "1")
|
||||
|
||||
second = migrate_options_utc_wall_clock_to_app(conn)
|
||||
self.assertEqual(second["skipped"], 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user