さて、前回までの記事でバックテストをして自動売買BOTのパフォーマンスを評価する方法は、大体マスターできたと思います!
今回は、バックテスト編の総仕上げとして最適なパラメーターを探索する方法を紹介しておきましょう。このパラメーター探索ができることこそ、ある意味、自分でpythonのプログラミングができることの優位性といっても過言ではないかもしれません。
手動だと何百通り、何千通りも試す必要のある膨大な組み合わせの中から、もっとも勝率や期待リターンが高く、最もリスクやドローダウンの小さいパラメーターは何なのか? それを調べる方法を解説します。
パラメーター最適化とは
パラメーターとは、ある自動売買BOTのロジックの中で自由に動かして変更できる値のことをいいます。例えば、前回までの記事で学習した一番シンプルなn期間ドンチャン・ブレイクアウトBOTには、以下の2つのパラメーターがありました。
1)X分足の時間軸を使う
2)n期間の最高値(最安値)ブレイクアウトで買う
同じドンチャンブレイクアウトでも、15分足で10期間のドンチャン・ブレイクアウト戦略を使うのと、1時間足で30期間のドンチャン・ブレイクアウト戦略を使うのとでは、全く成績が異なります。
上記の2つの組み合わせだけなら、前回までの記事の方法で1つ1つを手作業で検証することも可能です。しかし組み合わせが何百通り、何千通りと増えてくると、すべてのパターンを手作業で検証して比較するのは難しくなります。
そこでpythonの出番です。pythonのようなプログラムなら、自動的に何百通りものパラメーターの組み合わせでバックテストを行い、最も利益を最大化できるようなパラメーターを探索することができます。今回はその方法を解説していきます!
今回の練習で試すパラメーター
まずは準備として前回までのドンチャン・ブレイクアウトのコードを改良し、以下の4つのパラメーターを設定できるようにしてみましょう。
変更可能なパラメーター
1)X分足の時間軸を使う
2)n期間の最高値のブレイクアウトで買う
3)m期間の最安値のブレイクアウトで売る
4)ブレイクアウトの判定基準に、高値/安値 or 終値 を使う
より実践的なドンチャン・ブレイクアウト戦略では、上値のブレイクアウトと下値のブレイクアウトで異なるパラメーターを使用することがよくあります。そのため、今までのコードを改良し、買いエントリと売りエントリで、それぞれ違う期間のパラメーターを設定できるようにしておきます。
また今までのコードでは、ブレイクアウトの判定基準を「過去n期間の最高値(最安値)を直近の高値が上回ったとき」としていましたが、高値ではなく終値のパターンも検証しておきましょう。
上記の修正済みのコードは、以下の別記事にまとめておきます。
時間のある方は自分でも挑戦してみてください!
パラメーターの組み合わせを総当たりでテストする方法
パラメーター探索と聞くと、最近流行りの機械学習やニューラルネットワークなど、難しい数学やプログラミングスキルが必要だと誤解している方もいるかもしれません。
しかし基本的には、数千通りくらいのパターンであれば、ただの「総当たり」で全パターンを計算すれば十分です。全パターンを試しても数十秒~1分もあれば終わりますので、複雑な機械学習や最適化アルゴリズムは必要ありません。
例えば、以下のようなパターンの組み合わせをすべて試したいとしましょう。
# 4種類のパラメーター paramA = [ 10,20,30,40,50 ] paramB = [ 10,20,30,40,50 ] paramC = [ "A" , "B" ] paramD = [ 1,2,3,4,5,6,7,8,9,10 ]
この場合、以下のようにfor文を書くことで全パターンの組み合わせを試すことができます。
# for文で総当たりのパターンを記述 for a in paramA: for b in paramB: for c in paramC: for d in paramD: # 各組合せで実行したい処理
パラメーターAのfor文処理をするなかで、1つの要素についてさらにパラメーターBのfor文処理をし、パラメーターBのfor文処理のなかの1つの要素に対してさらにパラメーターCのfor文処理をし….という入れ子構造にしていくわけですね。
これを実行することで、5 × 5 × 2 × 10 = 500通りの組み合わせについて、全てのパターンでバックテストを実行することができます。
シンプルな記述方法
なお、pythonでは上記のコードをもっとシンプルにして以下のように書くことができます。
combinations = [(a, b, c, d) for a in paramA for b in paramB for c in paramC for d in paramD] for a,b,c,d in combinations: # 各組合せで実行したい処理
試すパラメーターの組み合わせ
なお、今回のドンチャンブレイクアウトのパラメーター最適化では、以下の組み合わせを総当たりで全て試してみることにします。
1)使用する時間軸
⇒ 30分足/1時間足/2時間足/6時間足/12時間足/1日足
2)上値ブレイクアウトの判定期間
⇒ 10/15/20/25/30/35/40/45
3)下値ブレイクアウトの判定期間
⇒ 10/15/20/25/30/35/40/45
4)判定に使用する価格
⇒ 高値・安値/終値・終値
時間足が6通り、判定期間が上値ブレイクアウト・下値ブレイクアウトでそれぞれ8通りずつ、判定に使用する価格が2通りで、合計 768通りのパターンをテストしてみましょう。
これをfor文の総当たりでテストするためには、以下のように書けばOKです。
# バックテストのパラメーター設定 chart_sec_list = [ 1800, 3600, 7200, 21600, 43200, 86400 ] # 時間足 buy_term_list = [ 10,15,20,25,30,35,40,45 ] # 上値ブレイクの判断期間 sell_term_list = [ 10,15,20,25,30,35,40,45 ] # 下値ブレイクの判断期間 judge_price_list = [ {"BUY":"close_price","SELL":"close_price"}, # 終値/終値 と 高値/安値 {"BUY":"high_price","SELL":"low_price"} ] # for文の記述方法 combinations = [(chart_sec, buy_term, sell_term, judge_price) for chart_sec in chart_sec_list for buy_term in buy_term_list for sell_term in sell_term_list for judge_price in judge_price_list] for chart_sec, buy_term, sell_term,judge_price in combinations: # 各回のバックテスト処理を記述 # (今までの記事で作成したもの)
これで総当たりのfor文の準備は完了です!
最大化したい指標を決める
何のためにパラメーター最適化を実施するのかといえば、リターンを極限まで高くしたいからですよね。なので「総利益が最大になるようなパラメーターを探す」というのも1つの方法です。
しかし自動売買BOTの評価を最終損益だけで判断するのは不十分です。より厳密にいえば、私たちが探している理想の売買ロジックは、「できるだけリスクを抑えながら、できるだけ大きなリターンを得ること」にあるはずです。そのため、リスクとリターンの比率を1つの数字で表せるような指標を「最大化したい数字」に設定すべきです。
このような指標には、シャープレシオ、MARレシオなど、いくつかの指標がありますが、ここでは一番オーソドックスな「プロフィットファクター」を使うことにします。
プロフィットファクター(PF)とは
同じ最終利益100万円でも、「利益110万円 / 損失10万円」の売買システムと、「利益200万円 / 損失100万円」の売買システムとでは、かなり意味が違ってきます。
前者は、損失が利益のおよそ1/10で済んでいるのに対し、後者は、なんと利益の半分もの損失を出してしまっています。当然、前者の方が安定した売買システムで、後者のほうがより不安定な(リスクの高い)売買システムということになります。
このような、「 総利益 / 総損失 」で表される指標のことをプロフィットファクター(PF)といいます。PFが大きければ大きいほど、そのシステムは安定していてリスクが小さいと評価できます。
プロフィットファクターの計算コード
今まで作成してきたドンチャン・ブレイクアウトのバックテスト検証コードに、プロフィットファクターを計算するコードを追加しておきましょう。
前回のpandasの記事を読んで練習した方なら書けると思います!
▽ プロフィットファクターの計算式 (総利益 ÷ 総損失)
PF = round( -1 * (records[records.Profit>0].Profit.sum() / records[records.Profit<0].Profit.sum()) ,2)
さて、それでは実際のコードを作っていきましょう!
2.pythonコード
いつものように、まず最初に全コードの内容と実行結果を示します。
その後コードの書き方を解説していきます。
import requests from datetime import datetime import time import matplotlib.pyplot as plt import pandas as pd import numpy as np import csv #-----設定項目 wait = 0 # ループの待機時間 lot = 1 # BTCの注文枚数 slippage = 0.001 # 手数料・スリッページ # バックテストのパラメーター設定 #--------------------------------------------------------------------------------------------- chart_sec_list = [ 1800, 3600, 7200, 21600, 43200, 86400 ] # テストに使う時間軸 buy_term_list = [ 10,15,20,25,30,35,40,45 ] # テストに使う上値ブレイクアウトの期間 sell_term_list = [ 10,15,20,25,30,35,40,45 ] # テストに使う下値ブレイクアウトの期間 judge_price_list = [ {"BUY":"close_price","SELL":"close_price"}, # ブレイクアウト判定に終値を使用 {"BUY":"high_price","SELL":"low_price"} # ブレイクアウト判定に高値・安値を使用 ] #--------------------------------------------------------------------------------------------- # CryptowatchのAPIを使用する関数 def get_price(min, before=0, after=0): price = [] params = {"periods" : min } if before != 0: params["before"] = before if after != 0: params["after"] = after response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc",params) data = response.json() if data["result"][str(min)] is not None: for i in data["result"][str(min)]: if i[1] != 0 and i[2] != 0 and i[3] != 0 and i[4] != 0: price.append({ "close_time" : i[0], "close_time_dt" : datetime.fromtimestamp(i[0]).strftime('%Y/%m/%d %H:%M'), "open_price" : i[1], "high_price" : i[2], "low_price" : i[3], "close_price": i[4] }) return price else: print("データが存在しません") return None # ドンチャンブレイクを判定する関数 def donchian( data,last_data ): highest = max(i["high_price"] for i in last_data[ (-1* buy_term): ]) if data[ judge_price["BUY"] ] > highest: return {"side":"BUY","price":highest} lowest = min(i["low_price"] for i in last_data[ (-1* sell_term): ]) if data[ judge_price["SELL"] ] < lowest: return {"side":"SELL","price":lowest} return {"side" : None , "price":0} # ドンチャンブレイクを判定してエントリー注文を出す関数 def entry_signal( data,last_data,flag ): signal = donchian( data,last_data ) if signal["side"] == "BUY": # ここに買い注文のコードを入れる flag["order"]["exist"] = True flag["order"]["side"] = "BUY" flag["order"]["price"] = round(data["close_price"] * lot) if signal["side"] == "SELL": # ここに売り注文のコードを入れる flag["order"]["exist"] = True flag["order"]["side"] = "SELL" flag["order"]["price"] = round(data["close_price"] * lot) return flag # サーバーに出した注文が約定したか確認する関数 def check_order( flag ): # 注文状況を確認して通っていたら以下を実行 # 一定時間で注文が通っていなければキャンセルする flag["order"]["exist"] = False flag["order"]["count"] = 0 flag["position"]["exist"] = True flag["position"]["side"] = flag["order"]["side"] flag["position"]["price"] = flag["order"]["price"] return flag # 手仕舞いのシグナルが出たら決済の成行注文 + ドテン注文 を出す関数 def close_position( data,last_data,flag ): flag["position"]["count"] += 1 signal = donchian( data,last_data ) if flag["position"]["side"] == "BUY": if signal["side"] == "SELL": # 決済の成行注文コードを入れる records( flag,data ) flag["position"]["exist"] = False flag["position"]["count"] = 0 # ここに売り注文のコードを入れる flag["order"]["exist"] = True flag["order"]["side"] = "SELL" flag["order"]["price"] = round(data["close_price"] * lot) if flag["position"]["side"] == "SELL": if signal["side"] == "BUY": # 決済の成行注文コードを入れる records( flag,data ) flag["position"]["exist"] = False flag["position"]["count"] = 0 # ここに買い注文のコードを入れる flag["order"]["exist"] = True flag["order"]["side"] = "BUY" flag["order"]["price"] = round(data["close_price"] * lot) return flag # 各トレードのパフォーマンスを記録する関数 def records(flag,data): # 取引手数料等の計算 entry_price = flag["position"]["price"] exit_price = round(data["close_price"] * lot) trade_cost = round( exit_price * slippage ) flag["records"]["slippage"].append(trade_cost) # 手仕舞った日時と保有期間を記録 flag["records"]["date"].append(data["close_time_dt"]) flag["records"]["holding-periods"].append( flag["position"]["count"] ) # 値幅の計算 buy_profit = exit_price - entry_price - trade_cost sell_profit = entry_price - exit_price - trade_cost # 利益が出てるかの計算 if flag["position"]["side"] == "BUY": flag["records"]["side"].append( "BUY" ) flag["records"]["profit"].append( buy_profit ) flag["records"]["return"].append( round( buy_profit / entry_price * 100, 4 )) if flag["position"]["side"] == "SELL": flag["records"]["side"].append( "SELL" ) flag["records"]["profit"].append( sell_profit ) flag["records"]["return"].append( round( sell_profit / entry_price * 100, 4 )) return flag # バックテストの集計用の関数 def backtest(flag): # 成績を記録したpandas DataFrameを作成 records = pd.DataFrame({ "Date" : pd.to_datetime(flag["records"]["date"]), "Profit" : flag["records"]["profit"], "Side" : flag["records"]["side"], "Rate" : flag["records"]["return"], "Periods" : flag["records"]["holding-periods"], "Slippage" : flag["records"]["slippage"] }) # 総損益の列を追加する records["Gross"] = records.Profit.cumsum() # 最大ドローダウンの列を追加する records["Drawdown"] = records.Gross.cummax().subtract(records.Gross) records["DrawdownRate"] = round(records.Drawdown / records.Gross.cummax() * 100,1) print("バックテストの結果") print("-----------------------------------") print("総合の成績") print("-----------------------------------") print("全トレード数 : {}回".format(len(records) )) print("勝率 : {}%".format(round(len(records[records.Profit>0]) / len(records) * 100,1))) print("平均リターン : {}%".format(round(records.Rate.mean(),2))) print("平均保有期間 : {}足分".format( round(records.Periods.mean(),1) )) print("") print("最大の勝ちトレード : {}円".format(records.Profit.max())) print("最大の負けトレード : {}円".format(records.Profit.min())) print("最大ドローダウン : {0}円 / {1}%".format(-1 * records.Drawdown.max(), -1 * records.DrawdownRate.loc[records.Drawdown.idxmax()] )) print("利益合計 : {}円".format( records[records.Profit>0].Profit.sum() )) print("損失合計 : {}円".format( records[records.Profit<0].Profit.sum() )) print("") print("最終損益 : {}円".format( records.Profit.sum() )) print("手数料合計 : {}円".format( -1 * records.Slippage.sum() )) # バックテストの計算結果を返す result = { "トレード回数" : len(records), "勝率" : round(len(records[records.Profit>0]) / len(records) * 100,1), "平均リターン" : round(records.Rate.mean(),2), "最大ドローダウン" : -1 * records.Drawdown.max(), "最終損益" : records.Profit.sum(), "プロフィットファクタ―" : round( -1 * (records[records.Profit>0].Profit.sum() / records[records.Profit<0].Profit.sum()) ,2) } return result # ここからメイン処理 # バックテストに必要な時間軸のチャートをすべて取得 price_list = {} for chart_sec in chart_sec_list: price_list[ chart_sec ] = get_price(chart_sec,after=1451606400) print("-----{}分軸の価格データをCryptowatchから取得中-----".format( int(chart_sec/60) )) time.sleep(10) # テストごとの各パラメーターの組み合わせと結果を記録する配列を準備 param_buy_term = [] param_sell_term = [] param_chart_sec = [] param_judge_price = [] result_count = [] result_winRate = [] result_returnRate = [] result_drawdown = [] result_profitFactor = [] result_gross = [] # 総当たりのためのfor文の準備 combinations = [(chart_sec, buy_term, sell_term, judge_price) for chart_sec in chart_sec_list for buy_term in buy_term_list for sell_term in sell_term_list for judge_price in judge_price_list] for chart_sec, buy_term, sell_term,judge_price in combinations: price = price_list[ chart_sec ] last_data = [] i = 0 # フラッグ変数の初期化 flag = { "order":{ "exist" : False, "side" : "", "price" : 0, "count" : 0 }, "position":{ "exist" : False, "side" : "", "price": 0, "count":0 }, "records":{ "date":[], "profit":[], "return":[], "side":[], "holding-periods":[], "slippage":[] } } while i < len(price): # ドンチャンの判定に使う期間分の安値・高値データを準備する if len(last_data) < buy_term or len(last_data) < sell_term: last_data.append(price[i]) time.sleep(wait) i += 1 continue data = price[i] if flag["order"]["exist"]: flag = check_order( flag ) elif flag["position"]["exist"]: flag = close_position( data,last_data,flag ) else: flag = entry_signal( data,last_data,flag ) last_data.append( data ) i += 1 time.sleep(wait) print("--------------------------") print("テスト期間 :") print("開始時点 : " + str(price[0]["close_time_dt"])) print("終了時点 : " + str(price[-1]["close_time_dt"])) print("時間軸 : " + str(int(chart_sec/60)) + "分足で検証") print("パラメータ1 : " + str(buy_term) + "期間 / 買い" ) print("パラメータ2 : " + str(sell_term) + "期間 / 売り" ) print(str(len(price)) + "件のローソク足データで検証") print("--------------------------") result = backtest( flag ) # 今回のループで使ったパラメータの組み合わせを配列に記録する param_buy_term.append( buy_term ) param_sell_term.append( sell_term ) param_chart_sec.append( chart_sec ) if judge_price["BUY"] == "high_price": param_judge_price.append( "高値/安値" ) else: param_judge_price.append( "終値/終値" ) # 今回のループのバックテスト結果を配列に記録する result_count.append( result["トレード回数"] ) result_winRate.append( result["勝率"] ) result_returnRate.append( result["平均リターン"] ) result_drawdown.append( result["最大ドローダウン"] ) result_profitFactor.append( result["プロフィットファクタ―"] ) result_gross.append( result["最終損益"] ) # 全てのパラメータによるバックテスト結果をPandasで1つの表にする df = pd.DataFrame({ "時間軸" : param_chart_sec, "買い期間" : param_buy_term, "売り期間" : param_sell_term, "判定基準" : param_judge_price, "トレード回数" : result_count, "勝率" : result_winRate, "平均リターン" : result_returnRate, "ドローダウン" : result_drawdown, "PF" : result_profitFactor, "最終損益" : result_gross }) # 列の順番を固定する df = df[[ "時間軸","買い期間","売り期間","判定基準","トレード回数","勝率","平均リターン","ドローダウン","PF","最終損益" ]] # トレード回数が100に満たない記録は消す df.drop( df[ df["トレード回数"] < 100].index, inplace=True ) # 最終結果をcsvファイルに出力 df.to_csv("result-{}.csv".format(datetime.now().strftime("%Y-%m-%d-%H-%M")) )
実行結果
これを実行すると以下のようなCSVファイルが出力されます。
プロフィットファクター(PF)のJ列を「降順」で並び変えてみると、ドンチャン・ブレイクアウトの有望なパラメーターの組み合わせがわかります。
▽ PFで並び替え後の成績ベスト30
上記のコードではトレード回数が100回に満たなかった結果データを全て捨てているので、どのパラメーターの組み合わせも最低限のサンプル数を確保できています。上位30の成績のパラメーターをみると、ほとんどが2時間足以上の時間軸なのがわかります。
▽ PFで並び替え後の成績ワースト30
逆に最もプロフィットファクターの低かった(悪かった成績)30コをみると、短い時間軸(30分足)に集中しているのがわかります。ドンチアンブレイクアウトは、あまり短い時間軸には適さない戦略の可能性がありそうです。
(ただし時間軸によってテストに使用しているローソク足の期間が違うので、厳密には比較できない点に注意してください。)
実行結果2
次はさらに範囲を絞ってテストをしてみましょう!
今度は時間軸を1時間足・2時間足だけに絞り、上値ブレイクアウトの期間・下値ブレイクアウトの期間を10~55期間の範囲にして1期間刻みでテストしています。以下のようにパラメーターを設定すればOKですね。
# バックテストのパラメーター設定 chart_sec_list = [ 3600,7200 ] buy_term_list = np.arange(10,56) # 10~55までの連番の配列を作る sell_term_list = np.arange(10,56)
この条件でテストしてみると以下のようなCSVファイルが出力されます。
ファイル1は先ほどと同じくトレード回数が100回に満たなかったテスト結果を削除したもの、ファイル2は削除せずに全ての結果を残したものです。今回はファイル2を使ってみましょう。
▽ PFで並び替え後の成績ベスト30(1時間足)
1時間足の上位の成績を見ると、明らかに上値ブレイクアウト(買い)の期間は40前後、下値ブレイクアウト(売り)の期間は20前後が良さそう、という傾向が見えます。また判定基準は「終値」が上位を独占していますね。
▽ PFで並び替え後の成績ベスト30(2時間足)
2時間足の上位の成績を見ると、上値ブレイクアウト(買い)の期間は50以上、下値ブレイクアウト(売り)の期間は20~30期間がいいようですね。またやはり「終値」でブレイクアウトの判定をした方が、トレード回数は減るものの成績が良くなる傾向にありそうです。
より詳しく分析したい方は、上位のパラメーターを前回までの記事で学習した「月別集計」「資産曲線」などを使って調べてみるといいでしょう。
Pythonコードの解説
さて、それでは新しく変更した部分のpythonコードを解説しておきましょう!
基本的なコードは前回まで勉強した内容とほとんど変わっていないことに気付いたと思います。要するに、今まで作成してきた「While文でローソク足を回してバックテストし、最終成績をbacktest() で集計するコード」を、まるまる上記で説明した「総当たりのfor文」の中に入れるだけですね。
図にすると以下のような感じです。
こののような入れ子構造になっている点だけ混乱しなければ、上記のコードで難しい箇所はなかったと思います。
今までの記事では、バックテスト集計用の関数 backtest() は、単に集計・計算した結果を表示(print)するだけでしたが、今回のパラメーター最適化のコードでは、各バックテストの結果を返すように変更する必要があります。
そのためバックテスト集計用の関数に以下の部分を足しています。
# バックテストの計算結果を返す result = { "トレード回数" : len(records), "勝率" : round(len(records[records.Profit>0]) / len(records) * 100,1), "平均リターン" : round(records.Rate.mean(),2), "最大ドローダウン" : -1 * records.Drawdown.max(), "最終損益" : records.Profit.sum(), "プロフィットファクタ―" : round( -1 * (records[records.Profit>0].Profit.sum() / records[records.Profit<0].Profit.sum()) ,2) } return result
各バックテストの結果を配列に記録して、それを最後にpandasで1つの表にする方法は、前回までの記事の「各トレード結果を配列に記録して最後に集計する方法」と全く同じなので、説明は省きます。
drop() で不要な行を削除する
唯一新しく登場したのは、最後のdrop()の箇所でしょう。
今回のコードでは、トレード回数の少ないパラメーターの組み合わせを除外できるようにしています。それが以下の部分のコードです。
# トレード回数が100に満たない記録は消す df.drop( df[ df["トレード回数"] < 100].index, inplace=True )
表名.drop()は、行のindexを指定することで、その行を表から削除することのできる関数です。これを使って以下のような仕組みで、トレード回数の少なすぎるバックテスト結果を削除しています。
# トレード回数が100未満のデータを抽出 df[df["トレード回数"] < 100] # トレード回数が100未満のデータの行のindexを取得 df[df["トレード回数"] < 100].index # トレード回数が100未満のデータを行ごと削除 df.drop( df[df["トレード回数"] < 100].index )
なお、inplce=Trueは、現在の表データに結果をそのまま上書きする、という意味です。以上でコードの解説はおしまいです!
まとめ
この章の前の方の記事でも解説したように、このようなパラメーター調整には常にカーブフィッティングの問題がつきまといます。以下のような点に注意しておきましょう。
(1)パラメーターの数を増やし過ぎないようにする
(2)おおまかな傾向を捉えることを目的にする
(3)トレード数(サンプル)が少なくならないようにする
一般論としていえば、今回のドンチャン・チャネル・ブレイクアウトのように広範なパラメーターのどの値を使ってもプラスの期待値が出る、という場合で、おおまかな傾向を捉える目的(例えば、20期間あたりが一番良さそう)でパラメーターを最適化するのは問題ありません。
一番問題があるのは、例えば、「6」という数字を使えば利益が出る、「8」という数字を使うとマイナスに転落する、「13」という数字を使えばプラスになる、といった全く連続性も傾向もないパラメーターを恣意的に調整することです。これをすると、過去データでしか利益の出せないパラメーター調整になります。
次回
今回の記事で、いったんバックテスト編はおしまいです。
まだ見てる方がいらっしゃるといいのですが、、、
無料でここまで丁寧に解説して下さっていて本当にありがたいです。
0スタートですが勉強させていただいています。
一つどうしてもわからない箇所があるのですが、以下のコードの末尾にある4は何を表しているのでしょうか?
flag[“records”][“return”].append( round( buy_profit / entry_price * 100, 4 ))
末尾の4は,round関数を使った際の「小数点以下の桁指定」のための引数です
ttps://note.nkmk.me/python-round-decimal-quantize/
↑これの,小数を任意の桁数で丸める の欄をご覧ください