FX自動売買 Python

FXの一目均衡表でおすすめの設定は?5通貨3,080ケースで検証

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

FXで一目均衡表を使う際、

  • 一般的な9・26・52でいいの?
  • 通貨ペアによって設定は変えた方がいいの?
  • LONG・SHORTで相性は変わるの?

と疑問に思う方も多いのではないでしょうか。

一目均衡表では9・26・52がよく使われますが、すべての通貨やLONG・SHORTで最適とは限りません。

そこで本記事では、USDJPY・EURJPY・GBPJPY・EURUSD・AUDUSDの5通貨を対象に、15分足で一目均衡表の設定を検証しました。

転換線8種類・基準線7種類・先行スパン2 11種類を組み合わせ、5通貨合計3,080ケースを比較します。

決済条件はTP50pips・SL30pipsに固定し、合計pipsだけでなく、PF・最大ドローダウン・勝率・取引回数なども確認します。

先に結果を確認したい方は、「5通貨で一目均衡表3,080ケースを検証」からご覧ください。

今回のバックテストでは、GMOコインFX APIから取得した15分足のCSVデータを使用しています。

ローソク足データの取得方法は、以下の記事で詳しく紹介しています。

また、今回使用したバックテストスクリプトは以下です。

run_ichimoku_backtest.py
"""5通貨の15分足で一目均衡表616設定を一括検証する。

三役好転・三役逆転を確認しながら価格が雲を抜けた確定足をシグナルとし、
次の15分足の始値で約定する。出口は全設定でTP50・SL30pipsに固定する。
現在位置から見える雲には、基準線期間前に計算された先行スパンを使用する。
標準ライブラリだけで実行できる。
"""

import csv
import time
from collections import deque
from datetime import date, datetime, timedelta
from pathlib import Path


# 使い方:
# 1. fetch_gmo_candles.pyで、5通貨の15分足を取得します。
# 2. GMO-APIフォルダで次のコマンドを実行します。
#    python backtest_research/ichimoku_cloud/run_ichimoku_backtest.py
# 3. 結果は、このファイルと同じフォルダにあるresultsへ保存されます。

# 一目均衡表の計算、雲抜け判定、売買には15分足だけを使用します。
ENTRY_TIMEFRAME = "15min"
ENTRY_TIMEFRAME_DURATION = timedelta(minutes=15)

# 既存のローソク足CSVに合わせ、ASK価格を読み込みます。
# BID価格、スプレッド、手数料、スリッページは考慮しません。
PRICE_TYPE = "ASK"

# バックテストで使用する期間です。
# CSV内のtimestamp_jstの日付が、この開始日から終了日までの足を検証します。
# 開始日と終了日のローソク足も対象に含みます。
BACKTEST_START_DATE = date(2024, 1, 1)
BACKTEST_END_DATE = date(2026, 7, 31)

# 検証する5通貨と、買い・売りの2方向です。
SYMBOLS = ("USD_JPY", "EUR_JPY", "GBP_JPY", "EUR_USD", "AUD_USD")
SIDES = ("LONG", "SHORT")

# 8 × 7 × 11 = 616設定。標準設定9・26・52を必ず含みます。
# TP/SLやエントリー条件は固定し、一目均衡表の期間だけを比較します。
TENKAN_PERIODS = (5, 7, 9, 11, 13, 15, 17, 19)
KIJUN_PERIODS = (20, 23, 26, 29, 32, 35, 38)
SENKOU_B_PERIODS = (40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80)
TAKE_PROFIT_PIPS = 50
STOP_LOSS_PIPS = 30

# 先行スパンのシフト幅と遅行スパンの比較幅は、各設定の基準線期間とします。
# 最大の基準線期間と先行スパン2期間を足し、全設定で計算開始位置を揃えます。
MAX_WARMUP_BARS = max(KIJUN_PERIODS) + max(SENKOU_B_PERIODS)

# __file__は、このPythonファイル自身の場所を表します。
# どのフォルダから実行しても、同じ入力・出力先を参照できます。
SCRIPT_DIR = Path(__file__).resolve().parent
GMO_API_DIR = SCRIPT_DIR.parents[1]
DEFAULT_DATA_DIR = GMO_API_DIR / "data" / "candles"
DEFAULT_OUTPUT_DIR = SCRIPT_DIR / "results"

# 一目均衡表設定ごとの集計結果に保存する列です。
RESULT_COLUMNS = (
    "通貨ペア", "売買方向", "転換線期間", "基準線期間", "先行スパン2期間",
    "シフト期間", "エントリー方式", "TP(pips)", "SL(pips)", "取引回数",
    "勝ち取引数", "負け取引数", "勝率(%)", "合計損益(pips)",
    "平均損益(pips)", "プロフィットファクター",
    "最大ドローダウン(pips)", "平均保有時間(分)", "最終足決済数",
    "全期間プラス月数", "全期間マイナス月数", "直近6か月取引回数",
    "直近6か月合計損益(pips)", "直近6か月平均損益(pips)",
    "直近6か月プロフィットファクター", "直近6か月プラス月数",
    "直近6か月マイナス月数", "記事候補条件合格",
)

# 記事候補として採用するための最低条件です。
# 全期間の成績だけでなく、直近6か月にも利益が残る設定へ絞ります。
MIN_CANDIDATE_TRADES = 100
MIN_CANDIDATE_PROFIT_FACTOR = 1.05
MIN_RECENT_POSITIVE_MONTHS = 3


def iter_months(start_date, end_date):
    """開始月から終了月までをYYYY-MM形式で返す。"""
    result = []
    year, month = start_date.year, start_date.month

    # 12月の次を翌年1月へ進めながら、検証対象月を順番に作ります。
    while (year, month) <= (end_date.year, end_date.month):
        result.append(f"{year:04d}-{month:02d}")
        month += 1
        if month == 13:
            year, month = year + 1, 1
    return result


