Add offline-first Trading Studio v0.1

This commit is contained in:
Daniel Maddern 2026-08-17 13:48:50 +07:00
parent 0c716660cb
commit 65f0d510ec
20 changed files with 1271 additions and 0 deletions

View file

@ -23,6 +23,7 @@ INSTALLED_APPS = [
"control_plane.resources",
"control_plane.ventures",
"control_plane.model_studio",
"control_plane.trading_studio",
"control_plane.secrets",
"control_plane.knowledge",
"control_plane.verification",

View file

@ -5,6 +5,7 @@ from django.urls import path
from control_plane.projects import views
from control_plane.model_studio import views as model_studio_views
from control_plane.trading_studio import views as trading_studio_views
urlpatterns = [
path("", views.dashboard, name="dashboard"),
@ -35,6 +36,8 @@ urlpatterns = [
path("resources/", views.resources, name="resources"),
path("model-studio/", model_studio_views.model_studio, name="model_studio"),
path("model-studio/<uuid:project_id>/", model_studio_views.model_studio_project, name="model_studio_project"),
path("trading-studio/", trading_studio_views.trading_studio, name="trading_studio"),
path("trading-studio/<uuid:project_id>/", trading_studio_views.trading_studio_project, name="trading_studio_project"),
path("approvals/", views.approvals, name="approvals"),
path("approvals/<int:approval_id>/action/", views.approval_action, name="approval_action"),
path("activity/", views.activity, name="activity"),

View file

View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class TradingStudioConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "control_plane.trading_studio"

View file

@ -0,0 +1,20 @@
import json
from django.core.management.base import BaseCommand, CommandError
from control_plane.trading_studio.models import TradingProject
from control_plane.trading_studio.services import TradingStudioService
class Command(BaseCommand):
help = "Import existing HyperScalper feature implementation metadata without trusting it."
def add_arguments(self, parser):
parser.add_argument("--project", required=True)
def handle(self, *args, **options):
project = TradingProject.objects.filter(slug=options["project"]).first()
if project is None:
raise CommandError("TradingProject not found.")
features = TradingStudioService().import_features(project)
self.stdout.write(json.dumps({"project": project.slug, "imported": len(features), "leakage_status": "UNKNOWN"}))

View file

@ -0,0 +1,17 @@
import json
from django.core.management.base import BaseCommand
from control_plane.trading_studio.services import TradingStudioService
class Command(BaseCommand):
help = "Create the offline-only HyperScalper Trading Studio project."
def add_arguments(self, parser):
parser.add_argument("--repository-path", required=True)
parser.add_argument("--slug", default="crypto-hyperscalper")
def handle(self, *args, **options):
project = TradingStudioService().import_hyperscalper(repository_path=options["repository_path"], slug=options["slug"])
self.stdout.write(json.dumps({"id": str(project.id), "slug": project.slug, "status": project.status, "live_execution_enabled": project.metadata["live_execution_enabled"]}))

View file

@ -0,0 +1,20 @@
import json
from django.core.management.base import BaseCommand, CommandError
from control_plane.trading_studio.models import TradingProject
from control_plane.trading_studio.services import TradingStudioService
class Command(BaseCommand):
help = "Create a canonical Trading Studio cohort report."
def add_arguments(self, parser):
parser.add_argument("--project", required=True)
def handle(self, *args, **options):
project = TradingProject.objects.filter(slug=options["project"]).first()
if project is None:
raise CommandError("TradingProject not found.")
report = TradingStudioService().report(project)
self.stdout.write(json.dumps({"report": str(report.id), "title": report.title, "content": report.content}, default=str))

View file

@ -0,0 +1,400 @@
# Generated by Django 5.2.16 on 2026-08-17 06:44
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('projects', '0006_roadmap_scenario_lab_v1'),
]
operations = [
migrations.CreateModel(
name='FeatureDefinition',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=160)),
('family', models.CharField(blank=True, max_length=120)),
('implementation_reference', models.TextField()),
('code_hash', models.CharField(blank=True, max_length=128)),
('lookback', models.JSONField(blank=True, default=dict)),
('dependencies', models.JSONField(blank=True, default=list)),
('normalization', models.JSONField(blank=True, default=dict)),
('timeframe', models.CharField(blank=True, max_length=80)),
('leakage_status', models.CharField(choices=[('CONFIRMED', 'Confirmed'), ('INFERRED', 'Inferred'), ('UNKNOWN', 'Unknown'), ('SAFE', 'Safe'), ('SUSPICIOUS', 'Suspicious'), ('BLOCKED', 'Blocked')], default='UNKNOWN', max_length=16)),
('evidence', models.JSONField(blank=True, default=dict)),
],
),
migrations.CreateModel(
name='MarketDataset',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=200)),
('kind', models.CharField(choices=[('OHLCV', 'Ohlcv'), ('TRADES', 'Trades'), ('TICKS', 'Ticks'), ('ORDER_BOOK', 'Order Book'), ('FUNDING', 'Funding'), ('OPEN_INTEREST', 'Open Interest'), ('LIQUIDATIONS', 'Liquidations'), ('MARK_PRICE', 'Mark Price'), ('INDEX_PRICE', 'Index Price'), ('FILL_HISTORY', 'Fill History'), ('OTHER', 'Other')], max_length=32)),
('venue', models.CharField(blank=True, max_length=120)),
('symbol', models.CharField(blank=True, max_length=120)),
('market_type', models.CharField(blank=True, max_length=80)),
('metadata', models.JSONField(blank=True, default=dict)),
],
),
migrations.CreateModel(
name='Strategy',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=200)),
('strategy_class', models.CharField(blank=True, max_length=80)),
('description', models.TextField(blank=True)),
('metadata', models.JSONField(blank=True, default=dict)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='StrategyExperiment',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('hypothesis', models.TextField()),
('market_rationale', models.TextField()),
('expected_regime', models.JSONField(default=dict)),
('controls', models.JSONField(default=dict)),
('success_criteria', models.JSONField(default=dict)),
('rejection_criteria', models.JSONField(default=dict)),
('risk_assumptions', models.JSONField(default=dict)),
('execution_assumptions', models.JSONField(default=dict)),
('estimated_evaluation_cost', models.JSONField(default=dict)),
('status', models.CharField(choices=[('PROPOSED', 'Proposed'), ('RUNNING', 'Running'), ('SUCCEEDED', 'Succeeded'), ('REJECTED', 'Rejected'), ('FAILED', 'Failed')], default='PROPOSED', max_length=32)),
('conclusion', models.CharField(blank=True, choices=[('KILL', 'Kill'), ('REVISE', 'Revise'), ('ADVANCE', 'Advance'), ('PROMOTE', 'Promote'), ('DEMOTE', 'Demote'), ('REQUIRE_MORE_EVIDENCE', 'Require More Evidence')], max_length=32)),
('failure_type', models.CharField(blank=True, choices=[('NO_ALPHA', 'No Alpha'), ('OVERFIT', 'Overfit'), ('LEAKAGE', 'Leakage'), ('FEE_DESTROYED', 'Fee Destroyed'), ('SLIPPAGE_DESTROYED', 'Slippage Destroyed'), ('LATENCY_DESTROYED', 'Latency Destroyed'), ('REGIME_FRAGILE', 'Regime Fragile'), ('PARAMETER_FRAGILE', 'Parameter Fragile'), ('INSUFFICIENT_TRADES', 'Insufficient Trades'), ('RISK_TOO_HIGH', 'Risk Too High'), ('TAIL_RISK', 'Tail Risk'), ('EXECUTION_FAILURE', 'Execution Failure'), ('DATA_FAILURE', 'Data Failure'), ('LIVE_DIVERGENCE', 'Live Divergence'), ('CAPACITY_LIMIT', 'Capacity Limit'), ('UNKNOWN', 'Unknown')], max_length=32)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='FeatureSetVersion',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=160)),
('version', models.CharField(max_length=120)),
('content_hash', models.CharField(max_length=128)),
('immutable', models.BooleanField(default=False)),
('feature_definitions', models.ManyToManyField(related_name='feature_sets', to='trading_studio.featuredefinition')),
],
),
migrations.CreateModel(
name='MarketDatasetVersion',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('version', models.CharField(max_length=120)),
('reference', models.TextField()),
('content_hash', models.CharField(max_length=128)),
('processing_version', models.CharField(blank=True, max_length=120)),
('start_at', models.DateTimeField(blank=True, null=True)),
('end_at', models.DateTimeField(blank=True, null=True)),
('resolution', models.CharField(blank=True, max_length=80)),
('record_count', models.BigIntegerField(blank=True, null=True)),
('fields', models.JSONField(blank=True, default=list)),
('quality', models.JSONField(blank=True, default=dict)),
('lookahead_risk_status', models.CharField(choices=[('CONFIRMED', 'Confirmed'), ('INFERRED', 'Inferred'), ('UNKNOWN', 'Unknown'), ('SAFE', 'Safe'), ('SUSPICIOUS', 'Suspicious'), ('BLOCKED', 'Blocked')], default='UNKNOWN', max_length=16)),
('quality_status', models.CharField(choices=[('CONFIRMED', 'Confirmed'), ('INFERRED', 'Inferred'), ('UNKNOWN', 'Unknown'), ('SAFE', 'Safe'), ('SUSPICIOUS', 'Suspicious'), ('BLOCKED', 'Blocked')], default='UNKNOWN', max_length=16)),
('temporal_splits', models.JSONField(blank=True, default=dict)),
('immutable', models.BooleanField(default=False)),
('dataset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='trading_studio.marketdataset')),
],
),
migrations.CreateModel(
name='BacktestRun',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('split', models.CharField(choices=[('DISCOVERY', 'Discovery'), ('TRAIN', 'Train'), ('VALIDATION', 'Validation'), ('HOLDOUT', 'Holdout'), ('FORWARD', 'Forward'), ('LIVE', 'Live')], max_length=32)),
('execution_model_version', models.CharField(max_length=120)),
('configuration', models.JSONField(default=dict)),
('status', models.CharField(choices=[('QUEUED', 'Queued'), ('RUNNING', 'Running'), ('SUCCEEDED', 'Succeeded'), ('FAILED', 'Failed'), ('INVALID', 'Invalid')], default='QUEUED', max_length=16)),
('metrics', models.JSONField(default=dict)),
('artifact_reference', models.TextField(blank=True)),
('failure_details', models.TextField(blank=True)),
('dataset_version', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='backtest_runs', to='trading_studio.marketdatasetversion')),
('experiment', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='backtests', to='trading_studio.strategyexperiment')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='StrategyVersion',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('version', models.CharField(max_length=120)),
('genome', models.JSONField(default=dict)),
('fingerprint', models.CharField(max_length=128)),
('code_reference', models.TextField(blank=True)),
('stage', models.CharField(choices=[('HYPOTHESIS', 'Hypothesis'), ('BACKTEST', 'Backtest'), ('WALK_FORWARD', 'Walk Forward'), ('ROBUSTNESS', 'Robustness'), ('HOLDOUT', 'Holdout'), ('SHADOW', 'Shadow'), ('MICRO_LIVE', 'Micro Live'), ('PROVEN', 'Proven'), ('CHAMPION', 'Champion'), ('KILLED', 'Killed')], default='HYPOTHESIS', max_length=32)),
('holdout_exposure_count', models.PositiveIntegerField(default=0)),
('immutable', models.BooleanField(default=False)),
('feature_set', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='strategy_versions', to='trading_studio.featuresetversion')),
('parent_version', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='trading_studio.strategyversion')),
('strategy', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='trading_studio.strategy')),
],
),
migrations.AddField(
model_name='strategyexperiment',
name='strategy_version',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='experiments', to='trading_studio.strategyversion'),
),
migrations.CreateModel(
name='StrategyEvaluation',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('stage', models.CharField(choices=[('HYPOTHESIS', 'Hypothesis'), ('BACKTEST', 'Backtest'), ('WALK_FORWARD', 'Walk Forward'), ('ROBUSTNESS', 'Robustness'), ('HOLDOUT', 'Holdout'), ('SHADOW', 'Shadow'), ('MICRO_LIVE', 'Micro Live'), ('PROVEN', 'Proven'), ('CHAMPION', 'Champion'), ('KILLED', 'Killed')], max_length=32)),
('verdict', models.CharField(choices=[('KILL', 'Kill'), ('REVISE', 'Revise'), ('ADVANCE', 'Advance'), ('PROMOTE', 'Promote'), ('DEMOTE', 'Demote'), ('REQUIRE_MORE_EVIDENCE', 'Require More Evidence')], max_length=32)),
('metrics', models.JSONField(default=dict)),
('evidence', models.JSONField(default=dict)),
('failure_type', models.CharField(blank=True, choices=[('NO_ALPHA', 'No Alpha'), ('OVERFIT', 'Overfit'), ('LEAKAGE', 'Leakage'), ('FEE_DESTROYED', 'Fee Destroyed'), ('SLIPPAGE_DESTROYED', 'Slippage Destroyed'), ('LATENCY_DESTROYED', 'Latency Destroyed'), ('REGIME_FRAGILE', 'Regime Fragile'), ('PARAMETER_FRAGILE', 'Parameter Fragile'), ('INSUFFICIENT_TRADES', 'Insufficient Trades'), ('RISK_TOO_HIGH', 'Risk Too High'), ('TAIL_RISK', 'Tail Risk'), ('EXECUTION_FAILURE', 'Execution Failure'), ('DATA_FAILURE', 'Data Failure'), ('LIVE_DIVERGENCE', 'Live Divergence'), ('CAPACITY_LIMIT', 'Capacity Limit'), ('UNKNOWN', 'Unknown')], max_length=32)),
('experiment', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='evaluations', to='trading_studio.strategyexperiment')),
('strategy_version', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='evaluations', to='trading_studio.strategyversion')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='StrategyCapitalAllocation',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('tier', models.CharField(choices=[('RESEARCH', 'Research'), ('SHADOW', 'Shadow'), ('MICRO', 'Micro'), ('VALIDATED', 'Validated'), ('PROVEN', 'Proven'), ('CHAMPION', 'Champion')], max_length=32)),
('amount', models.DecimalField(decimal_places=2, default=0, max_digits=12)),
('active', models.BooleanField(default=False)),
('policy_snapshot', models.JSONField(default=dict)),
('strategy_version', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='capital_allocations', to='trading_studio.strategyversion')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ShadowRun',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('status', models.CharField(choices=[('QUEUED', 'Queued'), ('RUNNING', 'Running'), ('SUCCEEDED', 'Succeeded'), ('FAILED', 'Failed'), ('INVALID', 'Invalid')], default='QUEUED', max_length=16)),
('evidence', models.JSONField(default=dict)),
('strategy_version', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='shadow_runs', to='trading_studio.strategyversion')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='LiveStrategyRun',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('status', models.CharField(choices=[('QUEUED', 'Queued'), ('RUNNING', 'Running'), ('SUCCEEDED', 'Succeeded'), ('FAILED', 'Failed'), ('INVALID', 'Invalid')], default='QUEUED', max_length=16)),
('allocated_capital', models.DecimalField(decimal_places=2, default=0, max_digits=12)),
('safety_policy', models.JSONField(default=dict)),
('reconciliation_status', models.CharField(default='NOT_STARTED', max_length=32)),
('enabled', models.BooleanField(default=False)),
('strategy_version', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='live_runs', to='trading_studio.strategyversion')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='TradeRecord',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('source', models.CharField(default='IMPORTED', max_length=32)),
('lifecycle', models.JSONField(default=dict)),
('gross_pnl', models.FloatField(default=0)),
('fees', models.FloatField(default=0)),
('funding', models.FloatField(default=0)),
('slippage', models.FloatField(default=0)),
('other_execution_cost', models.FloatField(default=0)),
('net_pnl', models.FloatField(default=0)),
('live_run', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='trades', to='trading_studio.livestrategyrun')),
('strategy_version', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='trade_records', to='trading_studio.strategyversion')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='TradingCohort',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=200)),
('status', models.CharField(choices=[('HYPOTHESIS', 'Hypothesis'), ('BACKTEST', 'Backtest'), ('WALK_FORWARD', 'Walk Forward'), ('ROBUSTNESS', 'Robustness'), ('HOLDOUT', 'Holdout'), ('SHADOW', 'Shadow'), ('MICRO_LIVE', 'Micro Live'), ('PROVEN', 'Proven'), ('CHAMPION', 'Champion'), ('KILLED', 'Killed')], default='HYPOTHESIS', max_length=32)),
('policy_snapshot', models.JSONField(default=dict)),
('dataset_version', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='cohorts', to='trading_studio.marketdatasetversion')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='strategyexperiment',
name='cohort',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='experiments', to='trading_studio.tradingcohort'),
),
migrations.CreateModel(
name='TradingProject',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=200)),
('slug', models.SlugField(max_length=120, unique=True)),
('goal', models.TextField()),
('repository_path', models.TextField(blank=True)),
('working_directory', models.TextField(blank=True)),
('profile_name', models.CharField(default='crypto_hyperliquid', max_length=120)),
('existing_system_profile', models.CharField(default='hyperscalper', max_length=120)),
('status', models.CharField(choices=[('ARCHAEOLOGY', 'Archaeology'), ('DATA_VALIDATION', 'Data Validation'), ('BACKTEST_AUDIT', 'Backtest Audit'), ('OFFLINE_ONLY', 'Offline Only'), ('SHADOW_READY', 'Shadow Ready'), ('PAUSED', 'Paused')], default='ARCHAEOLOGY', max_length=32)),
('metadata', models.JSONField(blank=True, default=dict)),
('current_champion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='champion_for_projects', to='trading_studio.strategyversion')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='trading_projects', to='projects.project')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='tradingcohort',
name='trading_project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cohorts', to='trading_studio.tradingproject'),
),
migrations.CreateModel(
name='StrategyPromotionDecision',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('decision', models.CharField(choices=[('KILL', 'Kill'), ('REVISE', 'Revise'), ('ADVANCE', 'Advance'), ('PROMOTE', 'Promote'), ('DEMOTE', 'Demote'), ('REQUIRE_MORE_EVIDENCE', 'Require More Evidence')], max_length=32)),
('policy_snapshot', models.JSONField(default=dict)),
('evidence', models.JSONField(default=dict)),
('reason', models.TextField()),
('candidate', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='promotion_decisions', to='trading_studio.strategyversion')),
('champion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='challenged_by', to='trading_studio.strategyversion')),
('trading_project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='promotion_decisions', to='trading_studio.tradingproject')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='strategy',
name='trading_project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='strategies', to='trading_studio.tradingproject'),
),
migrations.AddField(
model_name='marketdataset',
name='trading_project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='market_datasets', to='trading_studio.tradingproject'),
),
migrations.AddField(
model_name='featuresetversion',
name='trading_project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feature_sets', to='trading_studio.tradingproject'),
),
migrations.AddField(
model_name='featuredefinition',
name='trading_project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='features', to='trading_studio.tradingproject'),
),
migrations.CreateModel(
name='TradingResearchReport',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('report_type', models.CharField(max_length=80)),
('title', models.CharField(max_length=255)),
('content', models.JSONField(default=dict)),
('markdown', models.TextField(blank=True)),
('evidence_status', models.CharField(choices=[('CONFIRMED', 'Confirmed'), ('INFERRED', 'Inferred'), ('UNKNOWN', 'Unknown'), ('SAFE', 'Safe'), ('SUSPICIOUS', 'Suspicious'), ('BLOCKED', 'Blocked')], default='UNKNOWN', max_length=16)),
('trading_project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='research_reports', to='trading_studio.tradingproject')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='tradingcohort',
name='report',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cohorts', to='trading_studio.tradingresearchreport'),
),
migrations.CreateModel(
name='WalkForwardRun',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('folds', models.JSONField(default=list)),
('embargo_bars', models.PositiveIntegerField(default=0)),
('status', models.CharField(choices=[('QUEUED', 'Queued'), ('RUNNING', 'Running'), ('SUCCEEDED', 'Succeeded'), ('FAILED', 'Failed'), ('INVALID', 'Invalid')], default='QUEUED', max_length=16)),
('metrics', models.JSONField(default=dict)),
('dataset_version', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='walk_forward_runs', to='trading_studio.marketdatasetversion')),
('experiment', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='walk_forwards', to='trading_studio.strategyexperiment')),
],
options={
'abstract': False,
},
),
migrations.AddConstraint(
model_name='marketdatasetversion',
constraint=models.UniqueConstraint(fields=('dataset', 'version'), name='unique_trading_market_dataset_version'),
),
migrations.AddConstraint(
model_name='strategyversion',
constraint=models.UniqueConstraint(fields=('strategy', 'version'), name='unique_trading_strategy_version'),
),
migrations.AddConstraint(
model_name='strategyversion',
constraint=models.UniqueConstraint(fields=('strategy', 'fingerprint'), name='unique_trading_strategy_fingerprint'),
),
migrations.AddConstraint(
model_name='marketdataset',
constraint=models.UniqueConstraint(fields=('trading_project', 'name'), name='unique_trading_market_dataset'),
),
migrations.AddConstraint(
model_name='featuresetversion',
constraint=models.UniqueConstraint(fields=('trading_project', 'name', 'version'), name='unique_trading_feature_set_version'),
),
migrations.AddConstraint(
model_name='featuredefinition',
constraint=models.UniqueConstraint(fields=('trading_project', 'name', 'code_hash'), name='unique_trading_feature_definition'),
),
]

