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.ventures",
|
||||
"control_plane.model_studio",
|
||||
"control_plane.trading_studio",
|
||||
"control_plane.secrets",
|
||||
"control_plane.knowledge",
|
||||
"control_plane.verification",
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
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="/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>
|
||||
|
|
|
|||
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