PineForge HPO 1.11.0
Native hyperparameter optimization for PineForge strategies
Loading...
Searching...
No Matches
pineforge-hpo

Native hyperparameter optimization for PineForge strategies.

pineforge-hpo compiles a PineScript strategy once, then runs parameter trials in a parallel C++ hot loop. It supports exhaustive search, reproducible random search, dlib global optimization, and a native Tree-structured Parzen Estimator (TPE). Objective functions can combine PineForge backtest metrics with arithmetic expressions and constraints.

‍Project status: alpha. The single-strategy path supports Linux and macOS. The public C++ model includes portfolio observations and custom-objective contracts, but multiple-strategy shared-account execution is not implemented yet.

Why pineforge-hpo?

  • No Python in the trial hot loop. Python initializes the study and artifact; C++ samples, backtests, scores, and schedules trials.
  • Compile once, override at runtime. Search parameters use strategy_set_input() and supported strategy(...) settings use strategy_set_override().
  • Backtest-native objectives. Combine report metrics with +, -, *, /, comparisons, min, max, and abs.
  • Explicit coverage guarantees. TPE can preserve a continuous space or operate without replacement over a finite stepped space.
  • Reproducible artifacts and studies. Results record sampler identity, seed, artifact hash, search-space coverage, and the complete trial table.
PineScript
|
| pineforge-codegen-oss (once)
v
generated C++ + input manifest
|
| content-addressed build (once)
v
strategy plugin + immutable OHLCV
|
| native C++ trials
v
sampler -> runtime inputs/overrides -> backtest -> objective -> ask/tell

The HPO layer consumes the public PineForge C ABI. It is not a BacktestEngine subclass, and it does not move transpiler or optimizer behavior into pineforge-engine.

Quick start

Prerequisites

  • Python 3.11 or newer;
  • CMake 3.20 or newer (the projects require 3.17; ctest --test-dir needs 3.20) and a C++17 compiler;
  • Linux or macOS;
  • Git and network access for the first dependency build.

Clone the repository, then initialize the two pinned integration dependencies:

git clone https://github.com/pineforge-4pass/pineforge-hpo.git
cd pineforge-hpo
git submodule update --init \
external/pineforge-engine \
external/pineforge-codegen-oss

The Python distribution exported by pineforge-codegen-oss is named pineforge-codegen; its import module is pineforge_codegen.

The gitlinks pin the releases this HPO revision targets: engine v1.11.0 (a44723242ea34c8b2b00cb82f4a18e23a26280d5, C ABI 5) and codegen v1.11.0 (702fac4e653219401b7a5d272d158fb9b89650a7). Linux CI gates the nine-trial example, real multi-symbol C-ABI equality and real-artifact D7 refusal; both native CI runners require the independently pinned serial golden rather than silently skipping it. From 1.0.0 on, codegen X.Y.Z is supported only with engine vX.Y.Z: a mismatched pair may still compile, but equal PF_ABI_VERSION values do not guarantee a compatible C++ source layout or the same behavior. An enum input's categorical choice is accepted in either form a StudySpec may carry: the manifest tag (for example Side.long) or the member's ordinal, so a study written for either form runs against a codegen 1.4.0 or later artifact. The optional transpile extra requires pineforge-codegen >= 1.1.0 and < 2, so install the release that matches your engine (pineforge-codegen==1.11.0 for the gitlinks). The native adapter reads the equity statistics by their engine 1.0 names, so it needs engine headers >= 1.1.0 (including the checked-settings API). Regenerate a precompiled plugin referenced from a StudySpec with codegen 1.11.0 and rebuild it against engine v1.11.0: plugins from engine v1.7.x or earlier (ABI 4 or lower) are refused with an ABI mismatch. Plugins from engine v1.10.x releases or development builds before v1.11.0 may also report ABI 5 and load without an error although they do not pair with the pinned releases. C++ code that reads ReportSnapshot::metrics.equity.sharpe_tv or sortino_tv must use sharpe_monthly or sortino_monthly.

When upgrading to v1.0.0 from the old gitlinks (engine 7bff706, codegen cefeec8), 3 of the 9 example trials report different metrics: since v0.13.0 the engine reports a position still open after the final bar as a range-end close, which counts as a trade. The best trial is unchanged.

Do not replace the gitlinks with each dependency's moving main branch in a release build. Updating a gitlink requires the same native tests and nine-trial end-to-end study used by CI. The submodules remain separate projects under their own licenses; nested engine corpus and benchmark-asset submodules are not required by this quick start.

The first engine configure downloads Eigen when it is not installed. The first HPO configure downloads the pinned dlib 20.0.1 archive and verifies its SHA-256; pass -DPINEFORGE_HPO_USE_SYSTEM_DLIB=ON only when an exact 20.0.1 CMake package is already installed. If pineforge-hpo reports that the Eigen include directory was not found (for example, the engine configure reused an Eigen fetched by another build tree), pass --eigen-include <dir> or set EIGEN3_INCLUDE_DIR, where <dir> is the directory that contains Eigen/Core.

