"""Strategy execution orchestration services."""

from __future__ import annotations

import datetime as dt
import hashlib
import json
import time
from dataclasses import asdict, dataclass, replace
from pathlib import Path
from uuid import uuid4

import polars as pl

from ._contract import PUBLIC_ARTIFACT_SCHEMA_VERSION
from .column_map_provider import ColumnMapDataProvider
from .data_btc import BTCDataProvider
from .feature_materialization import hash_dataframe
from .feature_registry import DEFAULT_FEATURE_REGISTRY
from .framework_contract import ALLOCATION_SPAN_DAYS, ALLOCATION_WINDOW_OFFSET
from .prelude import BACKTEST_END, BACKTEST_START, backtest_dynamic_dca
from .runner_validation import (
    STRICT_MUTATION_MESSAGE,
    STRICT_PROFILE_MUTATION_MESSAGE,
    WIN_RATE_TOLERANCE,
    StrategyRunnerValidationMixin,
    WeightValidationError,
    _ValidationState,
)
from .runner_helpers import slice_window_or_filter
from .strategy_time_series import WeightTimeSeriesBatch
from .strategy_types import (
    BacktestConfig,
    BaseStrategy,
    DecideDailyConfig,
    ExportConfig,
    RunDailyConfig,
    StrategyArtifactSet,
    StrategyMetadata,
    ValidationConfig,
    strategy_context_from_features_df,
    validate_strategy_contract,
)
from .strategy_lint import lint_strategy_class, summarize_lint_findings


class _FrameworkBacktestAdapter:
    """Private runner-owned callable that enables batched backtest execution."""

    _stacksats_framework_fast_path = True

    def __init__(
        self,
        runner: "StrategyRunner",
        strategy: BaseStrategy,
        *,
        btc_df: pl.DataFrame | None = None,
    ):
        self._runner = runner
        self._strategy = strategy
        self._btc_df = btc_df

    @staticmethod
    def _empty_weights() -> pl.DataFrame:
        return pl.DataFrame(schema={"date": pl.Datetime("us"), "weight": pl.Float64})

    def _compute_window_weights(
        self,
        df_window: pl.DataFrame,
        *,
        window_start: dt.datetime | None = None,
        window_end: dt.datetime | None = None,
    ) -> pl.DataFrame:
        if df_window.is_empty():
            return self._empty_weights()

        resolved_start = window_start or df_window["date"].min()
        resolved_end = window_end or df_window["date"].max()
        weights, _ = self._runner._compute_strategy_weights(
            self._strategy,
            features_df=df_window,
            btc_df=self._btc_df,
            start_date=resolved_start,
            end_date=resolved_end,
            current_date=resolved_end,
            strict_mode=False,
            cache_namespace="base",
        )
        ctx = strategy_context_from_features_df(
            df_window,
            resolved_start,
            resolved_end,
            resolved_end,
            required_columns=tuple(self._strategy.required_feature_columns()),
            as_of_date=resolved_end,
        )
        self._runner._validate_weights(weights, resolved_start, resolved_end)
        self._strategy.validate_weights(weights, ctx)
        return weights

    def __call__(self, df_window: pl.DataFrame) -> pl.DataFrame:
        return self._compute_window_weights(df_window)

    def _compute_window_weights_batch(
        self,
        full_feat: pl.DataFrame,
        *,
        feature_plan,
        windows: pl.DataFrame,
        expected_days: int,
    ) -> pl.DataFrame:
        window_starts: list[dt.datetime] = []
        window_ends: list[dt.datetime] = []
        dates: list[dt.datetime] = []
        weights_out: list[float] = []

        for idx in range(windows.height):
            row = windows.row(idx, named=True)
            start_idx = row.get("feature_start_idx")
            end_idx = row.get("feature_end_idx")
            can_slice = bool(row.get("feature_can_slice", False))
            if can_slice and start_idx is not None and end_idx is not None:
                window_feat = full_feat.slice(
                    int(start_idx),
                    int(end_idx) - int(start_idx) + 1,
                )
            else:
                window_feat = slice_window_or_filter(
                    full_feat,
                    feature_plan,
                    row["window_start"],
                    row["window_end"],
                    expected_days=expected_days,
                )
            if window_feat.height != expected_days:
                continue
            weights = self._compute_window_weights(
                window_feat,
                window_start=row["window_start"],
                window_end=row["window_end"],
            )
            if weights.is_empty():
                continue
            count = weights.height
            window_starts.extend([row["window_start"]] * count)
            window_ends.extend([row["window_end"]] * count)
            dates.extend(weights["date"].to_list())
            weights_out.extend(
                weights["weight"].cast(pl.Float64, strict=False).to_list()
            )

        if not dates:
            return pl.DataFrame(
                schema={
                    "window_start": pl.Datetime("us"),
                    "window_end": pl.Datetime("us"),
                    "date": pl.Datetime("us"),
                    "weight": pl.Float64,
                }
            )
        return pl.DataFrame(
            {
                "window_start": window_starts,
                "window_end": window_ends,
                "date": dates,
                "weight": weights_out,
            }
        )


@dataclass(frozen=True)
class _DailyDecisionComputation:
    metadata: StrategyMetadata
    run_date: str
    decision_key: str
    weight_today: float
    recommended_notional_usd: float
    recommended_quantity_btc: float
    reference_price_usd: float
    weights: pl.DataFrame
    state_db_path: str
    forced_rerun: bool
    bootstrap: bool
    validation_receipt_id: int | None
    validation_passed: bool | None
    data_hash: str
    feature_snapshot_hash: str


class _DailyDecisionPreparationError(ValueError):
    """Internal error carrying partial decision-preflight state."""

    def __init__(
        self,
        message: str,
        *,
        validation_receipt_id: int | None,
        data_hash: str,
        feature_snapshot_hash: str,
        bootstrap: bool = False,
    ) -> None:
        super().__init__(message)
        self.validation_receipt_id = validation_receipt_id
        self.data_hash = data_hash
        self.feature_snapshot_hash = feature_snapshot_hash
        self.bootstrap = bootstrap


