Tutorial / reference image. Same spirit as
tutorial/— a minimal, end-to-end example you can copy and adapt. Production deployments will want their own image (smaller base, pinned libpineforge version, hardened entrypoint, image signing, etc.). Use this one to learn the inputs/outputs and as a starting point.
Self-contained image for backtesting a PineScript v6 strategy against an
OHLCV CSV. The image bundles the pineforge-codegen transpiler (pip,
source-available), so you can mount a strategy.pine directly — it
transpiles → compiles → runs locally, no hosted API, no API key, source
never leaves the container. A pre-transpiled strategy.cpp is still
accepted for back-compat. JSON report on stdout, build/transpile noise on
stderr.
Each push to main (and every semver tag vX.Y.Z) publishes a
multi-arch image to GitHub Container Registry:
docker pull ghcr.io/pineforge-4pass/pineforge-engine:latest
# or pin a release:
docker pull ghcr.io/pineforge-4pass/pineforge-engine:0.1.0Available tags: latest, main, vX.Y.Z, X.Y, and sha-<short>.
Linux amd64 + arm64. Image must be marked public in the repo's
package settings for anonymous pulls; otherwise docker login ghcr.io
with a PAT (read:packages scope) first.
docker build -t pineforge -f docker/Dockerfile .Multi-stage build: stage 1 compiles libpineforge.a, stage 2 keeps only the
static lib, public headers, g++, python3, the pineforge-codegen
transpiler, and the JSON harness. One-time cost; per-run transpile+compile is
~1 second.
docker run --rm \
-v $(pwd)/strategy.pine:/in/strategy.pine:ro \
-v $(pwd)/ohlcv.csv:/in/ohlcv.csv:ro \
pineforge > report.jsonMount points (provide exactly one of strategy.pine / strategy.cpp):
| Host path | Container path | Required | Notes |
|---|---|---|---|
strategy.pine |
/in/strategy.pine |
preferred | PineScript v6 source; transpiled in-container |
strategy.cpp |
/in/strategy.cpp |
back-compat | Pre-transpiled PineForge translation unit (used only if no .pine) |
ohlcv.csv |
/in/ohlcv.csv |
yes | timestamp,open,high,low,close,volume |
Set PINEFORGE_TRANSPILE_ONLY=1 to emit the generated C++ on stdout and exit
— no OHLCV needed:
docker run --rm -e PINEFORGE_TRANSPILE_ONLY=1 \
-v $(pwd)/strategy.pine:/in/strategy.pine:ro \
pineforge > strategy.cppOptional env vars apply parameter overrides before the backtest runs:
| Env var | Maps to | Example |
|---|---|---|
PINEFORGE_INPUTS |
strategy_set_input(k, v) |
'{"Fast Length": "8", "Slow Length": "21"}' |
PINEFORGE_OVERRIDES |
strategy_set_override(k, v) |
'{"default_qty_value": "5", "commission_value": "0.04"}' |
PINEFORGE_INPUTS / PINEFORGE_OVERRIDES are JSON objects of
{string: string} (numeric values must be quoted strings; the runtime
parses on its side). Empty / unset → defaults from the original
strategy(...) and input.*() calls.
Each key maps to a single argument of the Pine strategy(...) call
and may be set independently. The runtime applies only the keys you
provide; everything else stays at the strategy's compiled-in default.
| Key | Type | Allowed values / range | Notes |
|---|---|---|---|
initial_capital |
number | > 0 |
Starting equity in account currency. |
pyramiding |
integer | >= 0 |
Max same-direction entries before further entries are blocked. |
slippage |
integer | >= 0 |
Per-fill slippage in ticks (mintick units). |
commission_value |
number | >= 0 |
Commission magnitude. Units depend on commission_type. |
commission_type |
enum | percent, cash_per_order, cash_per_contract |
Selects how commission_value is interpreted. |
default_qty_value |
number | any | Default order size, interpreted per default_qty_type. |
default_qty_type |
enum | fixed, percent_of_equity, cash |
Default sizing mode for strategy.entry/order calls. |
process_orders_on_close |
boolean | true / false (or 1 / 0) |
When true, market orders fill at bar close instead of next open. |
close_entries_rule |
enum | ANY, FIFO |
How strategy.close(id) selects entries (FIFO is the default). |
Example combining several keys:
docker run --rm \
-v $(pwd)/strategy.cpp:/in/strategy.cpp:ro \
-v $(pwd)/ohlcv.csv:/in/ohlcv.csv:ro \
-e 'PINEFORGE_OVERRIDES={
"initial_capital":"100000",
"default_qty_type":"percent_of_equity",
"default_qty_value":"10",
"commission_type":"percent",
"commission_value":"0.04",
"slippage":"2",
"pyramiding":"0",
"process_orders_on_close":"true",
"close_entries_rule":"ANY"
}' \
pineforge | jq '.applied_overrides'Runtime args (passed to run_backtest_full rather than the strategy
header) are configured via separate env vars:
| Env var | Default | Notes |
|---|---|---|
PINEFORGE_INPUT_TF |
auto-detect | Chart bar timeframe: '1', '5', '15', '60', 'D', 'W', ... |
PINEFORGE_SCRIPT_TF |
= input_tf | Strategy timeframe; must be ≥ input_tf (engine throws otherwise) |
PINEFORGE_BAR_MAGNIFIER |
false |
true enables intra-bar OHLC path sampling for stop/limit fills |
PINEFORGE_MAGNIFIER_SAMPLES |
4 |
Sub-bar sample count when magnifier is on (≥2) |
PINEFORGE_MAGNIFIER_DIST |
endpoints |
uniform, cosine, triangle, endpoints, front_loaded, back_loaded |
The engine catches every error (TF mismatch, unsupported emulation
flags, unknown-input-TF, etc.) into strategy_get_last_error(); the
container surfaces these as {"engine":"pineforge","error":"..."} on
stdout with exit code 1 instead of crashing.
Mount a strategy.pine and the bundled pineforge-codegen
(source-available,
pip install pineforge-codegen) transpiles it in-container. Advanced users may
instead mount a pre-transpiled strategy.cpp (the C++ must export the PineForge
C ABI in <pineforge/pineforge.h> — i.e. compile unchanged against
libpineforge.a into the standard 10-symbol strategy .so). Inputs are
read-only mounts; the image performs no network I/O at run time.
{
"engine": "pineforge",
"input": {
"ohlcv": "/in/ohlcv.csv",
"bars": 672,
"first_ts": 1745182800000,
"last_ts": 1745786700000,
"first_time": "2026-04-29 18:15 UTC",
"last_time": "2026-05-06 18:00 UTC"
},
"applied_inputs": {},
"applied_overrides": {},
"applied_runtime": {
"input_tf": "",
"script_tf": "",
"input_tf_seconds": 900,
"script_tf_seconds": 900,
"script_tf_ratio": 1,
"needs_aggregation": false,
"bar_magnifier": false,
"magnifier_samples": 4,
"magnifier_dist": "endpoints"
},
"elapsed_seconds": 0.0042,
"summary": {
"total_trades": 49,
"wins": 16,
"losses": 33,
"win_rate_pct": 32.6531,
"net_pnl": -190.85,
"avg_trade": -3.8949,
"best_trade": 1149.00,
"worst_trade": -1111.97,
"max_drawdown": -4045.15,
"bars_processed": 672
},
"trades": [
{
"n": 1,
"side": "long",
"entry_time": 1745188200000,
"exit_time": 1745192700000,
"entry_price": 75200.50,
"exit_price": 75312.00,
"qty": 1,
"pnl": 111.50,
"pnl_pct": 0.1483,
"max_runup": 150.00,
"max_drawdown": -22.10
}
]
}Every JSON report carries a fingerprint recording exactly what produced it —
reversible, no key required:
"fingerprint": {
"token": "<base64 of the canonical provenance JSON>",
"digest": "sha256:<hex>",
"provenance": {
"engine": { "version_string": "...", "major": 0, "minor": 10, "patch": 2, "commit_sha": "..." },
"codegen": { "version": "0.6.4", "generated_cpp_sha256": "...", "transpiled_from_pine": true },
"strategy": { "initial_capital": 1000000.0, "pyramiding": 1, "commission_type": "percent", "...": "all strategy() params, effective" },
"inputs": { "Fast Length": { "type": "int", "default": 9, "value": "8" }, "...": "all input()s, effective" },
"applied": { "inputs": { "Fast Length": "8" }, "overrides": {} },
"runtime": { "input_tf": "", "bar_magnifier": false, "...": "..." }
}
}strategy and inputs list the full effective parameter set — every
strategy() field and every input() value, with declared defaults, even
when no override was passed. value is the applied override if one was given,
otherwise the default. digest is a stable id for a run under a given harness and its runtime settings (same inputs + same settings ⇒ same digest).
Decode the token to recover the provenance:
jq -r '.fingerprint.token' report.json | base64 -d | jq .The provenance is also inlined under fingerprint.provenance, so decoding is
only needed to verify the token round-trips.
| Code | Meaning |
|---|---|
0 |
Success — JSON report (or C++ in transpile-only mode) on stdout |
2 |
Missing input mount (/in/strategy.pine or /in/strategy.cpp, and /in/ohlcv.csv) |
3 |
g++ compile of the strategy translation unit failed |
4 |
Backtest aborted at runtime |
5 |
Transpile failed (unsupported Pine construct or syntax error) |
The repo's tutorial/macd/strategy.pine and
tutorial/data/btcusdt_15m_7d.csv make a convenient smoke pair:
docker run --rm \
-v $(pwd)/tutorial/macd/strategy.pine:/in/strategy.pine:ro \
-v $(pwd)/tutorial/data/btcusdt_15m_7d.csv:/in/ohlcv.csv:ro \
pineforge | jq '.summary'- Linux-only image (Debian bookworm-slim). Run on macOS / Windows via Docker Desktop or any other amd64/arm64 OCI runtime.
- The image bundles
libeigen3-devfor the matrix-typed PineScript surface, even when your strategy does not touch it;<Eigen/...>must resolve at compile time because<pineforge/engine.hpp>transitively includes it. - Reproducibility: the libpineforge inside the image is pinned by the
image SHA. Tag releases (
pineforge:0.1.0) for stable backtests.