# 全期間の対象月と、検証終了月を含む直近6か月です。
BACKTEST_MONTHS = tuple(iter_months(BACKTEST_START_DATE, BACKTEST_END_DATE))
RECENT_SIX_MONTHS = frozenset(BACKTEST_MONTHS[-6:])


def load_candles(path):
    """検証前の計算用足と検証期間のOHLCを読み込む。"""
    # 検証開始前の足は指標計算だけに使い、売買には使用しません。
    # dequeは上限を超えると古い足を捨てるため、必要本数だけを保持できます。
    warmup = deque(maxlen=MAX_WARMUP_BARS)
    backtest = []

    # utf-8-sigを使うと、Excel向けのBOM付きCSVも読み込めます。
    with path.open("r", encoding="utf-8-sig", newline="") as file:
        for row in csv.DictReader(file):
            timestamp = datetime.fromisoformat(row["timestamp_jst"])

            # 検証終了日より後の足は、指標計算にも売買にも使いません。
            if timestamp.date() > BACKTEST_END_DATE:
                continue

            # CSVの文字列を、計算に使えるdatetimeとfloatへ変換します。
            candle = {
                "open_time": timestamp,
                "close_time": timestamp + ENTRY_TIMEFRAME_DURATION,
                "open": float(row["open"]),
                "high": float(row["high"]),
                "low": float(row["low"]),
                "close": float(row["close"]),
            }

            # 開始日より前なら計算用、それ以降なら検証対象として分けます。
            (warmup if timestamp.date() < BACKTEST_START_DATE else backtest).append(candle)
    return list(warmup) + backtest, len(warmup), len(backtest)


def rolling_midpoint(candles, period):
    """直近period本の最高値と最安値の中間値を返す。"""
    # period本が揃う前は計算できないため、先頭をNoneで埋めます。
    values = [None] * len(candles)
    for index in range(period - 1, len(candles)):
        # 現在足を含む直近period本だけを計算窓として取り出します。
        window = candles[index - period + 1:index + 1]

        # 転換線・基準線・先行スパン2は、期間内最高値と最安値の中間です。
        values[index] = (
            max(candle["high"] for candle in window)
            + min(candle["low"] for candle in window)
        ) / 2.0
    return values


def build_ichimoku(candles, tenkan_period, kijun_period, senkou_b_period, cache=None):
    """現在足で利用可能な転換線・基準線・雲・遅行比較値を作る。"""
    # 一目均衡表の役割が逆転しない期間の組み合わせだけを許可します。
    if not (0 < tenkan_period < kijun_period < senkou_b_period):
        raise ValueError("期間は 0 < 転換線 < 基準線 < 先行スパン2 にしてください")
    cache = {} if cache is None else cache

    def midpoint(period):
        # 同じ期間の中間値は複数設定で共有し、繰り返し計算を避けます。
        if period not in cache:
            cache[period] = rolling_midpoint(candles, period)
        return cache[period]

    tenkan = midpoint(tenkan_period)
    kijun = midpoint(kijun_period)
    span_b_source = midpoint(senkou_b_period)
    cloud_a = [None] * len(candles)
    cloud_b = [None] * len(candles)
    lagging_reference = [None] * len(candles)
    for index in range(kijun_period, len(candles)):
        # 現在位置の雲には、基準線期間前に計算された値が表示されます。
        # 現在足の値をそのまま雲に使わないことで、表示位置を正しく再現します。
        source_index = index - kijun_period

        # 先行スパン1 =(転換線 + 基準線)÷ 2 です。
        if tenkan[source_index] is not None and kijun[source_index] is not None:
            cloud_a[index] = (tenkan[source_index] + kijun[source_index]) / 2.0

        # 先行スパン2は長期期間の中間値です。
        # 遅行スパン相当の判定では、現在終値と基準線期間前の終値を比較します。
        cloud_b[index] = span_b_source[source_index]
        lagging_reference[index] = candles[source_index]["close"]
    return tenkan, kijun, cloud_a, cloud_b, lagging_reference


def make_trade(position, candle, exit_price, reason, side, pip_size):
    """決済済み取引の損益・保有時間・決済情報を作る。"""
    # LONGは値上がり、SHORTは値下がりしたときにpipsがプラスになります。
    direction = 1.0 if side == "LONG" else -1.0
    return {
        "損益(pips)": direction * (exit_price - position["price"]) / pip_size,
        # 日時差から求めるため、土日やデータ欠損をまたぐ時間も含まれます。
        "保有時間(分)": (candle["close_time"] - position["time"]).total_seconds() / 60,
        "決済理由": reason,
        "決済時刻": candle["open_time"],
    }


