From 7bc4e8833500dbd840d48b50e460d3c4664a2e2f Mon Sep 17 00:00:00 2001 From: agentson Date: Fri, 27 Feb 2026 08:44:05 +0900 Subject: [PATCH 1/2] feat: separate strategy and fx pnl fields in trade logs (TASK-CODE-011) --- src/db.py | 30 ++++++++++++++++++++++++++---- tests/test_db.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/db.py b/src/db.py index 9c24584..c8638c4 100644 --- a/src/db.py +++ b/src/db.py @@ -31,8 +31,11 @@ def init_db(db_path: str) -> sqlite3.Connection: quantity INTEGER, price REAL, pnl REAL DEFAULT 0.0, + strategy_pnl REAL DEFAULT 0.0, + fx_pnl REAL DEFAULT 0.0, market TEXT DEFAULT 'KR', exchange_code TEXT DEFAULT 'KRX', + selection_context TEXT, decision_id TEXT, mode TEXT DEFAULT 'paper' ) @@ -53,6 +56,10 @@ def init_db(db_path: str) -> sqlite3.Connection: 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'") + if "strategy_pnl" not in columns: + conn.execute("ALTER TABLE trades ADD COLUMN strategy_pnl REAL DEFAULT 0.0") + if "fx_pnl" not in columns: + conn.execute("ALTER TABLE trades ADD COLUMN fx_pnl REAL DEFAULT 0.0") # Context tree tables for multi-layered memory management conn.execute( @@ -171,6 +178,8 @@ def log_trade( quantity: int = 0, price: float = 0.0, pnl: float = 0.0, + strategy_pnl: float | None = None, + fx_pnl: float | None = None, market: str = "KR", exchange_code: str = "KRX", selection_context: dict[str, any] | None = None, @@ -187,7 +196,9 @@ def log_trade( rationale: AI decision rationale quantity: Number of shares price: Trade price - pnl: Profit/loss + pnl: Total profit/loss (backward compatibility) + strategy_pnl: Strategy PnL component + fx_pnl: FX PnL component market: Market code exchange_code: Exchange code selection_context: Scanner selection data (RSI, volume_ratio, signal, score) @@ -196,15 +207,24 @@ def log_trade( """ # Serialize selection context to JSON context_json = json.dumps(selection_context) if selection_context else None + if strategy_pnl is None and fx_pnl is None: + strategy_pnl = pnl + fx_pnl = 0.0 + elif strategy_pnl is None: + strategy_pnl = pnl - float(fx_pnl or 0.0) + elif fx_pnl is None: + fx_pnl = pnl - float(strategy_pnl) + if pnl == 0.0 and (strategy_pnl or fx_pnl): + pnl = float(strategy_pnl) + float(fx_pnl) conn.execute( """ INSERT INTO trades ( timestamp, stock_code, action, confidence, rationale, - quantity, price, pnl, market, exchange_code, selection_context, decision_id, - mode + quantity, price, pnl, strategy_pnl, fx_pnl, + market, exchange_code, selection_context, decision_id, mode ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( datetime.now(UTC).isoformat(), @@ -215,6 +235,8 @@ def log_trade( quantity, price, pnl, + strategy_pnl, + fx_pnl, market, exchange_code, context_json, diff --git a/tests/test_db.py b/tests/test_db.py index ead224a..9705ca2 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -155,6 +155,8 @@ def test_mode_column_exists_in_schema() -> None: cursor = conn.execute("PRAGMA table_info(trades)") columns = {row[1] for row in cursor.fetchall()} assert "mode" in columns + assert "strategy_pnl" in columns + assert "fx_pnl" in columns def test_mode_migration_adds_column_to_existing_db() -> None: @@ -190,6 +192,52 @@ def test_mode_migration_adds_column_to_existing_db() -> None: cursor = conn.execute("PRAGMA table_info(trades)") columns = {row[1] for row in cursor.fetchall()} assert "mode" in columns + assert "strategy_pnl" in columns + assert "fx_pnl" in columns conn.close() finally: os.unlink(db_path) + + +def test_log_trade_stores_strategy_and_fx_pnl_separately() -> None: + conn = init_db(":memory:") + log_trade( + conn=conn, + stock_code="AAPL", + action="SELL", + confidence=90, + rationale="fx split", + pnl=120.0, + strategy_pnl=100.0, + fx_pnl=20.0, + market="US_NASDAQ", + exchange_code="NASD", + ) + row = conn.execute( + "SELECT pnl, strategy_pnl, fx_pnl FROM trades ORDER BY id DESC LIMIT 1" + ).fetchone() + assert row is not None + assert row[0] == 120.0 + assert row[1] == 100.0 + assert row[2] == 20.0 + + +def test_log_trade_backward_compat_sets_strategy_pnl_from_pnl() -> None: + conn = init_db(":memory:") + log_trade( + conn=conn, + stock_code="005930", + action="SELL", + confidence=80, + rationale="legacy", + pnl=50.0, + market="KR", + exchange_code="KRX", + ) + row = conn.execute( + "SELECT pnl, strategy_pnl, fx_pnl FROM trades ORDER BY id DESC LIMIT 1" + ).fetchone() + assert row is not None + assert row[0] == 50.0 + assert row[1] == 50.0 + assert row[2] == 0.0 From 34cf081c961883a224377313b39a2ecaad1a35c7 Mon Sep 17 00:00:00 2001 From: agentson Date: Fri, 27 Feb 2026 08:46:22 +0900 Subject: [PATCH 2/2] fix: backfill split pnl migration and harden partial pnl inputs --- src/db.py | 14 ++++++++++++-- tests/test_db.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/db.py b/src/db.py index c8638c4..96fb1c7 100644 --- a/src/db.py +++ b/src/db.py @@ -60,6 +60,16 @@ def init_db(db_path: str) -> sqlite3.Connection: conn.execute("ALTER TABLE trades ADD COLUMN strategy_pnl REAL DEFAULT 0.0") if "fx_pnl" not in columns: conn.execute("ALTER TABLE trades ADD COLUMN fx_pnl REAL DEFAULT 0.0") + # Backfill legacy rows where only pnl existed before split accounting columns. + conn.execute( + """ + UPDATE trades + SET strategy_pnl = pnl, fx_pnl = 0.0 + WHERE pnl != 0.0 + AND strategy_pnl = 0.0 + AND fx_pnl = 0.0 + """ + ) # Context tree tables for multi-layered memory management conn.execute( @@ -211,9 +221,9 @@ def log_trade( strategy_pnl = pnl fx_pnl = 0.0 elif strategy_pnl is None: - strategy_pnl = pnl - float(fx_pnl or 0.0) + strategy_pnl = pnl - float(fx_pnl or 0.0) if pnl != 0.0 else 0.0 elif fx_pnl is None: - fx_pnl = pnl - float(strategy_pnl) + fx_pnl = pnl - float(strategy_pnl) if pnl != 0.0 else 0.0 if pnl == 0.0 and (strategy_pnl or fx_pnl): pnl = float(strategy_pnl) + float(fx_pnl) diff --git a/tests/test_db.py b/tests/test_db.py index 9705ca2..49b822c 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -184,6 +184,13 @@ def test_mode_migration_adds_column_to_existing_db() -> None: decision_id TEXT )""" ) + old_conn.execute( + """ + INSERT INTO trades ( + timestamp, stock_code, action, confidence, rationale, quantity, price, pnl + ) VALUES ('2026-01-01T00:00:00+00:00', 'AAPL', 'SELL', 90, 'legacy', 1, 100.0, 123.45) + """ + ) old_conn.commit() old_conn.close() @@ -194,6 +201,13 @@ def test_mode_migration_adds_column_to_existing_db() -> None: assert "mode" in columns assert "strategy_pnl" in columns assert "fx_pnl" in columns + migrated = conn.execute( + "SELECT pnl, strategy_pnl, fx_pnl FROM trades WHERE stock_code='AAPL' LIMIT 1" + ).fetchone() + assert migrated is not None + assert migrated[0] == 123.45 + assert migrated[1] == 123.45 + assert migrated[2] == 0.0 conn.close() finally: os.unlink(db_path) @@ -241,3 +255,25 @@ def test_log_trade_backward_compat_sets_strategy_pnl_from_pnl() -> None: assert row[0] == 50.0 assert row[1] == 50.0 assert row[2] == 0.0 + + +def test_log_trade_partial_fx_input_does_not_infer_negative_strategy_pnl() -> None: + conn = init_db(":memory:") + log_trade( + conn=conn, + stock_code="AAPL", + action="SELL", + confidence=70, + rationale="fx only", + pnl=0.0, + fx_pnl=10.0, + market="US_NASDAQ", + exchange_code="NASD", + ) + row = conn.execute( + "SELECT pnl, strategy_pnl, fx_pnl FROM trades ORDER BY id DESC LIMIT 1" + ).fetchone() + assert row is not None + assert row[0] == 10.0 + assert row[1] == 0.0 + assert row[2] == 10.0