Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ for the full tool catalog, request schemas, and env vars (`PINEFORGE_ALLOW_ANYWH

- 🎯 **TradingView-exact.** 251 of 252 reference strategies match TV trade-for-trade. The lone outlier is a stress probe at the 1× margin boundary where TV's broker emulator is non-deterministic — engine is correct. **100 of 100** PineForge excellent vs PyneCore + PineTS on the public three-way benchmark (~167,000 TV trades; PyneCore: 85 of 100; PineTS indicator-only).
- ⚡ **Microsecond-class.** Median **162× faster than PyneCore** across 99 commonly-timed strategies (full 41,307-bar OHLCV, magnifier-on hot loop; see [benchmarks/results/speed.md](benchmarks/results/speed.md)). Parameter sweeps load one `.so` and re-run with new inputs — no recompile, no fork, no IPC.
- 🔒 **Stable C ABI.** 27 functions, one header (`<pineforge/pineforge.h>`). Append-only across minor versions, `static_assert`-pinned struct layouts, hidden-visibility hygiene. Drop a strategy `.so` in any harness; it just runs.
- 🔒 **Stable C ABI.** 28 functions, one header (`<pineforge/pineforge.h>`). Append-only across minor versions, `static_assert`-pinned struct layouts, hidden-visibility hygiene. Drop a strategy `.so` in any harness; it just runs.
- 🧪 **Reproducible to the bit.** Deterministic float ordering, deterministic bar magnifier, no internal RNG seeded from time. Two runs with the same inputs produce bit-identical trade lists.
- 🧰 **FFI-friendly.** Call from Python (`ctypes`), Rust (`libloading`), Go (`cgo`), Node, Julia. Worked examples for [pure C](https://cdocs.pineforge.dev/examples_c.html), [Python sweep](https://cdocs.pineforge.dev/examples_python_sweep.html), [Rust](https://cdocs.pineforge.dev/examples_rust.html), [multi-strategy harness](https://cdocs.pineforge.dev/examples_multi.html), and [magnifier A/B](https://cdocs.pineforge.dev/examples_magnifier.html) ship in the docs.
- 🌍 **Cross-platform CI.** Linux + macOS × Release + Debug. Universal mac binary. Static library, no runtime DSO surprises at deploy time.
Expand All @@ -129,7 +129,7 @@ for the full tool catalog, request schemas, and env vars (`PINEFORGE_ALLOW_ANYWH

## For developers: embed the runtime directly

PineForge ships as a static C library (`libpineforge.a`) with a stable 27-symbol C ABI. Call from C, Python, Rust, Go, Node, Julia — one harness, swap strategies forever.
PineForge ships as a static C library (`libpineforge.a`) with a stable 28-symbol C ABI. Call from C, Python, Rust, Go, Node, Julia — one harness, swap strategies forever.

### See it in 30 seconds

Expand Down Expand Up @@ -320,6 +320,7 @@ int main(void) {
| `run_backtest` | Run with auto-detected timeframe |
| `run_backtest_full` | Run with timeframe + magnifier configuration |
| `report_free` | Free arrays inside a filled `pf_report_t` |
| `strategy_closed_trade_entry_incarnation`| Read per-run physical entry provenance |
| `strategy_set_input` | Override a Pine `input.*()` value |
| `strategy_set_override` | Override a `strategy(...)` declaration param |
| `strategy_set_magnifier_volume_weighted` | Toggle volume-weighted magnifier |
Expand Down Expand Up @@ -398,7 +399,7 @@ cmake/smoke_consumer/ - Minimal find_package(PineForge) CI smoke project

## Visibility hygiene

Every compiled strategy `.so` that statically links `libpineforge.a` exports **exactly the 27 documented C ABI symbols** and zero internal C++ symbols. This is enforced at the library level:
Every compiled strategy `.so` that statically links `libpineforge.a` exports **exactly the 28 documented C ABI symbols** and zero internal C++ symbols. This is enforced at the library level:

- `libpineforge.a` is built with `-fvisibility=hidden -fvisibility-inlines-hidden`
- Public symbols are tagged `PF_API` (visibility=default)
Expand Down
29 changes: 25 additions & 4 deletions docker/run_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@
"max_drawdown": float,
"commission": float, # ABI v2
"entry_bar_index": int, # ABI v2: script-bar index of entry fill
"exit_bar_index": int # ABI v2: script-bar index of exit fill
"exit_bar_index": int, # ABI v2: script-bar index of exit fill
"entry_incarnation": int # run-scoped physical-entry provenance;
# 0 when the strategy lacks the accessor
},
...
],
Expand Down Expand Up @@ -552,6 +554,10 @@ def load_strategy(so_path: Path) -> ctypes.CDLL:
if hasattr(lib, "strategy_get_last_error"):
lib.strategy_get_last_error.argtypes = [ctypes.c_void_p]
lib.strategy_get_last_error.restype = ctypes.c_char_p
if hasattr(lib, "strategy_closed_trade_entry_incarnation"):
lib.strategy_closed_trade_entry_incarnation.argtypes = [
ctypes.c_void_p, ctypes.c_int]
lib.strategy_closed_trade_entry_incarnation.restype = ctypes.c_uint64

