Add offline-first Trading Studio v0.1
This commit is contained in:
parent
0c716660cb
commit
65f0d510ec
20 changed files with 1271 additions and 0 deletions
|
|
@ -23,6 +23,7 @@ INSTALLED_APPS = [
|
||||||
"control_plane.resources",
|
"control_plane.resources",
|
||||||
"control_plane.ventures",
|
"control_plane.ventures",
|
||||||
"control_plane.model_studio",
|
"control_plane.model_studio",
|
||||||
|
"control_plane.trading_studio",
|
||||||
"control_plane.secrets",
|
"control_plane.secrets",
|
||||||
"control_plane.knowledge",
|
"control_plane.knowledge",
|
||||||
"control_plane.verification",
|
"control_plane.verification",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from django.urls import path
|
||||||
|
|
||||||
from control_plane.projects import views
|
from control_plane.projects import views
|
||||||
from control_plane.model_studio import views as model_studio_views
|
from control_plane.model_studio import views as model_studio_views
|
||||||
|
from control_plane.trading_studio import views as trading_studio_views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.dashboard, name="dashboard"),
|
path("", views.dashboard, name="dashboard"),
|
||||||
|
|
@ -35,6 +36,8 @@ urlpatterns = [
|
||||||
path("resources/", views.resources, name="resources"),
|
path("resources/", views.resources, name="resources"),
|
||||||
path("model-studio/", model_studio_views.model_studio, name="model_studio"),
|
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("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/", views.approvals, name="approvals"),
|
||||||
path("approvals/<int:approval_id>/action/", views.approval_action, name="approval_action"),
|
path("approvals/<int:approval_id>/action/", views.approval_action, name="approval_action"),
|
||||||
path("activity/", views.activity, name="activity"),
|
path("activity/", views.activity, name="activity"),
|
||||||
|
|
|
||||||
0
control_plane/trading_studio/__init__.py
Normal file
0
control_plane/trading_studio/__init__.py
Normal file
6
control_plane/trading_studio/apps.py
Normal file
6
control_plane/trading_studio/apps.py
Normal 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"
|
||||||
0
control_plane/trading_studio/management/__init__.py
Normal file
0
control_plane/trading_studio/management/__init__.py
Normal 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"}))
|
||||||
|
|
@ -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"]}))
|
||||||
|
|
@ -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))
|
||||||
400
control_plane/trading_studio/migrations/0001_initial.py
Normal file
400
control_plane/trading_studio/migrations/0001_initial.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
control_plane/trading_studio/migrations/__init__.py
Normal file
0
control_plane/trading_studio/migrations/__init__.py
Normal file
340
control_plane/trading_studio/models.py
Normal file
340
control_plane/trading_studio/models.py
Normal 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)
|
||||||
54
control_plane/trading_studio/profiles.py
Normal file
54
control_plane/trading_studio/profiles.py
Normal 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
|
||||||
189
control_plane/trading_studio/services.py
Normal file
189
control_plane/trading_studio/services.py
Normal 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()
|
||||||
20
control_plane/trading_studio/views.py
Normal file
20
control_plane/trading_studio/views.py
Normal 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],
|
||||||
|
})
|
||||||
83
docs/trading_program_understanding_20260817.md
Normal file
83
docs/trading_program_understanding_20260817.md
Normal 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.
|
||||||
|
|
@ -47,6 +47,7 @@
|
||||||
<a href="/">Dashboard</a>
|
<a href="/">Dashboard</a>
|
||||||
<a href="/projects/">Projects</a>
|
<a href="/projects/">Projects</a>
|
||||||
<a href="/model-studio/">Model Studio</a>
|
<a href="/model-studio/">Model Studio</a>
|
||||||
|
<a href="/trading-studio/">Trading Studio</a>
|
||||||
<a href="/agents/">Agents</a>
|
<a href="/agents/">Agents</a>
|
||||||
<a href="/progeny/">Progeny</a>
|
<a href="/progeny/">Progeny</a>
|
||||||
<a href="/steward/">Steward</a>
|
<a href="/steward/">Steward</a>
|
||||||
|
|
|
||||||
9
templates/control_plane/trading_studio.html
Normal file
9
templates/control_plane/trading_studio.html
Normal 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 %}
|
||||||
9
templates/control_plane/trading_studio_project.html
Normal file
9
templates/control_plane/trading_studio_project.html
Normal 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 %}
|
||||||
99
tests/test_trading_studio_v01.py
Normal file
99
tests/test_trading_studio_v01.py
Normal 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
|
||||||
Loading…
Add table
Reference in a new issue