quantbt-engine 1.0.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. quantbt/__init__.py +862 -0
  2. quantbt/adapters/__init__.py +5 -0
  3. quantbt/adapters/nautilus/__init__.py +42 -0
  4. quantbt/adapters/nautilus/_dependency.py +53 -0
  5. quantbt/adapters/nautilus/backend.py +639 -0
  6. quantbt/adapters/nautilus/instruments.py +275 -0
  7. quantbt/adapters/nautilus/options.py +465 -0
  8. quantbt/adapters/nautilus/reports.py +231 -0
  9. quantbt/backends/__init__.py +29 -0
  10. quantbt/backends/_native_event_rust.py +2168 -0
  11. quantbt/backends/native_event.py +5048 -0
  12. quantbt/backends/native_option.py +796 -0
  13. quantbt/backends/native_portfolio.py +928 -0
  14. quantbt/backends/native_vectorized.py +1299 -0
  15. quantbt/backtester.py +485 -0
  16. quantbt/benchmarks/__init__.py +1 -0
  17. quantbt/benchmarks/compare_phase9_parity.py +302 -0
  18. quantbt/benchmarks/gamma_scalping_backtestsample.py +802 -0
  19. quantbt/benchmarks/profile_phase7.py +415 -0
  20. quantbt/benchmarks/run_arbitrage_phase_e.py +160 -0
  21. quantbt/benchmarks/run_optimization_overhead.py +195 -0
  22. quantbt/benchmarks/run_options_engine.py +272 -0
  23. quantbt/benchmarks/run_pct_equity_nautilus_smoke.py +227 -0
  24. quantbt/benchmarks/run_phase12_arbitrage_cert.py +402 -0
  25. quantbt/benchmarks/run_phase12_benchmark_nautilus_cert.py +430 -0
  26. quantbt/benchmarks/run_phase13_portfolio_report.py +97 -0
  27. quantbt/benchmarks/run_phase13_wfo_cache.py +162 -0
  28. quantbt/benchmarks/run_phase14_service_loop.py +694 -0
  29. quantbt/benchmarks/run_phase15a_nautilus_certification.py +514 -0
  30. quantbt/benchmarks/run_phase15b_synthetic_depth.py +175 -0
  31. quantbt/benchmarks/run_phase16_performance_debt.py +389 -0
  32. quantbt/benchmarks/run_phase30e_reactive_runner.py +161 -0
  33. quantbt/benchmarks/run_phase31_intrabar.py +441 -0
  34. quantbt/benchmarks/run_phase34a_native_event_memory.py +179 -0
  35. quantbt/benchmarks/run_phase34b_native_event_prepared_score.py +173 -0
  36. quantbt/benchmarks/run_phase34c_native_event_single_pass.py +176 -0
  37. quantbt/benchmarks/run_phase7.py +654 -0
  38. quantbt/benchmarks/run_portfolio_real_parity.py +537 -0
  39. quantbt/core/__init__.py +323 -0
  40. quantbt/core/arbitrage.py +767 -0
  41. quantbt/core/basket.py +234 -0
  42. quantbt/core/certification.py +274 -0
  43. quantbt/core/constraints.py +155 -0
  44. quantbt/core/engine.py +1144 -0
  45. quantbt/core/event.py +869 -0
  46. quantbt/core/execution_contract.py +197 -0
  47. quantbt/core/execution_depth.py +613 -0
  48. quantbt/core/intrabar_kernel.py +1906 -0
  49. quantbt/core/intrabar_reference.py +907 -0
  50. quantbt/core/intrabar_session.py +156 -0
  51. quantbt/core/market_tape.py +480 -0
  52. quantbt/core/native_event_capabilities.py +121 -0
  53. quantbt/core/native_event_parity.py +336 -0
  54. quantbt/core/order_compiler.py +422 -0
  55. quantbt/core/orders.py +319 -0
  56. quantbt/core/portfolio.py +392 -0
  57. quantbt/core/preprocessor.py +256 -0
  58. quantbt/core/reactive.py +173 -0
  59. quantbt/core/results.py +341 -0
  60. quantbt/core/schema.py +211 -0
  61. quantbt/core/scopes.py +96 -0
  62. quantbt/core/structured_orders.py +378 -0
  63. quantbt/core/types.py +87 -0
  64. quantbt/core/vectorized.py +203 -0
  65. quantbt/endpoint.py +4465 -0
  66. quantbt/engines.py +1014 -0
  67. quantbt/metrics/__init__.py +45 -0
  68. quantbt/metrics/options_analytics.py +46 -0
  69. quantbt/metrics/performance.py +541 -0
  70. quantbt/optimization/__init__.py +94 -0
  71. quantbt/optimization/callbacks.py +116 -0
  72. quantbt/optimization/candidate_selection.py +397 -0
  73. quantbt/optimization/config.py +84 -0
  74. quantbt/optimization/constraints.py +22 -0
  75. quantbt/optimization/evaluator.py +21 -0
  76. quantbt/optimization/evaluators/__init__.py +32 -0
  77. quantbt/optimization/evaluators/arbitrage.py +22 -0
  78. quantbt/optimization/evaluators/generic.py +34 -0
  79. quantbt/optimization/evaluators/grid_dca.py +22 -0
  80. quantbt/optimization/evaluators/intrabar.py +54 -0
  81. quantbt/optimization/evaluators/native_event.py +51 -0
  82. quantbt/optimization/evaluators/options.py +22 -0
  83. quantbt/optimization/evaluators/portfolio.py +42 -0
  84. quantbt/optimization/evaluators/signal.py +43 -0
  85. quantbt/optimization/multiseed.py +176 -0
  86. quantbt/optimization/objectives.py +221 -0
  87. quantbt/optimization/optimizer.py +500 -0
  88. quantbt/optimization/result.py +80 -0
  89. quantbt/optimization/samplers.py +84 -0
  90. quantbt/optimization/space.py +223 -0
  91. quantbt/options/__init__.py +215 -0
  92. quantbt/options/cache.py +116 -0
  93. quantbt/options/conventions.py +158 -0
  94. quantbt/options/data.py +177 -0
  95. quantbt/options/execution.py +600 -0
  96. quantbt/options/fees.py +135 -0
  97. quantbt/options/greeks.py +173 -0
  98. quantbt/options/hedging.py +232 -0
  99. quantbt/options/iv.py +188 -0
  100. quantbt/options/ledger.py +263 -0
  101. quantbt/options/lifecycle.py +94 -0
  102. quantbt/options/margin.py +311 -0
  103. quantbt/options/packages.py +147 -0
  104. quantbt/options/pricing.py +191 -0
  105. quantbt/options/schema.py +227 -0
  106. quantbt/options/selectors.py +281 -0
  107. quantbt/options/strategy.py +249 -0
  108. quantbt/options/surface.py +142 -0
  109. quantbt/options/tape.py +226 -0
  110. quantbt/options/templates/__init__.py +33 -0
  111. quantbt/options/templates/packages.py +354 -0
  112. quantbt/portfolio.py +606 -0
  113. quantbt/py.typed +1 -0
  114. quantbt/reporting/__init__.py +38 -0
  115. quantbt/reporting/arbitrage_audit.py +211 -0
  116. quantbt/reporting/nautilus_bundle.py +758 -0
  117. quantbt/reporting/nautilus_certification.py +226 -0
  118. quantbt/reporting/nautilus_diagnostics.py +225 -0
  119. quantbt/reporting/parity.py +496 -0
  120. quantbt/reporting/portfolio_audit.py +206 -0
  121. quantbt/reporting/portfolio_nautilus.py +140 -0
  122. quantbt/sizing/__init__.py +3 -0
  123. quantbt/sizing/fast.py +67 -0
  124. quantbt/sizing/modes.py +162 -0
  125. quantbt/viz/__init__.py +4 -0
  126. quantbt/viz/plots.py +310 -0
  127. quantbt/viz/themes.py +102 -0
  128. quantbt/walkforward.py +3136 -0
  129. quantbt_engine-1.0.7.dist-info/METADATA +891 -0
  130. quantbt_engine-1.0.7.dist-info/RECORD +133 -0
  131. quantbt_engine-1.0.7.dist-info/WHEEL +5 -0
  132. quantbt_engine-1.0.7.dist-info/licenses/LICENSE +21 -0
  133. quantbt_engine-1.0.7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,22 @@
