FX / EA Python 個人開発

PythonでGMO FX APIから最新ローソク足を取得する方法|既存CSVへ不足分を追加

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

PythonでFX自動売買を行うには、エントリー判定に使用するローソク足データを、常に最新の状態へ更新しておく必要があります。

しかし、初めてPythonとGMO FX APIを使ってローソク足を更新する場合は、

  • 既存CSVの続きから最新データを取得するにはどうすればいいのか
  • 15分足・1時間足・4時間足・日足をまとめて更新できるのか
  • 取得済みのローソク足が重複して保存されないのか
  • 毎回すべての過去データを取得し直す必要があるのか

といった疑問を持つ方も多いのではないでしょうか。

そこで本記事では、PythonとGMO FX APIを使って、作成済みのローソク足CSVへ不足している最新データを追加する方法を紹介します。

15分足・1時間足・4時間足・日足をまとめて更新できるPythonのサンプルコードも掲載しているので、ぜひ最後までご覧ください!

サンプルコード

↓ ↓ ↓ クリックすると開けます ↓ ↓ ↓

candle_updater.py
import csv
import json
import ssl
import time
from datetime import datetime, timedelta, timezone
from urllib.parse import urlencode
from urllib.request import Request, urlopen

import certifi

import backtest_config as strategy

# 更新するローソク足の時間足です。
# 1時間足以下は日単位、4時間足以上は年単位でGMO APIから取得します。
CANDLE_INTERVALS = ("15min", "1hour", "4hour", "1day")

# CSVに出力する列です。
CSV_COLUMNS = ["timestamp_jst", "open", "high", "low", "close"]

# GMO FXのPublic APIの接続先です。
PUBLIC_URL = "https://forex-api.coin.z.com/public"

# certifiが持っている証明書一覧を使って、HTTPS通信の証明書を検証します。
# Windows環境でCERTIFICATE_VERIFY_FAILEDになる場合の対策です。
SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where())

# GMO APIの時刻はUTCのミリ秒で返るため、CSVにはJSTへ変換して保存します。
JST = timezone(timedelta(hours=9), name="JST")


def public_get(path, params=None):
    """GMO FXのPublic APIへGETリクエストを送る。"""

    # APIに渡すパラメータがある場合は、URL用のクエリ文字列へ変換します。
    query = f"?{urlencode(params)}" if params else ""

    # Public APIの接続先、APIごとのパス、クエリ文字列をつないでURLを作ります。
    request = Request(PUBLIC_URL + path + query)

    # GMO APIへアクセスして、返ってきたJSON文字列をPythonの辞書に変換します。
    # 20秒以内に応答がない場合は、待ち続けずタイムアウトにします。
    with urlopen(request, context=SSL_CONTEXT, timeout=20) as response:
        payload = json.loads(response.read().decode("utf-8"))

    # GMO APIのstatusが0以外なら、エラーとして処理を止めます。
    if int(payload.get("status", -1)) != 0:
        raise RuntimeError(f"GMO FX APIエラー: {payload.get('messages')}")

    # payload["data"]に、APIから取得したデータが入っています。
    return payload.get("data")


def fetch_klines(symbol, price_type, interval, date_key):
    """指定した日付のローソク足を取得し、CSV用の形式へ変換する。"""

    # GMO APIへ連続でアクセスしすぎないように、1秒待ってから呼び出します。
    time.sleep(1.0)

    # 通貨ペア、価格種別、時間足、日付を指定してローソク足を取得します。
    # date_keyは、1時間足以下では"20260102"、4時間足以上では"2026"の形式です。
    rows = public_get(
        "/v1/klines",
        {
            "symbol": symbol,
            "priceType": price_type,
            "interval": interval,
            "date": date_key,
        },
    )

    # APIのレスポンスから、CSVに保存するローソク足をこのリストへまとめます。
    result = []
    for row in rows or []:
        # openTimeはUTCのミリ秒なので、日時へ変換してから日本時間へ直します。
        timestamp = datetime.fromtimestamp(
            int(row["openTime"]) / 1000, tz=timezone.utc
        ).astimezone(JST)

        # CSVに必要な開始日時と四本値だけを取り出します。
        result.append(
            {
                "timestamp_jst": timestamp.isoformat(timespec="seconds"),
                "open": row["open"],
                "high": row["high"],
                "low": row["low"],
                "close": row["close"],
            }
        )

    # CSVへ追加できる形式に変換したローソク足を返します。
    return result


