Artifex/control_plane/trading_studio/models.py

399 lines
18 KiB
Python
Raw Permalink Normal View History

2026-08-17 13:48:50 +07:00
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 CohortMembership(TimestampedModel):
"""An ordered, immutable strategy membership imported from a frozen cohort spec."""
cohort = models.ForeignKey(TradingCohort, on_delete=models.CASCADE, related_name="memberships")
strategy_version = models.ForeignKey(StrategyVersion, on_delete=models.PROTECT, related_name="cohort_memberships")
ordinal = models.PositiveIntegerField()
member_id = models.UUIDField(unique=True)
provenance = models.CharField(max_length=80)
metadata = models.JSONField(default=dict)
class Meta:
constraints = [models.UniqueConstraint(fields=["cohort", "ordinal"], name="unique_cohort_member_ordinal")]
def save(self, *args, **kwargs):
if self.pk and type(self).objects.filter(pk=self.pk).exists():
original = type(self).objects.get(pk=self.pk)
fields = ["cohort_id", "strategy_version_id", "ordinal", "member_id", "provenance", "metadata"]
if any(getattr(self, field) != getattr(original, field) for field in fields):
raise ValueError("CohortMembership is immutable after frozen-spec materialization.")
super().save(*args, **kwargs)
2026-08-17 13:48:50 +07:00
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 QualificationReplayRun(TimestampedModel):
"""Persisted CANARY_ONLY qualification replay and its immutable inputs."""
strategy_version = models.ForeignKey(StrategyVersion, on_delete=models.PROTECT, related_name="qualification_runs")
dataset_version = models.ForeignKey(MarketDatasetVersion, on_delete=models.PROTECT, related_name="qualification_runs")
fold = models.CharField(max_length=120)
scenario_name = models.CharField(max_length=160)
runner_name = models.CharField(max_length=160)
replay_mode = models.CharField(max_length=32, default="CANARY_ONLY")
input_hash = models.CharField(max_length=128)
calibration = models.JSONField(default=dict)
configuration = models.JSONField(default=dict)
summary = models.JSONField(default=dict)
class Meta:
constraints = [models.UniqueConstraint(
fields=["strategy_version", "dataset_version", "fold", "scenario_name", "runner_name", "input_hash"],
name="unique_qualification_replay_identity",
)]
class QualificationReplayLedger(TimestampedModel):
"""Append-only replay result. Payload preserves every directly emitted ledger field."""
qualification_run = models.ForeignKey(QualificationReplayRun, on_delete=models.CASCADE, related_name="ledger_rows")
sequence = models.PositiveIntegerField()
identity = models.CharField(max_length=512)
result = models.JSONField(default=dict)
class Meta:
constraints = [
models.UniqueConstraint(fields=["qualification_run", "sequence"], name="unique_qualification_ledger_sequence"),
models.UniqueConstraint(fields=["qualification_run", "identity"], name="unique_qualification_ledger_identity"),
]
2026-08-17 13:48:50 +07:00
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)