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