FX / EA Python 個人開発

PythonでFX自動売買Botを個人開発|バックテストからLIVE運用までの全体構成を紹介

本ページにはプロモーションが含まれています。

この記事では、Pythonで個人開発しているFX自動売買Botについて紹介します。

このBotは、GMO FXのAPIを利用し、ローソク足データの取得、バックテスト、PAPER運用、LIVE注文、ポジション管理、Discord通知までをPythonで実装しています。

この記事で分かること

  • PythonでFX自動売買Botをどのような構成で作っているのか
  • バックテストした戦略をPAPER運用、LIVE運用へどうつなげているのか
  • GMO FX APIを使った注文やポジション管理をどのように行っているのか
  • DockerやSupercronicを使ってBotをどのように定期実行しているのか
  • Discordを使ってエントリーや決済、エラーをどのように監視しているのか

FXの自動売買というと、MT4・MT5上で動作するEAをイメージする方も多いと思います。

今回紹介するBotは、MetaTrader上で動作するEAではなく、Pythonで売買戦略を判定し、GMO FXのAPIへ直接接続する自動売買Botです。

この記事では、「どの売買戦略が勝てるのか」といった戦略そのものではなく、PythonでFX自動売買Botを作り、バックテストからLIVE運用までつなげるために、どのような構成にしているのかを中心に紹介します。

FX自動売買Botの全体構成

このBotは、1つのPythonプロセスを常駐させて、データ取得から注文まですべてを処理する構成にはしていません。

ローソク足取得、エントリー判定、決済監視、運用監視などを独立したジョブに分け、それぞれを決められたタイミングで実行しています。

また、バックテストや検証で使用するコードと、LIVE運用に使用するコードも役割ごとに分けています。

戦略の検証中に変更したコードが、意図せずLIVE注文へ影響しにくくするためです。

Bot全体の処理フロー

LIVE運用時の大まかな処理フローは、次のようになっています。

GMO FX API
    ↓
BID・ASKローソク足を取得
    ↓
Parquetへ保存
    ↓
Bot設定をYAMLから読み込む
    ↓
戦略ごとに売買シグナルを判定
    ↓
共通形式の取引候補を生成
    ↓
建玉上限・重複エントリーを確認
    ↓
実行モードを判定
    ├─ PAPER → ローカルポジションを作成
    └─ LIVE  → GMO FXへ注文
                    ↓
              約定・建玉を同期
    ↓
決済・週末ルールを監視
    ↓
ポジション・イベント履歴を保存
    ↓
Discordへ通知

まずGMO FXから対象通貨ペアのローソク足を取得し、Parquet形式で保存します。

その後、YAMLで管理しているBot設定を読み込み、有効になっている戦略について売買シグナルを判定します。

戦略ごとにエントリー条件は異なりますが、条件を満たした場合は共通形式の取引候補へ変換するのです。

これにより、取引基盤側では「ボリンジャーバンド戦略なのか」「EMA戦略なのか」といった違いを意識せず、同じ仕組みで注文やポジション管理を行えます。

取引候補が作られた後は、建玉上限や重複エントリーなどを確認します。

安全確認を通過すると、実行モードによって処理が分かれます。

PAPERでは実際の注文を送信せず、ローカル上でポジションを管理します。

LIVEではGMO FXへ注文を送り、その後、実際の約定情報や建玉情報とローカルのポジションを同期します。

最後に、決済や週末ルールの監視、イベント履歴の保存、Discordへの通知を行います。

細かな注文や同期の仕組みについては、後半で詳しく紹介します。

使用している技術

このプロジェクトで使用している主な技術は次のとおりです。

技術主な用途
技術主な用途
Python 3.12Bot全体の実装
pandas / NumPyデータ加工・指標計算・バックテスト
PyArrow / Parquetローソク足データの保存
requestsGMO FX APIとの通信
YAMLBotごとの戦略設定
JSONローカルポジションの保存
JSONL注文・約定・決済イベントの保存
Docker / Docker Compose実行環境の統一
SupercronicPythonジョブの定期実行
Discord Webhook売買通知・運用監視

長期バックテスト用のデータにはGMOのAPIを利用し、LIVE運用時のローソク足取得や注文にはGMO FX APIを利用しています。

データ加工では主にpandasを使用しています。

複数時間足のローソク足を扱ったり、移動平均線やATR、RSIなどのテクニカル指標を計算したりする処理をPython上で行っています。

ローソク足の保存形式にはParquetを採用しました。

CSVでも保存できますが、Parquetはpandasから扱いやすく、数値型や時刻型を維持したまま保存できるため、複数年のローソク足を扱う用途でも使いやすいと感じています。

一方、現在のポジション管理にはJSON、注文や約定などのイベント履歴にはJSONLを使用しています。

ポジションは「現在の状態」を更新して管理するためJSON、イベントは過去の履歴を追記して残すためJSONLという使い分けです。

個人開発としてまず小さく始めることを優先した構成ですが、取引件数やBot数が増えた場合はデータベース化も検討しています。

また、売買条件と秘密情報も分離しています。

戦略パラメータはYAMLへ保存し、GMO FXのAPI認証情報やDiscordの通知先などは環境変数から読み込む構成にしています。

常駐ではなく定期実行型にした理由

このBotでは、Pythonの無限ループを使って一つのプロセスを常駐させるのではなく、各ジョブが「1回分の処理を実行して終了する」構成にしています。

現在はSupercronicから、おおむね次のスケジュールで各処理を実行しています。

ジョブ実行タイミング
ローソク足取得毎時2・17・32・47分
エントリー判定毎時6・21・36・51分
決済監視毎分
LIVE整合性確認毎時25分
ハートビート2時間ごと
ポジションレポート毎日22時

ローソク足取得とエントリー判定は、意図的に実行時刻をずらしています。

例えば15分足を利用する場合、ローソク足を取得した直後ではなく、数分後にエントリー判定を行います。

データ取得とシグナル判定を分けることで、保存処理と判定処理が同じタイミングで動くことを避けています。

また、エントリー判定と決済監視も別のジョブです。

エントリー戦略が15分足を利用していても、TP・SLや週末決済については、より短い間隔で確認したいためです。

