モンテカルロシミュレーションを使ったFXバックテスト分析

今回は、**モンテカルロシミュレーション** を用いてドル円（USD/JPY）のFX取引における最適な移動平均線とレバレッジを決定し、累積利益を最大化するバックテストの結果を共有します。

この記事では、以下を説明します：
1. モンテカルロシミュレーションの概要
2. 使用したスクリプト
3. 実験結果とその解釈
4. 実装方法と注意点

---

## 1. モンテカルロシミュレーションとは？

モンテカルロシミュレーションは、乱数を用いて多くの試行を繰り返し、最適なパラメータや条件を見つける手法です。今回は、以下のパラメータをランダムに生成し、バックテストの累積損益を最大化する組み合わせを探索しました。

### ランダムに生成したパラメータ
- **レバレッジ倍率**: 1.0 ~ 10.0
- **短期移動平均期間 (MA Short)**: 5 ~ 50
- **長期移動平均期間 (MA Long)**: 50 ~ 200

## 2. 使用したスクリプト

以下は、モンテカルロシミュレーションを用いたバックテストのスクリプトです。

<details><summary>スクリプト全文</summary>

``` Python
import yfinance as yf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# 修正版のfetch_fx_data関数
def fetch_fx_data(ticker, start_date, end_date):
    data = yf.download(ticker, start=start_date, end=end_date)
    # MultiIndexをフラット化
    data.columns = ['_'.join(col).strip() for col in data.columns.values]
    # 'Close'列を選択
    data = data[['Close_USDJPY=X']]
    data = data.rename(columns={'Close_USDJPY=X': 'Close'})  # 列名をシンプルに
    return data

# バックテスト関数
def backtest(data, leverage, ma_short, ma_long):
    data['Short_MA'] = data['Close'].rolling(window=ma_short).mean()
    data['Long_MA'] = data['Close'].rolling(window=ma_long).mean()
    data['Signal'] = np.where(data['Short_MA'] > data['Long_MA'], 1, -1)
    
    # 損益計算
    data['PnL'] = (leverage * data['Signal'] * data['Close'].pct_change()).fillna(0)
    total_pnl = data['PnL'].cumsum().iloc[-1]  # 累積利益
    return total_pnl

# モンテカルロシミュレーション
def monte_carlo_simulation(data, n_simulations):
    results = []
    for _ in tqdm(range(n_simulations)):
        leverage = np.random.uniform(1, 10)  # ランダムなレバレッジ倍率
        ma_short = np.random.randint(5, 50) # ランダムな短期移動平均
        ma_long = np.random.randint(50, 200) # ランダムな長期移動平均

        if ma_short >= ma_long:
            continue  # 短期移動平均が長期移動平均以上の場合はスキップ

        pnl = backtest(data, leverage, ma_short, ma_long)
        results.append((pnl, leverage, ma_short, ma_long))
    
    return sorted(results, key=lambda x: x[0], reverse=True)

# 結果の可視化
def plot_results(data, title="Backtest Result"):
    plt.figure(figsize=(12, 6))
    data['PnL'].cumsum().plot()
    plt.title(title)
    plt.xlabel("Date")
    plt.ylabel("Cumulative PnL")
    plt.grid()
    plt.show()
    
# メイン処理
NUM_SIMULATION = 50000

if __name__ == "__main__":
    # FXデータの取得 (ドル円: USD/JPY)
    ticker = "USDJPY=X"
    start_date = "2014-01-01"
    end_date = "2024-11-29"
    data = fetch_fx_data(ticker, start_date, end_date)

    # デバッグ: データの構造を確認
    # print(data.info())
    # print(data.head())

    # モンテカルロシミュレーションの実行
    simulation_results = monte_carlo_simulation(data, NUM_SIMULATION)

    # 最適な結果の表示
    best_result = simulation_results[0]
    print(f"Optimized Result: PnL={best_result[0]:.2f}, Leverage={best_result[1]:.2f}, MA Short={best_result[2]}, MA Long={best_result[3]}")

    # 最適なパラメータでバックテスト
    leverage = best_result[1]
    ma_short = best_result[2]
    ma_long = best_result[3]
    backtest(data, leverage, ma_short, ma_long)
    
    # 結果をプロット
    plot_results(data)
```

</details>

### 必要なライブラリ
```python
import yfinance as yf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from tqdm import tqdm
```

### データの取得
```python
def fetch_fx_data(ticker, start_date, end_date):
    data = yf.download(ticker, start=start_date, end=end_date)
    data.columns = ['_'.join(col).strip() for col in data.columns.values]
    data = data[['Close_USDJPY=X']].rename(columns={'Close_USDJPY=X': 'Close'})
    return data
```

### バックテスト
```python
def backtest(data, leverage, ma_short, ma_long):
    data['Short_MA'] = data['Close'].rolling(window=ma_short).mean()
    data['Long_MA'] = data['Close'].rolling(window=ma_long).mean()
    data['Signal'] = np.where(data['Short_MA'] > data['Long_MA'], 1, -1)
    data['PnL'] = (leverage * data['Signal'] * data['Close'].pct_change()).fillna(0)
    return data['PnL'].cumsum().iloc[-1]
```

