- 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>
236 lines
7.2 KiB
Python
236 lines
7.2 KiB
Python
"""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],
|
|
)
|