Skip to content

Reading the engine_trades.csv format

5 min read

Reference for engine_trades.csv, the trade list PineForge writes for every strategy in its open corpus: column by column, how rows pair into trades, and a pandas loader.

Current release: pineforge-engine v1.4.0, released 2026-10-08.

Updated on 2026-09-30 to the format the engine writes (pineforge-engine v1.0.0).

engine_trades.csv is the engine's trade list in a CSV shaped like TradingView's "List of Trades" export, so the two can be lined up trade by trade. The engine repo's scripts/run_strategy.py writes it, and each of the 312 strategies in the public pineforge-corpus ships one next to TradingView's own export, tv_trades.csv (one self-test strategy ships a TradingView export per mode instead, trades-<mode>.csv). The Docker image and the MCP servers return the same trades as JSON, in the report's trades array; this post covers the CSV.

What the file is

Each row is one side of a closed trade: the exit or the entry. Every trade has exactly two rows, the exit row first, and both carry the same Trade #. Trades run newest first: the file starts with the highest trade number and ends with trade 1.

Column-by-column reference

ColumnTypeNotes
Trade #integerThe trade's number, shared by its exit and entry rows.
TypetextEntry long, Exit long, Entry short or Exit short.
Date and timetextThe fill time in UTC, YYYY-MM-DD HH:MM, no timezone suffix.
PricefloatFill price for this side.
QtyfloatUnits traded, always positive.
Net PnLfloatThe trade's net profit or loss. The same value on both rows.
Net PnL %floatNet PnL as a percentage. The same value on both rows.
Favorable excursion USDfloatBest unrealized gain during the trade, zero or positive.
Adverse excursion USDfloatWorst unrealized loss during the trade, zero or negative (TradingView's convention).
Cumulative PnLfloatRunning sum of Net PnL from trade 1 through this trade. The same value on both rows.
Engine entry incarnationinteger or emptyThe engine's id for the physical entry fill, on entry rows; empty on exit rows.
Engine range-endopen or emptyOnly in files from engine v0.13.0 and later, v1.0.0 included: open on the exit row of a position the engine closed at the end of the data range.

Trade #: the pairing key

Group by Trade # to rebuild a round trip. Each group has exactly one entry row and one exit row. A strategy that pyramids gets one trade number per entry, the way TradingView numbers them.

Net PnL and Cumulative PnL on both rows

Both rows of a trade carry the trade's Net PnL, Net PnL % and Cumulative PnL. When you sum Net PnL, count each trade once (for example, the exit rows only).

Excursions

Favorable excursion USD and Adverse excursion USD are the best and worst unrealized results the position reached before it closed, in the account currency. The adverse value is zero or negative, as in TradingView's export. Together they help judge stop placement: a small adverse excursion on a winning trade means the position was never squeezed.

Loading into pandas

This loader joins each trade's entry and exit rows into one row per trade. It was checked against a corpus file (analyzer-anvil-percent-costs-01: 325 trades, net P&L −2,817.54).

import pandas as pd
 
def load_trades(path: str) -> pd.DataFrame:
    df = pd.read_csv(path)
 
    # Times are UTC, formatted "YYYY-MM-DD HH:MM", with no timezone suffix
    df["Date and time"] = pd.to_datetime(df["Date and time"], utc=True)
 
    entries = df[df["Type"].str.startswith("Entry")].rename(columns={
        "Date and time": "entry_dt",
        "Price":         "entry_price",
        "Type":          "direction",
    })
    entries["direction"] = entries["direction"].str.replace("Entry ", "")
 
    exits = df[df["Type"].str.startswith("Exit")].rename(columns={
        "Date and time":           "exit_dt",
        "Price":                   "exit_price",
        "Net PnL":                 "net_pnl",
        "Net PnL %":               "net_pnl_pct",
        "Favorable excursion USD": "mfe",
        "Adverse excursion USD":   "mae",
    })
 
    # One entry row and one exit row per Trade #
    trades = entries[["Trade #", "direction", "entry_dt", "entry_price", "Qty"]].merge(
        exits[["Trade #", "exit_dt", "exit_price", "net_pnl", "net_pnl_pct", "mfe", "mae"]],
        on="Trade #",
    )
    return trades.sort_values("Trade #").reset_index(drop=True)
 
 
if __name__ == "__main__":
    trades = load_trades("engine_trades.csv")
    print(trades[["Trade #", "direction", "entry_dt", "exit_dt", "net_pnl", "mfe", "mae"]]
          .head(5)
          .to_string(index=False))
    print(f"\nTotal trades: {len(trades)}")
    print(f"Win rate:     {(trades['net_pnl'] > 0).mean():.1%}")
    print(f"Net P&L:      {trades['net_pnl'].sum():.2f}")

Common gotchas

Count each trade once. Net PnL sits on both rows of a trade, so summing the whole column doubles the result. Sum the exit rows, or group by Trade # first.

The file runs newest first. Row 1 is the exit of the last trade. Sort by Trade # if you need chronological order.

Timestamps are UTC. TradingView's own export uses your chart's timezone, so line the two up before comparing times. Trade times in the corpus's TradingView trade lists are in Asia/Taipei (UTC+8), and the grader assumes that unless a strategy's inputs.json names another zone in tv_trades_csv_tz.

Positions open at the end are closed. Like TradingView, the engine closes a position that is still open after the last bar, so it appears as a normal trade (current engines flag its exit row in Engine range-end).

Comparing with TradingView's export

TradingView's "List of Trades" export has more columns and different names: Trade number (older exports: Trade #), Signal, a currency-suffixed Price USDT, Size (qty), Net PnL USDT and so on, with dates in your chart's timezone. Match columns by meaning, not by name, as the engine's grader scripts/verify_corpus.py does: it accepts every spelling and pairs trades by direction, entry time (within an hour) and entry price.

Where to go next

  • Browse the gallery: strategy pages show the engine's trade list next to TradingView's and link both CSV files.
  • Use the MCP servers from Claude or Cursor: backtest_pine returns the same trades as JSON for your assistant to analyze; a report too large to return inline is saved to a file (local server) or linked for download (hosted server).