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.
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
| Column | Type | Notes |
|---|---|---|
| Trade # | integer | Unique pair ID. Entry and exit share the same number. |
| Type | enum string | One of: Entry long, Exit long, Entry short, Exit short |
| Date and time | UTC string | Format: YYYY-MM-DD HH:MM. No timezone suffix; always UTC. |
| Price | float | Fill price in quote currency at the moment of execution. |
| Qty | float | Units transacted. Always positive regardless of direction. |
| Net PnL | float | Profit or loss for this closing transaction. Set on Exit rows only. Entry rows have an empty or zero value here. |
| Net PnL % | float | Net PnL expressed as a percentage of the entry notional cost. |
| MFE | float | Maximum Favorable Excursion within the trade — the best unrealized gain the position reached before close. |
| MAE | float | Maximum Adverse Excursion within the trade — the worst unrealized loss the position reached before close. |
| Cumulative PnL | float | Running 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
- Browse the gallery to see
engine_trades.csvfiles from 162 live strategy backtests — click any card, then "Download trades CSV." - Use the MCP tool from Claude or Cursor —
backtest_pinereturns the same JSON summary and you can ask Claude to analyze the trade list inline, without saving the CSV at all.