Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions dashboard/backend/api/routers/admin_traces.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,57 @@ def finite(value):
}


@router.get("/{trace_id}/chart-data")
def get_trace_chart_data(trace_id: str):
"""The Backtest panel's chart payload for this trace's exact run.

Same builder, market-hour filter and benchmarks as the owner-scoped
``/api/backtest/{run_id}/chart-data``; only the lookup differs, by run ID
across owners, because an administrator reads every user's runs. A trace
whose run has no stored equity answers ``available: False`` rather than
404, so the panel can still show its decision snapshots.
"""
from dashboard.backend.api.routers import backtests as bt
from dashboard.backend.database import db
from dashboard.backend.equity_plot import build_backtest_chart_data

trace = trace_store.get_trace(trace_id)
if trace is None:
raise HTTPException(status_code=404, detail="Trace not found")
run_id = trace.get("run_id")
run = db.get_run(run_id) if run_id else None
if run is None:
return {"available": False, "run_id": run_id}
agent_curve = bt._filter_equity_for_run(run, db.get_equity_curve(run_id))
if not agent_curve:
return {"available": False, "run_id": run_id}
profile = bt._market_profile_for_run(run)
try:
payload = build_backtest_chart_data(
run_id=run_id,
agent_name=run.get("agent_name") or "Agent",
llm_model=run.get("llm_model"),
start_date=run.get("start_date") or "",
end_date=run.get("end_date") or "",
initial_capital=bt._run_initial_capital(run, agent_curve[0].get("equity")),
agent_curve=agent_curve,
stored_baselines=bt._stored_buyhold_baseline(run),
include_market_indexes=profile.index_baseline_enabled,
market_timezone=profile.timezone,
)
except ValueError:
return {"available": False, "run_id": run_id}
# The comparison model reads these to pick its columns (A-share runs have
# no US index benchmarks) and to match the stored buy-and-hold series.
metadata = run.get("metadata") if isinstance(run.get("metadata"), dict) else {}
payload["run"] = {
"data_source": metadata.get("data_source"),
"baseline_buyhold_run_id": run.get("baseline_buyhold_run_id"),
"reporting_currency": metadata.get("reporting_currency"),
}
return {"available": True, **payload}


@router.get("/{trace_id}/export")
def export_trace(trace_id: str, format: str = Query(default="json", pattern="^(json|markdown)$")):
from fastapi.responses import StreamingResponse
Expand Down
2 changes: 1 addition & 1 deletion dashboard/backend/tests/test_admin_analytics_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def test_app_lifecycle_and_cache_versions_are_wired():
assert 'app.js?v=161' in APP_HTML
assert 'js/admin-tabs.js?v=12' in APP_HTML
for tag in (
'href="admin.css?v=13"',
'href="admin.css?v=14"',
'src="js/admin-shell.js?v=8"',
'src="js/credit-format.js?v=1"',
'src="js/admin-live.js?v=1"',
Expand Down
2 changes: 1 addition & 1 deletion dashboard/backend/tests/test_admin_page_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def test_each_module_is_an_iife_exposing_exactly_one_global():

def test_every_module_admin_html_loads_exists_and_nothing_else_is_loaded():
srcs = re.findall(r'<script src="js/([^?"]+)\?v=\d+" defer></script>', ADMIN_HTML)
assert set(srcs) == set(NAMES) | {"credit-format.js"} | {"admin-ticker.js"}
assert set(srcs) == set(NAMES) | {"credit-format.js"} | {"admin-ticker.js"} | {"backtest-comparison.js"}
for name in srcs:
assert (FRONTEND / "js" / name).exists(), name

Expand Down
10 changes: 6 additions & 4 deletions dashboard/backend/tests/test_admin_page_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@
"js/admin-providers.js?v=1",
# The absorbed credits console (design N2/PR2).
"js/admin-credits.js?v=4",
"js/admin-trace-timeline.js?v=1",
"js/admin-trace-performance.js?v=2",
"js/admin-traces.js?v=3",
# The trace chart reuses the Backtest panel's comparison model.
"js/backtest-comparison.js?v=1",
"js/admin-trace-timeline.js?v=2",
"js/admin-trace-performance.js?v=4",
"js/admin-traces.js?v=5",
]

