[FX-ACCOUNTING] TKT-P1-006 전략/환율 PnL 분리 회계 #296

Merged
agentson merged 2 commits from feature/issue-tkt-p1-006-fx-pnl-separation into feature/v3-session-policy-stream 2026-02-27 08:46:57 +09:00
2 changed files with 120 additions and 4 deletions

View File

@@ -31,8 +31,11 @@ def init_db(db_path: str) -> sqlite3.Connection:
quantity INTEGER, quantity INTEGER,
price REAL, price REAL,
pnl REAL DEFAULT 0.0, pnl REAL DEFAULT 0.0,
strategy_pnl REAL DEFAULT 0.0,
fx_pnl REAL DEFAULT 0.0,
market TEXT DEFAULT 'KR', market TEXT DEFAULT 'KR',
exchange_code TEXT DEFAULT 'KRX', exchange_code TEXT DEFAULT 'KRX',
selection_context TEXT,
decision_id TEXT, decision_id TEXT,
mode TEXT DEFAULT 'paper' mode TEXT DEFAULT 'paper'
) )
@@ -53,6 +56,20 @@ def init_db(db_path: str) -> sqlite3.Connection:
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: if "mode" not in columns:
conn.execute("ALTER TABLE trades ADD COLUMN mode TEXT DEFAULT 'paper'") 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")
# 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 # Context tree tables for multi-layered memory management
conn.execute( conn.execute(
@@ -171,6 +188,8 @@ def log_trade(
quantity: int = 0, quantity: int = 0,
price: float = 0.0, price: float = 0.0,
pnl: float = 0.0, pnl: float = 0.0,
strategy_pnl: float | None = None,
fx_pnl: float | None = None,
market: str = "KR", market: str = "KR",
exchange_code: str = "KRX", exchange_code: str = "KRX",
selection_context: dict[str, any] | None = None, selection_context: dict[str, any] | None = None,
@@ -187,7 +206,9 @@ def log_trade(
rationale: AI decision rationale rationale: AI decision rationale
quantity: Number of shares quantity: Number of shares
price: Trade price 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 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)
@@ -196,15 +217,24 @@ def log_trade(
""" """
# 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
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) if pnl != 0.0 else 0.0
elif fx_pnl is None:
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)
conn.execute( conn.execute(
""" """
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, strategy_pnl, fx_pnl,
mode market, exchange_code, selection_context, decision_id, mode
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
datetime.now(UTC).isoformat(), datetime.now(UTC).isoformat(),
@@ -215,6 +245,8 @@ def log_trade(
quantity, quantity,
price, price,
pnl, pnl,
strategy_pnl,
fx_pnl,
market, market,
exchange_code, exchange_code,
context_json, context_json,

View File

@@ -155,6 +155,8 @@ def test_mode_column_exists_in_schema() -> None:
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()}
assert "mode" in columns assert "mode" in columns
assert "strategy_pnl" in columns
assert "fx_pnl" in columns
def test_mode_migration_adds_column_to_existing_db() -> None: def test_mode_migration_adds_column_to_existing_db() -> None:
@@ -182,6 +184,13 @@ def test_mode_migration_adds_column_to_existing_db() -> None:
decision_id TEXT 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.commit()
old_conn.close() old_conn.close()
@@ -190,6 +199,81 @@ def test_mode_migration_adds_column_to_existing_db() -> None:
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()}
assert "mode" in columns 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() conn.close()
finally: finally:
os.unlink(db_path) 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
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