Python3 進化する自動売買BOTのつくりかた:遺伝的アルゴリズムによる最適パラメータ探索手法の解説②


こんにちは!magito(@magimagi1223)です。

前回の解説①では、遺伝的アルゴリズム(Genetic Algorithm : GA)についてざっくりと説明しました。「GAとは、生物進化の仕組みを模倣して作られた機械学習の一種で、これを使うと自動売買BOTの最適パラメータを見つけることができるかもしれない!」ということでしたね。

そして今回の解説②では、いよいよGAを自動売買BOTのバックテストプログラムに実装し、最適パラメータを探索してみたいと思います。ソースコードをみながら実践的に説明していきますが、プログラミングを始めたばかりの方でもついてこれるように、できるだけ平易な説明を心がけますのでどうぞお付き合いください!

--------------------------------------------------------

目次

1.  事前準備

2. バックテストプログラムをつくる

3. 遺伝的アルゴリズムを実装する

4. BOTを進化させてみる

--------------------------------------------------------


1.  事前準備


まずは以下の3つを用意します。

(1)Python3の実行環境

BOT開発をするときと同様の環境でOKです。

(2)ライブラリ「DEAP」

詳しくは後半で説明しますが、GAを実装するために使用します。

導入方法はターミナルでpipインストール。これだけです。

$ pip install deap

(3)自動売買BOT(ロジック)

このnoteを読みながら(もしくは読んだ後に)GAを自分のBOTに実装してみたい!と考えている方は、準備しておいてください。BOT本体はともかく、ロジックがなければバックテストプログラムもつくれませんので、、

なお、解説では僕が用意したサンプルロジックを使いますので、自分で実装する予定のない方は準備不要です。
※ただし、あくまでサンプルですので、間違っても本番環境で使わないようにお願いします。(損しても責任はとれません、、)


2. バックテストプログラムをつくる


バックテストとは、過去チャートを用いて仮想的にトレードを行い、トレードロジックやパラメータの性能を検証することです。GAに限った話ではありませんが、BOTの最適パラメータ探索では、パラメータを変えながらたくさんバックテストを行うので、それを実行するためのプログラムが必要です。

ここではサンプルとして、以下のようなロジックで動くBOTを定義します。

「1時間足の期間L(長期)と期間S(短期)の2つの単純移動平均 (SMA)のゴールデンクロスでロングエントリ、デッドクロスでショートエントリ、ロングエントリからの値幅Xで利確、ショートエントリからの値幅Yで利確、ロング/ショートエントリから値幅Zでトレールにより損切」

このBOTのバックテスト関数は以下のようになります。テスト期間は3/1-3/31の1ヶ月。ちなみに、実稼働の予定はありませんが一応BitMEXでの運用を想定して作りました。

# 18/04/28追記:
ohlcvの終値のキーが誤っていたので以下のとおり修正しました。
(誤)ohlcv['l']  → (正)ohlcv['c']
#!/usr/bin/python3
# coding: utf-8

import requests

#3/1-3/31のohlcv(1時間足)を取得
timestamp = '1519830000'
r = requests.get('https://www.bitmex.com/api/udf/history?symbol=XBTUSD&resolution=60&from=' + timestamp + '&to=' + str(int(timestamp)+60*60*24*31))
ohlcv = r.json()

