CCXTを使って仮想通貨のトレードをしてみる(第3回):リスク管理とバックテストの完全ガイド

約31分で読めます by ぽんたぬき
CCXTを使って仮想通貨のトレードをしてみる(第3回):リスク管理とバックテストの完全ガイド

CCXTで仮想通貨バックテストとリスク管理を完全解説(第3回)

キミ、第3回もやってきちゃいましたヨ♪ CCXTバックテストって、名前からしてムズかしそうに聞こえるよネ😅 僕もサ、最初に「バックテスト実装してみよう!!」と思ったとき、何から手をつければいいかわからなくて、ターミナルの前でしばらくフリーズしちゃいましたヨ……(笑)

でもネ、今回の第3回を読み終わったあとには、キミも「あ、なんだ、こういうことか!!」って声に出ちゃうハズデス✨ 試行錯誤しながら身につけた知識を、ギュッとまとめちゃいますカラネ🎉

第1回(CCXTを使って仮想通貨のトレードをしてみる 第1回)では環境構築と基本操作を、第2回(CCXTを使って仮想通貨のトレードをしてみる 第2回)では実際の取引の流れと基本的なトレーディング戦略をやりましたネ。まだ読んでない人は先そっちを確認してきてネ!!

今回の第3回では、CCXTバックテスト仮想通貨リスク管理という2大テーマをガッツリやっちゃいますヨ💡 この2つがちゃんとできていないと、いくら良い戦略でも、いつか大きく資金を溶かしちゃうことになるんだヨネ……😭 それだけは避けたいじゃん!!というワケで、一緒にやっていきましょう🦝


1. CCXTバックテストの基本概念:仮想通貨トレードへの活用

1-1. バックテストとCCXTの関係を理解する

キミ、突然だけどサ。「過去のデータで戦略を試せたら最高じゃん!!」って思ったことない? バックテストというのは、まさにソレをやるプロセスデスヨ♪

過去の価格データを使って、「もしこの戦略を○年前から実行していたら、どうなっていたか?」をシミュレーションするんだヨネ。ただ、ここで超重要な注意点がありましてネ——「CCXTバックテストでわかること」と「わからないこと」が、ちゃんとあるんだヨ💡

わかること:

  • 過去データに対する戦略の有効性
  • おおまかなリスク・リターンの特性
  • パラメータの感度(この数値が少し変わると結果がどう変わるか)

わからないこと:

  • 未来のパフォーマンス(当たり前だけど超重要!!)
  • スリッページや流動性の影響(特に大口注文)
  • 取引所の障害・APIエラーなどの運用リスク

そして、CCXTバックテストで最もやらかしやすいのが「過学習(カーブフィッティング)」デスネ😭 過去データにめちゃくちゃ最適化しすぎて、未来では全然機能しない戦略を作っちゃうやつ。

僕もサ、昔やったんだヨネ……パラメータをいじりまくって「シャープレシオ3.5!!最強!!🔥」とか喜んでたら、実運用で即死したという黒歴史が……😭 バックテストの数字は、あくまでも「過去に対する結果」にすぎないと、今は肝に銘じていますヨ!!

1-2. 評価指標の読み方:シャープレシオ・ドローダウン・プロフィットファクター

バックテスト結果を見るとき、どの数字を見ればいいか迷うよネ!!僕もそうでしたヨ😅 主要な評価指標と「目安となる基準値」をまとめちゃいますネ♪

指標 目安となる基準値 意味
シャープレシオ 1.0以上 リスク調整後リターン。高いほど良い
最大ドローダウン 20%未満 資産が最高値からどこまで落ちたか
取引数 50件以上 統計的有意性を確保するための最低ライン
勝率 50%以上(プロフィットファクターとセット) 勝率だけでは不十分!!
プロフィットファクター 1.5以上 総利益÷総損失。2.0超えたら優秀✨

シャープレシオが1.0未満だったり、最大ドローダウンが30%超えてたりしたら、その戦略は再考したほうがいいよネ💦 取引数が少ない(たとえば10件とか)だと、それもう「たまたま」の域を出ないカラネ!!

