- 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.
191 lines
5.9 KiB
Python
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
|