移動平均線を使った株式自動売買のためのプログラムを書き始めた話。

目次。

 

 

はじめに。

前回、上場企業の株価を数年分、取ってくるというプログラムを書いた。株価データが得られたので、データをいじくり始めましたというのが今回の話。

blog.sun-ek2.com

 

同じく前回、早起きしてプログラミングするって書いたと思うが、書いた後から早起きできなくなった(なんでや)。今は、隙間時間を使って開発を進めている。

 

 

移動平均線。

前回も書いたが、しばらくは「7日でマスター 株チャートがおもしろいくらいわかる本」に書かれていることをとりあえず実装してみる。なんでこの本を選んだかというと、あんまり量が多くなく、スラスラ読めそうだったから。僕は、株に関する話とか、テクニカル分析の細かい話には興味がない。あんまり興味のないことに時間を割きたくないので、すぐ読めそうなこの本を選んだ。

移動平均線は、ものすごく有名なテクニカル指標。株式を買う前から移動平均線ぐらいは知っていた。n日移動平均線は、t-n+1日からt日(それぞれ株式市場の営業日)の終値の平均値を取って繋げた線のこと。

 

通常は、短期と長期の移動平均線を組み合わせて使う。短期移動平均線が長期移動平均線を上に突き抜けることを「ゴールデンクロス」、下に突き抜けることを「デッドクロス」と言い、それぞれ「買いサイン」、「売りサイン」を表す。

 

上記の本では、5日と25日の移動平均線を使っていたのでそれに倣う。取得した約4000社の株価データを使って約4年間、移動平均線を使って株式売買をしたらどうなるか、それぞれ個別に調べてみる。

 

なぜ、移動平均線を使ったプログラムを書こうと思ったかというと、実装が簡単そうだったから。移動平均線だけで株式売買をするのは、上記の本ではおすすめされていない。

 

けれども、まずは初めの一歩として…。

 

 

 

 

ソースコード。

使った言語は、Python。

blog.sun-ek2.com

import glob
import pandas as pd
import re
import numpy as np
import matplotlib.pyplot as plt
import numpy as np
import csv
from numba import jit


@jit
def prepare_data(file_source):
    stock_data = pd.read_csv(file_source)
    #この後、色々と処理しているけれど省略。
    
    return stock_data


@jit
def calc_nDayMovingAverageLine(stock_data_portion, n):
    np_stock_data_portion = stock_data_portion[['close']].values #closeは終値。
    np_nDayMovingAvergeLine =  np.zeros(len(np_stock_data_portion))
    for i in range(n-1, len(np_stock_data_portion)):
        np_nDayMovingAvergeLine[i] = np.sum(np_stock_data_portion[i-n+1:i+1])/n
    nDayMovingAvergeLine = pd.DataFrame(np_nDayMovingAvergeLine, index=stock_data_portion.index)
    
    return nDayMovingAvergeLine


def compare_nDayMovingAverageLines(line0, line1):
    diff_lines = pd.DataFrame(np.zeros(len(line0)), index=line0.index)
    diff_lines = line0 - line1
    if np.count_nonzero(line1 == 0) < np.count_nonzero(line0 == 0):
        diff_lines = -diff_lines
        
    return diff_lines


@jit
def find_intersections(diff_lines):
    np_diff_lines = diff_lines.values
    np_buy_or_sell_array = np.zeros(len(np_diff_lines))
    negative_flag = False
    for i in range(len(np_diff_lines)):
        if (np_diff_lines[i] < 0) and (negative_flag == False):
            negative_flag = True
            np_buy_or_sell_array[i] = -1 #sell
        if (np_diff_lines[i] > 0) and (negative_flag == True):
            negative_flag = False
            np_buy_or_sell_array[i] = 1 #buy
    buy_or_sell_array =  pd.DataFrame(np_buy_or_sell_array, index=diff_lines.index)
    
    return buy_or_sell_array


@jit
def evaluate(buy_or_sell_array, stock_data_portion):
    np_buy_or_sell_array = buy_or_sell_array.values
    np_stock_data_portion = stock_data_portion[['close']].values
    np_profit_loss = np.zeros(len(np_buy_or_sell_array))

    possess_flag = False
    purchase_price = 0
    result = [0,]
    for i in range(len(np_buy_or_sell_array)):
        if np_stock_data_portion[i] == 0:
            continue

        if possess_flag == False and np_buy_or_sell_array[i] == 1:
            possess_flag = True
            purchase_price = np_stock_data_portion[i]
            np_profit_loss[i] = -purchase_price

        if possess_flag == True and (np_buy_or_sell_array[i] == -1 or np_stock_data_portion[i] < 0.95*purchase_price):
            possess_flag = False
            result += (np_stock_data_portion[i]-purchase_price)/purchase_price
            np_profit_loss[i] = np_stock_data_portion[i]
    profit_loss =  pd.DataFrame(np_profit_loss, index=buy_or_sell_array.index)
    
    return profit_loss, result[0]


