End-to-end backtest you can run from a fresh clone in under a minute.
tutorial/
├── macd/
│ ├── strategy.pine Pine v6 reference
│ └── generated.cpp compiled C++ (becomes strategy.so)
├── mtf/
│ ├── strategy_htf.pine HTF SMA filter via request.security
│ ├── generated_htf.cpp → strategy_htf.so
│ ├── strategy_ltf.pine intra-bar via request.security_lower_tf
│ └── generated_ltf.cpp → strategy_ltf.so
├── data/
│ ├── btcusdt_15m_7d.csv 672 frozen bars (Binance)
│ └── fetch_btcusdt.py refresh from Binance public API
├── run.py ctypes harness + stats
├── run_advanced.py parameter sweep using ABI overrides
├── run_mtf.py MTF demo — script_tf switch + lower_tf
├── run.sh one-shot: cmake build + run.py
└── CMakeLists.txt
Pick whichever path matches what you have installed.
bash tutorial/run.shConfigures CMake (first time only), builds
tutorial/macd/strategy.so, then runs tutorial/run.py. Expected:
MACD(12,26,9) on BTCUSDT 15m — 672 bars, 2026-04-29 18:15 → 2026-05-06 18:00 UTC
trades: 49 (16W / 33L, 32.7% win)
net pnl: -190.85
best/worst:+1149.00 / -1111.97
max dd: -4045.15
elapsed: 0.4 ms
Numbers depend on the OHLCV snapshot.
Mount the strategy + OHLCV into the published runtime image; get a JSON report on stdout.
docker run --rm \
-v "$(pwd)/tutorial/macd/generated.cpp:/in/strategy.cpp:ro" \
-v "$(pwd)/tutorial/data/btcusdt_15m_7d.csv:/in/ohlcv.csv:ro" \
ghcr.io/pineforge-4pass/pineforge-engine:latest > report.json
jq '.summary' report.jsonSame engine, same numbers. Build the image locally instead with
docker build -t pineforge -f docker/Dockerfile . if you don't want
to pull from GHCR. Full mount/schema reference in
docker/README.md.
The compiled strategy.so exports two C ABI hooks for runtime overrides:
| Hook | Overrides |
|---|---|
strategy_set_input(k, v) |
input.*() named values from strategy.pine (e.g. "Fast Length", "Slow Length", "Source") |
strategy_set_override(k, v) |
strategy(...) header fields (initial_capital, commission_value, default_qty_value, pyramiding, slippage, default_qty_type, commission_type, process_orders_on_close) |
python3 tutorial/run_advanced.pyLoops a small (fast, slow) MACD grid × two qty sizes, prints a
ranked table:
MACD sweep on BTCUSDT 15m — 672 bars, 8 configs (commission 0.04% each side)
fast slow qty trades win% net_pnl max_dd ms
----------------------------------------------------------------
12 26 1 49 28.6% -3270.77 -6093.70 0.1
8 21 1 65 27.7% -3318.18 -7270.83 0.4
...
docker run --rm \
-e PINEFORGE_INPUTS='{"Fast Length": "8", "Slow Length": "21"}' \
-e PINEFORGE_OVERRIDES='{"default_qty_value": "5", "commission_value": "0.04"}' \
-v "$(pwd)/tutorial/macd/generated.cpp:/in/strategy.cpp:ro" \
-v "$(pwd)/tutorial/data/btcusdt_15m_7d.csv:/in/ohlcv.csv:ro" \
ghcr.io/pineforge-4pass/pineforge-engine:latest \
| jq '{applied_inputs, applied_overrides, summary}'Both env vars are JSON {key: value} objects (values stringified).
Empty / unset → defaults from strategy.pine.
Two extra .so files demonstrate the runtime's two MTF surfaces:
tutorial/mtf/strategy_htf.so— chart at 15m, HTF SMA trend filter pulled in viarequest.security. Demonstrates upward aggregation (input feed → coarser TF inside the strategy).tutorial/mtf/strategy_ltf.so— chart at 15m (or any TF), intra-bar 1m sub-bars synthesized viarequest.security_lower_tffrom each chart bar's OHLC path. Demonstrates downward synthesis — PF's design is that the input feed's resolution is the upper bound on what the lower-TF target can be, with no separate finer feed (contrast TradingView).
Build and run:
cmake --build build --target strategy_tutorial_mtf_htf strategy_tutorial_mtf_ltf -j
python3 tutorial/run_mtf.pyrun_mtf.py prints three tables, each with the exact
run_backtest_full(...) call signature above it:
- Table A —
script_tfsweep (b"",b"15",b"60",b"240") over a fixed input feed. Shows how the same compiled.soreinterprets cadence per run. - Table B —
(input_tf, script_tf)pair matrix. Shows the auto-detect → defaulting → concatenation chain on resolvedinput_tf_seconds/script_tf_seconds/script_tf_ratio. - Table C — lower-TF synthesis ratio at two different input TFs
(
b"15"→ 15 sub-bars/bar;b"60"→ 60 sub-bars/bar). Confirmssecurity_feeds_total == (input_tf_seconds / 60) * input_bars_processed.
Full design notes (validation rules, codegen contract, comparison with TradingView's lower-TF model) live in docs/pages/mtf.md.
generated.cpp is plain C++ over <pineforge/engine.hpp>. Edit it
(swap ta::MACD for ta::RSI, change params, add an exit rule),
then rerun whichever path you used. strategy.pine is the PineScript
form the C++ mirrors.
Refresh OHLCV: python3 tutorial/data/fetch_btcusdt.py
(supports --symbol, --interval, --limit).
Opt out of the tutorial build: cmake -B build -DPINEFORGE_BUILD_TUTORIAL=OFF.