処理をジョブ単位に分けることで、それぞれに適した実行周期を設定できます。

もう一つのメリットは、障害範囲を分けやすいことです。

例えばローソク足取得でエラーが発生しても、それだけで決済監視や他の処理すべてが停止する構成にはしていません。

また、複数のBotを実行する際も、一つの戦略で例外が発生した場合に後続Botまで停止しないよう、Bot単位で例外を処理しています。

各ジョブが1回実行して終了するため、実行環境を変更しやすい点もメリットです。

現在はDocker ComposeとSupercronicで実行していますが、将来的には、

EventBridge Scheduler
    ↓
ECS / Fargate Task
    ↓
Pythonジョブを1回実行
    ↓
終了

といったクラウド構成へ移行しやすい形を意識しています。

一方、定期実行型では重複実行への対策も必要です。

前回の処理が完了する前に次のジョブが起動した場合や、同じ確定足に対して複数回判定した場合、同じシグナルから重複注文が発生する可能性があります。

そのため、建玉数だけでなくシグナル時刻なども管理し、同じシグナルから複数のポジションを作りにくい構成にしています。

常駐型と定期実行型のどちらが適しているかはシステムによって異なりますが、15分足を中心に運用している現在のBotでは、データ取得・エントリー・決済・監視を小さなジョブへ分けて定期実行する構成が管理しやすいと考えています。

プロジェクトのディレクトリ構成

fx-trade-botでは、バックテスト用のコードとLIVE運用に使うコードを同じ場所にまとめず、役割ごとにディレクトリを分けています。

現在の主な構成は次のとおりです。

fx-trade-bot/
├─ backend/
├─ scripts/
├─ configs/
├─ data/
├─ docs/
├─ ops/
├─ tests/
├─ compose.yaml
├─ Dockerfile
└─ pyproject.toml

開発を始めた当初は、売買ロジックとバックテスト用のコードがあれば十分だと考えていました。

しかし、PAPER運用やLIVE運用へ進むにつれて、ローソク足の取得、注文、ポジション管理、約定・建玉の同期、通知など、売買戦略以外の処理が増えていきます。

そこで現在は、LIVE運用を担当するbackend、バックテストや検証を行うscripts、Botごとの設定を管理するconfigsというように、それぞれの役割を分けています。

backend|LIVE運用に使うアプリケーション本体

backendには、GMO FXのAPIと接続し、PAPER運用やLIVE運用を行うためのアプリケーション本体を配置しています。

主な構成は次のとおりです。

backend/
├─ jobs/
├─ market_data/
├─ bots/
├─ trading/
├─ notifications/
└─ shared/

それぞれの役割は大きく次のように分かれています。

jobsは、定期実行される各処理の入口です。

ローソク足の取得、エントリー判定、決済監視、GMO FXとの整合性確認、ハートビート通知などを、それぞれ独立したジョブとして実行します。

market_dataは、GMO FXからローソク足を取得し、保存するための処理を担当します。

BID・ASKそれぞれのローソク足を取得し、1分足、5分足、15分足、1時間足、4時間足、日足など、Botで利用する時間足をParquet形式で保存します。

botsには、ボリンジャーバンドやEMAなどを使った売買戦略のシグナル判定を配置しています。

戦略によって判定条件は異なりますが、条件を満たした場合は共通形式の取引候補を返す構成です。

tradingは、取引候補が作られた後の処理を担当します。

主な役割は、建玉上限の確認、Positionの作成、PAPERポジションの保存、GMO FXへのLIVE注文、約定・建玉の同期、決済処理などです。

notificationsには、Discordへの通知処理をまとめています。

エントリーや決済だけでなく、注文失敗、ハートビート、LIVE運用時の状態不一致なども通知します。

このように、売買戦略と注文・ポジション管理を分離することで、新しい戦略を追加しても取引基盤を作り直さずに済む構成にしています。

scripts|バックテスト・検証用コード

scriptsには、売買戦略を検証するためのバックテストやパラメータ探索、データ品質確認などのコードを配置しています。

主な構成は次のとおりです。

scripts/
├─ market_data/
├─ rule_based/
│  ├─ backtest/
│  ├─ usd_jpy/
│  ├─ eur_usd/
│  └─ aud_usd/
└─ trading/

market_dataには、バックテストで使用するローソク足の品質を確認するための処理があります。

バックテストは、入力データに欠損や重複があっても結果自体は出力できてしまいます。

そのため、戦略を検証する前に、以下の確認を行います。

  • データ件数
  • 対象期間
  • 時刻の並び
  • 重複
  • 欠損

rule_based/backtestには、複数の戦略から利用できるバックテスト用の共通処理を配置しています。

例えば、

  • Positionの管理
  • TP・SLの判定
  • 最大保有時間
  • 週末決済
  • 建玉数の制限
  • 取引履歴の作成
  • Profit Factorや最大ドローダウンの集計

などです。

通貨ペアごとのディレクトリには、個別戦略のバックテストやパラメータ探索用のスクリプトを配置しています。

バックテスト用コードとLIVE運用用コードを分けているのは、検証中の変更が意図せずLIVE注文へ影響することを防ぐためです。

一方で、価格の扱い、TP・SL、建玉制限、週末ルールなどについては、できるだけ実運用と同じ前提になるようにしています。

configs|戦略パラメータと実行モード

configsには、Botごとの売買条件や実行設定をYAML形式で保存しています。

configs/
└─ bots/
   └─ rule_based/
      ├─ usd_jpy/
      ├─ eur_usd/
      └─ aud_usd/

設定ファイルには、主に次のような情報を持たせています。

  • Bot名やバージョン
  • 対象通貨ペア
  • 使用する時間足
  • エントリー条件
  • TP・SL
  • 取引数量
  • 最大建玉数
  • PAPERまたはLIVE
  • Botを有効にするか

簡略化すると、次のような設定です。

config_id: sample_rule_based_bot
enabled: false
bot_type: rule_based
provider: usd_jpy
execution_mode: PAPER

market_data:
  signal_price_type: BID
  entry_price_type: ASK
  entry_interval: 15min

execution:
  max_open_positions: 1
  trade_units: 1000
  take_profit_pips: 40.0
  stop_loss_pips: 20.0

