FX自動売買 Python

FXのCCIでおすすめの設定は?5通貨1,340ケースで検証

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

FXでCCIを使う際、

  • 期間は14・20でいいの?
  • ±100・±200を基準にすればいいの?
  • 通貨ペアによって設定は変えた方がいいの?

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

CCIでは期間14・20、基準値±100・±200などがよく使われますが、すべての通貨やLONG・SHORTで最適とは限りません。

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

CCI期間13種類・基準値11種類から理論上シグナルが発生しない設定を除外し、5通貨・LONG/SHORTの合計1,340ケースを比較します。

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

先に結果を確認したい方は、「5通貨でCCI1,340ケースを検証」からご覧ください。

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

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

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

run_cci_backtest.py
"""5通貨の15分足でCCIの期間・基準値を比較する。

CCIが極端な水準の外側から内側へ戻った確定足をシグナルとし、次の
15分足始値でエントリーする。出口は全ケースでTP50/SL30 pipsに固定する。
"""

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/commodity_channel_index/run_cci_backtest.py
# 3. 結果は、このファイルと同じフォルダにあるresultsへ保存されます。

# CCIの計算、シグナル判定、売買には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")

# CCI期間は、直近何本の値動きを基準に現在価格の離れ具合を測るかを表します。
# 一般的な14・20を含め、短期から長期までを比較します。
CCI_PERIODS = (5, 7, 9, 10, 14, 18, 20, 21, 25, 28, 30, 35, 40)

# 上下対称の基準値です。例えば100なら、LONGは-100、SHORTは+100を使います。
CCI_LEVELS = (50, 75, 100, 125, 150, 175, 200, 225, 250, 275, 300)
# CCIで一般的に使われる補正定数です。値の多くが±100付近に収まりやすくなります。
CCI_CONSTANT = 0.015

# 期間と基準値だけを公平に比較するため、出口は全設定で固定します。
TAKE_PROFIT_PIPS = 50
STOP_LOSS_PIPS = 30

# 期間の短い設定だけ先に取引を開始しないよう、全設定で開始位置を揃える。
WARMUP_BARS = max(CCI_PERIODS)

# __file__を基準にするため、どのフォルダから実行しても入出力先は同じです。
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.csvへ保存する列と、その並び順です。
RESULT_COLUMNS = (
    "通貨ペア", "売買方向", "CCI期間", "CCI基準値", "エントリー方式",
    "CCI理論最大絶対値", "理論上シグナル発生可能",
    "TP(pips)", "SL(pips)", "取引回数", "勝ち取引数", "負け取引数",
    "勝率(%)", "合計損益(pips)", "平均損益(pips)",
    "プロフィットファクター", "最大ドローダウン(pips)",
    "平均保有時間(分)", "最終足決済数", "全期間プラス月数",
    "全期間マイナス月数", "直近6か月取引回数",
    "直近6か月合計損益(pips)", "直近6か月平均損益(pips)",
    "直近6か月プロフィットファクター", "直近6か月プラス月数",
    "直近6か月マイナス月数", "記事候補条件合格",
)

# 月別CSV自体は保存しませんが、月ごとの内部集計を作る際の列順として使います。
MONTHLY_RESULT_COLUMNS = (
    "通貨ペア", "売買方向", "CCI期間", "CCI基準値", "TP(pips)",
    "SL(pips)", "年月", "取引回数", "勝ち取引数", "負け取引数",
    "勝率(%)", "合計損益(pips)", "平均損益(pips)",
    "プロフィットファクター",
)

# 「記事候補条件合格」をTrueにする最低条件です。
# 全期間だけでなく直近6か月も確認し、一時的な好成績を除きやすくします。
MIN_CANDIDATE_TRADES = 100
MIN_CANDIDATE_PROFIT_FACTOR = 1.05
MIN_RECENT_POSITIVE_MONTHS = 3


def load_candles(path):
    """検証開始前のウォームアップ足と検証期間内のOHLCを読む。"""
    # deque(maxlen=40)は上限を超えると古い足を自動的に捨てるため、
    # 検証開始前の全データではなく、CCI計算に必要な直近40本だけを残せます。
    warmup = deque(maxlen=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"]),
            }
            if timestamp.date() < BACKTEST_START_DATE:
                # 検証開始前の足はCCIを準備するためだけに使い、売買には使いません。
                warmup.append(candle)
            else:
                backtest.append(candle)
    return list(warmup) + backtest, len(warmup), len(backtest)


