目次。
この文章を読んで、面白い!役に立った!...と思った分だけ、投げ銭していただけると嬉しいです。
【宣伝】ギターも歌も下手だけど、弾き語りをやっているので、よければ聴いてください。
はじめに。
株式自動売買プログラム開発関連の文章は、以下のカテゴリーにまとめているので、興味のある方はどうぞ。
前回は、移動平均線ペア間の平均利益率月間推移に関する相関を調べてみた。
今回は、移動平均線を2本から3本に増やしてみた話。
移動平均線ペアが一組だけだと、株式売買で利益を上げ続けることは難しい。そのため、複数の移動平均線を使ったアルゴリズムを作りたいなと思い立ったのが前回。そして、移動平均線ペアを増やしておくと、あるペアが利益を出せない局面に突入したとしても、別のペアがそれを補ってくれるのではないかと考えた。
けれども、同じタイミングで利益が出て、同じタイミングで損失が出る移動平均線ペアを選んでも意味がない。両者は、相補的でないといけない。そんなことを考えつつ、前回は色々な移動平均線間の相関を調べていた。
一応、結果は出たが、正直あまりぱっとしない。それに加えて、その後、どのように、この結果を使っていくか少し行き詰っていた。
そして、「複数の移動平均線ペアの相関云々というややこしい話に持っていくのではなく、シンプルに移動平均線ペアを任意に2組、選んだ時に株式売買の損益がどのようになるか網羅的に解析した方が手っ取り早いんじゃないか」と思うようになった。
そして、さっそくプログラムを組んだ。2組の移動平均線、つまり4本の移動平均線を使った株式売買タイミング判断のアルゴリズム。一応、プログラムを書き終わり、網羅的に解析を始めたのだが、4本の移動平均線の組み合わせの数が多すぎて、全く計算が終わらない、終わらない。
そのため、移動平均線の数を4本から3本に減らした。それでも、計算が終わるまでに3日以上かかったが。
アルゴリズムの説明。
株価が高すぎる銘柄を解析から除外。
具体的には、直近の100営業日間での終値の平均が2500円を超えるものを除外した。知っている人も多いと思うが、株は、1株から買うことはできない。大体、100株単位で売られている。1株2500円だった場合、この銘柄を買うためには、最低25万円必要である。
リスク分散の意味で、僕は多くの銘柄を少量ずつ保持している。あまり、一つの銘柄にお金をつぎ込みたくない。僕の資産規模で、2500円以上の銘柄(25万円)を買うと、リスクをあまり分散できない。
出来高順に並び変え。
前に紹介した解析で、出来高が程よく大きいほど、移動平均線ペアを使ったアルゴリズムの成績がよくなることが分かった。
今回も出来高ごとに解析を分けて行う。最初は、2000銘柄を500銘柄ずつ分けて解析していたが、出来高順位1501位~2000位の銘柄では、どの移動平均線を使ってもあまり利益が得られないことが分かったので、もう少し絞って、1600銘柄を400銘柄ずつ分けて解析している。
株式の売買判断。
基本は、短期移動平均線が長期移動平均線を下から上に突き破ったら「買い」で、逆に上から下に突き破ったら「売り」。
ここに第三の移動平均線、中期移動平均線を加える。短期(中期)移動平均線が長期移動平均線よりも上にある状態で、中期(短期)移動線が長期移動線を下から上に突き破ったら「買い」、短期・中期移動平均線のどちらか一方だけが長期移動平均線を上から下に突き破ったら「売り」になる。(+終値が買値よりも10%以上下落したら、損切を行う)
短期・中期移動平均線の2本がともに長期移動平均線の上部になければならないので、短期・長期移動平行線ペアの場合よりも「買いシグナル」が発生しにくい。一方で、どちらか一方だけが長期移動平均線の下部にきた瞬間に株式を売るので、「売りシグナル」の頻度は「買いシグナル」の減少の影響を除けば、そんなに変わらない。
前よりも株の購入に慎重になるアルゴリズム。そして、移動平均線ペアを使ったアルゴリズムの天敵である「だまし」に強いアルゴリズムだと思う。
ソースコード。
パソコンから見た方が見やすいかも。
import XXXXX #昔の記事を参照のこと。 import glob import datetime import re import numpy as np import seaborn as sns import pandas as pd import matplotlib.pyplot as plt from matplotlib import rcParams storage_uri = 'YYYYY' gl = glob.glob(storage_uri+'outputs\\stock_price_data\\day\\*.csv') def evaluate2(buy_or_sell_array, stock_data_portion): np_buy_or_sell_array = [ele.values for ele in buy_or_sell_array] np_stock_data_portion = stock_data_portion[['close']].values np_profit_loss = np.zeros(len(np_buy_or_sell_array[0])) possess_flag = False buying_timing = 0 purchase_price = 0 tmp_date = datetime.date(year=2010, month=1, day=1) #result[0] 与えられた期間中の買値で規格化した最終損益。 #result[1] 株の平均保有期間。 #result[2] 売買回数。 result = [0.0, 0, 0] index_list = buy_or_sell_array[0].index for i in range(len(np_buy_or_sell_array[0])): for j in range(len(np_buy_or_sell_array)): if np_stock_data_portion[i] == 0: continue if np_buy_or_sell_array[j][i] == 1: buying_timing += 1 if np_buy_or_sell_array[j][i] == -1: buying_timing -= 1 if possess_flag == False and np_buy_or_sell_array[j][i] == 1 and buying_timing == 2: possess_flag = True purchase_price = np_stock_data_portion[i] np_profit_loss[i] = -purchase_price date_1 = str(index_list[i]) tmp_date = datetime.date(year=int(date_1[0:4]), month=int(date_1[4:6]), day=int(date_1[6:8])) if possess_flag == True and (np_buy_or_sell_array[j][i] == -1 or np_stock_data_portion[i] < 0.90*purchase_price): possess_flag = False result[0] += ((np_stock_data_portion[i]-purchase_price)/purchase_price)[0] np_profit_loss[i] = np_stock_data_portion[i] date_1 = str(index_list[i]) result[1] += (datetime.date(year=int(date_1[0:4]), month=int(date_1[4:6]), day=int(date_1[6:8])) - tmp_date).days result[2] += 1 if result[2] == 0: result[1] = float('inf') else: result[1] = result[1]/result[2] profit_loss = pd.DataFrame(np_profit_loss, index=index_list) return profit_loss, result stock_volume_list = np.load(storage_uri + 'outputs\\stock_volume_list.npy').astype(np.float) stock_volume_ranking = np.zeros((len(stock_volume_list[:,0]), 2), dtype=int) stock_volume_ranking[:, 0] = stock_volume_list[:,0] stock_close_list = np.load(storage_uri + 'outputs\\stock_close_list.npy').astype(np.float) for i in range(100): stock_volume_ranking[:, 1] += np.argsort(np.argsort(stock_volume_list[:,i+1])) stock_volume_ranking_code = stock_volume_ranking[np.argsort(stock_volume_ranking[:, 1]), 0][::-1].tolist() stock_close_list = np.load(storage_uri + 'outputs\\stock_close_list.npy').astype(np.float) for scl in stock_close_list: if np.mean(scl[1:]) > 2500: stock_volume_ranking_code.remove(int(scl[0])) stock_volume_ranking_code = np.array(stock_volume_ranking_code) for f in gl[:]: file_name = f.replace(storage_uri+'outputs\\stock_price_data\\day\\', '') if '-.csv' in f: continue if not(int(re.sub('-.*', '', file_name)) in stock_volume_ranking_code[:2000]): continue stock_data = XXXXX.prepare_data(f) nDMAL = [] for i in range(1, 31): nDMAL.append(XXXXX.calc_nDayMovingAverageLine(stock_data, i)) for short in range(1, 25): for middle in range(short+1, 26): for long in range(middle+5, 31): intersections_nDMAL_1 = XXXXX.find_intersections(XXXXX.compare_nDayMovingAverageLines(nDMAL[short-1], nDMAL[long-1])) intersections_nDMAL_2 = XXXXX.find_intersections(XXXXX.compare_nDayMovingAverageLines(nDMAL[middle-1], nDMAL[long-1])) buy_or_sell_array = [intersections_nDMAL_1, intersections_nDMAL_2] temp = re.sub('-.*', '', file_name) + ',' + str(short) + ',' + str(middle) + ',' + str(long) + ',' evaluation, result = evaluate2(buy_or_sell_array, stock_data) temp += str(result[0]) + ',' + str(result[1]) + ',' + str(result[2]) with open(storage_uri + 'outputs\\profit_holdingPeriod_numberBuyingSelling2.txt', mode='a') as file_1: print(temp, file=file_1) profit_holdingPeriod_numberBuyingSelling = None with open(storage_uri + 'outputs\\profit_holdingPeriod_numberBuyingSelling2.txt', encoding="utf-8_sig") as f: profit_holdingPeriod_numberBuyingSelling = [s.replace('\n','').split(',') for s in f.readlines()] profit_holdingPeriod_numberBuyingSelling_2 = np.zeros((4, 4, 24, 24)) DMALs_3 = dict() for phn in profit_holdingPeriod_numberBuyingSelling: if not(int(phn[0]) in stock_volume_ranking_code[0:1600]): continue cl = -1 if int(phn[0]) in stock_volume_ranking_code[0:400]: cl = 0 if int(phn[0]) in stock_volume_ranking_code[401:800]: cl = 1 if int(phn[0]) in stock_volume_ranking_code[801:1200]: cl = 2 if int(phn[0]) in stock_volume_ranking_code[1201:1600]: cl = 3 DMALs_3_index = phn[1] + '-' + phn[2] + '-' + phn[3] + '-' + str(cl) if not(DMALs_3_index in DMALs_3): DMALs_3[DMALs_3_index] = [0, 0, 0, 0, 0] if float(phn[-1]) != 0: DMALs_3[DMALs_3_index][0] += 1 DMALs_3[DMALs_3_index][1] = (DMALs_3[DMALs_3_index][1]*(DMALs_3[DMALs_3_index][0]-1)+float(phn[-3]))/DMALs_3[DMALs_3_index][0] DMALs_3[DMALs_3_index][2] = (DMALs_3[DMALs_3_index][2]*(DMALs_3[DMALs_3_index][0]-1)+float(phn[-2]))/DMALs_3[DMALs_3_index][0] DMALs_3[DMALs_3_index][3] = (DMALs_3[DMALs_3_index][3]*(DMALs_3[DMALs_3_index][0]-1)+float(phn[-1]))/DMALs_3[DMALs_3_index][0] if float(phn[-3]) > 0: DMALs_3[DMALs_3_index][4] += 1 for ele in DMALs_3.items(): short = int(ele[0].split('-')[0]) long = int(ele[0].split('-')[2]) cl = int(ele[0].split('-')[-1]) if float(ele[1][1]) > float(profit_holdingPeriod_numberBuyingSelling_2[0, cl, short-1, long-7]): profit_holdingPeriod_numberBuyingSelling_2[0, cl, short-1, long-7] = float(ele[1][1])*100 profit_holdingPeriod_numberBuyingSelling_2[1, cl, short-1, long-7] = float(ele[1][2]) profit_holdingPeriod_numberBuyingSelling_2[2, cl, short-1, long-7] = float(ele[1][3]) profit_holdingPeriod_numberBuyingSelling_2[3, cl, short-1, long-7] = float(ele[1][4])/float(ele[1][0])*100 for k in range(4): plt.figure(figsize=(20, 20)) plt_title = '' for j in range(4): ax = plt.subplot(2,2,j+1) tmp = pd.DataFrame(data=profit_holdingPeriod_numberBuyingSelling_2[k,j,:,:], index=[i for i in range(1, 25)], columns=[i for i in range(7, 31)]) if k == 0: plt_title = '5年間・1銘柄あたりの平均利益率(%)(1株 2500円以下・移動平均線 3本)' sns.heatmap(tmp, square=True, fmt="2.0f", vmax=75, annot=True) if k == 1: plt_title = '平均株式保有日数(日)(1株 2500円以下・移動平均線 3本)' sns.heatmap(tmp, square=True, fmt="2.0f", vmax=70, annot=True) if k == 2: plt_title = '5年間・1銘柄あたり平均取引回数(回)(1株 2500円以下・移動平均線 3本)' sns.heatmap(tmp, square=True, fmt="2.0f", vmax=120, annot=True) if k == 3: plt_title = '黒字率(%)(1株 2500円以下・移動平均線 3本)' sns.heatmap(tmp, square=True, cmap='coolwarm', fmt="2.0f", vmax=100, annot=True) plt.xlabel('長期移動平均線(m日)') plt.ylabel('短期移動平均線(n日)') if j == 0: plt.title('①出来高:1位~400位', fontsize=24) if j == 1: plt.title('②出来高:401位~800位', fontsize=24) if j == 2: plt.title('③出来高:801位~1200位', fontsize=24) if j == 3: plt.title('③出来高:1201位~1600位', fontsize=24) plt.suptitle(plt_title, fontsize=36) plt.savefig(plt_title) plt.figure()
ヒートマップは、縦軸-短期移動平均線(n日)、横軸-長期移動平均線(m日)。中期移動平均線(k日)のkは、n+1≦k≦m-5で、それぞれのセルは、m-n+5通りの中期移動平均線の中で一番、平均利益率が高かったものを示している。
結果。
5年間・1銘柄あたりの平均利益率(%)。
移動平均線3本。
移動平均線2本。
考察とか。
平均利益率の定義は、こんな感じ。
今回の平均利益率は、百分率表記にした。後から見直した時に分かりやすいという理由でヒートマップに数値を加えたのだが、0.〇〇っていう表記は冗長なので。
前回と同様に、移動平均線が2本であっても3本であっても、出来高が程よく大きい方が利益率が高いということが分かった。大事なのは、「程よく多い」。出来高(順位1位~500位)がものすごく多かったらなぜか利益率はかなり少なくなる。
移動平均線2本と3本の違いは歴然。移動平均線を増やすことで、平均利益率は大幅に上昇する。移動平均線が2本の時の平均利益率の最大値は27%ぐらいなのだが、移動平均線を3本にして、出来高の順位が401位から1200位の銘柄(②、③)をターゲットにして売買すれば、多くの場合、その最大値を超える平均利益率を叩き出すことができる。平均利益率が2倍以上になる場合も多々見られる。
けれどもターゲットにする銘柄によって、逆に移動平均線が2本の方が平均利益率が高い場合(①、④)もあり、やっぱり出来高を元にした銘柄選びは大事だということが分かった。
黒字率(%)。
移動平均線3本。
移動平均線2本。
考察とか。
セルが赤みがかっているほど、5年間、売買を続けたときに最終的に黒字になる銘柄の割合が多いことを示し、セルが青みがかっていると最終的に赤字になる銘柄の割合が多いことを示している。また、0は、黒字率が0%、もしくは1回も売買されていない状態を表している。
移動平均線が2本の場合、黒字と赤字の割合が大体半々の場合が多いが、移動平均線を3本にすると、黒字割合が高い場合と赤字割合が高い場合にもっとはっきりと分かれる。けれども、先ほどの平均利益率が高い領域に対応する領域はきちんと黒字割合も高いということが分かる。
移動平均線3本のときに平均利益率が高いのは、ある銘柄の取引で大きな黒字になって、小さな赤字を補填したのではなく(平均利益率:高、黒字率:低)、どの銘柄であっても小さな黒字が得られ、それが積み重ねていった結果であるということが分かった(平均利益率:高、黒字率:高)。前者の場合であれば、再現性に問題があるが、後者の場合、再現性があり、今後も安定的に利益を出し続けることができるだろう。
(大きな黒字が発生する銘柄に遭遇するのは稀→再現性が低い。小さな黒字が発生する銘柄に遭遇するのは稀ではない→再現性が高い)
平均株式保有日数(日)。
移動平均線3本。
移動平均線2本。
考察とか。
僕は、スイングトレード用の株式自動売買プログラムを開発しているので、出来ることなら平均株式保有日数は少ない方がいい。多くても2~3週間(14~21日)ぐらいだとありがたい。ヒートマップの0は、取引されていないことを表している。
結果を見て分かる通り、移動平均線の数を増やすと、平均株式保有日数が大きくなる傾向にある。保有日数が多くなるほど、景気の影響、コロナウイルスによる影響といったチャートでは得られない情報がアルゴリズムの性能に影響を及ぼしてしまう。そのため、そういったチャート外の情報によるアルゴリズムへの影響を小さくするために保有日数を短くしたいのだが、上手くはいかない。
僕が保持している株価データは、日単位のものである。もっと細かく刻んだデータは、取ってこれるし、そのデータでこの問題は解決するが、逆に計算量が増大するという問題が出てくる。
5年間・1銘柄あたり平均取引回数(回)。
移動平均線3本。
移動平均線2本。
考察とか。
これは、完全に予想通り移動平均線の数を増やすと、平均取引回数は下がる。けれどもここまで下がるのは予想外だった。てっきり2分の1くらいになるのかと...。
と言っても、ある移動平均線トリオを使ったときの平均取引回数が5年間で10回だったとしても、それが400銘柄あれば、取引回数は年間160回になるので、回数が少なくなってもあまり困ることはない。逆に今の資産規模から考えて、取引回数は、もう少し少なくても構わない。
考察とか。
おもに出来高、平均利益率、平均株式保有日数より、、、
出来高の順位が801位~1200位の銘柄(③)をターゲットに短期移動平均線を1~7日、長期移動平均線を7日~25日、中期移動平均線は平均利益率が高くなるように設定したらよさそう。
出来高の順位が1位~400位、1201位~1600位の銘柄(①、④)の平均利益率はかなり低いので、これらを取り除いた後に再度、上記の解析を行い、もっとターゲットとする銘柄を絞っていきたい。
出来れば、来週中に実際の株式売買で使えるような形にプログラムを書き終わりたいと思う。
この文章を読んで、面白い!役に立った!...と思った分だけ、投げ銭していただけると嬉しいです。