From 11b9ad126f8988603ff46c7cda3973f2872c5f2a Mon Sep 17 00:00:00 2001 From: agentson Date: Sat, 28 Feb 2026 14:37:32 +0900 Subject: [PATCH] feat: propagate runtime session_id across decision and trade logs (#326) --- src/db.py | 22 +++++++++++++++ src/logging/decision_logger.py | 34 +++++++++++++---------- src/main.py | 8 ++++++ tests/test_db.py | 50 ++++++++++++++++++++++++++++++++++ tests/test_decision_logger.py | 23 +++++++++++++++- 5 files changed, 122 insertions(+), 15 deletions(-) diff --git a/src/db.py b/src/db.py index 4a0c9f0..0839521 100644 --- a/src/db.py +++ b/src/db.py @@ -109,6 +109,7 @@ def init_db(db_path: str) -> sqlite3.Connection: stock_code TEXT NOT NULL, market TEXT NOT NULL, exchange_code TEXT NOT NULL, + session_id TEXT DEFAULT 'UNKNOWN', action TEXT NOT NULL, confidence INTEGER NOT NULL, rationale TEXT NOT NULL, @@ -121,6 +122,27 @@ def init_db(db_path: str) -> sqlite3.Connection: ) """ ) + decision_columns = { + row[1] + for row in conn.execute("PRAGMA table_info(decision_logs)").fetchall() + } + if "session_id" not in decision_columns: + conn.execute("ALTER TABLE decision_logs ADD COLUMN session_id TEXT DEFAULT 'UNKNOWN'") + conn.execute( + """ + UPDATE decision_logs + SET session_id = 'UNKNOWN' + WHERE session_id IS NULL OR session_id = '' + """ + ) + if "outcome_pnl" not in decision_columns: + conn.execute("ALTER TABLE decision_logs ADD COLUMN outcome_pnl REAL") + if "outcome_accuracy" not in decision_columns: + conn.execute("ALTER TABLE decision_logs ADD COLUMN outcome_accuracy INTEGER") + if "reviewed" not in decision_columns: + conn.execute("ALTER TABLE decision_logs ADD COLUMN reviewed INTEGER DEFAULT 0") + if "review_notes" not in decision_columns: + conn.execute("ALTER TABLE decision_logs ADD COLUMN review_notes TEXT") conn.execute( """ diff --git a/src/logging/decision_logger.py b/src/logging/decision_logger.py index b2f52a5..cd19b28 100644 --- a/src/logging/decision_logger.py +++ b/src/logging/decision_logger.py @@ -19,6 +19,7 @@ class DecisionLog: stock_code: str market: str exchange_code: str + session_id: str action: str confidence: int rationale: str @@ -47,6 +48,7 @@ class DecisionLogger: rationale: str, context_snapshot: dict[str, Any], input_data: dict[str, Any], + session_id: str | None = None, ) -> str: """Log a trading decision with full context. @@ -59,20 +61,22 @@ class DecisionLogger: rationale: Reasoning for the decision context_snapshot: L1-L7 context snapshot at decision time input_data: Market data inputs (price, volume, orderbook, etc.) + session_id: Runtime session identifier Returns: decision_id: Unique identifier for this decision """ decision_id = str(uuid.uuid4()) timestamp = datetime.now(UTC).isoformat() + resolved_session = session_id or "UNKNOWN" self.conn.execute( """ INSERT INTO decision_logs ( decision_id, timestamp, stock_code, market, exchange_code, - action, confidence, rationale, context_snapshot, input_data + session_id, action, confidence, rationale, context_snapshot, input_data ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( decision_id, @@ -80,6 +84,7 @@ class DecisionLogger: stock_code, market, exchange_code, + resolved_session, action, confidence, rationale, @@ -106,7 +111,7 @@ class DecisionLogger: query = """ SELECT decision_id, timestamp, stock_code, market, exchange_code, - action, confidence, rationale, context_snapshot, input_data, + session_id, action, confidence, rationale, context_snapshot, input_data, outcome_pnl, outcome_accuracy, reviewed, review_notes FROM decision_logs WHERE reviewed = 0 AND confidence >= ? @@ -168,7 +173,7 @@ class DecisionLogger: """ SELECT decision_id, timestamp, stock_code, market, exchange_code, - action, confidence, rationale, context_snapshot, input_data, + session_id, action, confidence, rationale, context_snapshot, input_data, outcome_pnl, outcome_accuracy, reviewed, review_notes FROM decision_logs WHERE decision_id = ? @@ -196,7 +201,7 @@ class DecisionLogger: """ SELECT decision_id, timestamp, stock_code, market, exchange_code, - action, confidence, rationale, context_snapshot, input_data, + session_id, action, confidence, rationale, context_snapshot, input_data, outcome_pnl, outcome_accuracy, reviewed, review_notes FROM decision_logs WHERE confidence >= ? @@ -223,13 +228,14 @@ class DecisionLogger: stock_code=row[2], market=row[3], exchange_code=row[4], - action=row[5], - confidence=row[6], - rationale=row[7], - context_snapshot=json.loads(row[8]), - input_data=json.loads(row[9]), - outcome_pnl=row[10], - outcome_accuracy=row[11], - reviewed=bool(row[12]), - review_notes=row[13], + session_id=row[5] or "UNKNOWN", + action=row[6], + confidence=row[7], + rationale=row[8], + context_snapshot=json.loads(row[9]), + input_data=json.loads(row[10]), + outcome_pnl=row[11], + outcome_accuracy=row[12], + reviewed=bool(row[13]), + review_notes=row[14], ) diff --git a/src/main.py b/src/main.py index cc158a2..97f9fd8 100644 --- a/src/main.py +++ b/src/main.py @@ -217,6 +217,7 @@ async def sync_positions_from_broker( price=avg_price, market=log_market, exchange_code=market.exchange_code, + session_id=get_session_info(market).session_id, mode=settings.MODE, ) logger.info( @@ -1368,10 +1369,12 @@ async def trading_cycle( "pnl_pct": pnl_pct, } + runtime_session_id = get_session_info(market).session_id decision_id = decision_logger.log_decision( stock_code=stock_code, market=market.code, exchange_code=market.exchange_code, + session_id=runtime_session_id, action=decision.action, confidence=decision.confidence, rationale=decision.rationale, @@ -1636,6 +1639,7 @@ async def trading_cycle( pnl=0.0, market=market.code, exchange_code=market.exchange_code, + session_id=runtime_session_id, mode=settings.MODE if settings else "paper", ) logger.info("Order result: %s", result.get("msg1", "OK")) @@ -1690,6 +1694,7 @@ async def trading_cycle( pnl=trade_pnl, market=market.code, exchange_code=market.exchange_code, + session_id=runtime_session_id, selection_context=selection_context, decision_id=decision_id, mode=settings.MODE if settings else "paper", @@ -2497,10 +2502,12 @@ async def run_daily_session( "pnl_pct": pnl_pct, } + runtime_session_id = get_session_info(market).session_id decision_id = decision_logger.log_decision( stock_code=stock_code, market=market.code, exchange_code=market.exchange_code, + session_id=runtime_session_id, action=decision.action, confidence=decision.confidence, rationale=decision.rationale, @@ -2777,6 +2784,7 @@ async def run_daily_session( pnl=trade_pnl, market=market.code, exchange_code=market.exchange_code, + session_id=runtime_session_id, decision_id=decision_id, mode=settings.MODE, ) diff --git a/tests/test_db.py b/tests/test_db.py index bbd600e..9bd190d 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -329,3 +329,53 @@ def test_log_trade_unknown_market_falls_back_to_unknown_session() -> None: 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_decision_logs_session_id_migration_backfills_unknown() -> None: + import sqlite3 + + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f: + db_path = f.name + try: + old_conn = sqlite3.connect(db_path) + old_conn.execute( + """ + CREATE TABLE decision_logs ( + decision_id TEXT PRIMARY KEY, + timestamp TEXT NOT NULL, + stock_code TEXT NOT NULL, + market TEXT NOT NULL, + exchange_code TEXT NOT NULL, + action TEXT NOT NULL, + confidence INTEGER NOT NULL, + rationale TEXT NOT NULL, + context_snapshot TEXT NOT NULL, + input_data TEXT NOT NULL + ) + """ + ) + old_conn.execute( + """ + INSERT INTO decision_logs ( + decision_id, timestamp, stock_code, market, exchange_code, + action, confidence, rationale, context_snapshot, input_data + ) VALUES ( + 'd1', '2026-01-01T00:00:00+00:00', 'AAPL', 'US_NASDAQ', 'NASD', + 'BUY', 80, 'legacy row', '{}', '{}' + ) + """ + ) + old_conn.commit() + old_conn.close() + + conn = init_db(db_path) + columns = {row[1] for row in conn.execute("PRAGMA table_info(decision_logs)").fetchall()} + assert "session_id" in columns + row = conn.execute( + "SELECT session_id FROM decision_logs WHERE decision_id='d1'" + ).fetchone() + assert row is not None + assert row[0] == "UNKNOWN" + conn.close() + finally: + os.unlink(db_path) diff --git a/tests/test_decision_logger.py b/tests/test_decision_logger.py index 652d3c3..dec3a64 100644 --- a/tests/test_decision_logger.py +++ b/tests/test_decision_logger.py @@ -49,7 +49,7 @@ def test_log_decision_creates_record(logger: DecisionLogger, db_conn: sqlite3.Co # Verify record exists in database cursor = db_conn.execute( - "SELECT decision_id, action, confidence FROM decision_logs WHERE decision_id = ?", + "SELECT decision_id, action, confidence, session_id FROM decision_logs WHERE decision_id = ?", (decision_id,), ) row = cursor.fetchone() @@ -57,6 +57,7 @@ def test_log_decision_creates_record(logger: DecisionLogger, db_conn: sqlite3.Co assert row[0] == decision_id assert row[1] == "BUY" assert row[2] == 85 + assert row[3] == "UNKNOWN" def test_log_decision_stores_context_snapshot(logger: DecisionLogger) -> None: @@ -84,6 +85,24 @@ def test_log_decision_stores_context_snapshot(logger: DecisionLogger) -> None: assert decision is not None assert decision.context_snapshot == context_snapshot assert decision.input_data == input_data + assert decision.session_id == "UNKNOWN" + + +def test_log_decision_stores_explicit_session_id(logger: DecisionLogger) -> None: + decision_id = logger.log_decision( + stock_code="AAPL", + market="US_NASDAQ", + exchange_code="NASD", + action="BUY", + confidence=88, + rationale="session check", + context_snapshot={}, + input_data={}, + session_id="US_PRE", + ) + decision = logger.get_decision_by_id(decision_id) + assert decision is not None + assert decision.session_id == "US_PRE" def test_get_unreviewed_decisions(logger: DecisionLogger) -> None: @@ -278,6 +297,7 @@ def test_decision_log_dataclass() -> None: stock_code="005930", market="KR", exchange_code="KRX", + session_id="KRX_REG", action="BUY", confidence=85, rationale="Test", @@ -286,6 +306,7 @@ def test_decision_log_dataclass() -> None: ) assert log.decision_id == "test-uuid" + assert log.session_id == "KRX_REG" assert log.action == "BUY" assert log.confidence == 85 assert log.reviewed is False -- 2.49.1