The full PineScript v6 → deterministic backtest product image: the
pineforge-engine pure
runtime plus the bundled pineforge-codegen
transpiler. Run a .pine file in, get trades out — no hosted API, source never
leaves the box.
docker pull ghcr.io/pineforge-4pass/pineforge-release:latest
docker run --rm \
-v "$PWD/strategy.pine:/in/strategy.pine:ro" \
-v "$PWD/ohlcv.csv:/in/ohlcv.csv:ro" \
ghcr.io/pineforge-4pass/pineforge-release > report.json
strategy.pine— a PineScript v6 strategy (//@version=6; any other version is rejected with exit code 5).ohlcv.csv— a header rowtimestamp,open,high,low,close,volume, then one bar per row;timestampis UNIX milliseconds (UTC).- stdout is the JSON report (
summary,trades,metrics,equity_curve,fingerprint, ...); progress lines go to stderr, so the redirect above stays clean.
A minimal strategy to try it with (any hourly or daily OHLCV file works):
//@version=6
strategy("EMA cross", overlay=true)
fast = ta.ema(close, 9)
slow = ta.ema(close, 21)
if ta.crossover(fast, slow)
strategy.entry("L", strategy.long)
if ta.crossunder(fast, slow)
strategy.close("L")
jq '.summary' report.json # total_trades, wins, losses, net_pnl, max_drawdown, ...
latest is the newest stable release; pin one with :X.Y.Z (see
Image tags). The image is multi-arch (linux/amd64, linux/arm64).
The entrypoint (docker/entrypoint.sh) is configured by mounts and -e variables:
| Input | Effect |
|---|---|
/in/strategy.pine |
PineScript v6 source, transpiled in the container (preferred). |
/in/strategy.cpp |
A pre-transpiled translation unit, used when there is no .pine. Provide exactly one of the two. |
-e PINEFORGE_TRANSPILE_ONLY=1 |
Transpile /in/strategy.pine and print the C++ on stdout; no compile, no backtest, no OHLCV needed. |
-e PINEFORGE_INPUTS='{"Fast Length": "8"}' |
input.*() name → value overrides. |
-e PINEFORGE_OVERRIDES='{"default_qty_value": "5"}' |
strategy() header overrides (initial_capital, commission_value, pyramiding, ...). |
-e PINEFORGE_INPUT_TF, PINEFORGE_SCRIPT_TF |
Chart and strategy timeframe (1, 5, 15, 60, D, ...). Unset input timeframe = detected from the bar timestamps; the script timeframe must be coarser than or equal to it. |
-e PINEFORGE_BAR_MAGNIFIER, PINEFORGE_MAGNIFIER_SAMPLES, PINEFORGE_MAGNIFIER_DIST |
Bar magnifier switch (true/false, default false), sub-bar sample count (≥ 2, default 4) and distribution (uniform, cosine, triangle, endpoints (default), front_loaded, back_loaded). |
-e PINEFORGE_TRADE_START_MS, PINEFORGE_CHART_TZ, PINEFORGE_MAGNIFIER_VOLUME_WEIGHTED, PINEFORGE_SYMINFO, PINEFORGE_BENCH (+ _WARMUP, _REPEATS) |
Optional run knobs; the comments in docker/entrypoint.sh say what each does. |
-e PINEFORGE_SYMBOL_FEEDS=/in/symbols/symbols.json |
Other symbols' bars for request.security on another symbol, from 1.1.0; see Other symbols' bars. |
| Exit code | Meaning |
|---|---|
0 |
Success (JSON report on stdout, or C++ in transpile-only mode). |
2 |
A required input mount is missing. |
3 |
The generated C++ failed to compile. |
4 |
The backtest failed; a structured {"engine":"pineforge","error":"..."} is printed on stdout (for example when PINEFORGE_SCRIPT_TF is finer than PINEFORGE_INPUT_TF). |
5 |
The transpile failed (unsupported construct, syntax error or a script that is not PineScript v6). |
To see what a given image contains, read its labels (or the PINEFORGE_*
variables listed under Why this repo exists):
docker inspect --format '{{ index .Config.Labels "io.pineforge.engine.version" }} {{ index .Config.Labels "io.pineforge.codegen.version" }}' \
ghcr.io/pineforge-4pass/pineforge-release:latest # e.g. "1.0.0 1.0.0"
A script that calls request.security on another symbol ("BINANCE:ETHUSDT",
or an input.symbol) reads that symbol's own bars, never the chart's. Without
them, or when the index lacks the requested symbol string or timeframe, the run
stops where the request's value is read: exit 4, with
{"engine":"pineforge","error":"request.security(...) at line N: no data is pinned for this request, and its value was read"}
on stdout. From 1.1.0 the image installs those bars from the JSON index that
PINEFORGE_SYMBOL_FEEDS names (the harness's --symbol-feeds); the published
images up to 1.0.1 ignore the variable.
{"symbols": {
"BINANCE:ETHUSDT": {
"syminfo": {"tickerid": "BINANCE:ETHUSDT", "type": "crypto", "currency": "USDT",
"mintick": 0.01, "session": "24x7", "timezone": "UTC"},
"feeds": {"240": "ethusdt-240.csv", "1D": "ethusdt-1D.csv"}}}}docker run --rm \
-v $(pwd)/strategy.pine:/in/strategy.pine:ro \
-v $(pwd)/btcusdt-240.csv:/in/ohlcv.csv:ro \
-v $(pwd)/symbols:/in/symbols:ro \
-e PINEFORGE_SYMBOL_FEEDS=/in/symbols/symbols.json \
ghcr.io/pineforge-4pass/pineforge-release:1.3.0 > report.json- A key is the exact string the script passes at run time, exchange prefix and
suffix included:
BINANCE:ETHUSDT,ETHUSDTandBINANCE:ETHUSDT.Pare three symbols. For aninput.symbolit is the input's value (its default, or thePINEFORGE_INPUTSoverride). feedsholds one CSV per timeframe the script requests, in the format of/in/ohlcv.csv, with paths relative to the index. Timeframes use the engine's spelling: whole minutes as a bare integer ("240", not"4h"), else<n>D|W|M|S, a bareD/W/M/Smeaning1D/1W/1M/1S. Nothing is aggregated: a240feed does not serve aDrequest, and a1feed serves only a1request.syminfois the symbol's catalog object: itstype,timezone,session,currencyandmintickare whatsyminfo.*reads inside the request, wheresyminfo.tickeridis the key itself.- The chart must be its own input (
PINEFORGE_SCRIPT_TFunset or equal to the input timeframe). At most 256 symbols and 256 feeds. - An index or feed the harness cannot install fails the run before it starts:
exit
4, one{"engine":"pineforge","error":"--symbol-feeds: ..."}line. - What was installed is recorded as
applied_runtime.symbol_feeds, so the fingerprint digest differs from a run without it. Unset, the report is what it was without the variable.
The engine's docker/README.md
has the full contract: bar close times (time_close for a session-bound
symbol), warm-up history, the merge rule and its refusals.
The engine (the C++ runtime) and the transpiler (pineforge-codegen) are
separate products. This repo is the single place they are composed. On the 0.x
line they have independent version lineages: it pins each one independently
and owns its own semver. From 1.0.0 on they share one version: the only
supported pair is engine vX.Y.Z with codegen X.Y.Z, prerelease included, and
the release version is that pair version (see Pairing).
Downstream consumers — the MCP servers, the app and end users — depend on this
combined image, never on the bare engine.
No upstream version is pinned in source. CI resolves them at release time and
records the pair in the release tag message (engine= / codegen=, read
back by publish.yml) and the image labels below; docker/Dockerfile takes them
as build-args with no defaults.
| Version | Where | Meaning |
|---|---|---|
ENGINE_VERSION |
resolved by CI; recorded in the release tag message + io.pineforge.engine.version label |
pineforge-engine release whose static-lib tarball (libpineforge.a + headers) is fetched |
CODEGEN_VERSION |
resolved by CI; recorded in the release tag message + io.pineforge.codegen.version label |
pineforge-codegen PyPI version baked in |
VERSION |
repo root | 0.x: this image's own semver; from 1.0.0 on: the pair version |
The combined image also carries io.pineforge.engine.version /
io.pineforge.codegen.version labels and the
PINEFORGE_ENGINE_VERSION / PINEFORGE_CODEGEN_VERSION / PINEFORGE_RELEASE_VERSION
env vars so consumers can read exactly what is inside (PINEFORGE_RELEASE_VERSION
is the release tag, with its v: v1.0.0).
{engine, codegen, HPO} releases → existing producer events → handle-upstream.yml
→ lockstep VERSION + tag (App) → publish.yml: build and verify candidate
→ release-finalize.yml: packages + measured release facts + immutable manifest
→ pineforge-release → consumer PR → own CI + E2E → merge/deploy → served receipt
→ release-receipts.yml → neutral aggregate status and owner issue
handle-upstream.yml— receivesrepository_dispatchfrom engine / codegen-oss / HPO ({version, prerelease, run_id}), applies the pairing rule, setsVERSION, commits + tags (idempotent, with half-failed-release recovery and a downgrade guard).publish.yml— on the pushed tag or explicit replay: checks the pair, waits for upstream artifacts, builds a multi-arch candidate and verifies both child digests.release-finalize.ymlbinds package identities and measured release facts before publishing the immutable manifest, fixed/moving tags andpineforge-releasewith the release-ready/v1 payload. It supports offline, hosted, application, web, data and license roles; only explicitly upgraded receivers are enabled. Private targets are configured in the protectedrelease-automationenvironment, not published in the workflow or this diagram; each consumer gets an App token scoped to its configured target. A prerelease reaches the application by hand-off, with no repository dispatch and no App token. Stable consumers without verified completion stay pending.release-reconcile.ymlvalidates and repairs missed stable producer events;release-receipts.ymlvalidates consumer evidence and repairs dropped callbacks.
The rules live in scripts/release_pair.py (unit-tested in tests/).
tools/release-dry-run/ replays the current handler and readiness graph offline,
including all three-component arrival orders, duplicate/lost delivery, missing
prerequisites and negative controls. Run in CI or on the approved cloud host,
never on the Mac:
python3 -m unittest discover -s tests -p 'test_release_*.py' # the release rules
python3 tools/release-dry-run/dry_run.py --ready # see its README for dependencies
Status. The 1.0 line started on 2026-09-30: prerelease
v1.0.0-rc.1, thenv1.0.0(engine 1.0.0 + codegen 1.0.0). The 0.x line ended atv0.1.25. Read the Releases page for the newest pair.
- 0.x — an upstream event moves its own component's pin; the other stays at
the last release, and
VERSIONgets a patch bump. The 0.x line is stable-only. - From 1.0.0 on — engine and codegen must be the same
X.Y.Z, prerelease included, andVERSIONbecomes that pair version. Upstreams dispatch after their artifacts are published, so in either order the first event of a pair waits (its run logs the pending pin and builds nothing) and the partner's event finds the first one's artifacts (engine tarballs, codegen on PyPI) and completes the pair. It fails naming both versions when the partner has released a different new version (the pair can never form), when a tag records a mismatched pair, and when a 0.x event arrives after a 1.0 pair has landed. A probe that cannot reach GitHub or PyPI fails the run instead of waiting; re-run it. If a pair's tag push failed after its release commit reachedmain, the next event for that pair tags that commit. If both upstreams released but the hub only shows the first event's wait (the partner's dispatch was lost), re-run that waiting run: it probes again and completes the pair. - Prereleases (
X.Y.Z-alpha.N,-beta.N,-rc.N; PyPI spells the codegenX.Y.ZaN,X.Y.ZbN,X.Y.ZrcN) are published as a GitHub prerelease that is never Latest, withprerelease: truedownstream. client_payload.force: truelets a 0.x event move its component backwards. On the 1.0 line a release older than the landed pair is refused even forced: the release version is the pair's, so roll back by re-pointing image tags.
Credentials are a single org GitHub App (PINEFORGE_APP_ID /
PINEFORGE_APP_PRIVATE_KEY); no personal access tokens.
Operators may trigger events manually with their own gh login; no personal token is stored in this repository.
The automatic release chain's candidate/readiness gates, immutable event contract, consumer completion adapter and supervisor setup are documented in docs/release-chain.md. Releases fan out only after the combined image, package identities and measured release facts are ready.
X.Y.Z · X.Y · latest · engine<E>-codegen<C> · sha-<short>
latest and X.Y move only for the newest stable release. A prerelease (for
example 1.0.0-rc.1) gets X.Y.Z-rc.N · engine<E>-codegen<C> · sha-<short>
only.
Every release carries or links the exact source packages for the Debian
packages installed in its image, including prereleases. The notice at
/usr/share/doc/pineforge/SOURCES.txt points to the image's release page,
whose notes link to the checked source bundle and its set-NN.SOURCES.list.
The io.pineforge.sources.notice and io.pineforge.sources.url image labels
identify the notice and release page. A local build with RELEASE_VERSION=unknown
uses the general releases page in its notice and adds a local-build line.
[BIZ licence statement placeholder: BIZ will supply the approved licence statement.]
A release's source assets are never deleted while its image remains on ghcr.
Before any image is pushed, the publish job builds the selected target locally
for both architectures, reads their Debian inventories, and requires equal
source/version sets. scripts/release_sources.py checks that the set is
registered in docker/SOURCE-SETS.tsv, the downloaded list matches its registered
SHA-256, every listed asset has the recorded size and SHA-256 on the holding
release, and the list covers exactly the image's source/version pairs. A failure
stops both image publishing and release creation for every v* tag.
The registry has five tab-separated columns: set-id, release-tag, n-pairs,
sources-list-sha256, and list-asset (the exact set-NN.SOURCES.list name).
The set id hashes sorted unique source<TAB>version lines with a final newline.
The separate box run fetches the real sources; the owner attaches them to a
holding release. Add registry rows only after those real list
files and their source assets are available. The registry records the 11 EN-150
source sets, set-12 (linux 6.1.190-1 from bookworm-security, first in 1.6.1; its
other 83 sources are set-02's) and the earliest release holding each bundle.
A new source set (an image package set not yet in docker/SOURCE-SETS.tsv) needs
its bundle attached to an earlier or dedicated release before its first publish,
because the guard looks up the holding release by tag before the new tag's
GitHub Release exists.
The files in this repository (Dockerfile, entrypoint, scripts, workflows, tests) are Apache-2.0: see LICENSE. The image bundles software under its own terms, so that licence does not cover the whole image:
pineforge-engine(libpineforge.aand headers): Apache-2.0, with Eigen under MPL-2.0 — see the engine'sLEGAL.md.pineforge-codegen(the transpiler): source-available. From codegen 1.3.0 (images from 1.3.0 on) it is under the PineForge Source License 1.2; earlier releases keep the licence they shipped with: codegen 1.2.0 (the 1.2.0 image) the PineForge Source License 1.1, and releases up to 1.1.0 (the images up to 1.1.0) the PolyForm Noncommercial License 1.0.0 with a Personal Trading exception. Personal trading is free; investment management (from 1.2 of the licence this includes trading a funded-trader challenge, evaluation or simulated account), use by or for a company or fund, embedding in a product and hosted or public-facing services need a commercial licence — see itsLICENSEandLEGAL.md.- Debian base image, g++, Eigen and Python: their upstream licences.
The image's org.opencontainers.image.licenses label names the licences of the
PineForge components: Apache-2.0 AND LicenseRef-PineForge-Source-License-1.2
from 1.3.0 on, Apache-2.0 for the engine and this repository's files, and a
LicenseRef for the transpiler's LICENSE, the PineForge Source License 1.2 (no
SPDX identifier exists for it). The base image's packages, Eigen included, keep
the upstream licences listed above. The 1.2.0 image carries
Apache-2.0 AND LicenseRef-PineForge-Source-License-1.1, images 1.0.1 to 1.1.0
carry
Apache-2.0 AND LicenseRef-PolyForm-Noncommercial-1.0.0-Personal-Trading
(the transpiler's earlier LICENSE, the PolyForm Noncommercial License 1.0.0 as
modified by its Personal Trading and Commercial Use sections), and images up
to 1.0.0 carry Apache-2.0.
docker/run_json.py is the engine's harness, vendored from the pinned
pineforge-engine tag (scripts/sync-harness.sh <engine-version>; handle-upstream.yml
fetches the same file for the engine version it releases).
The Dockerfile refuses to build when the harness EXPECTED_PF_ABI differs from
PF_ABI_VERSION in the bundled engine headers.
docker build without --target, using docker/Dockerfile, builds the
backtest image: the last default stage aliases the runtime, release.
The default image differs from 1.5.0 only by the source notice, the two source
labels and the retained Debian package copyright files.
Only ENGINE_VERSION and CODEGEN_VERSION are required for that build, and its
entrypoint remains /opt/pineforge/bin/entrypoint.sh.
Use --target hpo to build the FULL image with the HPO command. It requires
ENGINE_VERSION, CODEGEN_VERSION, HPO_VERSION and all six toolchain arguments
listed below. That image accepts docker run <image> hpo <subcommand> for
space-info, compile, run and warm-encode. For example, inspect each
command's inputs with docker run --rm <image> hpo compile --help. The HPO
command has the C++ compiler, Eigen, engine headers and static library needed
to compile Pine-source studies, and a built pineforge-hpo-native runner.
PINEFORGE_ENGINE_ROOT and PINEFORGE_HPO_NATIVE point to those installed files.
The artifact cache is writable by the image's existing user; use --cache-dir
with a writable mount to retain compiled artifacts between containers.
With no first argument hpo, the dispatcher passes every argument unchanged
to the engine's existing /opt/pineforge/bin/entrypoint.sh. The eight engine
harness files and their byte and ABI checks remain in place.
For --target hpo, HPO_VERSION is a required build argument with no default,
recorded in the io.pineforge.hpo.version label and PINEFORGE_HPO_VERSION.
The build fetches
that release's wheel and native source, checks the wheel against
HPO_WHEEL_SHA256 when supplied, and asks pip to resolve the wheel's transpile
extra against the already installed codegen. A resolution error or any planned
codegen replacement fails the build. The wheel is then installed separately
without dependencies under /opt/pineforge/hpo, beside codegen on PYTHONPATH.
Publishing validates both bound artifact digests before passing
HPO_WHEEL_SHA256 and HPO_SDIST_SHA256 to every HPO build. The native source
is checked against the sdist digest before extraction and compilation. These
per-release artifact pins are declared in scripts/release_sources.py, separate
from the six Debian toolchain versions below.
This change merges without a release; the first publish carrying HPO is the
1.6.0 lockstep. HPO 0.15.0 is the fixture for the signed box proof.
The box must supply exact Debian package versions through these required build arguments; no toolchain version has been guessed:
| Build argument | Packages |
|---|---|
HPO_GCC_DEFAULTS_VERSION |
gcc, g++, cpp |
HPO_GCC_VERSION |
gcc-12, g++-12, cpp-12, libgcc-12-dev, libstdc++-12-dev, libgcc-s1, libstdc++6 |
HPO_BINUTILS_VERSION |
binutils |
HPO_EIGEN_VERSION |
libeigen3-dev |
HPO_CMAKE_VERSION |
cmake (native build only) |
HPO_MAKE_VERSION |
make (native build only) |
docker/hpo-toolchain.args records those six pins as plain NAME=VALUE lines,
with comments and blank lines allowed, and no shell syntax. The values were
recorded by a signed box run (and a value left empty is reported by the tests as
an unfilled skip); the publish workflow reads the file and passes the
values as build arguments for --target hpo. The Dockerfile uses build
arguments only. The default target does not need this file or its arguments.
The installed wheels' own *.dist-info/licenses/ files are also copied to
/opt/pineforge/licenses/pineforge-hpo/ (LICENSE, NOTICE and
THIRD_PARTY_LICENSES/sobol_joe_kuo.txt) and
/opt/pineforge/licenses/pineforge-codegen/ (LICENSE). The native source's
dlib and CORE-MATH licences sit in HPO's THIRD_PARTY_LICENSES/ as dlib.txt
and core_math.txt. The runtime compiler packages, binutils and Eigen retain
their package copyright files at /opt/pineforge/licenses/<package>/copyright,
copied from /usr/share/doc/<package>/copyright before documentation cleanup.
docker/LICENSE-SHA256SUMS records every copied file; the build rejects missing
entries, remaining box-run placeholders or any sha256sum -c difference.
OWNER/LEGAL PLACEHOLDER: [State how GPL toolchain sources are obtainable at the exact pinned Debian versions, including source-package names and the approved source-availability wording.]
The box runs tests/test_release_hpo_image.py (the CI test job discovers test_release_*.py). Its resolution check needs
PINEFORGE_HPO_REJECTED_WHEEL (the real 0.13.2 wheel) and
PINEFORGE_HPO_ACCEPTED_WHEEL (the real 0.15.0 wheel), each a file path or HTTPS
URL. With neither supplied it skips with an explicit fixture message; with
only one supplied it fails. PINEFORGE_CODEGEN_WHEEL can supply the real 1.5.0
wheel for an offline run; otherwise the test fetches it from PyPI and verifies
PyPI's digest. The same build helper must reject 0.13.2 because its real
pineforge-codegen<1.4,>=1.1.0 bound excludes 1.5.0, then resolve 0.15.0 without
changing codegen. The tests also compute each target's stage dependencies,
compare release with the original runtime at 989a0fd9~1, and check dispatcher
argv and exit status, the unchanged harness check, a forced licence mutation
and the README pair-rule bytes against origin/main. The args-file tests
compare its names with the real Dockerfile's toolchain declarations and
required guards, reject malformed or duplicate entries, and report empty
values as a named unfilled toolchain versions skip until the box fills them.