1
+ """Formal constraint helpers for Optuna-backed optimization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Sequence
6
+
7
+
8
+ CONSTRAINTS_USER_ATTR = "quantbt_constraints"
9
+
10
+
11
+ def set_trial_constraints(trial, constraints: Sequence[float]) -> tuple[float, ...]:
12
+ """Store constraints on an Optuna trial using QuantBT's canonical key."""
13
+
14
+ values = tuple(float(value) for value in constraints)
15
+ trial.set_user_attr(CONSTRAINTS_USER_ATTR, values)
16
+ return values
17
+
18
+
19
+ def constraints_from_trial(frozen_trial) -> tuple[float, ...]:
20
+ """Optuna sampler callback returning trial constraints."""
21
+
22
+ return tuple(float(value) for value in frozen_trial.user_attrs.get(CONSTRAINTS_USER_ATTR, ()))
@@ -0,0 +1,21 @@
1
+ """Evaluator protocol for domain-specific optimization adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping, Protocol
6
+
7
+ from .result import ObjectiveResult
8
+
9
+
10
+ class TrialEvaluator(Protocol):
11
+ """Protocol implemented by domain adapters.
12
+
13
+ The optimizer only sees parameters and an ObjectiveResult. Signal,
14
+ intrabar, portfolio, arbitrage, grid/DCA, and options details must remain
15
+ inside evaluator implementations.
16
+ """
17
+
18
+ def evaluate(self, params: Mapping[str, Any]) -> ObjectiveResult:
19
+ """Evaluate one parameter set and return objective values."""
20
+
21
+ ...
@@ -0,0 +1,32 @@
1
+ """Domain-specific optimization evaluators.
2
+
3
+ Phase 32A intentionally keeps this namespace empty except for package
4
+ discovery. Prepared signal/intrabar/portfolio and generic endpoint evaluators
5
+ are implemented in Phase 32B.
6
+ """
7
+
8
+ __all__: list[str] = []
9
+ """Domain evaluator adapters for QuantBT optimization."""
10
+
11
+ from .arbitrage import ArbitrageGenericEvaluator, ArbitrageTrialOutput
12
+ from .generic import GenericEndpointEvaluator
13
+ from .grid_dca import GridDCAGenericEvaluator, GridDCATrialOutput
14
+ from .intrabar import PreparedIntrabarEvaluator
15
+ from .native_event import PreparedNativeEventStrategyEvaluator
16
+ from .options import OptionPackageGenericEvaluator, OptionTrialOutput
17
+ from .portfolio import PreparedPortfolioEvaluator
18
+ from .signal import PreparedSignalEvaluator
19
+
20
+ __all__ = [
21
+ "ArbitrageGenericEvaluator",
22
+ "ArbitrageTrialOutput",
23
+ "GenericEndpointEvaluator",
24
+ "GridDCAGenericEvaluator",
25
+ "GridDCATrialOutput",
26
+ "OptionPackageGenericEvaluator",
27
+ "OptionTrialOutput",
28
+ "PreparedIntrabarEvaluator",
29
+ "PreparedNativeEventStrategyEvaluator",
30
+ "PreparedPortfolioEvaluator",
31
+ "PreparedSignalEvaluator",
32
+ ]
@@ -0,0 +1,22 @@
1
+ """Generic arbitrage optimization adapter contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+ from .generic import GenericEndpointEvaluator
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class ArbitrageTrialOutput:
13
+ """Domain output contract for arbitrage trial builders."""
14
+
15
+ signal: Any
16
+ hedge_ratios: Any = None
17
+ run_overrides: dict[str, Any] = field(default_factory=dict)
18
+
19
+
20
+ class ArbitrageGenericEvaluator(GenericEndpointEvaluator):
21
+ """Generic fallback for arbitrage endpoints until specialized evaluators exist."""
22
+
@@ -0,0 +1,34 @@
1
+ """Generic QuantBT endpoint evaluator fallback."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Callable, Mapping
7
+
8
+ from ..result import ObjectiveResult
9
+
10
+
11
+ ObjectiveBuilder = Callable[[Any, Mapping[str, Any]], ObjectiveResult]
12
+
13
+
14
+ @dataclass
15
+ class GenericEndpointEvaluator:
16
+ """Evaluate params by building endpoint inputs and calling a run function."""
17
+
18
+ build_run_inputs: Callable[[Mapping[str, Any]], Mapping[str, Any]]
19
+ run_func: Callable[..., Any]
20
+ objective_builder: ObjectiveBuilder
21
+ metadata: dict[str, Any] = field(default_factory=dict)
22
+
23
+ last_result: Any = field(default=None, init=False)
24
+ last_run_inputs: dict[str, Any] = field(default_factory=dict, init=False)
25
+
26
+ def evaluate(self, params: Mapping[str, Any]) -> ObjectiveResult:
27
+ run_inputs = dict(self.build_run_inputs(params))
28
+ result = self.run_func(**run_inputs)
29
+ objective = self.objective_builder(result, params)
30
+ if not isinstance(objective, ObjectiveResult):
31
+ raise TypeError("objective_builder must return ObjectiveResult")
32
+ self.last_run_inputs = run_inputs
33
+ self.last_result = result
34
+ return objective
@@ -0,0 +1,22 @@
1
+ """Generic grid/DCA optimization adapter contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+ from .generic import GenericEndpointEvaluator
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class GridDCATrialOutput:
13
+ """Domain output contract for structural grid/DCA trial builders."""
14
+
15
+ levels: Any = None
16
+ order_plan: Any = None
17
+ run_overrides: dict[str, Any] = field(default_factory=dict)
18
+
19
+
20
+ class GridDCAGenericEvaluator(GenericEndpointEvaluator):
21
+ """Generic fallback for grid/DCA endpoints until prepared adapters exist."""
22
+
@@ -0,0 +1,54 @@
1
+ """Prepared intrabar evaluator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Callable, Mapping, Optional
7
+
8
+ import pandas as pd
9
+
10
+ from ..result import ObjectiveResult
11
+ from .generic import ObjectiveBuilder
12
+
13
+
14
+ @dataclass
15
+ class PreparedIntrabarEvaluator:
16
+ """Replay intrabar strategy intents through a prepared intrabar runner."""
17
+
18
+ runner: Any
19
+ strategy_func: Callable[..., Any]
20
+ objective_builder: ObjectiveBuilder
21
+ intent_builder: Optional[Callable[[Any, Mapping[str, Any]], Any]] = None
22
+ report_level: str = "minimal"
23
+ pass_runner: bool = False
24
+ pass_market: bool = False
25
+
26
+ last_result: Any = field(default=None, init=False)
27
+ last_intent: Any = field(default=None, init=False)
28
+
29
+ def evaluate(self, params: Mapping[str, Any]) -> ObjectiveResult:
30
+ if self.pass_runner:
31
+ output = self.strategy_func(self.runner, params)
32
+ elif self.pass_market:
33
+ output = self.strategy_func(self.runner.market, params)
34
+ else:
35
+ output = self.strategy_func(params)
36
+ intent = self._to_intent(output, params)
37
+ result = self.runner.run(intent, report_level=self.report_level)
38
+ objective = self.objective_builder(result, params)
39
+ if not isinstance(objective, ObjectiveResult):
40
+ raise TypeError("objective_builder must return ObjectiveResult")
41
+ self.last_intent = intent
42
+ self.last_result = result
43
+ return objective
44
+
45
+ def _to_intent(self, output: Any, params: Mapping[str, Any]) -> Any:
46
+ from ...core.intrabar_reference import IntrabarIntentTape
47
+
48
+ if self.intent_builder is not None:
49
+ return self.intent_builder(output, params)
50
+ if isinstance(output, IntrabarIntentTape):
51
+ return output
52
+ if isinstance(output, pd.DataFrame):
53
+ return IntrabarIntentTape.from_frame(output)
54
+ raise TypeError("intrabar strategy must return IntrabarIntentTape or DataFrame, or provide intent_builder")
@@ -0,0 +1,51 @@
1
+ """Prepared native-event strategy evaluator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Callable, Mapping
7
+
8
+ from ..result import ObjectiveResult
9
+ from ...backends.native_event import NativeEventScoreRequirements
10
+ from .generic import ObjectiveBuilder
11
+
12
+
13
+ @dataclass
14
+ class PreparedNativeEventStrategyEvaluator:
15
+ """Evaluate reactive native-event strategies through a prepared runner."""
16
+
17
+ runner: Any
18
+ strategy_factory: Callable[[Mapping[str, Any]], Any]
19
+ objective_builder: ObjectiveBuilder
20
+ trading_days: int = 365
21
+ retain_last: bool = False
22
+ score_requirements: NativeEventScoreRequirements = field(
23
+ default_factory=NativeEventScoreRequirements.scalar_score_contract
24
+ )
25
+
26
+ last_result: Any = field(default=None, init=False)
27
+ last_strategy: Any = field(default=None, init=False)
28
+
29
+ def evaluate(self, params: Mapping[str, Any]) -> ObjectiveResult:
30
+ strategy = self.strategy_factory(params)
31
+ requirements = NativeEventScoreRequirements.from_strategy(
32
+ strategy,
33
+ base=self.score_requirements,
34
+ )
35
+ result = self.runner.score(
36
+ strategy,
37
+ trading_days=self.trading_days,
38
+ score_requirements=requirements,
39
+ )
40
+ objective = self.objective_builder(result, params)
41
+ if not isinstance(objective, ObjectiveResult):
42
+ raise TypeError("objective_builder must return ObjectiveResult")
43
+ if self.retain_last:
44
+ self.last_strategy = strategy
45
+ self.last_result = result
46
+ else:
47
+ # Optimization can run thousands of trials. Retaining a strategy
48
+ # and score result pins their arrays until the evaluator dies.
49
+ self.last_strategy = None
50
+ self.last_result = None
51
+ return objective
@@ -0,0 +1,22 @@
1
+ """Generic option-package optimization adapter contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+ from .generic import GenericEndpointEvaluator
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class OptionTrialOutput:
13
+ """Domain output contract for option package trial builders."""
14
+
15
+ package: Any = None
16
+ hedge_plan: Any = None
17
+ run_overrides: dict[str, Any] = field(default_factory=dict)
18
+
19
+
20
+ class OptionPackageGenericEvaluator(GenericEndpointEvaluator):
21
+ """Generic fallback for option package endpoints until prepared adapters exist."""
22
+
@@ -0,0 +1,42 @@
1
+ """Prepared native portfolio evaluator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Callable, Mapping, Optional
7
+
8
+ from ..result import ObjectiveResult
9
+ from .generic import ObjectiveBuilder
10
+
11
+
12
+ @dataclass
13
+ class PreparedPortfolioEvaluator:
14
+ """Replay strategy position matrices through a prepared portfolio context."""
15
+
16
+ prepared_context: Any
17
+ strategy_func: Callable[..., Any]
18
+ objective_builder: ObjectiveBuilder
19
+ pass_context: bool = False
20
+ positions_key: Optional[str] = None
21
+
22
+ last_result: Any = field(default=None, init=False)
23
+ last_positions: Any = field(default=None, init=False)
24
+
25
+ def evaluate(self, params: Mapping[str, Any]) -> ObjectiveResult:
26
+ output = self.strategy_func(self.prepared_context, params) if self.pass_context else self.strategy_func(params)
27
+ positions = _extract_positions(output, positions_key=self.positions_key)
28
+ result = self.prepared_context.backtest(positions=positions)
29
+ objective = self.objective_builder(result, params)
30
+ if not isinstance(objective, ObjectiveResult):
31
+ raise TypeError("objective_builder must return ObjectiveResult")
32
+ self.last_positions = positions
33
+ self.last_result = result
34
+ return objective
35
+
36
+
37
+ def _extract_positions(output: Any, *, positions_key: Optional[str]) -> Any:
38
+ if positions_key is None:
39
+ return output
40
+ if isinstance(output, Mapping):
41
+ return output[positions_key]
42
+ return getattr(output, positions_key)
@@ -0,0 +1,43 @@
1
+ """Prepared single-symbol signal evaluator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Callable, Mapping, Optional
7
+
8
+ from ..result import ObjectiveResult
9
+ from .generic import ObjectiveBuilder
10
+
11
+
12
+ @dataclass
13
+ class PreparedSignalEvaluator:
14
+ """Replay strategy signals through a prepared single-symbol context."""
15
+
16
+ prepared_context: Any
17
+ strategy_func: Callable[..., Any]
18
+ objective_builder: ObjectiveBuilder
19
+ pass_context: bool = False
20
+ signal_key: Optional[str] = None
21
+ signal_col: Optional[str] = None
22
+
23
+ last_result: Any = field(default=None, init=False)
24
+ last_signal: Any = field(default=None, init=False)
25
+
26
+ def evaluate(self, params: Mapping[str, Any]) -> ObjectiveResult:
27
+ output = self.strategy_func(self.prepared_context, params) if self.pass_context else self.strategy_func(params)
28
+ signal = _extract_signal(output, signal_key=self.signal_key)
29
+ result = self.prepared_context.backtest(signal=signal, signal_col=self.signal_col)
30
+ objective = self.objective_builder(result, params)
31
+ if not isinstance(objective, ObjectiveResult):
32
+ raise TypeError("objective_builder must return ObjectiveResult")
33
+ self.last_signal = signal
34
+ self.last_result = result
35
+ return objective
36
+
37
+
38
+ def _extract_signal(output: Any, *, signal_key: Optional[str]) -> Any:
39
+ if signal_key is None:
40
+ return output
41
+ if isinstance(output, Mapping):
42
+ return output[signal_key]
43
+ return getattr(output, signal_key)
@@ -0,0 +1,176 @@
1
+ """Multi-seed optimization orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field, replace
6
+ from typing import Any, Callable, Mapping, Optional, Sequence
7
+
8
+ from .candidate_selection import CandidateSelector, RobustSelectionConfig
9
+ from .config import OptimizationConfig, SamplerConfig
10
+ from .evaluator import TrialEvaluator
11
+ from .optimizer import OptunaOptimizer, _apply_baseline_floor, _is_better
12
+ from .result import OptimizationResult, OptimizationTrialRecord
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class MultiSeedOptimization:
17
+ """Run the same search across several sampler seeds and aggregate trials.
18
+
19
+ This is a search-quality tool, not a different objective. Each seed still
20
+ optimizes the same evaluator; the aggregate result then selects production
21
+ params from regions that survive multiple random trajectories.
22
+ """
23
+
24
+ evaluator: TrialEvaluator
25
+ config: OptimizationConfig
26
+ sampler_config: SamplerConfig = field(default_factory=SamplerConfig)
27
+ seeds: Sequence[Optional[int]] = (None, 41, 42, 43, 44)
28
+ trials_per_seed: Optional[int] = None
29
+
30
+ def optimize(
31
+ self,
32
+ *,
33
+ param_ranges: Mapping[str, Any],
34
+ fixed_params: Optional[Mapping[str, Any]] = None,
35
+ initial_trials: Optional[Sequence[Mapping[str, Any]]] = None,
36
+ effective_params_builder: Optional[Callable[[Mapping[str, Any]], Mapping[str, Any]]] = None,
37
+ candidate_selector: Optional[CandidateSelector] = None,
38
+ ) -> OptimizationResult:
39
+ if not self.seeds:
40
+ raise ValueError("MultiSeedOptimization.seeds must be non-empty")
41
+
42
+ seed_results: list[OptimizationResult] = []
43
+ combined_trials: list[OptimizationTrialRecord] = []
44
+ seed_summaries: list[dict[str, Any]] = []
45
+ global_number = 0
46
+ for seed_index, seed in enumerate(self.seeds):
47
+ seed_label = "none" if seed is None else str(seed)
48
+ config = replace(
49
+ self.config,
50
+ seed=seed,
51
+ n_trials=int(self.trials_per_seed or self.config.n_trials),
52
+ study_name=f"{self.config.study_name}_seed_{seed_label}",
53
+ )
54
+ result = OptunaOptimizer(
55
+ evaluator=self.evaluator,
56
+ config=config,
57
+ sampler_config=self.sampler_config,
58
+ ).optimize(
59
+ param_ranges=param_ranges,
60
+ fixed_params=fixed_params,
61
+ initial_trials=initial_trials,
62
+ effective_params_builder=effective_params_builder,
63
+ )
64
+ seed_results.append(result)
65
+ seed_summaries.append(_seed_summary(result, seed=seed, seed_index=seed_index))
66
+ for record in result.trials:
67
+ metadata = dict(record.metadata)
68
+ metadata.update(
69
+ {
70
+ "quantbt_seed": seed_label,
71
+ "quantbt_seed_index": int(seed_index),
72
+ "quantbt_original_trial_number": int(record.number),
73
+ }
74
+ )
75
+ combined_trials.append(
76
+ OptimizationTrialRecord(
77
+ number=int(global_number),
78
+ state=str(record.state),
79
+ params=dict(record.params),
80
+ values=tuple(record.values),
81
+ metrics=dict(record.metrics),
82
+ constraints=tuple(record.constraints),
83
+ metadata=metadata,
84
+ )
85
+ )
86
+ global_number += 1
87
+
88
+ study_view = _StudyDirectionsView(seed_results[0].study.directions)
89
+ aggregate = OptimizationResult(
90
+ study=study_view,
91
+ best_params=None,
92
+ best_values=None,
93
+ pareto_trials=[],
94
+ trials=combined_trials,
95
+ trials_frame=None,
96
+ )
97
+ aggregate.baseline_trials = [
98
+ record
99
+ for record in combined_trials
100
+ if record.metadata.get("quantbt_source") == "warm_start"
101
+ ]
102
+ aggregate.seed_results = seed_summaries
103
+ _set_best_from_trials(aggregate)
104
+
105
+ selector = candidate_selector or CandidateSelector(
106
+ mode="robust_plateau",
107
+ config=RobustSelectionConfig(seed_consensus=min(2, len(self.seeds))),
108
+ )
109
+ selected = selector.select(aggregate)
110
+ aggregate.selected_params = dict(selected.params)
111
+ aggregate.selection_metadata = dict(selected.metadata)
112
+ aggregate.selection_metadata.update(
113
+ {
114
+ "selected_by_multiseed": True,
115
+ "seed_count": int(len(self.seeds)),
116
+ }
117
+ )
118
+ aggregate.search_diagnostics = {
119
+ "seed_count": int(len(self.seeds)),
120
+ "seed_results": seed_summaries,
121
+ "completed_trials": int(sum(1 for record in combined_trials if record.state == "COMPLETE")),
122
+ "pruned_trials": int(sum(1 for record in combined_trials if record.state == "PRUNED")),
123
+ "failed_trials": int(sum(1 for record in combined_trials if record.state == "FAIL")),
124
+ "top_parameter_frequency": _top_parameter_frequency(combined_trials),
125
+ }
126
+ _apply_baseline_floor(aggregate)
127
+ return aggregate
128
+
129
+
130
+ def _set_best_from_trials(result: OptimizationResult) -> None:
131
+ try:
132
+ direction = str(result.study.directions[0].name).lower()
133
+ except Exception:
134
+ direction = "maximize"
135
+ completed = [record for record in result.trials if record.state == "COMPLETE" and record.values]
136
+ if not completed:
137
+ return
138
+ best = completed[0]
139
+ for record in completed[1:]:
140
+ if _is_better(record.values[0], best.values[0], direction):
141
+ best = record
142
+ result.best_params = dict(best.params)
143
+ result.best_values = tuple(best.values)
144
+
145
+
146
+ def _seed_summary(result: OptimizationResult, *, seed: Optional[int], seed_index: int) -> dict[str, Any]:
147
+ return {
148
+ "seed": None if seed is None else int(seed),
149
+ "seed_index": int(seed_index),
150
+ "best_params": None if result.best_params is None else dict(result.best_params),
151
+ "best_values": None if result.best_values is None else tuple(float(value) for value in result.best_values),
152
+ "selected_params": None if result.selected_params is None else dict(result.selected_params),
153
+ "search_regression": bool(result.search_regression),
154
+ "baseline_rank": list(result.search_diagnostics.get("baseline_rank", [])),
155
+ "completed_trials": int(result.search_diagnostics.get("completed_trials", 0)),
156
+ }
157
+
158
+
159
+ def _top_parameter_frequency(records: Sequence[OptimizationTrialRecord]) -> dict[str, dict[str, int]]:
160
+ completed = [record for record in records if record.state == "COMPLETE" and record.values]
161
+ if not completed:
162
+ return {}
163
+ ranked = sorted(completed, key=lambda record: record.values[0], reverse=True)
164
+ top_n = max(1, len(ranked) // 10)
165
+ counts: dict[str, dict[str, int]] = {}
166
+ for record in ranked[:top_n]:
167
+ for name, value in record.params.items():
168
+ bucket = counts.setdefault(str(name), {})
169
+ label = str(value)
170
+ bucket[label] = bucket.get(label, 0) + 1
171
+ return counts
172
+
173
+
174
+ class _StudyDirectionsView:
175
+ def __init__(self, directions: Sequence[Any]):
176
+ self.directions = tuple(directions)