Compare commits

...

4 Commits

Author SHA1 Message Date
agentson
e2275a23b1 fix: daily_review 테스트에서 날짜 불일치로 인한 실패 수정 (#129)
Some checks failed
CI / test (pull_request) Has been cancelled
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
7522bb7e66 Merge pull request 'feat: 대시보드 실행 통합 - CLI + 환경변수 (issue #97)' (#128) from feature/issue-97-dashboard-integration into main
Some checks failed
CI / test (push) Has been cancelled
Reviewed-on: #128
2026-02-15 00:01:57 +09:00
agentson
63fa6841a2 feat: dashboard background thread with CLI flag (issue #97)
Some checks failed
CI / test (pull_request) Has been cancelled
Add --dashboard CLI flag and DASHBOARD_ENABLED env var to start
FastAPI dashboard in a daemon thread alongside the trading loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 00:01:29 +09:00
ece3c5597b Merge pull request 'feat: FastAPI 읽기 전용 대시보드 (issue #96)' (#127) from feature/issue-96-evolution-main-integration into main
Some checks failed
CI / test (push) Has been cancelled
Reviewed-on: #127
2026-02-14 23:57:17 +09:00
5 changed files with 107 additions and 3 deletions

View File

@@ -10,6 +10,7 @@ dependencies = [
"google-genai>=1.0,<2", "google-genai>=1.0,<2",
"scipy>=1.11,<2", "scipy>=1.11,<2",
"fastapi>=0.110,<1", "fastapi>=0.110,<1",
"uvicorn>=0.29,<1",
] ]
[project.optional-dependencies] [project.optional-dependencies]

View File

@@ -83,6 +83,11 @@ class Settings(BaseSettings):
TELEGRAM_COMMANDS_ENABLED: bool = True TELEGRAM_COMMANDS_ENABLED: bool = True
TELEGRAM_POLLING_INTERVAL: float = 1.0 # seconds TELEGRAM_POLLING_INTERVAL: float = 1.0 # seconds
# Dashboard (optional)
DASHBOARD_ENABLED: bool = False
DASHBOARD_HOST: str = "127.0.0.1"
DASHBOARD_PORT: int = Field(default=8080, ge=1, le=65535)
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"} model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
@property @property

View File

@@ -10,6 +10,7 @@ import argparse
import asyncio import asyncio
import logging import logging
import signal import signal
import threading
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import Any from typing import Any
@@ -844,6 +845,48 @@ async def _run_evolution_loop(
logger.warning("Evolution notification failed on %s: %s", market_date, exc) logger.warning("Evolution notification failed on %s: %s", market_date, exc)
def _start_dashboard_server(settings: Settings) -> threading.Thread | None:
"""Start FastAPI dashboard in a daemon thread when enabled."""
if not settings.DASHBOARD_ENABLED:
return None
def _serve() -> None:
try:
import uvicorn
from src.dashboard import create_dashboard_app
app = create_dashboard_app(settings.DB_PATH)
uvicorn.run(
app,
host=settings.DASHBOARD_HOST,
port=settings.DASHBOARD_PORT,
log_level="info",
)
except Exception as exc:
logger.warning("Dashboard server failed to start: %s", exc)
thread = threading.Thread(
target=_serve,
name="dashboard-server",
daemon=True,
)
thread.start()
logger.info(
"Dashboard server started at http://%s:%d",
settings.DASHBOARD_HOST,
settings.DASHBOARD_PORT,
)
return thread
def _apply_dashboard_flag(settings: Settings, dashboard_flag: bool) -> Settings:
"""Apply CLI dashboard flag over environment settings."""
if dashboard_flag and not settings.DASHBOARD_ENABLED:
return settings.model_copy(update={"DASHBOARD_ENABLED": True})
return settings
async def run(settings: Settings) -> None: async def run(settings: Settings) -> None:
"""Main async loop — iterate over open markets on a timer.""" """Main async loop — iterate over open markets on a timer."""
broker = KISBroker(settings) broker = KISBroker(settings)
@@ -1042,6 +1085,7 @@ async def run(settings: Settings) -> None:
low_volatility_threshold=30.0, low_volatility_threshold=30.0,
) )
priority_queue = PriorityTaskQueue(max_size=1000) priority_queue = PriorityTaskQueue(max_size=1000)
_start_dashboard_server(settings)
# Track last scan time for each market # Track last scan time for each market
last_scan_time: dict[str, float] = {} last_scan_time: dict[str, float] = {}
@@ -1395,10 +1439,16 @@ def main() -> None:
default="paper", default="paper",
help="Trading mode (default: paper)", help="Trading mode (default: paper)",
) )
parser.add_argument(
"--dashboard",
action="store_true",
help="Enable FastAPI dashboard server in background thread",
)
args = parser.parse_args() args = parser.parse_args()
setup_logging() setup_logging()
settings = Settings(MODE=args.mode) # type: ignore[call-arg] settings = Settings(MODE=args.mode) # type: ignore[call-arg]
settings = _apply_dashboard_flag(settings, args.dashboard)
asyncio.run(run(settings)) asyncio.run(run(settings))