def calculate_cci(candles, period, constant=CCI_CONSTANT):
    """典型価格と平均偏差を使った標準的なCCIを返す。"""
    if period <= 0:
        raise ValueError("CCI期間は1以上にしてください")
    if constant <= 0:
        raise ValueError("CCI定数は0より大きくしてください")

    # 典型価格 =(高値 + 安値 + 終値)÷ 3 です。
    # 終値だけでなく、その足の高値・安値も含めた代表価格として使います。
    typical_prices = [
        (candle["high"] + candle["low"] + candle["close"]) / 3.0
        for candle in candles
    ]
    # period本が揃う前はCCIを計算できないため、先頭をNoneで埋めます。
    cci = [None] * len(candles)
    for index in range(period - 1, len(candles)):
        # 現在足を含む直近period本だけを計算窓として取り出します。
        window = typical_prices[index - period + 1:index + 1]
        # averageは期間内の典型価格の単純平均です。
        average = sum(window) / period
        # 各典型価格が平均からどれだけ離れたかを絶対値で平均します。
        mean_deviation = sum(abs(value - average) for value in window) / period
        # CCI =(現在の典型価格 - 平均)÷(0.015 × 平均偏差)です。
        # 全足が同値で平均偏差が0なら、中立値の0としてゼロ除算を避けます。
        cci[index] = (
            0.0
            if mean_deviation == 0.0
            else (typical_prices[index] - average) / (constant * mean_deviation)
        )
    return cci


def theoretical_cci_max(period, constant=CCI_CONSTANT):
    """指定期間のCCIが取り得る最大絶対値を返す。"""
    # 短いCCIでは、計算上どうしても届かない大きな基準値があります。
    # 例: 期間5の最大絶対値は約166.67なので、基準値200には届きません。
    return period / (2.0 * constant)


def is_level_reachable(period, level):
    """CCIが理論上その基準値へ到達できるかを返す。"""
    return level <= theoretical_cci_max(period)


def make_trade(position, candle, exit_price, reason, side, pip_size):
    """決済済み取引を作る。"""
    # LONGは値上がり、SHORTは値下がりしたときに損益がプラスになります。
    direction = 1.0 if side == "LONG" else -1.0
    return {
        # 円通貨は1pips=0.01円、それ以外は1pips=0.0001で換算します。
        "損益(pips)": direction * (exit_price - position["price"]) / pip_size,
        # 日時差なので、土日やデータ欠損をまたぐ時間も含みます。
        "保有時間(分)": (
            candle["close_time"] - position["time"]
        ).total_seconds() / 60,
        "決済理由": reason,
        # 月別集計は入力CSVのtimestamp_jst(足の開始時刻)へ帰属させる。
        # 最終日の23:45足を翌月扱いにせず、検証対象月内へ確実に含める。
        "決済時刻": candle["open_time"],
    }


