Some checks failed
CI / test (pull_request) Has been cancelled
Add observability dashboard: status, playbook, scorecard, performance, context browser, decisions, and active scenarios endpoints. SQLite read-only on separate connections from trading loop. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
271 lines
7.6 KiB
Python
271 lines
7.6 KiB
Python
"""Tests for FastAPI dashboard endpoints."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
pytest.importorskip("fastapi")
|
|
from fastapi.testclient import TestClient
|
|
|
|
from src.dashboard.app import create_dashboard_app
|
|
from src.db import init_db
|
|
|
|
|
|
def _seed_db(conn: sqlite3.Connection) -> None:
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO playbooks (
|
|
date, market, status, playbook_json, generated_at,
|
|
token_count, scenario_count, match_count
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
"2026-02-14",
|
|
"KR",
|
|
"ready",
|
|
json.dumps({"market": "KR", "stock_playbooks": []}),
|
|
"2026-02-14T08:30:00+00:00",
|
|
123,
|
|
2,
|
|
1,
|
|
),
|
|
)
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO contexts (layer, timeframe, key, value, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
"L6_DAILY",
|
|
"2026-02-14",
|
|
"scorecard_KR",
|
|
json.dumps({"market": "KR", "total_pnl": 1.5, "win_rate": 60.0}),
|
|
"2026-02-14T15:30:00+00:00",
|
|
"2026-02-14T15:30:00+00:00",
|
|
),
|
|
)
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO contexts (layer, timeframe, key, value, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
"L7_REALTIME",
|
|
"2026-02-14T10:00:00+00:00",
|
|
"volatility_KR_005930",
|
|
json.dumps({"momentum_score": 70.0}),
|
|
"2026-02-14T10:00:00+00:00",
|
|
"2026-02-14T10:00:00+00:00",
|
|
),
|
|
)
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO decision_logs (
|
|
decision_id, timestamp, stock_code, market, exchange_code,
|
|
action, confidence, rationale, context_snapshot, input_data
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
"d-kr-1",
|
|
"2026-02-14T09:10:00+00:00",
|
|
"005930",
|
|
"KR",
|
|
"KRX",
|
|
"BUY",
|
|
85,
|
|
"signal matched",
|
|
json.dumps({"scenario_match": {"rsi": 28.0}}),
|
|
json.dumps({"current_price": 70000}),
|
|
),
|
|
)
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO decision_logs (
|
|
decision_id, timestamp, stock_code, market, exchange_code,
|
|
action, confidence, rationale, context_snapshot, input_data
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
"d-us-1",
|
|
"2026-02-14T21:10:00+00:00",
|
|
"AAPL",
|
|
"US",
|
|
"NASDAQ",
|
|
"SELL",
|
|
80,
|
|
"no match",
|
|
json.dumps({"scenario_match": {}}),
|
|
json.dumps({"current_price": 200}),
|
|
),
|
|
)
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO trades (
|
|
timestamp, stock_code, action, confidence, rationale,
|
|
quantity, price, pnl, market, exchange_code, selection_context, decision_id
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
"2026-02-14T09:11:00+00:00",
|
|
"005930",
|
|
"BUY",
|
|
85,
|
|
"buy",
|
|
1,
|
|
70000,
|
|
2.0,
|
|
"KR",
|
|
"KRX",
|
|
None,
|
|
"d-kr-1",
|
|
),
|
|
)
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO trades (
|
|
timestamp, stock_code, action, confidence, rationale,
|
|
quantity, price, pnl, market, exchange_code, selection_context, decision_id
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
"2026-02-14T21:11:00+00:00",
|
|
"AAPL",
|
|
"SELL",
|
|
80,
|
|
"sell",
|
|
1,
|
|
200,
|
|
-1.0,
|
|
"US",
|
|
"NASDAQ",
|
|
None,
|
|
"d-us-1",
|
|
),
|
|
)
|
|
conn.commit()
|
|
|
|
|
|
def _client(tmp_path: Path) -> TestClient:
|
|
db_path = tmp_path / "dashboard_test.db"
|
|
conn = init_db(str(db_path))
|
|
_seed_db(conn)
|
|
conn.close()
|
|
app = create_dashboard_app(str(db_path))
|
|
return TestClient(app)
|
|
|
|
|
|
def test_index_serves_html(tmp_path: Path) -> None:
|
|
client = _client(tmp_path)
|
|
resp = client.get("/")
|
|
assert resp.status_code == 200
|
|
assert "The Ouroboros Dashboard API" in resp.text
|
|
|
|
|
|
def test_status_endpoint(tmp_path: Path) -> None:
|
|
client = _client(tmp_path)
|
|
resp = client.get("/api/status")
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert "KR" in body["markets"]
|
|
assert "US" in body["markets"]
|
|
assert "totals" in body
|
|
|
|
|
|
def test_playbook_found(tmp_path: Path) -> None:
|
|
client = _client(tmp_path)
|
|
resp = client.get("/api/playbook/2026-02-14?market=KR")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["market"] == "KR"
|
|
|
|
|
|
def test_playbook_not_found(tmp_path: Path) -> None:
|
|
client = _client(tmp_path)
|
|
resp = client.get("/api/playbook/2026-02-15?market=KR")
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_scorecard_found(tmp_path: Path) -> None:
|
|
client = _client(tmp_path)
|
|
resp = client.get("/api/scorecard/2026-02-14?market=KR")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["scorecard"]["total_pnl"] == 1.5
|
|
|
|
|
|
def test_scorecard_not_found(tmp_path: Path) -> None:
|
|
client = _client(tmp_path)
|
|
resp = client.get("/api/scorecard/2026-02-15?market=KR")
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_performance_all(tmp_path: Path) -> None:
|
|
client = _client(tmp_path)
|
|
resp = client.get("/api/performance?market=all")
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["market"] == "all"
|
|
assert body["combined"]["total_trades"] == 2
|
|
assert len(body["by_market"]) == 2
|
|
|
|
|
|
def test_performance_market_filter(tmp_path: Path) -> None:
|
|
client = _client(tmp_path)
|
|
resp = client.get("/api/performance?market=KR")
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["market"] == "KR"
|
|
assert body["metrics"]["total_trades"] == 1
|
|
|
|
|
|
def test_performance_empty_market(tmp_path: Path) -> None:
|
|
client = _client(tmp_path)
|
|
resp = client.get("/api/performance?market=JP")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["metrics"]["total_trades"] == 0
|
|
|
|
|
|
def test_context_layer_all(tmp_path: Path) -> None:
|
|
client = _client(tmp_path)
|
|
resp = client.get("/api/context/L7_REALTIME")
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["layer"] == "L7_REALTIME"
|
|
assert body["count"] == 1
|
|
|
|
|
|
def test_context_layer_timeframe_filter(tmp_path: Path) -> None:
|
|
client = _client(tmp_path)
|
|
resp = client.get("/api/context/L6_DAILY?timeframe=2026-02-14")
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["count"] == 1
|
|
assert body["entries"][0]["key"] == "scorecard_KR"
|
|
|
|
|
|
def test_decisions_endpoint(tmp_path: Path) -> None:
|
|
client = _client(tmp_path)
|
|
resp = client.get("/api/decisions?market=KR")
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["count"] == 1
|
|
assert body["decisions"][0]["decision_id"] == "d-kr-1"
|
|
|
|
|
|
def test_scenarios_active_filters_non_matched(tmp_path: Path) -> None:
|
|
client = _client(tmp_path)
|
|
resp = client.get("/api/scenarios/active?market=KR&date_str=2026-02-14")
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["count"] == 1
|
|
assert body["matches"][0]["stock_code"] == "005930"
|
|
|
|
|
|
def test_scenarios_active_empty_when_no_matches(tmp_path: Path) -> None:
|
|
client = _client(tmp_path)
|
|
resp = client.get("/api/scenarios/active?market=US&date_str=2026-02-14")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["count"] == 0
|