From 11dff9d3e5c0e89403fa0b5ceccad83796300eca Mon Sep 17 00:00:00 2001 From: agentson Date: Mon, 23 Feb 2026 10:33:02 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20trades=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20mode=20=EC=BB=AC=EB=9F=BC=20=EC=B6=94=EA=B0=80=20(p?= =?UTF-8?q?aper/live=20=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) From ad79082dcc8c2f8cb04986adb8d9e22979dbf761 Mon Sep 17 00:00:00 2001 From: agentson Date: Mon, 23 Feb 2026 12:28:30 +0900 Subject: [PATCH 2/3] =?UTF-8?q?docs:=20CLAUDE.md=20=EB=B9=84=ED=98=91?= =?UTF-8?q?=EC=83=81=20=EA=B7=9C=EC=B9=99=20=EB=AA=85=EC=8B=9C=20=EA=B0=95?= =?UTF-8?q?=ED=99=94=20=E2=80=94=20BULLISH=20=EC=8B=9C=20confidence=20?= =?UTF-8?q?=EC=9E=84=EA=B3=84=EA=B0=92=20=ED=8F=AC=ED=95=A8=20(#205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BULLISH 시장에서도 confidence < 80 → HOLD 규칙이 동일하게 적용됨을 명시. 시장 전망별 임계값: BEARISH=90(더 엄격), BULLISH/NEUTRAL=80(최소값). Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index addfb0d..588702c 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 < 80 → force HOLD — **절대 낮출 수 없음** (BULLISH 시장 포함, 모든 market_outlook에서 최소 80 적용, BEARISH는 90으로 더 엄격) - All code changes → corresponding tests → coverage ≥ 80% ## Contributing From 48b87a79f672e2619cbb713cbbaac994e20791d2 Mon Sep 17 00:00:00 2001 From: agentson Date: Mon, 23 Feb 2026 12:29:39 +0900 Subject: [PATCH 3/3] =?UTF-8?q?docs:=20CLAUDE.md=20confidence=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20BULLISH=3D75=20=EB=AA=85=EC=8B=9C=20(#205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 시장 전망별 BUY confidence 최소 임계값: - BEARISH: 90 (더 엄격) - NEUTRAL/기본: 80 - BULLISH: 75 (낙관적 시장에서 완화) Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 588702c..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 — **절대 낮출 수 없음** (BULLISH 시장 포함, 모든 market_outlook에서 최소 80 적용, BEARISH는 90으로 더 엄격) +- Confidence 임계값 (market_outlook별, 낮출 수 없음): BEARISH ≥ 90, NEUTRAL/기본 ≥ 80, BULLISH ≥ 75 - All code changes → corresponding tests → coverage ≥ 80% ## Contributing