From 11dff9d3e5c0e89403fa0b5ceccad83796300eca Mon Sep 17 00:00:00 2001 From: agentson Date: Mon, 23 Feb 2026 10:33:02 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20trades=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?mode=20=EC=BB=AC=EB=9F=BC=20=EC=B6=94=EA=B0=80=20(paper/live=20?= =?UTF-8?q?=EA=B1=B0=EB=9E=98=20=EB=B6=84=EB=A6=AC)=20(#212)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - trades 테이블에 mode TEXT DEFAULT 'paper' 컬럼 추가 - 기존 DB 마이그레이션: ALTER TABLE으로 mode 컬럼 자동 추가 - log_trade() 함수에 mode 파라미터 추가 (기본값 'paper') - trading_cycle(), run_daily_session()에서 settings.MODE 전달 - 테스트 5개 추가 (mode 저장, 기본값, 스키마 검증, 마이그레이션) Co-Authored-By: Claude Sonnet 4.6 --- src/db.py | 16 ++++++-- src/main.py | 2 + tests/test_db.py | 100 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 4 deletions(-) diff --git a/src/db.py b/src/db.py index 60dae11..6149eb5 100644 --- a/src/db.py +++ b/src/db.py @@ -28,12 +28,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()} @@ -45,6 +46,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( @@ -167,6 +170,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. @@ -182,6 +186,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 @@ -190,9 +196,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(), @@ -207,6 +214,7 @@ def log_trade( exchange_code, context_json, decision_id, + mode, ), ) conn.commit() diff --git a/src/main.py b/src/main.py index b91c03d..3cfc25a 100644 --- a/src/main.py +++ b/src/main.py @@ -822,6 +822,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 @@ -1318,6 +1319,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 fe956eb..ad65e27 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -58,3 +58,103 @@ def test_get_open_position_returns_none_when_latest_is_sell() -> None: def test_get_open_position_returns_none_when_no_trades() -> None: conn = init_db(":memory:") assert get_open_position(conn, "AAPL", "US_NASDAQ") is None + + +# --------------------------------------------------------------------------- +# 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 tempfile + import os + + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f: + db_path = f.name + try: + # Create DB without mode column (simulate old schema) + import sqlite3 + 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)