diff --git a/CLAUDE.md b/CLAUDE.md index addfb0d..5247fb2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -170,7 +170,7 @@ Markets auto-detected based on timezone and enabled in `ENABLED_MARKETS` env var - `src/core/risk_manager.py` is **READ-ONLY** — changes require human approval - Circuit breaker at -3.0% P&L — may only be made **stricter** - Fat-finger protection: max 30% of cash per order — always enforced -- Confidence < 80 → force HOLD — cannot be weakened +- Confidence 임계값 (market_outlook별, 낮출 수 없음): BEARISH ≥ 90, NEUTRAL/기본 ≥ 80, BULLISH ≥ 75 - All code changes → corresponding tests → coverage ≥ 80% ## Contributing diff --git a/src/db.py b/src/db.py index 5b23a38..fc2070e 100644 --- a/src/db.py +++ b/src/db.py @@ -33,12 +33,13 @@ def init_db(db_path: str) -> sqlite3.Connection: pnl REAL DEFAULT 0.0, market TEXT DEFAULT 'KR', exchange_code TEXT DEFAULT 'KRX', - decision_id TEXT + decision_id TEXT, + mode TEXT DEFAULT 'paper' ) """ ) - # Migration: Add market and exchange_code columns if they don't exist + # Migration: Add columns if they don't exist (backward-compatible schema upgrades) cursor = conn.execute("PRAGMA table_info(trades)") columns = {row[1] for row in cursor.fetchall()} @@ -50,6 +51,8 @@ def init_db(db_path: str) -> sqlite3.Connection: conn.execute("ALTER TABLE trades ADD COLUMN selection_context TEXT") if "decision_id" not in columns: 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'") # Context tree tables for multi-layered memory management conn.execute( @@ -172,6 +175,7 @@ def log_trade( exchange_code: str = "KRX", selection_context: dict[str, any] | None = None, decision_id: str | None = None, + mode: str = "paper", ) -> None: """Insert a trade record into the database. @@ -187,6 +191,8 @@ def log_trade( market: Market code exchange_code: Exchange code 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 @@ -195,9 +201,10 @@ def log_trade( """ INSERT INTO trades ( timestamp, stock_code, action, confidence, rationale, - quantity, price, pnl, market, exchange_code, selection_context, decision_id + quantity, price, pnl, market, exchange_code, selection_context, decision_id, + mode ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( datetime.now(UTC).isoformat(), @@ -212,6 +219,7 @@ def log_trade( exchange_code, context_json, decision_id, + mode, ), ) conn.commit() diff --git a/src/main.py b/src/main.py index 5951617..35acfa0 100644 --- a/src/main.py +++ b/src/main.py @@ -828,6 +828,7 @@ async def trading_cycle( exchange_code=market.exchange_code, selection_context=selection_context, decision_id=decision_id, + mode=settings.MODE if settings else "paper", ) # 7. Latency monitoring @@ -1325,6 +1326,7 @@ async def run_daily_session( market=market.code, exchange_code=market.exchange_code, decision_id=decision_id, + mode=settings.MODE, ) logger.info("Daily trading session completed") diff --git a/tests/test_db.py b/tests/test_db.py index af2fcf8..ead224a 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -95,3 +95,101 @@ def test_wal_mode_not_applied_to_memory_db() -> None: # In-memory DBs default to 'memory' journal mode assert mode != "wal", "WAL should not be set on in-memory database" conn.close() + + +# --------------------------------------------------------------------------- +# mode column tests (issue #212) +# --------------------------------------------------------------------------- + + +def test_log_trade_stores_mode_paper() -> None: + """log_trade must persist mode='paper' in the trades table.""" + conn = init_db(":memory:") + log_trade( + conn=conn, + stock_code="005930", + action="BUY", + confidence=85, + rationale="test", + mode="paper", + ) + row = conn.execute("SELECT mode FROM trades ORDER BY id DESC LIMIT 1").fetchone() + assert row is not None + assert row[0] == "paper" + + +def test_log_trade_stores_mode_live() -> None: + """log_trade must persist mode='live' in the trades table.""" + conn = init_db(":memory:") + log_trade( + conn=conn, + stock_code="005930", + action="BUY", + confidence=85, + rationale="test", + mode="live", + ) + row = conn.execute("SELECT mode FROM trades ORDER BY id DESC LIMIT 1").fetchone() + assert row is not None + assert row[0] == "live" + + +def test_log_trade_default_mode_is_paper() -> None: + """log_trade without explicit mode must default to 'paper'.""" + conn = init_db(":memory:") + log_trade( + conn=conn, + stock_code="005930", + action="HOLD", + confidence=50, + rationale="test", + ) + row = conn.execute("SELECT mode FROM trades ORDER BY id DESC LIMIT 1").fetchone() + assert row is not None + assert row[0] == "paper" + + +def test_mode_column_exists_in_schema() -> None: + """trades table must have a mode column after init_db.""" + conn = init_db(":memory:") + cursor = conn.execute("PRAGMA table_info(trades)") + columns = {row[1] for row in cursor.fetchall()} + assert "mode" in columns + + +def test_mode_migration_adds_column_to_existing_db() -> None: + """init_db must add mode column to existing DBs that lack it (migration).""" + import sqlite3 + + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f: + db_path = f.name + try: + # Create DB without mode column (simulate old schema) + old_conn = sqlite3.connect(db_path) + old_conn.execute( + """CREATE TABLE trades ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL, + stock_code TEXT NOT NULL, + action TEXT NOT NULL, + confidence INTEGER NOT NULL, + rationale TEXT, + quantity INTEGER, + price REAL, + pnl REAL DEFAULT 0.0, + market TEXT DEFAULT 'KR', + exchange_code TEXT DEFAULT 'KRX', + decision_id TEXT + )""" + ) + old_conn.commit() + old_conn.close() + + # Run init_db — should add mode column via migration + conn = init_db(db_path) + cursor = conn.execute("PRAGMA table_info(trades)") + columns = {row[1] for row in cursor.fetchall()} + assert "mode" in columns + conn.close() + finally: + os.unlink(db_path)