def backtest():
   last = 0
   sma_s = 0
   sma_l = 0
   list_last = []
   list_rate = []
   list_sma_s = []
   list_sma_l = []
   rate_max = 0
   rate_min = 99999999
   margin = 0
   total_prof = 0
   total_loss = 0
   count_prof = 0
   count_loss = 0
   pos = 'none'

   #終値とSMAを計算
   for i in range(len(ohlcv['c']) - 1):
       last = ohlcv['c'][i]
       list_last.append(last)

       if len(list_last) > S:
           sma_s = sum(list_last[i-1-S:i-1]) / S
       if len(list_last) > L:
           sma_l = sum(list_last[i-1-L:i-1]) / L

           list_rate.append(last)
           list_sma_s.append(sma_s)
           list_sma_l.append(sma_l)

   #バックテスト実行
   for i in range(1,len(list_rate)):
       rate = list_rate[i]
       sma_s = list_sma_s[i]
       sma_l = list_sma_l[i]
       prev_sma_s = list_sma_s[i-1]
       prev_sma_l = list_sma_l[i-1]

       #ゴールデンクロスでロングエントリ
       if pos == 'none' and sma_s > sma_l and prev_sma_s < prev_sma_l:
           rate_entry = rate
           pos = 'entry_long'

       #デッドクロスでショートエントリ
       if pos == 'none' and sma_l > sma_s and prev_sma_l < prev_sma_s:
           rate_entry = rate
           pos = 'entry_short'

       #ロングの利確/損切
       if pos == 'entry_long':
           rate_max = max(rate_max, rate)

           if rate > rate_entry + X or rate < rate_max - Z:
               fee = (rate_entry + rate) * 0.00075 #taker手数料0.075%
               margin = rate - rate_entry - fee
               pos = 'exit'

       #ショートの利確/損切
       if pos == 'entry_short':
           rate_min = min(rate_min, rate)

           if rate < rate_entry - Y or rate > rate_min + Z:
               fee = (rate_entry + rate) * 0.00075 #taker手数料0.075%
               margin = rate_entry - rate - fee
               pos = 'exit'

       #損益計算
       if pos == 'exit':
           if margin >= 0:
               total_prof += margin
               count_prof += 1
           else:
               total_loss += margin
               count_loss += 1

           #後処理
           rate_max = 0
           rate_min = 99999999
           pos = 'none'

   #テスト結果(損益、PF、勝率)を返す
   pal = float(total_prof + total_loss)
   pf = float(total_prof / (-total_loss))
   wp = float(count_prof / (count_prof + count_loss))

   return {'pal': pal, 'pf': pf, 'wp': wp}

backtest()は1時間足の終値と短期/長期のSMAをそれぞれ時系列データとしてリストに格納し、それらを用いてバックテストを実行する関数です。backtest()を実行すると、設定したパラメータでの損益、プロフィットファクター、勝率が出力されます。

#パラメータ設定
S = 5
L = 30
X = 200
Y = 200
Z = 50

#実行
print(backtest())
#実行結果
=> {'pal': 339.161875, 'pf': 1.228225852578596, 
'wp': 0.34615384615384615}

上の例は、「短期SMAを期間5、長期SMAを期間30、ロング/ショート利確幅を200、トレールの損切幅を50としたとき、最終損益が339(USD)、プロフィットファクターが1.23、勝率が34.6%」であることを示しています。

バックテストプログラムをご自身で用意する場合も、このように「パラメータ値を入力すると、テスト結果を出力する」という要件を満たす関数をつくればOKです。

さて、これでバックテストの準備はできました!次からはいよいよGAの実装に入ります!


3. 遺伝的アルゴリズムを実装する


僕が紹介する手法では、GAによる最適パラメータ探索にDEAPというライブラリを使います。DEAPにはGAを実装するためのクラスや関数が用意されているの3.1で、これを使うだけで簡単(?)に進化するBOTが作れてしまいます。便利な世の中ですね。

(1)用語の説明

まず最初に、GAで使う用語の対応を説明しておきます。

遺伝子(gene):調整するパラメータ
個体(individual):パラメータ値の組み合わせ
世代(population):複数個体のセット
適応度:個体の性能を評価するための指標となる数値
目的関数:個体の適応度を返す関数

今回のサンプルロジックだと、S,L,X,Y,Zの各々が遺伝子、[S,L,X,Y,Z ]= [5,30,200,200,100]等が個体、複数の個体をlist型にまとめたものが世代に対応します。

適応度と目的関数は少し抽象的で分かりにくいので例を挙げます。例えば、先生が20人の生徒を比較して成績をつけるとき、まず「評価基準」と「評価方法」を決めますよね。「足の速さ」で比較したければ「100m走」をするし、「記憶力」で比較したければ「暗記テスト」をするでしょう(もちろん他にも方法はありますが)。このとき、「足の速さ」や「記憶力」が「適応度」、「100m走」や「暗記テスト」が目的関数に対応します。

今回は様々なパラメータ値をもつBOT君たちの性能を比較して成績をつけたいので、「損益」や「勝率」を適応度、「バックテストプログラム」を目的関数とします。

また、前回の解説①でも少し触れましたが、GAの用語とデータ操作との対応についても以下に示しておきます。

選択:世代の中からランダムに個体を規定数だけ選び、その中から適応度の最も高い個体を取り出す
交叉:ある一定の確率で2個体の遺伝子を一部交換し、子孫となる2個体を生成する
変異:個体の各遺伝子をある一定の確率で別の値に書き換える