def read_csv(path):
    """既存のローソク足CSVを読み込む。"""

    # 初回実行などでCSVがまだ存在しない場合は、空のデータとして扱います。
    if not path.exists():
        return []

    # utf-8-sigを指定し、BOM付き・BOMなしのどちらのCSVも読み込めるようにします。
    with path.open("r", encoding="utf-8-sig", newline="") as file:
        return list(csv.DictReader(file))


def update_candle_csvs(now):
    """作成済みCSVへ、最新のローソク足を重複なく追加する。"""

    # GMOの1時間足以下の日付キーは、JST午前6時に切り替わります。
    # 午前0時から6時までは前日をAPI上の基準日として扱います。
    business_date = (now - timedelta(hours=6)).date()
    print("ローソク足CSVの差分更新を開始します。")

    # CANDLE_INTERVALSで指定した時間足を1つずつ更新します。
    for interval in CANDLE_INTERVALS:
        # 時間足に対応するCSVの保存先をbacktest_config.pyから取得します。
        path = strategy.CANDLE_CSV_PATHS[interval]

        # 保存済みのローソク足を読み込み、新しく取得した足を後から追加します。
        rows = read_csv(path)
        print(f"[{interval}] のデータ取得中...")

        if interval in ("15min", "1hour"):
            # 1時間足以下は日ごとに取得するため、CSVの最終日時を取得開始日にします。
            # CSVが空の場合は、現在の基準日から取得します。
            start_date = (
                datetime.fromisoformat(rows[-1]["timestamp_jst"]).date()
                if rows
                else business_date
            )

            # 日付境界付近の足を取りこぼさないよう、前日から再取得します。
            start_date = min(start_date, business_date) - timedelta(days=1)

            # APIへ渡す日付を"20260102"のようなYYYYMMDD形式で1日ずつ作ります。
            date_keys = [
                (start_date + timedelta(days=offset)).strftime("%Y%m%d")
                for offset in range((business_date - start_date).days + 1)
            ]
        else:
            # 4時間足以上は年ごとに取得するため、CSVの最終日時から開始年を決めます。
            # CSVが空の場合は、現在の基準年だけを取得します。
            start_year = (
                datetime.fromisoformat(rows[-1]["timestamp_jst"]).year
                if rows
                else business_date.year
            )

            # APIへ渡す日付を"2026"のようなYYYY形式で1年ずつ作ります。
            date_keys = [
                str(year) for year in range(start_year, business_date.year + 1)
            ]

        # 作成した日付または年を順番にAPIへ渡し、ローソク足を取得します。
        for date_key in date_keys:
            fetched_rows = fetch_klines(
                strategy.SYMBOL,
                strategy.PRICE_TYPE,
                interval,
                date_key,
            )

            # 既存のローソク足へ、今回取得したローソク足を追加します。
            rows.extend(fetched_rows)

        # 再取得した同じ開始時刻の足を1本にまとめます。
        # 同じ時刻が複数ある場合は、後からAPIで取得した新しい内容を残します。
        unique_rows = {row["timestamp_jst"]: row for row in rows}

        # CSVが古い足から新しい足の順になるよう、開始日時で並べ替えます。
        rows = sorted(unique_rows.values(), key=lambda row: row["timestamp_jst"])

        # 保存先フォルダが存在しない場合は、親フォルダも含めて作成します。
        path.parent.mkdir(parents=True, exist_ok=True)

        # 更新後の全ローソク足でCSVを上書きします。
        with path.open("w", encoding="utf-8", newline="") as file:
            writer = csv.DictWriter(file, fieldnames=CSV_COLUMNS)

            # 1行目へ列名を書き、その後へローソク足をまとめて書き込みます。
            writer.writeheader()
            writer.writerows(rows)

    print("ローソク足CSVの差分更新が完了しました。")


def main():
    # ローソク足の日付判定を日本時間で行うため、現在時刻をJSTで取得します。
    now = datetime.now(JST)

    # 設定されたすべての時間足について、CSVへ最新データを追加します。
    update_candle_csvs(now)
    print("15分足・1時間足・4時間足・日足のCSVを更新しました。")


# このファイルを直接実行したときだけmain()を動かします。
if __name__ == "__main__":
    main()

GMO FX APIで最新ローソク足を取得する仕組み

GMO FX APIでは、認証不要のPublic APIを利用して、外国為替FXのローソク足データを取得可能です。

取得時には、通貨ペア、BID・ASKの価格種別、15分足や1時間足などの時間足、対象の日付を指定します。

APIからは、ローソク足の開始時刻と始値・高値・安値・終値が返されます。

Pythonから定期的にAPIへアクセスし、取得済みのデータと組み合わせることで、最新のローソク足を継続的に保存が可能です。

今回作成するPythonスクリプトの処理内容

