From b50eb526a75ceda4e7afa58049cfa521667f3e7e Mon Sep 17 00:00:00 2001 From: agentson Date: Wed, 4 Feb 2026 15:26:36 +0900 Subject: [PATCH] WIP: Add decision logging infrastructure - Add decision_logs table to database schema - Create decision logger module with comprehensive logging - Prepare for decision tracking and audit trail Co-Authored-By: Claude Sonnet 4.5 --- src/db.py | 33 +++++ src/logging/__init__.py | 5 + src/logging/decision_logger.py | 235 +++++++++++++++++++++++++++++++++ 3 files changed, 273 insertions(+) create mode 100644 src/logging/__init__.py create mode 100644 src/logging/decision_logger.py diff --git a/src/db.py b/src/db.py index f61d84d..6f8a2ee 100644 --- a/src/db.py +++ b/src/db.py @@ -39,6 +39,39 @@ def init_db(db_path: str) -> sqlite3.Connection: if "exchange_code" not in columns: conn.execute("ALTER TABLE trades ADD COLUMN exchange_code TEXT DEFAULT 'KRX'") + # Decision logging table for comprehensive audit trail + conn.execute( + """ + CREATE TABLE IF NOT EXISTS 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, + outcome_pnl REAL, + outcome_accuracy INTEGER, + reviewed INTEGER DEFAULT 0, + review_notes TEXT + ) + """ + ) + + # Create indices for efficient decision log queries + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_decision_logs_timestamp ON decision_logs(timestamp)" + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_decision_logs_reviewed ON decision_logs(reviewed)" + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_decision_logs_confidence ON decision_logs(confidence)" + ) + conn.commit() return conn diff --git a/src/logging/__init__.py b/src/logging/__init__.py new file mode 100644 index 0000000..6ad189a --- /dev/null +++ b/src/logging/__init__.py @@ -0,0 +1,5 @@ +"""Decision logging and audit trail for trade decisions.""" + +from src.logging.decision_logger import DecisionLog, DecisionLogger + +__all__ = ["DecisionLog", "DecisionLogger"] diff --git a/src/logging/decision_logger.py b/src/logging/decision_logger.py new file mode 100644 index 0000000..b2f52a5 --- /dev/null +++ b/src/logging/decision_logger.py @@ -0,0 +1,235 @@ +"""Decision logging system with context snapshots for comprehensive audit trail.""" + +from __future__ import annotations + +import json +import sqlite3 +import uuid +from dataclasses import dataclass +from datetime import UTC, datetime +from typing import Any + + +@dataclass +class DecisionLog: + """A logged trading decision with context and outcome.""" + + decision_id: str + timestamp: str + stock_code: str + market: str + exchange_code: str + action: str + confidence: int + rationale: str + context_snapshot: dict[str, Any] + input_data: dict[str, Any] + outcome_pnl: float | None = None + outcome_accuracy: int | None = None + reviewed: bool = False + review_notes: str | None = None + + +class DecisionLogger: + """Logs trading decisions with full context for review and evolution.""" + + def __init__(self, conn: sqlite3.Connection) -> None: + """Initialize the decision logger with a database connection.""" + self.conn = conn + + def log_decision( + self, + stock_code: str, + market: str, + exchange_code: str, + action: str, + confidence: int, + rationale: str, + context_snapshot: dict[str, Any], + input_data: dict[str, Any], + ) -> str: + """Log a trading decision with full context. + + Args: + stock_code: Stock symbol + market: Market code (e.g., "KR", "US_NASDAQ") + exchange_code: Exchange code (e.g., "KRX", "NASDAQ") + action: Trading action (BUY/SELL/HOLD) + confidence: Confidence level (0-100) + rationale: Reasoning for the decision + context_snapshot: L1-L7 context snapshot at decision time + input_data: Market data inputs (price, volume, orderbook, etc.) + + Returns: + decision_id: Unique identifier for this decision + """ + decision_id = str(uuid.uuid4()) + timestamp = datetime.now(UTC).isoformat() + + self.conn.execute( + """ + INSERT INTO decision_logs ( + decision_id, timestamp, stock_code, market, exchange_code, + action, confidence, rationale, context_snapshot, input_data + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + decision_id, + timestamp, + stock_code, + market, + exchange_code, + action, + confidence, + rationale, + json.dumps(context_snapshot), + json.dumps(input_data), + ), + ) + self.conn.commit() + + return decision_id + + def get_unreviewed_decisions( + self, min_confidence: int = 80, limit: int | None = None + ) -> list[DecisionLog]: + """Get unreviewed decisions with high confidence. + + Args: + min_confidence: Minimum confidence threshold (default 80) + limit: Maximum number of results (None = unlimited) + + Returns: + List of unreviewed DecisionLog objects + """ + query = """ + SELECT + decision_id, timestamp, stock_code, market, exchange_code, + action, confidence, rationale, context_snapshot, input_data, + outcome_pnl, outcome_accuracy, reviewed, review_notes + FROM decision_logs + WHERE reviewed = 0 AND confidence >= ? + ORDER BY timestamp DESC + """ + if limit is not None: + query += f" LIMIT {limit}" + + cursor = self.conn.execute(query, (min_confidence,)) + return [self._row_to_decision_log(row) for row in cursor.fetchall()] + + def mark_reviewed(self, decision_id: str, notes: str) -> None: + """Mark a decision as reviewed with notes. + + Args: + decision_id: Decision identifier + notes: Review notes and insights + """ + self.conn.execute( + """ + UPDATE decision_logs + SET reviewed = 1, review_notes = ? + WHERE decision_id = ? + """, + (notes, decision_id), + ) + self.conn.commit() + + def update_outcome( + self, decision_id: str, pnl: float, accuracy: int + ) -> None: + """Update the outcome of a decision after trade execution. + + Args: + decision_id: Decision identifier + pnl: Actual profit/loss realized + accuracy: 1 if decision was correct, 0 if wrong + """ + self.conn.execute( + """ + UPDATE decision_logs + SET outcome_pnl = ?, outcome_accuracy = ? + WHERE decision_id = ? + """, + (pnl, accuracy, decision_id), + ) + self.conn.commit() + + def get_decision_by_id(self, decision_id: str) -> DecisionLog | None: + """Get a specific decision by ID. + + Args: + decision_id: Decision identifier + + Returns: + DecisionLog object or None if not found + """ + cursor = self.conn.execute( + """ + SELECT + decision_id, timestamp, stock_code, market, exchange_code, + action, confidence, rationale, context_snapshot, input_data, + outcome_pnl, outcome_accuracy, reviewed, review_notes + FROM decision_logs + WHERE decision_id = ? + """, + (decision_id,), + ) + row = cursor.fetchone() + return self._row_to_decision_log(row) if row else None + + def get_losing_decisions( + self, min_confidence: int = 80, min_loss: float = -100.0 + ) -> list[DecisionLog]: + """Get high-confidence decisions that resulted in losses. + + Useful for identifying patterns in failed predictions. + + Args: + min_confidence: Minimum confidence threshold (default 80) + min_loss: Minimum loss amount (default -100.0, i.e., loss >= 100) + + Returns: + List of losing DecisionLog objects + """ + cursor = self.conn.execute( + """ + SELECT + decision_id, timestamp, stock_code, market, exchange_code, + action, confidence, rationale, context_snapshot, input_data, + outcome_pnl, outcome_accuracy, reviewed, review_notes + FROM decision_logs + WHERE confidence >= ? + AND outcome_pnl IS NOT NULL + AND outcome_pnl <= ? + ORDER BY outcome_pnl ASC + """, + (min_confidence, min_loss), + ) + return [self._row_to_decision_log(row) for row in cursor.fetchall()] + + def _row_to_decision_log(self, row: tuple[Any, ...]) -> DecisionLog: + """Convert a database row to a DecisionLog object. + + Args: + row: Database row tuple + + Returns: + DecisionLog object + """ + return DecisionLog( + decision_id=row[0], + timestamp=row[1], + 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], + )