View file

@ -0,0 +1,340 @@
from __future__ import annotations
from django.db import models
from control_plane.common import TimestampedModel
class TradingProjectStatus(models.TextChoices):
ARCHAEOLOGY = "ARCHAEOLOGY"
DATA_VALIDATION = "DATA_VALIDATION"
BACKTEST_AUDIT = "BACKTEST_AUDIT"
OFFLINE_ONLY = "OFFLINE_ONLY"
SHADOW_READY = "SHADOW_READY"
PAUSED = "PAUSED"
class DataKind(models.TextChoices):
OHLCV = "OHLCV"
TRADES = "TRADES"
TICKS = "TICKS"
ORDER_BOOK = "ORDER_BOOK"
FUNDING = "FUNDING"
OPEN_INTEREST = "OPEN_INTEREST"
LIQUIDATIONS = "LIQUIDATIONS"
MARK_PRICE = "MARK_PRICE"
INDEX_PRICE = "INDEX_PRICE"
FILL_HISTORY = "FILL_HISTORY"
OTHER = "OTHER"
class EvidenceStatus(models.TextChoices):
CONFIRMED = "CONFIRMED"
INFERRED = "INFERRED"
UNKNOWN = "UNKNOWN"
SAFE = "SAFE"
SUSPICIOUS = "SUSPICIOUS"
BLOCKED = "BLOCKED"
class SplitKind(models.TextChoices):
DISCOVERY = "DISCOVERY"
TRAIN = "TRAIN"
VALIDATION = "VALIDATION"
HOLDOUT = "HOLDOUT"
FORWARD = "FORWARD"
LIVE = "LIVE"
class StrategyStage(models.TextChoices):
HYPOTHESIS = "HYPOTHESIS"
BACKTEST = "BACKTEST"
WALK_FORWARD = "WALK_FORWARD"
ROBUSTNESS = "ROBUSTNESS"
HOLDOUT = "HOLDOUT"
SHADOW = "SHADOW"
MICRO_LIVE = "MICRO_LIVE"
PROVEN = "PROVEN"
CHAMPION = "CHAMPION"
KILLED = "KILLED"
class ExperimentStatus(models.TextChoices):
PROPOSED = "PROPOSED"
RUNNING = "RUNNING"
SUCCEEDED = "SUCCEEDED"
REJECTED = "REJECTED"
FAILED = "FAILED"
class RunStatus(models.TextChoices):
QUEUED = "QUEUED"
RUNNING = "RUNNING"
SUCCEEDED = "SUCCEEDED"
FAILED = "FAILED"
INVALID = "INVALID"
class Decision(models.TextChoices):
KILL = "KILL"
REVISE = "REVISE"
ADVANCE = "ADVANCE"
PROMOTE = "PROMOTE"
DEMOTE = "DEMOTE"
REQUIRE_MORE_EVIDENCE = "REQUIRE_MORE_EVIDENCE"
class FailureType(models.TextChoices):
NO_ALPHA = "NO_ALPHA"
OVERFIT = "OVERFIT"
LEAKAGE = "LEAKAGE"
FEE_DESTROYED = "FEE_DESTROYED"
SLIPPAGE_DESTROYED = "SLIPPAGE_DESTROYED"
LATENCY_DESTROYED = "LATENCY_DESTROYED"
REGIME_FRAGILE = "REGIME_FRAGILE"
PARAMETER_FRAGILE = "PARAMETER_FRAGILE"
INSUFFICIENT_TRADES = "INSUFFICIENT_TRADES"
RISK_TOO_HIGH = "RISK_TOO_HIGH"
TAIL_RISK = "TAIL_RISK"
EXECUTION_FAILURE = "EXECUTION_FAILURE"
DATA_FAILURE = "DATA_FAILURE"
LIVE_DIVERGENCE = "LIVE_DIVERGENCE"
CAPACITY_LIMIT = "CAPACITY_LIMIT"
UNKNOWN = "UNKNOWN"
class AllocationTier(models.TextChoices):
RESEARCH = "RESEARCH"
SHADOW = "SHADOW"
MICRO = "MICRO"
VALIDATED = "VALIDATED"
PROVEN = "PROVEN"
CHAMPION = "CHAMPION"
class TradingProject(TimestampedModel):
project = models.ForeignKey("projects.Project", on_delete=models.PROTECT, related_name="trading_projects")
name = models.CharField(max_length=200)
slug = models.SlugField(max_length=120, unique=True)
goal = models.TextField()
repository_path = models.TextField(blank=True)
working_directory = models.TextField(blank=True)
profile_name = models.CharField(max_length=120, default="crypto_hyperliquid")
existing_system_profile = models.CharField(max_length=120, default="hyperscalper")
status = models.CharField(max_length=32, choices=TradingProjectStatus.choices, default=TradingProjectStatus.ARCHAEOLOGY)
current_champion = models.ForeignKey("StrategyVersion", on_delete=models.SET_NULL, null=True, blank=True, related_name="champion_for_projects")
metadata = models.JSONField(default=dict, blank=True)
class MarketDataset(TimestampedModel):
trading_project = models.ForeignKey(TradingProject, on_delete=models.CASCADE, related_name="market_datasets")
name = models.CharField(max_length=200)
kind = models.CharField(max_length=32, choices=DataKind.choices)
venue = models.CharField(max_length=120, blank=True)
symbol = models.CharField(max_length=120, blank=True)
market_type = models.CharField(max_length=80, blank=True)
metadata = models.JSONField(default=dict, blank=True)
class Meta:
constraints = [models.UniqueConstraint(fields=["trading_project", "name"], name="unique_trading_market_dataset")]
class MarketDatasetVersion(TimestampedModel):
dataset = models.ForeignKey(MarketDataset, on_delete=models.CASCADE, related_name="versions")
version = models.CharField(max_length=120)
reference = models.TextField()
content_hash = models.CharField(max_length=128)
processing_version = models.CharField(max_length=120, blank=True)
start_at = models.DateTimeField(null=True, blank=True)
end_at = models.DateTimeField(null=True, blank=True)
resolution = models.CharField(max_length=80, blank=True)
record_count = models.BigIntegerField(null=True, blank=True)
fields = models.JSONField(default=list, blank=True)
quality = models.JSONField(default=dict, blank=True)
lookahead_risk_status = models.CharField(max_length=16, choices=EvidenceStatus.choices, default=EvidenceStatus.UNKNOWN)
quality_status = models.CharField(max_length=16, choices=EvidenceStatus.choices, default=EvidenceStatus.UNKNOWN)
temporal_splits = models.JSONField(default=dict, blank=True)
immutable = models.BooleanField(default=False)
class Meta:
constraints = [models.UniqueConstraint(fields=["dataset", "version"], name="unique_trading_market_dataset_version")]
def save(self, *args, **kwargs):
if self.pk and self.immutable:
original = type(self).objects.get(pk=self.pk)
fields = ["version", "reference", "content_hash", "processing_version", "start_at", "end_at", "resolution", "record_count", "fields", "quality", "lookahead_risk_status", "temporal_splits"]
if any(getattr(self, field) != getattr(original, field) for field in fields):
raise ValueError("MarketDatasetVersion is immutable after research use.")
super().save(*args, **kwargs)
class FeatureDefinition(TimestampedModel):
trading_project = models.ForeignKey(TradingProject, on_delete=models.CASCADE, related_name="features")
name = models.CharField(max_length=160)
family = models.CharField(max_length=120, blank=True)
implementation_reference = models.TextField()
code_hash = models.CharField(max_length=128, blank=True)
lookback = models.JSONField(default=dict, blank=True)
dependencies = models.JSONField(default=list, blank=True)
normalization = models.JSONField(default=dict, blank=True)
timeframe = models.CharField(max_length=80, blank=True)
leakage_status = models.CharField(max_length=16, choices=EvidenceStatus.choices, default=EvidenceStatus.UNKNOWN)
evidence = models.JSONField(default=dict, blank=True)
class Meta:
constraints = [models.UniqueConstraint(fields=["trading_project", "name", "code_hash"], name="unique_trading_feature_definition")]
class FeatureSetVersion(TimestampedModel):
trading_project = models.ForeignKey(TradingProject, on_delete=models.CASCADE, related_name="feature_sets")
name = models.CharField(max_length=160)
version = models.CharField(max_length=120)
feature_definitions = models.ManyToManyField(FeatureDefinition, related_name="feature_sets")
content_hash = models.CharField(max_length=128)
immutable = models.BooleanField(default=False)
class Meta:
constraints = [models.UniqueConstraint(fields=["trading_project", "name", "version"], name="unique_trading_feature_set_version")]
class Strategy(TimestampedModel):
trading_project = models.ForeignKey(TradingProject, on_delete=models.CASCADE, related_name="strategies")
name = models.CharField(max_length=200)
strategy_class = models.CharField(max_length=80, blank=True)
description = models.TextField(blank=True)
metadata = models.JSONField(default=dict, blank=True)
class StrategyVersion(TimestampedModel):
strategy = models.ForeignKey(Strategy, on_delete=models.CASCADE, related_name="versions")
version = models.CharField(max_length=120)
parent_version = models.ForeignKey("self", on_delete=models.SET_NULL, null=True, blank=True, related_name="children")
feature_set = models.ForeignKey(FeatureSetVersion, on_delete=models.PROTECT, null=True, blank=True, related_name="strategy_versions")
genome = models.JSONField(default=dict)
fingerprint = models.CharField(max_length=128)
code_reference = models.TextField(blank=True)
stage = models.CharField(max_length=32, choices=StrategyStage.choices, default=StrategyStage.HYPOTHESIS)
holdout_exposure_count = models.PositiveIntegerField(default=0)
immutable = models.BooleanField(default=False)
class Meta:
constraints = [models.UniqueConstraint(fields=["strategy", "version"], name="unique_trading_strategy_version"), models.UniqueConstraint(fields=["strategy", "fingerprint"], name="unique_trading_strategy_fingerprint")]
def save(self, *args, **kwargs):
if self.pk and self.immutable:
original = type(self).objects.get(pk=self.pk)
if any(getattr(self, field) != getattr(original, field) for field in ["version", "parent_version_id", "feature_set_id", "genome", "fingerprint", "code_reference"]):
raise ValueError("Live or evaluated StrategyVersion is immutable; create a child version.")
super().save(*args, **kwargs)
class TradingCohort(TimestampedModel):
trading_project = models.ForeignKey(TradingProject, on_delete=models.CASCADE, related_name="cohorts")
name = models.CharField(max_length=200)
status = models.CharField(max_length=32, choices=StrategyStage.choices, default=StrategyStage.HYPOTHESIS)
policy_snapshot = models.JSONField(default=dict)
dataset_version = models.ForeignKey(MarketDatasetVersion, on_delete=models.PROTECT, related_name="cohorts")
report = models.ForeignKey("TradingResearchReport", on_delete=models.SET_NULL, null=True, blank=True, related_name="cohorts")
class StrategyExperiment(TimestampedModel):
cohort = models.ForeignKey(TradingCohort, on_delete=models.CASCADE, related_name="experiments")
strategy_version = models.ForeignKey(StrategyVersion, on_delete=models.PROTECT, related_name="experiments")
hypothesis = models.TextField()
market_rationale = models.TextField()
expected_regime = models.JSONField(default=dict)
controls = models.JSONField(default=dict)
success_criteria = models.JSONField(default=dict)
rejection_criteria = models.JSONField(default=dict)
risk_assumptions = models.JSONField(default=dict)
execution_assumptions = models.JSONField(default=dict)
estimated_evaluation_cost = models.JSONField(default=dict)
status = models.CharField(max_length=32, choices=ExperimentStatus.choices, default=ExperimentStatus.PROPOSED)
conclusion = models.CharField(max_length=32, choices=Decision.choices, blank=True)
failure_type = models.CharField(max_length=32, choices=FailureType.choices, blank=True)
class BacktestRun(TimestampedModel):
experiment = models.ForeignKey(StrategyExperiment, on_delete=models.PROTECT, related_name="backtests")
dataset_version = models.ForeignKey(MarketDatasetVersion, on_delete=models.PROTECT, related_name="backtest_runs")
split = models.CharField(max_length=32, choices=SplitKind.choices)
execution_model_version = models.CharField(max_length=120)
configuration = models.JSONField(default=dict)
status = models.CharField(max_length=16, choices=RunStatus.choices, default=RunStatus.QUEUED)
metrics = models.JSONField(default=dict)
artifact_reference = models.TextField(blank=True)
failure_details = models.TextField(blank=True)
class WalkForwardRun(TimestampedModel):
experiment = models.ForeignKey(StrategyExperiment, on_delete=models.PROTECT, related_name="walk_forwards")
dataset_version = models.ForeignKey(MarketDatasetVersion, on_delete=models.PROTECT, related_name="walk_forward_runs")
folds = models.JSONField(default=list)
embargo_bars = models.PositiveIntegerField(default=0)
status = models.CharField(max_length=16, choices=RunStatus.choices, default=RunStatus.QUEUED)
metrics = models.JSONField(default=dict)
class ShadowRun(TimestampedModel):
strategy_version = models.ForeignKey(StrategyVersion, on_delete=models.PROTECT, related_name="shadow_runs")
status = models.CharField(max_length=16, choices=RunStatus.choices, default=RunStatus.QUEUED)
evidence = models.JSONField(default=dict)
class LiveStrategyRun(TimestampedModel):
strategy_version = models.ForeignKey(StrategyVersion, on_delete=models.PROTECT, related_name="live_runs")
status = models.CharField(max_length=16, choices=RunStatus.choices, default=RunStatus.QUEUED)
allocated_capital = models.DecimalField(max_digits=12, decimal_places=2, default=0)
safety_policy = models.JSONField(default=dict)
reconciliation_status = models.CharField(max_length=32, default="NOT_STARTED")
enabled = models.BooleanField(default=False)
class TradeRecord(TimestampedModel):
strategy_version = models.ForeignKey(StrategyVersion, on_delete=models.PROTECT, related_name="trade_records")
live_run = models.ForeignKey(LiveStrategyRun, on_delete=models.SET_NULL, null=True, blank=True, related_name="trades")
source = models.CharField(max_length=32, default="IMPORTED")
lifecycle = models.JSONField(default=dict)
gross_pnl = models.FloatField(default=0)
fees = models.FloatField(default=0)
funding = models.FloatField(default=0)
slippage = models.FloatField(default=0)
other_execution_cost = models.FloatField(default=0)
net_pnl = models.FloatField(default=0)
class StrategyEvaluation(TimestampedModel):
strategy_version = models.ForeignKey(StrategyVersion, on_delete=models.PROTECT, related_name="evaluations")
experiment = models.ForeignKey(StrategyExperiment, on_delete=models.PROTECT, related_name="evaluations")
stage = models.CharField(max_length=32, choices=StrategyStage.choices)
verdict = models.CharField(max_length=32, choices=Decision.choices)
metrics = models.JSONField(default=dict)
evidence = models.JSONField(default=dict)
failure_type = models.CharField(max_length=32, choices=FailureType.choices, blank=True)
class StrategyPromotionDecision(TimestampedModel):
trading_project = models.ForeignKey(TradingProject, on_delete=models.CASCADE, related_name="promotion_decisions")
candidate = models.ForeignKey(StrategyVersion, on_delete=models.PROTECT, related_name="promotion_decisions")
champion = models.ForeignKey(StrategyVersion, on_delete=models.SET_NULL, null=True, blank=True, related_name="challenged_by")
decision = models.CharField(max_length=32, choices=Decision.choices)
policy_snapshot = models.JSONField(default=dict)
evidence = models.JSONField(default=dict)
reason = models.TextField()
class StrategyCapitalAllocation(TimestampedModel):
strategy_version = models.ForeignKey(StrategyVersion, on_delete=models.PROTECT, related_name="capital_allocations")
tier = models.CharField(max_length=32, choices=AllocationTier.choices)
amount = models.DecimalField(max_digits=12, decimal_places=2, default=0)
active = models.BooleanField(default=False)
policy_snapshot = models.JSONField(default=dict)
class TradingResearchReport(TimestampedModel):
trading_project = models.ForeignKey(TradingProject, on_delete=models.CASCADE, related_name="research_reports")
report_type = models.CharField(max_length=80)
title = models.CharField(max_length=255)
content = models.JSONField(default=dict)
markdown = models.TextField(blank=True)
evidence_status = models.CharField(max_length=16, choices=EvidenceStatus.choices, default=EvidenceStatus.UNKNOWN)

