Add configurable data providers and localize frontend UI
This commit is contained in:
@@ -25,7 +25,7 @@ class TestAnalystAgent:
|
||||
)
|
||||
|
||||
assert agent.analyst_type_key == "technical_analyst"
|
||||
assert agent.name == "technical_analyst_analyst"
|
||||
assert agent.name == "technical_analyst"
|
||||
assert agent.analyst_persona == "Technical Analyst"
|
||||
|
||||
def test_init_invalid_analyst_type(self):
|
||||
|
||||
10
backend/tests/test_analysis_tools.py
Normal file
10
backend/tests/test_analysis_tools.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from backend.tools.analysis_tools import _resolved_date
|
||||
|
||||
|
||||
def test_resolved_date_clamps_future_date():
|
||||
future_date = (datetime.today() + timedelta(days=2)).strftime("%Y-%m-%d")
|
||||
|
||||
assert _resolved_date(future_date) == datetime.today().strftime("%Y-%m-%d")
|
||||
55
backend/tests/test_data_config.py
Normal file
55
backend/tests/test_data_config.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for data source config ordering."""
|
||||
|
||||
from backend.config.data_config import get_config, reset_config
|
||||
|
||||
|
||||
def test_data_config_prefers_env_source(monkeypatch):
|
||||
monkeypatch.setenv("FIN_DATA_SOURCE", "financial_datasets")
|
||||
monkeypatch.setenv("FINNHUB_API_KEY", "fh")
|
||||
monkeypatch.setenv("FINANCIAL_DATASETS_API_KEY", "fd")
|
||||
reset_config()
|
||||
|
||||
config = get_config()
|
||||
|
||||
assert config.sources[0] == "financial_datasets"
|
||||
assert "local_csv" in config.sources
|
||||
|
||||
|
||||
def test_enabled_data_sources_filters_available_sources(monkeypatch):
|
||||
monkeypatch.setenv("FINNHUB_API_KEY", "fh-key")
|
||||
monkeypatch.setenv("FINANCIAL_DATASETS_API_KEY", "fd-key")
|
||||
monkeypatch.setenv("ENABLED_DATA_SOURCES", "financial_datasets,local_csv")
|
||||
monkeypatch.delenv("FIN_DATA_SOURCE", raising=False)
|
||||
reset_config()
|
||||
|
||||
config = get_config()
|
||||
|
||||
assert config.sources == ["financial_datasets", "local_csv"]
|
||||
assert config.source == "financial_datasets"
|
||||
|
||||
|
||||
def test_preferred_source_reorders_enabled_sources(monkeypatch):
|
||||
monkeypatch.setenv("FINNHUB_API_KEY", "fh-key")
|
||||
monkeypatch.setenv("FINANCIAL_DATASETS_API_KEY", "fd-key")
|
||||
monkeypatch.setenv("ENABLED_DATA_SOURCES", "financial_datasets,finnhub,local_csv")
|
||||
monkeypatch.setenv("FIN_DATA_SOURCE", "finnhub")
|
||||
reset_config()
|
||||
|
||||
config = get_config()
|
||||
|
||||
assert config.sources == ["finnhub", "financial_datasets", "local_csv"]
|
||||
assert config.source == "finnhub"
|
||||
|
||||
|
||||
def test_yfinance_can_be_enabled_without_api_key(monkeypatch):
|
||||
monkeypatch.delenv("FINNHUB_API_KEY", raising=False)
|
||||
monkeypatch.delenv("FINANCIAL_DATASETS_API_KEY", raising=False)
|
||||
monkeypatch.setenv("FIN_DATA_SOURCE", "yfinance")
|
||||
monkeypatch.setenv("ENABLED_DATA_SOURCES", "yfinance,local_csv")
|
||||
reset_config()
|
||||
|
||||
config = get_config()
|
||||
|
||||
assert config.sources == ["yfinance", "local_csv"]
|
||||
assert config.source == "yfinance"
|
||||
25
backend/tests/test_env_config.py
Normal file
25
backend/tests/test_env_config.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for normalized env config helpers."""
|
||||
|
||||
from backend.config.env_config import (
|
||||
canonicalize_model_provider,
|
||||
get_agent_model_config,
|
||||
)
|
||||
|
||||
|
||||
def test_canonicalize_model_provider_aliases():
|
||||
assert canonicalize_model_provider("claude") == "ANTHROPIC"
|
||||
assert canonicalize_model_provider("openai_compatible") == "OPENAI"
|
||||
assert canonicalize_model_provider("google") == "GEMINI"
|
||||
|
||||
|
||||
def test_get_agent_model_config_fallback(monkeypatch):
|
||||
monkeypatch.delenv("AGENT_RISK_MANAGER_MODEL_NAME", raising=False)
|
||||
monkeypatch.delenv("AGENT_RISK_MANAGER_MODEL_PROVIDER", raising=False)
|
||||
monkeypatch.setenv("MODEL_NAME", "gpt-4o-mini")
|
||||
monkeypatch.setenv("MODEL_PROVIDER", "openai")
|
||||
|
||||
config = get_agent_model_config("risk_manager")
|
||||
|
||||
assert config.model_name == "gpt-4o-mini"
|
||||
assert config.provider == "OPENAI"
|
||||
@@ -157,6 +157,15 @@ class TestPollingPriceManager:
|
||||
|
||||
assert manager.api_key == "test_key"
|
||||
assert manager.poll_interval == 30
|
||||
assert manager.provider == "finnhub"
|
||||
assert manager.running is False
|
||||
|
||||
def test_init_yfinance(self):
|
||||
manager = PollingPriceManager(provider="yfinance", poll_interval=15)
|
||||
|
||||
assert manager.api_key is None
|
||||
assert manager.poll_interval == 15
|
||||
assert manager.provider == "yfinance"
|
||||
assert manager.running is False
|
||||
|
||||
def test_subscribe(self):
|
||||
@@ -182,7 +191,7 @@ class TestPollingPriceManager:
|
||||
assert callback in manager.price_callbacks
|
||||
|
||||
@patch.object(PollingPriceManager, "_fetch_prices")
|
||||
def test_start_stop(self):
|
||||
def test_start_stop(self, _mock_fetch_prices):
|
||||
manager = PollingPriceManager(api_key="test_key", poll_interval=1)
|
||||
manager.subscribe(["AAPL"])
|
||||
|
||||
@@ -246,6 +255,20 @@ class TestMarketService:
|
||||
assert service.mock_mode is False
|
||||
assert service.api_key == "test_key"
|
||||
|
||||
@patch("backend.services.market.get_data_source", return_value="yfinance")
|
||||
@patch.object(PollingPriceManager, "start")
|
||||
def test_start_real_mode_with_yfinance(self, _mock_start, _mock_source):
|
||||
service = MarketService(
|
||||
tickers=["AAPL"],
|
||||
poll_interval=10,
|
||||
mock_mode=False,
|
||||
)
|
||||
|
||||
service._start_real_mode()
|
||||
|
||||
assert isinstance(service._price_manager, PollingPriceManager)
|
||||
assert service._price_manager.provider == "yfinance"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_mock_mode(self):
|
||||
service = MarketService(
|
||||
@@ -264,8 +287,9 @@ class TestMarketService:
|
||||
|
||||
service.stop()
|
||||
|
||||
@patch("backend.services.market.get_data_source", return_value="finnhub")
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_real_mode_without_api_key(self):
|
||||
async def test_start_real_mode_without_api_key(self, _mock_source):
|
||||
service = MarketService(
|
||||
tickers=["AAPL"],
|
||||
mock_mode=False,
|
||||
|
||||
29
backend/tests/test_provider_router.py
Normal file
29
backend/tests/test_provider_router.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for provider router fallback behavior."""
|
||||
|
||||
from backend.data.provider_router import DataProviderRouter
|
||||
from backend.config.data_config import reset_config
|
||||
|
||||
|
||||
def test_router_includes_local_csv_fallback(monkeypatch):
|
||||
monkeypatch.delenv("FINNHUB_API_KEY", raising=False)
|
||||
monkeypatch.delenv("FINANCIAL_DATASETS_API_KEY", raising=False)
|
||||
monkeypatch.delenv("FIN_DATA_SOURCE", raising=False)
|
||||
reset_config()
|
||||
|
||||
router = DataProviderRouter()
|
||||
|
||||
assert router.price_sources() == ["local_csv"]
|
||||
|
||||
|
||||
def test_router_allows_yfinance_when_enabled(monkeypatch):
|
||||
monkeypatch.setenv("FIN_DATA_SOURCE", "yfinance")
|
||||
monkeypatch.setenv("ENABLED_DATA_SOURCES", "yfinance,local_csv")
|
||||
monkeypatch.delenv("FINNHUB_API_KEY", raising=False)
|
||||
monkeypatch.delenv("FINANCIAL_DATASETS_API_KEY", raising=False)
|
||||
reset_config()
|
||||
|
||||
router = DataProviderRouter()
|
||||
|
||||
assert router.price_sources() == ["yfinance", "local_csv"]
|
||||
assert router.api_sources() == ["yfinance"]
|
||||
15
backend/tests/test_provider_utils.py
Normal file
15
backend/tests/test_provider_utils.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for market symbol normalization helpers."""
|
||||
|
||||
from backend.data.provider_utils import describe_symbol, normalize_symbol
|
||||
|
||||
|
||||
def test_normalize_symbol_exchange_prefix():
|
||||
assert normalize_symbol("sh600519") == "600519"
|
||||
assert normalize_symbol("600519.SH") == "600519"
|
||||
|
||||
|
||||
def test_normalize_symbol_us_ticker():
|
||||
symbol = describe_symbol("aapl")
|
||||
assert symbol.canonical == "AAPL"
|
||||
assert symbol.market == "us"
|
||||
22
backend/tests/test_technical_signals.py
Normal file
22
backend/tests/test_technical_signals.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for structured technical analyzer."""
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from backend.tools.technical_signals import StockTechnicalAnalyzer
|
||||
|
||||
|
||||
def test_technical_analyzer_detects_bullish_trend():
|
||||
df = pd.DataFrame(
|
||||
{
|
||||
"time": pd.date_range("2024-01-01", periods=40, freq="D"),
|
||||
"close": [100 + i for i in range(40)],
|
||||
},
|
||||
)
|
||||
analyzer = StockTechnicalAnalyzer()
|
||||
|
||||
result = analyzer.analyze("AAPL", df)
|
||||
|
||||
assert result.current_price == 139.0
|
||||
assert result.trend in {"BULLISH", "STRONG BULLISH"}
|
||||
assert result.momentum_20d_pct > 0
|
||||
Reference in New Issue
Block a user