DEV Community

PineForge
PineForge

Posted on • Originally published at pineforge.dev

We Transpiled PineScript v6 to C++ So Backtests Are Actually Reproducible

We Transpiled PineScript v6 to C++ So Backtests Are Actually Reproducible

TradingView's PineScript is the most widely used language for writing trading strategies. Millions of scripts. One problem: you can't run them anywhere except TradingView.

That means:

  • Your data, locked to their symbols and timeframes
  • Backtests that drift between runs (floating-point, bar-index quirks)
  • No way to optimize with a custom objective
  • No CI pipeline, no audit trail, no compliance story

We built PineForge to fix this. The core idea: transpile PineScript v6 source to C++, compile it, run it offline on any OHLCV CSV.

Here's how the pipeline works and what we learned building it.


The Transpiler Pipeline

PineScript v6 source
        │
        ▼
    Lexer / Tokenizer
        │
        ▼
    Parser → AST
        │
        ▼
    Semantic Analyzer
    (type inference, series detection, scope resolution)
        │
        ▼
    Codegen
        │
        ▼
    C++ class (extends BacktestEngine)
        │
        ▼
    g++ / clang++ → native binary
Enter fullscreen mode Exit fullscreen mode

Each Pine script becomes a C++ class that extends our BacktestEngine base. TA call-sites (ta.sma, ta.rsi, ta.crossover, etc.) resolve to inlined C++ implementations. Pine's series[] type — which is really a lazy reverse-index into a rolling buffer — becomes a fixed-size ring buffer with bounds checking.

The trickiest part wasn't the TA functions. It was Pine's execution model.


Pine's Execution Model Is Weird

Pine evaluates top-to-bottom on every bar, with implicit state accumulation. There's no explicit loop — the runtime loops over bars for you, and every var-prefixed declaration persists across bars.

//@version=6
strategy("Example")

var float cumulative = 0.0
cumulative += close

sma20 = ta.sma(close, 20)

if ta.crossover(close, sma20)
    strategy.entry("Long", strategy.long)
Enter fullscreen mode Exit fullscreen mode

In C++, this becomes something like:

class ExampleStrategy : public BacktestEngine {
    float cumulative = 0.0f;  // var → class member, initialized once

    void onBar() override {
        cumulative += close();  // series access via method

        float sma20 = ta_sma(close_series, 20);

        if (ta_crossover(close_series, sma20_series)) {
            strategy_entry("Long", Direction::LONG);
        }
    }
};
Enter fullscreen mode Exit fullscreen mode

var declarations become class members. Non-var locals get re-initialized every bar. Series lookbacks (close[1], close[5]) become ring buffer accesses with automatic history tracking.


The Hard Part: Strict TradingView Parity

Writing a transpiler is one thing. Making it match TradingView trade-for-trade is another.

We built a corpus of 246 reference strategies — everything from classic MACD crossovers to multi-timeframe trend followers with complex entry/exit logic. For each:

  1. Run the strategy on TradingView, export the full trade list (entry/exit price, bar index, P&L)
  2. Run the same script through PineForge on the same OHLCV data
  3. Diff every trade, exact match required

Current result: 245/246 strategies at strict parity. 375,000+ trades validated. Zero engine bugs.

The one failing strategy hits a confirmed TradingView-side anomaly (their bar-close ordering in a specific multi-timeframe edge case). We've documented it; it's not our bug.

Getting from "mostly works" to 245/246 required fixing:

  • Floating-point order of operations — Pine's runtime accumulates differently in some TA functions; had to match the exact sequence
  • strategy.close_all timing — executes at bar-close, but the bar-close price depends on whether you're in calc_on_every_tick mode
  • barstate.isconfirmed semantics — subtly different from barstate.islast in historical replay
  • request.security bar alignment — when pulling a higher timeframe, the "current" bar alignment follows specific rules we had to reverse-engineer

Intra-Bar Resolution

TradingView's "Bar Magnifier" (premium feature) lets you simulate limit fills inside a bar. We implemented this with six distribution modes:

Mode Description
uniform Equal probability across the bar range
cosine Bell-shaped, price spends more time near midpoint
triangle Linear taper from open to close
endpoints Bimodal, price near open/close
front_loaded Higher probability near open
back_loaded Higher probability near close

All modes support optional volume weighting. A limit order at $100 inside a $95–$105 bar fills at exactly $100 if the simulated path crosses it — no last-tick approximation.


Optimization with Optuna

Pine strategies have parameters. Finding good ones via grid search is slow. We wired in Optuna with a custom objective interface:

# Any objective you want the optimizer to chase
def objective(trades):
    returns = [t.pnl_pct for t in trades]
    sharpe = mean(returns) / std(returns) * sqrt(252)
    max_dd = compute_max_drawdown(trades)
    return sharpe - 2.0 * max_dd  # penalize drawdown heavily
Enter fullscreen mode Exit fullscreen mode

TPE sampler, pruning via MedianPruner, parallel trials via n_jobs. The optimizer calls the compiled C++ binary directly — no Python overhead on the hot path.


Running It

One Docker container. No API key. No account.

# Transpile Pine to C++
docker run --rm -v $(pwd):/workspace pineforge/engine:latest \
  transpile --input /workspace/my_strategy.pine --output /workspace/out/

# Backtest against your OHLCV CSV
docker run --rm -v $(pwd):/workspace pineforge/engine:latest \
  backtest \
    --strategy /workspace/out/my_strategy \
    --data /workspace/BTCUSDT_1h.csv \
    --from 2022-01-01 --to 2024-01-01
Enter fullscreen mode Exit fullscreen mode

Output: JSON trade list, equity curve, summary stats. Pipe it anywhere.


What's Next

  • Hosted Studio (Q4 2026) — browser UI for backtest runs, parameter sweeps, equity curve comparison
  • Strategy marketplace — sell compiled .so binaries; buyers tune exposed inputs, never see source. AES-256-GCM encrypted, Ed25519-signed, machine-bound licenses.
  • Forward-test webhooks — same runtime as backtest, same JSON shape as TradingView alerts, no rate limits

Try It

The engine is open-core (Apache 2.0). The codegen is on GitHub. The 246-strategy parity corpus is public.

If you've ever hit TradingView's runtime ceiling — wrong fills, irreproducible results, locked data — this is the escape hatch.


Questions about the transpiler architecture, parity methodology, or the optimizer integration? Ask in the comments.


Licensing: Two Repos, Two Licenses

Worth being explicit about this since it trips people up.

pineforge-engine — Apache 2.0. The C++ runtime, the backtest engine, the ABI-stable .so interface. Full open source. CI runs on Ubuntu + macOS, 93.06% line coverage, 16 ctest binaries on every commit. Free to audit, fork, deploy.

pineforge-codegen — the Python transpiler package — is source-available under PolyForm Noncommercial 1.0.0 with a Personal Trading exception. Free to research, backtest, and trade your own account. What we charge for is hosted Optuna optimization and the Studio cloud IDE, plus a commercial license if you use the codegen inside a business.

Short version: the engine is fully open. The transpiler is free for personal trading, source-available, paid for commercial use.

Top comments (0)