ちなみにサ、先週コーヒーを飲みながらバックテストの結果をぼんやり眺めていたら、気づいたら3時間経っていたんだヨネ🦝 数字を見てると時間を忘れちゃうんだよサ……(笑)まあ、それだけ奥が深いってことデスネ!!

1-3. CCXTバックテストの全体フロー

CCXTを使ったバックテストの全体フローをザクッとまとめるとサ、こんな感じデスネ♪

Step 1: fetch_ohlcv() で過去OHLCVデータを取得
        ↓
Step 2: pandasでデータ整形・インジケーター計算
        ↓
Step 3: 手数料・スリッページを考慮してシミュレーション
        ↓
Step 4: シャープレシオ・ドローダウン・プロフィットファクター等の指標を算出・評価

CCXTの役割は主にStep 1のデータ取得デスネ🎵 バックテストのロジックそのものはpandasや専用フレームワークで実装するのが普通デス。「CCXTだけで全部やろう」とするとかなり大変なので、餅は餅屋でいきましょうネ!!


2. CCXTで仮想通貨バックテスト用データを取得・活用する

2-1. fetch_ohlcv() の使い方と注意点

さっそくCCXTバックテスト用のデータ取得コードを見てくよ!!コレ見てよ!!スゴくナイ!?✨

import ccxt
import pandas as pd
import time

exchange = ccxt.binance({
    'rateLimit': 1200,
    'enableRateLimit': True,
    'timeout': 30000,
    'options': {
        'defaultType': 'spot',
        'adjustForTimeDifference': True,
    },
})