def collect_trades(candles, indicator, side, pip_size, entry_start_index):
    """三役が同方向に揃った雲抜けを、次足始値で売買する。"""
    tenkan, kijun, cloud_a, cloud_b, lagging_reference = indicator

    # positionがNoneなら未保有です。
    # pending_entryは確定足のシグナルを次足始値で実行する予約です。
    position = None
    pending_entry = False
    trades = []
    for index in range(1, len(candles)):
        # 前足で確定したシグナルだけを、現在足の始値で約定します。
        # シグナル足の終値で約定させないことで未来参照を防ぎます。
        if position is None and pending_entry and index >= entry_start_index:
            position = {"time": candles[index]["open_time"], "price": candles[index]["open"]}
        pending_entry = False

        # ポジション保有中は、現在足の高値・安値でTP/SL到達を確認します。
        if position is not None:
            tp = TAKE_PROFIT_PIPS * pip_size
            sl = STOP_LOSS_PIPS * pip_size
            if side == "LONG":
                tp_price, sl_price = position["price"] + tp, position["price"] - sl
                tp_hit = candles[index]["high"] >= tp_price
                sl_hit = candles[index]["low"] <= sl_price
            else:
                tp_price, sl_price = position["price"] - tp, position["price"] + sl
                tp_hit = candles[index]["low"] <= tp_price
                sl_hit = candles[index]["high"] >= sl_price
            # OHLCだけでは足内の到達順が分からないため、同時到達時はSLを優先します。
            if sl_hit:
                trades.append(make_trade(position, candles[index], sl_price, "SL", side, pip_size))
                position = None
            elif tp_hit:
                trades.append(make_trade(position, candles[index], tp_price, "TP", side, pip_size))
                position = None

        # 雲抜け判定に必要な値がすべて計算済みか確認します。
        required = (
            tenkan[index], kijun[index], cloud_a[index], cloud_b[index],
            lagging_reference[index], cloud_a[index - 1], cloud_b[index - 1],
        )
        if position is not None or any(value is None for value in required):
            continue
        close = candles[index]["close"]
        previous_close = candles[index - 1]["close"]
        cloud_top = max(cloud_a[index], cloud_b[index])
        cloud_bottom = min(cloud_a[index], cloud_b[index])
        previous_top = max(cloud_a[index - 1], cloud_b[index - 1])
        previous_bottom = min(cloud_a[index - 1], cloud_b[index - 1])
        if side == "LONG":
            # LONGは価格の雲上抜けをトリガーに、転換線と遅行条件も確認します。
            # 転換線のクロスそのものを要求せず、基準線より上なら条件成立です。
            signal = (
                previous_close <= previous_top and close > cloud_top
                and tenkan[index] > kijun[index]
                and close > lagging_reference[index]
            )
        else:
            # SHORTはLONGと反対に、価格の雲下抜けと三役逆転を確認します。
            signal = (
                previous_close >= previous_bottom and close < cloud_bottom
                and tenkan[index] < kijun[index]
                and close < lagging_reference[index]
            )
        if signal and index >= entry_start_index:
            # この足では約定せず、次のループの足始値で入ります。
            pending_entry = True

    # 最終足までポジションが残った場合は、最終足終値で強制決済します。
    if position is not None:
        trades.append(make_trade(position, candles[-1], candles[-1]["close"], "データ終了", side, pip_size))
    return trades


def calculate_metrics(trades):
    """指定された取引群の基本成績を計算する。"""
    profits = [trade["損益(pips)"] for trade in trades]

    # 0pipsは勝ちにも負けにも含めませんが、取引回数には含めます。
    wins = [value for value in profits if value > 0]
    losses = [value for value in profits if value < 0]

    # 累積pipsの過去最高値からの最大下落幅を求めます。
    equity = peak = max_drawdown = 0.0
    for profit in profits:
        equity += profit
        peak = max(peak, equity)
        max_drawdown = max(max_drawdown, peak - equity)
    count = len(profits)

    # PF(プロフィットファクター)= 勝ち取引の総利益 ÷ 負け取引の総損失です。
    gross_loss = abs(sum(losses))
    durations = [trade["保有時間(分)"] for trade in trades]
    return {
        "取引回数": count,
        "勝ち取引数": len(wins),
        "負け取引数": len(losses),
        "勝率(%)": round(len(wins) / count * 100, 2) if count else 0.0,
        "合計損益(pips)": round(sum(profits), 2),
        "平均損益(pips)": round(sum(profits) / count, 2) if count else 0.0,
        "プロフィットファクター": round(sum(wins) / gross_loss, 4) if gross_loss else "",
        "最大ドローダウン(pips)": round(max_drawdown, 2),
        "平均保有時間(分)": round(sum(durations) / count, 2) if count else 0.0,
    }


def summarize(symbol, side, setting, trades):
    """1設定の全期間・月別・直近6か月成績をまとめる。"""
    tenkan_period, kijun_period, senkou_b_period = setting
    metrics = calculate_metrics(trades)

    # 決済時刻を基準に、取引0回の月も含めて月別損益を作ります。
    monthly_profits = []
    for month in BACKTEST_MONTHS:
        selected = [t for t in trades if t["決済時刻"].strftime("%Y-%m") == month]
        monthly_profits.append(calculate_metrics(selected)["合計損益(pips)"])
    recent_trades = [
        trade for trade in trades
        if trade["決済時刻"].strftime("%Y-%m") in RECENT_SIX_MONTHS
    ]
    recent = calculate_metrics(recent_trades)
    recent_profits = monthly_profits[-6:]
    pf = metrics["プロフィットファクター"]

    # 記事候補は取引数、全期間PF、直近成績をすべて満たす設定です。
    candidate = (
        metrics["取引回数"] >= MIN_CANDIDATE_TRADES
        and metrics["合計損益(pips)"] > 0
        and pf != "" and pf >= MIN_CANDIDATE_PROFIT_FACTOR
        and recent["合計損益(pips)"] > 0
        and sum(value > 0 for value in recent_profits) >= MIN_RECENT_POSITIVE_MONTHS
    )
    return {
        "通貨ペア": symbol, "売買方向": side,
        "転換線期間": tenkan_period, "基準線期間": kijun_period,
        "先行スパン2期間": senkou_b_period, "シフト期間": kijun_period,
        "エントリー方式": "三役好転・三役逆転の雲抜け",
        "TP(pips)": TAKE_PROFIT_PIPS, "SL(pips)": STOP_LOSS_PIPS,
        **metrics,
        "最終足決済数": sum(t["決済理由"] == "データ終了" for t in trades),
        "全期間プラス月数": sum(value > 0 for value in monthly_profits),
        "全期間マイナス月数": sum(value < 0 for value in monthly_profits),
        "直近6か月取引回数": recent["取引回数"],
        "直近6か月合計損益(pips)": recent["合計損益(pips)"],
        "直近6か月平均損益(pips)": recent["平均損益(pips)"],
        "直近6か月プロフィットファクター": recent["プロフィットファクター"],
        "直近6か月プラス月数": sum(value > 0 for value in recent_profits),
        "直近6か月マイナス月数": sum(value < 0 for value in recent_profits),
        "記事候補条件合格": candidate,
    }