View file

@ -0,0 +1,54 @@
from __future__ import annotations
import hashlib
from pathlib import Path
from typing import Protocol
class TradingProjectProfile(Protocol):
name: str
def archaeology(self, repository_path: str) -> dict: ...
class CryptoTradingProfile:
name = "crypto"
def archaeology(self, repository_path: str) -> dict:
root = Path(repository_path)
return {
"profile": self.name,
"repository": str(root),
"status": "UNKNOWN" if not root.exists() else "CONFIRMED",
"datasets": [],
"findings": [],
}
class HyperliquidProfile(CryptoTradingProfile):
name = "crypto_hyperliquid"
class ExistingSystemProfile(Protocol):
name: str
def feature_inventory(self, repository_path: str) -> list[dict]: ...
class HyperScalperProfile:
name = "hyperscalper"
def feature_inventory(self, repository_path: str) -> list[dict]:
root = Path(repository_path)
candidates = [
root / "src" / "hyperscalper" / "fast_engine.py",
root / "src" / "hyperscalper" / "indicators_depth_nb.py",
root / "src" / "hyperscalper" / "indicator_library.py",
]
rows = []
for path in candidates:
if not path.exists():
continue
digest = hashlib.sha256(path.read_bytes()).hexdigest()
rows.append({"name": path.stem, "family": "HYPERSCALPER_IMPORT", "implementation_reference": str(path), "code_hash": digest})
return rows

