pineforgeGet started
Reference

Reading the engine_trades.csv format

Complete reference for the trade-list CSV that PineForge emits. Column-by-column meaning, how trade pairs are encoded, and a 30-line Python snippet for loading into pandas.

5 min read#docs#csv#engine

Every PineForge backtest run writes a file called engine_trades.csv alongside the JSON summary report. It's the row-level ledger of every fill the engine saw: entries, exits, quantities, PnL, and excursion metrics. This post is the column-by-column reference for that file.

What the file is

engine_trades.csv is the tabular trade list for a completed backtest run. Each row is one fill event — either an entry into a position or an exit from one. Entries and exits are linked by a shared Trade # so you can reconstruct round-trips.

The format intentionally mirrors TradingView's "List of Trades" CSV export. If you've worked with that export, the column headers and row semantics will be immediately recognizable. The one meaningful difference: PineForge always emits the MFE and MAE columns, which TradingView only exposes for Premium subscribers. Everything else lines up header-for-header.

Column-by-column reference

ColumnTypeNotes
Trade #integerUnique pair ID. Entry and exit share the same number.
Typeenum stringOne of: Entry long, Exit long, Entry short, Exit short
Date and timeUTC stringFormat: YYYY-MM-DD HH:MM. No timezone suffix; always UTC.
PricefloatFill price in quote currency at the moment of execution.
QtyfloatUnits transacted. Always positive regardless of direction.
Net PnLfloatProfit or loss for this closing transaction. Set on Exit rows only. Entry rows have an empty or zero value here.
Net PnL %floatNet PnL expressed as a percentage of the entry notional cost.
MFEfloatMaximum Favorable Excursion within the trade — the best unrealized gain the position reached before close.
MAEfloatMaximum Adverse Excursion within the trade — the worst unrealized loss the position reached before close.
Cumulative PnLfloatRunning sum of all Net PnL values from row 1 through the current Exit row.

Trade #: the pairing key

The integer in Trade # identifies a round-trip. Every entry row gets a number, and the corresponding exit row carries the same number. If your strategy pyramids — adding to a position across multiple bars — you may see the same Trade # appear on more than two rows. Walk by this key, not by row sequence.

Type: four states

The four valid values are Entry long, Exit long, Entry short, and Exit short. Long and short indicate direction; entry and exit indicate which leg of the round-trip the row belongs to. There is no "hold" or "flat" row; only fill events are recorded.

Net PnL and where it lives

Net PnL is populated only on exit rows. Entry rows will have this column blank or zero depending on how the CSV was generated. If you see a non-zero Net PnL on an entry row, treat it as a formatting artifact and discard it. The matching exit row has the authoritative value.

MFE and MAE

Maximum Favorable Excursion (MFE) is the largest unrealized gain the position attained at any point during its lifetime, measured in quote-currency terms. Maximum Adverse Excursion (MAE) is the largest unrealized loss. Both are measured from the entry price to the best/worst intra-trade price, using bar OHLC data from the backtest window. These two columns make it straightforward to evaluate stop-placement quality: a small MAE with a large Net PnL suggests you weren't squeezed before hitting target.

Trade pairing: walking the file

The simplest way to work with the file is to group by Trade #. Each group will have exactly one entry row and one or more exit rows. For non-pyramiding strategies every group is a pair; for pyramiding strategies a group may have multiple entry rows sharing the same Trade number, with a corresponding set of exits.

Rows may not be sorted by Trade #. If a strategy fires entries across interleaved bars (common with pyramiding or when multiple OCA groups are active), you can see Trade 17's exit before Trade 12's entry in the raw file. Always sort or group before assuming sequential order.

Loading into pandas

This snippet loads the CSV, separates entry and exit rows, and joins them into one DataFrame with per-trade round-trip statistics.

import pandas as pd
 
def load_trades(path: str) -> pd.DataFrame:
    df = pd.read_csv(path)
 
    # Parse timestamps; UTC, no tz suffix in the file
    df["Date and time"] = pd.to_datetime(df["Date and time"], utc=True)
 
    entries = df[df["Type"].str.startswith("Entry")].copy()
    exits   = df[df["Type"].str.startswith("Exit")].copy()
 
    entries = entries.rename(columns={
        "Date and time": "entry_dt",
        "Price":         "entry_price",
        "Qty":           "entry_qty",
        "Type":          "direction",
    })
    entries["direction"] = entries["direction"].str.replace("Entry ", "")
 
    exits = exits.rename(columns={
        "Date and time": "exit_dt",
        "Price":         "exit_price",
        "Net PnL":       "net_pnl",
        "Net PnL %":     "net_pnl_pct",
        "MFE":           "mfe",
        "MAE":           "mae",
    })
 
    # Join on Trade # (take last exit for pyramiding strategies)
    exits_agg = exits.groupby("Trade #").last().reset_index()
    entries_agg = entries.groupby("Trade #").first().reset_index()
 
    trades = entries_agg.merge(exits_agg[
        ["Trade #", "exit_dt", "exit_price", "net_pnl", "net_pnl_pct", "mfe", "mae"]
    ], on="Trade #")
 
    return trades
 
 
if __name__ == "__main__":
    trades = load_trades("engine_trades.csv")
    print(trades[["Trade #", "direction", "entry_dt", "exit_dt", "net_pnl", "mfe", "mae"]]
          .head(10)
          .to_string(index=False))
    print(f"\nTotal trades: {len(trades)}")
    print(f"Win rate:     {(trades['net_pnl'] > 0).mean():.1%}")
    print(f"Avg MFE:      {trades['mfe'].mean():.4f}")
    print(f"Avg MAE:      {trades['mae'].mean():.4f}")

The snippet treats each Trade # as a single round-trip, taking the first entry and the last exit for pyramiding cases. For strategies with no pyramiding this is exact; for pyramiding strategies you may want to iterate within each group separately to capture intermediate partial exits.

Common gotchas

Net PnL is on the exit row, not the entry row. If you iterate row by row and accumulate Net PnL values, you'll double-count or miss values depending on whether you skip entry rows. Group by Trade # and pull Net PnL from the exit side only.

Cumulative PnL excludes open positions. At any point in the file, the Cumulative PnL column only reflects closed trades. If the backtest ends with an open position, that position's unrealized gain or loss is not included in the final Cumulative PnL value. The JSON summary report carries a separate open_equity field if you need the total mark-to-market value.

Timestamps are UTC, not exchange local time. The Date and time column has no timezone suffix, but it is always UTC. If you're comparing to another source that uses New York or London time, you'll need to apply the offset yourself. pd.to_datetime(..., utc=True) is the safest parse path.

Rows may not be in trade-number order. With pyramiding active, the file is ordered by fill timestamp, which means entries and exits for different trades can interleave. Don't assume row N+1 is the exit for row N.

Cross-engine compatibility

TradingView's "List of Trades" CSV export uses the same column headers as engine_trades.csv. The columns Trade #, Type, Date and time, Price, Qty, Net PnL, Net PnL %, and Cumulative PnL all match. MFE and MAE appear in TradingView's export for Premium-plan accounts; on Basic and Pro plans those columns are absent. PineForge always emits them.

This means you can use the same pandas loader against a TradingView CSV export with a minor guard: check whether MFE and MAE exist before reading them. If you're building a parity harness that diffs PineForge output against a TradingView export, the shared schema makes column-level alignment straightforward.

Where to go next