def write_csv(path, rows):
    """結果をExcelでも開きやすいUTF-8 BOM付きCSVへ保存する。"""
    # フォルダが存在しない初回実行時は、自動的に作成します。
    path.parent.mkdir(parents=True, exist_ok=True)

    # utf-8-sigはUTF-8にBOMを付け、Excelの文字化けを起きにくくします。
    with path.open("w", encoding="utf-8-sig", newline="") as file:
        writer = csv.DictWriter(file, fieldnames=RESULT_COLUMNS)
        writer.writeheader()
        writer.writerows(rows)


def number(row, column):
    """CSV出力用の空文字を考慮して集計列を数値化する。"""
    return float(row[column]) if row[column] != "" else 0.0


def format_setting(row):
    """記事で使う「転換線・基準線・先行スパン2」表記を返す。"""
    return f"{row['転換線期間']}・{row['基準線期間']}・{row['先行スパン2期間']}"


def print_article_summary(rows):
    """全結果から記事掲載用のMarkdown表を標準出力する。"""
    # 設定の欠落や重複を見逃さないよう、出力前に全件数を確認します。
    expected_rows = len(SYMBOLS) * len(SIDES) * len(TENKAN_PERIODS) * len(
        KIJUN_PERIODS
    ) * len(SENKOU_B_PERIODS)
    if len(rows) != expected_rows:
        raise ValueError(
            f"結果行数が想定と異なります: {len(rows)}(想定: {expected_rows})"
        )

    print("\n## 通貨・方向別TOP5(候補条件合格後、合計pips順)")
    print("| 通貨 | 方向 | 順位 | 設定 | 取引 | 合計pips | PF | 最大DD |")
    print("|---|---|---:|---:|---:|---:|---:|---:|")
    for symbol in SYMBOLS:
        for side in SIDES:
            selected = [
                row for row in rows
                if row["通貨ペア"] == symbol
                and row["売買方向"] == side
                and row["記事候補条件合格"] is True
            ]
            if not selected:
                print(
                    f"| {symbol.replace('_', '')} | {side} | - | "
                    "候補なし | - | - | - | - |"
                )
                continue

            # 候補条件を通過した設定を合計pips順、同値ならPF順に並べます。
            ranked = sorted(
                selected,
                key=lambda row: (
                    number(row, "合計損益(pips)"),
                    number(row, "プロフィットファクター"),
                ),
                reverse=True,
            )
            for rank, row in enumerate(ranked[:5], start=1):
                print(
                    f"| {symbol.replace('_', '')} | {side} | {rank} | "
                    f"{format_setting(row)} | {row['取引回数']} | "
                    f"{row['合計損益(pips)']} | "
                    f"{row['プロフィットファクター']} | "
                    f"{row['最大ドローダウン(pips)']} |"
                )

    print("\n## 標準設定9・26・52")
    print("| 通貨 | 方向 | 取引 | 合計pips | PF | 最大DD |")
    print("|---|---|---:|---:|---:|---:|")
    standard = [
        row for row in rows
        if row["転換線期間"] == 9
        and row["基準線期間"] == 26
        and row["先行スパン2期間"] == 52
    ]
    for row in standard:
        print(
            f"| {row['通貨ペア'].replace('_', '')} | {row['売買方向']} | "
            f"{row['取引回数']} | {row['合計損益(pips)']} | "
            f"{row['プロフィットファクター']} | "
            f"{row['最大ドローダウン(pips)']} |"
        )

    print("\n## 方向別集計(重複設定の単純合算)")
    print("| 方向 | 取引 | 合計pips | PF中央値 | プラス設定 | 候補合格 |")
    print("|---|---:|---:|---:|---:|---:|")
    settings_per_side = len(rows) // len(SIDES)
    for side in SIDES:
        selected = [row for row in rows if row["売買方向"] == side]
        pfs = sorted(number(row, "プロフィットファクター") for row in selected)
        midpoint = len(pfs) // 2
        median = (pfs[midpoint - 1] + pfs[midpoint]) / 2
        print(
            f"| {side} | {sum(int(row['取引回数']) for row in selected)} | "
            f"{sum(number(row, '合計損益(pips)') for row in selected):.2f} | "
            f"{median:.4f} | "
            f"{sum(number(row, '合計損益(pips)') > 0 for row in selected)}"
            f"/{settings_per_side} | "
            f"{sum(row['記事候補条件合格'] is True for row in selected)} |"
        )


def run_symbol(candles, symbol, entry_start_index, output_dir):
    """1通貨について616設定をLONG・SHORT別に検証する。"""
    # 円を決済通貨にする通貨ペアと、それ以外で1pipsの値が異なります。
    pip_size = 0.01 if symbol.endswith("_JPY") else 0.0001

    # 同じ期間の中間値を設定間で共有するキャッシュです。
    cache = {}
    rows_by_side = {side: [] for side in SIDES}
    for tenkan_period in TENKAN_PERIODS:
        for kijun_period in KIJUN_PERIODS:
            for senkou_b_period in SENKOU_B_PERIODS:
                setting = (tenkan_period, kijun_period, senkou_b_period)
                indicator = build_ichimoku(candles, *setting, cache=cache)
                for side in SIDES:
                    # LONGとSHORTは同じ指標値を使い、売買条件だけを反転します。
                    trades = collect_trades(candles, indicator, side, pip_size, entry_start_index)
                    rows_by_side[side].append(summarize(symbol, side, setting, trades))
    rows = []
    for side in SIDES:
        # 1通貨・1方向の616設定をresult.csvへ保存します。
        write_csv(output_dir / symbol / side / "result.csv", rows_by_side[side])
        rows.extend(rows_by_side[side])
    return rows