View file

@ -0,0 +1,189 @@
from __future__ import annotations
import hashlib
import json
from datetime import datetime
from decimal import Decimal
from typing import Any
from django.db import transaction
from control_plane.events.bus import EventBus
from control_plane.projects.models import Project, ProjectStatus
from control_plane.trading_studio.models import (
AllocationTier, BacktestRun, DataKind, EvidenceStatus, ExperimentStatus,
FailureType, FeatureDefinition, FeatureSetVersion, LiveStrategyRun,
MarketDataset, MarketDatasetVersion, RunStatus, ShadowRun, SplitKind,
Strategy, StrategyCapitalAllocation, StrategyEvaluation, StrategyExperiment,
StrategyPromotionDecision, StrategyStage, StrategyVersion, TradingCohort,
TradingProject, TradingProjectStatus, TradingResearchReport,
)
from control_plane.trading_studio.profiles import CryptoTradingProfile, ExistingSystemProfile, HyperScalperProfile, TradingProjectProfile
class TradingStudioService:
"""Canonical, offline-first trading research service.
This service intentionally has no exchange adapter. A future live stage must
be separately authorized and supplied with a deterministic risk executor.
"""
def __init__(self, *, profile: TradingProjectProfile | None = None, existing_system: ExistingSystemProfile | None = None, bus: EventBus | None = None) -> None:
self.profile = profile or CryptoTradingProfile()
self.existing_system = existing_system or HyperScalperProfile()
self.bus = bus or EventBus()
def import_hyperscalper(self, *, repository_path: str, slug: str = "crypto-hyperscalper") -> TradingProject:
project, _ = Project.objects.get_or_create(
name="Crypto Trading Studio", project_type="TRADING",
defaults={"goal": "Falsify trading hypotheses before any capital allocation.", "repository_path": repository_path, "status": ProjectStatus.ARCHAEOLOGY},
)
trading_project, _ = TradingProject.objects.update_or_create(
slug=slug,
defaults={
"project": project, "name": "Crypto / Hyperliquid Strategy Lab",
"goal": "Prefer no strategy to an overfit strategy.",
"repository_path": repository_path, "profile_name": self.profile.name,
"existing_system_profile": self.existing_system.name,
"status": TradingProjectStatus.ARCHAEOLOGY,
"metadata": {"live_execution_enabled": False, "maximum_automatic_live_capital": "0", "safety_note": "Trading Studio V0.1 is offline-only. No exchange calls are implemented."},
},
)
self._event("TRADING_PROJECT_IMPORTED", trading_project, {"repository_path": repository_path})
return trading_project
def register_market_dataset(self, trading_project: TradingProject, *, name: str, kind: str, version: str, reference: str, content_hash: str, fields: list[str], record_count: int, start_at: datetime | None, end_at: datetime | None, resolution: str, quality: dict[str, Any], temporal_splits: dict[str, Any]) -> MarketDatasetVersion:
if kind not in DataKind.values:
raise ValueError("Unknown market data kind.")
self._validate_temporal_splits(temporal_splits)
dataset, _ = MarketDataset.objects.get_or_create(trading_project=trading_project, name=name, defaults={"kind": kind})
if dataset.kind != kind:
raise ValueError("Market dataset kind cannot change after registration.")
return MarketDatasetVersion.objects.create(
dataset=dataset, version=version, reference=reference, content_hash=content_hash,
fields=fields, record_count=record_count, start_at=start_at, end_at=end_at,
resolution=resolution, quality=quality, temporal_splits=temporal_splits,
lookahead_risk_status=EvidenceStatus.UNKNOWN, quality_status=EvidenceStatus.SUSPICIOUS,
)
def import_features(self, trading_project: TradingProject) -> list[FeatureDefinition]:
features = []
for item in self.existing_system.feature_inventory(trading_project.repository_path):
feature, _ = FeatureDefinition.objects.get_or_create(
trading_project=trading_project, name=item["name"], code_hash=item["code_hash"],
defaults={"family": item["family"], "implementation_reference": item["implementation_reference"], "leakage_status": EvidenceStatus.UNKNOWN},
)
features.append(feature)
return features
def create_feature_set(self, trading_project: TradingProject, *, name: str, version: str, features: list[FeatureDefinition]) -> FeatureSetVersion:
if any(feature.trading_project_id != trading_project.id for feature in features):
raise ValueError("Feature sets cannot cross trading projects.")
content_hash = self._hash({"features": sorted(str(feature.id) for feature in features)})
feature_set = FeatureSetVersion.objects.create(trading_project=trading_project, name=name, version=version, content_hash=content_hash)
feature_set.feature_definitions.set(features)
return feature_set
def create_strategy_version(self, trading_project: TradingProject, *, name: str, genome: dict[str, Any], feature_set: FeatureSetVersion | None = None, parent_version: StrategyVersion | None = None) -> StrategyVersion:
if self._contains_prohibited_sizing(genome):
raise ValueError("Martingale, loss chasing, and uncapped averaging are prohibited.")
if feature_set and feature_set.feature_definitions.filter(leakage_status=EvidenceStatus.BLOCKED).exists():
raise ValueError("Strategies using BLOCKED features cannot qualify.")
fingerprint = self._hash({"genome": genome, "feature_set": str(feature_set.id) if feature_set else ""})
strategy, _ = Strategy.objects.get_or_create(trading_project=trading_project, name=name)
return StrategyVersion.objects.create(strategy=strategy, version=f"v{strategy.versions.count() + 1}", parent_version=parent_version, feature_set=feature_set, genome=genome, fingerprint=fingerprint)
def propose_experiment(self, cohort: TradingCohort, strategy_version: StrategyVersion, contract: dict[str, Any]) -> StrategyExperiment:
required = ["hypothesis", "market_rationale", "expected_regime", "controls", "success_criteria", "rejection_criteria", "risk_assumptions", "execution_assumptions", "estimated_evaluation_cost"]
missing = [name for name in required if contract.get(name) in (None, "", {}, [])]
if missing:
raise ValueError("Incomplete strategy experiment contract: " + ", ".join(missing))
if strategy_version.strategy.trading_project_id != cohort.trading_project_id:
raise ValueError("Strategy and cohort must belong to the same TradingProject.")
if strategy_version.holdout_exposure_count and contract["controls"].get("uses_holdout_for_design"):
raise ValueError("Consumed holdout cannot be used for adaptive strategy design.")
return StrategyExperiment.objects.create(cohort=cohort, strategy_version=strategy_version, **{key: contract[key] for key in required})
@transaction.atomic
def record_backtest(self, experiment: StrategyExperiment, *, split: str, execution_model_version: str, metrics: dict[str, Any], configuration: dict[str, Any], artifact_reference: str = "") -> BacktestRun:
if split not in SplitKind.values:
raise ValueError("Unknown temporal split.")
if configuration.get("same_bar_close_execution"):
raise ValueError("Same-bar close execution is prohibited.")
required = ["gross_pnl", "fees", "funding", "slippage", "other_execution_cost", "net_pnl", "trade_count"]
missing = [name for name in required if name not in metrics]
if missing:
raise ValueError("Backtest metrics missing: " + ", ".join(missing))
computed_net = float(metrics["gross_pnl"]) - float(metrics["fees"]) - float(metrics["funding"]) - float(metrics["slippage"]) - float(metrics["other_execution_cost"])
if abs(computed_net - float(metrics["net_pnl"])) > 1e-9:
raise ValueError("Net PnL must equal gross PnL minus all execution costs.")
run = BacktestRun.objects.create(experiment=experiment, dataset_version=experiment.cohort.dataset_version, split=split, execution_model_version=execution_model_version, configuration=configuration, metrics=metrics, artifact_reference=artifact_reference, status=RunStatus.SUCCEEDED)
if split == SplitKind.HOLDOUT:
version = experiment.strategy_version
version.holdout_exposure_count += 1
version.immutable = True
version.save(update_fields=["holdout_exposure_count", "immutable", "updated_at"])
return run
def judge_backtest(self, experiment: StrategyExperiment, backtest: BacktestRun, *, policy: dict[str, Any]) -> StrategyEvaluation:
metrics = backtest.metrics
failure = ""
verdict = "ADVANCE"
if float(metrics["net_pnl"]) < 0 <= float(metrics["gross_pnl"]):
failure, verdict = FailureType.FEE_DESTROYED, "KILL"
elif float(metrics["net_pnl"]) < 0:
failure, verdict = FailureType.NO_ALPHA, "KILL"
elif int(metrics["trade_count"]) < int(policy.get("minimum_trade_count", 0)):
failure, verdict = FailureType.INSUFFICIENT_TRADES, "REQUIRE_MORE_EVIDENCE"
elif float(metrics.get("pnl_concentration_top_trade", 0)) > float(policy.get("maximum_top_trade_concentration", 1)):
failure, verdict = FailureType.OVERFIT, "KILL"
evaluation = StrategyEvaluation.objects.create(strategy_version=experiment.strategy_version, experiment=experiment, stage=StrategyStage.BACKTEST, verdict=verdict, metrics=metrics, evidence={"policy": policy, "execution_model": backtest.execution_model_version}, failure_type=failure)
if verdict == "KILL":
experiment.status, experiment.conclusion, experiment.failure_type = ExperimentStatus.REJECTED, verdict, failure
experiment.strategy_version.stage = StrategyStage.KILLED
experiment.strategy_version.save(update_fields=["stage", "updated_at"])
else:
experiment.status, experiment.conclusion = ExperimentStatus.SUCCEEDED, verdict
experiment.save(update_fields=["status", "conclusion", "failure_type", "updated_at"])
return evaluation
def request_shadow(self, strategy_version: StrategyVersion) -> ShadowRun:
if strategy_version.stage not in {StrategyStage.HOLDOUT, StrategyStage.SHADOW}:
raise ValueError("Shadow requires a holdout-qualified immutable strategy version.")
return ShadowRun.objects.create(strategy_version=strategy_version, status=RunStatus.QUEUED, evidence={"mode": "PREPARED_ONLY", "live_execution_enabled": False})
def request_micro_live(self, strategy_version: StrategyVersion, amount: Decimal) -> LiveStrategyRun:
raise ValueError("Trading Studio V0.1 is offline-only. Explicit human authorization and a future deterministic execution/risk adapter are required before micro-live.")
def report(self, trading_project: TradingProject, *, title: str = "Trading cohort report") -> TradingResearchReport:
experiments = list(StrategyExperiment.objects.filter(cohort__trading_project=trading_project).select_related("strategy_version"))
payload = {"project": trading_project.slug, "status": trading_project.status, "offline_only": not trading_project.metadata.get("live_execution_enabled", False), "experiments": [{"strategy": item.strategy_version.strategy.name, "version": item.strategy_version.version, "status": item.status, "conclusion": item.conclusion, "failure_type": item.failure_type} for item in experiments], "counts": {"proposed": sum(item.status == ExperimentStatus.PROPOSED for item in experiments), "rejected": sum(item.status == ExperimentStatus.REJECTED for item in experiments), "survivors": sum(item.status == ExperimentStatus.SUCCEEDED for item in experiments)}}
return TradingResearchReport.objects.create(trading_project=trading_project, report_type="COHORT", title=title, content=payload, markdown="# TRADING COHORT REPORT\n\n" + json.dumps(payload, indent=2), evidence_status=EvidenceStatus.CONFIRMED)
def _validate_temporal_splits(self, splits: dict[str, Any]) -> None:
if splits.get("split_method") == "random":
raise ValueError("Random time-series splits are prohibited.")
ordered = [splits.get(kind) for kind in ["DISCOVERY", "TRAIN", "VALIDATION", "HOLDOUT", "FORWARD", "LIVE"] if splits.get(kind)]
previous_end = None
for item in ordered:
start, end = item.get("start"), item.get("end")
if not start or not end:
raise ValueError("Temporal split boundaries are required.")
if start >= end or previous_end and start < previous_end:
raise ValueError("Temporal splits must be chronological and non-overlapping.")
previous_end = end
def _contains_prohibited_sizing(self, value: Any) -> bool:
prohibited = ("martingale", "double_after_loss", "averaging_down", "loss_chasing", "negative_progression", "uncapped_grid")
if isinstance(value, dict):
return any(self._contains_prohibited_sizing(key) or self._contains_prohibited_sizing(item) for key, item in value.items())
if isinstance(value, list):
return any(self._contains_prohibited_sizing(item) for item in value)
return any(term in str(value).lower() for term in prohibited)
def _event(self, event_type: str, trading_project: TradingProject, payload: dict[str, Any]) -> None:
self.bus.publish(project=trading_project.project, event_type=event_type, payload=payload)
@staticmethod
def _hash(value: Any) -> str:
return hashlib.sha256(json.dumps(value, sort_keys=True, default=str).encode()).hexdigest()

