参考

读懂 engine_trades.csv

PineForge 输出的交易列表 CSV 速查:逐列含义、成交配对如何编码,以及一段约 30 行的 Python 载入 pandas 示例。

约 5 分钟阅读#docs#csv#engine

每次 PineForge 回测都会在 JSON 摘要旁写出 engine_trades.csv:逐笔 fill 台账——开仓、平仓、数量、盈亏与持仓期内最优/最劣浮动。此文按列说明该文件,帮助你 不含糊地理解每一格,并在重建 round-trip 或做对齐 diff 时不踩坑。

文件是什么

engine_trades.csv 是一次完整回测的表格化成交列表。每行一笔 fill——入场或离场。Trade # 把同一 round-trip 的进出连在一起。

格式刻意贴近 TradingView「成交列表」CSV。差异主要在:PineForge 始终输出 MFEMAE;TradingView 仅在 Premium 档给出等价列。其余列名一致。

列说明

列名保持英文,与 TradingView 导出一致,便于逐行 diff。下面说明用中文。

ColumnType说明
Trade #整数round-trip 编号。开仓与平仓共享同一编号。
Type字符串Entry long / Exit long / Entry short / Exit short 之一。
Date and timeUTCYYYY-MM-DD HH:MM。无时区后缀。始终 UTC。
Pricefloat成交瞬间的 fill 价格(报价货币)。
Qtyfloat成交数量。恒为正。方向由 Type 表达。
Net PnLfloat该笔平仓的盈亏。仅在平仓行填写。 开仓行为空或零。
Net PnL %floatNet PnL 相对开仓名义金额的百分比。
MFEfloat持仓期内最大有利偏移——平仓前曾达到的最佳浮动盈利。
MAEfloat持仓期内最大不利偏移——平仓前的最差浮动亏损。
Cumulative PnLfloat从首行至当前平仓行的 Net PnL 累加和。

Trade #

往返编号。加仓时同一编号可出现多行;请按编号聚合,不要只看行序。

Type

四种取值;没有单独的「持仓」行。

Net PnL

只在平仓行有意义;开仓行多为空或零。

MFE / MAE

持仓期内最有利/最不利的浮动结果,便于评估止损摆放。

如何配对

Trade # 分组最稳妥。文件按成交时间排序,不同交易的进出可能交错。

用 pandas 读取

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}")

常见坑

Net PnL 只看平仓行。 逐行累加会重复或漏记。

累计盈亏不含未平仓。 若结束时仍有头寸,看 JSON 里的 open_equity

时间戳为 UTC,解析用 pd.to_datetime(..., utc=True) 较稳。

行顺序不等于逻辑交易顺序。

跨引擎对齐

与 TV 导出共用同一套列名;若做 parity harness,可先判断是否存在 MFE/MAE 再读。

延伸阅读

  • 画廊 — 公开回测 167 条;在卡片上使用「Download trades CSV」(若可用)。
  • MCPbacktest_pine 返回与独立 Docker 相同的 JSON;无需落盘 CSV 即可在对话里分析成交列表。