feat: trades 테이블 mode 컬럼 추가 (#212) #221
@@ -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
|
||||
|
||||
16
src/db.py
16
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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user