伝説のトレーダー集団が使った投資戦略を検証してみる

## 伝説のトレーダー集団「タートルズ」について

> ウォール街で伝説的トレーダー集団「タートルズ」―
彼らはプロの投資家による指導により、素人がトレーダーになれるかどうか？という実験の結果生まれたトレーダー集団でした。
> 【[タートルズ投資とは？そのルールと概要](https://www.ig.com/jp/trading-strategies/turtle-trading--what-is-it-and-what-are-the-rules--181207)より引用】

要するにズブズブの素人を伝説のトレーダー集団に仕立てた、すごい投資戦略ということです。
（つまりこの戦略使えば誰でも大金持ちってコト....!?!?）

今回はこのすごい投資戦略が本当に通用するのか検証したいと思います。

## どうやって検証するの？（バックテストとは？）

検証はバックテストを実施することで行います。

バックテストとは過去のデータを使って、その投資戦略がどの程度良いルールなのか（どのぐらい利益を出せるか）を検証することです。
この過程を行うことで、実際に売買する前にその買い方が本当に適切なのか、判断することができます。

具体的な実装としてはPythonのライブラリを使うことで簡単に検証することができます。

## ライブラリのインストール
主に下記の３つを使って検証します。

```shell
pip install backtesting # 実際にバックテストを実施する際のライブラリ
pip install yfinance # 最新の株式情報を取得するライブラリ
pip install stock-dataframe # テクニカル指数を計算するためのライブラリ
```

<details><summary>ライブラリの詳細、動かし方</summary>


### [backtesting.py](https://kernc.github.io/backtesting.py/)
Backtesting.pyは、過去データで取引戦略の実行可能性を推測するためのフレームワーク
収益、損益のシュミレーションはこれでできるが、テクニカル指数の計算はできないので、別途こちら側で計算する必要がある。

公式が用意している[サンプル](https://kernc.github.io/backtesting.py/doc/examples/Quick%20Start%20User%20Guide.html)

### [yfinance](https://pypi.org/project/yfinance/)
過去の市場データを取得するためのライブラリ
```python
# 5年分のナスダック100指数を取ってくる例
response = yfinance.download(
    tickers="NDX", # ナスダック100指数
    period="5y",
    interval="1d",
)
```

### [stock-dataframe](https://pypi.org/project/stock-dataframe/)
テクニカル指数を計算するためのライブラリ
今回は使いませんでしたが、便利なので他のトレード手法を検証するときは利用しています。
```python
# yfinanceで取ってきた値をヘッダ名が違うので、変換する関数を作る
def convert_df_to_stock_df(df: pd.DataFrame) -> StockDataFrame:
    sdf = df.copy()
    sdf.rename(
        columns={
            "Open": "open",
            "High": "high",
            "Low": "low",
            "Close": "close",
            "Adj Close": "amount",
            "Volume": "volume",
        },
        inplace=True,
    )
    sdf.index.names = ["date"]
    return StockDataFrame(sdf)


df = convert_df_to_stock_df(arr)
df['rsi'] # RSI
df['macd'] # MACD
df["macds"] # MACD signal
# 他にも有名なテクニカル指数はだいたい利用できる
```
</details>

## タートルズ投資の具体的な投資方法について

>①マーケットが過去20日間（チャート上の20本の足）の最高値（最安値）を取ったら、新規に買う（新規に売る）。
②マーケットが自分の建玉に逆行して過去10期間（10本の足）の最安値（最高値）を取ったら、その玉を仕切る。
③新規に建玉をした後、もしマーケットが逆行して引かされたなら、資金の2％を厳格な仕切りポイントとして用いて損切りしなさい。
④利益目標を置いてはいけない。利が乗った建玉を手仕舞う唯一の方法は、マーケットが逆行したときだけである。
【[タートルズ投資とは？そのルールと概要](https://www.ig.com/jp/trading-strategies/turtle-trading--what-is-it-and-what-are-the-rules--181207)より引用】

過去N日間の最高値と最安値を示すインジケータとして、ドンチャンチャネル（以下DC）と言われるインジケータが存在しており、こちらを利用します。
資金管理の話が出てくると複雑になるので、③は割愛します
※ 記事外で③を含め検証しましたが大きくパフォーマンスが向上することはありませんでした。


### 実際にやってみる

<details><summary>実際のコード</summary>

ドンチャンチャネルの計算はstock-dataframeでは提供されていなかったので、自分でつくります。
```python
def DC(arr: pd.DataFrame, payload=20):
    count = 0

    max = []
    min = []

    df = convert_df_to_stock_df(arr).copy()
    for index, data in df.iterrows():

        if payload > count:
            # 計算できていない間は反応しないように適当に広げておく
            max.append(data["high"] + 10)
            min.append(data["low"] - 10)
            count += 1
            continue

        # 当日は含めないので-1しとく
        range = df[count - payload : count - 1]
        max.append(range["high"].max())
        min.append(range["low"].min())
        count += 1

    df["max"] = max
    df["min"] = min

    return (df["min"], df["max"])
```

売買パターンクラス
```python
class My_Strategy(Strategy):
    entry_payload_day = 20
    stop_loss_payload_day = 10

    stop_loss = 5  # percent

    day = 0

    def init(self):
        self.atr = self.I(ATR, self.data.df)

        (self.dc_min, self.dc_max) = self.I(DC, self.data.df, self.entry_payload_day)

        (self.dc_half_min, self.dc_half_max) = self.I(
            DC, self.data.df, self.stop_loss_payload_day
        )

    def next(self):
        self.day += 1

        # 計算できていない場合トレードしない
        if len(self.data.index) < self.entry_payload_day:
            return

        # 手仕舞い (半分のDCの高値または安値を下回った場合)
        if (self.dc_half_min[-1] > self.data.Low[-1] and self.position.is_long) or (
            self.data.High[-1] > self.dc_half_max[-1] and self.position.is_short
        ):
            self.position.close()
            return

        # 買い注文
        if self.data.Close[-1] > self.dc_max[-1] and not self.position.is_long:
            self.buy(sl=self.data.Close[-1] * (1 - (self.stop_loss / 100)))  # 損切り
            return

        # 売り注文
        if self.dc_min[-1] > self.data.Close[-1] and not self.position.is_short:

            self.sell(sl=self.data.Close[-1] * (1 + (self.stop_loss / 100)))  # 損切り
            return
```

</details>

ナスダック100指数 2021-11-06 ~ 2022-11-09 で検証

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2878085/32ef3762-9925-88dd-6e7d-66660e37ab6a.png)

<details><summary>詳細なパフォーマンス</summary>

```text
Start                     2021-11-08 00:00:00
End                       2022-11-08 00:00:00
Duration                    365 days 00:00:00
Exposure Time [%]                       66.80
Equity Final [$]                  10590383.51
Equity Peak [$]                   11886387.73
Return [%]                               5.90
Buy & Hold Return [%]                  -32.30
Return (Ann.) [%]                        5.88
Volatility (Ann.) [%]                   26.32
Sharpe Ratio                             0.22
Sortino Ratio                            0.37
Calmar Ratio                             0.50
Max. Drawdown [%]                      -11.66
Avg. Drawdown [%]                       -4.90
Max. Drawdown Duration      120 days 00:00:00
Avg. Drawdown Duration       26 days 00:00:00
# Trades                                    9
Win Rate [%]                            55.56
Best Trade [%]                           9.47
Worst Trade [%]                         -6.00
Avg. Trade [%]                           0.64
Max. Trade Duration          60 days 00:00:00
Avg. Trade Duration          26 days 00:00:00
Profit Factor                            1.37
Expectancy [%]                           0.79
SQN                                      0.32
_strategy                         My_Strategy
_equity_curve                             ...
_trades                      Size  EntryBa...
dtype: object
```
</details>

一年間で6%資産がふえました！
特に下落局面でもしっかり利益を出せているのは良さそう！

Equityが株式と買付余力を含めた資産額で、このシュミレーションでは95%以下になることはありませんでした。

Profit/Lossは取引で何％損か、得ができたかを表示しており、この売買ルールでは最大6%の損と最大で10%の利益が出せる取引ができることを確認できました。

グラフのオレンジ線が過去10日の最高値と最安値
青色線が過去20日の最高値と最安値となっています。

ただこのトレードがどのぐらい良いのか、客観的に示す値がなく、なんとなく良さそうということしかわかりません。
**そこでSQNという指数を使って評価してみます。**

### SQN（システム品質） について

SQNとはSystem Quality Number（システム品質数値計算式）のことでトレードの品質を期待値とその標準偏差の比率に、取引数の平方根をかけたものです。
こちらを利用することで、ひと目で優れた売買ルールか判断することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2878085/8175691c-722d-ef50-528b-d48f36109a80.png)
【[Van Tharp’s SQN](https://indextrader.com.au/van-tharps-sqn/)より引用】
具体的には上記のスコアが参考の値となっており、2以上を目指すのが一般的です。
（5以上だと過学習や過度な最適化をしている可能性がある。）


詳しくは[Do You Know Your System Quality Number?](https://tradingtact.com/system-quality-number/)を参照

さきほどのSQN値を確認するとSQNは`0.32`でした。

えーっと表と比べて確認すると...
😇😇 `Very Hard to Trade（取引が非常に難しい）` 😇😇


### チューニングするで！！

**あかんこのままやと破産してまう、、、**

ということで、いくつかパラメータを設定して用意して、スコアを改善できるように修正をしてみます。
SQNが最大になるようにチューニングをしてみます。

```python
# 最適化
optimize = bt.optimize(
    maximize="SQN", # SQNが最大になるようにチューニング
    stop_loss=range(1, 10, 1), # 損切りするパーセント、例えば5だと買付金額の5%以上の損失が出た時点で損切りを行います。 
    entry_payload_day=[20, 55], # DCに関する値、過去何日間を20日と55日で試す
    stop_loss_payload_day=range(10, 35, 1), #　半分の日数も色々変えて試す
    constraint=lambda p: p.stop_loss_payload_day <= (p.entry_payload_day / 2), # 計算量をへらすため、不要なパターンを除く
)
bt.plot()
print(optimize)
print(optimize._strategy)
```

#### 結果
※ 売買ルール以外の条件はすべて同じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2878085/ae53e8ca-4a93-19bb-7425-44036298beab.png)


<details><summary>詳細なパフォーマンス</summary>

```text
Start                     2021-11-08 00:00:00
End                       2022-11-08 00:00:00
Duration                    365 days 00:00:00
Exposure Time [%]                       59.68
Equity Final [$]                  11764104.59
Equity Peak [$]                   12455873.61
Return [%]                              17.64
Buy & Hold Return [%]                  -32.30
Return (Ann.) [%]                       17.57
Volatility (Ann.) [%]                   26.92
Sharpe Ratio                             0.65
Sortino Ratio                            1.25
Calmar Ratio                             1.65
Max. Drawdown [%]                      -10.62
Avg. Drawdown [%]                       -4.10
Max. Drawdown Duration       80 days 00:00:00
Avg. Drawdown Duration       22 days 00:00:00
# Trades                                   11
Win Rate [%]                            45.45
Best Trade [%]                           9.47
Worst Trade [%]                         -2.89
Avg. Trade [%]                           1.49
Max. Trade Duration          60 days 00:00:00
Avg. Trade Duration          19 days 00:00:00
Profit Factor                            2.85
Expectancy [%]                           1.57
SQN                                      1.16
```
</details>



```text
My_Strategy(stop_loss=2,entry_payload_day=20,stop_loss_payload_day=10)
```
最適化したパラメータとしては上記の通りになり、もともと5%で損切りしていたところを2%に変更したようです。

SQNは`1.16`となり、グラフに当てはめると`Avarage(平均的)`となりました。よかった！！
ちなみに1年で資産は18%ほど増えました。

改善できてよかったことと、少しでも値が異なると大きくパフォーマンスが変化することがわかりました。

### この投資戦略が他の日付でも通用するか検証してみる

```text
2017-11-27 ~ 2019-11-20 -1.473388747568901
2018-04-19 ~ 2020-04-14 0.38332392289953
2019-12-04 ~ 2021-11-29 0.7610136718180349
2020-11-25 ~ 2022-11-10 0.581597865366391
2020-07-28 ~ 2022-07-22 -0.4260434745659967
```

SQN的には良い結果とはいえませんでしたが、全体を通りして見ると大きく損をすることはありませんでした。
この辺りはパラメータを更に最適化することで、特定の相場に強くすることも、
どの相場や年代にでも通用するようにするのも自由なので最適化していきたいところです。

### やってみた所感

さすがに誰でも大金持ちとは行きませんでしたが、株式相場が不調な現在でも収益をあげれるのは悪くないのかなと思いました。

パラメータを更に最適化したり、この買い方以外に複数の買い方を用意して、相場によって切り分けたり、
ボラティリティによってエントリーする金額を変更するなどアイディアはまだまだあるので、引き続き検証していきたいです。