Thanks for your interest. This document covers the practical workflow for contributing changes to the runtime.
Community interaction is expected to follow the Code of Conduct. For licensing and third-party obligations (Eigen, optional benchmark tools), see LEGAL.md.
The corpus/ validation tree and benchmarks/assets are published as
separate Apache-2.0 submodules. Initialize them after cloning:
git submodule update --init corpus benchmarks/assetsBoth submodules are redistributable: tv_trades.csv files are produced
from PineScript sources we own, and the corpus ships under the same
Apache-2.0 license as the engine. The corpus also ships the per-probe
generated.cpp (transpiler output of our own clean-room
strategy.pine) so public users can rebuild without access to the
separate pineforge-codegen transpiler (source-available, PolyForm Noncommercial). The compiled
strategy.dylib / strategy.so / strategy.dll artefacts are
platform-specific and rebuilt locally by scripts/run_corpus.sh.
git clone https://github.com/pineforge-4pass/pineforge-engine.git pineforge-engine
cd pineforge-engine
git submodule update --init corpus benchmarks/assets
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j
ctest --test-dir build --output-on-failureYou'll need:
- CMake ≥ 3.16
- A C++17 compiler (GCC ≥ 9, Clang ≥ 10, Apple Clang ≥ 12)
- Eigen 3.3+ (will be fetched automatically if not on system)
The full test suite — 30 binaries, 29 C++ + 1 pure-C ABI sanity test — completes in under a second. There is no slow-test tier; if your change makes ctest take more than ~10s, that's a regression.
Pure implementation changes that touch internal C++ are easy. Examples:
- Bug fixes in
engine_*.cppthat improve TV parity - New TA classes (add to the appropriate
ta_*.cpppartition + its declaration in<pineforge/ta.hpp>) - Refactors of internal C++ that don't touch the C ABI surface
- Added unit tests in
tests/
Changes to <pineforge/pineforge.h> are hard. See "ABI stability" below.
<pineforge/pineforge.h> is the canonical consumer surface and follows append-only semver:
- PATCH (
0.1.X): no ABI changes. Bug fixes, internal refactors, perf, new tests, doc fixes. - MINOR (
0.X.0): append-only additions. New functions added at the end of the header; new fields appended at the end of existing struct definitions; new enum values at the end of an enum.- You may not add a new field in the middle of
pf_report_t. That's a layout change. - You may add
pf_some_new_function()at the end of the header. Old binaries that don't reference it keep working.
- You may not add a new field in the middle of
- MAJOR (
X.0.0): breaking changes. Reordering struct fields, removing functions, renaming things. Requires explicit maintainer signoff.
The compile-time static_asserts in src/c_abi.cpp enforce layout pinning between pf_*_t POD types and their internal C++ mirrors. If those static_asserts fail in your branch, do not "fix" them by changing the asserts. Find what changed in the C++ representation and revert.
- C++17. No
std::filesystem, no<format>. Yes to structured bindings,if constexpr, andstd::optional. - 4-space indent (no tabs). 100-column soft limit. The repo's
.clang-formatis canonical. - Names:
lower_snake_casefor functions and members,PascalCasefor types,kPascalfor constants where they exist. - Comments explain why, not what. The runtime has hundreds of subtle TV-parity quirks; if you write code that handles one, leave a one-paragraph comment on what TV does, and link the offending probe / test fixture from
tests/.
The runtime targets functions ≤ 80 lines (one screen). Most are well under. Three are intentionally larger because splitting fragments a single concept:
BacktestEngine::classify_order_eligibilityBacktestEngine::evaluate_fill_priceBacktestEngine::sort_orders_by_fill_phase
If you add a new function over 80 lines, expect the reviewer to ask "can this be split?". If the answer is "no, it's one cohesive thing", say so in the PR description. Don't fragment cohesive code just to hit a number.
The runtime is split by concern, not by class. When adding code:
- New TA indicator class? Pick the correct
ta_*.cpppartition by category (averages / oscillators / volatility-trend / extremes-volume / misc) and add it there. - New
BacktestEnginemethod? It probably belongs in one of theengine_*.cpppartitions. Add the declaration to<pineforge/engine.hpp>'s appropriate section (private/protected/public) and the definition to whichever partition matches the concern. - New file-local helper that's only used in one
.cpp? Put it in an anonymous namespace inside that file. Do not declare it inengine_internal.hppunless it's genuinely cross-TU. - New cross-TU internal helper? Declare it in
engine_internal.hppunderpineforge::internal.
Every PR must keep ctest green:
ctest --test-dir build --output-on-failureFor meaningful changes, add a test. The easiest pattern: copy tests/test_kc.cpp and rename — it shows the standard test harness. New tests should be added to the TEST_SOURCES list in tests/CMakeLists.txt.
For TA-class changes, the test pattern is:
- Construct the indicator
- Feed a known input series
- Compare against a hand-computed expected output (use Python or a spreadsheet)
printfand assert
For engine changes, look at tests/test_integration.cpp and tests/test_request_security.cpp for examples of multi-bar simulation tests.
The runtime ships an opt-in coverage harness (Apple Clang's llvm-cov or
GCC's gcov/gcovr). It is not wired into the default cmake -B build
flow — coverage instrumentation forces -fprofile-instr-generate /
--coverage, which slows the build and produces .profraw / .gcda
side-files that don't belong in regular development trees.
Run the full coverage pipeline with:
bash scripts/coverage.shThat script:
- Configures a separate
build-cov/tree with-DPINEFORGE_ENABLE_COVERAGE=ON. - Builds the runtime + every test binary with the right instrumentation flags.
- Runs every test (
LLVM_PROFILE_FILEredirected tobuild-cov/coverage/raw/). - Merges the per-test
.profrawfiles into one.profdata. - Emits a per-file totals table and per-file annotated source listings.
Outputs live under build-cov/coverage/:
totals.txt— line / region / function / branch percentages per source file.uncovered.txt— same rows sorted ascending by line coverage (lowest first).per-file/<source>.txt— annotated listings showing which lines/branches are unreached.html/index.html— only whenFORMAT=html bash scripts/coverage.shis used (requireslcov/gcovr).
Honoured env vars: BUILD_DIR (default build-cov), COMPILER
(clang | gcc, auto-detected), JOBS, SKIP_BUILD=1 and SKIP_TESTS=1
to reuse cached artefacts, FORMAT=html to also emit a clickable report.
The current totals on a fresh clone hover around 81% line coverage; new
PRs that touch a file with <80% coverage should ideally raise (or at least
not lower) that file's line coverage. The uncovered.txt report makes it
easy to find candidates.
The parity corpus lives in the corpus git submodule (see
top of this file). After git submodule update --init corpus, read
corpus/README.md for layout and threshold profiles. The full sweep is:
bash scripts/run_corpus.shIt builds every corpus/validation/<probe>/generated.cpp into a
strategy.dylib / strategy.so, runs each against
corpus/data/ohlcv_ETH-USDT-USDT_15m_warmup6m.csv when present
(falling back to corpus/data/ohlcv_ETH-USDT-USDT_15m.csv), and
rewrites the regenerated engine_trades.csv files. It also prints a
canonical scripts/verify_corpus.py --all --quiet summary with the five
parity labels (excellent, strong, moderate, weak, minimal).
Review the regenerated CSV diff before committing runtime-semantics changes.
Probe directories follow the convention
<category>-<descriptive-slug>-NN (e.g. bracket-exit-tp-sl-fixed-01,
analyzer-parity-stop-limit-timing-01). The leading <category> token
groups probes by the engine sub-system they exercise — bracket,
analyzer-parity, barstate, anomaly, and so on. See
corpus/README.md for the full category list and
when to pick which one when adding a new probe.
If your change has a non-trivial chance of affecting TV parity (anything in engine_orders.cpp, engine_fills.cpp, engine_path_resolve.cpp, engine_strategy_commands.cpp, the ta classes, the magnifier, or session/timeframe handling), run the corpus sweep locally and include the diff in the PR description.
For a multi-engine cross-check, benchmarks/ ships
the same 50 strategies through PineForge, PyneCore,
and PineTS — useful for spotting
whether a parity drift is engine-specific or a TV-side semantic both
engines see. bash benchmarks/run_all.sh runs the whole pipeline.
- Open against
main. - Keep PRs small. One concept per PR; multiple commits is fine.
- Title format:
area: short imperative summary(e.g.engine_fills: handle dual-stop tie-break). - Body: explain the why. If it's a TV-parity fix, link the failing probe / cite the TV behavior.
- All CI must pass before merge: build on Ubuntu + macOS in both Release and Debug, ctest, and the install/
find_packagesmoke test.
Before cutting a release on the public default branch:
- Submodule pins — bump
corpus/andbenchmarks/assetssubmodules to the intended commits and verify both upstream tags resolve under their published Apache-2.0 trees. LEGAL.md describes the submodule split. - Secrets — No API keys,
.env, or machine-specific paths in tracked files; keepbenchmarks/_workdir,.venv, andnode_modulesuntracked. - Notices — Keep NOTICE aligned with anything linked into
libpineforge(e.g. Eigen). Update LEGAL.md if you add a new mandatory runtime dependency. - Benchmark AGPL — Optional
benchmarks/tooling installs AGPL-covered PineTS (pinets). Default CI stays on ctest only so a minimal clone is not forced to pull AGPL into the lib build.
By contributing, you agree your contributions will be licensed under the Apache License 2.0 (the same license as the rest of the project). See LICENSE.