Files
The-Ouroboros/tests/test_daily_review.py
agentson e2275a23b1
Some checks failed
CI / test (pull_request) Has been cancelled
fix: daily_review 테스트에서 날짜 불일치로 인한 실패 수정 (#129)
DecisionLogger와 log_trade가 datetime.now(UTC)로 현재 날짜를 저장하는데,
테스트에서 하드코딩된 '2026-02-14'로 조회하여 0건이 반환되던 문제 수정.
generate_scorecard 호출 시 TODAY 변수를 사용하도록 변경.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:05:17 +09:00

388 lines
10 KiB
Python

"""Tests for DailyReviewer."""
from __future__ import annotations
import json
import sqlite3
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
import pytest
from src.context.layer import ContextLayer
from src.context.store import ContextStore
from src.db import init_db, log_trade
from src.evolution.daily_review import DailyReviewer
from src.evolution.scorecard import DailyScorecard
from src.logging.decision_logger import DecisionLogger
from datetime import UTC, datetime
TODAY = datetime.now(UTC).strftime("%Y-%m-%d")
@pytest.fixture
def db_conn() -> sqlite3.Connection:
return init_db(":memory:")
@pytest.fixture
def context_store(db_conn: sqlite3.Connection) -> ContextStore:
return ContextStore(db_conn)
def _log_decision(
logger: DecisionLogger,
*,
stock_code: str,
market: str,
action: str,
confidence: int,
scenario_match: dict[str, float] | None = None,
) -> str:
return logger.log_decision(
stock_code=stock_code,
market=market,
exchange_code="KRX" if market == "KR" else "NASDAQ",
action=action,
confidence=confidence,
rationale="test",
context_snapshot={"scenario_match": scenario_match or {}},
input_data={"stock_code": stock_code},
)
def test_generate_scorecard_market_scoped(
db_conn: sqlite3.Connection, context_store: ContextStore,
) -> None:
reviewer = DailyReviewer(db_conn, context_store)
logger = DecisionLogger(db_conn)
buy_id = _log_decision(
logger,
stock_code="005930",
market="KR",
action="BUY",
confidence=90,
scenario_match={"rsi": 29.0},
)
_log_decision(
logger,
stock_code="000660",
market="KR",
action="HOLD",
confidence=60,
)
_log_decision(
logger,
stock_code="AAPL",
market="US",
action="SELL",
confidence=80,
scenario_match={"volume_ratio": 2.1},
)
log_trade(
db_conn,
"005930",
"BUY",
90,
"buy",
quantity=1,
price=100.0,
pnl=10.0,
market="KR",
exchange_code="KRX",
decision_id=buy_id,
)
log_trade(
db_conn,
"000660",
"HOLD",
60,
"hold",
quantity=0,
price=0.0,
pnl=0.0,
market="KR",
exchange_code="KRX",
)
log_trade(
db_conn,
"AAPL",
"SELL",
80,
"sell",
quantity=1,
price=200.0,
pnl=-5.0,
market="US",
exchange_code="NASDAQ",
)
scorecard = reviewer.generate_scorecard(TODAY, "KR")
assert scorecard.market == "KR"
assert scorecard.total_decisions == 2
assert scorecard.buys == 1
assert scorecard.sells == 0
assert scorecard.holds == 1
assert scorecard.total_pnl == 10.0
assert scorecard.win_rate == 100.0
assert scorecard.avg_confidence == 75.0
assert scorecard.scenario_match_rate == 50.0
def test_generate_scorecard_top_winners_and_losers(
db_conn: sqlite3.Connection, context_store: ContextStore,
) -> None:
reviewer = DailyReviewer(db_conn, context_store)
logger = DecisionLogger(db_conn)
for code, pnl in [("005930", 30.0), ("000660", 10.0), ("035420", -15.0), ("051910", -5.0)]:
decision_id = _log_decision(
logger,
stock_code=code,
market="KR",
action="BUY" if pnl >= 0 else "SELL",
confidence=80,
scenario_match={"rsi": 30.0},
)
log_trade(
db_conn,
code,
"BUY" if pnl >= 0 else "SELL",
80,
"test",
quantity=1,
price=100.0,
pnl=pnl,
market="KR",
exchange_code="KRX",
decision_id=decision_id,
)
scorecard = reviewer.generate_scorecard(TODAY, "KR")
assert scorecard.top_winners == ["005930", "000660"]
assert scorecard.top_losers == ["035420", "051910"]
def test_generate_scorecard_empty_day(
db_conn: sqlite3.Connection, context_store: ContextStore,
) -> None:
reviewer = DailyReviewer(db_conn, context_store)
scorecard = reviewer.generate_scorecard(TODAY, "KR")
assert scorecard.total_decisions == 0
assert scorecard.total_pnl == 0.0
assert scorecard.win_rate == 0.0
assert scorecard.avg_confidence == 0.0
assert scorecard.scenario_match_rate == 0.0
assert scorecard.top_winners == []
assert scorecard.top_losers == []
@pytest.mark.asyncio
async def test_generate_lessons_without_gemini_returns_empty(
db_conn: sqlite3.Connection, context_store: ContextStore,
) -> None:
reviewer = DailyReviewer(db_conn, context_store, gemini_client=None)
lessons = await reviewer.generate_lessons(
DailyScorecard(
date="2026-02-14",
market="KR",
total_decisions=1,
buys=1,
sells=0,
holds=0,
total_pnl=5.0,
win_rate=100.0,
avg_confidence=90.0,
scenario_match_rate=100.0,
)
)
assert lessons == []
@pytest.mark.asyncio
async def test_generate_lessons_parses_json_array(
db_conn: sqlite3.Connection, context_store: ContextStore,
) -> None:
mock_gemini = MagicMock()
mock_gemini.decide = AsyncMock(
return_value=SimpleNamespace(rationale='["Cut losers earlier", "Reduce midday churn"]')
)
reviewer = DailyReviewer(db_conn, context_store, gemini_client=mock_gemini)
lessons = await reviewer.generate_lessons(
DailyScorecard(
date="2026-02-14",
market="KR",
total_decisions=3,
buys=1,
sells=1,
holds=1,
total_pnl=-2.5,
win_rate=50.0,
avg_confidence=70.0,
scenario_match_rate=66.7,
)
)
assert lessons == ["Cut losers earlier", "Reduce midday churn"]
@pytest.mark.asyncio
async def test_generate_lessons_fallback_to_lines(
db_conn: sqlite3.Connection, context_store: ContextStore,
) -> None:
mock_gemini = MagicMock()
mock_gemini.decide = AsyncMock(
return_value=SimpleNamespace(rationale="- Keep risk tighter\n- Increase selectivity")
)
reviewer = DailyReviewer(db_conn, context_store, gemini_client=mock_gemini)
lessons = await reviewer.generate_lessons(
DailyScorecard(
date="2026-02-14",
market="US",
total_decisions=2,
buys=1,
sells=1,
holds=0,
total_pnl=1.0,
win_rate=50.0,
avg_confidence=75.0,
scenario_match_rate=100.0,
)
)
assert lessons == ["Keep risk tighter", "Increase selectivity"]
@pytest.mark.asyncio
async def test_generate_lessons_handles_gemini_error(
db_conn: sqlite3.Connection, context_store: ContextStore,
) -> None:
mock_gemini = MagicMock()
mock_gemini.decide = AsyncMock(side_effect=RuntimeError("boom"))
reviewer = DailyReviewer(db_conn, context_store, gemini_client=mock_gemini)
lessons = await reviewer.generate_lessons(
DailyScorecard(
date="2026-02-14",
market="US",
total_decisions=0,
buys=0,
sells=0,
holds=0,
total_pnl=0.0,
win_rate=0.0,
avg_confidence=0.0,
scenario_match_rate=0.0,
)
)
assert lessons == []
def test_store_scorecard_in_context(
db_conn: sqlite3.Connection, context_store: ContextStore,
) -> None:
reviewer = DailyReviewer(db_conn, context_store)
scorecard = DailyScorecard(
date="2026-02-14",
market="KR",
total_decisions=5,
buys=2,
sells=1,
holds=2,
total_pnl=15.0,
win_rate=66.67,
avg_confidence=82.0,
scenario_match_rate=80.0,
lessons=["Keep position sizing stable"],
cross_market_note="US risk-off",
)
reviewer.store_scorecard_in_context(scorecard)
stored = context_store.get_context(
ContextLayer.L6_DAILY,
"2026-02-14",
"scorecard_KR",
)
assert stored is not None
assert stored["market"] == "KR"
assert stored["total_pnl"] == 15.0
assert stored["lessons"] == ["Keep position sizing stable"]
def test_store_scorecard_key_is_market_scoped(
db_conn: sqlite3.Connection, context_store: ContextStore,
) -> None:
reviewer = DailyReviewer(db_conn, context_store)
kr = DailyScorecard(
date="2026-02-14",
market="KR",
total_decisions=1,
buys=1,
sells=0,
holds=0,
total_pnl=1.0,
win_rate=100.0,
avg_confidence=90.0,
scenario_match_rate=100.0,
)
us = DailyScorecard(
date="2026-02-14",
market="US",
total_decisions=1,
buys=0,
sells=1,
holds=0,
total_pnl=-1.0,
win_rate=0.0,
avg_confidence=70.0,
scenario_match_rate=100.0,
)
reviewer.store_scorecard_in_context(kr)
reviewer.store_scorecard_in_context(us)
kr_ctx = context_store.get_context(ContextLayer.L6_DAILY, "2026-02-14", "scorecard_KR")
us_ctx = context_store.get_context(ContextLayer.L6_DAILY, "2026-02-14", "scorecard_US")
assert kr_ctx["market"] == "KR"
assert us_ctx["market"] == "US"
assert kr_ctx["total_pnl"] == 1.0
assert us_ctx["total_pnl"] == -1.0
def test_generate_scorecard_handles_invalid_context_snapshot(
db_conn: sqlite3.Connection, context_store: ContextStore,
) -> None:
reviewer = DailyReviewer(db_conn, context_store)
db_conn.execute(
"""
INSERT INTO decision_logs (
decision_id, timestamp, stock_code, market, exchange_code,
action, confidence, rationale, context_snapshot, input_data
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
"d1",
"2026-02-14T09:00:00+00:00",
"005930",
"KR",
"KRX",
"HOLD",
50,
"test",
"{invalid_json",
json.dumps({}),
),
)
db_conn.commit()
scorecard = reviewer.generate_scorecard("2026-02-14", "KR")
assert scorecard.total_decisions == 1
assert scorecard.scenario_match_rate == 0.0