enabledtrueのBotだけを定期実行の対象にします。

また、execution_modePAPERの場合は実注文を送らず、LIVEの場合のみGMO FXへの注文処理へ進みます。

売買条件をPythonコードへ直接書き込まず、YAMLで管理することで、パラメータの変更履歴をGitで確認しやすくなります。

PAPERからLIVEへ切り替えたタイミングも明確に残せるため、設定変更を追跡しやすい点もメリットです。

一方で、GMO FXのAPI認証情報やDiscordの通知先などはYAMLへ保存せず、環境変数から読み込んでいます。

戦略設定と秘密情報を分離することで、設定ファイルを管理しやすくしています。

data|ローソク足・検証結果・ポジション履歴

dataには、Botで使用するローソク足やバックテスト結果、現在のポジション、売買イベントなどを保存しています。

data/
├─ candles/
├─ rule_based/
├─ positions/
├─ trade_events/
└─ runtime/

candlesには、GMO FXから取得したローソク足を保存します。

例えばUSD/JPYの場合、次のように時間足とBID・ASKを分けて管理します。

data/candles/USD_JPY/
├─ 1min_BID.parquet
├─ 1min_ASK.parquet
├─ 15min_BID.parquet
└─ 15min_ASK.parquet

バックテストやパラメータ探索の結果は、rule_based配下へ保存します。

主に出力しているファイルは次のとおりです。

  • summary.csv
  • trades.csv
  • yearly_summary.csv
  • monthly_summary.csv
  • rejected_signals.csv
  • 実行時の設定スナップショット

summary.csvには、取引数、勝率、純利益、Profit Factor、最大ドローダウンなどを保存します。

trades.csvには、各取引のエントリー価格、決済価格、保有時間、決済理由などを残しています。

採用した結果だけでなく、却下されたシグナルや不採用候補も保存することで、後から条件を比較できるようにしています。

現在のポジションはJSON、注文や約定などのイベント履歴はJSONLで管理しています。

Positionは「現在どうなっているか」を表すデータ、TradeEventは「過去に何が起きたか」を追跡するためのデータという使い分けです。

個人開発として小さく始めることを優先していますが、Bot数や取引件数が増えた場合は、ポジションやイベント履歴のデータベース化も検討しています。

docs|実験結果と意思決定の記録

docsには、戦略の運用方針、実験結果、採用・不採用の理由、バージョン履歴などをMarkdownで残しています。

バックテスト結果をCSVへ保存するだけでは、数か月後に見返したとき、

「なぜこの条件を採用したのか」

が分からなくなることがあります。

Profit Factorが最も高い候補でも、取引回数が少なすぎたり、特定の年だけの利益に依存していたりすれば、採用しない場合があります。

逆に、最高成績ではなくても、複数年で利益が残り、近接するパラメータでも大きく崩れない候補を選ぶこともあります。

そこで、

  • どの条件を比較したか
  • どの指標を重視したか
  • なぜ採用・不採用にしたか
  • どの課題が残っているか
  • PAPERやLIVEへ進めるか
  • 次に何を検証するか

といった判断も文章として残しています。

コードと検証結果だけでなく、判断した理由まで記録しておくことで、開発の経緯を後から追えるようにしています。

バックテストからLIVE運用までの流れ

このプロジェクトでは、バックテストで良い結果が出た戦略を、すぐにLIVE運用へ切り替えることはしません。

大きく次の5段階に分けて、戦略やシステムの動作を確認します。

ローソク足を用意
    ↓
バックテスト
    ↓
実運用に近い条件で再検証
    ↓
PAPER運用
    ↓
LIVE運用

工程を分けているのは、それぞれ確認する目的が異なるからです。

バックテストでは、売買戦略に優位性がありそうかを確認します。

PAPER運用では、リアルタイムのデータ取得や定期実行、重複エントリー防止などが想定どおりに動くかを確認します。

そしてLIVE運用では、注文受付、約定、建玉同期など、ブローカーと接続して初めて発生する処理まで確認します。

STEP1|検証用のローソク足データを用意する

最初に、GMO FXからバックテストに使用するローソク足を取得します。

現在はBIDとASKの両方について、

  • 1分足
  • 5分足
  • 15分足
  • 1時間足
  • 4時間足
  • 日足

などを保存しています。

毎回すべてのデータを取得し直すのではなく、保存済みデータの最新時刻を確認し、それ以降のローソク足を差分取得します。

保存先は、次のような形式です。

data/candles/{通貨ペア}/{時間足}_{価格種別}.parquet

例えばUSD/JPYの15分足なら、

data/candles/USD_JPY/15min_BID.parquet
data/candles/USD_JPY/15min_ASK.parquet

のようにBIDとASKを分けて保存します。

バックテストを始める前には、データの品質も確認します。

例えば、

  • 対象期間
  • データ件数
  • 時刻の並び
  • 重複
  • 欠損
  • UTCとJSTの扱い
  • BIDとASKの対応

などです。

バックテストは、入力データに問題があっても結果自体は出力できてしまいます。

そのため、戦略を調整する前に、まず検証に使うデータが正しいかを確認することが重要だと考えています。

STEP2|バックテストで売買戦略を検証する

ローソク足を用意したら、売買戦略をバックテストします。

この段階では、まず考えた戦略の仮説が過去データでも成立するかを確認します。

例えば、ボリンジャーバンドを使ったトレンドフォローであれば、

  • 一定以上のブレイクが発生しているか
  • ローソク足に十分な値動きがあるか
  • 上位足の方向と一致しているか
  • 曜日や時間帯の条件を満たしているか

などを組み合わせてエントリーを判定します。

シグナルが発生した後は、TP・SL、最大保有時間、週末決済、最大建玉数などのルールを適用します。

バックテストでは、主に次のような指標を確認しています。

  • 取引数
  • 勝率
  • Profit Factor
  • 純利益
  • 最大ドローダウン
  • 最大連敗数
  • 平均保有時間
  • 年別・月別成績

特に、勝率だけで戦略を判断しないようにしています。

勝率が高くても、一度の損失が大きければ利益が残らない場合があります。

反対に、勝率が50%を下回っていても、平均利益が平均損失を上回れば、全体として利益が残る可能性があります。

