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.
- quantbt/__init__.py +862 -0
- quantbt/adapters/__init__.py +5 -0
- quantbt/adapters/nautilus/__init__.py +42 -0
- quantbt/adapters/nautilus/_dependency.py +53 -0
- quantbt/adapters/nautilus/backend.py +639 -0
- quantbt/adapters/nautilus/instruments.py +275 -0
- quantbt/adapters/nautilus/options.py +465 -0
- quantbt/adapters/nautilus/reports.py +231 -0
- quantbt/backends/__init__.py +29 -0
- quantbt/backends/_native_event_rust.py +2168 -0
- quantbt/backends/native_event.py +5048 -0
- quantbt/backends/native_option.py +796 -0
- quantbt/backends/native_portfolio.py +928 -0
- quantbt/backends/native_vectorized.py +1299 -0
- quantbt/backtester.py +485 -0
- quantbt/benchmarks/__init__.py +1 -0
- quantbt/benchmarks/compare_phase9_parity.py +302 -0
- quantbt/benchmarks/gamma_scalping_backtestsample.py +802 -0
- quantbt/benchmarks/profile_phase7.py +415 -0
- quantbt/benchmarks/run_arbitrage_phase_e.py +160 -0
- quantbt/benchmarks/run_optimization_overhead.py +195 -0
- quantbt/benchmarks/run_options_engine.py +272 -0
- quantbt/benchmarks/run_pct_equity_nautilus_smoke.py +227 -0
- quantbt/benchmarks/run_phase12_arbitrage_cert.py +402 -0
- quantbt/benchmarks/run_phase12_benchmark_nautilus_cert.py +430 -0
- quantbt/benchmarks/run_phase13_portfolio_report.py +97 -0
- quantbt/benchmarks/run_phase13_wfo_cache.py +162 -0
- quantbt/benchmarks/run_phase14_service_loop.py +694 -0
- quantbt/benchmarks/run_phase15a_nautilus_certification.py +514 -0
- quantbt/benchmarks/run_phase15b_synthetic_depth.py +175 -0
- quantbt/benchmarks/run_phase16_performance_debt.py +389 -0
- quantbt/benchmarks/run_phase30e_reactive_runner.py +161 -0
- quantbt/benchmarks/run_phase31_intrabar.py +441 -0
- quantbt/benchmarks/run_phase34a_native_event_memory.py +179 -0
- quantbt/benchmarks/run_phase34b_native_event_prepared_score.py +173 -0
- quantbt/benchmarks/run_phase34c_native_event_single_pass.py +176 -0
- quantbt/benchmarks/run_phase7.py +654 -0
- quantbt/benchmarks/run_portfolio_real_parity.py +537 -0
- quantbt/core/__init__.py +323 -0
- quantbt/core/arbitrage.py +767 -0
- quantbt/core/basket.py +234 -0
- quantbt/core/certification.py +274 -0
- quantbt/core/constraints.py +155 -0
- quantbt/core/engine.py +1144 -0
- quantbt/core/event.py +869 -0
- quantbt/core/execution_contract.py +197 -0
- quantbt/core/execution_depth.py +613 -0
- quantbt/core/intrabar_kernel.py +1906 -0
- quantbt/core/intrabar_reference.py +907 -0
- quantbt/core/intrabar_session.py +156 -0
- quantbt/core/market_tape.py +480 -0
- quantbt/core/native_event_capabilities.py +121 -0
- quantbt/core/native_event_parity.py +336 -0
- quantbt/core/order_compiler.py +422 -0
- quantbt/core/orders.py +319 -0
- quantbt/core/portfolio.py +392 -0
- quantbt/core/preprocessor.py +256 -0
- quantbt/core/reactive.py +173 -0
- quantbt/core/results.py +341 -0
- quantbt/core/schema.py +211 -0
- quantbt/core/scopes.py +96 -0
- quantbt/core/structured_orders.py +378 -0
- quantbt/core/types.py +87 -0
- quantbt/core/vectorized.py +203 -0
- quantbt/endpoint.py +4465 -0
- quantbt/engines.py +1014 -0
- quantbt/metrics/__init__.py +45 -0
- quantbt/metrics/options_analytics.py +46 -0
- quantbt/metrics/performance.py +541 -0
- quantbt/optimization/__init__.py +94 -0
- quantbt/optimization/callbacks.py +116 -0
- quantbt/optimization/candidate_selection.py +397 -0
- quantbt/optimization/config.py +84 -0
- quantbt/optimization/constraints.py +22 -0
- quantbt/optimization/evaluator.py +21 -0
- quantbt/optimization/evaluators/__init__.py +32 -0
- quantbt/optimization/evaluators/arbitrage.py +22 -0
- quantbt/optimization/evaluators/generic.py +34 -0
- quantbt/optimization/evaluators/grid_dca.py +22 -0
- quantbt/optimization/evaluators/intrabar.py +54 -0
- quantbt/optimization/evaluators/native_event.py +51 -0
- quantbt/optimization/evaluators/options.py +22 -0
- quantbt/optimization/evaluators/portfolio.py +42 -0
- quantbt/optimization/evaluators/signal.py +43 -0
- quantbt/optimization/multiseed.py +176 -0
- quantbt/optimization/objectives.py +221 -0
- quantbt/optimization/optimizer.py +500 -0
- quantbt/optimization/result.py +80 -0
- quantbt/optimization/samplers.py +84 -0
- quantbt/optimization/space.py +223 -0
- quantbt/options/__init__.py +215 -0
- quantbt/options/cache.py +116 -0
- quantbt/options/conventions.py +158 -0
- quantbt/options/data.py +177 -0
- quantbt/options/execution.py +600 -0
- quantbt/options/fees.py +135 -0
- quantbt/options/greeks.py +173 -0
- quantbt/options/hedging.py +232 -0
- quantbt/options/iv.py +188 -0
- quantbt/options/ledger.py +263 -0
- quantbt/options/lifecycle.py +94 -0
- quantbt/options/margin.py +311 -0
- quantbt/options/packages.py +147 -0
- quantbt/options/pricing.py +191 -0
- quantbt/options/schema.py +227 -0
- quantbt/options/selectors.py +281 -0
- quantbt/options/strategy.py +249 -0
- quantbt/options/surface.py +142 -0
- quantbt/options/tape.py +226 -0
- quantbt/options/templates/__init__.py +33 -0
- quantbt/options/templates/packages.py +354 -0
- quantbt/portfolio.py +606 -0
- quantbt/py.typed +1 -0
- quantbt/reporting/__init__.py +38 -0
- quantbt/reporting/arbitrage_audit.py +211 -0
- quantbt/reporting/nautilus_bundle.py +758 -0
- quantbt/reporting/nautilus_certification.py +226 -0
- quantbt/reporting/nautilus_diagnostics.py +225 -0
- quantbt/reporting/parity.py +496 -0
- quantbt/reporting/portfolio_audit.py +206 -0
- quantbt/reporting/portfolio_nautilus.py +140 -0
- quantbt/sizing/__init__.py +3 -0
- quantbt/sizing/fast.py +67 -0
- quantbt/sizing/modes.py +162 -0
- quantbt/viz/__init__.py +4 -0
- quantbt/viz/plots.py +310 -0
- quantbt/viz/themes.py +102 -0
- quantbt/walkforward.py +3136 -0
- quantbt_engine-1.0.7.dist-info/METADATA +891 -0
- quantbt_engine-1.0.7.dist-info/RECORD +133 -0
- quantbt_engine-1.0.7.dist-info/WHEEL +5 -0
- quantbt_engine-1.0.7.dist-info/licenses/LICENSE +21 -0
- 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)
|