今回のスクリプト(candle_updater.py)では、作成済みのローソク足CSVを読み込み、最後に保存されている日時を確認します。

その日時を基準に、GMO FX APIから不足している期間のローソク足データを取得し、既存CSVへ追加します

15分足・1時間足は日付単位、4時間足・日足は年単位で取得対象を判定可能です。

また、日付の切り替わり付近でデータを取りこぼさないよう、15分足と1時間足では最終日より前の日付から再取得します。

取得したローソク足は既存データと結合し、開始時刻が同じデータを1本にまとめます。

同じ時刻のデータが複数ある場合は、あとからGMO FX APIで取得した内容を残す仕組みです。

最後に、すべてのローソク足を日時順に並べ直し、15分足・1時間足・4時間足・日足のCSVを上書き保存します。

これにより、既存データを残したまま、不足している最新のローソク足を追加が可能です。

最新ローソク足を既存CSVへ追加する3ステップ

ここからは、GMO FX APIから最新のローソク足を取得し、作成済みのCSVへ追加する手順を解説します。

今回のスクリプトを実行するには、事前に以下の2つが必要です。

  • 15分足・1時間足・4時間足・日足のローソク足CSV
  • 通貨ペア・価格種別・CSV保存先を設定したbacktest_config.py

まだ準備できていない場合は、以下の記事を参考に用意してください。

1. ローソク足CSVの作成方法

2. backtest_config.pyの作成方法

なお、今回はバックテストを実行する必要はありません。

バックテストの記事で作成したbacktest_config.pyを用意し、通貨ペア・価格種別・各時間足のCSV保存先が正しいことを確認すれば準備完了です。

準備ができたら、次の3ステップで進めます。

  • candle_updater.pyを用意する
  • Pythonスクリプトを実行する
  • 更新されたCSVを確認する

それでは、1つずつ詳しく見ていきましょう。

ステップ1:candle_updater.pyを用意する

まずは、GMO FX APIからローソク足を取得し、既存CSVへ追加するcandle_updater.pyを用意します。

backtest_config.pyと同じフォルダに、candle_updater.pyという名前で新しいファイルを作成し、今回紹介するサンプルコードを貼り付けて保存してください。

サンプルコード

↓ ↓ ↓ クリックすると開けます ↓ ↓ ↓

candle_updater.py
import csv
import json
import ssl
import time
from datetime import datetime, timedelta, timezone
from urllib.parse import urlencode
from urllib.request import Request, urlopen

import certifi

import backtest_config as strategy

# 更新するローソク足の時間足です。
# 1時間足以下は日単位、4時間足以上は年単位でGMO APIから取得します。
CANDLE_INTERVALS = ("15min", "1hour", "4hour", "1day")

# CSVに出力する列です。
CSV_COLUMNS = ["timestamp_jst", "open", "high", "low", "close"]

# GMO FXのPublic APIの接続先です。
PUBLIC_URL = "https://forex-api.coin.z.com/public"

# certifiが持っている証明書一覧を使って、HTTPS通信の証明書を検証します。
# Windows環境でCERTIFICATE_VERIFY_FAILEDになる場合の対策です。
SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where())

# GMO APIの時刻はUTCのミリ秒で返るため、CSVにはJSTへ変換して保存します。
JST = timezone(timedelta(hours=9), name="JST")


def public_get(path, params=None):
    """GMO FXのPublic APIへGETリクエストを送る。"""

    # APIに渡すパラメータがある場合は、URL用のクエリ文字列へ変換します。
    query = f"?{urlencode(params)}" if params else ""

    # Public APIの接続先、APIごとのパス、クエリ文字列をつないでURLを作ります。
    request = Request(PUBLIC_URL + path + query)

    # GMO APIへアクセスして、返ってきたJSON文字列をPythonの辞書に変換します。
    # 20秒以内に応答がない場合は、待ち続けずタイムアウトにします。
    with urlopen(request, context=SSL_CONTEXT, timeout=20) as response:
        payload = json.loads(response.read().decode("utf-8"))

    # GMO APIのstatusが0以外なら、エラーとして処理を止めます。
    if int(payload.get("status", -1)) != 0:
        raise RuntimeError(f"GMO FX APIエラー: {payload.get('messages')}")

    # payload["data"]に、APIから取得したデータが入っています。
    return payload.get("data")