gl = glob.glob('ワイルドカードを使って株価データ一覧を取得。')
for f in gl:
    stock_data = prepare_data(f)
    DMAL_5 = calc_nDayMovingAverageLine(stock_data,5)
    DMAL_25 = calc_nDayMovingAverageLine(stock_data,25)
    diff_DMAL = compare_nDayMovingAverageLines(DMAL_5, DMAL_25)
    buy_or_sell_list = find_intersections(diff_DMAL)
    evaluation, result = evaluate(buy_or_sell_list, stock_data)
 

 

関数の引数、戻り値は、pandasのDataFrame。株価データは時系列データなので時間と株価の対応関係を常に保っておきたかった。関数の中では、DataFrameをnumpy arrayに変換して処理している。最初は、「pandasはnumpyのラッパークラスみたいなもんやから、全部DataFrameで実装して、numba.jitでコンパイルすればいいやろ」なんてお気楽に考えていたが、なんとまあ、、、クソ遅い。

関数の中身をnumpy arrayにしてnumba.jitでコンパイルしたら、3時間以上かかっていた処理が3分ぐらいで終わった。

 

「関数compare_nDayMovingAverageLinesの戻り値を引数にして直接評価したらいいのでは?」と思った人もいるかもしれないが、ここでは関数find_intersectionsを一旦挟んでいる。関数evaluateは、移動平均線を用いたアルゴリズム以外でも使いたいので、buy_or_sell_arrayをインターフェイスにしている。

 

 

 

 

結果・考察。

(正規化)最終損益。

ゴールデンクロスになったら株を買って、デッドクロスになったら株を売るというのを数年間繰り返した場合、損益はどうなるのか、個別に約4000社について調べてみた。

(正規化)最終損益は、売値と買値の差分を買値で割ってすべての売買を足し合わせた値。要は、累計損益を累計購入金額で割っただけ。

 

横軸は、正規化された最終損益で縦軸(対数)は頻度。ついでに購入金額から株価が5%下落したら得るというロスカット(黒色の棒グラフ)をしてみたが、思ったより影響は少ない。。。

f:id:sun_ek2:20190621222858p:plain

 

正の方向にテーリングしたガウシアン?と言っても、正の領域の面積と負の領域の面積は、あんま変わらない…。つまり適当に銘柄を選んで、ゴールデンクロス/デッドクロスに愚直に従って株を売買してもそんなに儲かりませんよ、ってこと。ロスカットすると致命的な損失は抑えれられるというのがこの結果からわかる。

 

 

高利益上位100銘柄の(正規化)損益の時系列変化。

横軸は日付。縦軸は(正規化)損益。目盛にあんまり意味が見いだせなかったのでグラフから取り除いている。大事なのは、グラフの形状。

f:id:sun_ek2:20190621222910p:plain

 

見ての通りステップ関数のようになっているグラフ(No. 1, 5, 9など)と線形増加しているグラフ(No. 44, 64, 68など)がある。ほぼ、ステップ関数まみれやないかいっ。

ステップ関数のような形状になっている銘柄は、何かしらの出来事あって株価が急上昇したと考えられる。そして、この急上昇をゴールデンクロス/デッドクロスを使ったアルゴリズムが拾ってきている。この株価急上昇は、外的な要因に依るところが大きいので再現性は取れないであろう。よって高利益ではあるが、ステップ関数のような形状になっている銘柄でゴールデンクロス/デッドクロスを用いた株式売買をしても利益はおそらく出ないだろう。

逆にグラフの形状が線形になっている銘柄は、再現良く利益を出せると期待できる。

 

 

 

 

今後。

  • 損益の時系列グラフがステップ関数様になっている銘柄を取り除く。
  • 5日、25日移動平均線の組み合わせ以外にも試してみる。
  • 移動平均線を3本、4本組み合わせて使った場合、どうなるの?
  • 複数(2本以上)の組み合わせのn日移動平均線を用意してアンサンブル学習させる。

 

  • 保有銘柄、口座残高を取得するプログラムを書く(ほぼ書いた)。
  • 株の売り注文・買い注文を自動で行うプログラムを書く。
  • 株価データ取得→データ解析→売買銘柄決定→注文の流れを完全自動化。

 

  • 最近、場の量子論の本を読んでいるのだが、そこでは時空間に住んでいる住人がフーリエ変換を使ってエネルギー-運動量空間によくお邪魔しているところを見かけるので、フーリエ変換を使ったコード書きたい(アイデアは頭の中にいくつかある)。

 

ブログランキング・にほんブログ村へ
にほんブログ村