### モンテカルロシミュレーション
```python
def monte_carlo_simulation(data, n_simulations=50000):
    results = []
    for _ in tqdm(range(n_simulations), desc="Running Monte Carlo Simulations"):
        leverage = np.random.uniform(1, 10)
        ma_short = np.random.randint(5, 50)
        ma_long = np.random.randint(50, 200)

        if ma_short >= ma_long:
            continue

        pnl = backtest(data, leverage, ma_short, ma_long)
        results.append((pnl, leverage, ma_short, ma_long))

    return sorted(results, key=lambda x: x[0], reverse=True)
```

### 結果の可視化
```python
def plot_results(data, title="Backtest Result"):
    plt.figure(figsize=(12, 6))
    data['PnL'].cumsum().plot()
    plt.title(title)
    plt.xlabel("Date")
    plt.ylabel("Cumulative PnL")
    plt.grid()
    plt.show()
```

### メイン処理
```python
if __name__ == "__main__":
    ticker = "USDJPY=X"
    start_date = "2014-01-01"
    end_date = "2024-11-29"
    data = fetch_fx_data(ticker, start_date, end_date)

    simulation_results = monte_carlo_simulation(data)

    best_result = simulation_results[0]
    print(f"Optimized Result: PnL={best_result[0]:.2f}, Leverage={best_result[1]:.2f}, MA Short={best_result[2]}, MA Long={best_result[3]}")

    leverage = best_result[1]
    ma_short = best_result[2]
    ma_long = best_result[3]
    backtest(data, leverage, ma_short, ma_long)
    
    plot_results(data)
```

---

## 3. 実験結果

実験条件：
- **対象ペア**: ドル円 (USD/JPY)
- **データ期間**: 2014年1月1日 ~ 2024年11月29日
- **試行回数**: 50,000回

最適な結果：
- **累積損益 (PnL)**: 8.72
- **レバレッジ倍率**: 9.96
- **短期移動平均 (MA Short)**: 6
- **長期移動平均 (MA Long)**: 71

### グラフ
以下は累積損益の推移を示したグラフです。

![graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/512829/ec82bb21-dfe8-516e-2014-3603fe41c8db.png)

この結果から、短期移動平均を6期間、長期移動平均を71期間に設定することで、最適な取引結果が得られました。

### 売買のタイミングと投資リターンの例

以下は、最適化されたパラメータで得られた**短期移動平均(MA Short) = 6**、**長期移動平均(MA Long) = 71**、**レバレッジ倍率 = 9.96** を使用した際の売買タイミングと、初期投資額1万円の場合のリターンを紹介します。

---

#### **売買のタイミング**

この取引戦略では、以下の条件で売買を行います：
- **買いエントリー（ロング）**: 短期移動平均（6期間）が長期移動平均（71期間）を上回ったとき
- **売りエントリー（ショート）**: 短期移動平均が長期移動平均を下回ったとき

例として、以下のようなドル円の価格推移があったとします。

| 日付       | 終値 (Close) | 短期移動平均 (6期間) | 長期移動平均 (71期間) | シグナル  | アクション   |
|------------|--------------|----------------------|-----------------------|-----------|-------------|
| 2024-01-01 | 130.00       | 130.05              | 129.90               | 1         | **買い**     |
| 2024-01-02 | 130.50       | 130.20              | 129.95               | 1         | 保持         |
| 2024-01-03 | 130.80       | 130.40              | 130.00               | 1         | 保持         |
| 2024-01-04 | 130.60       | 130.50              | 130.10               | 1         | 保持         |
| 2024-01-05 | 129.90       | 130.30              | 130.20               | -1        | **売り**     |

- **2024-01-01**: 短期移動平均(130.05)が長期移動平均(129.90)を上回り、**買い**ポジションをエントリー
- **2024-01-05**: 短期移動平均(130.30)が長期移動平均(130.20)を下回り、**売り**ポジションをエントリー

---

#### **初期投資額1万円の場合のリターン**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/512829/b85baaa2-5de8-42c3-d16d-4448bfd549d6.png)

---

#### **結果のポイント**

- 初期投資額1万円が、10年間の取引で約 **9.72倍** に増加しました
- この結果は過去データを基にしたシミュレーションで得られたものであり、実際の取引では市場条件や手数料などの影響を受ける可能性があります

---

#### **まとめ**

この取引戦略では、移動平均線のクロスを活用することで、効率的にトレンドを捉えることができました。最適なパラメータ（**MA Short=6, MA Long=71, レバレッジ=9.96**）を用いることで、累積損益 **8.72** という結果を得ました。

- **売買タイミング**: 短期移動平均と長期移動平均の交差ポイントを活用
- **初期投資額1万円の結果**: 最終リターンは約 **97,200円**

## 4. 注意点と次のステップ

### 注意点
- この結果は過去データに基づくものであり、将来の市場環境で同様の結果が得られる保証はありません
- スプレッドや取引手数料を考慮していないため、実際の取引ではパフォーマンスが低下する可能性があります

### 次のステップ
- **他の通貨ペア**やデータ期間で同様のシミュレーションを行い、結果を比較する
- 取引手数料やスプレッドを考慮したシミュレーションを追加
- 他のテクニカル指標（例: RSI, ボリンジャーバンド）との組み合わせを検討