feat: trades 테이블 mode 컬럼 추가 (paper/live 거래 분리) (#212)
Some checks failed
CI / test (pull_request) Has been cancelled
Some checks failed
CI / test (pull_request) Has been cancelled
- 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 <noreply@anthropic.com>
This commit is contained in:
16
src/db.py
16
src/db.py
@@ -28,12 +28,13 @@ def init_db(db_path: str) -> sqlite3.Connection:
|
|||||||
pnl REAL DEFAULT 0.0,
|
pnl REAL DEFAULT 0.0,
|
||||||
market TEXT DEFAULT 'KR',
|
market TEXT DEFAULT 'KR',
|
||||||
exchange_code TEXT DEFAULT 'KRX',
|
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)")
|
cursor = conn.execute("PRAGMA table_info(trades)")
|
||||||
columns = {row[1] for row in cursor.fetchall()}
|
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")
|
conn.execute("ALTER TABLE trades ADD COLUMN selection_context TEXT")
|
||||||
if "decision_id" not in columns:
|
if "decision_id" not in columns:
|
||||||
conn.execute("ALTER TABLE trades ADD COLUMN decision_id TEXT")
|
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
|
# Context tree tables for multi-layered memory management
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -167,6 +170,7 @@ def log_trade(
|
|||||||
exchange_code: str = "KRX",
|
exchange_code: str = "KRX",
|
||||||
selection_context: dict[str, any] | None = None,
|
selection_context: dict[str, any] | None = None,
|
||||||
decision_id: str | None = None,
|
decision_id: str | None = None,
|
||||||
|
mode: str = "paper",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Insert a trade record into the database.
|
"""Insert a trade record into the database.
|
||||||
|
|
||||||
@@ -182,6 +186,8 @@ def log_trade(
|
|||||||
market: Market code
|
market: Market code
|
||||||
exchange_code: Exchange code
|
exchange_code: Exchange code
|
||||||
selection_context: Scanner selection data (RSI, volume_ratio, signal, score)
|
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
|
# Serialize selection context to JSON
|
||||||
context_json = json.dumps(selection_context) if selection_context else None
|
context_json = json.dumps(selection_context) if selection_context else None
|
||||||
@@ -190,9 +196,10 @@ def log_trade(
|
|||||||
"""
|
"""
|
||||||
INSERT INTO trades (
|
INSERT INTO trades (
|
||||||
timestamp, stock_code, action, confidence, rationale,
|
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(),
|
datetime.now(UTC).isoformat(),
|
||||||
@@ -207,6 +214,7 @@ def log_trade(
|
|||||||
exchange_code,
|
exchange_code,
|
||||||
context_json,
|
context_json,
|
||||||
decision_id,
|
decision_id,
|
||||||
|
mode,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|||||||
@@ -822,6 +822,7 @@ async def trading_cycle(
|
|||||||
exchange_code=market.exchange_code,
|
exchange_code=market.exchange_code,
|
||||||
selection_context=selection_context,
|
selection_context=selection_context,
|
||||||
decision_id=decision_id,
|
decision_id=decision_id,
|
||||||
|
mode=settings.MODE if settings else "paper",
|
||||||
)
|
)
|
||||||
|
|
||||||
# 7. Latency monitoring
|
# 7. Latency monitoring
|
||||||
@@ -1318,6 +1319,7 @@ async def run_daily_session(
|
|||||||
market=market.code,
|
market=market.code,
|
||||||
exchange_code=market.exchange_code,
|
exchange_code=market.exchange_code,
|
||||||
decision_id=decision_id,
|
decision_id=decision_id,
|
||||||
|
mode=settings.MODE,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("Daily trading session completed")
|
logger.info("Daily trading session completed")
|
||||||
|
|||||||
100
tests/test_db.py
100
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:
|
def test_get_open_position_returns_none_when_no_trades() -> None:
|
||||||
conn = init_db(":memory:")
|
conn = init_db(":memory:")
|
||||||
assert get_open_position(conn, "AAPL", "US_NASDAQ") is None
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user