また、全期間のProfit Factorだけを見るのも危険です。

特定の年だけで利益の大部分を稼いでいないか、直近の成績が大きく悪化していないか、最大ドローダウンが大きすぎないかなども確認します。

パラメータについても、最も成績が高い一点だけを選ばないようにしています。

TPやSLを少し変えただけで成績が大きく崩れる場合は、過去データへ過剰に適合している可能性があるためです。

そのため、近接する複数の設定でも成績が大きく崩れないかを確認しながら、候補を絞り込んでいます。

STEP3|実運用に近い条件でバックテストを再検証する

戦略候補を絞り込んだ後は、より実運用に近い条件で再検証します。

特に重要なのが、BIDとASKの扱いです。

LONGの場合、実際にはASKで買い、BIDで売ります。

シグナル判定:BID
エントリー :ASK
決済    :BID

SHORTの場合は反対です。

シグナル判定:BID
エントリー :BID
決済    :ASK

エントリーと決済を同じ価格系列だけで計算すると、本来負担するスプレッドを十分に反映できず、バックテスト結果が実運用より楽観的になる可能性があります。

また、シグナルが確定した価格で必ず約定できるわけではありません。

例えば15分足が確定した時点でシグナルが発生しても、実際に注文できるのはその後です。

そのため、

  • BID・ASKを正しく使い分ける
  • スプレッドを考慮する
  • 実際に注文可能なタイミングでエントリーする
  • 1分足でTP・SLを判定する
  • 建玉上限を適用する
  • 週末決済を反映する

など、実運用へ近づけた条件でも戦略が成立するかを確認します。

条件を現実に近づければ、最初のバックテストより成績が悪くなることもあります。

しかし、大切なのはバックテストの数字を良く見せることではありません。

現実的な条件を加えても、戦略の優位性が残るかを確認することがこの工程の目的です。

STEP4|PAPER運用でリアルタイム相場のシグナルを検証する

バックテストを通過した戦略は、次にPAPER運用へ進めます。

PAPER運用ではGMO FXへ実際の注文は送らず、リアルタイムで取得したローソク足を使って、バックテストと同じ戦略ロジックを動かします。

設定では、例えば次のように指定します。

enabled: true
execution_mode: PAPER

PAPER運用でも、シグナルを出して終わりではありません。

取引候補からPositionを作成し、ローカルへ保存します。

その後、決済ジョブからTP・SLや週末ルールを監視し、エントリーや決済結果をDiscordへ通知します。

この工程では、主に次の点を確認します。

  • ローソク足取得とシグナル判定が予定どおり動くか
  • 未確定のローソク足を使っていないか
  • 同じシグナルから重複エントリーしないか
  • 建玉上限が機能しているか
  • TP・SLや週末決済が正しく動くか
  • PositionやTradeEventが正しく保存されるか
  • DiscordからBotの状態を確認できるか

バックテストでは、保存済みデータを連続して処理できます。

一方、PAPER運用ではローソク足取得、エントリー判定、決済監視などが、それぞれ別のタイミングで起動します。

そのため、バックテストだけでは見つからなかった、時刻処理やデータ更新、重複実行などの問題が見つかることがあります。

PAPER運用は、利益が出るかだけを確認する工程ではありません。

バックテストした戦略を実際の運用フローへ接続し、データ取得から決済まで継続して動作するかを確認するための工程でもあります。

STEP5|安全装置を確認してLIVE運用へ切り替える

PAPER運用で一連の処理を確認できたら、LIVE運用への切り替えを検討します。

ただし、YAMLのexecution_modeLIVEへ変更するだけでは不十分です。

LIVE運用では、少なくとも次の4つを確認します。

注文に関する確認

  • 取引数量が正しいか
  • 建玉上限が設定されているか
  • 重複注文を防止できるか
  • 最新価格やスプレッドを確認できるか

決済に関する確認

  • TP・SLを正しく設定できるか
  • 週末決済が実行されるか
  • Bot停止時にも決済注文を残せるか

状態管理に関する確認

  • 注文受付と約定を区別できるか
  • 部分約定と全数量約定を区別できるか
  • GMO FXの建玉とローカルPositionを対応付けられるか

異常時の確認

  • 注文失敗時に誤ってOPENにしないか
  • ブローカーとの状態不一致を検知できるか
  • 異常をDiscordへ通知できるか

LIVE注文では、エントリー価格、TP、SLが設定された取引について、GMO FXへIFDOCO注文を送ります。

ブローカー側へTP・SL注文を登録しておくことで、Botやコンテナが一時的に停止した場合でも、決済注文を残せるようにしています。

また、注文APIから正常なレスポンスが返ったからといって、すぐにポジションをOPENにはしません。

GMO FXへ注文
    ↓
注文受付
    ↓
PENDING_OPEN
    ↓
約定を確認
    ↓
OPEN

注文が受け付けられたことと、実際に約定したことは別だからです。

注文受付後はいったんPENDING_OPENとして管理し、約定情報やブローカー側の建玉を確認した後に、実際のポジションとして扱います。

LIVE運用を開始するときは、取引数量も小さくします。

バックテストやPAPER運用で多くの処理を確認できますが、実際の約定価格や注文制約、API固有の挙動、通信障害などはLIVE環境で初めて分かる場合があります。

そのため、最初から大きな数量で運用するのではなく、

注文 → 約定 → 建玉同期 → 決済 → ローカル状態更新

という一連の流れが問題なく動作することを、小さな数量で確認してから運用を進めます。

バックテストで利益が出たことは、LIVE運用へ進むための条件の一つにすぎません。

注文、約定、建玉、決済、監視まで正しく動き、問題が発生したときに停止・調査できる状態を作ってからLIVE運用へ進むことが重要です。

複数の売買戦略を共通基盤で動かす仕組み

現在のfx-trade-botでは、USD/JPY、EUR/USD、AUD/USDを対象に、複数のルールベース戦略を実装しています。

例えば、USD/JPYではボリンジャーバンドを使ったトレンドフォロー、EUR/USDではEMAを使ったブレイクアウト、AUD/USDでは上位足の方向と押し目・戻りを組み合わせた戦略などです。

