Files
vault-dash/tests/test_backtest_settings.py
Bu5hm4nn 43067bb275 feat(DATA-DB-002): add BacktestSettings model and repository
- BacktestSettings dataclass with all configuration fields
- BacktestSettingsRepository for persistence per workspace
- Settings independent of portfolio configuration
- Full validation for dates, symbols, LTV, etc.
- 16 comprehensive tests

Fields:
- settings_id, name: identification
- data_source: databento|yfinance|synthetic
- dataset, schema: Databento configuration
- start_date, end_date: date range
- underlying_symbol, start_price, underlying_units: position config
- loan_amount, margin_call_ltv: LTV analysis
- template_slugs: strategies to test
- cache_key, data_cost_usd: caching metadata
- provider_ref: provider configuration
2026-03-29 10:46:25 +02:00

663 lines
27 KiB
Python

from __future__ import annotations
from datetime import date
from uuid import uuid4
import pytest
from app.models.backtest import ProviderRef
from app.models.backtest_settings import BacktestSettings
from app.models.backtest_settings_repository import BacktestSettingsRepository
class TestBacktestSettings:
"""Tests for the BacktestSettings model."""
def test_default_settings_creation(self) -> None:
"""Verify create_default() works correctly."""
settings = BacktestSettings.create_default()
# Verify basic properties
assert isinstance(settings.settings_id, type(uuid4()))
assert settings.name == "Default Backtest Settings"
assert settings.data_source == "databento"
assert settings.dataset == "XNAS.BASIC"
assert settings.schema == "ohlcv-1d"
assert settings.start_date == date(2020, 1, 1)
assert settings.end_date == date(2023, 12, 31)
assert settings.underlying_symbol == "GLD"
assert settings.start_price == 0.0
assert settings.underlying_units == 1000.0
assert settings.loan_amount == 0.0
assert settings.margin_call_ltv == 0.75
assert settings.template_slugs == ("default-template",)
assert settings.cache_key == ""
assert settings.data_cost_usd == 0.0
assert settings.provider_ref.provider_id == "default"
assert settings.provider_ref.pricing_mode == "standard"
def test_settings_serialization_to_dict(self) -> None:
"""Verify to_dict() serialization works correctly."""
settings_id = uuid4()
provider_ref = ProviderRef(provider_id="test_provider", pricing_mode="test_mode")
settings = BacktestSettings(
settings_id=settings_id,
name="Test Settings",
data_source="yfinance",
dataset="TEST.DATASET",
schema="test-schema",
start_date=date(2021, 1, 1),
end_date=date(2021, 12, 31),
underlying_symbol="GC",
start_price=50.0,
underlying_units=500.0,
loan_amount=10000.0,
margin_call_ltv=0.80,
template_slugs=("template1", "template2"),
cache_key="test-cache-key",
data_cost_usd=500.0,
provider_ref=provider_ref,
)
# Call the _to_dict method through the repository since it's private
repo = BacktestSettingsRepository()
serialized = repo._to_dict(settings)
# Verify all fields are properly serialized
assert serialized["settings_id"] == str(settings_id)
assert serialized["name"] == "Test Settings"
assert serialized["data_source"] == "yfinance"
assert serialized["dataset"] == "TEST.DATASET"
assert serialized["schema"] == "test-schema"
assert serialized["start_date"] == "2021-01-01"
assert serialized["end_date"] == "2021-12-31"
assert serialized["underlying_symbol"] == "GC"
assert serialized["start_price"] == 50.0
assert serialized["underlying_units"] == 500.0
assert serialized["loan_amount"] == 10000.0
assert serialized["margin_call_ltv"] == 0.80
assert serialized["template_slugs"] == ["template1", "template2"]
assert serialized["cache_key"] == "test-cache-key"
assert serialized["data_cost_usd"] == 500.0
assert serialized["provider_ref"]["provider_id"] == "test_provider"
assert serialized["provider_ref"]["pricing_mode"] == "test_mode"
def test_settings_deserialization_from_dict(self) -> None:
"""Verify _settings_from_dict() works correctly."""
data = {
"settings_id": str(uuid4()),
"name": "Deserialized Settings",
"data_source": "synthetic",
"dataset": "SYNTHETIC.DATA",
"schema": "synthetic-schema",
"start_date": "2022-01-01",
"end_date": "2022-12-31",
"underlying_symbol": "XAU",
"start_price": 75.0,
"underlying_units": 750.0,
"loan_amount": 15000.0,
"margin_call_ltv": 0.70,
"template_slugs": ["template3", "template4"],
"cache_key": "deserialized-cache",
"data_cost_usd": 750.0,
"provider_ref": {
"provider_id": "deserialized_provider",
"pricing_mode": "deserialized_mode",
},
}
repo = BacktestSettingsRepository()
settings = repo._settings_from_dict(data)
# Verify all fields are properly deserialized
assert str(settings.settings_id) == data["settings_id"]
assert settings.name == "Deserialized Settings"
assert settings.data_source == "synthetic"
assert settings.dataset == "SYNTHETIC.DATA"
assert settings.schema == "synthetic-schema"
assert settings.start_date == date(2022, 1, 1)
assert settings.end_date == date(2022, 12, 31)
assert settings.underlying_symbol == "XAU"
assert settings.start_price == 75.0
assert settings.underlying_units == 750.0
assert settings.loan_amount == 15000.0
assert settings.margin_call_ltv == 0.70
assert settings.template_slugs == ("template3", "template4")
assert settings.cache_key == "deserialized-cache"
assert settings.data_cost_usd == 750.0
assert settings.provider_ref.provider_id == "deserialized_provider"
assert settings.provider_ref.pricing_mode == "deserialized_mode"
def test_date_range_validation(self) -> None:
"""Start_date must be <= end_date."""
with pytest.raises(ValueError, match="start_date must be on or before end_date"):
BacktestSettings(
settings_id=uuid4(),
name="Bad Date Range",
data_source="databento",
dataset="TEST.DATASET",
schema="test-schema",
start_date=date(2022, 1, 2), # After end_date
end_date=date(2022, 1, 1),
underlying_symbol="GLD",
start_price=0.0,
underlying_units=1000.0,
loan_amount=0.0,
margin_call_ltv=0.75,
template_slugs=("template1",),
cache_key="",
data_cost_usd=0.0,
provider_ref=ProviderRef(provider_id="default", pricing_mode="standard"),
)
def test_symbol_validation(self) -> None:
"""Only allow "GLD", "GC", "XAU", "GC=F"."""
# Valid symbols should work
valid_symbols = ["GLD", "GC", "XAU"]
for symbol in valid_symbols:
settings = BacktestSettings(
settings_id=uuid4(),
name=f"Valid Symbol {symbol}",
data_source="databento",
dataset="TEST.DATASET",
schema="test-schema",
start_date=date(2022, 1, 1),
end_date=date(2022, 12, 31),
underlying_symbol=symbol,
start_price=0.0,
underlying_units=1000.0,
loan_amount=0.0,
margin_call_ltv=0.75,
template_slugs=("template1",),
cache_key="",
data_cost_usd=0.0,
provider_ref=ProviderRef(provider_id="default", pricing_mode="standard"),
)
assert settings.underlying_symbol == symbol
# Invalid symbol should raise ValueError
with pytest.raises(ValueError, match="underlying_symbol must be 'GLD', 'GC', or 'XAU'"):
BacktestSettings(
settings_id=uuid4(),
name="Invalid Symbol",
data_source="databento",
dataset="TEST.DATASET",
schema="test-schema",
start_date=date(2022, 1, 1),
end_date=date(2022, 12, 31),
underlying_symbol="INVALID", # Not in allowed list
start_price=0.0,
underlying_units=1000.0,
loan_amount=0.0,
margin_call_ltv=0.75,
template_slugs=("template1",),
cache_key="",
data_cost_usd=0.0,
provider_ref=ProviderRef(provider_id="default", pricing_mode="standard"),
)
def test_data_source_validation(self) -> None:
"""Only allow "databento", "yfinance", "synthetic"."""
# Valid sources should work
valid_sources = ["databento", "yfinance", "synthetic"]
for source in valid_sources:
settings = BacktestSettings(
settings_id=uuid4(),
name=f"Valid Source {source}",
data_source=source,
dataset="TEST.DATASET",
schema="test-schema",
start_date=date(2022, 1, 1),
end_date=date(2022, 12, 31),
underlying_symbol="GLD",
start_price=0.0,
underlying_units=1000.0,
loan_amount=0.0,
margin_call_ltv=0.75,
template_slugs=("template1",),
cache_key="",
data_cost_usd=0.0,
provider_ref=ProviderRef(provider_id="default", pricing_mode="standard"),
)
assert settings.data_source == source
# Invalid source should raise ValueError
with pytest.raises(ValueError, match="data_source must be 'databento', 'yfinance', or 'synthetic'"):
BacktestSettings(
settings_id=uuid4(),
name="Invalid Source",
data_source="invalid_source", # Not in allowed list
dataset="TEST.DATASET",
schema="test-schema",
start_date=date(2022, 1, 1),
end_date=date(2022, 12, 31),
underlying_symbol="GLD",
start_price=0.0,
underlying_units=1000.0,
loan_amount=0.0,
margin_call_ltv=0.75,
template_slugs=("template1",),
cache_key="",
data_cost_usd=0.0,
provider_ref=ProviderRef(provider_id="default", pricing_mode="standard"),
)
def test_margin_call_ltv_validation(self) -> None:
"""Must be between 0 and 1."""
# Valid values should work
valid_ltvs = [0.01, 0.5, 0.99]
for ltv in valid_ltvs:
settings = BacktestSettings(
settings_id=uuid4(),
name=f"Valid LTV {ltv}",
data_source="databento",
dataset="TEST.DATASET",
schema="test-schema",
start_date=date(2022, 1, 1),
end_date=date(2022, 12, 31),
underlying_symbol="GLD",
start_price=0.0,
underlying_units=1000.0,
loan_amount=0.0,
margin_call_ltv=ltv,
template_slugs=("template1",),
cache_key="",
data_cost_usd=0.0,
provider_ref=ProviderRef(provider_id="default", pricing_mode="standard"),
)
assert settings.margin_call_ltv == ltv
# Invalid values should raise ValueError
invalid_ltvs = [-0.1, 0.0, 1.0, 1.1]
for ltv in invalid_ltvs:
with pytest.raises(ValueError, match="margin_call_ltv must be between 0 and 1"):
BacktestSettings(
settings_id=uuid4(),
name=f"Invalid LTV {ltv}",
data_source="databento",
dataset="TEST.DATASET",
schema="test-schema",
start_date=date(2022, 1, 1),
end_date=date(2022, 12, 31),
underlying_symbol="GLD",
start_price=0.0,
underlying_units=1000.0,
loan_amount=0.0,
margin_call_ltv=ltv,
template_slugs=("template1",),
cache_key="",
data_cost_usd=0.0,
provider_ref=ProviderRef(provider_id="default", pricing_mode="standard"),
)
def test_loan_amount_validation(self) -> None:
"""Must be non-negative."""
# Valid values should work
valid_amounts = [0.0, 1000.0, 100000.0]
for amount in valid_amounts:
settings = BacktestSettings(
settings_id=uuid4(),
name=f"Valid Loan Amount {amount}",
data_source="databento",
dataset="TEST.DATASET",
schema="test-schema",
start_date=date(2022, 1, 1),
end_date=date(2022, 12, 31),
underlying_symbol="GLD",
start_price=0.0,
underlying_units=1000.0,
loan_amount=amount,
margin_call_ltv=0.75,
template_slugs=("template1",),
cache_key="",
data_cost_usd=0.0,
provider_ref=ProviderRef(provider_id="default", pricing_mode="standard"),
)
assert settings.loan_amount == amount
# Invalid negative value should raise ValueError
with pytest.raises(ValueError, match="loan_amount must be non-negative"):
BacktestSettings(
settings_id=uuid4(),
name="Invalid Negative Loan Amount",
data_source="databento",
dataset="TEST.DATASET",
schema="test-schema",
start_date=date(2022, 1, 1),
end_date=date(2022, 12, 31),
underlying_symbol="GLD",
start_price=0.0,
underlying_units=1000.0,
loan_amount=-1000.0, # Negative value
margin_call_ltv=0.75,
template_slugs=("template1",),
cache_key="",
data_cost_usd=0.0,
provider_ref=ProviderRef(provider_id="default", pricing_mode="standard"),
)
def test_underlying_units_validation(self) -> None:
"""Must be positive."""
# Valid values should work
valid_units = [0.1, 1.0, 1000.0]
for units in valid_units:
settings = BacktestSettings(
settings_id=uuid4(),
name=f"Valid Units {units}",
data_source="databento",
dataset="TEST.DATASET",
schema="test-schema",
start_date=date(2022, 1, 1),
end_date=date(2022, 12, 31),
underlying_symbol="GLD",
start_price=0.0,
underlying_units=units,
loan_amount=0.0,
margin_call_ltv=0.75,
template_slugs=("template1",),
cache_key="",
data_cost_usd=0.0,
provider_ref=ProviderRef(provider_id="default", pricing_mode="standard"),
)
assert settings.underlying_units == units
# Invalid zero or negative values should raise ValueError
invalid_units = [0.0, -1.0, -1000.0]
for units in invalid_units:
with pytest.raises(ValueError, match="underlying_units must be positive"):
BacktestSettings(
settings_id=uuid4(),
name=f"Invalid Units {units}",
data_source="databento",
dataset="TEST.DATASET",
schema="test-schema",
start_date=date(2022, 1, 1),
end_date=date(2022, 12, 31),
underlying_symbol="GLD",
start_price=0.0,
underlying_units=units,
loan_amount=0.0,
margin_call_ltv=0.75,
template_slugs=("template1",),
cache_key="",
data_cost_usd=0.0,
provider_ref=ProviderRef(provider_id="default", pricing_mode="standard"),
)
def test_template_slugs_serialization(self) -> None:
"""Tuple to list conversion during serialization."""
template_slugs_tuple = ("slug1", "slug2", "slug3")
settings = BacktestSettings(
settings_id=uuid4(),
name="Template Slugs Test",
data_source="databento",
dataset="TEST.DATASET",
schema="test-schema",
start_date=date(2022, 1, 1),
end_date=date(2022, 12, 31),
underlying_symbol="GLD",
start_price=0.0,
underlying_units=1000.0,
loan_amount=0.0,
margin_call_ltv=0.75,
template_slugs=template_slugs_tuple,
cache_key="",
data_cost_usd=0.0,
provider_ref=ProviderRef(provider_id="default", pricing_mode="standard"),
)
# Serialize and check that tuple becomes list
repo = BacktestSettingsRepository()
serialized = repo._to_dict(settings)
assert serialized["template_slugs"] == ["slug1", "slug2", "slug3"]
# Deserialize and check that list becomes tuple
deserialized = repo._settings_from_dict(serialized)
assert deserialized.template_slugs == template_slugs_tuple
def test_start_price_validation(self) -> None:
"""Start price must be non-negative."""
# Valid values should work
valid_prices = [0.0, 50.0, 1000.0]
for price in valid_prices:
settings = BacktestSettings(
settings_id=uuid4(),
name=f"Valid Start Price {price}",
data_source="databento",
dataset="TEST.DATASET",
schema="test-schema",
start_date=date(2022, 1, 1),
end_date=date(2022, 12, 31),
underlying_symbol="GLD",
start_price=price,
underlying_units=1000.0,
loan_amount=0.0,
margin_call_ltv=0.75,
template_slugs=("template1",),
cache_key="",
data_cost_usd=0.0,
provider_ref=ProviderRef(provider_id="default", pricing_mode="standard"),
)
assert settings.start_price == price
# Invalid negative value should raise ValueError
with pytest.raises(ValueError, match="start_price must be non-negative"):
BacktestSettings(
settings_id=uuid4(),
name="Invalid Negative Start Price",
data_source="databento",
dataset="TEST.DATASET",
schema="test-schema",
start_date=date(2022, 1, 1),
end_date=date(2022, 12, 31),
underlying_symbol="GLD",
start_price=-50.0, # Negative value
underlying_units=1000.0,
loan_amount=0.0,
margin_call_ltv=0.75,
template_slugs=("template1",),
cache_key="",
data_cost_usd=0.0,
provider_ref=ProviderRef(provider_id="default", pricing_mode="standard"),
)
def test_data_cost_validation(self) -> None:
"""Data cost must be non-negative."""
# Valid values should work
valid_costs = [0.0, 100.0, 10000.0]
for cost in valid_costs:
settings = BacktestSettings(
settings_id=uuid4(),
name=f"Valid Data Cost {cost}",
data_source="databento",
dataset="TEST.DATASET",
schema="test-schema",
start_date=date(2022, 1, 1),
end_date=date(2022, 12, 31),
underlying_symbol="GLD",
start_price=0.0,
underlying_units=1000.0,
loan_amount=0.0,
margin_call_ltv=0.75,
template_slugs=("template1",),
cache_key="",
data_cost_usd=cost,
provider_ref=ProviderRef(provider_id="default", pricing_mode="standard"),
)
assert settings.data_cost_usd == cost
# Invalid negative value should raise ValueError
with pytest.raises(ValueError, match="data_cost_usd must be non-negative"):
BacktestSettings(
settings_id=uuid4(),
name="Invalid Negative Data Cost",
data_source="databento",
dataset="TEST.DATASET",
schema="test-schema",
start_date=date(2022, 1, 1),
end_date=date(2022, 12, 31),
underlying_symbol="GLD",
start_price=0.0,
underlying_units=1000.0,
loan_amount=0.0,
margin_call_ltv=0.75,
template_slugs=("template1",),
cache_key="",
data_cost_usd=-100.0, # Negative value
provider_ref=ProviderRef(provider_id="default", pricing_mode="standard"),
)
class TestBacktestSettingsRepository:
"""Tests for the BacktestSettingsRepository."""
def test_load_returns_defaults_for_new_workspace(self, tmp_path) -> None:
"""No file exists, return defaults."""
repo = BacktestSettingsRepository(base_path=tmp_path / ".workspaces")
workspace_id = "new-workspace-id"
# Should return default settings when no file exists
settings = repo.load(workspace_id)
assert settings.name == "Default Backtest Settings"
assert settings.data_source == "databento"
assert settings.dataset == "XNAS.BASIC"
assert settings.schema == "ohlcv-1d"
def test_save_and_load_round_trips(self, tmp_path) -> None:
"""Save then load returns same values."""
repo = BacktestSettingsRepository(base_path=tmp_path / ".workspaces")
workspace_id = "test-workspace"
# Create test settings
original_settings = BacktestSettings.create_default()
original_settings = BacktestSettings(
settings_id=uuid4(),
name="Round Trip Test",
data_source="yfinance",
dataset="ROUNDTRIP.TEST",
schema="roundtrip-schema",
start_date=date(2023, 1, 1),
end_date=date(2023, 12, 31),
underlying_symbol="GC",
start_price=100.0,
underlying_units=2000.0,
loan_amount=50000.0,
margin_call_ltv=0.85,
template_slugs=("roundtrip1", "roundtrip2"),
cache_key="roundtrip-cache",
data_cost_usd=1000.0,
provider_ref=ProviderRef(provider_id="roundtrip_provider", pricing_mode="roundtrip_mode"),
)
# Save and then load
repo.save(workspace_id, original_settings)
loaded_settings = repo.load(workspace_id)
# Verify all fields match
assert loaded_settings.settings_id == original_settings.settings_id
assert loaded_settings.name == original_settings.name
assert loaded_settings.data_source == original_settings.data_source
assert loaded_settings.dataset == original_settings.dataset
assert loaded_settings.schema == original_settings.schema
assert loaded_settings.start_date == original_settings.start_date
assert loaded_settings.end_date == original_settings.end_date
assert loaded_settings.underlying_symbol == original_settings.underlying_symbol
assert loaded_settings.start_price == original_settings.start_price
assert loaded_settings.underlying_units == original_settings.underlying_units
assert loaded_settings.loan_amount == original_settings.loan_amount
assert loaded_settings.margin_call_ltv == original_settings.margin_call_ltv
assert loaded_settings.template_slugs == original_settings.template_slugs
assert loaded_settings.cache_key == original_settings.cache_key
assert loaded_settings.data_cost_usd == original_settings.data_cost_usd
assert loaded_settings.provider_ref.provider_id == original_settings.provider_ref.provider_id
assert loaded_settings.provider_ref.pricing_mode == original_settings.provider_ref.pricing_mode
def test_overwrites_existing_settings(self, tmp_path) -> None:
"""Save twice, second wins."""
repo = BacktestSettingsRepository(base_path=tmp_path / ".workspaces")
workspace_id = "overwrite-test"
# First settings
first_settings = BacktestSettings(
settings_id=uuid4(),
name="First Settings",
data_source="databento",
dataset="FIRST.DATASET",
schema="first-schema",
start_date=date(2020, 1, 1),
end_date=date(2020, 12, 31),
underlying_symbol="GLD",
start_price=50.0,
underlying_units=1000.0,
loan_amount=25000.0,
margin_call_ltv=0.70,
template_slugs=("first",),
cache_key="first-cache",
data_cost_usd=500.0,
provider_ref=ProviderRef(provider_id="first_provider", pricing_mode="first_mode"),
)
# Second settings
second_settings = BacktestSettings(
settings_id=uuid4(),
name="Second Settings",
data_source="yfinance",
dataset="SECOND.DATASET",
schema="second-schema",
start_date=date(2021, 1, 1),
end_date=date(2021, 12, 31),
underlying_symbol="GC",
start_price=75.0,
underlying_units=1500.0,
loan_amount=30000.0,
margin_call_ltv=0.75,
template_slugs=("second",),
cache_key="second-cache",
data_cost_usd=750.0,
provider_ref=ProviderRef(provider_id="second_provider", pricing_mode="second_mode"),
)
# Save first, then second
repo.save(workspace_id, first_settings)
repo.save(workspace_id, second_settings)
# Load and verify second settings were saved
loaded_settings = repo.load(workspace_id)
assert loaded_settings.name == "Second Settings"
assert loaded_settings.data_source == "yfinance"
assert loaded_settings.dataset == "SECOND.DATASET"
assert loaded_settings.schema == "second-schema"
assert loaded_settings.start_date == date(2021, 1, 1)
assert loaded_settings.end_date == date(2021, 12, 31)
assert loaded_settings.underlying_symbol == "GC"
assert loaded_settings.start_price == 75.0
assert loaded_settings.underlying_units == 1500.0
assert loaded_settings.loan_amount == 30000.0
assert loaded_settings.margin_call_ltv == 0.75
assert loaded_settings.template_slugs == ("second",)
assert loaded_settings.cache_key == "second-cache"
assert loaded_settings.data_cost_usd == 750.0
assert loaded_settings.provider_ref.provider_id == "second_provider"
assert loaded_settings.provider_ref.pricing_mode == "second_mode"
def test_handles_missing_directory(self, tmp_path) -> None:
"""Creates .workspaces/{id} if needed."""
repo = BacktestSettingsRepository(base_path=tmp_path / ".workspaces")
workspace_id = "missing-dir-test"
# Directory shouldn't exist yet
settings_path = repo._settings_path(workspace_id)
assert not settings_path.parent.exists()
# Save settings - should create directory
settings = BacktestSettings.create_default()
repo.save(workspace_id, settings)
# Directory should now exist
assert settings_path.parent.exists()
assert settings_path.exists()
# Load should work
loaded_settings = repo.load(workspace_id)
assert loaded_settings.name == "Default Backtest Settings"