Files
vault-dash/tests/test_backtest_job.py
Bu5hm4nn d835544e58 fix: correct backtest job result serialization and add Playwright test fixtures
- Fix BacktestPageRunResult serialization in jobs.py to correctly access
  nested fields from scenario and run_result objects
- Add test_backtest_job.py with comprehensive tests for job execution
- Add conftest_playwright.py with ServerManager that starts FastAPI server
  for Playwright tests using uvicorn
- Add test_playwright_server.py with E2E tests using the server fixture

The job serialization bug was causing backtest results to fail silently
because it was trying to access non-existent fields on BacktestPageRunResult.
2026-04-04 18:27:34 +02:00

191 lines
5.9 KiB
Python

"""Tests for backtest job execution via the jobs module."""
from __future__ import annotations
from datetime import date
from app.services.backtesting.jobs import (
BacktestJobStore,
JobStage,
JobStatus,
job_store,
run_backtest_job,
)
from app.services.backtesting.ui_service import BacktestPageService
def test_backtest_job_store_creates_and_retrieves_jobs() -> None:
"""Test that the job store can create and retrieve jobs."""
store = BacktestJobStore()
job = store.create_job("workspace-1")
assert job.status == JobStatus.PENDING
assert job.stage == JobStage.VALIDATING
retrieved = store.get_job("workspace-1")
assert retrieved is job
# Creating a new job replaces the old one
job2 = store.create_job("workspace-1")
assert store.get_job("workspace-1") is job2
def test_backtest_job_store_updates_job_status() -> None:
"""Test that job status can be updated."""
store = BacktestJobStore()
store.create_job("workspace-1")
store.update_job(
"workspace-1",
status=JobStatus.RUNNING,
stage=JobStage.FETCHING_PRICES,
progress=50,
message="Fetching prices...",
)
updated = store.get_job("workspace-1")
assert updated is not None
assert updated.status == JobStatus.RUNNING
assert updated.stage == JobStage.FETCHING_PRICES
assert updated.progress == 50
assert updated.message == "Fetching prices..."
def test_run_backtest_job_completes_with_synthetic_data() -> None:
"""Test that a backtest job completes successfully with synthetic fixture data."""
# Use the global job_store singleton
workspace_id = "workspace-test-job-complete"
job = job_store.create_job(workspace_id)
service = BacktestPageService()
run_backtest_job(
workspace_id=workspace_id,
job=job,
service=service,
symbol="GLD",
start_date=date(2024, 1, 2),
end_date=date(2024, 1, 8),
template_slug="protective-put-atm-12m",
underlying_units=1000.0,
loan_amount=68000.0,
margin_call_ltv=0.75,
data_source="synthetic",
)
updated = job_store.get_job(workspace_id)
assert updated is not None
assert updated.status == JobStatus.COMPLETE, f"Job failed with error: {updated.error}"
assert updated.result is not None
# Verify result structure
result = updated.result
assert "scenario_id" in result
assert "entry_spot" in result
assert result["entry_spot"] == 100.0
assert "template_results" in result
assert len(result["template_results"]) == 1
# Verify template result structure
template_result = result["template_results"][0]
assert "template_slug" in template_result
assert "summary_metrics" in template_result
assert "daily_path" in template_result
# Verify there are daily points
daily_path = template_result["daily_path"]
assert len(daily_path) == 5 # 5 trading days in fixture
# Verify summary metrics exist
summary = template_result["summary_metrics"]
assert "start_value" in summary
assert "total_hedge_cost" in summary
def test_run_backtest_job_serializes_daily_path_correctly() -> None:
"""Test that daily_path is properly serialized with all fields."""
workspace_id = "workspace-test-daily-path"
job = job_store.create_job(workspace_id)
service = BacktestPageService()
run_backtest_job(
workspace_id=workspace_id,
job=job,
service=service,
symbol="GLD",
start_date=date(2024, 1, 2),
end_date=date(2024, 1, 8),
template_slug="protective-put-atm-12m",
underlying_units=1000.0,
loan_amount=68000.0,
margin_call_ltv=0.75,
data_source="synthetic",
)
result = job_store.get_job(workspace_id).result
daily_point = result["template_results"][0]["daily_path"][0]
# Verify all fields are present
assert "date" in daily_point
assert "spot_close" in daily_point
assert "underlying_value" in daily_point
assert "option_market_value" in daily_point
assert "net_portfolio_value" in daily_point
assert "ltv_hedged" in daily_point
assert "ltv_unhedged" in daily_point
assert "margin_call_hedged" in daily_point
assert "margin_call_unhedged" in daily_point
def test_run_backtest_job_handles_validation_errors() -> None:
"""Test that validation errors are properly captured."""
workspace_id = "workspace-test-validation"
job = job_store.create_job(workspace_id)
service = BacktestPageService()
# Use invalid dates (outside fixture window for synthetic)
run_backtest_job(
workspace_id=workspace_id,
job=job,
service=service,
symbol="GLD",
start_date=date(2023, 1, 1), # Outside fixture window
end_date=date(2023, 1, 7),
template_slug="protective-put-atm-12m",
underlying_units=1000.0,
loan_amount=68000.0,
margin_call_ltv=0.75,
data_source="synthetic",
)
updated = job_store.get_job(workspace_id)
assert updated is not None
assert updated.status == JobStatus.FAILED
assert updated.error is not None
assert "deterministic fixture data" in updated.error
def test_run_backtest_job_handles_invalid_symbol() -> None:
"""Test that invalid symbols are properly rejected."""
workspace_id = "workspace-test-invalid-symbol"
job = job_store.create_job(workspace_id)
service = BacktestPageService()
run_backtest_job(
workspace_id=workspace_id,
job=job,
service=service,
symbol="INVALID",
start_date=date(2024, 1, 2),
end_date=date(2024, 1, 8),
template_slug="protective-put-atm-12m",
underlying_units=1000.0,
loan_amount=68000.0,
margin_call_ltv=0.75,
data_source="synthetic",
)
updated = job_store.get_job(workspace_id)
assert updated is not None
assert updated.status == JobStatus.FAILED
assert "GLD, GC, XAU" in updated.error