48
59

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonでボリンジャーバンド、売買シグナル、バックテスト

Last updated at Posted at 2023-06-06

株価は95.4%の確立でボリンジャーバンド±2σの範囲内に収まる

 エンジニア未経験、Qiita覚えたので初投稿

仮説 4.6%でしか負けないならボリンジャーバンドで売買すれば絶対に勝てる

条件 初期資本100万円、1ポジション100株、手数料0、副ポジション無し、25日移動平均線を基準

使用ライブラリ

  • yfinance 株価取得
  • Pandas データフレーム
  • matplotlib.pyplot グラフ
  • tqdm プログレスバー
  • datetime Timestampオブジェクト
  • os csv保存

処理順序

  1. 株価取得
  2. 移動平均線、 標準偏差、ボリンジャーバンド、乖離率の算出
  3. 売買ルール制定、バックテスト
  4. リターンの算出
  5. グラフ化

必要なライブラリのインストール

install
    pip install yfinance
    pip install pandas
    pip install matplotlib
    pip install tqdm

株価取得

(株)みずほフィナンシャルグループ【8411】を取得する

取得する銘柄コードを格納
    Scraping_Stocks = [
        "8411",
    ]
data_listに株価取得
    import yfinance
    import pandas as pd
    from tqdm import tqdm
    
    data_list = []
    for code in tqdm(Scraping_Stocks):
        tmp = yfinance.download(f"{str(code)}.T", progress=False)
        tmp["code"] = code
        data_list.append(tmp)

データフレームを結合し、中身を確認する

    df = pd.concat(data_list) #データフレームの結合
    df #データの確認

image.png
20年分の株価データを取得することが出来た
調整後終値をプロットする

"Adj Close" = 調整後終値
    import matplotlib.pyplot as plt
    df["Adj Close"].plot()

image.png
2007年頃をピーク、2012年がボトムで、直近上昇傾向

調整後終値 = 株式分割前の終値を株式分割後の終値に調整したもの

移動平均線、 標準偏差、ボリンジャーバンド、乖離率の算出

25日移動平均線を基準とする
    window = 25  # 移動平均のウィンドウサイズ
    df['MA'] = df['Adj Close'].rolling(window=window).mean() #移動平均線
    df['StdDev'] = df['Adj Close'].rolling(window=25).std() #標準偏差
    df['Deviation'] = (df['Adj Close'] - df['MA']) / df['StdDev'] #移動平均線からの乖離率
    df['Bollinger bands+2'] = df['MA'] + (2*df['StdDev']) #ボリンジャーバンド+2σ
    df['Bollinger bands-2'] = df['MA'] - (2*df['StdDev']) #ボリンジャーバンド-2σ

データを視覚的に確認する
image.png
移動平均線が算出されるまでは NaN値 となっている

20年分だとグラフが潰れて確認できない。
ロシアのウクライナ進行付近、2022年1月から2023年6月までを範囲指定しグラフ化する

プロットの範囲指定
    start_date = '2022-01-01'
    end_date = '2023-06-06'
    
    # 範囲指定のためのスライシング
    filtered_data = df.loc[start_date:end_date]

標準偏差 

データのプロット
    filtered_data['StdDev'].plot()
    plt.show() # プロットの表示

戦争よりも金融危機のほうが反応している、直近は40円程度の利確は積極的に狙えそう
image.png
調整後終値、移動平均線、ボリンジャーバンド

データのプロット
    filtered_data[['Bollinger bands+2','MA','Bollinger bands-2','Adj Close']].plot()
    plt.show() # プロットの表示

ほぼボリンジャーバンドの内側に収まっているように見える
image.png
移動平均線からの乖離率

データのプロット
    filtered_data[['Deviation']].plot()
    plt.show() # プロットの表示

±2σの範囲外で取引が終わる日数 522日間 × 4.6% = 24日間
24日間よりも多く見える...と思う
image.png

売買ルール制定、バックテスト

売買ルール

  • cash100万円
  • 1lot100株
  • 手数料0
  • ポジションを有していたら反対売買のみ行う
今日をTimestampオブジェクトに変換
    import datetime
    today = datetime.date.today()
    today = pd.Timestamp(today)  # 日付をTimestampオブジェクトに変換
