Artifex/tests/test_trading_studio_v01.py
2026-08-17 13:48:50 +07:00

99 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