From the pineforge-hpo repository root, the following block builds the engine and HPO runner, installs both local Python packages, runs the test suite, and executes the bundled nine-trial example:

export PINEFORGE_ENGINE_ROOT="$PWD/external/pineforge-engine"
cmake -S "$PINEFORGE_ENGINE_ROOT" -B "$PINEFORGE_ENGINE_ROOT/build" \
-DCMAKE_BUILD_TYPE=Release \
-DPINEFORGE_BUILD_TESTS=OFF \
-DPINEFORGE_BUILD_TUTORIAL=OFF \
-DPINEFORGE_BUILD_CORPUS_STRATEGIES=OFF
cmake --build "$PINEFORGE_ENGINE_ROOT/build" -j4
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e external/pineforge-codegen-oss
python -m pip install -e . --no-deps
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DPINEFORGE_ENGINE_ROOT="$PINEFORGE_ENGINE_ROOT"
cmake --build build -j4
ctest --test-dir build --output-on-failure
pineforge-hpo run examples/single_strategy/study.json \
--engine-root "$PINEFORGE_ENGINE_ROOT" \
--native "$PWD/build/bin/pineforge-hpo-native" \
--output build/quick-start-result.json

The command prints the complete JSON result. Check the compact summary with:

python - <<'PY'
import json
with open("build/quick-start-result.json", encoding="utf-8") as source:
result = json.load(source)
for key in (
"ok",
"best_trial_id",
"best_value",
"trials_completed",
"full_parameter_coverage",
):
print(f"{key}: {result[key]}")
PY

You should see ok: True, trials_completed: 9, and full_parameter_coverage: True. The first run transpiles and builds the strategy plugin; later runs reuse its content-addressed cache when the Pine source, engine ABI, codegen, compiler, flags, and dependencies have not changed.

To compile a Pine strategy without starting a study:

pineforge-hpo compile path/to/strategy.pine \
--engine-root "$PINEFORGE_ENGINE_ROOT"

This returns the plugin, manifest, provenance, artifact key, and cache-hit state as JSON. An existing validated plugin can also be referenced from a StudySpec, allowing the native runner to be used without installing the transpiler. See the complete StudySpec reference.

Native runner 0.2.0

The native executable adds optional app-integration controls without changing candidate ordering or scoring for runs that omit them:

Flag Contract
--syminfo FILE Apply the instrument lot grid (mincontract), then series_origin_ms, mintick, pointvalue, timezone, and session, after inputs and overrides, in that order. Accept a flat JSON object or {"syminfo": {...}}; omitted values keep engine defaults.
--progress-fd N Write and flush one JSONL object per terminal trial to an inherited writable descriptor. A single writer emits exactly the objects in the final trials[], in trial-id order. Blocking and non-blocking descriptors are supported; drain pipes while the process runs.
--max-wall-seconds S Positive, finite study wall limit in seconds, including native initialization. Stop taking candidates cooperatively, including inside grid/random batches.
--record-metric PATH Repeatable additional report-metric path. Validate before execution and preserve the path spelling in each trial's metrics; unavailable values are JSON null.
--trial-timeout-seconds T Positive, finite per-trial wall cap. The first timeout records one trial_timeout, publishes the completed/timeout trials, and exits immediately without joining workers.

For example, append these controls to a native run command:

--syminfo symbol.json --progress-fd 3 --max-wall-seconds 600 \
--trial-timeout-seconds 20 --record-metric metrics.all.profit_factor \
--output result.json 3>trials.jsonl

Numeric symbol values must be finite and positive; timezone/session values must be strings without embedded NULs. Empty strings and omitted fields are no-ops; other catalog fields are ignored. Symbol timezone is distinct from chart timezone. The four setters mirror the pinned engine v1.11.0 release harness.

Fixed other-symbol feeds (0.8.0)

Optimize scripts reading other symbols with request.security, keeping those symbols fixed:

pineforge-hpo run work.json --symbol-feeds data/feeds/index.json \
--native ./build/bin/pineforge-hpo-native
# Native: pineforge-hpo-native run ... --symbol-feeds data/feeds/index.json

Alternatively add "symbol_feeds": "data/feeds/index.json" to work.json, or supply an inline index. The CLI option overrides work.json. Index example:

{
"symbols": {
"BINANCE:ETHUSDT": {
"syminfo": {
"tickerid": "BINANCE:ETHUSDT", "type": "crypto", "currency": "USDT",
"mintick": 0.01, "timezone": "UTC", "session": "24x7"
},
"feeds": {"240": "ethusdt-240.csv", "1D": "ethusdt-1D.csv"}
}
}
}