class StrategyRunner(StrategyRunnerValidationMixin):
    """Single orchestration path for strategy lifecycle operations."""

    def __init__(self, data_provider=None):
        self._data_provider = data_provider or BTCDataProvider()
        self._feature_registry = DEFAULT_FEATURE_REGISTRY

    @classmethod
    def from_dataframe(
        cls,
        df: pl.DataFrame,
        *,
        column_map: dict[str, str] | None = None,
    ) -> "StrategyRunner":
        """Construct a StrategyRunner backed by a user-supplied DataFrame.

        This is the primary entry point for using StackSats without a BRK
        parquet file.

        Parameters
        ----------
        df:
            A Polars DataFrame with a canonical ``date`` column. At minimum it
            must contain a column that maps to ``price_usd``.
        column_map:
            Mapping from **library column names** to **DataFrame column names**.
            Example: ``{"price_usd": "Close", "mvrv": "MVRV_Ratio"}``.
            If *None*, the DataFrame columns are used as-is.

        Returns
        -------
        StrategyRunner
            A fully configured runner using ``ColumnMapDataProvider``.

        Example
        -------
        ::

            runner = StrategyRunner.from_dataframe(
                df,
                column_map={"price_usd": "Close"},
            )
            result = runner.backtest(MyStrategy(), BacktestConfig())
        """
        return cls(
            data_provider=ColumnMapDataProvider(
                df=df,
                column_map=column_map or {},
            )
        )

    def _load_btc_df(
        self,
        btc_df: pl.DataFrame | None,
        *,
        end_date: str | None = None,
    ) -> pl.DataFrame:
        if btc_df is not None:
            return btc_df
        return self._data_provider.load(backtest_start=BACKTEST_START, end_date=end_date)

    def _validate_strategy_contract(self, strategy: BaseStrategy) -> None:
        validate_strategy_contract(strategy)

    def _strategy_lint_warnings(self, strategy: BaseStrategy) -> list[str]:
        _, warnings = summarize_lint_findings(lint_strategy_class(strategy.__class__))
        return warnings

    def _provenance(
        self,
        strategy: BaseStrategy,
        config: BacktestConfig | ValidationConfig | ExportConfig,
    ) -> dict[str, str]:
        metadata = strategy.metadata()
        config_hash = hashlib.sha256(
            json.dumps(asdict(config), sort_keys=True, default=str).encode("utf-8")
        ).hexdigest()[:12]
        return {
            "strategy_id": metadata.strategy_id,
            "version": metadata.version,
            "config_hash": config_hash,
            "run_id": str(uuid4()),
        }

    def _daily_fingerprint(
        self,
        strategy: BaseStrategy,
        *,
        run_date: str,
        mode: str,
        total_window_budget_usd: float,
        btc_price_col: str,
        adapter_spec: str | None = None,
    ) -> str:
        spec = strategy.spec()
        payload = {
            "strategy_metadata": asdict(spec.metadata),
            "strategy_params": spec.params,
            "strategy_intent_mode": spec.intent_mode,
            "required_feature_columns": list(spec.required_feature_columns),
            "required_feature_sets": list(strategy.required_feature_sets()),
            "strategy_class": strategy.__class__.__name__,
            "strategy_module": strategy.__class__.__module__,
            "run_date": run_date,
            "mode": mode,
            "total_window_budget_usd": float(total_window_budget_usd),
            "btc_price_col": btc_price_col,
            "adapter_spec": adapter_spec,
        }
        return hashlib.sha256(
            json.dumps(payload, sort_keys=True, default=str).encode("utf-8")
        ).hexdigest()

    def _daily_run_fingerprint(
        self,
        strategy: BaseStrategy,
        config: RunDailyConfig,
        run_date: str,
    ) -> str:
        return self._daily_fingerprint(
            strategy,
            run_date=run_date,
            mode=config.mode,
            total_window_budget_usd=config.total_window_budget_usd,
            btc_price_col=config.btc_price_col,
            adapter_spec=config.adapter_spec,
        )

    def _daily_decision_fingerprint(
        self,
        strategy: BaseStrategy,
        config: DecideDailyConfig,
        run_date: str,
    ) -> str:
        return self._daily_fingerprint(
            strategy,
            run_date=run_date,
            mode="decision",
            total_window_budget_usd=config.total_window_budget_usd,
            btc_price_col=config.btc_price_col,
        )

    @staticmethod
    def _config_hash(config: ValidationConfig) -> str:
        return hashlib.sha256(
            json.dumps(asdict(config), sort_keys=True, default=str).encode("utf-8")
        ).hexdigest()

    @staticmethod
    def _merge_strict_check_result(
        state: _ValidationState,
        *,
        ok: bool,
        messages: list[str],
    ) -> None:
        state.strict_checks_ok = state.strict_checks_ok and bool(ok)
        state.messages.extend(messages)

    def _parse_run_daily_config(
        self,
        strategy: BaseStrategy,
        config: RunDailyConfig,
    ) -> tuple[StrategyMetadata, float, dt.datetime, str]:
        metadata, budget, run_ts, run_date = self._parse_daily_decision_config(strategy, config)
        if config.mode not in {"paper", "live"}:
            raise ValueError("mode must be either 'paper' or 'live'.")
        if config.mode == "live" and not config.adapter_spec:
            raise ValueError("Live mode requires --adapter.")
        return metadata, budget, run_ts, run_date

    def _parse_daily_decision_config(
        self,
        strategy: BaseStrategy,
        config: DecideDailyConfig | RunDailyConfig,
    ) -> tuple[StrategyMetadata, float, dt.datetime, str]:
        metadata = strategy.metadata()
        budget = float(config.total_window_budget_usd)
        if not (budget == budget) or budget <= 0.0:
            raise ValueError("total_window_budget_usd must be a finite value greater than 0.")
        if config.run_date is not None:
            run_ts = dt.datetime.strptime(config.run_date[:10], "%Y-%m-%d")
        else:
            now = dt.datetime.now(dt.timezone.utc)
            run_ts = now.replace(hour=0, minute=0, second=0, microsecond=0)
        run_date = run_ts.strftime("%Y-%m-%d")
        return metadata, budget, run_ts, run_date

    @staticmethod
    def _decision_artifact_path(
        *,
        output_dir: str,
        metadata: StrategyMetadata,
        run_date: str,
        decision_key: str,
    ) -> Path:
        return (
            Path(output_dir)
            / metadata.strategy_id
            / metadata.version
            / "decisions"
            / run_date
            / decision_key
            / "decision_result.json"
        )

    def _build_noop_daily_result(
        self,
        *,
        metadata,
        config: RunDailyConfig,
        run_date: str,
        state_store,
        existing_run,
        daily_order_receipt_cls,
        daily_run_result_cls,
    ):
        payload = existing_run.payload
        order_receipt = None
        if existing_run.order_summary is not None:
            order_receipt = daily_order_receipt_cls(
                status=str(existing_run.order_summary.get("status", "unknown")),
                external_order_id=existing_run.order_summary.get("external_order_id"),
                filled_notional_usd=float(
                    existing_run.order_summary.get("filled_notional_usd", 0.0)
                ),
                filled_quantity_btc=float(
                    existing_run.order_summary.get("filled_quantity_btc", 0.0)
                ),
                fill_price_usd=float(existing_run.order_summary.get("fill_price_usd", 0.0)),
                metadata=dict(existing_run.order_summary.get("metadata") or {}),
            )
        return daily_run_result_cls(
            status="noop",
            strategy_id=metadata.strategy_id,
            strategy_version=metadata.version,
            run_date=run_date,
            run_key=existing_run.run_key,
            mode=config.mode,
            idempotency_hit=True,
            forced_rerun=False,
            weight_today=(
                float(payload["weight_today"])
                if payload.get("weight_today") is not None
                else None
            ),
            order_notional_usd=(
                float(payload["order_notional_usd"])
                if payload.get("order_notional_usd") is not None
                else None
            ),
            btc_quantity=(
                float(payload["btc_quantity"])
                if payload.get("btc_quantity") is not None
                else None
            ),
            price_usd=(
                float(payload["price_usd"]) if payload.get("price_usd") is not None else None
            ),
            adapter_name=str(payload.get("adapter_name", "unknown")),
            state_db_path=str(state_store.db_path),
            artifact_path=(
                str(payload["artifact_path"])
                if payload.get("artifact_path") is not None
                else None
            ),
            message="Existing completed run reused via idempotency ledger.",
            order_receipt=order_receipt,
            bootstrap=bool(payload.get("bootstrap", False)),
            validation_receipt_id=(
                int(payload["validation_receipt_id"])
                if payload.get("validation_receipt_id") is not None
                else None
            ),
            validation_passed=payload.get("validation_passed"),
            data_hash=str(payload.get("data_hash", "")),
            feature_snapshot_hash=str(payload.get("feature_snapshot_hash", "")),
        )

    def _build_noop_daily_decision_result(
        self,
        *,
        metadata: StrategyMetadata,
        config: DecideDailyConfig,
        run_date: str,
        state_store,
        existing_run,
        daily_decision_result_cls,
    ):
        payload = existing_run.payload
        return daily_decision_result_cls(
            status="noop",
            strategy_id=metadata.strategy_id,
            strategy_version=metadata.version,
            run_date=run_date,
            decision_key=existing_run.run_key,
            idempotency_hit=True,
            forced_rerun=False,
            weight_today=(
                float(payload["weight_today"])
                if payload.get("weight_today") is not None
                else None
            ),
            recommended_notional_usd=(
                float(payload["recommended_notional_usd"])
                if payload.get("recommended_notional_usd") is not None
                else None
            ),
            recommended_quantity_btc=(
                float(payload["recommended_quantity_btc"])
                if payload.get("recommended_quantity_btc") is not None
                else None
            ),
            reference_price_usd=(
                float(payload["reference_price_usd"])
                if payload.get("reference_price_usd") is not None
                else None
            ),
            btc_price_col=str(payload.get("btc_price_col", config.btc_price_col)),
            state_db_path=str(state_store.db_path),
            artifact_path=(
                str(payload["artifact_path"])
                if payload.get("artifact_path") is not None
                else None
            ),
            message="Existing completed decision reused via idempotency ledger.",
            validation_receipt_id=(
                int(payload["validation_receipt_id"])
                if payload.get("validation_receipt_id") is not None
                else None
            ),
            validation_passed=payload.get("validation_passed"),
            data_hash=str(payload.get("data_hash", "")),
            feature_snapshot_hash=str(payload.get("feature_snapshot_hash", "")),
            bootstrap=bool(payload.get("bootstrap", False)),
        )

    @staticmethod
    def _strict_validation_failure_message(messages: list[str]) -> str:
        details = "; ".join(message for message in messages if message)
        if details:
            return f"Strict validation failed before daily execution: {details}"
        return "Strict validation failed before daily execution."

    @staticmethod
    def _strict_decision_failure_message(messages: list[str]) -> str:
        details = "; ".join(message for message in messages if message)
        if details:
            return f"Strict validation failed before daily decision: {details}"
        return "Strict validation failed before daily decision."

    def _build_failed_daily_result(
        self,
        *,
        metadata: StrategyMetadata,
        config: RunDailyConfig,
        run_date: str,
        run_key: str,
        state_store,
        forced_rerun: bool,
        bootstrap: bool,
        validation_receipt_id: int | None,
        data_hash: str,
        feature_snapshot_hash: str,
        error_message: str,
        daily_run_result_cls,
    ):
        return daily_run_result_cls(
            status="failed",
            strategy_id=metadata.strategy_id,
            strategy_version=metadata.version,
            run_date=run_date,
            run_key=run_key,
            mode=config.mode,
            idempotency_hit=False,
            forced_rerun=forced_rerun,
            weight_today=None,
            order_notional_usd=None,
            btc_quantity=None,
            price_usd=None,
            adapter_name=(
                "PaperExecutionAdapter" if config.mode == "paper" else "custom-adapter"
            ),
            state_db_path=str(state_store.db_path),
            artifact_path=None,
            message=f"Daily execution failed: {error_message}",
            order_receipt=None,
            bootstrap=bootstrap,
            validation_receipt_id=validation_receipt_id,
            validation_passed=False if validation_receipt_id is not None else None,
            data_hash=data_hash,
            feature_snapshot_hash=feature_snapshot_hash,
        )

    def _build_failed_daily_decision_result(
        self,
        *,
        metadata: StrategyMetadata,
        config: DecideDailyConfig,
        run_date: str,
        decision_key: str,
        state_store,
        forced_rerun: bool,
        bootstrap: bool,
        validation_receipt_id: int | None,
        data_hash: str,
        feature_snapshot_hash: str,
        error_message: str,
        daily_decision_result_cls,
    ):
        return daily_decision_result_cls(
            status="failed",
            strategy_id=metadata.strategy_id,
            strategy_version=metadata.version,
            run_date=run_date,
            decision_key=decision_key,
            idempotency_hit=False,
            forced_rerun=forced_rerun,
            weight_today=None,
            recommended_notional_usd=None,
            recommended_quantity_btc=None,
            reference_price_usd=None,
            btc_price_col=config.btc_price_col,
            state_db_path=str(state_store.db_path),
            artifact_path=None,
            message=f"Daily decision failed: {error_message}",
            validation_receipt_id=validation_receipt_id,
            validation_passed=False if validation_receipt_id is not None else None,
            data_hash=data_hash,
            feature_snapshot_hash=feature_snapshot_hash,
            bootstrap=bootstrap,
        )

    @staticmethod
    def _classify_reconciliation_status(
        *,
        previous_weight_today: float | None,
        recomputed_weight_today: float | None,
        previous_feature_snapshot_hash: str,
        recomputed_feature_snapshot_hash: str,
        atol: float = 1e-12,
    ) -> str:
        # Reconciliation status invariant:
        # - stable: identical decision (and optionally identical snapshot hash)
        # - data_revised_no_decision_change: snapshot changed but decision did not
        # - decision_changed_due_to_revision: recomputed decision changed
        if previous_weight_today is None or recomputed_weight_today is None:
            return "stable"
        if abs(float(previous_weight_today) - float(recomputed_weight_today)) <= float(atol):
            if previous_feature_snapshot_hash == recomputed_feature_snapshot_hash:
                return "stable"
            return "data_revised_no_decision_change"
        return "decision_changed_due_to_revision"

    def _run_daily_strict_preflight(
        self,
        *,
        strategy: BaseStrategy,
        metadata: StrategyMetadata,
        config: DecideDailyConfig | RunDailyConfig,
        run_date: str,
        run_ts: dt.datetime,
        fingerprint: str,
        state_store,
        btc_df: pl.DataFrame | None = None,
    ) -> tuple[
        pl.DataFrame,
        dt.datetime,
        dt.datetime,
        pl.DataFrame,
        bool,
        list[str],
        int,
        str,
        str,
    ]:
        btc_df_loaded = self._load_btc_df(btc_df, end_date=run_date)
        if config.btc_price_col not in btc_df_loaded.columns:
            raise ValueError(
                f"Missing required BTC price column '{config.btc_price_col}'."
            )
        window_end = run_ts
        window_start = window_end - ALLOCATION_WINDOW_OFFSET
        required_dates = pl.datetime_range(
            window_start, window_end, interval="1d", eager=True
        ).to_list()
        required_dates_df = pl.DataFrame({"date": required_dates})
        required_window = required_dates_df.join(
            btc_df_loaded.select(["date", config.btc_price_col]),
            on="date",
            how="left",
        )
        if required_window[config.btc_price_col].null_count() > 0:
            raise ValueError(
                "Missing BTC price data for required allocation window ending on run_date."
            )

        # Strict gate invariant: run_daily must validate against the same snapshot
        # used for execution.
        validation_config = replace(
            strategy.default_run_daily_validation_config(),
            start_date=window_start.strftime("%Y-%m-%d"),
            end_date=window_end.strftime("%Y-%m-%d"),
        )
        validation_result = self.validate(
            strategy,
            validation_config,
            btc_df=btc_df_loaded,
        )
        provider_hash = self._feature_registry.provider_fingerprint(strategy)
        observed_features, feature_snapshot_hash = self._feature_registry.materialization_fingerprint(
            strategy,
            btc_df_loaded,
            start_date=window_start,
            end_date=window_end,
            current_date=window_end,
        )
        btc_slice = btc_df_loaded.filter(pl.col("date") <= window_end)
        data_hash = hash_dataframe(btc_slice)
        validation_receipt = state_store.create_validation_receipt(
            strategy_id=metadata.strategy_id,
            strategy_version=metadata.version,
            run_date=run_date,
            fingerprint=fingerprint,
            data_hash=data_hash,
            provider_hash=provider_hash,
            feature_snapshot_hash=feature_snapshot_hash,
            config_hash=self._config_hash(validation_config),
            passed=bool(validation_result.passed),
            diagnostics={
                "summary": validation_result.summary(),
                "messages": validation_result.messages,
                "diagnostics": validation_result.diagnostics,
            },
        )
        return (
            btc_df_loaded,
            window_start,
            window_end,
            observed_features,
            bool(validation_result.passed),
            list(validation_result.messages),
            validation_receipt.receipt_id,
            data_hash,
            feature_snapshot_hash,
        )

    def _compute_daily_decision(
        self,
        *,
        strategy: BaseStrategy,
        metadata: StrategyMetadata,
        config: DecideDailyConfig | RunDailyConfig,
        run_date: str,
        run_ts: dt.datetime,
        decision_key: str,
        forced_rerun: bool,
        fingerprint: str,
        state_store,
        btc_df: pl.DataFrame | None = None,
    ) -> _DailyDecisionComputation:
        (
            btc_df_loaded,
            window_start,
            window_end,
            observed_features,
            validation_passed,
            validation_messages,
            validation_receipt_id,
            data_hash,
            feature_snapshot_hash,
        ) = self._run_daily_strict_preflight(
            strategy=strategy,
            metadata=metadata,
            config=config,
            run_date=run_date,
            run_ts=run_ts,
            fingerprint=fingerprint,
            state_store=state_store,
            btc_df=btc_df,
        )
        if not validation_passed:
            if isinstance(config, RunDailyConfig):
                raise _DailyDecisionPreparationError(
                    self._strict_validation_failure_message(validation_messages),
                    validation_receipt_id=validation_receipt_id,
                    data_hash=data_hash,
                    feature_snapshot_hash=feature_snapshot_hash,
                )
            raise _DailyDecisionPreparationError(
                self._strict_decision_failure_message(validation_messages),
                validation_receipt_id=validation_receipt_id,
                data_hash=data_hash,
                feature_snapshot_hash=feature_snapshot_hash,
            )

        required_dates = pl.datetime_range(
            window_start, window_end, interval="1d", eager=True
        ).to_list()
        required_window = pl.DataFrame({"date": required_dates}).join(
            btc_df_loaded.select(["date", config.btc_price_col]),
            on="date",
            how="left",
        )

        locked_prefix = state_store.load_locked_prefix(
            strategy_id=metadata.strategy_id,
            strategy_version=metadata.version,
            mode="decision" if isinstance(config, DecideDailyConfig) else config.mode,
            run_date=run_date,
            window_start=window_start,
        )
        bootstrap = locked_prefix is None
        ctx = strategy_context_from_features_df(
            observed_features,
            window_start,
            window_end,
            window_end,
            required_columns=tuple(strategy.required_feature_columns()),
            as_of_date=window_end,
            locked_weights=locked_prefix,
            btc_price_col=config.btc_price_col,
        )
        weights = strategy.compute_weights(ctx)
        self._validate_weights(weights, window_start, window_end)
        strategy.validate_weights(weights, ctx)
        run_row = weights.filter(pl.col("date") == run_ts)
        if run_row.is_empty():
            raise ValueError("Strategy weights are missing run_date allocation.")

        weight_today = float(run_row["weight"][0])
        price_row = required_window.filter(pl.col("date") == run_ts)
        reference_price_usd = float(price_row[config.btc_price_col][0])
        if reference_price_usd <= 0.0:
            raise ValueError("BTC price must be greater than 0 for run_date.")
        recommended_notional_usd = float(config.total_window_budget_usd) * weight_today
        recommended_quantity_btc = recommended_notional_usd / reference_price_usd

        return _DailyDecisionComputation(
            metadata=metadata,
            run_date=run_date,
            decision_key=decision_key,
            weight_today=weight_today,
            recommended_notional_usd=recommended_notional_usd,
            recommended_quantity_btc=recommended_quantity_btc,
            reference_price_usd=reference_price_usd,
            weights=weights,
            state_db_path=str(state_store.db_path),
            forced_rerun=forced_rerun,
            bootstrap=bootstrap,
            validation_receipt_id=validation_receipt_id,
            validation_passed=True,
            data_hash=data_hash,
            feature_snapshot_hash=feature_snapshot_hash,
        )

    def _persist_daily_decision_result(
        self,
        *,
        computation: _DailyDecisionComputation,
        state_store,
        payload: dict,
    ) -> None:
        state_store.mark_run_success_with_snapshot(
            strategy_id=computation.metadata.strategy_id,
            strategy_version=computation.metadata.version,
            run_date=computation.run_date,
            mode="decision",
            payload=payload,
            order_summary=None,
            force_flag=computation.forced_rerun,
            snapshot_date=computation.run_date,
            weights=computation.weights,
            validation_receipt_id=computation.validation_receipt_id,
            data_hash=computation.data_hash,
            feature_snapshot_hash=computation.feature_snapshot_hash,
        )

    def backtest(
        self,
        strategy: BaseStrategy,
        config: BacktestConfig,
        *,
        btc_df: pl.DataFrame | None = None,
    ):
        from .api import BacktestResult

        self._validate_strategy_contract(strategy)
        metadata = strategy.metadata()
        btc_df = self._load_btc_df(btc_df, end_date=config.end_date)
        _, created_cache = self._ensure_runtime_cache(strategy, btc_df)
        started_at = time.perf_counter()
        start_ts = dt.datetime.strptime(
            (config.start_date or BACKTEST_START)[:10], "%Y-%m-%d"
        )
        end_col = btc_df["date"].max()
        data_end_ts = (
            end_col
            if isinstance(end_col, dt.datetime)
            else dt.datetime.fromisoformat(str(end_col)[:10])
        )
        requested_end = dt.datetime.strptime(
            (config.end_date or BACKTEST_END)[:10],
            "%Y-%m-%d",
        )
        if requested_end < start_ts:
            raise ValueError(
                "Backtest end date must be on or after start date. "
                f"Resolved start={start_ts.date()} end={requested_end.date()}."
            )
        end_ts = min(requested_end, data_end_ts)
        try:
            full_features_df = self._materialize_strategy_features(
                strategy,
                btc_df,
                start_date=start_ts,
                end_date=end_ts,
                current_date=end_ts,
                cache_namespace="base-full",
            )
            if strategy.intent_mode() == "profile":
                strategy_fn = _FrameworkBacktestAdapter(self, strategy, btc_df=btc_df)
            else:
                def strategy_fn(df_window: pl.DataFrame) -> pl.DataFrame:
                    if df_window.is_empty():
                        return pl.DataFrame(
                            schema={"date": pl.Datetime("us"), "weight": pl.Float64}
                        )

                    window_start = df_window["date"].min()
                    window_end = df_window["date"].max()
                    weights, _ = self._compute_strategy_weights(
                        strategy,
                        features_df=df_window,
                        btc_df=btc_df,
                        start_date=window_start,
                        end_date=window_end,
                        current_date=window_end,
                        strict_mode=False,
                        cache_namespace="base",
                    )
                    ctx = strategy_context_from_features_df(
                        df_window,
                        window_start,
                        window_end,
                        window_end,
                        required_columns=tuple(strategy.required_feature_columns()),
                        as_of_date=window_end,
                    )
                    self._validate_weights(weights, window_start, window_end)
                    strategy.validate_weights(weights, ctx)
                    return weights

            strategy_label = config.strategy_label or metadata.strategy_id
            spd_table, exp_decay_percentile, uniform_exp_decay_percentile = (
                backtest_dynamic_dca(
                    btc_df,
                    strategy_fn,
                    features_df=full_features_df,
                    strategy_label=strategy_label,
                    start_date=config.start_date,
                    end_date=config.end_date,
                )
            )
            if spd_table.is_empty():
                raise ValueError(
                    "No backtest windows were generated for the requested date range."
                )

            win_rate = self._compute_win_rate(spd_table)
            score = (0.5 * win_rate) + (0.5 * exp_decay_percentile)
            provenance = self._provenance(strategy, config)

            return BacktestResult(
                spd_table=spd_table,
                exp_decay_percentile=exp_decay_percentile,
                uniform_exp_decay_percentile=uniform_exp_decay_percentile,
                win_rate=win_rate,
                score=score,
                strategy_id=provenance["strategy_id"],
                strategy_version=provenance["version"],
                config_hash=provenance["config_hash"],
                run_id=provenance["run_id"],
            )
        finally:
            profile = self._runtime_profile()
            if profile is not None:
                profile.backtest_seconds += time.perf_counter() - started_at
            self._release_runtime_cache(clear=created_cache)

    def validate(
        self,
        strategy: BaseStrategy,
        config: ValidationConfig,
        *,
        btc_df: pl.DataFrame | None = None,
    ):
        from .api import ValidationResult

        try:
            self._validate_strategy_contract(strategy)
        except (TypeError, ValueError) as exc:
            return ValidationResult(
                passed=False,
                forward_leakage_ok=False,
                weight_constraints_ok=False,
                win_rate=0.0,
                win_rate_ok=False,
                messages=[str(exc)],
                strategy_id=strategy.strategy_id,
                min_win_rate=float(config.min_win_rate),
                diagnostics={},
            )
        btc_df = self._load_btc_df(btc_df, end_date=config.end_date)

        start_date = config.start_date or BACKTEST_START
        end_col = btc_df["date"].max()
        resolved_end = dt.datetime.strptime(
            (config.end_date or BACKTEST_END)[:10],
            "%Y-%m-%d",
        )
        data_end = (
            end_col
            if isinstance(end_col, dt.datetime)
            else dt.datetime.fromisoformat(str(end_col)[:10])
        )
        end_date = min(resolved_end, data_end).strftime("%Y-%m-%d")
        start_ts = dt.datetime.strptime(start_date[:10], "%Y-%m-%d")
        end_ts = dt.datetime.strptime(end_date[:10], "%Y-%m-%d")
        backtest_slice = btc_df.filter(
            (pl.col("date") >= start_ts) & (pl.col("date") <= end_ts)
        )
        backtest_idx = backtest_slice["date"].unique().sort()

        strict_mode = bool(config.strict)
        state = _ValidationState(messages=[], diagnostics={})
        state.messages.extend(
            f"Strategy lint warning: {warning}" for warning in self._strategy_lint_warnings(strategy)
        )

        if len(backtest_idx) == 0:
            return ValidationResult(
                passed=False,
                forward_leakage_ok=False,
                weight_constraints_ok=False,
                win_rate=0.0,
                win_rate_ok=False,
                messages=["No data available in the requested date range."],
                strategy_id=strategy.strategy_id,
                min_win_rate=float(config.min_win_rate),
                diagnostics={},
            )

        cache, created_cache = self._ensure_runtime_cache(strategy, btc_df)
        try:
            features_df = self._materialize_strategy_features(
                strategy,
                btc_df,
                start_date=start_ts,
                end_date=end_ts,
                current_date=end_ts,
                cache_namespace="base-full",
            )
            if strict_mode:
                probe_step = 1 if len(backtest_idx) <= (ALLOCATION_SPAN_DAYS * 2) else max(
                    len(backtest_idx) // 100,
                    1,
                )
            else:
                probe_step = max(len(backtest_idx) // 50, 1)
            active_intent_mode = strategy.intent_mode()
            has_propose_hook = active_intent_mode == "propose"
            has_profile_hook = active_intent_mode == "profile"
            self._run_forward_leakage_checks(
                strategy=strategy,
                btc_df=btc_df,
                full_features_df=features_df,
                backtest_idx=backtest_idx,
                start_ts=start_ts,
                strict_mode=strict_mode,
                has_propose_hook=has_propose_hook,
                has_profile_hook=has_profile_hook,
                probe_step=probe_step,
                state=state,
            )
            max_window_start = self._run_weight_constraint_checks(
                strategy=strategy,
                btc_df=btc_df,
                features_df=features_df,
                start_ts=start_ts,
                end_ts=end_ts,
                strict_mode=strict_mode,
                config=config,
                state=state,
            )
            self._run_locked_prefix_check(
                strategy=strategy,
                btc_df=btc_df,
                features_df=features_df,
                start_ts=start_ts,
                max_window_start=max_window_start,
                strict_mode=strict_mode,
                state=state,
            )

            backtest_result = self.backtest(
                strategy,
                BacktestConfig(
                    start_date=start_date,
                    end_date=end_date,
                    strategy_label="validation-run",
                ),
                btc_df=btc_df,
            )
            win_rate_ok = backtest_result.win_rate >= config.min_win_rate
            if not win_rate_ok:
                state.messages.append(
                    f"Win rate below threshold: {backtest_result.win_rate:.2f}% < "
                    f"{config.min_win_rate:.2f}%."
                )

            if strict_mode and state.strict_checks_ok:
                fold_ok, fold_messages = self._strict_fold_checks(
                    strategy=strategy,
                    btc_df=btc_df,
                    start_ts=start_ts,
                    end_ts=end_ts,
                    config=config,
                )
                self._merge_strict_check_result(
                    state,
                    ok=fold_ok,
                    messages=fold_messages,
                )

                shuffled_days = max(ALLOCATION_SPAN_DAYS * 2, 730)
                shuffled_start = max(start_ts, end_ts - dt.timedelta(days=shuffled_days - 1))
                shuffled_ok, shuffled_messages = self._strict_shuffled_check(
                    strategy=strategy,
                    btc_df=btc_df,
                    start_ts=shuffled_start,
                    end_ts=end_ts,
                    config=config,
                )
                self._merge_strict_check_result(
                    state,
                    ok=shuffled_ok,
                    messages=shuffled_messages,
                )
                self._strict_statistical_checks(
                    strategy=strategy,
                    btc_df=btc_df,
                    features_df=features_df,
                    start_ts=start_ts,
                    end_ts=end_ts,
                    config=config,
                    state=state,
                    backtest_result=backtest_result,
                )

            if strict_mode and not state.mutation_safe:
                state.strict_checks_ok = False
            if strict_mode and not state.deterministic_ok:
                state.strict_checks_ok = False

            if not state.messages:
                state.messages.append("All validation checks passed.")

            state.diagnostics = dict(state.diagnostics or {})
            state.diagnostics["hot_path_profile"] = cache.profile.to_dict()

            metadata = strategy.metadata()
            return ValidationResult(
                passed=state.forward_leakage_ok
                and state.weight_constraints_ok
                and win_rate_ok
                and (state.strict_checks_ok if strict_mode else True),
                forward_leakage_ok=state.forward_leakage_ok,
                weight_constraints_ok=state.weight_constraints_ok,
                win_rate=float(backtest_result.win_rate),
                win_rate_ok=win_rate_ok,
                messages=state.messages,
                strategy_id=metadata.strategy_id,
                min_win_rate=float(config.min_win_rate),
                diagnostics=state.diagnostics or {},
            )
        finally:
            self._release_runtime_cache(clear=created_cache)

    def decide_daily(
        self,
        strategy: BaseStrategy,
        config: DecideDailyConfig,
        *,
        btc_df: pl.DataFrame | None = None,
    ):
        from .api import DailyDecisionResult
        from .execution_state import IdempotencyConflictError, SQLiteExecutionStateStore

        self._validate_strategy_contract(strategy)
        metadata, _, run_ts, run_date = self._parse_daily_decision_config(strategy, config)
        decision_key = str(uuid4())
        state_store = SQLiteExecutionStateStore(config.state_db_path)
        fingerprint = self._daily_decision_fingerprint(strategy, config, run_date)
        claim = state_store.claim_run(
            strategy_id=metadata.strategy_id,
            strategy_version=metadata.version,
            run_date=run_date,
            mode="decision",
            run_key=decision_key,
            fingerprint=fingerprint,
            force=config.force,
        )
        if claim.status == "noop":
            existing = claim.existing_run
            assert existing is not None
            return self._build_noop_daily_decision_result(
                metadata=metadata,
                config=config,
                run_date=run_date,
                state_store=state_store,
                existing_run=existing,
                daily_decision_result_cls=DailyDecisionResult,
            )

        forced_rerun = bool(config.force or claim.forced_overwrite)
        bootstrap = False
        validation_receipt_id: int | None = None
        data_hash = ""
        feature_snapshot_hash = ""
        try:
            computation = self._compute_daily_decision(
                strategy=strategy,
                metadata=metadata,
                config=config,
                run_date=run_date,
                run_ts=run_ts,
                decision_key=decision_key,
                forced_rerun=forced_rerun,
                fingerprint=fingerprint,
                state_store=state_store,
                btc_df=btc_df,
            )
            bootstrap = computation.bootstrap
            validation_receipt_id = computation.validation_receipt_id
            data_hash = computation.data_hash
            feature_snapshot_hash = computation.feature_snapshot_hash
            artifact_path = self._decision_artifact_path(
                output_dir=config.output_dir,
                metadata=metadata,
                run_date=run_date,
                decision_key=decision_key,
            )
            result = DailyDecisionResult(
                status="decided",
                strategy_id=metadata.strategy_id,
                strategy_version=metadata.version,
                run_date=run_date,
                decision_key=decision_key,
                idempotency_hit=False,
                forced_rerun=computation.forced_rerun,
                weight_today=computation.weight_today,
                recommended_notional_usd=computation.recommended_notional_usd,
                recommended_quantity_btc=computation.recommended_quantity_btc,
                reference_price_usd=computation.reference_price_usd,
                btc_price_col=config.btc_price_col,
                state_db_path=computation.state_db_path,
                artifact_path=str(artifact_path),
                message=(
                    "Daily decision completed."
                    if not computation.bootstrap
                    else "Daily decision completed (bootstrap run with no prior snapshot)."
                ),
                validation_receipt_id=computation.validation_receipt_id,
                validation_passed=computation.validation_passed,
                data_hash=computation.data_hash,
                feature_snapshot_hash=computation.feature_snapshot_hash,
                bootstrap=computation.bootstrap,
            )
            result_payload = result.to_json(path=artifact_path)
            self._persist_daily_decision_result(
                computation=computation,
                state_store=state_store,
                payload=result_payload,
            )
            return result
        except IdempotencyConflictError:
            raise
        except Exception as exc:
            if isinstance(exc, _DailyDecisionPreparationError):
                bootstrap = exc.bootstrap
                validation_receipt_id = exc.validation_receipt_id
                data_hash = exc.data_hash
                feature_snapshot_hash = exc.feature_snapshot_hash
            error_message = str(exc)
            failure_payload = {
                "status": "failed",
                "strategy_id": metadata.strategy_id,
                "strategy_version": metadata.version,
                "run_date": run_date,
                "decision_key": decision_key,
                "error": error_message,
            }
            state_store.mark_run_failure(
                strategy_id=metadata.strategy_id,
                strategy_version=metadata.version,
                run_date=run_date,
                mode="decision",
                payload=failure_payload,
                force_flag=forced_rerun,
                validation_receipt_id=validation_receipt_id,
                data_hash=data_hash,
                feature_snapshot_hash=feature_snapshot_hash,
            )
            return self._build_failed_daily_decision_result(
                metadata=metadata,
                config=config,
                run_date=run_date,
                decision_key=decision_key,
                state_store=state_store,
                forced_rerun=forced_rerun,
                bootstrap=bootstrap,
                validation_receipt_id=validation_receipt_id,
                data_hash=data_hash,
                feature_snapshot_hash=feature_snapshot_hash,
                error_message=error_message,
                daily_decision_result_cls=DailyDecisionResult,
            )

    def run_daily(
        self,
        strategy: BaseStrategy,
        config: RunDailyConfig,
        *,
        btc_df: pl.DataFrame | None = None,
    ):
        from .api import DailyOrderReceipt, DailyOrderRequest, DailyRunResult
        from .execution_adapters import PaperExecutionAdapter, load_execution_adapter
        from .execution_state import IdempotencyConflictError, SQLiteExecutionStateStore

        self._validate_strategy_contract(strategy)
        metadata, _, run_ts, run_date = self._parse_run_daily_config(strategy, config)
        run_key = str(uuid4())
        state_store = SQLiteExecutionStateStore(config.state_db_path)
        fingerprint = self._daily_run_fingerprint(strategy, config, run_date)
        claim = state_store.claim_run(
            strategy_id=metadata.strategy_id,
            strategy_version=metadata.version,
            run_date=run_date,
            mode=config.mode,
            run_key=run_key,
            fingerprint=fingerprint,
            force=config.force,
        )
        if claim.status == "noop":
            existing = claim.existing_run
            assert existing is not None
            return self._build_noop_daily_result(
                metadata=metadata,
                config=config,
                run_date=run_date,
                state_store=state_store,
                existing_run=existing,
                daily_order_receipt_cls=DailyOrderReceipt,
                daily_run_result_cls=DailyRunResult,
            )

        forced_rerun = bool(config.force or claim.forced_overwrite)
        bootstrap = False
        validation_receipt_id: int | None = None
        data_hash = ""
        feature_snapshot_hash = ""
        try:
            computation = self._compute_daily_decision(
                strategy=strategy,
                metadata=metadata,
                config=config,
                run_date=run_date,
                run_ts=run_ts,
                decision_key=run_key,
                forced_rerun=forced_rerun,
                fingerprint=fingerprint,
                state_store=state_store,
                btc_df=btc_df,
            )
            bootstrap = computation.bootstrap
            validation_receipt_id = computation.validation_receipt_id
            data_hash = computation.data_hash
            feature_snapshot_hash = computation.feature_snapshot_hash

            if config.mode == "paper":
                adapter = PaperExecutionAdapter()
            else:
                adapter = load_execution_adapter(config.adapter_spec or "")
            adapter_name = adapter.__class__.__name__
            order_request = DailyOrderRequest(
                strategy_id=metadata.strategy_id,
                strategy_version=metadata.version,
                run_date=run_date,
                mode=config.mode,
                weight_today=computation.weight_today,
                notional_usd=computation.recommended_notional_usd,
                price_usd=computation.reference_price_usd,
                quantity_btc=computation.recommended_quantity_btc,
                btc_price_col=config.btc_price_col,
            )
            receipt = adapter.submit_order(order_request, idempotency_key=run_key)
            if isinstance(receipt, dict):
                receipt = DailyOrderReceipt(**receipt)
            if not isinstance(receipt, DailyOrderReceipt):
                raise TypeError("Execution adapter must return DailyOrderReceipt.")

            output_root = (
                Path(config.output_dir)
                / metadata.strategy_id
                / metadata.version
                / "daily"
                / run_date
            )
            output_root.mkdir(parents=True, exist_ok=True)
            artifact_path = output_root / f"{run_key}.json"
            result = DailyRunResult(
                status="executed",
                strategy_id=metadata.strategy_id,
                strategy_version=metadata.version,
                run_date=run_date,
                run_key=run_key,
                mode=config.mode,
                idempotency_hit=False,
                forced_rerun=computation.forced_rerun,
                weight_today=computation.weight_today,
                order_notional_usd=computation.recommended_notional_usd,
                btc_quantity=computation.recommended_quantity_btc,
                price_usd=computation.reference_price_usd,
                adapter_name=adapter_name,
                state_db_path=computation.state_db_path,
                artifact_path=str(artifact_path),
                message=(
                    "Daily execution completed."
                    if not computation.bootstrap
                    else "Daily execution completed (bootstrap run with no prior snapshot)."
                ),
                order_receipt=receipt,
                bootstrap=computation.bootstrap,
                validation_receipt_id=computation.validation_receipt_id,
                validation_passed=computation.validation_passed,
                data_hash=computation.data_hash,
                feature_snapshot_hash=computation.feature_snapshot_hash,
            )
            result_payload = result.to_json(path=artifact_path)
            state_store.mark_run_success_with_snapshot(
                strategy_id=metadata.strategy_id,
                strategy_version=metadata.version,
                run_date=run_date,
                mode=config.mode,
                payload=result_payload,
                order_summary=asdict(receipt),
                force_flag=computation.forced_rerun,
                snapshot_date=run_date,
                weights=computation.weights,
                validation_receipt_id=computation.validation_receipt_id,
                data_hash=computation.data_hash,
                feature_snapshot_hash=computation.feature_snapshot_hash,
            )
            return result
        except IdempotencyConflictError:
            raise
        except Exception as exc:
            if isinstance(exc, _DailyDecisionPreparationError):
                bootstrap = exc.bootstrap
                validation_receipt_id = exc.validation_receipt_id
                data_hash = exc.data_hash
                feature_snapshot_hash = exc.feature_snapshot_hash
            error_message = str(exc)
            failure_payload = {
                "status": "failed",
                "strategy_id": metadata.strategy_id,
                "strategy_version": metadata.version,
                "run_date": run_date,
                "run_key": run_key,
                "mode": config.mode,
                "error": error_message,
            }
            state_store.mark_run_failure(
                strategy_id=metadata.strategy_id,
                strategy_version=metadata.version,
                run_date=run_date,
                mode=config.mode,
                payload=failure_payload,
                force_flag=forced_rerun,
                validation_receipt_id=validation_receipt_id,
                data_hash=data_hash,
                feature_snapshot_hash=feature_snapshot_hash,
            )
            return self._build_failed_daily_result(
                metadata=metadata,
                config=config,
                run_date=run_date,
                run_key=run_key,
                state_store=state_store,
                forced_rerun=forced_rerun,
                bootstrap=bootstrap,
                validation_receipt_id=validation_receipt_id,
                data_hash=data_hash,
                feature_snapshot_hash=feature_snapshot_hash,
                error_message=error_message,
                daily_run_result_cls=DailyRunResult,
            )

    def reconcile_daily_run(
        self,
        strategy: BaseStrategy,
        *,
        run_date: str,
        mode: str = "paper",
        state_db_path: str = ".stacksats/run_state.sqlite3",
        btc_df: pl.DataFrame | None = None,
    ) -> dict[str, object]:
        from .execution_state import SQLiteExecutionStateStore

        self._validate_strategy_contract(strategy)
        metadata = strategy.metadata()
        state_store = SQLiteExecutionStateStore(state_db_path)
        stored = state_store.get_run(
            strategy_id=metadata.strategy_id,
            strategy_version=metadata.version,
            run_date=run_date,
            mode=mode,
        )
        if stored is None:
            raise ValueError("No stored daily run exists for the requested key.")
        btc_df_loaded = self._load_btc_df(btc_df, end_date=run_date)
        run_ts = dt.datetime.strptime(run_date[:10], "%Y-%m-%d")
        window_end = run_ts
        window_start = window_end - ALLOCATION_WINDOW_OFFSET
        locked_prefix = state_store.load_locked_prefix(
            strategy_id=metadata.strategy_id,
            strategy_version=metadata.version,
            mode=mode,
            run_date=run_date,
            window_start=window_start,
        )
        features_df, feature_snapshot_hash = self._feature_registry.materialization_fingerprint(
            strategy,
            btc_df_loaded,
            start_date=window_start,
            end_date=window_end,
            current_date=window_end,
        )
        weights = strategy.compute_weights(
            strategy_context_from_features_df(
                features_df,
                window_start,
                window_end,
                window_end,
                required_columns=tuple(strategy.required_feature_columns()),
                as_of_date=window_end,
                locked_weights=locked_prefix,
            )
        )
        run_row = weights.filter(pl.col("date") == run_ts)
        weight_today = float(run_row["weight"][0]) if not run_row.is_empty() else None
        prior_weight = stored.payload.get("weight_today")
        previous_feature_snapshot_hash = stored.payload.get("feature_snapshot_hash", "")
        status = self._classify_reconciliation_status(
            previous_weight_today=(
                float(prior_weight) if prior_weight is not None else None
            ),
            recomputed_weight_today=weight_today,
            previous_feature_snapshot_hash=previous_feature_snapshot_hash,
            recomputed_feature_snapshot_hash=feature_snapshot_hash,
        )
        return {
            "status": status,
            "previous_weight_today": prior_weight,
            "recomputed_weight_today": weight_today,
            "previous_feature_snapshot_hash": previous_feature_snapshot_hash,
            "recomputed_feature_snapshot_hash": feature_snapshot_hash,
        }

    def export(
        self,
        strategy: BaseStrategy,
        config: ExportConfig,
        *,
        btc_df: pl.DataFrame | None = None,
        current_date: dt.datetime | None = None,
    ) -> WeightTimeSeriesBatch:
        from .export_weights import process_start_date_batch
        from .prelude import generate_date_ranges, group_ranges_by_start_date

        self._validate_strategy_contract(strategy)
        metadata = strategy.metadata()
        btc_df = self._load_btc_df(btc_df, end_date=config.range_end)
        now = dt.datetime.now(dt.timezone.utc)
        run_date = current_date or now.replace(hour=0, minute=0, second=0, microsecond=0)
        start_ts = dt.datetime.strptime(config.range_start[:10], "%Y-%m-%d")
        end_ts = dt.datetime.strptime(config.range_end[:10], "%Y-%m-%d")
        features_df = self._materialize_strategy_features(
            strategy,
            btc_df,
            start_date=start_ts,
            end_date=end_ts,
            current_date=run_date,
        )

        date_ranges = generate_date_ranges(config.range_start, config.range_end)
        grouped_ranges = group_ranges_by_start_date(date_ranges)
        all_results = []
        for start_date, end_dates in sorted(grouped_ranges.items()):
            all_results.append(
                process_start_date_batch(
                    start_date,
                    end_dates,
                    features_df,
                    btc_df,
                    run_date,
                    config.btc_price_col,
                    strategy=strategy,
                    enforce_span_contract=True,
                )
            )
        if not all_results:
            raise ValueError("No export ranges generated from provided export config.")
        result_df = pl.concat(all_results, how="vertical_relaxed")

        provenance = self._provenance(strategy, config)
        series_batch = WeightTimeSeriesBatch.from_flat_dataframe(
            result_df,
            strategy_id=provenance["strategy_id"],
            strategy_version=provenance["version"],
            run_id=provenance["run_id"],
            config_hash=provenance["config_hash"],
        )
        output_root = (
            Path(config.output_dir)
            / metadata.strategy_id
            / metadata.version
            / provenance["run_id"]
        )
        output_root.mkdir(parents=True, exist_ok=True)
        result_path = output_root / "weights.csv"
        export_df = series_batch.to_dataframe()
        if isinstance(export_df, pl.DataFrame):
            export_df.write_csv(result_path, include_header=True)
        else:
            export_df.to_csv(result_path, index=False)
        schema_path = output_root / "timeseries_schema.md"
        schema_path.write_text(series_batch.schema_markdown(), encoding="utf-8")
        artifact_metadata = StrategyArtifactSet(
            schema_version=PUBLIC_ARTIFACT_SCHEMA_VERSION,
            strategy_id=provenance["strategy_id"],
            version=provenance["version"],
            config_hash=provenance["config_hash"],
            run_id=provenance["run_id"],
            output_dir=str(output_root),
            files={
                "weights_csv": result_path.name,
                "timeseries_schema_md": schema_path.name,
            },
        )
        (output_root / "artifacts.json").write_text(
            json.dumps(asdict(artifact_metadata), indent=2),
            encoding="utf-8",
        )
        return series_batch


__all__ = [
    "STRICT_MUTATION_MESSAGE",
    "STRICT_PROFILE_MUTATION_MESSAGE",
    "WIN_RATE_TOLERANCE",
    "StrategyRunner",
    "WeightValidationError",
]
