Add configurable data providers and localize frontend UI

This commit is contained in:
2026-03-15 00:55:12 +08:00
parent 12de93aa30
commit d233a3f55d
38 changed files with 1936 additions and 1038 deletions

View File

@@ -17,6 +17,7 @@ from backend.core.pipeline import TradingPipeline
from backend.core.state_sync import StateSync
from backend.services.market import MarketService
from backend.services.storage import StorageService
from backend.data.provider_router import get_provider_router
logger = logging.getLogger(__name__)
@@ -60,10 +61,14 @@ class Gateway:
# Session tracking for live returns
self._session_start_portfolio_value: Optional[float] = None
self._provider_router = get_provider_router()
self._loop: Optional[asyncio.AbstractEventLoop] = None
async def start(self, host: str = "0.0.0.0", port: int = 8766):
"""Start gateway server"""
logger.info(f"Starting gateway on {host}:{port}")
self._loop = asyncio.get_running_loop()
self._provider_router.add_listener(self._on_provider_usage_changed)
# Initialize terminal dashboard
self._dashboard.set_config(
@@ -77,6 +82,7 @@ class Gateway:
initial_cash=self.storage.initial_cash,
start_date=self._backtest_start_date or "",
end_date=self._backtest_end_date or "",
data_sources=self._provider_router.get_usage_snapshot(),
)
self._dashboard.start()
@@ -88,6 +94,10 @@ class Gateway:
"is_mock_mode",
self.config.get("mock_mode", False),
)
self.state_sync.update_state(
"data_sources",
self._provider_router.get_usage_snapshot(),
)
# Load and display existing portfolio state if available
summary = self.storage.load_file("summary")
@@ -130,6 +140,21 @@ class Gateway:
)
await asyncio.Future()
def _on_provider_usage_changed(self, snapshot: Dict[str, Any]):
"""Handle provider routing updates from the shared router."""
self.state_sync.update_state("data_sources", snapshot)
self._dashboard.update(data_sources=snapshot)
if self._loop and self._loop.is_running():
asyncio.run_coroutine_threadsafe(
self.broadcast(
{
"type": "data_sources_update",
"data_sources": snapshot,
},
),
self._loop,
)
@property
def state(self) -> Dict[str, Any]:
return self.state_sync.state
@@ -149,6 +174,9 @@ class Gateway:
state_payload = self.state_sync.get_initial_state_payload(
include_dashboard=True,
)
state_payload["data_sources"] = (
self._provider_router.get_usage_snapshot()
)
# Include market status in initial state
state_payload[
"market_status"

View File

@@ -10,6 +10,8 @@ from typing import Any, Callable, Dict, List, Optional
from zoneinfo import ZoneInfo
import pandas_market_calendars as mcal
from backend.config.data_config import get_data_source
from backend.data.provider_utils import normalize_symbol
logger = logging.getLogger(__name__)
@@ -40,7 +42,7 @@ class MarketService:
backtest_start_date: Optional[str] = None,
backtest_end_date: Optional[str] = None,
):
self.tickers = tickers
self.tickers = [normalize_symbol(ticker) for ticker in tickers]
self.poll_interval = poll_interval
self.mock_mode = mock_mode
self.backtest_mode = backtest_mode
@@ -123,11 +125,16 @@ class MarketService:
def _start_real_mode(self):
from backend.data.polling_price_manager import PollingPriceManager
if not self.api_key:
provider = get_data_source()
if provider == "local_csv":
provider = "yfinance"
if provider == "finnhub" and not self.api_key:
raise ValueError("API key required for live mode")
self._price_manager = PollingPriceManager(
api_key=self.api_key,
poll_interval=self.poll_interval,
provider=provider,
)
self._price_manager.add_price_callback(self._make_price_callback())
self._price_manager.subscribe(self.tickers)