Keys are exact script request strings, including exchange prefixes and suffixes. CSV paths resolve against the index directory (work.json's directory for an inline index). For BTCUSDT 4h pass the chart's own bars with input_tf and script_tf both "240"; do not aggregate lower-timeframe input into the chart. Include each requested feed and sufficient history; there is no network fetching. Initialization validates/hashes once, then every fresh trial installs facts/bars/closes through the public C ABI with no file I/O. Index/CSV/setter refusal is exit 1 with --symbol-feeds:, before any terminal trial output. **input.symbol search dimensions are refused (D7)**; such inputs may be fixed only. This requires engine/codegen **>= 1.1.0** (the optional submodules pin v1.11.0). Direct native artifact users must keep the adjacent codegen manifest.json when searching string-valued inputs. input.source and input.enum dimensions are supported; only a type: "string" input requires the manifest's explicit "input_kind_schema": 1 marker. PineForge HPO's artifact builder writes it only after observing the modern requests list (including an empty list) and a cached, separate one-line input.symbol canary transpile whose input metadata contains kind: "symbol". Individual input kind fields never vouch for other inputs. This supports symbol-free string/timeframe searches and codegen source checkouts whose package version is unknown. Unstamped manifests fail closed with a rebuild instruction; rebuild with pineforge-hpo >= 0.8.0 and codegen >= 1.1.0. Pre-stamp caches rebuild once because their artifact identity lacks the builder's input-metadata revision. The Python frontend validates generated/precompiled metadata automatically. An unreadable optional manifest does not break a numeric-only native study. Duplicate titles unrelated to searched/fixed inputs do not invalidate Python studies; native rejects duplicates only for searched inputs.

External artifact builders: input_kind_schema: 1 is a trusted assertion, not a signature or an inference made by HPO. Before writing it, verify the actual transpiler's symbol-kind canary, preserve every input's original kind (especially input.symbol), and bind the manifest to the exact plugin hash, artifact key and codegen request identity. Write the same marker/identity/hash into adjacent provenance.json; never copy a marker from another artifact or invent missing kinds. A known codegen version below 1.1.0 contradicts the marker and is refused. Python cross-checks adjacent provenance; native cross-checks the cheap marker/key/hash/version fields. Unknown versions still require a genuine capability check. Treat third-party builders as part of your trust boundary.

Results record applied_runtime.symbol_feeds and runtime_sha256. Warm JSON/JSONL/v2 refuse changed/added/removed feeds or facts; relocating identical files is safe. Empty/omitted feeds preserve prior grid/random result content and warm-v2 bytes, except the release-version marker. TPE additionally exposes numeric_build_identity and parent_numeric_build_identity (also in warm_start provenance); the parent is null when no compatible-version checkpoint was compared. restored_sampler_state requires equal, non-null parent/child identities. Since 0.9.0, TPE uses pinned, correctly-rounded CORE-MATH binary64 kernels instead of host transcendental functions, including every log-scale transform. Its identity describes the portable arithmetic contract, not CPU vendor, compiler brand or host libm. v0.8.0 TPE checkpoints rebuild from ordered objective history; their sampler state is never imported or mixed. This is not a bitwise continuation of the v0.8.0 algorithm. Native space-info and warm-encode also accept --symbol-feeds <index.json> to override the work file's feed index. See StudySpec for validation and header details.

Cross-vendor reproducibility

Portable TPE requires x86-64 with FMA3 or aarch64, IEEE-754 binary64, nearest rounding, gradual underflow and disabled implicit FMA contraction/fast math. Other runtime modes are refused before sampling. All eight space types, serial/threaded execution and Intel checkpoint restoration are exercised by the proof table and arithmetic inventory. Identical objective observations, configuration, seed and ask/tell scheduling remain necessary: portable proposal math does not make a strategy's objective vendor-independent. Grid/random behavior is retained; their v0.8.0 compatibility is checked separately.

Runtime refusals surface as CLI exit code 1 with the exception message, including portable TPE requires x86-64 FMA3 and portable TPE requires gradual underflow (FTZ/DAZ off) (aarch64: (FZ off)). An externally built plugin using -ffast-math/-Ofast can enable FTZ/DAZ at load time, causing initialization or the next ask() to fail. HPO-built plugins do not use these flags. Do not treat these environment refusals as a checkpoint rebuild or silently retry unchanged. Identity strings now use portable-tpe-v2;...portable_probe_sha256: instead of the host-libm ...libm_probe_sha256: format; compare them as opaque strings for equality only. Building now requires both C and C++ compilers. PineForgeHPO::core transitively links pineforge_hpo_portable_math; manual link lines must include that library too. INPUT_METADATA_REVISION = 2 causes a one-time artifact-cache rebuild. A trusted input_kind_schema: 1 marker contradicting known codegen versions below 1.1.0, or a precompiled manifest disagreeing with adjacent provenance, is refused before any trials. External builders must satisfy the trusted input-kind contract.

The real compiled-Pine equality gate compares three candidates' complete C-ABI metrics and trades (field bytes unchanged, unspecified ABI padding zeroed) against a release harness supporting --symbol-feeds, using deterministic synthetic BTCUSDT 4h / ETHUSDT 240 + 1D fixtures:

python3 tests/test_symbol_feeds_e2e.py --native build/bin/pineforge-hpo-native \
--probe build/bin/pineforge_hpo_symbol_feed_report \
--harness /path/to/pineforge-engine/docker/run_json.py --output build/feeds-e2e

Runtime/plugin gitlinks remain pinned. Evidence and fixture/artifact hashes stay under build.

mincontract (since 0.3.1) is the instrument's lot-size grid (TradingView syminfo.mincontract). Pass the catalog object as is: a number is applied to the engine as the metadata keys qty_step (the engine floors order quantities to this grid, so a percent-of-equity strategy no longer books sub-lot trades) and mincontract (what syminfo.mincontract reads in the script), before mintick; absent or null means no grid and no call. In the object that is read, any other value (zero, negative, string, boolean, array, object, or a number outside the double range such as 1e999 or 1e-400) is an initialization error with the message syminfo.mincontract must be a positive finite number (exit 1, no trial runs). Bare NaN and Infinity are not JSON: the parser rejects them first (exit 1, invalid JSON at byte N, which does not name the key).

Optional series_origin_ms is the epoch milliseconds of the first bar of the TradingView reference series. It enables range-start warm-up in an engine that has the series_origin_ms run option (an engine without it ignores the key): EMA-based series begin with na for the first length-minus-one bars and then use an SMA seed; request.security starts at the first whole bucket at or after that origin. Supply a finite JSON number that is an integer from 1 through 9007199254740991. It is applied through strategy_set_syminfo_metadata as series_origin_ms, after qty_step and mincontract and before mintick. Absent or null means no call and unchanged runs. Other values fail initialization (exit 1) with syminfo.series_origin_ms must be a positive integer.

Applying the grid needs the plugin to export strategy_set_syminfo_metadata, which the pinned engine v1.11.0 does (declared in pineforge.h; the engine honours the qty_step key). A plugin without it keeps working when neither mincontract nor series_origin_ms is declared; with either, each trial fails with a trial_error that names the key and the run exits 2 because no trial is feasible, instead of running without the grid or the declared origin.

SIGTERM and SIGINT produce cooperative cancelled stops; a wall limit produces deadline. In-flight trials may finish, but unstarted candidates are not reported. Use the per-trial cap to bound a non-returning engine call. All trials now include backtest.magnifier_sample_ticks_total, including zero when no report is available.

The existing statuses ok, constraint_violation, objective_error, constraint_error, engine_error, and trial_error remain unchanged; trial_timeout is added. The stop_reason set is trial_budget_reached, search_space_exhausted, sampler_stopped, no_improvement, cancelled, deadline, and trial_timeout. Exit codes are 0 when a best feasible trial exists, 1 for initialization/I/O errors, 2 when no trial is feasible, and 3 for a trial timeout even if an earlier trial was feasible. Cooperative stops, timeouts, and permanent progress I/O failures write final JSON to stdout and --output when those destinations remain writable; a progress I/O failure then exits 1. Timeout output includes exactly one timed-out trial and excludes other still-running trials. Initialization or result-output failures can prevent final publication.

--no-improvement-trials N stops after N ordered terminal trials without a strictly better feasible, finite objective. Both the native run command and the Python run wrapper accept this unsigned 64-bit integer; absent or 0 disables it. Counting starts only after the first feasible result in the current invocation. Ties, infeasible results, errors, and pruned trials do not reset the count. No-improvement is judged against the best found in THIS part, not the lineage's best ever. Continuation resets both the count and its reference best; warm history still trains the sampler. No patience state is added to sampler_checkpoint_v2.

When patience triggers, stop_reason is no_improvement. The runner finishes and reports the current batch and all already-proposed queued batches, then stops submitting work. Tail improvements remain eligible for the final best and do not retract the stop. With batch size B and lag L, at most (L+1)*B-1 trials follow the trigger, fewer near budget or exhaustion. The default batch size remains the worker count. To get the same stop trial with a different number of workers, pass an explicit batch size. Keep the seed, lag, artifact, data, objective, sampler configuration and numerical build fixed as well. Watchdog/output failures and observed cancellation/deadline take precedence; otherwise patience wins a tie with budget or exhaustion.

Only enabled studies add early_stop: patience_trials, trigger_trial_id (null if untriggered), drained_through_trial_id (the last terminal ID consumed by ordered feedback, null if none), and reference_scope: "part". External interruption can leave a partial drain. Disabled result/progress/trial and checkpoint formats are unchanged. See the batching contract.

The wall-time and per-trial timeout controls are native-only. StudySpec's reserved timeout_seconds remains rejected, and the Python CLI does not relay signals. Launchers can use public pineforge_hpo.prepare_run(study_path, engine_root, cache_dir) to obtain (native_argv, artifact_json) without starting the native process, then append native flags and execute directly. Optional keyword arguments native, compiler, and eigen_include preserve existing CLI overrides. Jailed launchers must keep sealed fd 5 open across exec, pass --output for the trial spool's writable parent directory, and avoid --progress-fd 5.

CMake accepts either an engine source/build tree or an installed release prefix, for example -DPINEFORGE_ENGINE_ROOT=/opt/pineforge. Generated version.h is found in either include/ or build/include/.

Continue a study (0.6.0)

A continuation is a new job seeded with a finished parent's trials. Check coverage without compiling the strategy or opening its market data, then request a new budget:

pineforge-hpo space-info --spec study.json --warm-start parent.result.json
pineforge-hpo run study.json --warm-start parent.result.json > continued.result.json
pineforge-hpo warm-encode --spec study.json --input parent.result.json --output parent.warm
pineforge-hpo run study.json --warm-start parent.warm > continued.result.json

pineforge-hpo run accepts --trials-out all|best-k|none and --best-k N (default 10); omitted flags preserve all-trial retention.

The study's sampler.trials is the additional trial budget. --warm-start accepts concatenated binary v2 blocks, the parent's complete result JSON, its trials array, or the exact terminal-trial JSONL written by --progress-fd/--trials-file. Input is auto-detected. Native runs accept the same flag. Warm trials never appear on the progress descriptor or in the new trials file; trial IDs start after the parent's highest ID. Results record the source file SHA-256, warm counts, and a versioned space_hash independent of HPO versions, compiler identity, and dependency pins.

Grid skips tried combinations in its original order. Random derives a deterministic stream from (seed, warm trial count) and rejects parent points. TPE restores finite feasible observations, startup progress, attempted-point reservations and bounded history. Pruned/partial trials do not train the final-objective model; available rung scores seed the prefix pruner. Grid and finite without_replacement runs preserve exhaustive final-set equivalence. TPE preserves exact proposals only with a matching sampler checkpoint, the same numerical build and future lag-zero ask/tell schedule. Row-only parents always reconstruct deterministically; matching batch boundaries alone are not sufficient.

Exit 4 means incompatible/malformed warm history; exit 5 means a finite space is exhausted. Both are initialization refusals, before strategy/data loading or any billable trial. space-info still succeeds for exhausted spaces. Existing timeout exit 3 is unchanged. See the API contract and canonical identity and replay rules.

Binary v2 stores all earlier trials, sampler fields only, without parsing JSON or retaining candidate maps per warm row. Blocks can be concatenated directly; there is no file footer or total-file trial count to rewrite. IDs are uint64 to preserve the existing ID domain. The app can use the dependency-free pineforge_hpo.warm_binary.write_warm_block reference writer for each ingested chunk. Keep the app's per-object SHA-256 and keep mapped files immutable during a run. JSON warm inputs remain accepted in 0.7.0 (with their existing 256-MiB cap). Binary input currently requires execution.pruner=none; use JSON for rung history. See the byte-level v2 specification and resource benchmark.

TPE continuation in 0.7.0: historical proposals are never replayed. Keep the complete result's tpe_sampler_state checkpoint with all attempted rows to preserve the exact uninterrupted stream; native warm-encode carries it into optional PFHSTATE blocks. Python ingest writers accept sampler_state=. Legacy row-only v0.5/v0.6 parents remain valid but use deterministic reconstruction, not the previous matching-batch TPE replay promise. Grid/random continuation is unchanged. Older binary readers reject the optional state extension; omit it for those readers. Full-history TPE fit/scoring reuses up to eight workers with serial-equivalent arithmetic for the same numerical build. TPE now uses -ffp-contract=off, so fresh streams change versus 0.6.0 on arm64/FMA targets; x86-64 builds without FMA retain their previous stream. sampler.config.max_threads (--tpe-max-threads) controls the resource limit, not model behavior; the default is min(8, available CPUs), respecting affinity and Linux cgroup v1/v2 quotas. history_switch is still opt-in. Warm files are trusted input: the checkpoint checksum detects corruption, not forgery or authenticity.

Define a study

Studies are strict JSON documents. Paths are resolved relative to the StudySpec file, not the shell's current directory. The bundled example is examples/single_strategy/study.json.

The three fields most commonly changed are the strategy search space, objective, and sampler:

{
"search_space": {
"Fast Length": {"kind": "integer", "low": 5, "high": 50, "step": 1},
"Multiplier": {"kind": "real", "low": 0.5, "high": 3.0, "step": 0.1},
"Use Filter": {"kind": "boolean"},
"Mode": {"kind": "categorical", "choices": ["ema", "sma"]}
},
"objective": {
"kind": "expression",
"direction": "maximize",
"expression": "metrics.all.net_profit_pct - 0.5 * metrics.equity.max_equity_drawdown_pct",
"constraints": ["metrics.all.num_trades >= 30"],
"nan_policy": "fail_trial",
"division_by_zero": "fail_trial"
},
"sampler": {
"kind": "tpe",
"candidate_policy": "without_replacement",
"seed": 20260718,
"trials": 1000,
"config": {"startup_trials": 200}
}
}

This is an excerpt rather than a complete StudySpec. Strategy, dataset, and execution objects are shown in the schema documentation.

Choose a sampler

Sampler Best fit Important behavior
grid Small finite spaces and coverage baselines Deterministically enumerates the Cartesian product.
random Broad, inexpensive exploration Seeded std::mt19937_64; supports continuous and log dimensions.
dlib_global Model-based search over mixed numeric spaces Native batched ask/tell through dlib global function search.
tpe Adaptive refinement over mixed spaces Native Parzen marginals, categorical probabilities, and constant-liar batches.
sobol Space-filling exploration with an indexed, reproducible stream 64-bit Gray-code Sobol points (Joe-Kuo direction numbers), optional seeded digital shift; trial ID is the sequence index, sampling is with replacement, and a complete earlier result continues the shifted stream. No quality claim. See Sobol.
candidates Re-scoring a known list of complete vectors Evaluates each line of an ordered JSONL list once, duplicates included; the whole file is validated before any trial. See Candidate list.

TPE and dlib are adaptive: proposal order depends on the seed, logical batch size, and fixed feedback lag, not worker timing. Set execution.batch_size explicitly to replay across worker counts; its default remains the worker count for compatibility. Compare optimizers under the same objective-evaluation budget and across multiple seeds. For large 10^6-10^8 candidate domains, do not ask one TPE study to enumerate the domain. Use an external coordinator to shard a finite grid or broad search, then reserve TPE for bounded refinement. This runner does not currently provide distributed partitioning, and its TPE deliberately retains its complete usable history.

Choose a candidate policy

Policy Guarantee
sampler_default Preserves the sampler's native behavior. Continuous real dimensions are allowed and TPE may repeat a vector.
without_replacement Every complete vector is reserved at most once. The trial budget cannot exceed the finite-space cardinality.
exhaustive Adds trials == cardinality, so every declared vector is attempted exactly once.

Finite policies are implemented for grid and tpe. Every varying real dimension must declare a positive step; low == high is a one-value dimension and needs no step. Pending, completed, infeasible, failed, and abandoned candidates remain reserved, so parallel workers cannot repeat them. A non-constant log-real dimension is continuous and therefore cannot use a finite policy in StudySpec v1.

exhaustive guarantees parameter coverage. Equality with a successful grid-search optimum also requires deterministic data and execution, plus a comparable result at every point. See ADR 0003 for the exact lattice and floating-point contract.

Build an objective

Expression objectives can read metrics.all.*, metrics.longs.*, metrics.shorts.*, metrics.equity.*, and selected report counters such as report.total_trades. Expressions are parsed once before trials begin. Unknown metric paths, non-finite final values, and configured division-by-zero failures cannot silently become a winning trial.

Metric names follow the engine's report structs. Engine 1.0 renamed the monthly Sharpe and Sortino ratios, so they are metrics.equity.sharpe_monthly and metrics.equity.sortino_monthly; the pre-1.0 names metrics.equity.sharpe_tv and metrics.equity.sortino_tv remain accepted aliases, so existing StudySpecs keep working.

Return statistics of the equity curve are available as eighteen metric names, returns.{bar,monthly}.{count,skipped,periods_per_year,mean,std,sharpe_per_period,skew,kurt_raw,status}, computed only for the series an expression or --record-metric names (nothing else changes when none is named; a build whose statistics identity cannot be bound refuses them and still runs everything else). The Sharpe value is per period and unannualized; see Return statistics and docs/return-stats.md for definitions, status codes and what is not claimed.

Applications embedding the C++ library can instead implement ObjectiveFn<Observation>. The generic objective contract is independent of a PineForge report; the executable CLI currently resolves expression objectives only.

Interfaces and API documentation

  • Python CLI: pineforge-hpo compile and pineforge-hpo run initialize artifacts and studies.
  • Native CLI: build/bin/pineforge-hpo-native runs a compiled plugin and normalized OHLCV directly. Use --help for its lower-level flags.
  • Python API: ArtifactBuilder, StudySpec, validation models, and transpiler bridge are exported from pineforge_hpo.
  • C++ API: public headers live under include/pineforge/hpo/. Build-tree consumers can link PineForgeHPO::core or PineForgeHPO::engine_adapter.

For an optimizer-only embedding with no PineForge engine checkout, configure with PINEFORGE_HPO_BUILD_ENGINE_ADAPTER=OFF and PINEFORGE_HPO_BUILD_NATIVE_CLI=OFF. PineForge HPO 0.1.x currently exposes CMake build-tree targets; an installed CMake package is not published yet. Both adapter targets default to off when PineForge HPO is included with add_subdirectory(). Core-only users may leave both external/ submodules uninitialized.

The API documentation workflow publishes the C++ and Python API reference to GitHub Pages; build the same site locally to preview changes. The reference is generated from the public headers and Python facade on the repository's default branch.

Architecture

Initialization and execution are deliberately separated:

  1. pineforge_codegen.transpile_full() emits generated C++, input metadata, and strategy(...) parameters once.
  2. ArtifactBuilder compiles and validates a content-addressed strategy plugin once.
  3. StrategyPlugin and immutable OHLCV data are loaded once for the study.
  4. Each trial creates a fresh strategy handle, applies inputs and overrides, runs the backtest, snapshots the required report fields, then frees report and handle in order.
  5. A persistent thread pool executes logical batches. The coordinator proposes and feeds back results in deterministic trial-ID order, with optional fixed-lag pipelining and deterministic prefix pruning.

The component boundaries, plugin lifecycle, objective abstraction, finite-space codec, and future account-level execution model are documented in `docs/architecture.md`.

Benchmarks

The benchmark suite separates optimizer quality from proposal/feedback API overhead and records raw per-seed results. It includes standard nonlinear functions, a mixed-type interaction problem, and an independently exhaustively verified one-million-candidate discrete problem. The native TPE comparison pins official Optuna in an isolated benchmark-only environment; Optuna is not a runtime dependency.

Start with `benchmarks/README.md` for the benchmark protocol, smoke/full commands, output schema, fairness rules, and guidance for contributing results. The detailed native-versus-Optuna methodology lives in `benchmarks/optuna/README.md`, with the checked-in reference run in `results-2026-07-18.md`.

Published numbers are evidence for the tested problems, budgets, seeds, and machine—not a claim that one sampler dominates every backtest surface.

Reproducibility

For a reproducible study, keep all of the following fixed:

  • Pine source or precompiled artifact and its recorded artifact key;
  • engine ABI/runtime, pineforge-codegen-oss, compiler target, and exact compile flags;
  • OHLCV bytes, timeframes, timezone, fixed inputs, and strategy overrides;
  • objective, constraints, search-space declaration, and candidate policy;
  • sampler implementation/configuration, seed, batch size/lag, pruning policy, and trial budget.

With the default batch size, the worker count still selects the logical batch size. With an explicit batch size, changing only workers leaves trials[] byte-identical for completed studies on the same deterministic artifact, data, runtime, and native build. Wall-deadline, cancellation, and timeout truncation are intentionally outside this guarantee. See batching and pruning measurements for the replay proof, metering contract, quality tradeoffs, and opt-in flags. By default --tpe-history-switch is unset: TPE never switches and uses the exact 0.3.0 full-history estimator throughout the study. --tpe-history-switch N explicitly opts into 25 elites, 64 recent non-elites, and 448 seeded older non-elites at N completed usable observations. The switch never depends on elapsed time, worker count, pending or abandoned proposals. StudySpec forwards sampler.config.history_switch; results and TPE terminal records persist tpe_history_switch, with null meaning never switch. Full-history mode retains all observations; its memory and ask cost grow with study length.

For very long studies (10,000–1,000,000 trials), --tpe-history-switch 1000 is a reasonable opt-in when flat acquisition cost matters more than refinement quality. Larger thresholds retain legacy behavior longer. Historical 16D snapshot probes measured 27,652 microseconds at 1,024 observations and 112,201 at 4,096. Identity below the switch is a replay guarantee, not evidence of long-budget quality parity.

**Measured bounded-mode tradeoff (--tpe-history-switch 1000).** At 3,000 trials (eight problems, ten seeds), its median-regret-ratio geomean is 1.036492 versus 0.3.0: about 3.65% worse, with rotated ellipsoid 20D at 1.245558. At 10,000 (four problems, five seeds), the geomean is 1.031570 versus the full-history benchmark variant, with worst ratio 1.091497: about 3.16% worse. These costs are accepted for explicit opt-in, not as default-quality parity. The earlier "about 1.6% worse median regret at 3k than 0.3.0 in exchange for flat ask cost" result describes the superseded issued-count transition, not the shipped opt-in. The reservoir, EI draws, and test thresholds are not tuned to recover that number. The flat 64D ask probe measures 656.450/663.061 microseconds at 100k/1M history, above the 375-microsecond W=8 budget; opted-in 64D sampling remains a known bottleneck. See the final switch evidence and all gates. The default 3k identity gate compares 72,000 proposal/value records across eight problems and three seeds, with zero diffs.

Generated strategies are compiled with the parity-critical -std=c++17 -O2 -ffp-contract=off -fPIC -shared flags; with Clang, the builder also passes -fbracket-depth=1024, which engine 1.0 requires for deeply nested generated C++. Result JSON includes the HPO version, sampler implementation identity, artifact key, trials selected by --trials-out (also accepted by pineforge-hpo run alongside --best-k N), cardinality, and coverage diagnostics. Preserve result and artifact provenance together when reporting a benchmark or bug.

Current scope

Implemented today:

  • one Pine strategy and one OHLCV dataset per executable study;
  • integer, real, Boolean, categorical, stepped, and supported log dimensions;
  • grid, seeded random, dlib global, and native TPE samplers;
  • parallel thread execution over independent strategy handles;
  • worker-independent logical batches, optional fixed-lag pipelining, and prefix pruning;
  • expression objectives, comparison constraints, runtime inputs, and runtime strategy overrides;
  • content-addressed artifact compilation and provenance.

Not implemented yet:

  • multiple-strategy shared cash/margin/order sequencing and portfolio CLI execution;
  • durable study storage, resume/checkpointing, or distributed workers;
  • conditional/hierarchical spaces, multi-objective Pareto optimization, and walk-forward orchestration;
  • executable resolution of registered custom C++ objectives.

Independent strategy reports must not be described as a shared-account simulation. See `docs/plan.md` for the staged roadmap.

Development

See CONTRIBUTING.md for the complete native/Python checks, architecture boundaries, benchmark contribution rules, and pull-request checklist. Repository settings that cannot be committed as files are listed in the GitHub setup checklist.

Run the required local checks from the repository root:

cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DPINEFORGE_ENGINE_ROOT="$PWD/external/pineforge-engine"
cmake --build build -j4
ctest --test-dir build --output-on-failure

CMake 3.21+ also supports the release, asan (ASan/UBSan), and tsan (ThreadSanitizer) presets. Use separate sanitizer builds:

cmake --preset asan && cmake --build --preset asan -j4
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1:quarantine_size_mb=16 \
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 ctest --preset asan
cmake --preset tsan && cmake --build --preset tsan -j4 && ctest --preset tsan

The 16 MiB ASan quarantine bounds instrumentation's retained freed allocations so the tpe_scale test's 64 MiB RSS-growth assertion measures sampler retention rather than ASan's default quarantine. This is test sensitivity, not a sampler-memory regression; address/undefined-behavior checks and leak detection remain enabled.

To include the installed-prefix gate, configure with -DPINEFORGE_HPO_TEST_ENGINE_BUILD="$PWD/external/pineforge-engine/build" after building the pinned engine. The test performs cmake --install into a temporary prefix and configures/builds HPO against that prefix.

Please report bugs and propose features in the GitHub issue tracker. Include a minimal StudySpec, the complete result/provenance JSON, platform and compiler information, and whether the behavior reproduces with one worker. Report suspected vulnerabilities privately according to SECURITY.md.

Additional design and validation material:

License

Starting with v0.11.0, original code in this repository is source-available under the [PineForge Source License 1.2](LICENSE); the LICENSE file is the controlling text. The licensor is PINEFORGE LLC. Releases up to and including v0.10.0 were released under Apache-2.0 and remain available under that license.

  • Free for noncommercial use: any noncommercial purpose, and use by a charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization or government institution for its teaching, research and other operations.
  • Free for Personal Trading: a natural person may research, develop or backtest strategies and execute trades for their own account with their own capital. Their own account includes joint and household accounts (spouse or domestic partner, dependants), retirement and other tax-advantaged accounts and a revocable trust for their benefit; their own capital includes margin and other ordinary borrowing from a broker or lender. A company's or fund's account is not a personal account, even if the person wholly owns the company. An account that a proprietary-trading firm or funded-trader program provides or allocates to them, including a challenge, evaluation or simulated account, is not their own account, and its capital, real or simulated, is not their own capital, so trading it is investment management, not Personal Trading.
  • Investment management is never free, except Personal Trading: managing, advising on or trading investment capital, or researching strategies for it, whether the capital is a friend's, clients' or investors', an endowment, a pension fund, a public fund, a foundation's treasury or an account a proprietary-trading firm or funded-trader program provides or allocates (a challenge, evaluation or simulated account included), is Commercial Use for every individual and every organization, noncommercial organizations included.
  • Commercial Use needs a commercial license: besides investment management, any other use that is not free, such as use by, for or on behalf of a company, fund, partnership or other organization (including an individual's work for one); embedding the software or its output in, or distributing either bundled with, a product or service made available to others; or operating a hosted, software-as-a-service or other public-facing service through which others run the software or receive its output.

This is source-available, not OSI open source. Obtain a commercial license at https://license.pineforge.dev. One commercial license covers both codegen and HPO.

dlib 20.0.1 is used under the Boost Software License 1.0; CORE-MATH is used under the MIT license. Third-party notices are recorded in [NOTICE](NOTICE), LEGAL.md and THIRD_PARTY_LICENSES.

The optional PineScript bridge invokes the pineforge-codegen distribution from the pineforge-codegen-oss repository. That separately distributed project has its own source-available license and commercial-use terms; it is not relicensed by this repository. Running pineforge-hpo-native with an already compiled strategy plugin does not require the transpiler. See LEGAL.md for the dependency boundary.