# syminfo setters — declare argtypes so ctypes does not default the float
# args to c_int (which would truncate mintick=0.5 to 0). Guarded with
Expand Down Expand Up @@ -624,7 +630,8 @@ def build_report_dict(report: ReportC, ohlcv_path: Path,
elapsed: float,
applied_inputs: dict[str, str],
applied_overrides: dict[str, str],
applied_runtime: dict[str, object] | None = None) -> dict:
applied_runtime: dict[str, object] | None = None,
trade_entry_incarnations: list[int] | None = None) -> dict:
trades = []
pnls: list[float] = []
for i in range(report.trades_len):
Expand All @@ -645,6 +652,11 @@ def build_report_dict(report: ReportC, ohlcv_path: Path,
"commission": float(t.commission),
"entry_bar_index": int(t.entry_bar_index),
"exit_bar_index": int(t.exit_bar_index),
"entry_incarnation": (
int(trade_entry_incarnations[i])
if trade_entry_incarnations is not None
and i < len(trade_entry_incarnations) else 0
),
})

n = len(pnls)
Expand Down Expand Up @@ -946,8 +958,17 @@ def _run(st, rep):
"trade_start_ms": args.trade_start_ms,
"chart_tz": args.chart_tz or "",
}
out = build_report_dict(report, args.ohlcv, n, first_ts, last_ts,
elapsed, inputs, overrides, applied_runtime)
incarnation_accessor = getattr(
lib, "strategy_closed_trade_entry_incarnation", None)
trade_entry_incarnations = (
[int(incarnation_accessor(state, i))
for i in range(report.trades_len)]
if incarnation_accessor is not None else None
)
out = build_report_dict(
report, args.ohlcv, n, first_ts, last_ts,
elapsed, inputs, overrides, applied_runtime,
trade_entry_incarnations)
if timing is not None:
out["diagnostics"]["timing"] = timing
out["diagnostics"]["throughput"] = _throughput_block(
Expand Down
33 changes: 33 additions & 0 deletions docker/run_json_diagnostics_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,39 @@ def test_summary_bars_processed_unchanged():
assert rep["summary"]["bars_processed"] == 525_600


def test_trade_entry_incarnation_is_optional_and_positionally_aligned():
report = _FakeReport()
report.trades_len = 1
report.trades = (run_json.TradeC * 1)()

with_identity = run_json.build_report_dict(
report,
ohlcv_path="x.csv",
n_bars=0,
first_ts=0,
last_ts=0,
elapsed=0.0,
applied_inputs={},
applied_overrides={},
applied_runtime={},
trade_entry_incarnations=[41],
)
legacy = run_json.build_report_dict(
report,
ohlcv_path="x.csv",
n_bars=0,
first_ts=0,
last_ts=0,
elapsed=0.0,
applied_inputs={},
applied_overrides={},
applied_runtime={},
)

assert with_identity["trades"][0]["entry_incarnation"] == 41
assert legacy["trades"][0]["entry_incarnation"] == 0


# --- --bench diagnostics contract (timing + throughput) ---------------------
# These pure blocks back the raw-data benchmark drivers
# (benchmarks/speed/time_pineforge_docker.py,
Expand Down
5 changes: 3 additions & 2 deletions docs/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
## Public C ABI

`<pineforge/pineforge.h>` is the **single canonical consumer header**.
Every compiled PineForge strategy `.so` exports exactly the 27 symbols
Every compiled PineForge strategy `.so` exports exactly the 28 symbols
declared there:


Expand All @@ -71,6 +71,7 @@ declared there:
| `run_backtest` | Run with auto-detected timeframe |
| `run_backtest_full` | Run with timeframe + magnifier configuration |
| `report_free` | Free arrays inside a filled `pf_report_t` |
| `strategy_closed_trade_entry_incarnation`| Read per-run physical entry provenance |
| `strategy_set_input` | Override a Pine `input.*()` value |
| `strategy_set_override` | Override a `strategy(...)` declaration param |
| `strategy_set_magnifier_volume_weighted` | Toggle volume-weighted magnifier |
Expand Down Expand Up @@ -120,7 +121,7 @@ single `.hpp`):