使用するテクニカル指標やエントリー条件は戦略ごとに異なります。

一方、シグナルが出た後に必要な処理は、どの戦略でもほとんど変わりません。

取引候補を作る
    ↓
建玉上限を確認する
    ↓
Positionへ変換する
    ↓
PAPERまたはLIVEで実行する
    ↓
ポジションを管理する
    ↓
決済を監視する
    ↓
Discordへ通知する

戦略を追加するたびに、この一連の処理をコピーしてしまうと、修正箇所が増えるだけでなく、Botごとに微妙な挙動の違いが生まれる原因になります。

そこでfx-trade-botでは、売買戦略が担当する部分と、注文やポジション管理を行う共通基盤を分離しています。

売買ロジックと注文処理を分離する

各戦略が担当するのは、ローソク足を評価し、「取引候補を作るかどうか」を判断するところまでです。

例えば、ボリンジャーバンドを使った戦略では、M15のブレイク幅やローソク足の実体、H1・H4のトレンドなどを確認します。

EMAを使った戦略では、EMAや直近高値・安値、上位足の方向、RSIなどを組み合わせてエントリーを判定します。

条件を満たした場合、各戦略は共通形式のTradeCandidateを返します。

TradeCandidateには、主に次の情報を持たせています。

  • 通貨ペア
  • LONG / SHORT
  • 想定エントリー価格
  • シグナル時刻
  • Bot名・バージョン
  • 戦略ID
  • 取引数量
  • TP・SL
  • 戦略固有のメタデータ

重要なのは、戦略側ではGMO FXへの注文やポジション保存を行わないことです。

注文処理まで各戦略へ持たせてしまうと、PAPERとLIVEの分岐、注文失敗時の処理、約定確認などを戦略ごとに実装する必要があります。

そのため、戦略は取引候補を作るところまでに限定し、その後の処理は共通基盤へ渡します。

TradeCandidate
    ↓
PositionFactory
    ↓
Position

PositionFactoryでは、取引候補を実際のポジションとして扱えるPositionへ変換します。

このとき、通貨ペアごとのpips単位や売買方向を考慮して、TP価格とSL価格も計算します。

LONGの場合は、

TP価格 = エントリー価格 + TP幅
SL価格 = エントリー価格 - SL幅

SHORTの場合は反対です。

TP価格 = エントリー価格 - TP幅
SL価格 = エントリー価格 + SL幅

Positionへ変換された後は、どの戦略から作られたかに関係なく、共通のエントリー処理へ進みます。

また、設定に応じて使用する戦略を切り替えられるよう、戦略Runnerを管理するレジストリも用意しています。

YAMLに設定したproviderbot_versionをもとに、対応するRunnerを選択する仕組みです。

新しい戦略を追加するときは、基本的に戦略Runnerを実装してレジストリへ登録します。

注文、Position保存、約定同期、決済監視、通知といった処理まで新しく作り直す必要はありません。

役割を整理すると、次のようになります。

領域主な役割
戦略Runnerローソク足を評価して取引候補を作る
TradeCandidate戦略から取引基盤へ情報を渡す
PositionFactory取引候補をPositionへ変換する
RunEntryJob建玉確認やPAPER・LIVEの分岐
Broker処理GMO FXへの注文
RunExitJob約定同期や決済監視

売買ロジックと取引処理を分離したことで、戦略の改善と取引基盤の改善を別々に進められるようになりました。

YAMLで戦略パラメータと実行モードを管理する

Botごとの設定は、configs/bots配下のYAMLファイルで管理しています。

configs/
└─ bots/
   └─ rule_based/
      ├─ usd_jpy/
      ├─ eur_usd/
      └─ aud_usd/

設定ファイルには、売買条件だけでなく、対象通貨ペア、取引数量、最大建玉数、PAPER・LIVEの実行モードなども記述します。

簡略化すると、次のような構成です。

config_id: sample_usd_jpy_bot
enabled: false
bot_type: rule_based
provider: usd_jpy
execution_mode: PAPER

runtime:
  bot_id: sample_bot
  bot_version: v1
  display_name: Sample USD/JPY Bot
  strategy_name: sample_strategy
  side: LONG

symbols:
  - USD_JPY

market_data:
  signal_price_type: BID
  entry_price_type: ASK
  entry_interval: 15min
  h1_interval: 1hour
  h4_interval: 4hour

strategy:
  breakout_min_pips: 8.0
  body_min_pips: 8.0
  friday_entry_cutoff_hour_jst: 19

execution:
  max_open_positions: 1
  trade_units: 1000
  take_profit_pips: 40.0
  stop_loss_pips: 20.0

enabledは、そのBotを定期実行の対象にするかを決める項目です。

enabled: false

この場合、設定ファイルを残したまま、Botを実行対象から外せます。

検証中の戦略や、一時的に停止したい戦略を削除する必要はありません。

execution_modeでは、PAPERとLIVEを切り替えます。

execution_mode: PAPER

PAPERではGMO FXへ実注文を送らず、ローカル上でPositionを管理します。

execution_mode: LIVE

LIVEへ変更すると、同じ戦略ロジックで作られた取引候補を使い、GMO FXへの注文処理へ進みます。

戦略パラメータをPythonコードへ直接書き込まずYAMLへ分離しているのは、設定変更をコード変更と分けて管理したいからです。

例えばGitの履歴から、

  • どの条件を変更したか
  • TP・SLをいつ変更したか
  • 取引数量をいつ変更したか
  • PAPERからLIVEへいつ切り替えたか
  • Botをいつ有効化・停止したか

といった変更を追いやすくなります。

売買ロジックそのものを変更したのか、運用設定を変更しただけなのかも判断しやすくなりました。

なお、YAMLへ保存するのは売買条件や運用設定です。

GMO FXのAPI認証情報やDiscordのWebhook URLなどの秘密情報は設定ファイルへ書かず、実行環境の環境変数から読み込んでいます。

PAPERとLIVEで同じ戦略ロジックを使う

PAPERとLIVEで別々の売買ロジックを持つと、同じ戦略のはずなのに判定結果が異なる可能性があります。

例えば、PAPERではエントリー条件を満たしているのに、LIVE側への実装移植時に条件が抜け、注文されないといったケースです。