def fetch_klines(symbol, price_type, interval, date_key):
    """指定した日付のローソク足を取得し、CSV用の形式へ変換する。"""

    # GMO APIへ連続でアクセスしすぎないように、1秒待ってから呼び出します。
    time.sleep(1.0)

    # 通貨ペア、価格種別、時間足、日付を指定してローソク足を取得します。
    # date_keyは、1時間足以下では"20260102"、4時間足以上では"2026"の形式です。
    rows = public_get(
        "/v1/klines",
        {
            "symbol": symbol,
            "priceType": price_type,
            "interval": interval,
            "date": date_key,
        },
    )

    # APIのレスポンスから、CSVに保存するローソク足をこのリストへまとめます。
    result = []
    for row in rows or []:
        # openTimeはUTCのミリ秒なので、日時へ変換してから日本時間へ直します。
        timestamp = datetime.fromtimestamp(
            int(row["openTime"]) / 1000, tz=timezone.utc
        ).astimezone(JST)

        # CSVに必要な開始日時と四本値だけを取り出します。
        result.append(
            {
                "timestamp_jst": timestamp.isoformat(timespec="seconds"),
                "open": row["open"],
                "high": row["high"],
                "low": row["low"],
                "close": row["close"],
            }
        )

    # CSVへ追加できる形式に変換したローソク足を返します。
    return result


def read_csv(path):
    """既存のローソク足CSVを読み込む。"""

    # 初回実行などでCSVがまだ存在しない場合は、空のデータとして扱います。
    if not path.exists():
        return []

    # utf-8-sigを指定し、BOM付き・BOMなしのどちらのCSVも読み込めるようにします。
    with path.open("r", encoding="utf-8-sig", newline="") as file:
        return list(csv.DictReader(file))


def update_candle_csvs(now):
    """作成済みCSVへ、最新のローソク足を重複なく追加する。"""

    # GMOの1時間足以下の日付キーは、JST午前6時に切り替わります。
    # 午前0時から6時までは前日をAPI上の基準日として扱います。
    business_date = (now - timedelta(hours=6)).date()
    print("ローソク足CSVの差分更新を開始します。")

    # CANDLE_INTERVALSで指定した時間足を1つずつ更新します。
    for interval in CANDLE_INTERVALS:
        # 時間足に対応するCSVの保存先をbacktest_config.pyから取得します。
        path = strategy.CANDLE_CSV_PATHS[interval]

        # 保存済みのローソク足を読み込み、新しく取得した足を後から追加します。
        rows = read_csv(path)
        print(f"[{interval}] のデータ取得中...")

        if interval in ("15min", "1hour"):
            # 1時間足以下は日ごとに取得するため、CSVの最終日時を取得開始日にします。
            # CSVが空の場合は、現在の基準日から取得します。
            start_date = (
                datetime.fromisoformat(rows[-1]["timestamp_jst"]).date()
                if rows
                else business_date
            )

            # 日付境界付近の足を取りこぼさないよう、前日から再取得します。
            start_date = min(start_date, business_date) - timedelta(days=1)

            # APIへ渡す日付を"20260102"のようなYYYYMMDD形式で1日ずつ作ります。
            date_keys = [
                (start_date + timedelta(days=offset)).strftime("%Y%m%d")
                for offset in range((business_date - start_date).days + 1)
            ]
        else:
            # 4時間足以上は年ごとに取得するため、CSVの最終日時から開始年を決めます。
            # CSVが空の場合は、現在の基準年だけを取得します。
            start_year = (
                datetime.fromisoformat(rows[-1]["timestamp_jst"]).year
                if rows
                else business_date.year
            )

            # APIへ渡す日付を"2026"のようなYYYY形式で1年ずつ作ります。
            date_keys = [
                str(year) for year in range(start_year, business_date.year + 1)
            ]

        # 作成した日付または年を順番にAPIへ渡し、ローソク足を取得します。
        for date_key in date_keys:
            fetched_rows = fetch_klines(
                strategy.SYMBOL,
                strategy.PRICE_TYPE,
                interval,
                date_key,
            )

            # 既存のローソク足へ、今回取得したローソク足を追加します。
            rows.extend(fetched_rows)

        # 再取得した同じ開始時刻の足を1本にまとめます。
        # 同じ時刻が複数ある場合は、後からAPIで取得した新しい内容を残します。
        unique_rows = {row["timestamp_jst"]: row for row in rows}

        # CSVが古い足から新しい足の順になるよう、開始日時で並べ替えます。
        rows = sorted(unique_rows.values(), key=lambda row: row["timestamp_jst"])

        # 保存先フォルダが存在しない場合は、親フォルダも含めて作成します。
        path.parent.mkdir(parents=True, exist_ok=True)

        # 更新後の全ローソク足でCSVを上書きします。
        with path.open("w", encoding="utf-8", newline="") as file:
            writer = csv.DictWriter(file, fieldnames=CSV_COLUMNS)

            # 1行目へ列名を書き、その後へローソク足をまとめて書き込みます。
            writer.writeheader()
            writer.writerows(rows)

    print("ローソク足CSVの差分更新が完了しました。")