View file

@ -0,0 +1,20 @@
from django.shortcuts import get_object_or_404, render
from control_plane.trading_studio.models import TradingProject
def trading_studio(request):
projects = TradingProject.objects.select_related("project", "current_champion").order_by("name")
return render(request, "control_plane/trading_studio.html", {"trading_projects": projects})
def trading_studio_project(request, project_id):
trading_project = get_object_or_404(TradingProject.objects.select_related("project", "current_champion"), id=project_id)
return render(request, "control_plane/trading_studio_project.html", {
"trading_project": trading_project,
"datasets": trading_project.market_datasets.prefetch_related("versions").all(),
"features": trading_project.features.order_by("name"),
"strategies": trading_project.strategies.prefetch_related("versions").all(),
"cohorts": trading_project.cohorts.prefetch_related("experiments").all(),
"reports": trading_project.research_reports.order_by("-created_at")[:10],
})

View file

@ -0,0 +1,83 @@
# Trading Program Understanding
## Scope
This report records read-only archaeology of `S:\PycharmProjects\SquadWatch`,
the Spark replay workspace, and the Pi service state. No exchange action,
credential access, service restart, or database mutation occurred.
## Current Decision
**CONFIRMED: no existing strategy qualifies for Shadow Mode from the latest
Spark evidence.** The latest 45-day selection/final comparison selected 50
families and recorded `survivor_count: 0`. This is a successful falsification
result, not a reason to loosen the gate.
## Data
| Status | Evidence |
| --- | --- |
| CONFIRMED | Spark snapshot `/home/daniel/hyperscalper-replay/binance_btcusd_2m_455d.csv` has 327,599 BTCUSD 2-minute OHLCV rows with `timestamp,open,high,low,close,volume`. Its observed range is Unix `1743840960` through `1783152720`. |
| CONFIRMED | Spark has a chronologically separated 90-day OOS snapshot and 45-day selection/final partitions: `binance_btcusd_2m_oos_90d.csv`, `binance_btcusd_2m_oos_select_45d.csv`, and `binance_btcusd_2m_oos_final_45d.csv`. The selection/final boundary is adjacent, not shuffled. |
| CONFIRMED | The newest discovered Spark HyperScalper artifacts were written 2026-08-08. They are not current market data on 2026-08-17. |
| CONFIRMED | Pi logs were updated 2026-08-17, but `hyperscalper-live.service` and `hyperscalper-risk50-paper.service` were inactive during the read-only check. |
| UNKNOWN | Current live exchange positions, fills, and account equity were not queried. Trading Studio V0.1 intentionally does not make exchange calls. |
| UNKNOWN | Funding, open interest, order book, tick, and liquidation datasets were not located in the verified Spark replay input. They must not be implied by OHLCV availability. |
## HyperScalper Architecture
| Status | Evidence |
| --- | --- |
| CONFIRMED | The source system is `src/hyperscalper` in SquadWatch. It contains indicator/search code, paper replay, feed, bot, executor, risk, and Postgres/SQLite journaling components. |
| CONFIRMED | Live topology documented in `docs/hyperscalper_pi_deployments.md` is feed -> paper/signal authority -> webhook-only live runner -> Hyperliquid executor. Documented initial live controls were single position, 1x leverage, and max notional 20. |
| CONFIRMED | The Pi documentation states a known accounting limitation: bot-managed exits may record local trigger price/PnL rather than the actual exchange close fill. Historical live PnL therefore needs reconciliation before it is treated as canonical. |
| INFERRED | Existing execution controls are useful source material but cannot be trusted as an Artifex execution adapter until restart recovery, idempotency, account-level risk persistence, and reconciliation are audited. |
## Features And Leakage
| Status | Evidence |
| --- | --- |
| CONFIRMED | `fast_engine.py`, `indicators_depth_nb.py`, and `indicator_library.py` provide a large multi-family feature universe. The implementation uses trend, oscillator, volatility, channel, pivot, time, and candle-derived signals. |
| CONFIRMED | The active paper-like replay computes signal at completed-bar close and enters at next-bar open with intrabar TP/SL handling. |
| BLOCKED | The high-throughput search kernel enters at same-bar close while the paper replay uses next-bar open. Search PnL is not execution-equivalent evidence and cannot qualify a strategy. |
| BLOCKED | Search volatility percentile calibration is derived from the full evaluated window in `fast_engine.py`; this leaks later observations into early threshold calibration. It must be fit on train only and frozen before validation/holdout. |
| SUSPICIOUS | Existing walk-forward code has no documented purge/embargo around position/cooldown boundaries. Future Artifex folds must embargo at least the maximum holding period plus cooldown. |
## Existing Strategy Evidence
| Status | Evidence |
| --- | --- |
| CONFIRMED | Spark report `/home/daniel/hyperscalper-replay/reports/oos_45d_selection_final_comparison.json` applied its stated selection rule to 50 candidates and reported zero final survivors. |
| CONFIRMED | The selection candidates exhibited strong prior-window PFs, while the displayed final-window results were negative with PF below one. This is direct evidence of regime/search fragility, not a live execution result. |
| CONFIRMED | `walkforward_14d_14d.json` states it used a read-only transaction and that selection used only fully completed training trades. It is useful historical evidence, but its thresholds and data partition policy must be revalidated by Artifex before reuse. |
| UNKNOWN | The local 14-row SQLite live journal is too small to establish a live alpha conclusion. The documented main 60-day paper database was not present in the inspected local workspace. |
## Backtester Integrity Assessment
**PARTIAL / NOT QUALIFIED FOR PROMOTION**
- Reuse candidate: `src/hyperscalper/paper_replay.py` because its timing model is closer to intended paper/live semantics.
- Reject for qualification: `fast_engine.evaluate_combo()` and the close-only kernel until same-bar and full-window calibration issues are repaired.
- Required before a future cohort: immutable source hash, chronological discovery/train/validation/holdout split, train-only thresholds, purge/embargo, fixed cost version, and next-open/intrabar conservative replay.
- Required before Shadow: a fresh data snapshot after the 2026-08-08 Spark cutoff, a successful independent cohort, and an Artifex shadow runner that records intended order, observed market evolution, and simulated fill without exchange submission.
## V0.1 Cohort Policy
The first Artifex cohort is bounded to 20 pre-existing, fingerprint-deduplicated
strategy genomes. It must run the paper-like replay with frozen data/splits and
cost stress. The latest Spark report already proves that the current 50-candidate
selection did not survive its final partition. Artifex must preserve that failure
knowledge and must not promote any member of that family from those results.
## Shadow Readiness
**NOT READY.** No shadow command is authorized to start yet. Once a new current
market snapshot is registered, has passed quality validation, and an offline
cohort produces a holdout-qualified survivor, the future explicit command is:
```powershell
python manage.py trading_shadow --project crypto-hyperscalper --strategy-version <uuid> --dry-run
```
That command is intentionally not implemented in V0.1 because it would create a
false impression that shadow qualification is currently satisfied.

