PineForge v0.10.12
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
FFI from Python

The C ABI is FFI-friendly by design: a handful of functions, 10 POD structs, one enum, no callbacks, no opaque types except pf_strategy_t (which is void*). This page shows the canonical ctypes wiring for Python; any language with a C-FFI (Rust libc, Go cgo, Node ffi-napi, Julia ccall) follows the same shape.

Full ctypes mirror

A complete, paste-able mirror of <pineforge/pineforge.h>:

import ctypes
class pf_bar_t(ctypes.Structure):
_fields_ = [
("open", ctypes.c_double),
("high", ctypes.c_double),
("low", ctypes.c_double),
("close", ctypes.c_double),
("volume", ctypes.c_double),
("timestamp", ctypes.c_int64),
]
class pf_trade_t(ctypes.Structure):
_fields_ = [
("entry_time", ctypes.c_int64),
("exit_time", ctypes.c_int64),
("entry_price", ctypes.c_double),
("exit_price", ctypes.c_double),
("pnl", ctypes.c_double),
("pnl_pct", ctypes.c_double),
("is_long", ctypes.c_int),
("max_runup", ctypes.c_double),
("max_drawdown", ctypes.c_double),
("qty", ctypes.c_double),
("commission", ctypes.c_double), # ABI v2
("entry_bar_index", ctypes.c_int32), # ABI v2
("exit_bar_index", ctypes.c_int32), # ABI v2
]
class pf_trade_stats_t(ctypes.Structure):
"""ABI v2 — one block each for all / long-only / short-only trades."""
_fields_ = [
("num_trades", ctypes.c_int32), ("num_wins", ctypes.c_int32),
("num_losses", ctypes.c_int32), ("num_even", ctypes.c_int32),
("percent_profitable", ctypes.c_double),
("net_profit", ctypes.c_double), ("net_profit_pct", ctypes.c_double),
("gross_profit", ctypes.c_double), ("gross_profit_pct", ctypes.c_double),
("gross_loss", ctypes.c_double), ("gross_loss_pct", ctypes.c_double),
("profit_factor", ctypes.c_double),
("avg_trade", ctypes.c_double), ("avg_trade_pct", ctypes.c_double),
("avg_win", ctypes.c_double), ("avg_win_pct", ctypes.c_double),
("avg_loss", ctypes.c_double), ("avg_loss_pct", ctypes.c_double),
("ratio_avg_win_avg_loss", ctypes.c_double),
("largest_win", ctypes.c_double), ("largest_win_pct", ctypes.c_double),
("largest_loss", ctypes.c_double), ("largest_loss_pct", ctypes.c_double),
("commission_paid", ctypes.c_double),
("expectancy", ctypes.c_double),
("max_consecutive_wins", ctypes.c_int32), ("max_consecutive_losses", ctypes.c_int32),
("avg_bars_in_trade", ctypes.c_double), ("avg_bars_in_wins", ctypes.c_double),
("avg_bars_in_losses", ctypes.c_double),
]
class pf_equity_stats_t(ctypes.Structure):
"""ABI v2 — equity-curve-derived stats (all-trades only)."""
_fields_ = [
("max_equity_drawdown", ctypes.c_double), ("max_equity_drawdown_pct", ctypes.c_double),
("max_equity_runup", ctypes.c_double), ("max_equity_runup_pct", ctypes.c_double),
("buy_hold_return", ctypes.c_double), ("buy_hold_return_pct", ctypes.c_double),
("sharpe_tv", ctypes.c_double), ("sortino_tv", ctypes.c_double),
("sharpe_bar", ctypes.c_double), ("sortino_bar", ctypes.c_double),
("cagr", ctypes.c_double), ("calmar", ctypes.c_double),
("recovery_factor", ctypes.c_double), ("time_in_market_pct", ctypes.c_double),
("open_pl", ctypes.c_double),
]
class pf_metrics_t(ctypes.Structure):
"""ABI v2 — composite metrics container."""
_fields_ = [("all", pf_trade_stats_t), ("longs", pf_trade_stats_t),
("shorts", pf_trade_stats_t), ("equity", pf_equity_stats_t)]
class pf_equity_point_t(ctypes.Structure):
"""ABI v2 — one per-script-bar equity point."""
_fields_ = [("time_ms", ctypes.c_int64), ("equity", ctypes.c_double),
("open_profit", ctypes.c_double)]
class pf_security_diag_t(ctypes.Structure):
_fields_ = [
("sec_id", ctypes.c_int),
("feed_count", ctypes.c_int64),
("complete_count", ctypes.c_int64),
("partial_count", ctypes.c_int64),
]
class pf_trace_entry_t(ctypes.Structure):
_fields_ = [
("timestamp", ctypes.c_int64),
("bar_index", ctypes.c_int32),
("name_id", ctypes.c_int32),
("value", ctypes.c_double),
]
class pf_report_t(ctypes.Structure):
_fields_ = [
("total_trades", ctypes.c_int),
("trades", ctypes.POINTER(pf_trade_t)),
("trades_len", ctypes.c_int),
("net_profit", ctypes.c_double),
("input_bars_processed", ctypes.c_int64),
("script_bars_processed", ctypes.c_int64),
("security_feeds_total", ctypes.c_int64),
("security_complete_total", ctypes.c_int64),
("security_partial_total", ctypes.c_int64),
("magnifier_sub_bars_total", ctypes.c_int64),
("magnifier_sample_ticks_total", ctypes.c_int64),
("input_tf_seconds", ctypes.c_int),
("script_tf_seconds", ctypes.c_int),
("script_tf_ratio", ctypes.c_int),
("needs_aggregation", ctypes.c_int),
("bar_magnifier_enabled", ctypes.c_int),
("security_diag", ctypes.POINTER(pf_security_diag_t)),
("security_diag_len", ctypes.c_int),
("trace", ctypes.POINTER(pf_trace_entry_t)),
("trace_len", ctypes.c_int),
("trace_names", ctypes.POINTER(ctypes.c_char_p)),
("trace_names_len", ctypes.c_int),
# ABI v2: computed metrics + per-script-bar equity curve
("metrics", pf_metrics_t),
("equity_curve", ctypes.POINTER(pf_equity_point_t)),
("equity_curve_len", ctypes.c_int64), # int64 in the C header, NOT c_int
]
class pf_version_t(ctypes.Structure):
_fields_ = [
("major", ctypes.c_int),
("minor", ctypes.c_int),
("patch", ctypes.c_int),
("commit_sha", ctypes.c_char_p),
]
# Magnifier distribution
PF_MAGNIFIER_UNIFORM = 0
PF_MAGNIFIER_COSINE = 1
PF_MAGNIFIER_TRIANGLE = 2
PF_MAGNIFIER_ENDPOINTS = 3 # default
PF_MAGNIFIER_FRONT_LOADED = 4
PF_MAGNIFIER_BACK_LOADED = 5
Single OHLCV bar pushed into the engine.
Definition pineforge.h:107
Single per-script-bar equity point.
Definition pineforge.h:257
Equity-curve-derived statistics (all-trades only, like TV).
Definition pineforge.h:210
Composite metrics container: trade stats (all / long / short) + equity-curve stats.
Definition pineforge.h:248
Backtest report filled by run_backtest / run_backtest_full.
Definition pineforge.h:297
Per-request.security() site diagnostic counters.
Definition pineforge.h:266
Single per-bar trace entry.
Definition pineforge.h:278
Trade-level statistics block — computed once each for all / long / short.
Definition pineforge.h:147
Closed-trade record returned in pf_report_t::trades.
Definition pineforge.h:119
Runtime version descriptor returned by pf_version_get.
Definition pineforge.h:572

