Containerised macro-strategy dashboard: 4-panel web UI (indicators, portfolio, flash news, AI strategic log), MariaDB store, hourly ingestion jobs, OpenRouter-backed AI analysis. Ports the four prototype scripts in the parent dir (market_pulse, flash_news, trading212, strategic_log) into async services backed by a persistent DB and served via FastAPI + Jinja2 + HTMX. APScheduler runs as a separate compose service for crash-safety and easier restarts. Portfolio composition + position names come live from Trading 212; news per-ticker headlines reuse those names. Tone (NOVICE/INTERMEDIATE/ PRO) and analysis style (DRY/SPECULATIVE) are env-configurable and stored on each log row so historical entries show what produced them. Default model is deepseek/deepseek-v4-flash (overridable via env). Light/dark theme toggle, sans-serif for prose surfaces, monospace for data. Bearer-token auth, OpenRouter monthly cost cap, RSS feeds auto- disabled on consecutive failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
47 lines
1.3 KiB
Python
47 lines
1.3 KiB
Python
"""Pure-function tests for app.services.market."""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
pytest.importorskip("httpx")
|
|
pytest.importorskip("pydantic_settings")
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from app.services.market import _pct, _parse_date, _yahoo_range_covering, parse_symbol
|
|
|
|
|
|
def test_pct_basic():
|
|
assert _pct(100, 110) == 10.0
|
|
assert _pct(100, 90) == -10.0
|
|
|
|
|
|
def test_pct_handles_none_and_zero():
|
|
assert _pct(None, 10) is None
|
|
assert _pct(10, None) is None
|
|
assert _pct(0, 5) is None
|
|
|
|
|
|
def test_parse_date():
|
|
assert _parse_date("2026-03-04") == datetime(2026, 3, 4)
|
|
|
|
|
|
def test_yahoo_range_covering_picks_smallest():
|
|
today = datetime.now(timezone.utc).date()
|
|
# anchor 100 days ago → 1y range is enough
|
|
short = (today.replace(year=today.year)).isoformat()
|
|
assert _yahoo_range_covering(None) == "1y"
|
|
assert _yahoo_range_covering("2026-01-01") == "1y"
|
|
|
|
|
|
def test_parse_symbol_routes_by_prefix():
|
|
fn, ident = parse_symbol("FRED:DFF")
|
|
assert ident == "DFF"
|
|
assert fn.__name__ == "fetch_fred"
|
|
fn2, ident2 = parse_symbol("AAPL")
|
|
assert ident2 == "AAPL"
|
|
assert fn2.__name__ == "fetch_yahoo"
|
|
# Unknown prefix falls through to yahoo.
|
|
fn3, ident3 = parse_symbol("UNKNOWN:XYZ")
|
|
assert ident3 == "UNKNOWN:XYZ"
|
|
assert fn3.__name__ == "fetch_yahoo"
|