def main():
    """5通貨・6,160結果の一目均衡表検証を実行する。"""
    # スクリプト全体の実行時間を測定します。
    started = time.perf_counter()
    settings_count = len(TENKAN_PERIODS) * len(KIJUN_PERIODS) * len(SENKOU_B_PERIODS)
    print(
        f"{len(SYMBOLS)}通貨 × {settings_count}設定 = "
        f"{len(SYMBOLS) * settings_count}設定"
    )
    print(f"LONG・SHORT別の結果行: {len(SYMBOLS) * len(SIDES) * settings_count}ケース")
    print(f"検証期間: {BACKTEST_START_DATE} ~ {BACKTEST_END_DATE}")
    print(f"固定出口: TP{TAKE_PROFIT_PIPS} / SL{STOP_LOSS_PIPS} pips")

    # 通貨・方向別CSVに加え、全結果をまとめるall_rowsも作ります。
    all_rows = []
    for symbol in SYMBOLS:
        # この通貨の15分足ASK CSVを読み込みます。
        path = DEFAULT_DATA_DIR / symbol / f"{ENTRY_TIMEFRAME}_{PRICE_TYPE}.csv"
        if not path.is_file():
            raise SystemExit(f"入力CSVが見つかりません: {path}")
        candles, warmup_count, backtest_count = load_candles(path)
        if backtest_count == 0:
            raise SystemExit(f"{symbol}の検証期間内データがありません")
        if warmup_count >= MAX_WARMUP_BARS:
            # 検証開始前の足が十分なら、指標計算だけに使用します。
            entry_start_index = warmup_count
        elif len(candles) > MAX_WARMUP_BARS:
            # 検証前データがなければ、CSV先頭を全設定共通で除外します。
            entry_start_index = MAX_WARMUP_BARS
            print(f"[WARN] {symbol}: 検証前データ不足のため先頭{MAX_WARMUP_BARS}本を除外")
        else:
            raise SystemExit(f"{symbol}のデータが不足しています: {len(candles)}本")
        print(f"[RUN] {symbol}: {settings_count * 2}ケース / 検証{backtest_count}本")
        all_rows.extend(run_symbol(candles, symbol, entry_start_index, DEFAULT_OUTPUT_DIR))

    # 個別CSVに加え、全6,160結果を一括比較できるCSVも保存します。
    write_csv(DEFAULT_OUTPUT_DIR / "all_results.csv", all_rows)
    print(f"完了: {DEFAULT_OUTPUT_DIR}")
    print(f"出力行数: {len(all_rows)} / 実行時間: {time.perf_counter() - started:.2f}秒")
    # 記事へ貼り付けられるTOP5・標準設定・方向別集計を表示します。
    print_article_summary(all_rows)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

同じように一目均衡表の期間設定を検証してみたい方は、参考にしてみてください。

FXの一目均衡表とは

一目均衡表とは、相場のトレンド方向や勢い、サポート・レジスタンスを判断するためのテクニカル指標です。

主に次の5つで構成されています。

  • 転換線
  • 基準線
  • 先行スパン1
  • 先行スパン2
  • 遅行スパン

さらに、先行スパン1と先行スパン2の間には「」と呼ばれる領域が表示されます。

一目均衡表は複数の線を見るため少し複雑ですが、それぞれの役割を理解すると、相場の方向を判断しやすくなります。

主なポイントは次の5つです。

  • 一目均衡表で分かること
  • 転換線・基準線
  • 先行スパン1・先行スパン2と雲
  • 遅行スパン
  • 一目均衡表の計算方法

1つずつ見ていきましょう。

一目均衡表で分かること

一目均衡表では、主に次のようなことを確認できます。

  • 相場のトレンド方向
  • 短期・中期の値動き
  • サポート・レジスタンス
  • 現在価格と過去価格の位置関係

特に分かりやすいのが、価格と雲の位置関係です。

  • 価格が雲より上:上昇方向
  • 価格が雲より下:下降方向
  • 価格が雲の中:方向感が弱い

このように、一目均衡表は複数の条件から相場全体の方向を見るために使われます。

転換線・基準線とは

転換線と基準線は、短期・中期の値動きを見るための線です。

一般的な設定では、

  • 転換線:9期間
  • 基準線:26期間

が使われます。

計算式は次のとおりです。

転換線 =(過去9期間の最高値+最安値)÷2

基準線 =(過去26期間の最高値+最安値)÷2

例えば、転換線が基準線より上にあれば、短期的には上昇方向が優勢と判断する方法があります。

反対に、転換線が基準線より下にあれば下降方向を意識します。

先行スパン1・先行スパン2と雲とは

一目均衡表の特徴の1つが「雲」です。

雲は、

  • 先行スパン1
  • 先行スパン2

の2本に挟まれた領域です。

一般的には、

  • 価格が雲より上 → 上昇方向
  • 価格が雲より下 → 下降方向
  • 価格が雲の中 → 方向感が弱い

と判断する際に使います。

例えば、上昇中の価格が下落しても雲付近で反発すれば、雲がサポートとして意識されている可能性があります。

なお、雲は未来側に表示されますが、未来の価格を予測しているわけではありません。

現在までの価格から計算した値を、先の位置へずらして表示しています。

遅行スパンとは

遅行スパンとは、現在の終値を過去方向へずらして表示した線です。

一般的には26期間前へ表示します。

例えば現在価格が過去の価格より高ければ、遅行スパンも過去価格より上に位置します。

そのため、

  • 遅行スパンが過去価格より上 → 上昇方向
  • 遅行スパンが過去価格より下 → 下降方向

といった判断に使われます。

また、

  • 転換線が基準線より上
  • 価格が雲より上
  • 遅行スパンが過去価格より上

という条件が揃った状態は「三役好転」と呼ばれます。

反対の状態は「三役逆転」です。

一目均衡表の計算方法

一般的な一目均衡表では、9・26・52という設定が使われます。

項目計算方法
転換線(過去9期間の最高値+最安値)÷2
基準線(過去26期間の最高値+最安値)÷2
先行スパン1(転換線+基準線)÷2
先行スパン2(過去52期間の最高値+最安値)÷2
遅行スパン現在の終値を26期間前に表示

