From b2b02b6f570f297079e5a7f14462c51b38617e96 Mon Sep 17 00:00:00 2001 From: agentson Date: Fri, 27 Feb 2026 08:49:04 +0900 Subject: [PATCH 1/2] feat: enforce session_id persistence in trade ledger (TASK-CODE-007) --- src/db.py | 24 ++++++++++++++++++++++-- tests/test_db.py | 22 +++++++++++++++++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/db.py b/src/db.py index 96fb1c7..9925883 100644 --- a/src/db.py +++ b/src/db.py @@ -8,6 +8,9 @@ from datetime import UTC, datetime from pathlib import Path from typing import Any +from src.core.order_policy import classify_session_id +from src.markets.schedule import MARKETS + def init_db(db_path: str) -> sqlite3.Connection: """Initialize the trade logs database and return a connection.""" @@ -35,6 +38,7 @@ def init_db(db_path: str) -> sqlite3.Connection: fx_pnl REAL DEFAULT 0.0, market TEXT DEFAULT 'KR', exchange_code TEXT DEFAULT 'KRX', + session_id TEXT DEFAULT 'UNKNOWN', selection_context TEXT, decision_id TEXT, mode TEXT DEFAULT 'paper' @@ -56,6 +60,8 @@ def init_db(db_path: str) -> sqlite3.Connection: conn.execute("ALTER TABLE trades ADD COLUMN decision_id TEXT") if "mode" not in columns: conn.execute("ALTER TABLE trades ADD COLUMN mode TEXT DEFAULT 'paper'") + if "session_id" not in columns: + conn.execute("ALTER TABLE trades ADD COLUMN session_id TEXT DEFAULT 'UNKNOWN'") if "strategy_pnl" not in columns: conn.execute("ALTER TABLE trades ADD COLUMN strategy_pnl REAL DEFAULT 0.0") if "fx_pnl" not in columns: @@ -70,6 +76,13 @@ def init_db(db_path: str) -> sqlite3.Connection: AND fx_pnl = 0.0 """ ) + conn.execute( + """ + UPDATE trades + SET session_id = 'UNKNOWN' + WHERE session_id IS NULL OR session_id = '' + """ + ) # Context tree tables for multi-layered memory management conn.execute( @@ -192,6 +205,7 @@ def log_trade( fx_pnl: float | None = None, market: str = "KR", exchange_code: str = "KRX", + session_id: str | None = None, selection_context: dict[str, any] | None = None, decision_id: str | None = None, mode: str = "paper", @@ -211,12 +225,17 @@ def log_trade( fx_pnl: FX PnL component market: Market code exchange_code: Exchange code + session_id: Session identifier (if omitted, auto-derived from market) selection_context: Scanner selection data (RSI, volume_ratio, signal, score) decision_id: Unique decision identifier for audit linking mode: Trading mode ('paper' or 'live') for data separation """ # Serialize selection context to JSON context_json = json.dumps(selection_context) if selection_context else None + resolved_session_id = session_id or "UNKNOWN" + market_info = MARKETS.get(market) + if session_id is None and market_info is not None: + resolved_session_id = classify_session_id(market_info) if strategy_pnl is None and fx_pnl is None: strategy_pnl = pnl fx_pnl = 0.0 @@ -232,9 +251,9 @@ def log_trade( INSERT INTO trades ( timestamp, stock_code, action, confidence, rationale, quantity, price, pnl, strategy_pnl, fx_pnl, - market, exchange_code, selection_context, decision_id, mode + market, exchange_code, session_id, selection_context, decision_id, mode ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( datetime.now(UTC).isoformat(), @@ -249,6 +268,7 @@ def log_trade( fx_pnl, market, exchange_code, + resolved_session_id, context_json, decision_id, mode, diff --git a/tests/test_db.py b/tests/test_db.py index 49b822c..aa1b938 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -155,6 +155,7 @@ def test_mode_column_exists_in_schema() -> None: cursor = conn.execute("PRAGMA table_info(trades)") columns = {row[1] for row in cursor.fetchall()} assert "mode" in columns + assert "session_id" in columns assert "strategy_pnl" in columns assert "fx_pnl" in columns @@ -199,15 +200,17 @@ def test_mode_migration_adds_column_to_existing_db() -> None: cursor = conn.execute("PRAGMA table_info(trades)") columns = {row[1] for row in cursor.fetchall()} assert "mode" in columns + assert "session_id" in columns assert "strategy_pnl" in columns assert "fx_pnl" in columns migrated = conn.execute( - "SELECT pnl, strategy_pnl, fx_pnl FROM trades WHERE stock_code='AAPL' LIMIT 1" + "SELECT pnl, strategy_pnl, fx_pnl, session_id FROM trades WHERE stock_code='AAPL' LIMIT 1" ).fetchone() assert migrated is not None assert migrated[0] == 123.45 assert migrated[1] == 123.45 assert migrated[2] == 0.0 + assert migrated[3] == "UNKNOWN" conn.close() finally: os.unlink(db_path) @@ -277,3 +280,20 @@ def test_log_trade_partial_fx_input_does_not_infer_negative_strategy_pnl() -> No assert row[0] == 10.0 assert row[1] == 0.0 assert row[2] == 10.0 + + +def test_log_trade_persists_explicit_session_id() -> None: + conn = init_db(":memory:") + log_trade( + conn=conn, + stock_code="AAPL", + action="BUY", + confidence=70, + rationale="session test", + market="US_NASDAQ", + exchange_code="NASD", + session_id="US_PRE", + ) + row = conn.execute("SELECT session_id FROM trades ORDER BY id DESC LIMIT 1").fetchone() + assert row is not None + assert row[0] == "US_PRE" From 694d73b212b75c02f49d7f501b240cf5ea46fc46 Mon Sep 17 00:00:00 2001 From: agentson Date: Fri, 27 Feb 2026 08:51:00 +0900 Subject: [PATCH 2/2] fix: lazy session resolver and one-time session_id backfill --- src/db.py | 40 ++++++++++++++++++++++++++-------------- tests/test_db.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/src/db.py b/src/db.py index 9925883..4a0c9f0 100644 --- a/src/db.py +++ b/src/db.py @@ -8,9 +8,6 @@ from datetime import UTC, datetime from pathlib import Path from typing import Any -from src.core.order_policy import classify_session_id -from src.markets.schedule import MARKETS - def init_db(db_path: str) -> sqlite3.Connection: """Initialize the trade logs database and return a connection.""" @@ -60,8 +57,10 @@ def init_db(db_path: str) -> sqlite3.Connection: conn.execute("ALTER TABLE trades ADD COLUMN decision_id TEXT") if "mode" not in columns: conn.execute("ALTER TABLE trades ADD COLUMN mode TEXT DEFAULT 'paper'") + session_id_added = False if "session_id" not in columns: conn.execute("ALTER TABLE trades ADD COLUMN session_id TEXT DEFAULT 'UNKNOWN'") + session_id_added = True if "strategy_pnl" not in columns: conn.execute("ALTER TABLE trades ADD COLUMN strategy_pnl REAL DEFAULT 0.0") if "fx_pnl" not in columns: @@ -76,13 +75,14 @@ def init_db(db_path: str) -> sqlite3.Connection: AND fx_pnl = 0.0 """ ) - conn.execute( - """ - UPDATE trades - SET session_id = 'UNKNOWN' - WHERE session_id IS NULL OR session_id = '' - """ - ) + if session_id_added: + conn.execute( + """ + UPDATE trades + SET session_id = 'UNKNOWN' + WHERE session_id IS NULL OR session_id = '' + """ + ) # Context tree tables for multi-layered memory management conn.execute( @@ -232,10 +232,7 @@ def log_trade( """ # Serialize selection context to JSON context_json = json.dumps(selection_context) if selection_context else None - resolved_session_id = session_id or "UNKNOWN" - market_info = MARKETS.get(market) - if session_id is None and market_info is not None: - resolved_session_id = classify_session_id(market_info) + resolved_session_id = _resolve_session_id(market=market, session_id=session_id) if strategy_pnl is None and fx_pnl is None: strategy_pnl = pnl fx_pnl = 0.0 @@ -277,6 +274,21 @@ def log_trade( conn.commit() +def _resolve_session_id(*, market: str, session_id: str | None) -> str: + if session_id: + return session_id + try: + from src.core.order_policy import classify_session_id + from src.markets.schedule import MARKETS + + market_info = MARKETS.get(market) + if market_info is not None: + return classify_session_id(market_info) + except Exception: + pass + return "UNKNOWN" + + def get_latest_buy_trade( conn: sqlite3.Connection, stock_code: str, market: str ) -> dict[str, Any] | None: diff --git a/tests/test_db.py b/tests/test_db.py index aa1b938..bbd600e 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -297,3 +297,35 @@ def test_log_trade_persists_explicit_session_id() -> None: row = conn.execute("SELECT session_id FROM trades ORDER BY id DESC LIMIT 1").fetchone() assert row is not None assert row[0] == "US_PRE" + + +def test_log_trade_auto_derives_session_id_when_not_provided() -> None: + conn = init_db(":memory:") + log_trade( + conn=conn, + stock_code="005930", + action="BUY", + confidence=70, + rationale="auto session", + market="KR", + exchange_code="KRX", + ) + row = conn.execute("SELECT session_id FROM trades ORDER BY id DESC LIMIT 1").fetchone() + assert row is not None + assert row[0] != "UNKNOWN" + + +def test_log_trade_unknown_market_falls_back_to_unknown_session() -> None: + conn = init_db(":memory:") + log_trade( + conn=conn, + stock_code="X", + action="BUY", + confidence=70, + rationale="unknown market", + market="MARS", + exchange_code="MARS", + ) + row = conn.execute("SELECT session_id FROM trades ORDER BY id DESC LIMIT 1").fetchone() + assert row is not None + assert row[0] == "UNKNOWN"