Hyperparameter Optimisation¶
Optuna-based hyperparameter-optimisation layer (tinycta.hyper).
Installed via the optional hyper extra:
tinycta.hyper
¶
Hyperparameter optimisation support via Optuna.
Public API¶
Study: Frozen dataclass wrapping a completed Optuna study.optimize: Convenience wrapper: build objective, run study, print, returnStudy.get_config: Set up logger and config sections for a notebook experiment.ExperimentConfig: NamedTuple returned byget_config.
ExperimentConfig
¶
Bases: NamedTuple
Resources bundled for a notebook experiment run.
Example
from tinycta.hyper import ExperimentConfig cfg = ExperimentConfig(name="momentum", logger=None, params={"fast": 16, "slow": 64}) cfg.name 'momentum' cfg.params
The three config sections are optional and default to None, so a
config file that omits one is not an error:
cfg.optuna is None and cfg.data is None True
Being a :class:~typing.NamedTuple, it also unpacks positionally:
name, _logger, params, _optuna, _data = cfg name, sorted(params) ('momentum', ['fast', 'slow'])
Source code in src/tinycta/hyper/_setup.py
Study
dataclass
¶
Frozen wrapper around a completed Optuna study.
Example
import optuna from tinycta.hyper import Study optuna.logging.set_verbosity(optuna.logging.WARNING) s = optuna.create_study(direction="maximize", sampler=optuna.samplers.TPESampler(seed=0)) s.optimize(lambda trial: trial.suggest_float("x", 0.0, 1.0), n_trials=5) study = Study.from_optuna(s) study.n_trials, study.n_completed (5, 5) sorted(study.best_params) ['x']
str renders the best trial as a report block:
print(study) # doctest: +ELLIPSIS === Best parameters === x = 0... Sharpe = 0... Completed = 5 / 5 trials
A study in which every trial was pruned (each scored a NaN Sharpe) is not an error — it reports no best parameters and a NaN best value:
pruned = optuna.create_study(direction="maximize") pruned.optimize( ... lambda trial: (_ for _ in ()).throw(optuna.exceptions.TrialPruned()), n_trials=2 ... ) empty = Study.from_optuna(pruned) empty.n_completed, empty.best_params (0, {}) print(empty) No completed trials — all returned NaN Sharpe.
Source code in src/tinycta/hyper/_study.py
__str__()
¶
Return a human-readable summary of the best trial.
Source code in src/tinycta/hyper/_study.py
from_optuna(s)
classmethod
¶
Wrap a completed optuna.Study in a frozen Study.
Source code in src/tinycta/hyper/_study.py
plot(output_dir)
¶
Write Optuna visualisation plots to output_dir (HTML, PNG if kaleido available).
Source code in src/tinycta/hyper/_study.py
get_config(name, config_path=None)
¶
Return logger and config sections for an experiment.
Accepts either a shared config.yml or an experiment-specific
config/{name}.yml. Paths in the config are resolved relative to the
notebooks directory (one level above any config/ subdirectory).
NOTEBOOK_OUTPUT_FOLDER env var overrides the output directory used for
the log file sink; otherwise the config-derived output directory is confined
under the notebooks directory.
Example
import tempfile from pathlib import Path import yaml from tinycta.hyper import get_config
A shared config.yml supplies the sections directly:
with tempfile.TemporaryDirectory() as tmp: ... config_path = Path(tmp) / "config.yml" ... _ = config_path.write_text( ... yaml.safe_dump({"params": {"fast": 16, "slow": 64}, "data": {"output_path": "out"}}) ... ) ... cfg = get_config("momentum", config_path=config_path) ... output_exists = (Path(tmp) / "out" / "momentum").is_dir() cfg.name 'momentum' cfg.params
The output directory is created as a side effect, ready for the run's
artefacts and its output.log sink:
output_exists True
An output_path that would escape the notebooks directory is rejected
rather than followed:
with tempfile.TemporaryDirectory() as tmp: ... config_path = Path(tmp) / "config.yml" ... _ = config_path.write_text(yaml.safe_dump({"data": {"output_path": "../../etc"}})) ... try: ... get_config("momentum", config_path=config_path) ... except ValueError: ... print("escaping output_path rejected") escaping output_path rejected
Source code in src/tinycta/hyper/_setup.py
optimize(suggest_portfolio_fn, n_trials=100, seed=42)
¶
Build objective, run study, log the summary and return a frozen Study.
suggest_portfolio_fn draws its parameters from the trial and returns a
portfolio; the trial is then scored by that portfolio's Sharpe ratio, which
the study maximises. A trial whose Sharpe is NaN is pruned rather than fatal.
Example
from types import SimpleNamespace from tinycta.hyper import optimize
Any object exposing .stats.sharpe() works here; in real use that is a
jquantstats Portfolio built from the strategy's returns:
def portfolio(sharpe): ... return SimpleNamespace(stats=SimpleNamespace(sharpe=lambda: sharpe))
The objective is maximised, so the best value is the highest Sharpe seen —
here the reward peaks where fast is largest:
study = optimize(lambda trial: portfolio(trial.suggest_int("fast", 1, 8)), n_trials=12) study.best_value 8.0 study.best_params
Runs are seeded, so the same objective and seed reproduce the same result:
repeat = optimize(lambda trial: portfolio(trial.suggest_int("fast", 1, 8)), n_trials=12) repeat.best_params == study.best_params True