そこでfx-trade-botでは、取引候補を作るところまではPAPERとLIVEで同じ処理を通す構成にしています。

YAMLを読み込む
    ↓
戦略Runnerを選択する
    ↓
ローソク足を評価する
    ↓
TradeCandidateを作る
    ↓
Positionへ変換する
    ↓
建玉上限などを確認する
    ↓
実行モードで分岐する

PAPERとLIVEで処理が分かれるのは、実際に注文する段階からです。

PAPERではGMO FXへ注文を送らず、Positionをローカルへ保存します。

PAPER
    ↓
PositionをOPENとして保存
    ↓
Discordへエントリー通知

LIVEでは、まずGMO FXへ注文します。

LIVE
    ↓
GMO FXへ注文
    ├─ 失敗 → Positionを保存せず、失敗を通知
    └─ 受付 → PENDING_OPENとして保存

注文に失敗した場合、ローカルPositionをOPENにはしません。

ブローカー側には建玉が存在しないのに、Bot側だけがポジションを保有している状態になるのを防ぐためです。

また、注文が受け付けられた場合も、すぐにエントリー成立とは判断しません。

GMO FXから全数量の約定を確認できるまではPENDING_OPENとして扱い、約定確認後にOPENへ変更します。

このように、PAPERとLIVEでは戦略ロジックを共通化しつつ、LIVE固有の注文・約定処理だけを取引基盤側で扱う構成にしています。

PAPERで確認した売買ロジックをそのままLIVEへ接続できることは、実装差を減らすうえで大きなメリットだと感じています。

LIVE運用で必要になった仕組み

バックテストやPAPER運用では、シグナルが発生した時点でPositionを作ることができます。

しかし、LIVEではシグナル発生からポジション保有までの間に、ブローカーへの注文と約定が入ります。

シグナル
    ↓
注文送信
    ↓
注文受付
    ↓
約定
    ↓
ブローカー建玉の作成
    ↓
決済

さらに、実際の注文では、注文拒否や通信エラー、部分約定なども考慮しなければなりません。

そのため、単純なOPENCLOSEDだけでは状態を管理できなくなりました。

実際にLIVE運用へ進む中で、注文失敗への対応、約定・建玉の同期、IFDOCO注文、tickerの確認、週末決済など、バックテスト時には必要なかった機能を段階的に追加しています。

注文前に建玉上限と重複エントリーを確認する

売買シグナルが発生しても、必ず注文を送るわけではありません。

取引候補を作成した後、すでに保有しているポジションや注文待ちのポジションを確認し、新たにエントリーできるか判断します。

例えば、共通の取引基盤では、

  • 全Botを合わせた最大ポジション数
  • 同一通貨ペアの最大ポジション数
  • 戦略ごとの最大建玉数

などを制限できます。

ここで重要なのが、OPENだけでなくPENDING_OPENも含めて数えることです。

例えば最大2ポジションまでの場合、

OPEN:1件
PENDING_OPEN:1件

合計:2件
    ↓
新規エントリーを見送る

となります。

注文受付直後のPENDING_OPENを数えなければ、約定確認を待っている間に別の注文が送られ、想定以上のポジションを保有する可能性があります。

さらに、同じシグナルから複数回エントリーしないため、シグナル時刻も管理しています。

定期実行型のBotでは、同じ確定足を複数回評価する可能性があります。

そこで、Positionへシグナル時刻を保存し、同じシグナルからすでにポジションが作られている場合は、新しいエントリーを行いません。

つまり、

建玉上限はリスク管理、シグナル時刻は重複注文防止

という異なる目的の安全装置として使っています。

注文受付・約定・建玉を別の状態として管理する

バックテストでは、エントリー条件を満たした時点でPositionをOPENにできます。

LIVEではそうはいきません。

APIから「注文を受け付けた」というレスポンスが返っても、まだ約定していない可能性があるためです。

そのため、現在は主に次の状態を使い分けています。

PENDING_OPEN
OPEN
CLOSED

LIVE注文が受け付けられた直後は、PENDING_OPENとして保存します。

注文送信
    ↓
注文受付
    ↓
PENDING_OPEN

その後、GMO FXから約定情報を取得し、実際に注文が成立したかを確認します。

部分約定の場合は、PositionをすぐにOPENへ変更しません。

例えば1,000通貨を注文し、

1回目:400通貨約定
    ↓
PENDING_OPENを維持

2回目:600通貨約定
    ↓
合計1,000通貨
    ↓
OPENへ変更

というように、発注した数量がすべて約定してからOPENへ移します。

複数回に分かれて約定した場合は、各約定の価格と数量から平均約定価格を計算し、Positionのエントリー価格へ反映します。

このように注文受付と約定を分けることで、注文が受け付けられただけの状態を「ポジション保有」と誤認しないようにしています。

ローカルポジションとGMO FXの建玉を同期する

LIVE運用では、ローカルに保存したPositionだけを正しい状態として扱うことはできません。

実際の注文や建玉はGMO FX側で管理されているためです。

そこで、ローカルPositionとGMO FX上の注文・約定・建玉を対応付けられるよう、複数のIDを記録しています。

主に使用するのは、

  • ローカルPositionのID
  • クライアント注文ID
  • GMO FX側の注文ID
  • 約定ID
  • 建玉ID

などです。

概念的には、次のようにつながります。

ローカルPosition
    ↓
注文
    ↓
約定
    ↓
GMO FXの建玉

特に重要なのが、ブローカー側の建玉IDです。

例えば金曜の強制決済を行う場合、「USD/JPYを決済する」だけでは不十分です。

複数の建玉が存在する可能性があるため、どの建玉を何通貨決済するのかを正しく指定する必要があります。

そのため、GMO FX側で作成された建玉とローカルPositionを同期し、対応関係を保持しています。

さらに、定期的に、

  • ローカルPosition
  • TradeEvent
  • GMO FX上の建玉

を比較し、状態に差がないか確認しています。

不一致が見つかった場合はDiscordへ警告を送り、早めに気づけるようにしています。

IFDOCO注文と毎分の決済監視を組み合わせる

TP・SLを設定するLIVE注文では、GMO FXへIFDOCO注文を送ります。

