跳到正文

读懂 engine_trades.csv

约 5 分钟阅读

engine_trades.csv 参考:PineForge 为公开语料库中每个策略写出的交易列表。逐列说明、各行如何配对成交易,以及一个 pandas 加载函数。

最新版本:pineforge-engine v1.10.0(2026-11-03 发布)。

2026-09-30 更新:内容已对应引擎当前写出的格式(pineforge-engine v1.0.0)。

engine_trades.csv 是引擎输出的交易列表,CSV 的结构仿照 TradingView 的「交易列表」导出,方便两者逐笔对照。它由引擎仓库里的 scripts/run_strategy.py 写出;公开的 pineforge-corpus 中的 312 个策略,每一个都在 TradingView 自己的导出文件 tv_trades.csv 旁附带了一份(有一个自测策略例外,它为每种模式各附一份 TradingView 导出,即 trades-<mode>.csv)。Docker 镜像和 MCP 服务器以 JSON 形式返回同样的交易,位于报告的 trades 数组中;本文只讲 CSV。

文件是什么

每一行对应一笔已平仓交易的一侧:出场或入场。每笔交易恰好有两行,出场行在前,两行带有相同的 Trade #。交易按从新到旧排列:文件以最大的交易编号开头,以第 1 笔交易结尾。

逐列说明

列类型说明
Trade #整数交易编号,出场行和入场行共用。
Type文本Entry long、Exit long、Entry short 或 Exit short。
Date and time文本UTC 成交时间,格式为 YYYY-MM-DD HH:MM,不带时区后缀。
Price浮点数这一侧的成交价。
Qty浮点数成交数量,始终为正。
Net PnL浮点数这笔交易的净盈亏,两行的值相同。
Net PnL %浮点数以百分比表示的 Net PnL,两行的值相同。
Favorable excursion USD浮点数交易期间最大的未实现盈利,为零或正数。
Adverse excursion USD浮点数交易期间最大的未实现亏损,为零或负数(沿用 TradingView 的惯例)。
Cumulative PnL浮点数从第 1 笔交易到本笔交易的 Net PnL 累计和,两行的值相同。
Engine entry incarnation整数或空引擎为实际入场成交分配的 id,填写在入场行;出场行留空。
Engine range-endopen 或空仅出现在引擎 v0.13.0 及以后版本(包括 v1.0.0)写出的文件中:若某个仓位是引擎在数据区间末尾平掉的,其出场行标为 open。

Trade #:配对键

按 Trade # 分组,就能还原一次完整的开平仓(round trip)。每组恰好有一行入场、一行出场。对于加仓(pyramiding)的策略,每次入场都有自己的交易编号,与 TradingView 的编号方式一致。

两行都带有 Net PnL 和 Cumulative PnL

一笔交易的两行都带有该交易的 Net PnL、Net PnL % 和 Cumulative PnL。对 Net PnL 求和时,每笔交易只能计一次(例如只取出场行)。

有利偏移与不利偏移

Favorable excursion USD 和 Adverse excursion USD 是仓位在平仓前达到过的最好和最差的未实现结果,以账户货币计。与 TradingView 的导出一样,不利偏移的值为零或负数。两者结合起来,可以帮助判断止损设置得是否合理:一笔盈利交易的不利偏移很小,说明这个仓位从未真正承压。

用 pandas 载入

下面这个加载函数把每笔交易的入场行和出场行合并成一行。它已在一个语料库文件上验证过(analyzer-anvil-percent-costs-01:325 笔交易,净盈亏 −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}")

常见的坑

每笔交易只计一次。 Net PnL 在一笔交易的两行上都有,所以直接对整列求和,结果会翻倍。请只对出场行求和,或者先按 Trade # 分组。

文件按从新到旧排列。 第 1 行是最后一笔交易的出场行。如果需要按时间先后排列,请按 Trade # 排序。

时间戳是 UTC。 TradingView 自己的导出使用你图表所在的时区,所以比较时间之前,要先把两者对齐。语料库里的 TradingView 交易列表中,交易时间采用 Asia/Taipei(UTC+8);除非某个策略的 inputs.json 在 tv_trades_csv_tz 中指定了其他时区,否则评分器都按这个时区处理。

期末未平的仓位会被平掉。 和 TradingView 一样,引擎会平掉最后一根 K 线之后仍未平仓的仓位,因此它会以一笔普通交易的形式出现(当前引擎会在 Engine range-end 中标记它的出场行)。

与 TradingView 的导出对照

TradingView 的「交易列表」导出列更多,列名也不同:Trade number(较早的导出为 Trade #)、Signal、带货币后缀的 Price USDT、Size (qty)、Net PnL USDT 等,日期使用你图表所在的时区。请按含义而不是按名称来匹配列,引擎的评分器 scripts/verify_corpus.py 就是这么做的:它接受所有写法,并按方向、入场时间(相差一小时以内)和入场价格来配对交易。

延伸阅读

  • 浏览策略画廊:策略页面会把引擎的交易列表与 TradingView 的并排展示,并提供两份 CSV 文件的链接。
  • 在 Claude 或 Cursor 中使用 MCP 服务器:backtest_pine 以 JSON 形式返回同样的交易,供你的 AI 助手分析;报告太大、无法直接放进响应时,本地服务器会把它存成文件,托管服务器则会给出下载链接。