diff --git a/scripts/repair_options_spot_vwap_from_sim_ledger.py b/scripts/repair_options_spot_vwap_from_sim_ledger.py index 0078f1d..8f54ced 100644 --- a/scripts/repair_options_spot_vwap_from_sim_ledger.py +++ b/scripts/repair_options_spot_vwap_from_sim_ledger.py @@ -104,19 +104,37 @@ def _ledger_buy_sell( """, (u, f"{u}/USDT%", start_id, end_id), ).fetchall() + # 同秒多笔卖回时,按「币卖出 id → 下一笔 USDT 入账」配对,避免重复吃到同一笔 USDT + used_usdt_ids: set[int] = set() for row in sells: + sid = int(row["id"]) usdt_row = conn.execute( """ - SELECT amount FROM sim_ledger_entries - WHERE ccy='USDT' AND account='trading' AND kind='convert' - AND amount > 0 AND ts=? AND note LIKE 'from %' - AND id > ? AND id < ? - ORDER BY id LIMIT 1 + SELECT id, amount FROM sim_ledger_entries + WHERE id = ? AND ccy='USDT' AND account='trading' AND kind='convert' + AND amount > 0 AND note LIKE 'from %' """, - (row["ts"], start_id, end_id), + (sid + 1,), ).fetchone() + if usdt_row is None or int(usdt_row["id"]) in used_usdt_ids: + cands = conn.execute( + """ + SELECT id, amount FROM sim_ledger_entries + WHERE ccy='USDT' AND account='trading' AND kind='convert' + AND amount > 0 AND note LIKE 'from %' + AND id > ? AND id < ? + ORDER BY id + """, + (sid, end_id), + ).fetchall() + usdt_row = next( + (r for r in cands if int(r["id"]) not in used_usdt_ids), + None, + ) if not usdt_row: continue + uid = int(usdt_row["id"]) + used_usdt_ids.add(uid) sell_coin += abs(float(row["amount"])) sell_usdt += float(usdt_row["amount"]) return ( diff --git a/tests/test_repair_options_spot_vwap_ledger.py b/tests/test_repair_options_spot_vwap_ledger.py new file mode 100644 index 0000000..ea155e3 --- /dev/null +++ b/tests/test_repair_options_spot_vwap_ledger.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +"""repair_options_spot_vwap_from_sim_ledger:同秒多笔卖回不得重复配对 USDT.""" +from __future__ import annotations + +import importlib.util +import sqlite3 +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "repair_options_spot_vwap_from_sim_ledger.py" + + +def _load_repair(): + spec = importlib.util.spec_from_file_location("repair_opt_vwap", SCRIPT) + assert spec and spec.loader + mod = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = mod + spec.loader.exec_module(mod) + return mod + + +def _mk_conn() -> sqlite3.Connection: + c = sqlite3.connect(":memory:") + c.row_factory = sqlite3.Row + c.executescript( + """ + CREATE TABLE sim_ledger_entries ( + id INTEGER PRIMARY KEY, + ts TEXT, kind TEXT, ccy TEXT, account TEXT, amount REAL, note TEXT + ); + """ + ) + return c + + +def test_same_ts_multi_sell_pairs_distinct_usdt(): + mod = _load_repair() + c = _mk_conn() + # 模拟 trade2:最后一笔 option_close 后同秒/连秒多笔 BTC→USDT + rows = [ + (21, "2026-10-13 00:06:58", "option_close", "BTC", "trading", 0.01, "sell BTC-USD-261015-84000-P"), + (23, "2026-10-13 00:07:35", "option_close", "BTC", "trading", 0.008, "sell BTC-USD-261015-84000-P"), + (24, "2026-10-13 00:07:35", "convert", "BTC", "trading", -0.01123875, "BTC/USDT mkt 81411.8737"), + (25, "2026-10-13 00:07:35", "convert", "USDT", "trading", 914.967695545875, "from btc"), + (26, "2026-10-13 00:07:36", "convert", "BTC", "trading", -0.018413659934387205, "BTC/USDT mkt 81424.9671"), + (27, "2026-10-13 00:07:36", "convert", "USDT", "trading", 1499.3316552687493, "from btc"), + (28, "2026-10-13 00:07:36", "convert", "BTC", "trading", -1.8413659934386636e-05, "BTC/USDT mkt 81418.9702"), + (29, "2026-10-13 00:07:36", "convert", "USDT", "trading", 1.4992212285500766, "from btc"), + ] + c.executemany( + "INSERT INTO sim_ledger_entries(id,ts,kind,ccy,account,amount,note) VALUES (?,?,?,?,?,?,?)", + rows, + ) + buy_u, buy_c, sell_u, sell_c = mod._ledger_buy_sell( + c, + underly="BTC", + inst_id="BTC-USD-261015-84000-P", + open_ts="", + close_ts="", + ) + assert buy_u is None and buy_c is None + assert sell_c is not None and abs(sell_c - 0.02967082359432159) < 1e-12 + assert sell_u is not None and abs(sell_u - 2415.798572043174) < 1e-6 + vwap = sell_u / sell_c + assert 81400 < vwap < 81450 + + +def test_old_bug_would_double_count_large_fill(): + """对照:若按同 ts 永远取第一笔 USDT,会得到 ~3913 而非 ~2415.""" + mod = _load_repair() + c = _mk_conn() + rows = [ + (23, "t0", "option_close", "BTC", "trading", 0.01, "sell BTC-USD-X"), + (24, "t1", "convert", "BTC", "trading", -0.01, "BTC/USDT mkt 1"), + (25, "t1", "convert", "USDT", "trading", 900.0, "from btc"), + (26, "t2", "convert", "BTC", "trading", -0.02, "BTC/USDT mkt 1"), + (27, "t2", "convert", "USDT", "trading", 1500.0, "from btc"), + (28, "t2", "convert", "BTC", "trading", -0.0001, "BTC/USDT mkt 1"), + (29, "t2", "convert", "USDT", "trading", 1.5, "from btc"), + ] + c.executemany( + "INSERT INTO sim_ledger_entries(id,ts,kind,ccy,account,amount,note) VALUES (?,?,?,?,?,?,?)", + rows, + ) + _, _, sell_u, sell_c = mod._ledger_buy_sell( + c, underly="BTC", inst_id="BTC-USD-X", open_ts="", close_ts="" + ) + assert sell_u == 2401.5 + assert abs(float(sell_c) - 0.0301) < 1e-12