|
PineForge HPO 0.1.0
Native hyperparameter optimization for PineForge strategies
|
The hardened single-strategy native path is implemented. PineScript initialization is Python; sampling, plugin execution, objective evaluation, and trial scheduling are C++. The executable path currently accepts one strategy and one dataset.
Portfolio observation and custom-objective types exist as C++ contracts, but no account aggregator or portfolio CLI has been implemented.
pineforge-hpo composes two separately distributed products:
pineforge-codegen-oss provides the pineforge-codegen Python distribution and its pineforge_codegen module. transpile_full() emits generated C++, an input manifest, and literal strategy(...) parameters.pineforge-engine executes a compiled strategy through its public C ABI.The HPO repository does not subclass or link against BacktestEngine internals. The native adapter loads a generated strategy plugin with local symbol scope and resolves only its C ABI.
The Python pineforge-hpo run wrapper resolves a StudySpec and translates it into a pineforge-hpo-native run invocation. Python is not in the trial hot loop.
python/pineforge_hpo/transpile.py imports pineforge_codegen.transpile_full and normalizes its response:
Pine lexer/parser/analyzer errors remain distinct from native compiler errors. The codegen identity records both the installed distribution version and a hash of its Python implementation, so an editable or dirty codegen checkout cannot silently reuse a stale strategy artifact.
ArtifactBuilder supports Linux and macOS and writes:
The cache has two identities:
generated_cpp_sha256. This preserves the generated translation unit in the content identity without forcing a repeat transpilation merely to discover the key.Per-key advisory locks serialize the first build. Artifact directories and request indexes are published atomically. Cache hits re-hash the plugin and generated C++ and verify their manifest/provenance identities.
The compile contract is:
The engine static library is linked with GNU whole-archive on Linux or force_load on macOS. Before publication, the builder loads the plugin, requires pf_abi_version, strategy_create, strategy_free, strategy_set_input, strategy_set_override, strategy_get_last_error, run_backtest_full, and report_free, then checks that pf_abi_version() matches the selected engine ABI.
The native search-space model implements:
step = 1);Candidates are type- and range-checked before serialization to the strategy ABI. Fixed inputs are rejected if they overlap a search dimension.
The optional candidate-policy layer is separate from sampler scoring and ordering:
sampler_default delegates reuse and exhaustion semantics to the sampler and preserves continuous real dimensions;without_replacement requires a finite domain and atomically reserves every complete parameter vector at most once;exhaustive adds the requirement that the requested trial count exactly equals the finite domain cardinality.Only TPE and grid accept the finite policies. Random and dlib reject them rather than silently changing optimizer. This keeps existing continuous TPE behavior backward compatible while making finite coverage an explicit contract.
Finite integer and varying-real dimensions are indexed by non-negative lattice ordinal k, with legal values low + k * step <= high. A varying real must declare a positive step; a fixed real where low == high contributes one value without a step. A non-constant log real remains continuous in StudySpec v1 and is not eligible for a finite policy, while a fixed log real is eligible. Boolean and categorical cardinalities are two and the declared choice count. Checked Cartesian multiplication resolves the full domain cardinality before execution. The stepped-real codec uses fused multiply-add decoding, permits a one-ULP snap only at the declared upper endpoint, caps real lattices at 2^53 ordinals, and rejects any binary64 or strategy-ABI collision before execution.
Four native samplers are implemented:
GridSampler exhausts the Cartesian product in stable dimension/value order;RandomSampler uses std::mt19937_64, an explicit seed, rejection sampling for bounded integers, and a stable 53-bit conversion for real values;DlibGlobalSampler wraps dlib global_function_search with move-only outstanding requests and explicit ask(), tell(), and abandon() calls;TpeSampler fits independent per-dimension Parzen marginals and exposes the same explicit adaptive request lifecycle.Grid and random candidates may be generated before execution. dlib and TPE candidates must be interleaved with feedback. The scheduler proposes a fixed worker-sized batch, runs its backtests concurrently, and reports results in trial-id order. This keeps a fixed seed and worker count repeatable without letting thread completion timing alter the model.
For a finite candidate policy, the coordinator reserves a full vector in the seen set before returning it from ask(). The reservation is therefore visible to every later request in the same batch. Pending, completed, infeasible, and failed vectors remain seen. This uniqueness state is independent of TPE's constant-liar model: constant liar changes acquisition density, while the seen set provides the exact no-repeat guarantee.
Stepped/discrete dlib dimensions are integer indices; linear continuous real dimensions retain their bounds. Log integer and real dimensions use continuous ln(value) coordinates. Log integer coordinate bounds expand by half a step before transformation, and values decode by nearest-integer exp(z). Constant dimensions are omitted and injected on decode. dlib always maximizes, so minimize objectives are negated internally. The dlib dependency is pinned to 20.0.1 by default and remains behind the sampler PIMPL.
TPE separates completed observations into good and bad groups using a bounded gamma fraction. Numeric marginals use bounded Gaussian mixtures with quantized bin-mass likelihoods for discrete values; categorical and boolean marginals use prior-smoothed probabilities. Candidate ranking maximizes the summed per-field log likelihood ratio. Constant-liar batching adds outstanding parameters only to the bad estimator. Failed and infeasible requests are abandoned and do not train either model. Under a finite policy, abandonment does not release the parameter vector from the seen set.
TPE finite sampling retains adaptive proposal order while selecting only unseen vectors. without_replacement validates trials <= cardinality; exhaustive validates equality. Grid covers the same declared set in its stable order. The two orders need not have the same intermediate best-so-far sequence.
The current TPE model is rebuilt from all usable observations. Its proposal cost and memory therefore grow with study history; it is a refinement sampler, not the engine for a 10^6-10^8 candidate sweep. The implementation does not silently cap or discard history.
Log dimensions share one transform contract across adaptive samplers. TPE encodes observations as z = ln(value) and fits numeric kernels in normalized log space. A real proposal decodes with exp(z). A log integer proposal rounds exp(z) and its likelihood is the Gaussian mass over that integer's transformed rounding bin. Its latent bounds are ln(low - 0.5) and ln(high + 0.5), while observation means remain ln(k). The l/g likelihood ratio can be evaluated in log coordinates because the real-valued Jacobian is common to both models and cancels. Priors are uniform in the transformed coordinate, not in the original linear units.
The implementation computes logarithmic coordinates relative to the lower boundary with log1p and reconstructs them with expm1. This is mathematically equivalent to subtracting absolute logarithms but avoids catastrophic cancellation in narrow, high-magnitude ranges. Log-integer construction also checks representative lower, middle, and upper quantization bins and rejects a domain if normalized double precision cannot keep every bin distinct.
One StrategyPlugin loads the dynamic library and resolves its function table. One immutable Dataset loads the OHLCV CSV. Each trial independently performs:
The plugin and dataset may be shared by worker threads, but strategy handles and reports are never shared. Cleanup order is structural: report-owned buffers are freed before the handle.
ReportSnapshot retains report counters and all/long/short/equity metrics independently of the plugin handle lifetime. It optionally copies the equity curve for portfolio/custom-objective consumers; the scalar expression CLI disables that copy.
MetricExpression compiles an expression once. It supports:
Comparisons yield 1.0 or 0.0. The executable CLI evaluates constraints as metric expressions and treats a finite non-zero result as satisfied. Metric identifiers resolve before any backtest. Constraints always reject division by zero and non-finite operands. Objective policies are explicit, and a non-finite final score is never ranked or treated as feasible.
The generic library boundary remains independent of PineForge reports:
Applications can therefore define a custom C++ objective over any observation. The executable StudySpec path currently resolves expression objectives only.
The intended user path accepts Pine source and StudySpec JSON:
compile owns transpilation, compilation, caching, and artifact provenance. run resolves paths relative to the StudySpec, reuses the artifact when valid, and launches the native executable.
pineforge-hpo-native consumes a compiled strategy plugin, one OHLCV CSV, search dimensions, fixed inputs, strategy overrides, objective/constraint expressions, sampler settings, and worker count. It produces a JSON study summary with every trial and the best feasible trial.
The native CLI intentionally knows nothing about Pine parsing or codegen. Its result JSON includes schema_version, pineforge_hpo_version, and a stable sampler_implementation identifier in addition to the sampler configuration, seed, complete trial table, and artifact key. Proposal-sequence comparisons must match all of those fields plus worker count and search-space definition. The policy layer also persists candidate_policy, candidate_policy_implementation, search_space_finite, search_space_cardinality, trials_requested, trials_completed, unique_candidates_attempted, duplicate_proposals_skipped, remaining_candidates, search_space_exhausted, stop_reason, full_parameter_coverage, and exhaustive_equivalent.
full_parameter_coverage is a parameter-set statement and may remain true when a terminal trial failed. exhaustive_equivalent additionally requires every trial status to be ok or constraint_violation. It is false after any engine, objective, constraint-evaluation, serialization, or other trial error. The flag still relies on the study's artifact, data, runtime, objective, and constraints being deterministic; it cannot detect external nondeterminism.
The executable MVP supports:
Independent trials share the artifact and immutable bars and use fresh handles. Runtime input.*() and strategy(...) values are applied without rebuilding the plugin.
The C++ API defines PortfolioObservation, AccountEquityPoint, Allocation, SleeveSummary, and PortfolioObjectiveFn. A portfolio objective may request only account return, drawdown, allocations, turnover, concentration, or other application-defined features; it does not need every constituent report field.
The following are not implemented:
Completed independent reports also cannot reconstruct a true shared account. Shared cash, margin, order admission, and cross-strategy order sequencing need a future strategy signal/order-intent ABI and a shared broker ledger.
Initialization failures stop before any trial:
Trial-local failures remain visible in the trial table:
Finite-policy coverage treats every terminal trial vector as seen, including a constraint violation or trial-local failure. Consequently, full parameter coverage means every declared vector received a terminal trial record. It does not mean every vector produced a usable objective. Equality with the best value from a successful deterministic grid search additionally requires exhaustive_equivalent=true under identical artifact, dataset, runtime, objective, and constraint inputs.
Original pineforge-hpo code is Apache-2.0. pineforge-engine is a separate Apache-2.0 runtime dependency. pineforge-codegen, from pineforge-codegen-oss, is a separate source-available dependency with its own terms. The direct-PineScript convenience path does not relicense codegen under Apache-2.0. Precompiled plugin users can run the native HPO layer without installing codegen.
dlib 20.0.1 is the native global-search dependency and is distributed under the Boost Software License 1.0. CMake verifies the pinned source archive hash unless the build explicitly selects an exact-version system package.