本文へスキップ

engine_trades.csv フォーマットを読み解く

読了 約5分

PineForgeが公開コーパスの全戦略について書き出すトレード一覧、engine_trades.csvのリファレンス。列ごとの意味、行がどうトレードとしてペアになるか、そしてpandas用のローダー。

現在の最新版は、pineforge-engine v1.10.0(2026-11-03 リリース)です。

2026-09-30 更新:エンジンが実際に書き出すフォーマット(pineforge-engine v1.0.0)に合わせました。

engine_trades.csv は、エンジンのトレード一覧を TradingView の「List of Trades」エクスポートと同じ形の CSV にしたもので、両者をトレード単位で突き合わせられるようになっています。書き出すのはエンジンリポジトリの scripts/run_strategy.py で、公開コーパス pineforge-corpus に収録された 312 本の戦略はそれぞれ、TradingView 自身のエクスポートである tv_trades.csv と並べてこのファイルを同梱しています(ただしセルフテスト用の戦略 1 本だけは、その代わりにモードごとの TradingView エクスポート trades-<mode>.csv を同梱しています)。Docker イメージと MCP サーバーは同じトレードを JSON で、レポートの trades 配列として返します。この記事で扱うのは CSV のほうです。

どんなファイルか

各行は、決済済みトレードの片側、つまりエグジットかエントリーのどちらかです。どのトレードもちょうど 2 行からなり、エグジット行が先に来て、両方の行に同じ Trade # が入ります。トレードは新しい順に並んでいて、ファイルは最大のトレード番号から始まり、トレード 1 で終わります。

列ごとのリファレンス

列型説明
Trade #整数トレードの番号。そのトレードのエグジット行とエントリー行で共通です。
Type文字列Entry long、Exit long、Entry short、Exit short のいずれか。
Date and time文字列約定時刻(UTC)。形式は YYYY-MM-DD HH:MM で、タイムゾーンの接尾辞は付きません。
Pricefloatこの側の約定価格。
Qtyfloat取引数量。常に正の値です。
Net PnLfloatトレードの純損益。両方の行に同じ値が入ります。
Net PnL %floatNet PnL をパーセントで表したもの。両方の行に同じ値が入ります。
Favorable excursion USDfloatトレード中の最大の含み益。ゼロまたは正の値です。
Adverse excursion USDfloatトレード中の最大の含み損。ゼロまたは負の値です(TradingView の慣例)。
Cumulative PnLfloatトレード 1 からこのトレードまでの Net PnL の累計。両方の行に同じ値が入ります。
Engine entry incarnation整数または空物理的なエントリー約定に対してエンジンが付ける ID。エントリー行に入り、エグジット行では空です。
Engine range-endopen または空エンジン v0.13.0 以降(v1.0.0 を含む)のファイルにのみある列です。データ範囲の終端でエンジンが決済したポジションのエグジット行に open が入ります。

Trade #:ペアをつなぐキー

ラウンドトリップを復元するには、Trade # でグループ化します。各グループにはエントリー行とエグジット行がちょうど 1 行ずつあります。ピラミッディングする戦略では、TradingView の採番と同じく、エントリーごとに 1 つのトレード番号が振られます。

Net PnL と Cumulative PnL は両方の行に入る

トレードの 2 行にはどちらも、そのトレードの Net PnL、Net PnL %、Cumulative PnL が入っています。Net PnL を合計するときは、各トレードを一度だけ数えてください(たとえばエグジット行だけを使うなど)。

エクスカーション

Favorable excursion USD と Adverse excursion USD は、ポジションが決済されるまでに到達した最良と最悪の含み損益で、口座通貨建てです。TradingView のエクスポートと同じく、adverse 側の値はゼロまたは負になります。この 2 つを合わせて見ると、ストップの置き方を評価しやすくなります。勝ちトレードで adverse excursion が小さければ、そのポジションは一度も追い込まれなかったということです。

pandas に読み込む

次のローダーは、各トレードのエントリー行とエグジット行を結合し、1 トレードにつき 1 行にまとめます。コーパスのファイル(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 はトレードの 2 行の両方に入っているので、列全体を合計すると結果が 2 倍になります。エグジット行だけを合計するか、先に Trade # でグループ化してください。

ファイルは新しい順に並んでいる。 1 行目は最後のトレードのエグジットです。時系列順が必要なら、Trade # でソートしてください。

タイムスタンプは UTC。 TradingView 自身のエクスポートはチャートのタイムゾーンを使うので、時刻を比べる前に両者をそろえてください。コーパスの TradingView トレード一覧の時刻は Asia/Taipei(UTC+8)です。グレーダーも、戦略の inputs.json が tv_trades_csv_tz で別のタイムゾーンを指定していない限り、このタイムゾーンを前提にします。

最後まで残ったポジションは決済される。 TradingView と同じく、エンジンは最終バーを過ぎても保有中のポジションを決済するので、通常のトレードとして現れます(現行エンジンでは、そのエグジット行が Engine range-end でフラグ付けされます)。

TradingView のエクスポートと比較する

TradingView の「List of Trades」エクスポートは列が多く、名前も異なります。Trade number(古いエクスポートでは Trade #)、Signal、通貨名の付いた Price USDT、Size (qty)、Net PnL USDT などで、日時はチャートのタイムゾーンで記録されます。列は名前ではなく意味で対応づけてください。エンジンのグレーダーである scripts/verify_corpus.py もそうしています。このスクリプトはどの表記も受け付け、方向、エントリー時刻(1 時間以内)、エントリー価格でトレードをペアにします。

次に読むなら

  • ギャラリーを見る:戦略ページでは、エンジンのトレード一覧が TradingView のものと並べて表示され、両方の CSV ファイルへのリンクもあります。
  • Claude や Cursor から MCP サーバーを使う:backtest_pine は同じトレードを JSON で返すので、アシスタントに分析を任せられます。インラインで返すには大きすぎるレポートは、ファイルに保存されるか(ローカルサーバー)、ダウンロード用のリンクとして返されます(ホスト型サーバー)。