先行スパン1・2は一般的に26期間先へ表示します。

一方、遅行スパンは現在の終値を26期間前へ表示します。

一目均衡表では、移動平均線のように価格の平均を使うのではなく、一定期間の最高値と最安値の中間値を使うのが特徴です。

今回は、この一般的な9・26・52を含めて複数の設定を検証し、どの設定が良い成績になるのかを確認していきます。

今回の一目均衡表検証条件

今回は、5通貨の15分足データを使って、一目均衡表の設定を比較します。

検証条件は次のとおりです。

  • 5通貨を対象
  • 15分足を使用
  • 一目均衡表616設定を比較
  • LONG・SHORTをそれぞれ検証
  • TP50pips
  • SL30pips

5通貨合計では、

616設定 × 5通貨 = 3,080ケース

を検証します。

検証する5通貨・期間・一目均衡表設定

今回検証する通貨は次の5つです。

  • USDJPY
  • EURJPY
  • GBPJPY
  • EURUSD
  • AUDUSD

検証期間は「2024年1月1日~2026年7月31日」です。

時間足はすべて15分足を使用します。

一目均衡表の設定は次のとおりです。

転換線

5 / 7 / 9 / 11 / 13 / 15 / 17 / 19

基準線

20 / 23 / 26 / 29 / 32 / 35 / 38

先行スパン2

40 / 44 / 48 / 52 / 56 / 60 / 64 / 68 / 72 / 76 / 80

組み合わせは「8 × 7 × 11 = 616設定」です。

一般的な9・26・52も検証対象に含めています。

LONG・SHORTのエントリー条件

LONGは、次の条件をすべて満たした場合にエントリーします。

  • 価格が雲を上抜け
  • 転換線が基準線より上
  • 現在の終値が基準線期間前の終値より高い

SHORTは反対です。

  • 価格が雲を下抜け
  • 転換線が基準線より下
  • 現在の終値が基準線期間前の終値より低い

シグナルが確定した後、次の15分足の始値でエントリーします。

TP50・SL30に固定して検証

決済条件はすべて、

  • TP:50pips
  • SL:30pips

に固定します。

今回は一目均衡表の設定差を比較することが目的なので、TP・SLは変更しません。

また、同じ15分足の中でTPとSLの両方に到達した場合は、SLを優先して集計します。

5通貨で一目均衡表3,080ケースを検証

ここからは、5通貨それぞれで成績が良かった一目均衡表の設定を見ていきます。

今回の検証では、各通貨616設定を対象に、LONG・SHORTをそれぞれバックテストしました。

その中から、

  • 取引回数100回以上
  • 合計pipsがプラス
  • PF1.05以上
  • 直近6か月の合計pipsがプラス
  • 直近6か月のうち3か月以上がプラス

という条件を満たした設定を候補とし、合計pipsが大きい順にTOP5を紹介します。

USDJPYで成績が良かった一目均衡表設定

USDJPY LONG:TOP5

順位転換線基準線先行スパン2取引回数合計pipsPF最大DD
1113548412+2,3601.3450730
2173568375+2,3501.3821530
353544438+2,3001.3129620
493540423+2,2701.3206690
5113540413+2,2501.3261710

USDJPYのLONGでは、TOP5すべてで基準線35が使われています。

転換線や先行スパン2の期間には違いがあるものの、基準線だけは35に集中しました。

そのため今回の検証範囲では、USDJPY LONGは標準設定の基準線26よりも、やや長めの基準線35と相性が良かったと考えられます。

また、616設定中606設定が候補条件を通過しているため、特定の1設定だけが偶然良かったわけではなく、今回のエントリー条件自体がUSDJPYのLONGと相性が良かった可能性もあります。

USDJPY SHORT:TOP5

USDJPYのSHORTでは、候補条件を満たした設定は2つだけでした。

順位転換線基準線先行スパン2取引回数合計pipsPF最大DD
1193880311+7501.1351560
2173880325+4901.0829790

どちらも「基準線38・先行スパン2 80」という長めの設定でした。

ただし、616設定中2設定しか候補条件を通過していないため、USDJPYではSHORTよりもLONGの方が明らかに安定した結果となっています。

EURJPYで成績が良かった一目均衡表設定

EURJPY LONG:TOP5

順位転換線基準線先行スパン2取引回数合計pipsPF最大DD
152964531+2,3901.2638420
272964510+2,3001.2644390
352968527+2,1901.2417440
452640567+2,1901.2232620
592368529+2,1301.2336500

EURJPYでは、上位3設定に基準線29が入りました。

特に、

  • 転換線5〜7
  • 基準線29
  • 先行スパン2 64〜68

付近で好成績となっています。

USDJPYほど特定の基準線に集中してはいませんが、近い設定が複数上位に入っている点は注目です。

EURJPY SHORT:TOP5

EURJPYのSHORTでは、候補条件を満たした設定はありませんでした。

そのため今回の検証条件では、EURJPYもSHORTよりLONGの方が明確に良い結果となっています。

GBPJPYで成績が良かった一目均衡表設定

GBPJPY LONG:TOP5

順位転換線基準線先行スパン2取引回数合計pipsPF最大DD
152968572+3,0001.3125440
2193880432+2,8001.3972350
352956579+2,5501.2576590
4193876428+2,5201.3559320
593572514+2,5001.2874450

GBPJPYでは、今回の5通貨の中で最も大きい+3,000pipsの設定が見つかりました。

また上位を見ると、

  • 転換線5・基準線29付近
  • 転換線19・基準線38付近

という、異なる2つの設定帯が入っています。

特に2位の19・38・80は、+2,800pips、PF1.3972、最大DD350pipsとなっており、利益だけでなくPFや最大DDとのバランスも良い結果でした。

GBPJPY SHORT:TOP5