def main():
    # ローソク足の日付判定を日本時間で行うため、現在時刻をJSTで取得します。
    now = datetime.now(JST)

    # 設定されたすべての時間足について、CSVへ最新データを追加します。
    update_candle_csvs(now)
    print("15分足・1時間足・4時間足・日足のCSVを更新しました。")


# このファイルを直接実行したときだけmain()を動かします。
if __name__ == "__main__":
    main()

主なファイル構成は、以下のとおりです。

プロジェクトフォルダ
├─ candle_updater.py
├─ backtest_config.py
└─ data
   └─ candles
      └─ USD_JPY
         ├─ 15min_ASK.csv
         ├─ 1hour_ASK.csv
         ├─ 4hour_ASK.csv
         └─ 1day_ASK.csv

candle_updater.pyでは、次の時間足を更新します。

CANDLE_INTERVALS = ("15min", "1hour", "4hour", "1day")

更新する時間足を変更したい場合は、CANDLE_INTERVALSの内容を変更してください。

ファイルを保存したら、candle_updater.pybacktest_config.pyが同じフォルダにあることを確認しましょう。

ステップ2:Pythonスクリプトを実行する

必要なファイルを用意したら、PowerShellやコマンドプロンプトを開き、candle_updater.pyがあるフォルダへ移動します。

cd プロジェクトフォルダのパス

続いて、以下のコマンドを実行してください。

python candle_updater.py

スクリプトを実行すると、15分足・1時間足・4時間足・日足の順番で、既存CSVへ不足しているローソク足が追加されます。

正常に実行された場合は、以下のような内容が表示されます。

ローソク足CSVの差分更新を開始します。
[15min] のデータ取得中...
[1hour] のデータ取得中...
[4hour] のデータ取得中...
[1day] のデータ取得中...
ローソク足CSVの差分更新が完了しました。
15分足・1時間足・4時間足・日足のCSVを更新しました。

GMO FX APIへ連続でアクセスしすぎないよう、リクエストごとに1秒待機します。

そのため、取得する日付や年が多い場合は、処理の完了まで少し時間がかかることがあります。

エラーが表示された場合は、backtest_config.pyの通貨ペア・価格種別・CSV保存先と、実際に用意したCSVが一致しているか確認してください。

ステップ3:更新されたCSVを確認する

スクリプトの実行が完了したら、backtest_config.pyで指定した保存先のCSVを開きます。

CSVの最後の行を確認し、実行前より新しい日時のローソク足が追加されていれば更新完了です。

timestamp_jst,open,high,low,close
2026-08-06T18:30:00+09:00,147.120,147.180,147.090,147.160
2026-08-06T18:45:00+09:00,147.160,147.210,147.130,147.190

今回のスクリプトでは、既存データと新しく取得したデータを結合したあと、timestamp_jstを基準に重複を削除します。

そのため、既存CSVと重なる期間のローソク足を再取得しても、同じ開始時刻のデータが複数保存されることはありません。

15分足・1時間足・4時間足・日足のすべてについて、最新日時まで正常に更新されているか確認しましょう。

最新ローソク足をCSVへ追加する際の注意点

今回のスクリプトは、更新後のデータで既存CSVを上書きします。

万が一に備えて、初回実行前には元のCSVをコピーしておくと安心です。

また、既存CSVにはtimestamp_jstopenhighlowcloseの列が必要です。

列名やデータ形式が異なる場合は、読み込みや日時の判定でエラーになる可能性があります。

実行前には、backtest_config.pyの通貨ペア・価格種別・CSV保存先が、用意したCSVと一致しているか確認してください。

なお、今回のサンプルには、通信エラーが発生した場合の再試行処理や、取得したローソク足が確定済みかを判定する処理は含まれていません。

まとめ

今回は、PythonとGMO FX APIを使って、作成済みのローソク足CSVへ不足している最新データを追加する方法を解説しました。

今回作成したスクリプトでは、既存CSVの最終日時を確認し、GMO FX APIから不足期間のローソク足を取得可能です。

取得したデータは既存データと結合し、重複を削除したうえで日時順に並べ直し、15分足・1時間足・4時間足・日足のCSVを更新します。

これにより、毎回すべてのローソク足を取得し直さなくても、作成済みのCSVを最新の状態に保てるようになるのです。

次のステップでは、更新したローソク足CSVとバックテストで作成したコンフィグを使い、最新の足でエントリー条件を判定する処理を実装します。

-FX / EA, Python, 個人開発