Loading a strategy .so

Each compiled PineForge strategy .so exports the public ABI symbols itself. Open it with ctypes.CDLL:

lib = ctypes.CDLL("./my_strategy.so")
# ABI guard — pf_report_t is CALLER-allocated, so running an old .so
# against the v2 mirror above (or vice versa) silently corrupts memory.
# Verify the .so's layout version before any run:
EXPECTED_PF_ABI = 2 # PF_ABI_VERSION in <pineforge/pineforge.h>
try:
lib.pf_abi_version.restype = ctypes.c_int
abi = lib.pf_abi_version()
except AttributeError:
raise RuntimeError(".so predates pf_abi_version (ABI v1); rebuild it")
if abi != EXPECTED_PF_ABI:
raise RuntimeError(f"ABI mismatch: .so={abi}, mirror={EXPECTED_PF_ABI}")
lib.strategy_create.argtypes = [ctypes.c_char_p]
lib.strategy_create.restype = ctypes.c_void_p
lib.strategy_free.argtypes = [ctypes.c_void_p]
lib.strategy_free.restype = None
lib.run_backtest.argtypes = [ctypes.c_void_p,
ctypes.POINTER(pf_bar_t), ctypes.c_int,
ctypes.POINTER(pf_report_t)]
lib.run_backtest.restype = None
lib.run_backtest_full.argtypes = [
ctypes.c_void_p,
ctypes.POINTER(pf_bar_t), ctypes.c_int,
ctypes.c_char_p, ctypes.c_char_p, # input_tf, script_tf
ctypes.c_int, ctypes.c_int, ctypes.c_int, # magnifier, samples, dist
ctypes.POINTER(pf_report_t),
]
lib.run_backtest_full.restype = None
lib.report_free.argtypes = [ctypes.POINTER(pf_report_t)]
lib.report_free.restype = None
lib.strategy_set_input.argtypes = [ctypes.c_void_p,
ctypes.c_char_p, ctypes.c_char_p]
lib.strategy_set_input.restype = None
lib.strategy_set_override.argtypes = [ctypes.c_void_p,
ctypes.c_char_p, ctypes.c_char_p]
lib.strategy_set_override.restype = None

End-to-end run

import csv
# Load OHLCV
bars = (pf_bar_t * n)()
with open("ohlcv.csv") as f:
for i, row in enumerate(csv.DictReader(f)):
bars[i] = pf_bar_t(
float(row["open"]), float(row["high"]),
float(row["low"]), float(row["close"]),
float(row["volume"]), int(row["timestamp"]),
)
s = lib.strategy_create(b"{}")
report = pf_report_t()
lib.strategy_set_override(s, b"initial_capital", b"100000")
lib.strategy_set_input (s, b"Length", b"21")
lib.run_backtest_full(s, bars, n, b"15", b"15",
0, 4, PF_MAGNIFIER_ENDPOINTS,
ctypes.byref(report))
print(f"trades: {report.trades_len}")
print(f"net pnl: {report.net_profit:+.2f}")
for i in range(report.trades_len):
t = report.trades[i]
print(f" {'L' if t.is_long else 'S'} "
f"{t.entry_price:.4f}->{t.exit_price:.4f} pnl={t.pnl:+.2f}")
lib.report_free(ctypes.byref(report))
lib.strategy_free(s)

Common pitfalls

Pitfall Fix
Forgot ctypes.byref(report) and got a segfault. The report is out — pass by reference.
commit_sha truncated. Use c_char_p and .decode("utf-8"), not a fixed-size buffer.
int parameters silently truncated. Set argtypes explicitly — never rely on inference.
Tried to report_free twice. Safe — it's idempotent.
Held report.trace_names[i] past strategy_free. The strings live on the strategy. Copy before freeing.
Tried to share a handle across threads. Don't — handles are not thread-safe. One handle per worker.

See also