View file

@ -47,6 +47,7 @@
<a href="/">Dashboard</a>
<a href="/projects/">Projects</a>
<a href="/model-studio/">Model Studio</a>
<a href="/trading-studio/">Trading Studio</a>
<a href="/agents/">Agents</a>
<a href="/progeny/">Progeny</a>
<a href="/steward/">Steward</a>

View file

@ -0,0 +1,9 @@
{% extends "control_plane/base.html" %}
{% block content %}
<section class="page-header"><div><p class="eyebrow">Offline-first quantitative research</p><h1>Trading Studio</h1><p>Scientific strategy research. No live execution is enabled in V0.1.</p></div></section>
<section class="card-grid">
{% for item in trading_projects %}
<a class="card" href="{% url 'trading_studio_project' item.id %}"><p class="eyebrow">{{ item.status }}</p><h2>{{ item.name }}</h2><p>{{ item.goal }}</p><p>Champion: {% if item.current_champion %}{{ item.current_champion.strategy.name }} {{ item.current_champion.version }}{% else %}None{% endif %}</p></a>
{% empty %}<div class="empty-state"><h2>No trading projects</h2><p>Import an existing system before proposing strategies.</p></div>{% endfor %}
</section>
{% endblock %}

View file

@ -0,0 +1,9 @@
{% extends "control_plane/base.html" %}
{% block content %}
<section class="page-header"><div><p class="eyebrow">{{ trading_project.status }}</p><h1>{{ trading_project.name }}</h1><p>{{ trading_project.goal }}</p></div></section>
<section class="panel"><h2>Safety status</h2><p>Live execution enabled: {{ trading_project.metadata.live_execution_enabled|yesno:"yes,no" }}</p><p>Automatic live capital: {{ trading_project.metadata.maximum_automatic_live_capital }}</p></section>
<section class="panel"><h2>Data</h2><ul>{% for dataset in datasets %}<li>{{ dataset.name }} ({{ dataset.kind }}){% for version in dataset.versions.all %}: {{ version.version }} / {{ version.record_count }} records / {{ version.quality_status }}{% endfor %}</li>{% empty %}<li>No registered datasets.</li>{% endfor %}</ul></section>
<section class="panel"><h2>Feature library</h2><p>{{ features|length }} imported feature implementations.</p><ul>{% for feature in features %}<li>{{ feature.name }}: {{ feature.leakage_status }}</li>{% endfor %}</ul></section>
<section class="panel"><h2>Strategies and cohorts</h2><ul>{% for strategy in strategies %}<li>{{ strategy.name }}{% for version in strategy.versions.all %}: {{ version.version }} / {{ version.stage }}{% endfor %}</li>{% empty %}<li>No strategies registered.</li>{% endfor %}</ul><p>Cohorts: {{ cohorts|length }}</p></section>
<section class="panel"><h2>Research reports</h2><ul>{% for report in reports %}<li>{{ report.created_at }}: {{ report.title }} ({{ report.evidence_status }})</li>{% empty %}<li>No reports yet.</li>{% endfor %}</ul></section>
{% endblock %}

View file

@ -0,0 +1,99 @@
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