| Module | Header | Source | Pine-facing role |
| ------------------ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Public C ABI | `pineforge.h` | `c_abi.cpp` (+ layout `static_assert`s) | The 27 documented C symbols every compiled strategy `.so` exports. |
| Public C ABI | `pineforge.h` | `c_abi.cpp` (+ layout `static_assert`s) | The 28 documented C symbols every compiled strategy `.so` exports. |
| Engine | `engine.hpp` | `engine_run.cpp`, `engine_stream.cpp`, `engine_orders.cpp`, `engine_fills.cpp`, `engine_path_resolve.cpp`, `engine_strategy_commands.cpp`, `engine_trade_accessors.cpp`, `engine_security.cpp`, `engine_lower_tf.cpp`, `engine_risk.cpp`, `engine_report.cpp` | One-shot and continuous lifecycle, orders, raw-trade/bar fills, risk, reports, inputs / syminfo, magnifier, TF aggregation, and `request.security` plumbing. |
| Engine internals | `engine_internal.hpp` | (private cross-TU header) | `pineforge::internal::`* types and helpers shared between engine `.cpp` partitions; not part of the public ABI. |
| Technical analysis | `ta.hpp` | `ta_moving_averages.cpp`, `ta_oscillators.cpp`, `ta_volatility_trend.cpp`, `ta_extremes_volume.cpp`, `ta_misc.cpp` | Official `ta.`* functions and series variables backed by stateful runtime classes with `compute` / `recompute`, plus `pivot_point_levels(...)` free function. |
Expand Down
12 changes: 7 additions & 5 deletions docs/pages/abi-stability.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Three layers:

## Symbol inventory

A compiled strategy `.so` exports **exactly these 27 C symbols** and
A compiled strategy `.so` exports **exactly these 28 C symbols** and
zero internal C++ symbols:

| Symbol | Group |
Expand All @@ -64,6 +64,7 @@ zero internal C++ symbols:
| `run_backtest` | @ref pf_lifecycle |
| `run_backtest_full` | @ref pf_lifecycle |
| `report_free` | @ref pf_lifecycle |
| `strategy_closed_trade_entry_incarnation` | @ref pf_lifecycle |
| `strategy_set_input` | @ref pf_config |
| `strategy_set_override` | @ref pf_config |
| `strategy_set_magnifier_volume_weighted` | @ref pf_config |
Expand All @@ -87,10 +88,11 @@ zero internal C++ symbols:
| `pf_abi_version` | @ref pf_version |
| `pf_version_string` | @ref pf_version |

The five strategy-lifecycle functions are emitted by codegen. Runtime exports
are force-linked into each strategy library, so consumers resolve the same
complete ABI from the strategy `.so`. All additions remain covered by the
minor-version append-only guarantee.
The five create/run/free lifecycle functions are emitted by codegen. The
closed-trade incarnation accessor and other runtime exports are force-linked
into each strategy library, so consumers resolve the same complete ABI from the
strategy `.so`. All additions remain covered by the minor-version append-only
guarantee.

You can verify this against any strategy `.so`:

Expand Down
6 changes: 3 additions & 3 deletions docs/pages/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,18 @@ End-to-end, runnable examples that go beyond the MACD tutorial:

## API at a glance

The entire public surface fits in **one header** and **27 functions**:
The entire public surface fits in **one header** and **28 functions**:

| Group | Symbols | Reference |
| --- | --- | --- |
| Lifecycle | `strategy_create`, `strategy_free`, `run_backtest`, `run_backtest_full`, `report_free` | @ref pf_lifecycle |
| Lifecycle | `strategy_create`, `strategy_free`, `run_backtest`, `run_backtest_full`, `report_free`, `strategy_closed_trade_entry_incarnation` | @ref pf_lifecycle |
| Streaming | `strategy_stream_begin`, `strategy_stream_push_tick`, `strategy_stream_push_ticks`, `strategy_stream_advance_time`, `strategy_stream_end`, `strategy_stream_fill_report` | @ref pf_streaming |
| Configuration | Inputs, strategy overrides, tracing, trade start, chart / symbol timezone, session, tick size, point value, numeric metadata, and timestamped account-currency FX | @ref pf_config |
| Diagnostics | `strategy_get_last_error` | #strategy_get_last_error |
| Version | `pf_version_get`, `pf_abi_version`, `pf_version_string` | @ref pf_version |
| Types | `pf_bar_t`, `pf_trade_tick_t`, `pf_trade_t`, `pf_report_t`, metrics, diagnostics, trace, equity, version, and `pf_magnifier_distribution_t` | @ref pf_types |

Every PineForge-generated strategy `.so` exports exactly these 27 symbols
Every PineForge-generated strategy `.so` exports exactly these 28 symbols
and zero internal C++ symbols — see
**[ABI stability](@ref abi_stability)** for the full guarantee.

Expand Down
Loading
Loading