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-end | open 或空 | 仅出现在引擎 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 助手分析;报告太大、无法直接放进响应时,本地服务器会把它存成文件,托管服务器则会给出下载链接。