def fetch_all_ohlcv(symbol, timeframe, days=365, max_retries=3):
    """大量データを分割取得する関数"""
    all_ohlcv = []
    since = exchange.parse8601(
        (pd.Timestamp.now() - pd.Timedelta(days=days)).strftime('%Y-%m-%dT00:00:00Z')
    )
    limit = 500  # 1回あたりの最大取得数

    while True:
        retries = 0
        ohlcv = None
        while retries < max_retries:
            try:
                ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=since, limit=limit)
                break
            except ccxt.BaseError as e:
                retries += 1
                print(f"データ取得エラー ({retries}/{max_retries}): {e}")
                if retries >= max_retries:
                    raise
                time.sleep(5 * retries)

        if not ohlcv:
            break
        all_ohlcv.extend(ohlcv)
        since = ohlcv[-1][0] + 1  # 最後のタイムスタンプの次から
        time.sleep(exchange.rateLimit / 1000)  # レートリミット対策

        # 直近データに達したら終了
        if len(ohlcv) < limit:
            break

    if not all_ohlcv:
        raise ValueError("OHLCVデータを取得できませんでした")

    # DataFrameに変換
    df = pd.DataFrame(all_ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
    df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
    df = df.set_index('timestamp')
    df = df[~df.index.duplicated(keep='first')]  # 重複除去
    return df

# 実行
try:
    df = fetch_all_ohlcv('BTC/USDT', '1d', days=365)
    print(df.head())
    print(f"取得件数: {len(df)}件")
except ccxt.BaseError as e:
    print(f"取引所エラー: {e}")
except Exception as e:
    print(f"予期しないエラー: {e}")

動いた時サ、思わず「おっしゃ!!」って声出ちゃいましたヨ😆 テンションが上がっちゃうよネ!!

ここで超重要なポイントがあってネ💡 レートリミット対策はマジで大事デス!!enableRateLimit: True にしておくと、CCXTが自動で待機してくれるんだケド、大量データ取得のときは time.sleep() で明示的に待つのが安全デスネ🎵 僕、コレをサボって一時的にAPIアクセスを制限されかけた経験がありマス……やらかしちゃいましたヨ……😭

あとサ、タイムスタンプの変換も忘れないでネ!!CCXTはミリ秒のUNIXタイムスタンプで返してくるカラ、unit='ms' を指定しないと意味不明な数字の羅列になっちゃうんだヨ(笑)

2-2. 移動平均クロス戦略でCCXTバックテストを実装する

データが取れたら、次は実際にバックテストをやってみよ!!今回はMA40/MA100クロス戦略をサンプルにするよネ♪ コレ見てよ!!スゴくナイ!?✨ データ取得からシミュレーションまで一気にやっちゃうよ!!

import ccxt
import pandas as pd
import numpy as np

# --- データ取得 ---
exchange = ccxt.binance({
    'enableRateLimit': True,
    'timeout': 30000,
    'options': {
        'defaultType': 'spot',
        'adjustForTimeDifference': True,
    },
})

try:
    ohlcv = exchange.fetch_ohlcv('BTC/USDT', '1d', limit=300)
except ccxt.BaseError as e:
    raise RuntimeError(f"OHLCVデータの取得に失敗しました: {e}")

if not ohlcv:
    raise ValueError("取得したOHLCVデータが空です")

df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
df = df.set_index('timestamp')

# --- インジケーター計算 ---
df['MA40'] = df['close'].rolling(40).mean()
df['MA100'] = df['close'].rolling(100).mean()

# シグナル生成:MA40がMA100を上回ったら買い(1)、下回ったら売り(0)
df['signal'] = (df['MA40'] > df['MA100']).astype(int)
df['position'] = df['signal'].diff()  # 1=買いエントリー、-1=売りエグジット

# --- シミュレーション ---
FEE_RATE = 0.001    # 手数料 0.1%(Binance Takerの場合)
SLIPPAGE = 0.0005   # スリッページ 0.05%
INITIAL_CAPITAL = 10000  # 初期資金(USDT)

capital = INITIAL_CAPITAL
position_size = 0
entry_price = 0
trades = []

for idx, row in df.dropna().iterrows():
    if row['position'] == 1:  # 買いエントリー
        # 手数料+スリッページ込みの実効価格
        entry_price = row['close'] * (1 + FEE_RATE + SLIPPAGE)
        position_size = capital / entry_price
        capital = 0

    elif row['position'] == -1 and position_size > 0:  # 売りエグジット
        exit_price = row['close'] * (1 - FEE_RATE - SLIPPAGE)
        profit = (exit_price - entry_price) * position_size
        capital = position_size * exit_price
        trades.append({
            'exit_time': idx,
            'entry_price': entry_price,
            'exit_price': exit_price,
            'profit': profit,
            'return_pct': (exit_price - entry_price) / entry_price * 100
        })
        position_size = 0

# --- パフォーマンス指標 ---
trades_df = pd.DataFrame(trades)
if len(trades_df) > 0:
    total_return = (capital - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
    win_rate = len(trades_df[trades_df['profit'] > 0]) / len(trades_df) * 100
    avg_profit = trades_df[trades_df['profit'] > 0]['return_pct'].mean()
    avg_loss = trades_df[trades_df['profit'] < 0]['return_pct'].mean()
    profit_factor = (
        trades_df[trades_df['profit'] > 0]['profit'].sum() /
        abs(trades_df[trades_df['profit'] < 0]['profit'].sum())
        if trades_df[trades_df['profit'] < 0]['profit'].sum() != 0 else float('inf')
    )

    print(f"=== バックテスト結果 ===")
    print(f"総リターン        : {total_return:.2f}%")
    print(f"取引数            : {len(trades_df)}件")
    print(f"勝率              : {win_rate:.1f}%")
    print(f"平均利益          : {avg_profit:.2f}%")
    print(f"平均損失          : {avg_loss:.2f}%")
    print(f"プロフィットFactor: {profit_factor:.2f}")
else:
    print("取引が発生しませんでした")

このコード、手数料とスリッページまでちゃんと考慮してるのがミソデスヨ🎵 コレをサボると「バックテストでは+30%だったのに実運用でマイナス!!」という地獄が待ってるカラネ😭

2-3. シャープレシオと最大ドローダウンを実装する

さっきのバックテスト結果にサ、シャープレシオと最大ドローダウンの計算もプラスしてみよ!!コレ見てよ!!スゴくナイ!?✨ 戦略評価の精度がグッと上がるよネ!!

import numpy as np

def calculate_sharpe_ratio(
    returns: pd.Series,
    risk_free_rate: float = 0.0,
    periods_per_year: int = 252
) -> float:
    """
    シャープレシオを計算する関数

    Parameters
    ----------
    returns : pd.Series
        各取引・各期間のリターン(小数表記。例:0.05 は5%)
    risk_free_rate : float
        年率リスクフリーレート(デフォルト 0.0)
    periods_per_year : int
        年間の取引期間数(日足なら252、時間足なら8760など)

    Returns
    -------
    float
        シャープレシオ
    """
    if len(returns) == 0 or returns.std() == 0:
        return 0.0
    excess_returns = returns - risk_free_rate / periods_per_year
    return float((excess_returns.mean() / excess_returns.std()) * np.sqrt(periods_per_year))


def calculate_max_drawdown(equity_curve: pd.Series) -> float:
    """
    最大ドローダウンを計算する関数

    Parameters
    ----------
    equity_curve : pd.Series
        資産推移の時系列データ(例:[10000, 10300, 9800, ...])

    Returns
    -------
    float
        最大ドローダウン(0〜1の小数。例:-0.20 は -20%)
    """
    rolling_max = equity_curve.cummax()
    drawdown = (equity_curve - rolling_max) / rolling_max
    return float(drawdown.min())


# trades_df が取得済みの前提で使用例
if len(trades_df) > 0:
    # 各取引のリターンをSeries化(パーセント→小数に変換)
    returns_series = trades_df['return_pct'] / 100

    # 資産推移(初期資金から累積計算)
    equity_curve = (1 + returns_series).cumprod() * INITIAL_CAPITAL

    sharpe = calculate_sharpe_ratio(returns_series)
    max_dd = calculate_max_drawdown(equity_curve)

    print(f"シャープレシオ    : {sharpe:.3f}")
    print(f"最大ドローダウン  : {max_dd * 100:.2f}%")

シャープレシオが1.0以上だったら「よし!!」って感じだよネ♪ 最大ドローダウンは絶対値が小さいほど良くて、-20%を超えてきたら戦略を見直す合図デスヨ💡 この2つをセットで見ることで、「リターンだけ高くてリスクが爆裂してる」みたいな戦略を弾けるんだヨネ✨

2-4. CCXTバックテストフレームワークの選び方

「専用フレームワーク使ったほうが楽じゃない?」って思うよネ!!キミ、センスあるネ!!😆 そうなんだヨ、用途によって使い分けるのがベストデス✨

フレームワーク 特徴 こんな人に
backtesting.py シンプルで学習コストが低い 初心者・まず動かしたい人
vectorbt 高速・ベクトル演算・可視化強 データ分析好きなエンジニア
backtrader 機能豊富・長年の実績あり ガッツリ作り込みたい人
Freqtrade CCXTと深く統合・本番運用も視野 本番稼働まで一貫してやりたい人
Jesse シンプルなAPI・暗号通貨特化 コードをスッキリ書きたい人

CCXTとの統合という観点でいうとサ、Freqtradeが一番相性がいいデスネ♪ CCXTをデータソースとして内部で使っているので、取引所をまたいだバックテストもわりとスムーズにできちゃうんだヨネ✨ 「バックテストから本番自動売買まで一気通貫でやりたい!!」なら、Freqtradeを最初から選ぶのはアリな選択デスネ!!


3. 仮想通貨リスク管理の実践:ポジションサイジングとケリー基準

3-1. ポジションサイジングの重要性

仮想通貨リスク管理の中でも、ポジションサイジングは超重要なテーマデスヨ!!「どれだけ買うか」を間違えると、良い戦略でも資金がなくなっちゃうカラネ😭

一般的によく使われるリスク管理ルールとしてサ、こんなものがありマス♪

  • 固定比率法:毎回、資産の○%をリスクにさらす(例:1回の取引で最大2%損失)
  • ケリー基準:期待値から「最も資産を伸ばせる」ポジションサイズを数学的に算出
  • 等金額法:毎回同じ金額(例:$1,000)でエントリー

固定比率法は管理がシンプルで、初心者にも使いやすいデス💡 ケリー基準は「理論的に最適」だけど、過大なポジションを取りがちなので、フルケリーではなくハーフケリー(計算値の半分)を使うのが実務では一般的デスヨ!!

3-2. ケリー基準でポジションサイズを計算する

コレ見てよ!!スゴくナイ!?✨ ケリー基準を使ったポジションサイジングの実装だよ!!バックテスト結果から直接パラメータを渡せるようにしてあるよネ♪

def calculate_kelly_fraction(
    win_rate: float,
    avg_win: float,
    avg_loss: float
) -> float:
    """
    ケリー基準によるポジション比率を計算する関数

    Parameters
    ----------
    win_rate : float
        勝率(0〜1の小数。例:0.55 は55%)
    avg_win : float
        平均利益(小数表記の正の値。例:0.08 は8%)
    avg_loss : float
        平均損失(小数表記の正の絶対値。例:0.04 は4%損失)

    Returns
    -------
    float
        推奨ポジション比率(0〜1の小数)。
        計算結果がマイナスの場合は0を返す(期待値がないため)
    """
    if avg_loss == 0:
        return 0.0

    b = abs(avg_win / avg_loss)  # ペイオフレシオ(平均利益÷平均損失)
    p = win_rate                 # 勝率
    q = 1.0 - win_rate           # 負け率

    kelly = (b * p - q) / b      # ケリー公式

    # 期待値がゼロ以下(トレードしない方がいい)なら0を返す
    return max(0.0, kelly)


def get_position_size(
    capital: float,
    kelly_fraction: float,
    half_kelly: bool = True
) -> float:
    """
    実際のポジションサイズ(USDT建て)を計算する関数

    Parameters
    ----------
    capital : float
        現在の資産額(USDT)
    kelly_fraction : float
        calculate_kelly_fraction() の返り値
    half_kelly : bool
        True の場合、ハーフケリー(ケリー値の半分)を使用する

    Returns
    -------
    float
        投入すべき資金量(USDT)
    """
    multiplier = 0.5 if half_kelly else 1.0
    position_ratio = kelly_fraction * multiplier
    # 安全のため最大25%以上は張らない
    position_ratio = min(position_ratio, 0.25)
    return capital * position_ratio


# バックテスト結果から直接計算する使用例
if len(trades_df) > 0:
    avg_win_pct  = trades_df[trades_df['profit'] > 0]['return_pct'].mean() / 100
    avg_loss_pct = abs(trades_df[trades_df['profit'] < 0]['return_pct'].mean()) / 100
    win_rate_dec = len(trades_df[trades_df['profit'] > 0]) / len(trades_df)

    kelly = calculate_kelly_fraction(win_rate_dec, avg_win_pct, avg_loss_pct)
    position_usdt = get_position_size(capital=10000, kelly_fraction=kelly, half_kelly=True)

    print(f"ケリー比率 (full) : {kelly * 100:.1f}%")
    print(f"推奨ポジション   : {position_usdt:.2f} USDT(ハーフケリー)")

フルケリーだとサ、理論上は最速で資産が増えるんだケドネ……ちょっとパラメータが外れると資産が激減しやすくて、メンタル的にもキツいんだヨネ💦 実務ではハーフケリー(ケリー値の半分)を使うのが定石デスネ!! min(position_ratio, 0.25) でキャップをかけてるのもミソで、いくら期待値が高くても25%超えのポジションは張らない設計にしてありマス✨

3-3. 最大ドローダウンによるトレード停止ロジック

最後に、仮想通貨リスク管理の実践としてトレード停止ロジックを実装してみよ!!コレ見てよ!!スゴくナイ!?✨ ドローダウンが一定水準を超えたら自動的にポジションを持たなくなるやつデスネ♪

class DrawdownGuard:
    """
    最大ドローダウンを監視してトレードを制御するクラス

    Parameters
    ----------
    initial_capital : float
        初期資産額
    max_drawdown_limit : float
        これを超えたらトレードを停止するドローダウン上限
        (正の値で指定。例:0.20 は20%)
    """

    def __init__(self, initial_capital: float, max_drawdown_limit: float = 0.20):
        self.initial_capital = initial_capital
        self.peak_capital = initial_capital
        self.max_drawdown_limit = max_drawdown_limit
        self.is_trading_allowed = True

    def update(self, current_capital: float) -> bool:
        """
        現在の資産額をもとにドローダウンを更新し、トレード可否を返す

        Parameters
        ----------
        current_capital : float
            現在の資産額

        Returns
        -------
        bool
            True: トレード可、False: トレード停止
        """
        if current_capital > self.peak_capital:
            self.peak_capital = current_capital

        drawdown = (self.peak_capital - current_capital) / self.peak_capital

        if drawdown >= self.max_drawdown_limit:
            self.is_trading_allowed = False

        return self.is_trading_allowed

    def get_current_drawdown(self, current_capital: float) -> float:
        """現在のドローダウン率を返す(0〜1の小数)"""
        if self.peak_capital == 0:
            return 0.0
        return (self.peak_capital - current_capital) / self.peak_capital


# 使用例
guard = DrawdownGuard(initial_capital=10000, max_drawdown_limit=0.20)

current_balance = 8500  # ある時点での残高
can_trade = guard.update(current_balance)
dd = guard.get_current_drawdown(current_balance)

print(f"現在のドローダウン : {dd * 100:.1f}%")
print(f"トレード可否       : {'可' if can_trade else '停止中'}")

ドローダウンが20%を超えたら「一旦止まれ!!」という仕組みデスネ🎵 これをバックテストのシミュレーションループ内に組み込むと、ドローダウン制限を考慮した、より現実的な仮想通貨リスク管理ができちゃいますヨ!! さっき作ったケリー基準のポジションサイジングとセットで使うと、「攻め」と「守り」が両立した戦略になるよネ✨


4. まとめ:CCXTバックテストとリスク管理の次のステップ

今回の第3回では、CCXTバックテストと仮想通貨リスク管理の2大テーマをやっちゃいましたネ♪ おさらいするとサ——

  • バックテストの全体フローfetch_ohlcv() でデータ取得 → pandas で整形 → シミュレーション → 指標算出
  • 評価指標の読み方:シャープレシオ・最大ドローダウン・プロフィットファクターの目安値を押さえておこう!!
  • シャープレシオ・最大ドローダウンの実装:再利用可能な関数として切り出すのがベストデスネ
  • ケリー基準によるポジションサイジング:フルケリーよりハーフケリーが実務では安全!!
  • ドローダウンガードによるトレード停止:仮想通貨リスク管理の最後の砦デスヨ!!

キミ、ここまでついてきてくれてありがとヨ!!😆 バックテストのコードを初めて動かしたとき、数字がズラーッと出てきた瞬間って、なんか嬉しいよネ✨ あの感覚がクセになって、それ以来どっぷりハマっちゃいましたヨ(笑)

次回(第4回)では、バックテストの結果を活かした自動売買システムへの展開と、リアルタイム監視の実装をやっていく予定デスネ🦝 どうぞお楽しみに!!

なおサ、「バックテストでは勝率60%・プロフィットファクター1.8だったのに、実運用に移したら微妙……」ってなっちゃうケースが多いんだヨネ😭 それは過学習だったり、手数料の見積もりが甘かったりすることが多いデスカラネ。今回実装したコードで、手数料・スリッページをきちんと織り込んで再チェックしてみてよ!!きっと新しい発見があるハズデスヨ!!

それではまた次回!!キミのトレードが良い結果になることを祈ってますヨ🦝✨

関連記事

CCXTを使って仮想通貨のトレードをしてみる(第4回):バックテストから本番運用へ
投資・トレード

CCXTを使って仮想通貨のトレードをしてみる(第4回):バックテストから本番運用へ

CCXTを使った仮想通貨ライブトレードの完全実装ガイド。Binance Testnetでのサンドボックス検証、CCXT ProのWebSocketによるリアルタイム取引、asyncioでの並列管理、Telegram通知・AWS EC2本番運用まで徹底解説。

CCXTを使って仮想通貨のトレードをしてみる(第2回):注文実行と基本戦略の実装
投資・トレード

CCXTを使って仮想通貨のトレードをしてみる(第2回):注文実行と基本戦略の実装

CCXTを使った仮想通貨の自動売買・注文実行を解説。成行注文・指値注文の実装方法、サンドボックスでの安全な練習方法、レートリミット対策まで初心者向けにPythonコードで丁寧に説明します。

コメント

0/2000