売買シグナル、ポジション変数を初期化
    df['Signal'] = 0
    df['Position'] = 0
バックテストの実行
for i in range(25, len(df)): #24行目以前は 移動平均線等が NaN のため25行目から最終行まで
    if df.index[i] == pd.Timestamp(today.date()):
        break
    
    df.loc[df.index[i], 'Position'] = df.loc[df.index[i-1], 'Position']
    if df['Adj Close'].iloc[i] <= df['Bollinger bands-2'].iloc[i]:
        if df['Position'].iloc[i] == 0:  # ポジションがない場合にのみ買いシグナルを発生
            df.loc[df.index[i], 'Position'] = 1
            df.loc[df.index[i], 'Signal'] = 1
        elif df['Position'].iloc[i] == -1:  # 売りポジションがある場合には売りポジションをクローズ
            df.loc[df.index[i], 'Position'] = 0
            df.loc[df.index[i], 'Signal'] = 1
    elif df['Adj Close'].iloc[i] >= df['Bollinger bands+2'].iloc[i]:
        if df['Position'].iloc[i] == 0:  # ポジションがある場合にのみ売りシグナルを発生
            df.loc[df.index[i], 'Position'] = -1  # ポジションを売りに切り替え
            df.loc[df.index[i], 'Signal'] = -1
        elif df['Position'].iloc[i] == 1:  # 買いポジションがある場合には買いポジションをクローズ
            df.loc[df.index[i], 'Position'] = 0
            df.loc[df.index[i], 'Signal'] = -1
            
df = df.drop(df.index[i+1:])  # i行目以降の行を削除

バックテストが正しく実行されているか確認する
移動平均線からの乖離率が+2以上又は-2以下でデータを抽出

標準偏差 +2以上 or -2以下
filtered_data = df[(df['Deviation'] >= 2) | (df['Deviation'] <= -2)][['Deviation','Signal','Position']]
filtered_data.tail(10)

image.png
乖離率が -2以下の時に買いシグナル、買いポジション
乖離率が -2以下だが買いポジションを持っているのでシグナルが出ていない
乖離率が +2以上なので売りシグナル、ポジション解除
乖離率が +2以上なので売りシグナル、売りポジション
正しく動作している

リターンの算出

初期資本 100万年、1ポジション = 100株
    capital = 1000000
    position_size = 100
株数変化列、株式資産、キャッシュ残高、総資産、リターン、累積リターンの算出
df['Shares'] = df['Position'] - df['Position'].shift()  # 株数変化列を作成
df['Shares'].fillna(0, inplace=True)  # 株数変化列の最初の値を0に設定

df['Portfolio'] = df['Adj Close'] * df['Position'] * position_size  # ポートフォリオ価値列を作成
df['Portfolio'].iloc[0] = 0  # 最初の日のポートフォリオ価値を0に設定

df['Cash'] = capital - (df['Shares'] * df['Adj Close'] * position_size).cumsum()  # キャッシュ残高列を作成
df['Value'] = df['Cash'] + df['Portfolio']  # 総資産列を作成

df['Returns'] = df['Value'].pct_change()  # リターン列を作成
df['Cumulative Returns'] = (1 + df['Returns']).cumprod()  # 累積リターン列を作成

Shares = 株数変化列
Portfolio = 株式資産
Cash = キャッシュ残高
Value = 総資産
Returns = リターン
Cumulative Returns = 累積リターン

結果を確認してみる

バックテストの結果を表示
    print('Cumulative Returns:', df['Returns'].sum())
    print('Return:', df['Returns'].sum() * capital)  # リターンの計算

Cumulative Returns: -0.26667785862970095
Return: -266677.85862970096
視覚的に確認する

プロットの設定
    plt.plot(df[['Returns','Cumulative Returns']])
    plt.legend(df[['Returns','Cumulative Returns']])
    plt.show()

image.png
2006年から2008年にかけて上昇、
上記以外の期間は損失を出している

結論

-26万6000円
ボリンジャーバンドだけだと売買シグナルとしては使えない。

最後までお読みいただき、ありがとうございました。

※条件を変えることで結果は変わります。
1. 銘柄を変える
2. ポジションを複数持つ
3. 初期資本を多くする
4. ロット数の調整
5. 損切の制定 etc...

48
59
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
48
59

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?