IFDOCOでは、新規エントリーと、その後の利確・損切り注文をまとめて発注できます。

LONGであれば、

新規:BUY
利確:SELL
損切り:SELL

SHORTなら反対です。

新規:SELL
利確:BUY
損切り:BUY

TP・SLをGMO FX側にも置く理由は、Botが停止した場合でも決済注文を残しておくためです。

すべての決済をPython側だけで管理すると、コンテナ停止や通信障害などが発生した際に、決済処理を実行できない可能性があります。

ブローカー側へTP・SLを登録しておくことで、そのリスクを小さくできます。

ただし、IFDOCOを送信すればBot側の処理が終わるわけではありません。

ローカルPositionを最新の状態に保つため、決済監視ジョブを毎分実行しています。

Bot側では、

毎分
  ↓
約定情報を確認
  ↓
建玉情報を確認
  ↓
Positionを更新
  ↓
必要に応じて通知

という処理を行います。

PAPERでは、1分足を使ってTP・SLへの到達を判定します。

価格系列についても、

LONG決済:BID
SHORT決済:ASK

と、実際に決済できる側の価格を使用します。

つまり、

GMO FX側ではTP・SLを保持し、Bot側では状態を監視・同期する

という二重の構成です。

金曜決済で週末持ち越しを防ぐ

戦略によっては、ポジションを週末まで持ち越さないルールを設けています。

例えば、

金曜日19時以降:新規エントリーを停止
金曜日22時  :保有ポジションを決済
土曜日    :新規エントリーを停止

といった設定です。

具体的な時刻は、戦略ごとのYAMLで管理します。

週末持ち越しを避ける理由は、土日の間に相場へ影響する出来事が発生した場合、週明けの価格が金曜日の終値から大きく離れる可能性があるためです。

通常のSL価格で決済できるとは限らないため、戦略上のリスクとして週末持ち越しを制限しています。

LIVE運用では、「金曜22時になったらPositionをCLOSEDにする」だけではありません。

実際にGMO FX上の建玉を決済する必要があります。

大まかな流れは次のとおりです。

金曜22時
    ↓
対象となるGMO FXの建玉を確認
    ↓
残っている決済注文を確認・キャンセル
    ↓
対象建玉へ決済注文を送る
    ↓
決済約定を確認
    ↓
ローカルPositionをCLOSEDへ変更

決済注文を送信しただけでは、すぐにローカルPositionをCLOSEDにはしません。

GMO FX側で決済の成立を確認してから状態を更新します。

また、決済監視ジョブは毎分動くため、同じポジションへ金曜決済注文を繰り返し送らない仕組みも必要です。

そのため、すでに決済注文を送信済みかどうかをイベント履歴から確認します。

バックテストでは、「金曜22時に決済する」という一つのルールで済みます。

しかしLIVE運用では、

対象建玉の特定 → 注文の整理 → 決済注文 → 約定確認 → ローカル状態更新

まで必要になります。

この違いは、実際に自動売買BotをLIVE運用してみて特に大きく感じた部分です。

売買ロジックを作るだけでなく、「現実の注文が今どうなっているのか」を正しく管理することが、LIVE運用では重要になります。

Dockerと定期実行でBotを動かす

fx-trade-botは、ローカルPCへPythonやライブラリを直接インストールして動かすのではなく、Docker上で実行しています。

自動売買Botは、一度処理を実行すれば終わりではありません。

ローソク足の取得、エントリー判定、決済監視などを決められた間隔で繰り返し、長期間安定して動かす必要があります。

そこで、現在は次の2つを組み合わせています。

Docker Compose
    └─ 実行環境を統一する

Supercronic
    └─ Pythonジョブを決められた時刻に実行する

一つのPythonプロセスを常駐させて、すべての処理を無限ループで動かす構成にはしていません。

ローソク足取得、エントリー判定、決済監視などを独立したジョブに分け、それぞれが1回分の処理を実行して終了する構成にしています。

Docker Composeで実行環境を統一する

Pythonのプログラムは、Python本体やライブラリのバージョンによって動作が変わる場合があります。

fx-trade-botでは、pandasやPyArrowを使ってParquetファイルを扱うため、開発環境と運用環境でライブラリのバージョンが異なると、データ型やファイル読み込みの挙動に差が出る可能性があります。

そこで、Dockerfileを使って実行環境を固定しています。

主な構成は次のとおりです。

  • Python 3.12
  • requests
  • NumPy
  • pandas
  • PyArrow
  • Supercronic
  • タイムゾーン:Asia/Tokyo

Pythonパッケージはpyproject.tomlで管理し、Dockerイメージのビルド時にインストールします。

Docker Composeでは、主に手動実行用と定期実行用のサービスを分けています。

services:
  bot:
    profiles: ["manual"]

  scheduler:
    # Supercronicを起動して定期実行

botは、単発でジョブを実行したい場合や動作確認に利用します。

通常の運用ではschedulerを起動し、Supercronicから各Pythonジョブを定期的に実行します。

また、ソースコード、設定ファイル、運用データは役割に応じてマウント方法を分けています。

backend  :読み取り専用
configs  :読み取り専用
data     :読み書き可能
cron設定 :読み取り専用

Botが実行中にソースコードや戦略設定を書き換える必要はないため、backendconfigsは読み取り専用にしています。

一方、dataにはローソク足、Position、TradeEventなど、運用中に更新されるデータを保存するため、書き込みを許可しています。

GMO FXのAPI認証情報やDiscordのWebhook URLも、DockerイメージやYAMLへ直接書き込みません。

秘密情報は環境変数としてコンテナへ渡し、

アプリケーションコード、戦略設定、秘密情報、運用データをそれぞれ分けて管理する

構成にしています。

定期運用を開始する場合は、例えば次のようにschedulerを起動します。

docker compose up -d --build scheduler

ログもDocker Composeから確認できます。

docker compose logs -f scheduler

Dockerを利用することで、ローカルPC側のPython環境に依存せず、同じ実行環境でBotを動かせるようになりました。

Supercronicで各ジョブを定期実行する

コンテナ内の定期実行にはSupercronicを利用しています。