def collect_trades(candles, cci, side, level, pip_size, entry_start_index):
    """CCIが圏外から基準値の内側へ戻った後、次足始値で入る。"""
    if side not in SIDES:
        raise ValueError("sideはLONGまたはSHORTにしてください")
    if level <= 0:
        raise ValueError("CCI基準値は0より大きくしてください")

    # positionがNoneならポジションを持っていません。
    # pending_entryは、確定足で出たシグナルを次足始値で実行する予約です。
    position = None
    pending_entry = False
    trades = []

    # クロス判定で前足も使うため、2本目にあたるindex=1から処理します。
    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_distance = TAKE_PROFIT_PIPS * pip_size
            sl_distance = STOP_LOSS_PIPS * pip_size
            if side == "LONG":
                # LONGは上昇でTP、下落でSLへ到達します。
                tp_price = position["price"] + tp_distance
                sl_price = position["price"] - sl_distance
                tp_hit = candles[index]["high"] >= tp_price
                sl_hit = candles[index]["low"] <= sl_price
            else:
                # SHORTは下落でTP、上昇でSLへ到達します。
                tp_price = position["price"] - tp_distance
                sl_price = position["price"] + sl_distance
                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

        # 前足と現在足のCCIを比較し、基準値の内側へ戻った瞬間を探します。
        previous_cci = cci[index - 1]
        current_cci = cci[index]
        if position is not None or previous_cci is None or current_cci is None:
            # 保有中は新規シグナルを無視します。NoneはCCI未計算区間です。
            continue

        if side == "LONG":
            # -level以下から-levelを上抜けたときに売られすぎ圏からの復帰とする。
            crossed_back = previous_cci <= -level and current_cci > -level
        else:
            # +level以上から+levelを下抜けたときに買われすぎ圏からの復帰とする。
            crossed_back = previous_cci >= level and current_cci < level

        if crossed_back 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 = [profit for profit in profits if profit > 0]
    losses = [profit for profit in profits if profit < 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 iter_months(start_date, end_date):
    """開始月から終了月までをYYYY-MM形式で返す。"""
    # 2024年12月の次を2025年1月に進めながら、対象月を順番に作ります。
    year, month = start_date.year, start_date.month
    result = []
    while (year, month) <= (end_date.year, end_date.month):
        result.append(f"{year:04d}-{month:02d}")
        month += 1
        if month == 13:
            year += 1
            month = 1
    return result


# 全期間は2024-01~2026-07の31か月、直近6か月は最後の6要素です。
BACKTEST_MONTHS = tuple(iter_months(BACKTEST_START_DATE, BACKTEST_END_DATE))
RECENT_SIX_MONTHS = frozenset(BACKTEST_MONTHS[-6:])


def build_monthly_rows(symbol, side, period, level, trades):
    """取引を決済月別に集計し、取引0回の月も含めて返す。"""
    rows = []
    for month in BACKTEST_MONTHS:
        # 決済がこの年月に属する取引だけを抜き出します。
        selected = [
            trade for trade in trades
            if trade["決済時刻"].strftime("%Y-%m") == month
        ]
        metrics = calculate_metrics(selected)
        rows.append({
            "通貨ペア": symbol,
            "売買方向": side,
            "CCI期間": period,
            "CCI基準値": level,
            "TP(pips)": TAKE_PROFIT_PIPS,
            "SL(pips)": STOP_LOSS_PIPS,
            "年月": month,
            **{column: metrics[column] for column in MONTHLY_RESULT_COLUMNS[7:]},
        })
    return rows


def summarize(symbol, side, period, level, trades, monthly_rows):
    """1設定の全期間・月別・直近6か月成績を集計する。"""
    # まず全期間の成績を作り、その後に同じ方法で直近6か月を集計します。
    metrics = calculate_metrics(trades)
    recent_trades = [
        trade for trade in trades
        if trade["決済時刻"].strftime("%Y-%m") in RECENT_SIX_MONTHS
    ]
    recent_metrics = calculate_metrics(recent_trades)
    monthly_profits = [row["合計損益(pips)"] for row in monthly_rows]
    recent_monthly_profits = [
        row["合計損益(pips)"]
        for row in monthly_rows
        if row["年月"] in RECENT_SIX_MONTHS
    ]
    theoretical_max = theoretical_cci_max(period)
    signal_possible = is_level_reachable(period, level)
    profit_factor = metrics["プロフィットファクター"]
    # 記事候補は、取引数・全期間PF・直近成績をすべて満たした設定です。
    candidate_passed = (
        signal_possible
        and metrics["取引回数"] >= MIN_CANDIDATE_TRADES
        and metrics["合計損益(pips)"] > 0
        and profit_factor != ""
        and profit_factor >= MIN_CANDIDATE_PROFIT_FACTOR
        and recent_metrics["合計損益(pips)"] > 0
        and sum(profit > 0 for profit in recent_monthly_profits)
        >= MIN_RECENT_POSITIVE_MONTHS
    )
    return {
        "通貨ペア": symbol,
        "売買方向": side,
        "CCI期間": period,
        "CCI基準値": level,
        "エントリー方式": "圏外からの復帰",
        "CCI理論最大絶対値": round(theoretical_max, 2),
        "理論上シグナル発生可能": signal_possible,
        "TP(pips)": TAKE_PROFIT_PIPS,
        "SL(pips)": STOP_LOSS_PIPS,
        **metrics,
        "最終足決済数": sum(
            trade["決済理由"] == "データ終了" for trade in trades
        ),
        "全期間プラス月数": sum(profit > 0 for profit in monthly_profits),
        "全期間マイナス月数": sum(profit < 0 for profit in monthly_profits),
        "直近6か月取引回数": recent_metrics["取引回数"],
        "直近6か月合計損益(pips)": recent_metrics["合計損益(pips)"],
        "直近6か月平均損益(pips)": recent_metrics["平均損益(pips)"],
        "直近6か月プロフィットファクター": recent_metrics["プロフィットファクター"],
        "直近6か月プラス月数": sum(
            profit > 0 for profit in recent_monthly_profits
        ),
        "直近6か月マイナス月数": sum(
            profit < 0 for profit in recent_monthly_profits
        ),
        "記事候補条件合格": candidate_passed,
    }


def write_csv(path, rows, columns=RESULT_COLUMNS):
    """結果をExcelでも開きやすいUTF-8 BOM付きCSVへ保存する。"""
    # resultsや通貨・方向別フォルダが存在しない初回実行時は自動作成します。
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8-sig", newline="") as file:
        writer = csv.DictWriter(file, fieldnames=columns)
        writer.writeheader()
        writer.writerows(rows)


def run_symbol(candles, symbol, entry_start_index, output_dir):
    """1通貨について理論上比較可能な全CCI設定を検証する。"""
    # USDJPY・EURJPY・GBPJPYは1pips=0.01円、それ以外は0.0001です。
    pip_size = 0.01 if symbol.endswith("_JPY") else 0.0001
    # LONGとSHORTを別々のCSVへ保存するため、方向別に結果を貯めます。
    rows_by_side = {side: [] for side in SIDES}

    # CCIは期間ごとに1回だけ計算し、方向・基準値間で共有する。
    for period in CCI_PERIODS:
        cci = calculate_cci(candles, period)
        for side in SIDES:
            for level in CCI_LEVELS:
                # 期間5・基準値200など、計算上届かない組み合わせは除外します。
                if not is_level_reachable(period, level):
                    continue
                trades = collect_trades(
                    candles, cci, side, level, pip_size, entry_start_index,
                )
                # 月別CSVは保存しませんが、月数と直近6か月の列を作るため集計します。
                setting_monthly_rows = build_monthly_rows(
                    symbol, side, period, level, trades,
                )
                rows_by_side[side].append(
                    summarize(
                        symbol, side, period, level, trades,
                        setting_monthly_rows,
                    )
                )

    # 1通貨・1方向の134ケースをresult.csvへ保存し、全件用にも返します。
    rows = []
    for side in SIDES:
        side_rows = rows_by_side[side]
        write_csv(output_dir / symbol / side / "result.csv", side_rows)
        rows.extend(side_rows)
    return rows


def main():
    """5通貨・2方向の理論上比較可能なCCI設定を検証する。"""
    # スクリプト全体の実行時間を測り始めます。
    started = time.perf_counter()
    if BACKTEST_START_DATE > BACKTEST_END_DATE:
        raise SystemExit("検証開始日は検証終了日以前に設定してください")
    if TAKE_PROFIT_PIPS <= 0 or STOP_LOSS_PIPS <= 0:
        raise SystemExit("TPとSLは1pips以上にしてください")

    # 候補の直積から、CCIの理論上限へ届かない組み合わせを取り除きます。
    valid_parameter_pairs = tuple(
        (period, level)
        for period in CCI_PERIODS
        for level in CCI_LEVELS
        if is_level_reachable(period, level)
    )
    excluded_parameter_pairs = len(CCI_PERIODS) * len(CCI_LEVELS) - len(
        valid_parameter_pairs
    )
    total_cases = len(SYMBOLS) * len(SIDES) * len(valid_parameter_pairs)
    print(
        f"{len(SYMBOLS)}通貨 × {len(SIDES)}方向 × "
        f"CCI期間{len(CCI_PERIODS)}種類 × 基準値{len(CCI_LEVELS)}種類 / "
        f"有効な期間・基準値{len(valid_parameter_pairs)}組 = {total_cases}ケース"
    )
    print(f"理論上到達不能のため除外: {excluded_parameter_pairs}組")
    print(f"検証期間: {BACKTEST_START_DATE} ~ {BACKTEST_END_DATE}")
    print(f"固定出口: TP{TAKE_PROFIT_PIPS} / SL{STOP_LOSS_PIPS} pips")

    # 通貨・方向別result.csvに加え、全結果をまとめるall_rowsも作ります。
    all_rows = []
    cases_per_symbol = len(SIDES) * len(valid_parameter_pairs)
    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 >= WARMUP_BARS:
            # 検証開始前の足が十分なら、その足をCCI計算だけに使います。
            entry_start_index = warmup_count
            warmup_description = f"検証開始前{warmup_count}本"
        elif len(candles) > WARMUP_BARS:
            # 検証前データがなければ、CSV先頭40本を全設定共通で除外します。
            # 短い期間だけ先に取引を始めないためです。
            entry_start_index = WARMUP_BARS
            warmup_description = f"CSV先頭{WARMUP_BARS}本"
            print(f"[WARN] {symbol}: 検証前データ不足のため{warmup_description}を使用")
        else:
            raise SystemExit(f"{symbol}のデータが不足しています: {len(candles)}本")

        print(
            f"[RUN] {symbol}: {cases_per_symbol}ケース / 検証{backtest_count}本 / "
            f"ウォームアップ: {warmup_description}"
        )
        all_rows.extend(run_symbol(
            candles, symbol, entry_start_index, DEFAULT_OUTPUT_DIR,
        ))

    # 個別CSVに加え、全1,340ケースを一括比較できる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}秒")
    return 0


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

同じようにCCIの期間や基準値を検証してみたい方は、参考にしてみてください。

FXのCCIとは

CCI(Commodity Channel Index)は、現在の価格が一定期間の平均価格からどの程度離れているかを表すテクニカル指標です。

価格が平均より上に離れているのか、下に離れているのかを数値で確認できるため、相場の勢いや価格の偏りを見る際に使われます。

CCIについて押さえておきたいポイントは、主に次の3つです。

  • 現在の価格が平均からどれくらい離れているか分かる
  • プラス・マイナスで価格の位置を判断できる
  • 高値・安値・終値から計算する

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

CCIで分かること

CCIを見ると、現在の価格が一定期間の平均価格からどの程度離れているかを確認できます。

例えば、

  • CCIが大きくプラス → 平均より高い位置
  • CCIが大きくマイナス → 平均より低い位置

にあると判断できます。

一般的には、+100・-100などを基準値として確認する方法があります。

ただし、CCIが+100を超えたからといって、必ず下落するわけではありません。

強いトレンドでは高い水準や低い水準が続くこともあるため、基準値を超えたことだけで売買を判断しないことが重要です。

CCIのプラス・マイナスは何を意味する?

CCIは0を中心に、プラスとマイナスで表示されます。

基本的には、

  • CCIがプラス:現在の価格が期間内平均より高い
  • CCIがマイナス:現在の価格が期間内平均より低い
  • CCIが0付近:現在の価格が期間内平均に近い

と考えられます。

例えばCCIが+100を超えていれば平均より大きく上に離れ、-100を下回っていれば平均より大きく下に離れている状態です。

今回の検証では、±50〜±300まで複数の基準値を比較します。

CCIの計算方法

CCIは、高値・安値・終値から求める典型価格を使って計算します。

まず、典型価格を求めます。

典型価格 =(高値 + 安値 + 終値)÷ 3

次に、一定期間の典型価格の平均と平均偏差を求め、次の式でCCIを計算します。

CCI =(典型価格 − 典型価格の移動平均)÷(0.015 × 平均偏差)

少し複雑に見えますが、考え方はシンプルです。

価格が平均から上に離れるほどCCIはプラス方向へ、下に離れるほどマイナス方向へ大きくなると覚えておけば問題ありません。

FXのCCIでよく使われる設定

CCIでは、主に期間と基準値を設定して使います。

期間は14や20、基準値は±100や±200がよく使われます。

期間を短くすると直近の値動きに反応しやすくなり、長くすると動きがゆるやかになります。

また、基準値は大きくするほどシグナルが出にくくなります。

例えば、

  • ±100:比較的シグナルが出やすい
  • ±200:より大きく平均から離れた場面を捉えやすい

といった違いがあります。

ただし、期間14・20や±100・±200が必ず最適とは限りません。

そこで今回の検証では、CCI期間5〜40の13種類、基準値±50〜±300の11種類を比較し、どの設定で成績が良くなるのかを確認します。

今回のCCI検証条件

今回は、5通貨の15分足を使い、CCIの期間と基準値を変えながらバックテストを行います。

検証条件は次の3つに分けて整理します。

検証する5通貨・期間・CCI設定

検証する通貨ペアは次の5つです。

  • USDJPY
  • EURJPY
  • GBPJPY
  • EURUSD
  • AUDUSD

時間足は15分足、検証期間は2024年1月1日〜2026年7月31日です。

CCI期間は次の13種類を使用します。

5・7・9・10・14・18・20・21・25・28・30・35・40

CCI基準値は次の11種類です。

±50・±75・±100・±125・±150・±175・±200・±225・±250・±275・±300

CCI期間13種類×基準値11種類では143通りありますが、理論上シグナルが発生しない9通りを除外します。

そのため、今回は134設定×5通貨×LONG・SHORT=1,340ケースを検証します。

LONG・SHORTのエントリー条件

エントリーは、CCIが基準値の外側から内側へ戻ったタイミングを使用します。

LONG

  • CCIがマイナス側の基準値以下になる
  • その後、基準値を上抜ける
  • 次の15分足始値でLONG

SHORT

  • CCIがプラス側の基準値以上になる
  • その後、基準値を下抜ける
  • 次の15分足始値でSHORT

例えば基準値100の場合、LONGではCCIが-100以下から-100を上抜けたあと、次の15分足始値でエントリーします。

TP50・SL30に固定して検証

決済条件は、すべてのケースで次のように固定します。

  • TP:50pips
  • SL:30pips

今回はCCI期間と基準値による成績の違いを比較することが目的なので、TP・SLは変更しません。

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

5通貨でCCI1,340ケースを検証

ここからは、5通貨・LONG/SHORTで検証した1,340ケースの結果を見ていきます。

成績の良い設定を選ぶ際は、単純に合計pipsだけを見るのではなく、次の条件を満たした設定を候補とします。

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

この条件を満たした設定の中から、合計pipsが高い順に紹介します。

USDJPYで成績が良かったCCI設定

USDJPY LONG:TOP5

順位CCI期間基準値取引回数勝率合計pipsPF最大DD
17501,79939.63%+3,070.01.09421,410
210751,69639.50%+2,737.61.08901,010
3251001,24440.11%+2,600.01.1163750
471001,83539.29%+2,580.11.07721,340
510501,64339.44%+2,550.01.08541,330

USDJPY LONGでは、CCI期間7・10の短めの設定が上位に多く入りました。

1位は期間7・基準値50で、合計+3,070pipsです。

一方、3位の期間25・基準値100は合計pipsではやや劣るものの、最大DDは750pipsとTOP5の中では小さくなっています。

USDJPY SHORT:基準を満たす設定なし

USDJPY SHORTでは、今回の候補条件をすべて満たした設定はありませんでした。

LONGでは多くの設定が候補に残った一方で、SHORTでは明確な差が出ています。

EURJPYで成績が良かったCCI設定

EURJPY LONG:TOP5

順位CCI期間基準値取引回数勝率合計pipsPF最大DD
15751,95140.49%+4,670.01.13411,010
27501,74740.81%+4,585.21.1478610
371001,82040.60%+4,520.01.13941,050
410751,65140.88%+4,425.21.15111,030
59751,71340.75%+4,405.21.14471,200

EURJPY LONGでは、CCI期間5〜10、基準値50〜100付近がTOP5を占めました。

特に2位の期間7・基準値50は、+4,585.2pipsに加えて最大DD610pipsと、上位の中では比較的抑えられています。

EURJPY SHORT:基準を満たした4設定

CCI期間基準値取引回数勝率合計pipsPF最大DD
1425031442.68%+1,300.01.2407350
1827521340.85%+570.01.1508440
2030013341.35%+410.01.1752510
2130013839.86%+260.01.1044320

EURJPY SHORTでは、候補となったのは4設定のみです。

いずれも基準値250〜300と高く、より大きくCCIが上昇した場面に絞った設定が残りました。

GBPJPYで成績が良かったCCI設定

GBPJPY LONG:TOP5

順位CCI期間基準値取引回数勝率合計pipsPF最大DD
191002,21941.64%+7,350.01.1892690
2101251,99241.92%+7,040.01.2028670
37752,36041.23%+7,040.01.1692770
471002,41540.87%+6,510.01.1520830
59752,18141.22%+6,440.81.1675780

1位はCCI期間9・基準値100で、合計+7,350pipsでした。

また、TOP5すべてがCCI期間7〜10、基準値75〜125に集中しています。

1つの設定だけではなく、周辺の設定でも良い結果が出ている点は注目できます。

GBPJPY SHORT:TOP5

順位CCI期間基準値取引回数勝率合計pipsPF最大DD
12022555140.11%+1,150.01.1162630
21822555139.02%+670.01.0665730
32522553039.06%+660.01.0681610
42122555138.84%+590.01.0584710
51827520640.78%+540.01.1475510

GBPJPY SHORTでは、基準値225付近の設定が多く上位に入りました。

ただし、LONGと比べると合計pipsや候補数はかなり小さくなっています。

EURUSDで成績が良かったCCI設定

EURUSD LONG:基準を満たした設定は1つ

CCI期間基準値取引回数勝率合計pipsPF最大DD
1025019339.90%+370.01.1063320

EURUSD LONGで候補条件を満たしたのは、CCI期間10・基準値250のみでした。

そのため、今回の条件ではLONGに適した設定はかなり限定されています。

EURUSD SHORT:TOP5

順位CCI期間基準値取引回数勝率合計pipsPF最大DD
13010062441.51%+2,000.01.1826540
2357558941.43%+1,850.01.1787360
34010057241.43%+1,800.01.1791460
4407557741.07%+1,650.01.1618700
5287561740.68%+1,570.01.1430360

EURUSD SHORTでは、CCI期間28〜40の長めの設定がTOP5に入りました。

JPY系LONGでは短い期間が上位だったため、通貨や方向によって適したCCI期間に違いがあることが分かります。

AUDUSDで成績が良かったCCI設定

AUDUSD LONG:TOP5

順位CCI期間基準値取引回数勝率合計pipsPF最大DD
1512554239.85%+985.61.1008680
2510056339.43%+835.61.0817650
31830014143.97%+730.01.3080260
41430010744.86%+630.01.3559380
51827518939.15%+250.01.0725440

合計pipsでは期間5・基準値125が1位でした。

一方、期間14・18と基準値300を組み合わせた設定では、取引回数は少ないもののPFが高くなっています。

AUDUSD SHORT:TOP5

順位CCI期間基準値取引回数勝率合計pipsPF最大DD
11025014748.98%+1,375.91.6186170
2922522544.89%+1,355.91.3670350
31817542641.31%+1,261.21.1682480
41830012249.18%+1,165.51.6353240
51827518145.30%+1,155.51.3924410

AUDUSD SHORTでは、1位の期間10・基準値250が+1,375.9pips、PF1.6186となりました。

また4位の期間18・基準値300はPF1.6353とさらに高い結果です。

ただし、どちらも取引回数は100〜150回程度と少なめなので、PFの高さだけで判断せず、取引回数も含めて見る必要があります。

5通貨のCCI検証結果を比較

5通貨・1,340ケースをまとめて比較すると、今回の検証ではCCI期間18が比較的良い結果となりました。

一般的に使われる14・20もプラスでしたが、期間18は合計pips・PF中央値・候補合格数のバランスが良い結果です。

CCI基準値では、±125が合計pipsで最も高く、±300はPF中央値が最も高い結果となりました。

ただし、基準値を大きくすると取引回数が減りやすいため、PFだけで良し悪しを判断するのは注意が必要です。

LONG・SHORTを比較すると、今回の検証ではLONGの方が全体的に良い結果となっています。

特にUSDJPY・EURJPY・GBPJPYではLONGが強く、EURUSD・AUDUSDではSHORTの方が候補設定が多くなりました。

今回の結果から、CCIはすべての通貨で同じ設定を使うのではなく、通貨ペアやLONG・SHORTごとに期間と基準値を調整することが重要と考えられます。

CCIをFXで使う際の注意点

CCIをFXで使う際は、設定だけでなく使い方やバックテスト結果の見方にも注意が必要です。

主な注意点は、次の3つです。

  • CCIだけでエントリーを判断しない
  • 取引回数が少ない設定は慎重に判断する
  • バックテストの好成績をそのまま過信しない

それぞれ詳しく見ていきましょう。

CCIだけでエントリーを判断しない

CCIは価格の偏りを確認するのに便利ですが、CCIだけでエントリーを判断すると、相場全体の流れと逆方向に入ってしまうことがあります。

そのため、移動平均線やADXなど、トレンド方向を確認できる指標と組み合わせて使う方法も検討した方がよいでしょう。

取引回数が少ない設定は慎重に判断する

取引回数が少ない設定では、数回の好成績によってPFや合計pipsが高く見えることがあります。

今回の検証では、記事掲載候補の条件として取引回数100回以上を設定しています。

PFが高くても取引回数が少ない場合は、他の設定と比較しながら慎重に判断することが重要です。

過去の好成績が将来も続くとは限らない

バックテストで良い成績が出た設定でも、今後の相場で同じように機能するとは限りません。

特に今回は多くのパラメータを比較しているため、偶然好成績になった設定が含まれる可能性もあります。

そのため、1つの期間だけで判断せず、直近成績や別期間でも安定しているか確認することが大切です。

バックテストではスプレッドや約定差に注意する

今回の検証では、スプレッドや手数料、スリッページは考慮していません。

また、LONG・SHORTともにASK価格を使用し、TP・SLは指定価格で約定するものとして計算しています。

そのため、実際の取引ではバックテストより成績が悪くなる可能性があります

特に平均損益が小さい設定では、取引コストを考慮すると利益が大きく減る場合があるため注意が必要です。

FXのCCIに関するよくある質問

CCIについて、よくある質問をまとめました。

CCIとRSIの違いは?

CCIとRSIはどちらも相場の勢いを見る際に使われますが、計算方法や数値の見方が異なります。

RSIは0〜100の範囲で表示されるのに対し、CCIには決まった上限・下限がなく、0を中心にプラス・マイナスで表示されます。

そのため、似た場面で使われることはありますが、同じ指標ではありません。

CCIはトレンド相場でも使える?

CCIはトレンド相場でも使えます。

強い上昇相場ではCCIがプラス側の高い水準を維持し、下落相場ではマイナス側の低い水準が続くことがあります。

そのため、逆張りだけでなく、トレンドの勢いを確認する目的でも利用可能です。

CCIはどの時間足で使える?

CCIは15分足・1時間足・4時間足・日足など、さまざまな時間足で使えます。

今回の検証では、15分足を使用しています。

時間足によってシグナルの出やすさは変わるため、自分の取引スタイルに合わせて選ぶことが大切です。

CCIの期間や基準値は通貨ごとに変えた方がいい?

今回の検証結果を見ると、通貨やLONG・SHORTによって成績の良い設定に違いがありました。

そのため、すべての通貨で同じCCI期間・基準値を使うよりも、通貨ペアや売買方向ごとに検証して調整する方がよいと考えられます。

今回も、USDJPY・EURJPY・GBPJPYではLONGが強く、EURUSD・AUDUSDではSHORTの候補が多くなりました。

まとめ

今回は、USDJPY・EURJPY・GBPJPY・EURUSD・AUDUSDの5通貨を対象に、CCI期間13種類・CCI基準値11種類・LONG/SHORTを組み合わせ、理論上到達できない設定を除外した1,340ケースを検証しました。

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

  • 一般的なCCI期間14・20が必ずしも最適とは限らない
  • 通貨ペアやLONG・SHORTによって良い設定が異なる
  • 全体ではCCI期間18が比較的良い結果だった
  • USDJPY SHORTでは候補条件を満たす設定を確認できなかった
  • 期間と基準値をまとめて検証することが重要

今回、合計損益が特に大きかったのは、GBPJPY LONGのCCI期間9・基準値100で、+7,350pips、PF1.1892となった設定でした。

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

また、5通貨全体で比較すると、CCI期間18が合計pips・PF中央値・候補合格数のバランスで比較的良く、基準値では±125が合計pips、±300がPF中央値で高い結果となっています。

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

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

実際にCCIをトレードへ取り入れる場合は、一般的な設定をそのまま使うのではなく、通貨ペアや売買方向に合わせてバックテストしたうえで判断してください。

-FX自動売買, Python