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 <noreply@anthropic.com>
This commit is contained in:
33
src/db.py
33
src/db.py
@@ -39,6 +39,39 @@ def init_db(db_path: str) -> sqlite3.Connection:
|
|||||||
if "exchange_code" not in columns:
|
if "exchange_code" not in columns:
|
||||||
conn.execute("ALTER TABLE trades ADD COLUMN exchange_code TEXT DEFAULT 'KRX'")
|
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()
|
conn.commit()
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
|
|||||||
5
src/logging/__init__.py
Normal file
5
src/logging/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Decision logging and audit trail for trade decisions."""
|
||||||
|
|
||||||
|
from src.logging.decision_logger import DecisionLog, DecisionLogger
|
||||||
|
|
||||||
|
__all__ = ["DecisionLog", "DecisionLogger"]
|
||||||
235
src/logging/decision_logger.py
Normal file
235
src/logging/decision_logger.py
Normal file
@@ -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],
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user