(2)クラス・関数を設定する

次に、GAによる計算を実行するためのクラス・関数を設定します。

import random
from deap import base
from deap import creator
from deap import tools

↑まずは必要モジュールをimportします。randomは確率の関わる操作に使用します。

#パラメータの候補値リストを定義
list_S = [3, 4, 5, 6, 12, 13]
list_L = [24, 25, 26, 30, 48]

X = 5
Y = 5
Z = 5
list_X = []
list_Y = []
list_Z = []
while X <= 500:
   list_X.append(X)
   X += 5
while Y <= 500:
   list_Y.append(Y)
   Y += 5
while Z <= 500:
   list_Z.append(Z)
   Z += 5

↑BOTに設定する各パラメータの候補値を定義し、listに格納します。個体を生成するための源泉ですね。S,Lについては候補値をすべてベタ打ちしてますが、候補数が多くなる場合は、X,Y,Zのように繰り返し構文でlistに追加していくと楽ちんです。

#パラメータ値をランダムに決定する関数(個体生成関数の源泉)
def shuffle(container):
   params = [list_S,list_L,list_X,list_Y,list_Z]
   shuffled = []
   for x in params:
       shuffled.append(random.choice(x))
   return container(shuffled)

↑各パラメータ(遺伝子)の値を候補値からランダムに選び、shuffledというlist型に格納しています。つまり、shuffle()は「個体の素」みたいなものをつくる関数です。

ちなみに、container()という引数にshuffledを突っ込んだものを返していますが、後の操作でcontainer()creator.Individual()を代入したかたちに関数を加工することで、「個体の素」「適応度」が追加され「正式な個体」をつくる関数が出来上がるみたいです。(この辺よく分かってないです、、)

#list内のパラメータ値をランダムに変更する関数(突然変異関数の源泉)
def mutShuffle(individual, indpb):
  params = [list_S,list_L,list_X,list_Y,list_Z]
  for i in range(len(individual)):
      if random.random() < indpb:
          individual[i] = random.choice(params[i])
  return individual,

↑ある個体の各パラメータを一定の確率indpbで別の値に書き換える関数です。以降で突然変異関数をつくるための源泉として使います。最後に","がついていますが、ゴミではなく今後の処理に必要なものなので消さないように。

#適合度クラスを作成
creator.create("FitnessMax", base.Fitness, weights=(1.0,))
creator.create("Individual", list, fitness=creator.FitnessMax)

toolbox = base.Toolbox()

#個体生成関数,世代生成関数を定義
toolbox.register("individual", shuffle, creator.Individual)
toolbox.register("population", tools.initRepeat, list, toolbox.individual)

#評価関数,交叉関数,突然変異関数,選択関数を定義
toolbox.register("evaluate", eval_backtest)
toolbox.register("mate", tools.cxTwoPoint)
toolbox.register("mutate", mutShuffle, indpb=0.05)
toolbox.register("select", tools.selTournament, tournsize=3)

↑GAによる進化計算を実行するための各種クラス・関数を定義しています。このコードは、DEAPのGitHub上にあるチュートリアルコードを参考に作成しました。チュートリアルでは「OneMax問題」という別のターゲットを扱っていますが、手法自体は同じです。

自分でやったことは主に、shuffle()関数、mutShuffle()関数、backtest()関数をつくったことだけですね。他のロジックのBOTに実装する場合も、shuffle()とbacktest()をつくりかえてもらうだけで、あとは上記をそのままコピペでいけると思います。

ここでは、ご自身のBOTへ応用するために必要な知識のみに絞って簡単に解説しましたが、DEAPのドキュメントには各クラス・関数の役割や用途を解説しているページがありますので、もっと詳しく知りたい方、興味のある方は、そちらを読んでいただけると理解が深まるかと思います。日本語で解説されてるサイトさんもあります。


(3)メイン処理をつくる

先ほど設定したクラス・関数を用いてBOTを「進化(最適化)」させるための手順を組み立てます。