GBPJPYのSHORTでは、候補条件を満たす設定はありませんでした。

LONGでは379設定が候補条件を通過しているため、今回の検証ではLONGとSHORTで大きな差が出ています。

EURUSDで成績が良かった一目均衡表設定

EURUSD LONG:TOP5

EURUSDのLONGでは、候補条件を満たした設定はありませんでした。

今回の一目均衡表のエントリー条件では、EURUSD LONGとの相性は良くなかったと考えられます。

EURUSD SHORT:TOP5

順位転換線基準線先行スパン2取引回数合計pipsPF最大DD
1192044165+600.51.2107280
2112972309+5701.1022300
3192948275+5501.1111410
4172676271+5101.1043320
5152048306+5001.0901480

EURUSD SHORTの1位は19・20・44で、+600.5pips、PF1.2107となりました。

ただし、JPY3通貨のLONGと比べると合計pipsは小さく、候補設定数も48設定にとどまっています。

そのため、SHORTでプラスになる設定は見つかったものの、JPY3通貨のLONGほど広い設定範囲で安定していたわけではありません。

AUDUSDで成績が良かった一目均衡表設定

AUDUSD LONG:TOP5

AUDUSDのLONGでは、候補条件を通過したのは2設定のみでした。

順位転換線基準線先行スパン2合計pipsPF
152052+6101.1082
272048+3401.0590

候補そのものが少ないため、AUDUSD LONGでは特定の設定だけを見て判断するのは慎重になった方がよさそうです。

AUDUSD SHORT:TOP5

順位転換線基準線先行スパン2取引回数合計pipsPF最大DD
1113880232+7201.1765410
2152360262+7001.1505430
3152356265+6901.1465340
4153880236+6801.1631400
5113580248+643.81.1461430

AUDUSD SHORTでは、上位5設定のうち3設定で先行スパン2 80が使われています。

また、

  • 転換線15・基準線23・先行スパン2 56〜60
  • 基準線35〜38・先行スパン2 80

といった複数の設定帯で比較的良い結果となりました。

今回の検証では、AUDUSDはLONGよりもSHORTとの相性が良かったといえそうです。

5通貨の一目均衡表検証結果を比較

ここまでの検証結果から、各通貨で最も合計pipsが大きかった設定をまとめると次のとおりです。

通貨方向転換線基準線先行スパン2合計pipsPF最大DD
USDJPYLONG113548+2,3601.3450730
USDJPYSHORT193880+7501.1351560
EURJPYLONG52964+2,3901.2638420
GBPJPYLONG52968+3,0001.3125440
EURUSDSHORT192044+600.51.2107280
AUDUSDLONG52052+6101.1082730
AUDUSDSHORT113880+7201.1765410

※EURJPY SHORT・GBPJPY SHORT・EURUSD LONGは、今回の候補条件を満たす設定がありませんでした。

5通貨を比較すると、特に成績が良かったのはUSDJPY・EURJPY・GBPJPYのLONGです。

3通貨とも合計pipsが+2,000pipsを超えており、中でもGBPJPYの「5・29・68」は+3,000pipsと今回の検証で最も大きな利益になりました。

一方、SHORTでは全体的に候補が少なく、USDJPYでは616設定中2設定、EURJPY・GBPJPYでは候補なしとなっています。

ただしEURUSDとAUDUSDでは反対にSHORTの方が良く、通貨によってLONG・SHORTの結果に大きな違いが出ました。

また、設定についても全通貨で共通する1つの組み合わせが最も良かったわけではありません。

例えば、

  • USDJPY LONG:基準線35が上位に集中
  • EURJPY LONG:転換線5〜7・基準線29付近が好成績
  • GBPJPY LONG:5・29付近と19・38付近で好成績
  • AUDUSD SHORT:先行スパン2 80を使った設定が複数上位

と、通貨ごとに特徴が見られました。

この結果から、一目均衡表は「この設定にすればすべての通貨で勝ちやすい」と考えるより、通貨ごとに設定や売買方向との相性を確認することが重要だと分かります。

なお、一般的な9・26・52も今回の検証対象に含めています。

標準設定ではUSDJPY・EURJPY・GBPJPYのLONGが候補条件をクリアしましたが、それぞれの通貨で最も成績が良かった設定は別にありました。

そのため今回の検証では、9・26・52でも一定の成績は確認できたものの、期間を変更することでさらに成績が良くなる設定も見つかったという結果になりました。

一目均衡表をFXで使う際の注意点

一目均衡表をFXで使う際の注意点は、主に次の3つです。

  • 一目均衡表だけで判断せず、他の条件と組み合わせる
  • バックテスト結果は取引回数や直近成績も確認する
  • 実際の取引とバックテストの違いに注意する

1つずつ詳しく見ていきましょう。

一目均衡表だけで判断せず、他の条件と組み合わせる

一目均衡表では、

  • 転換線と基準線
  • 価格と雲
  • 遅行スパンと過去価格

など、複数の情報を確認できます。

ただし、すべての相場で一目均衡表が機能するわけではありません。

例えばレンジ相場では、価格が雲を何度も上下し、上抜け・下抜けを繰り返すことがあります。

その場合、三役好転や三役逆転に近い状態になっても、その後すぐ反対方向へ動く可能性があります。

そのため実際のトレードでは、一目均衡表だけでエントリーを決めるのではなく、

  • ADXでトレンドの強さを確認する
  • ATRで値動きの大きさを確認する
  • 移動平均線で大きな方向を見る
  • 上位足のトレンドを確認する

など、他の条件と組み合わせる方法も検討できます。

今回の検証結果も、一目均衡表単体で必ず利益が出ることを示すものではなく、今後ほかのテクニカル指標と組み合わせるための材料として考えます。

バックテスト結果は取引回数や直近成績も確認する

バックテストでは、合計pipsが最も大きい設定だけを見て判断しないことも重要です。