View File

@@ -16,6 +16,10 @@ from src.evolution.daily_review import DailyReviewer
from src.evolution.scorecard import DailyScorecard from src.evolution.scorecard import DailyScorecard
from src.logging.decision_logger import DecisionLogger from src.logging.decision_logger import DecisionLogger
from datetime import UTC, datetime
TODAY = datetime.now(UTC).strftime("%Y-%m-%d")
@pytest.fixture @pytest.fixture
def db_conn() -> sqlite3.Connection: def db_conn() -> sqlite3.Connection:
@@ -116,7 +120,7 @@ def test_generate_scorecard_market_scoped(
exchange_code="NASDAQ", exchange_code="NASDAQ",
) )
scorecard = reviewer.generate_scorecard("2026-02-14", "KR") scorecard = reviewer.generate_scorecard(TODAY, "KR")
assert scorecard.market == "KR" assert scorecard.market == "KR"
assert scorecard.total_decisions == 2 assert scorecard.total_decisions == 2
@@ -158,7 +162,7 @@ def test_generate_scorecard_top_winners_and_losers(
decision_id=decision_id, decision_id=decision_id,
) )
scorecard = reviewer.generate_scorecard("2026-02-14", "KR") scorecard = reviewer.generate_scorecard(TODAY, "KR")
assert scorecard.top_winners == ["005930", "000660"] assert scorecard.top_winners == ["005930", "000660"]
assert scorecard.top_losers == ["035420", "051910"] assert scorecard.top_losers == ["035420", "051910"]
@@ -167,7 +171,7 @@ def test_generate_scorecard_empty_day(
db_conn: sqlite3.Connection, context_store: ContextStore, db_conn: sqlite3.Connection, context_store: ContextStore,
) -> None: ) -> None:
reviewer = DailyReviewer(db_conn, context_store) reviewer = DailyReviewer(db_conn, context_store)
scorecard = reviewer.generate_scorecard("2026-02-14", "KR") scorecard = reviewer.generate_scorecard(TODAY, "KR")
assert scorecard.total_decisions == 0 assert scorecard.total_decisions == 0
assert scorecard.total_pnl == 0.0 assert scorecard.total_pnl == 0.0

View File

@@ -5,6 +5,7 @@ from unittest.mock import ANY, AsyncMock, MagicMock, patch
import pytest import pytest
from src.config import Settings
from src.context.layer import ContextLayer from src.context.layer import ContextLayer
from src.context.scheduler import ScheduleResult from src.context.scheduler import ScheduleResult
from src.core.risk_manager import CircuitBreakerTripped, FatFingerRejected from src.core.risk_manager import CircuitBreakerTripped, FatFingerRejected
@@ -12,9 +13,11 @@ from src.db import init_db, log_trade
from src.evolution.scorecard import DailyScorecard from src.evolution.scorecard import DailyScorecard
from src.logging.decision_logger import DecisionLogger from src.logging.decision_logger import DecisionLogger
from src.main import ( from src.main import (
_apply_dashboard_flag,
_handle_market_close, _handle_market_close,
_run_context_scheduler, _run_context_scheduler,
_run_evolution_loop, _run_evolution_loop,
_start_dashboard_server,
safe_float, safe_float,
trading_cycle, trading_cycle,
) )
@@ -1454,3 +1457,44 @@ async def test_run_evolution_loop_notification_error_is_ignored() -> None:
optimizer.evolve.assert_called_once() optimizer.evolve.assert_called_once()
telegram.send_message.assert_called_once() telegram.send_message.assert_called_once()
def test_apply_dashboard_flag_enables_dashboard() -> None:
settings = Settings(
KIS_APP_KEY="test_key",
KIS_APP_SECRET="test_secret",
KIS_ACCOUNT_NO="12345678-01",
GEMINI_API_KEY="test_gemini_key",
DASHBOARD_ENABLED=False,
)
updated = _apply_dashboard_flag(settings, dashboard_flag=True)
assert updated.DASHBOARD_ENABLED is True
def test_start_dashboard_server_disabled_returns_none() -> None:
settings = Settings(
KIS_APP_KEY="test_key",
KIS_APP_SECRET="test_secret",
KIS_ACCOUNT_NO="12345678-01",
GEMINI_API_KEY="test_gemini_key",
DASHBOARD_ENABLED=False,
)
thread = _start_dashboard_server(settings)
assert thread is None
def test_start_dashboard_server_enabled_starts_thread() -> None:
settings = Settings(
KIS_APP_KEY="test_key",
KIS_APP_SECRET="test_secret",
KIS_ACCOUNT_NO="12345678-01",
GEMINI_API_KEY="test_gemini_key",
DASHBOARD_ENABLED=True,
)
mock_thread = MagicMock()
with patch("src.main.threading.Thread", return_value=mock_thread) as mock_thread_cls:
thread = _start_dashboard_server(settings)
assert thread == mock_thread
mock_thread_cls.assert_called_once()
mock_thread.start.assert_called_once()