Compare commits
2 Commits
feature/is
...
feature/is
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63fa6841a2 | ||
| ece3c5597b |
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
50
src/main.py
50
src/main.py
@@ -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))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user