例えば、

  • 合計pipsは大きいが取引回数が少ない
  • 全期間では好成績だが直近では負けている
  • 1つの設定だけ突出しており、周辺設定は成績が悪い

といった場合は、過去の値動きに偶然合っていた可能性があります。

そのため今回の検証では、合計pipsだけでなく、

  • 取引回数100回以上
  • PF1.05以上
  • 直近6か月の合計pipsがプラス
  • 直近6か月のうち3か月以上がプラス

という条件も使って候補を絞りました。

また、今回3,080ケースを検証したことで、単独の1位だけではなく、近い期間設定でも良い成績が出ているかを確認できます。

例えばUSDJPY LONGでは基準線35を使った設定が複数上位に入るなど、一定の傾向も確認できました。

このように、1つの設定だけでなく周辺設定でも成績が安定しているかを見ることが重要です。

なお、今回好成績だった設定が将来も同じ結果になるとは限りません。

相場環境は変化するため、バックテスト後も直近データや実際の取引で成績を確認する必要があります。

実際の取引とバックテストの違いに注意する

今回のバックテストは、一目均衡表の設定を比較することを目的としているため、実際の取引環境を完全に再現しているわけではありません。

例えば今回は、

  • スプレッド
  • 手数料
  • スリッページ

などを考慮していません。

そのため、実際の取引ではバックテストより利益が小さくなったり、損失が大きくなったりする可能性があります。

また、一目均衡表を自分でバックテストする場合は、先行スパンの扱いにも注意が必要です。

先行スパン1・2は未来側へずらして表示されるため、実装方法を間違えると、その時点ではまだ分からない未来の情報を売買判断に使ってしまう可能性があります。

これは未来参照(ルックアヘッドバイアス)と呼ばれます。

今回の検証では、その時点で実際に確認できる雲だけを使って判定しています。

Pythonなどで一目均衡表をバックテストする場合も、シグナルが出た時点で本当に利用可能だった情報だけを使っているかを確認することが重要です。

バックテストは設定を比較するうえで便利ですが、実際の取引環境との差を理解したうえで結果を見るようにしましょう。

FXの一目均衡表に関するよくある質問

一目均衡表について、よくある質問をまとめます。

一目均衡表はどの時間足で使える?

一目均衡表は、5分足・15分足・1時間足・4時間足・日足など、さまざまな時間足で使えます。

ただし、時間足が変わると値動きの特徴やシグナルの回数も変わります。

今回の検証では15分足を使用しましたが、1時間足や4時間足では同じ設定でも結果が変わる可能性があります。

一目均衡表の期間は通貨ごとに変えた方がいい?

必ず変える必要はありませんが、今回の検証では通貨ごとに成績が良かった設定は異なりました。

例えばUSDJPYでは基準線35、EURJPYでは基準線29付近の設定が上位に入っています。

そのため、一般的な設定を基準にしつつ、実際に使う通貨でバックテストして比較するのがおすすめです。

一目均衡表は短期トレードでも使える?

一目均衡表は短期トレードでも使用できます。

今回の検証でも15分足を使い、シグナル確定後の次足始値でエントリーしました。

ただし、短い時間足ほど細かな値動きが増えるため、雲の上抜け・下抜けによるダマシが増える可能性があります。

短期足で使う場合は、上位足の方向や他のテクニカル指標も確認するとよいでしょう。

一目均衡表はレンジ相場でも使える?

一目均衡表はレンジ相場でも表示できますが、トレンド相場と比べると使いにくくなることがあります。

レンジ相場では価格が雲を何度も上下し、売買シグナルが頻繁に切り替わることがあるためです。

そのため、価格が雲の中にある場合や方向感が弱い場合は、無理にエントリーしないという使い方も考えられます

一目均衡表は他のテクニカル指標と組み合わせた方がいい?

一目均衡表だけで判断することもできますが、他のテクニカル指標と組み合わせる方法もあります。

例えば、

  • ADXでトレンドの強さを確認する
  • ATRで値動きの大きさを確認する
  • 移動平均線で大きなトレンド方向を確認する

といった組み合わせが考えられます。

今回の検証では一目均衡表だけの条件で比較しましたが、今後EAなどで使用する場合は、他のテクニカル指標と組み合わせることでエントリー条件をさらに絞り込むこともできます。

まとめ

今回は、USDJPY・EURJPY・GBPJPY・EURUSD・AUDUSDの5通貨を対象に、転換線8種類・基準線7種類・先行スパン2 11種類を組み合わせた、3,080ケースを検証しました。

今回の結果から分かった主なポイントは、以下の通りです。

  • 一般的な9・26・52が必ずしも最適とは限らない
  • 通貨ペアやLONG・SHORTによって良い設定が異なる
  • USDJPY・EURJPY・GBPJPYではLONGが比較的好成績
  • EURUSD・AUDUSDではSHORTの方が候補設定が多かった
  • 特定の1設定だけでなく、周辺設定も確認することが重要

今回、合計損益が最も大きかったのは、GBPJPY LONGの転換線5・基準線29・先行スパン2 68で、+3,000pips、PF1.3125となった設定でした。

また、USDJPY LONGでは基準線35、EURJPY LONGでは基準線29付近など、通貨ごとに好成績となる設定の傾向も異なっています。

一方、EURJPY SHORT・GBPJPY SHORT・EURUSD LONGでは、取引回数100回以上・PF1.05以上・直近6か月プラスなどの候補条件をすべて満たす設定はありませんでした。

ただし、今回のバックテストは一目均衡表の設定差を比較することを目的としているため、スプレッド・手数料・スリッページなどの取引コストは考慮していません。

検証期間も2024年1月1日〜2026年7月31日に限定しているため、今回成績が良かった設定が今後も同じように機能するとは限りません。

実際に一目均衡表をトレードへ取り入れる場合は、一般的な9・26・52を基準にしつつ、通貨や時間足に合わせて検証することが重要です。

-FX自動売買, Python