100 lines
5.9 KiB
Python
100 lines
5.9 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from decimal import Decimal
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from control_plane.trading_studio.models import (
|
||
|
|
EvidenceStatus, ExperimentStatus, FailureType, FeatureDefinition, StrategyStage, TradingCohort,
|
||
|
|
)
|
||
|
|
from control_plane.trading_studio.services import TradingStudioService
|
||
|
|
|
||
|
|
|
||
|
|
def splits():
|
||
|
|
return {
|
||
|
|
"split_method": "chronological",
|
||
|
|
"DISCOVERY": {"start": "2026-01-01T00:00:00Z", "end": "2026-01-10T00:00:00Z"},
|
||
|
|
"TRAIN": {"start": "2026-01-10T00:00:00Z", "end": "2026-01-20T00:00:00Z"},
|
||
|
|
"VALIDATION": {"start": "2026-01-20T00:00:00Z", "end": "2026-01-25T00:00:00Z"},
|
||
|
|
"HOLDOUT": {"start": "2026-01-25T00:00:00Z", "end": "2026-02-01T00:00:00Z"},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def metrics(**overrides):
|
||
|
|
payload = {"gross_pnl": 10.0, "fees": 2.0, "funding": 1.0, "slippage": 1.0, "other_execution_cost": 0.0, "net_pnl": 6.0, "trade_count": 40, "pnl_concentration_top_trade": 0.1}
|
||
|
|
return {**payload, **overrides}
|
||
|
|
|
||
|
|
|
||
|
|
def contract(**overrides):
|
||
|
|
payload = {"hypothesis": "A frozen existing signal retains net edge.", "market_rationale": "Imported evidence only.", "expected_regime": {"trend": "unknown"}, "controls": {"same_execution_model": True}, "success_criteria": {"net_pnl": ">0"}, "rejection_criteria": {"net_pnl": "<0"}, "risk_assumptions": {"leverage": 1}, "execution_assumptions": {"entry": "next_bar_open"}, "estimated_evaluation_cost": {"cpu_seconds": 1}}
|
||
|
|
return {**payload, **overrides}
|
||
|
|
|
||
|
|
|
||
|
|
def ready():
|
||
|
|
service = TradingStudioService()
|
||
|
|
project = service.import_hyperscalper(repository_path="missing-for-test", slug="trading-test")
|
||
|
|
dataset = service.register_market_dataset(project, name="BTCUSD 2m", kind="OHLCV", version="v1", reference="fake://btc", content_hash="a" * 64, fields=["timestamp", "open", "high", "low", "close", "volume"], record_count=10, start_at=None, end_at=None, resolution="2m", quality={"duplicates": 0}, temporal_splits=splits())
|
||
|
|
cohort = TradingCohort.objects.create(trading_project=project, name="bounded", dataset_version=dataset, policy_snapshot={"minimum_trade_count": 30})
|
||
|
|
strategy = service.create_strategy_version(project, name="candidate", genome={"entry_conditions": ["close > prior_close"], "position_sizing": {"kind": "fixed"}})
|
||
|
|
experiment = service.propose_experiment(cohort, strategy, contract())
|
||
|
|
return service, project, dataset, cohort, strategy, experiment
|
||
|
|
|
||
|
|
|
||
|
|
def test_random_time_split_and_overlap_are_rejected():
|
||
|
|
service = TradingStudioService()
|
||
|
|
project = service.import_hyperscalper(repository_path="missing", slug="split-test")
|
||
|
|
with pytest.raises(ValueError, match="Random"):
|
||
|
|
service.register_market_dataset(project, name="bad", kind="OHLCV", version="v1", reference="x", content_hash="a", fields=[], record_count=1, start_at=None, end_at=None, resolution="2m", quality={}, temporal_splits={"split_method": "random"})
|
||
|
|
bad = splits()
|
||
|
|
bad["TRAIN"]["start"] = "2026-01-09T00:00:00Z"
|
||
|
|
with pytest.raises(ValueError, match="non-overlapping"):
|
||
|
|
service.register_market_dataset(project, name="overlap", kind="OHLCV", version="v1", reference="x", content_hash="b", fields=[], record_count=1, start_at=None, end_at=None, resolution="2m", quality={}, temporal_splits=bad)
|
||
|
|
|
||
|
|
|
||
|
|
def test_blocked_features_and_martingale_are_rejected():
|
||
|
|
service, project, _, _, _, _ = ready()
|
||
|
|
feature = FeatureDefinition.objects.create(trading_project=project, name="blocked", implementation_reference="fake://feature", code_hash="f", leakage_status=EvidenceStatus.BLOCKED)
|
||
|
|
feature_set = service.create_feature_set(project, name="blocked", version="v1", features=[feature])
|
||
|
|
with pytest.raises(ValueError, match="BLOCKED"):
|
||
|
|
service.create_strategy_version(project, name="bad-feature", genome={}, feature_set=feature_set)
|
||
|
|
with pytest.raises(ValueError, match="Martingale"):
|
||
|
|
service.create_strategy_version(project, name="bad-sizing", genome={"position_sizing": "martingale"})
|
||
|
|
|
||
|
|
|
||
|
|
def test_same_bar_and_cost_accounting_are_enforced():
|
||
|
|
service, _, _, _, _, experiment = ready()
|
||
|
|
with pytest.raises(ValueError, match="Same-bar"):
|
||
|
|
service.record_backtest(experiment, split="VALIDATION", execution_model_version="v1", metrics=metrics(), configuration={"same_bar_close_execution": True})
|
||
|
|
with pytest.raises(ValueError, match="Net PnL"):
|
||
|
|
service.record_backtest(experiment, split="VALIDATION", execution_model_version="v1", metrics=metrics(net_pnl=7), configuration={})
|
||
|
|
|
||
|
|
|
||
|
|
def test_negative_after_fees_is_killed_as_economics_failure():
|
||
|
|
service, _, _, _, strategy, experiment = ready()
|
||
|
|
run = service.record_backtest(experiment, split="VALIDATION", execution_model_version="v1", metrics=metrics(gross_pnl=2, fees=3, funding=0, slippage=0, net_pnl=-1), configuration={})
|
||
|
|
evaluation = service.judge_backtest(experiment, run, policy={"minimum_trade_count": 30})
|
||
|
|
experiment.refresh_from_db()
|
||
|
|
strategy.refresh_from_db()
|
||
|
|
assert evaluation.failure_type == FailureType.FEE_DESTROYED
|
||
|
|
assert experiment.status == ExperimentStatus.REJECTED
|
||
|
|
assert strategy.stage == StrategyStage.KILLED
|
||
|
|
|
||
|
|
|
||
|
|
def test_holdout_is_consumed_and_cannot_drive_adaptive_design():
|
||
|
|
service, _, _, _, strategy, experiment = ready()
|
||
|
|
service.record_backtest(experiment, split="HOLDOUT", execution_model_version="v1", metrics=metrics(), configuration={})
|
||
|
|
strategy.refresh_from_db()
|
||
|
|
assert strategy.holdout_exposure_count == 1
|
||
|
|
assert strategy.immutable is True
|
||
|
|
with pytest.raises(ValueError, match="Consumed holdout"):
|
||
|
|
service.propose_experiment(experiment.cohort, strategy, contract(controls={"uses_holdout_for_design": True}))
|
||
|
|
|
||
|
|
|
||
|
|
def test_micro_live_is_hard_refused_and_no_survivor_report_is_valid():
|
||
|
|
service, project, _, _, strategy, _ = ready()
|
||
|
|
with pytest.raises(ValueError, match="offline-only"):
|
||
|
|
service.request_micro_live(strategy, Decimal("20"))
|
||
|
|
report = service.report(project)
|
||
|
|
assert report.content["counts"]["survivors"] == 0
|
||
|
|
assert report.content["offline_only"] is True
|