最適化手順
Step1.  個体をランダムにn個生成し、初期世代(第1世代)を生成する
Step2. 現行世代(最初は初期世代)の全個体の適応度を目的関数により評価する
Step3. 現行世代の個体の中から、選択により適応度の高い個体を選び次世代に追加していく、これを決まった数だけ繰り返す
Step4. 次世代の個体に交叉・変異を適応する
Step5. 次世代の個体を現行世代にコピーする
Step6. Step2~5の操作を繰り返し、最後の世代の中で最も適応度の高い個体のもつパラメータの組み合わせを準最適解とする


この手順を実行するためのコードを書きます。

def main():

   #個体をランダムにn個生成し、初期世代を生成
   pop = toolbox.population(n=100) #n:世代の個体数
   CXPB, MUTPB, NGEN = 0.5, 0.2, 40 #交叉確率、突然変異確率、ループ回数

   print("Start of evolution")

   #初期世代の全個体の適応度を目的関数により評価
   fitnesses = list(map(toolbox.evaluate, pop))
   for ind, fit in zip(pop, fitnesses):
       ind.fitness.values = fit

   print("  Evaluated %i individuals" % len(pop))

   #ループ開始
   for g in range(NGEN):
       print("-- Generation %i --" % g)

       #現行世代から個体を選択し次世代に追加
       offspring = toolbox.select(pop, len(pop))
       offspring = list(map(toolbox.clone, offspring))

       #選択した個体に交叉を適応
       for child1, child2 in zip(offspring[::2], offspring[1::2]):
           if random.random() < CXPB:
               toolbox.mate(child1, child2)
               del child1.fitness.values
               del child2.fitness.values

       #選択した個体に突然変異を適応
       for mutant in offspring:
           if random.random() < MUTPB:
               toolbox.mutate(mutant)
               del mutant.fitness.values

       #適応度が計算されていない個体を集めて適応度を計算
       invalid_ind = [ind for ind in offspring if not ind.fitness.valid]
       fitnesses = map(toolbox.evaluate, invalid_ind)
       for ind, fit in zip(invalid_ind, fitnesses):
           ind.fitness.values = fit

       print("  Evaluated %i individuals" % len(invalid_ind))

       #次世代を現行世代にコピー
       pop[:] = offspring

       #全個体の適応度をlistに格納
       fits = [ind.fitness.values[0] for ind in pop]

       #適応度の最大値、最小値、平均値、標準偏差を計算
       length = len(pop)
       mean = sum(fits) / length
       sum2 = sum(x*x for x in fits)
       std = abs(sum2 / length - mean**2)**0.5

       print("  Min %s" % min(fits))
       print("  Max %s" % max(fits))
       print("  Avg %s" % mean)
       print("  Std %s" % std)

   print("-- End of (successful) evolution --")

   #最後の世代の中で最も適応度の高い個体のもつパラメータを準最適解として出力
   best_ind = tools.selBest(pop, 1)[0]
   print("Best parameter is %s, %s" % (best_ind, best_ind.fitness.values))

このコードも、OneMax問題のチュートリアルとほぼ同じものとなります。交叉確率CXPB, 突然変異確率MUTPB, ループ回数NGEN,個体数nを変えてやると進化の仕方が変わります。また、NGENnは大きいほど計算に時間がかかります。このへんの設定、一番難しいところだと思います。どうするのがベストかはターゲットによっても変わってくるでしょうし、いろいろ試行錯誤が必要そうですね。


4. BOTを進化させてみる

ようやくGA実装に必要なモノはすべて揃いました!これまで出てきたコードを全部まとめるとこんな感じです。

#!/usr/bin/python3
# coding: utf-8

import requests

#3/1-3/31のohlcv(1時間足)を取得
timestamp = '1519830000'
r = requests.get('https://www.bitmex.com/api/udf/history?symbol=XBTUSD&resolution=60&from='
+ timestamp + '&to=' + str(int(timestamp)+60*60*24*31))
ohlcv = r.json()