# This guard's whole job is "no inline script anywhere in this page", so a
Expand Down Expand Up @@ -94,7 +96,7 @@ def test_gate_module_loads_first_and_every_script_is_pinned():


def test_stylesheet_is_admin_css_and_the_page_does_not_inherit_styles_css():
assert '<link rel="stylesheet" href="admin.css?v=13">' in ADMIN_HTML
assert '<link rel="stylesheet" href="admin.css?v=14">' in ADMIN_HTML
assert "styles.css" not in ADMIN_HTML
assert "@import" not in ADMIN_CSS
assert "cdn.jsdelivr.net" not in ADMIN_HTML
Expand Down
78 changes: 78 additions & 0 deletions dashboard/backend/tests/test_admin_trace_chart_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""The admin trace chart reuses the Backtest panel's payload for the exact run."""

import pytest
from fastapi import HTTPException

import dashboard.backend.api.routers.backtests as bt
from dashboard.backend.api.routers import admin_traces as api
from dashboard.backend.database import db


def _curve():
return [
{"timestamp": "2026-05-04T14:30:00", "equity": 1_000.0},
{"timestamp": "2026-05-04T15:30:00", "equity": 1_010.0},
]


def _install(monkeypatch, *, run, curves):
monkeypatch.setattr(api.trace_store, "get_trace", lambda _: {"run_id": "agent-1", "status": "completed"})
calls = []

def get_run(run_id):
calls.append(run_id)
return run if run_id == "agent-1" else {"run_id": run_id, "agent_name": "buy-and-hold"}

monkeypatch.setattr(db, "get_run", get_run)
monkeypatch.setattr(db, "get_equity_curve", lambda run_id: curves.get(run_id, []))
monkeypatch.setattr(bt, "_filter_equity_for_run", lambda _run, curve: curve)

def fake_indexes(timestamps, *_args, **_kwargs):
return ([
("DJIA index", "index:^DJI", [1_000.0, 1_005.0]),
("Nasdaq-100", "index:^NDX", [1_000.0, 1_015.0]),
], True)

monkeypatch.setattr("dashboard.backend.equity_plot.market_index_baselines_with_status", fake_indexes)
return calls


def test_chart_data_has_the_backtest_series_for_the_exact_run(monkeypatch):
run = {
"run_id": "agent-1", "agent_name": "Agent", "start_date": "2026-05-04", "end_date": "2026-05-04",
"initial_equity": 1_000.0, "baseline_buyhold_run_id": "buyhold-1",
"metadata": {"data_source": "alpaca"},
}
calls = _install(monkeypatch, run=run, curves={"agent-1": _curve(), "buyhold-1": _curve()})
result = api.get_trace_chart_data("trace")
assert calls[0] == "agent-1"
assert result["available"] is True
assert result["agent_run_id"] == "agent-1"
assert [s["run_id"] for s in result["series"]] == ["agent-1", "buyhold-1", "index:^DJI", "index:^NDX"]
assert result["series"][0]["dashed"] is False
assert all(s["dashed"] for s in result["series"][1:])
assert result["run"] == {"data_source": "alpaca", "baseline_buyhold_run_id": "buyhold-1", "reporting_currency": None}


def test_a_run_without_stored_equity_is_unavailable_not_an_error(monkeypatch):
_install(monkeypatch, run={"run_id": "agent-1", "metadata": {}}, curves={})
assert api.get_trace_chart_data("trace") == {"available": False, "run_id": "agent-1"}


def test_a_trace_whose_run_is_gone_is_unavailable(monkeypatch):
monkeypatch.setattr(api.trace_store, "get_trace", lambda _: {"run_id": "gone", "status": "failed"})
monkeypatch.setattr(db, "get_run", lambda _: None)
assert api.get_trace_chart_data("trace") == {"available": False, "run_id": "gone"}


def test_missing_trace_is_404(monkeypatch):
monkeypatch.setattr(api.trace_store, "get_trace", lambda _: None)
with pytest.raises(HTTPException) as exc:
api.get_trace_chart_data("missing")
assert exc.value.status_code == 404


def test_chart_data_route_keeps_admin_dependency():
from dashboard.backend.api.auth import require_admin
route = next(r for r in api.router.routes if r.path.endswith("/chart-data"))
assert require_admin in [dependency.call for dependency in route.dependant.dependencies]
Loading
Loading