Supercronicはcron形式でジョブをスケジュールでき、標準出力や標準エラーをコンテナログへ出力できるため、Dockerとの相性がよいと感じています。

スケジュールは、ops/supercronic/fx-trade-bot.crontabで管理しています。

現在の主な実行タイミングは次のとおりです。

処理実行タイミング
ローソク足取得毎時2・17・32・47分
エントリー判定毎時6・21・36・51分
決済監視毎分
LIVE整合性確認毎時25分
ハートビート2時間ごと
ポジションレポート毎日22時

ローソク足取得とエントリー判定は、意図的に実行時刻をずらしています。

ローソク足取得を、

2分・17分・32分・47分

に実行し、エントリー判定はその4分後です。

6分・21分・36分・51分

この間に、GMO FXからデータを取得し、Parquetへの保存を完了させます。

取得処理とシグナル判定を同時に動かさないことで、更新途中のデータを戦略が読み込む可能性を減らしています。

決済監視は、エントリー判定より短い間隔で実行します。

エントリー戦略が15分足を中心にしていても、TP・SLや約定同期、金曜決済まで15分ごとに確認する必要はありません。

そのため、決済ジョブは毎分実行しています。

運用監視も売買処理とは分けています。

LIVE整合性確認では、

  • ローカルPosition
  • TradeEvent
  • GMO FX上の建玉

を比較します。

ハートビートではBotが動作していることをDiscordへ通知し、ポジションレポートでは現在の保有状況を確認します。

各ジョブは、処理が終わるとプロセスを終了します。

Supercronicがジョブを起動
    ↓
Pythonが1回分の処理を実行
    ↓
ログを出力
    ↓
プロセス終了
    ↓
次回のスケジュールを待つ

一つの常駐プロセスの中で複数のタイマーを管理しないため、どの処理で問題が起きたのかを切り分けやすい点もメリットです。

また、将来的にクラウドへ移行する場合も、1回実行型のジョブは流用しやすいと考えています。

例えばSupercronicの代わりに、EventBridge SchedulerからECS/Fargate Taskを起動する構成へ変更しても、Pythonジョブ自体はそのまま使いやすい形です。

ローソク足取得・エントリー・決済を別ジョブにする

ローソク足取得、エントリー判定、決済監視を分けているのは、それぞれ必要な実行周期と役割が異なるためです。

ローソク足取得ジョブは、GMO FXから最新データを取得し、保存することだけを担当します。

GMO FX API
    ↓
BID・ASKローソク足を取得
    ↓
既存データと結合
    ↓
重複を削除
    ↓
Parquetへ保存

このジョブでは、売買条件の判定は行いません。

エントリージョブは、保存済みローソク足とYAML設定を読み込み、有効なBotを順番に実行します。

YAMLを読み込む
    ↓
enabled: trueのBotを選ぶ
    ↓
戦略Runnerでシグナル判定
    ↓
TradeCandidateを作る
    ↓
Positionへ変換
    ↓
建玉上限を確認
    ↓
PAPERまたはLIVEで実行

Botごとに例外を処理しているため、一つの戦略でエラーが発生しても、後続のBotまで停止しない構成です。

決済ジョブは、すでに作成されたPositionの管理を担当します。

OPEN・PENDING_OPENを読み込む
    ↓
LIVEなら約定・建玉を同期
    ↓
TP・SL・金曜決済を確認
    ↓
Positionを更新
    ↓
必要に応じて通知

このように分離することで、エントリーは15分ごと、決済は毎分というように、用途に応じた実行周期を設定できます。

また、障害の影響範囲を限定しやすい点もメリットです。

例えば、一度ローソク足取得に失敗しても、決済監視は別ジョブとして動き続けられます。

反対に、エントリー判定で一つの戦略に問題が起きても、既存ポジションの監視まで停止するわけではありません。

ただし、ジョブを分けるだけで安全になるわけではありません。

前回のエントリー処理が終わる前に次の処理が開始された場合、同じシグナルから重複注文が発生する可能性があります。

そのため、OPENだけでなくPENDING_OPENも建玉数へ含め、シグナル時刻もPositionへ保存しています。

ジョブを分離することと、重複実行への対策を組み合わせることで、定期実行型のBotとして運用しています。

現在の課題と今後の改善

現在のfx-trade-botは、ローソク足の取得からバックテスト、PAPER運用、LIVE注文、約定・建玉同期、決済監視まで一通り動かせる状態になっています。

一方で、Botや取引件数が増えると、現在のシンプルな構成では管理しにくくなる部分もあります。

今後は、現在の構成を活かしながら、運用規模に合わせて必要な部分を改善していく予定です。

ポジションとイベント履歴のデータベース化を検討する

現在は、PositionをJSON、TradeEventをJSONLで保存しています。

小規模な運用では内容を直接確認でき、構成もシンプルなので扱いやすい方法です。

一方、Botや取引件数が増えると、ファイル全体の読み書きやイベント検索、複数ジョブからの同時更新が課題になります。

将来的にはSQLiteやPostgreSQLなどを利用し、Positionや注文、約定、実行履歴を管理しやすい構成へ移行することも検討しています。

バックテストとLIVEのロジック差をさらに減らす

PAPERとLIVEでは同じ戦略ロジックを利用していますが、バックテストとLIVEでは、価格の扱いや決済処理などに一部違いがあります。

例えば、LIVEではシグナル価格とは別に最新tickerを確認し、実際の約定価格も注文価格と一致するとは限りません。

こうした実運用特有の差は残りますが、シグナル判定、BID・ASKの使い分け、TP・SL計算、週末ルールなどはできるだけ共通化し、検証結果とLIVE運用の差を減らしていきたいと考えています。

戦略が増えても運用・監視しやすい構成に改善する

現在は複数の戦略を同じ取引基盤で動かし、Discord通知やログから状態を確認しています。

ただし、Bot数が増えると、どのBotが動いているのか、最後に正常実行されたのはいつか、現在の建玉や損益はどうなっているか、といった情報をまとめて確認したくなります。

今後はBotごとの実行履歴や成績を記録し、バックテスト結果との比較や異常検知まで行える仕組みを追加する予定です。複数戦略を「動かす」だけでなく、「安全に管理できる」構成を目指します。

-FX / EA, Python, 個人開発