def eval_backtest(individual):
   #個体から遺伝子を取り出しパラメータとして代入
   S = individual[0]
   L = individual[1]
   X = individual[2]
   Y = individual[3]
   Z = individual[4]

   last = 0
   sma_s = 0
   sma_l = 0
   list_last = []
   list_rate = []
   list_sma_s = []
   list_sma_l = []
   rate_max = 0
   rate_min = 99999999
   margin = 0
   total_prof = 0
   total_loss = 0
   count_prof = 0
   count_loss = 0
   pos = 'none'

   #終値とSMAを計算
   for i in range(len(ohlcv['c']) - 1):
       last = ohlcv['c'][i]
       list_last.append(last)

       if len(list_last) > S:
           sma_s = sum(list_last[i-1-S:i-1]) / S
       if len(list_last) > L:
           sma_l = sum(list_last[i-1-L:i-1]) / L

           list_rate.append(last)
           list_sma_s.append(sma_s)
           list_sma_l.append(sma_l)

   #バックテスト実行
   for i in range(1,len(list_rate)):
       rate = list_rate[i]
       sma_s = list_sma_s[i]
       sma_l = list_sma_l[i]
       prev_sma_s = list_sma_s[i-1]
       prev_sma_l = list_sma_l[i-1]

       #ゴールデンクロスでロングエントリ
       if pos == 'none' and sma_s > sma_l and prev_sma_s < prev_sma_l:
           rate_entry = rate
           pos = 'entry_long'

       #デッドクロスでショートエントリ
       if pos == 'none' and sma_l > sma_s and prev_sma_l < prev_sma_s:
           rate_entry = rate
           pos = 'entry_short'

       #ロングの利確/損切
       if pos == 'entry_long':
           rate_max = max(rate_max, rate)

           if rate > rate_entry + X or rate < rate_max - Z:
               fee = (rate_entry + rate) * 0.00075 #taker手数料0.075%
               margin = rate - rate_entry - fee
               pos = 'exit'

       #ショートの利確/損切
       if pos == 'entry_short':
           rate_min = min(rate_min, rate)

           if rate < rate_entry - Y or rate > rate_min + Z:
               fee = (rate_entry + rate) * 0.00075 #taker手数料0.075%
               margin = rate_entry - rate - fee
               pos = 'exit'

       #損益計算
       if pos == 'exit':
           if margin >= 0:
               total_prof += margin
               count_prof += 1
           else:
               total_loss += margin
               count_loss += 1

           #後処理
           rate_max = 0
           rate_min = 99999999
           pos = 'none'

    #テスト結果
   pal = float(total_prof + total_loss)
   pf = float(total_prof / (-total_loss))
   wp = float(count_prof / (count_prof + count_loss))

   #適応度にする指標を返り値として設定
   return pal,

#--------------------------------------------------------------------

import random
from deap import base
from deap import creator
from deap import tools

#パラメータの候補値リストを定義
list_S = [3, 4, 5, 6, 12, 13]
list_L = [24, 25, 26, 30, 48]

X = 5
Y = 5
Z = 5
list_X = []
list_Y = []
list_Z = []
while X <= 500:
   list_X.append(X)
   X += 5
while Y <= 500:
   list_Y.append(Y)
   Y += 5
while Z <= 500:
   list_Z.append(Z)
   Z += 5

#パラメータ値をランダムに決定する関数(個体生成関数の源泉)
def shuffle(container):
   params = [list_S,list_L,list_X,list_Y,list_Z]
   shuffled = []
   for x in params:
       shuffled.append(random.choice(x))
   return container(shuffled)

#list内のパラメータ値をランダムに変更する関数(突然変異関数の源泉)
def mutShuffle(individual, indpb):
   params = [list_S,list_L,list_X,list_Y,list_Z]
   for i in range(len(individual)):
       if random.random() < indpb:
           individual[i] = random.choice(params[i])
   return individual,

#適合度クラスを作成
creator.create("FitnessMax", base.Fitness, weights=(1.0,))
creator.create("Individual", list, fitness=creator.FitnessMax)

toolbox = base.Toolbox()

#個体生成関数,世代生成関数を定義
toolbox.register("individual", shuffle, creator.Individual)
toolbox.register("population", tools.initRepeat, list, toolbox.individual)

#評価関数,交叉関数,突然変異関数,選択関数を定義
toolbox.register("evaluate", eval_backtest)
toolbox.register("mate", tools.cxTwoPoint)
toolbox.register("mutate", mutShuffle, indpb=0.05)
toolbox.register("select", tools.selTournament, tournsize=3)

#--------------------------------------------------------------------

