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_stream.py OHLCV warmup → contiguous trade-stream replay
├── 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: 50 (17W / 33L, 34.0% win)
net pnl: +569.97
best/worst:+1149.00 / -1111.97
max dd: -4045.15
elapsed: 0.4 ms
Numbers depend on the OHLCV snapshot.
After bash tutorial/run.sh has built the MACD strategy, run:
python3 tutorial/run_stream.pyThe script warms the strategy on the first 640 confirmed candles, converts the
remaining 32 candles into 128 ordered trade ticks, and sends that entire tail
through one strategy_stream_push_ticks(...) call. Positions, equity, pending
orders, Pine/TA history, and partially aggregated timeframe state remain on the
same strategy instance across the handoff.
The frozen tutorial dataset contains OHLCV rather than raw trades, so this example expands each live candle into a deterministic OHLC trade path. Replace that expansion with your exchange feed in production. The public API and state lifecycle remain the same.
Mount the strategy + OHLCV into the release hub's image,
ghcr.io/pineforge-4pass/pineforge-release (a released runtime plus a pinned
pineforge-codegen, named by its engine<E>-codegen<C> tag; this repository
publishes no image of its own); get a JSON report on stdout.
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" \
ghcr.io/pineforge-4pass/pineforge-release:latest > report.json
jq '.summary' report.jsonThe image transpiles the .pine with its own codegen and runs it on its own
engine, so it gives the numbers of the engine release it carries; the hub's
1.0.0 image (engine v1.0.0, codegen 1.0.0) gives Path A's numbers above.
To build the image yourself, use pineforge-release's docker/Dockerfile,
which vendors this tree's docker/ harness; this repository ships no
Dockerfile. 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", "Signal Length") |
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 50 30.0% -2575.42 -6093.70 0.4
8 21 1 66 27.3% -3368.61 -7270.83 0.7
...
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/strategy.pine:/in/strategy.pine:ro" \
-v "$(pwd)/tutorial/data/btcusdt_15m_7d.csv:/in/ohlcv.csv:ro" \
ghcr.io/pineforge-4pass/pineforge-release: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: this chart is fed only its own bars, so the 1m sub-bars are synthesized (a finer input or an auxiliary feed would supply real ones; TradingView always reads real bars).
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 the Pine source host
(<pineforge/source/pine_strategy_host.hpp>, on top of <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.