ATRにもとづいた資金管理の実装
この記事は何か
資金管理は投資成果を左右する重要な要素です。ここで言う資金管理とは、どの対象にどの程度の資金を投じるかを定量的に管理するということです。
今回の記事は、チャネルブレイクアウト戦略botについて解説した記事の補稿になります。このbotスクリプトにはATRをもとに発注量を自動設定する仕組みを加えましたが、そのコードの実装を公開しようと思います。
発注量の決め方では、常に一定額(例えば$300)にする、レバレッジを一定値(例えば残高の1.5倍)にするなどが考えつきます。BitMEXでは常に価格が動くBTCを証拠金にしながら発注量をUSDで指定するので、変動する市場価格も自ずと発注量を考える要素に含まれます。
ATRにもとづくと、市場価格そのものの他にその変動性が発注量に加味されます。価格変動が大きい時期には少なめに発注し、価格変動が小さくなると多めに発注することができます。つまり市場の状況が変化しても、リスク量(1日に想定される残高の変動幅)がなるべく一定になるように管理ができます。
ではATRとはどんなもので、どう計算するのでしょう。
ATRとは何か
ATRとはAverage True Rangeの略で、一定期間における日々の値幅の平均値です。
True Rangeとは次の3つの値、当日の高値と安値の差、当日の高値と前日の終値の差、前日の終値と当日の終値の差を比べて、その最も大きい値のことです。この値を継続して観察することで、価格変動の大小を見極めます。
True Range = 最大値(
当日の高値 – 当日の安値 ,
絶対値( 当日の高値 – 前日の終値 ) ,
絶対値( 前日の終値 – 当日の安値 )
)
ビットコインは24時間365日取引されているので、前日終値との比較、いわゆる寄り付きギャップは考慮しなくてもよさそうですが、このコードでは計算に入れています。
移動平均にはいくつか種類があります。ATRを計算するにはシンプルにSMA(単純移動平均)を使ったり直前の日に重みをつけてEMA(指数移動平均)を使う人もいるようです。
ここでは投資集団タートルズのルールに沿ってSMMA(平滑移動平均)を用います。botスクリプトでは日数もならって20日を採用しましたが、この記事では分かりやすいように5日間で説明します。
n = 5
ATR = ( 前日のATR × ( n - 1 ) + 当日のTR ) ÷ n
それではコードです。
ATRを計算する
from time import time
import ccxt
import numpy as np
"""中略"""
def fetch_candles(n=5):
"""2n日間の日足を取得する"""
now = time() * 1000
since = now - (n * 2 + 1) * 86400 * 1000
daily_candles = bitmex.fetchOHLCV(
'BTC/USD',
timeframe='1d',
since=since,
)
return = np.array(daily_candles)
def calculate_atr(candles, n=5):
"""n日間における値幅(TR)の平滑移動平均値(SMMA)を計算する"""
past_high = candles[-n*2:-n, [2]]
past_low = candles[-n*2:-n, [3]]
past_close = candles[-n*2-1:-n-1, [4]]
# n = 5
# i.e. past_high = candles[-10:-5, [2]]
# i.e. past_close = candles[-11:-6, [4]]
past_true_range = np.amax(
[
np.ravel(past_high - past_low),
np.ravel(np.abs(past_high - past_close)),
np.ravel(np.abs(past_close - past_low)),
],
axis=0,
)
high = candles[-n:-1, [2]]
low = candles[-n:-1, [3]]
previous_close = candles[-n-1:-2, [4]]
# i.e. high = candles[-5:-1, [2]]
# i.e. previous_close = candles[-6:-2, [4]]
true_range = np.amax(
[
np.ravel(high - low),
np.ravel(np.abs(high - previous_close)),
np.ravel(np.abs(previous_close - low)),
],
axis=0,
)
atr = np.empty(n)
atr[0] = np.mean(past_true_range)
for i in range(1, n):
atr[i] = atr[i-1] * (n-1) + true_range[i-1]
atr[i] /= n
# i.e. atr[1~4] = atr[0~3] * (5-1) + true_range[0~3]
return atr[-1]
def run():
n = 5
candles = fetch_candles(n)
atr = calculate_atr(candles, n)
run()
前掲のコードも以下に示していくコードも説明に該当する部分の抜粋ですから、単独で動作をするものではありません。
図にするとこのようになります。図中Jは最新の未確定足ですから利用しないデータになります。では説明します。
step1. ほしいATR算出期間(n=5)の2倍長の日足を取ります。ccxtの仕様上このコードでは1日分多く(11日分)取れますが、それでちょうどよいです。
追記 2020年2月8日
いつの間にか仕様上1日分多く取れなくなっていたので、明示的に1日分多く取るように修正しました。
Fix February 8, 2020
- since = now - n * 2 * 86400 * 1000
+ since = now - (n * 2 + 1) * 86400 * 1000
step2. 関数calculate_atr()にその日足を投げ込みます。
step3.
past_high = candles[-n*2:-n, [2]]
past_low = candles[-n*2:-n, [3]]
past_close = candles[-n*2-1:-n-1, [4]]
past_true_range = np.amax(
[
np.ravel(past_high - past_low),
np.ravel(np.abs(past_high - past_close)),
np.ravel(np.abs(past_close - past_low)),
],
axis=0,
)
step3. 前日のATRがない1日目は、単純平均を求めてATRに代用します。分かりやすくpast_high, past_low, past_close(図中AからE)のように区別して価格を取り出して、日々のレンジの最大値を配列past_true_rangeに納めておきます。
step4.
high = candles[-n:-1, [2]]
low = candles[-n:-1, [3]]
previous_close = candles[-n-1:-2, [4]]
true_range = np.amax(
[
np.ravel(high - low),
np.ravel(np.abs(high - previous_close)),
np.ravel(np.abs(previous_close - low)),
],
axis=0,
)
step4. 残りの4日間分の日足もstep3.と同様にhigh, low, previous_close(図中FからI)を分離して取り出し、比較をして日々のレンジの最大値を求め配列true_rangeにまとめます。
コラム
ccxtで取得したローソク足データは、日時[0]、始値[1]、高値[2]、安値[3]、終値[4]、取引量[5]の順に並んでいます。このインデックス[数値]を指定してデータを取り出します。
step5.
atr = np.empty(n)
atr[0] = np.mean(past_true_range)
step6.
for i in range(1, n):
atr[i] = atr[i-1] * (n-1) + true_range[i-1]
atr[i] /= n
return atr[-1]
step5. 要素数が5つの空配列atrを作り、配列冒頭の[0](図中V)にpast_true_rangeの単純平均値を書き込みます。
step6. atr[1]から[4]まで順に、直前のatr[i-1]を4倍して現在のtrue_range[i-1]を加え、5で割ってATR[i]を求めます。最後尾のatr[-1](n=5の本例では図中Zのatr[4])が求めたいATR(n=5)になります。
コラム
>>> a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> a[-1]
9
>>> a[:-1]
[0, 1, 2, 3, 4, 5, 6, 7, 8]
>>> a[-6:-2]
[4, 5, 6, 7]
>>> a[-6:-1]
[4, 5, 6, 7, 8]
>>> a[-6:]
[4, 5, 6, 7, 8, 9]
>>> a[-10:-6]
[0, 1, 2, 3]
>>>
ATRからロットを求める
タートル流の資金管理法では、投資対象のATRをもとに、手元資金を1日当たり1%変動させると見込まれる量を1単位として、発注量を決めていました。ルールにより2単位3単位と積み増すこともしています。
このコードではその1単位をUNITと定義しています。UNIT = 1で日次1%の残高の揺れを許容する発注量になります。繰り返しになりますが、BitMEXでは証拠金はBTCで用意し発注量はUSDで指示します。ルールを整理します。
発注量はすなわち保有したいポジションサイズである。
左辺と右辺が等しくなるように保有サイズを管理したい。
残高USD × 0.01 ÷ 1単位の発注量USD = ATR ÷ 1BTCの現在価格USD
上の式を満たす1単位の発注量USDは式変形で下記のとおり。
1単位の発注量USD = 残高USD × 0.01 ÷ ATR × 1BTCの現在価格USD
リスク量の調整は係数UNITを任意に掛けて行う。
発注量USD = 残高USD × 0.01 ÷ ATR × 1BTCの現在価格USD × UNIT
UNIT = 1.0
LOT = 0 # UNITをセットすると無効
LEVERAGE = 1.0 # 同上
def fetch_balance(last_price, atr):
"""残高を取得しlotをセットする"""
bal_dict = bitmex.privateGetUserMargin({
'columns': json.dumps(['marginBalance'])
})
total_BTC = bal_dict['marginBalance'] * 0.00000001
total_USD = total_BTC * last_price
order_lot = (
int(total_USD * 0.01
/ atr
* last_price
* UNIT) if UNIT else
LOT if LOT else
int(total_USD * LEVERAGE)
)
return order_lot
def run():
last_price = fetch_position()['lastPrice'] # (仮)
n = 5
candles = fetch_candles(n)
atr = calculate_atr(candles, n)
order_lot = fetch_balance(last_price, atr)
run()
このコードでは、UNITを0とするとLOTを直接USDで設定でき、そのLOTも0であれば残高に対するレバレッジで発注量を決めるように、3段構えになっています。
ATRをもとにorder_lotを計算する部分は、読解しやすいよう変形せずに掛け合わせています。
UNITをどう決めるか
UNIT = 1.0と設定すると、日次のリスク量目安が残高の1%程度になります。しかしビットコイン価格は最近でも日次2%程度の高低差がありますから、UNIT値が1.0では1倍を下回る低いレバレッジでしか建てられないことになります。
リスク管理としては妥当なのでしょうが、レバレッジを活かして資金効率を上げたいこともあるでしょう。
leverage = order_lot / total_USD
logger.info(
'atr: {:.2f}, '
'balance: {:.8f}, '
'leverage: {:.2f}, '
'order_lot: {}'.format(
atr,
total_BTC,
leverage,
order_lot,
))
そこで実際に稼働しているbotスクリプトでは、UNIT値を設定する目安となるように、逆算したレバレッジをログとDiscord通知に表示させています。
何UNIT建てるとどのくらいのレバレッジが掛かるのか確認できますし、レバレッジがATRと現在価格によって動的に変化する様子も分かります。UNIT値は小数でも構いません。
解説はここまでです。今回の記事もご自身のbot開発や改良の参考になればうれしく思います。
幸運あれ。
長期で見てプラスのリターンを生む戦略を見つけること。
取引を継続できるようリスク管理すること。
計画は首尾一貫して遂行すること。
シンプルなシステムは複雑なそれよりも長期にわたって持ちこたえる。
タートルズの教えより
RESOLUTION = '1d' FIRST_PERIOD_STICKS = 14 SECOND_PERIOD_STICKS = 3 MARKET_ID = 'ETHUSD' OC_MODE = True