#メイン処理
def main():
   #random.seed(1024)

   #個体をランダムにn個生成し、初期世代を生成
   pop = toolbox.population(n=100) #n:世代の個体数
   CXPB, MUTPB, NGEN = 0.5, 0.2, 40 #交叉確率、突然変異確率、ループ回数

   print("Start of evolution")

   #初期世代の全個体の適応度を目的関数により評価
   fitnesses = list(map(toolbox.evaluate, pop))
   for ind, fit in zip(pop, fitnesses):
       ind.fitness.values = fit

   print("  Evaluated %i individuals" % len(pop))

   #ループ開始
   for g in range(NGEN):
       print("-- Generation %i --" % g)

       #現行世代から個体を選択し次世代に追加
       offspring = toolbox.select(pop, len(pop))
       offspring = list(map(toolbox.clone, offspring))

       #選択した個体に交叉を適応
       for child1, child2 in zip(offspring[::2], offspring[1::2]):
           if random.random() < CXPB:
               toolbox.mate(child1, child2)
               del child1.fitness.values
               del child2.fitness.values

       #選択した個体に突然変異を適応
       for mutant in offspring:
           if random.random() < MUTPB:
               toolbox.mutate(mutant)
               del mutant.fitness.values

       #適応度が計算されていない個体を集めて適応度を計算
       invalid_ind = [ind for ind in offspring if not ind.fitness.valid]
       fitnesses = map(toolbox.evaluate, invalid_ind)
       for ind, fit in zip(invalid_ind, fitnesses):
           ind.fitness.values = fit

       print("  Evaluated %i individuals" % len(invalid_ind))

       #次世代を現行世代にコピー
       pop[:] = offspring

       #全個体の適応度をlistに格納
       fits = [ind.fitness.values[0] for ind in pop]

       #適応度の最大値、最小値、平均値、標準偏差を計算
       length = len(pop)
       mean = sum(fits) / length
       sum2 = sum(x*x for x in fits)
       std = abs(sum2 / length - mean**2)**0.5

       print("  Min %s" % min(fits))
       print("  Max %s" % max(fits))
       print("  Avg %s" % mean)
       print("  Std %s" % std)

   print("-- End of (successful) evolution --")

   #最後の世代の中で最も適応度の高い個体のもつパラメータを準最適解として出力
   best_ind = tools.selBest(pop, 1)[0]
   print("Best parameter is %s, %s" % (best_ind, best_ind.fitness.values))

#--------------------------------------------------------------------

#実行
if __name__ == "__main__":
   main()

eval_backtest()については、個体(list)を引数としてバックテスト結果を返してくれるようにbacktest()から一部変更を加えています。適応度にする指標をreturn valueに設定しましょう。今回は損益を最大化するためにpalを入れていますが、勝率やPFを最大化したければwpやpfを入れればOKです。


それでは実行してみましょう!

Start of evolution
 Evaluated 100 individuals
-- Generation 0 --
 Evaluated 54 individuals
 Min -1321.5413750000002
 Max 3548.5971249999993
 Avg 1100.8590924999999
 Std 892.9771101457322
-- Generation 1 --
 Evaluated 64 individuals
 Min -1618.3383749999998
 Max 3548.5971249999993
 Avg 1641.94404
 Std 854.1630829508014
-- Generation 2 --
 Evaluated 65 individuals
 Min -535.9068750000006
 Max 3726.9174999999996
 Avg 2194.92963625
 Std 790.8990124642011
-- Generation 3 --
 Evaluated 69 individuals
 Min -466.1844999999994
 Max 3726.9174999999996
 Avg 2720.74827
 Std 738.8562361728118
-- Generation 4 --
 Evaluated 68 individuals
 Min 1355.6040000000005
 Max 4078.293
 Avg 3230.150373749998
 Std 422.14841401708856


...(中略)


-- Generation 10 --
 Evaluated 69 individuals
 Min 858.7195000000006
 Max 4134.8031249999985
 Avg 4027.761812500003
 Std 358.88843313102745


...(中略)


-- Generation 39 --
 Evaluated 68 individuals
 Min 1138.9227499999993
 Max 4269.120625
 Avg 4177.0779987499955
 Std 436.2110900189282
-- End of (successful) evolution --
Best parameter is [5, 25, 235, 490, 230], (4269.120625,)

世代を重ねるごとに適応度(損益)の最大値Maxが大きくなっているのが分かりますね。世代全体の適応度の平均値Avgも徐々にMaxに近づいていることから、優秀な個体の割合が増加しているのも分かります。

つまり、我々生命体と同じように、このBOTもパラメータという「遺伝子」を子孫に引き継ぎながら、環境に合わせて姿形を変えていっているわけです。これが「進化する」ということですね。

そして、最後に出力された [S, L, X, Y, Z] = [5, 25, 235, 490, 230]というパラメータの組み合わせが、今回のパラメータ探索によって得られた準最適解となります。


・留意事項

「準最適解」といっても、今回設定した条件の中でのベストな解というだけなので、厳密な最適解と比べてどれだけのパフォーマンスがあるかは、全パターン計算して比較してみないと分かりません。(今回くらいのパターン数だったらたぶん頑張れば計算できるので、時間あるときにやってみます。)

また、このパラメータが実際のトレードで有効なものかどうかも別問題です。機械学習による最適化処理には「部分最適化」「カーブフィッティング」などの問題がつきものであり、適当に実装してすぐ結果が出せるわけではないみたいです。

この辺については僕もまだまだ勉強不足なので、後ほど改めてnoteにまとめることができればと思います。



おわりに


いかがでしたでしょうか。詳しい説明はかなり省いてしまいましたが、「遺伝的アルゴリズムって何?どんなことやってるの?」ってところはなんとなくイメージしていただけたのではないかと思います。

このnoteを読んで、僕と同じようにシストレや機械学習に触れるのが初めての方でも「難しそうと思ってたけど、頑張れば自分でもできそう!ちょっと勉強してやってみよう!」と思っていただけるきっかけになれば幸いです。

これからも、「これ面白い!」と思ったことについては、noteやtwitter(@magimagi1223)を通じてどんどん発信していければいいなと考えています。トレードに疲れたときにでも、休憩がてら読んでみてください。



以上です。
最後まで読んでいただき、ありがとうございました!

この記事が気に入ったら、サポートをしてみませんか?気軽にクリエイターを支援できます。

note.user.nickname || note.user.urlname

BTC: 3BMEXK7zADCmyZUFhJ5M62MvvagkSnnAo5 ETH:0xA5a4eced8f47B4506c434A19A2CeA889831757C8

254

magito

仮想通貨

15つのマガジンに含まれています

コメント15件

>>Githubのソースを時間をかけて読み取ったわけでなく、このソースを丸ごとローカルで落として、(他サイトを参考にしつつ)実際に引数をいじってPrintで挙動を理解していったって感じですかね?
その通りです。とはいっても、大変ありがたいことに、とても丁寧に解説されているサイトが沢山ありましたので、試行錯誤もそんなに手間はありませんでした。「巨人の肩の上に立つ」とはこのことですね。

細部を気にすることは良いことだと思いますよ。僕はざっくりと大枠を掴んでから、徐々に各論を吟味していくというスタイルです。

コミュの方も楽しみにしています!
いつも非常に参考になる情報をありがとうございます!機械学習には夢が詰まっていますね。

しょうもない質問で申し訳ないのですが、このようにしてバックテストプログラムを作った場合、
取得している1時間足で行ったエントリーと同じ足で安値が損切価格を下回っていた場合、
実際は先に利確ラインに達していたとしても即損切と判断されるような気がするのですが、
間違っていますでしょうか?

まだプログラミングを初めて1か月もたっていないので、間違っていましたらお時間ある際にご指摘いただけると助かります。
okahiroさん

コメントありがとうございます!

今回、1時間毎の取得価格'rate'には1時間足の'終値'を入れています。
エントリー以降、Long/Shortのポジションに応じて'rate'の最大値または最小値を記録していき、
そこからZだけ悪い方向に動いたら損切りを行うようにしています。
(このように損切りラインが値動きを追従していく注文を「トレールストップ注文」といいます。)

したがって、エントリー時点では'rate = rate_max (or rate_min)'であるため、
損切り判定は行われないはずです。

今回のはサンプル用として作成した即席BOTなので、他におかしなところあるかもしれません、、
もし本コードを参考にバックテストプログラムを作成される場合は、
テスト開始から終了までの実行結果(毎時の価格、指標の値、エントリタイミングなど)を
実際のBOTを動かす時のように出力して挙動をチェックすることをお勧めします!
貴重な情報をありがとうございます。
サンプルプログラムをそのままコピーし、同じ条件(期間3/1-3-31)で何度か実行すると、Best parameterと損益が毎回変わります。留意事項に書いてある”今回設定した条件の中でのベストな解”と言えるのでしょうか。
コメントを投稿するには、 ログイン または 会員登録 をする必要があります。