私と私の猫の他は誰でも隠し事を持っている

野良プログラマ発、日々のアウトプット

CloudWatch ログをダンプしたり空ログストリーム削除したりするスクリプト書いた

AWS Lambda 関数開発をしてると CloudWatch ログを参照することが多いので、書いたスクリプトを備忘録的に公開。

CloudWatch ログは、ロググループ→ログストリーム→ログレコード という階層構造になっており、Web コンソールからログレコードを参照する場合はいちいちこの階層を辿らなきゃならず、ウザったいです。という訳で、ロググループ名と期間を指定して、ログレコードをダンプするスクリプトを書きますた↓。jq が要ります。

#!/bin/sh

if [ $# -ne 3 ]; then
	echo "usage: "`basename $0`" <ロググループ名> <開始期間(YYYY-MM-DD hh:mm:ss)> <終了期間(YYYY-MM-DD hh:mm:ss)>"
	exit
fi

# パラメタ
LOGGROUP=$1
DT_FROM=$2
DT_TO=$3

# 指定期間を Unixtime [ms] に
UT_FROM=`date -d "$DT_FROM" +%s`000
UT_TO=`date -d "$DT_TO" +%s`000

# jq の select 条件 
COND="(($UT_FROM <= .firstEventTimestamp) and (.firstEventTimestamp <= $UT_TO))"
COND="$COND or (($UT_FROM <= .lastEventTimestamp) and (.lastEventTimestamp <= $UT_TO))"
COND="$COND or ((.firstEventTimestamp <= $UT_FROM) and ($UT_TO <= .lastEventTimestamp))"

# 指定ロググループ名のログストリーム一覧をげとして、指定期間に重複するものだけフィルタリング
aws logs describe-log-streams --log-group-name $LOGGROUP --order-by LastEventTime --no-descending | jq -r ".logStreams[] | select($COND) | .logStreamName" | while read LOGSTREAM; do
	# ログストリームのログレコードを、タイムスタンプとメッセージのタブ区切りに整形して出力
	aws logs get-log-events --log-group-name $LOGGROUP --log-stream-name $LOGSTREAM | jq -r '.events[] | [(.timestamp/1000+32400 | strftime("%Y-%m-%d %H:%M:%S")), .message] | @tsv' | sed 's/\\n$//'
done

【註記】describe-log-streams ではページングを nextToken で制御するらしいのですが、今回要件では nextToken が返されることは無く、常にいちレスポンスで足りてしまっていたので、ページング処理はネグってあります。

もうひとつは、ロググループで一定期間後にイベント失効するように設定した時に、空のログストリームが残ってしまうので(大量に残るとかなりウザいw)、これを見つけて削除するスクリプト↓。

if [ $# -ne 1 ]; then
	echo "usage: "`basename $0`" <ロググループ名>"
	exit
fi

LOGGROUP=$1

# 指定ロググループ名のログストリーム一覧をげとして、開始タイムスタンプが無い(=ログレコードが無い)ものだけフィルタリング
aws logs describe-log-streams --log-group-name $LOGGROUP --order-by LastEventTime | jq -r ".logStreams[] | select(.firstEventTimestamp == null) | .logStreamName" | while read LOGSTREAM ; do
	# ログストリーム削除
	echo "delete "$LOGSTREAM
	aws logs delete-log-stream --log-group-name $LOGGROUP --log-stream-name $LOGSTREAM
done

jq は今まで JSON の整形や要素の抜き取りくらいにしか使ってなかったのですが、今回初めてフィルタや演算・関数等についてちゃんと勉強しました。チョー便利っすね。クラスメソッドさんのマニュアル和訳が大助かりでした。

dev.classmethod.jp

Pandas 演習としてのテクニカル指標計算【最終回】 〜 MA-RCI (オリジナル)の巻

このシリーズも今回で10回目。ネタが尽きてきたというか、指標計算に関してはわりとこなれてきた感もあるのでこれで最後にします。最終回記念としてお題は既存のテクニカル指標ではなく、先ごろ考案したオリジナル(たぶん)のトレンド系指標です。

アイディアの発端は移動平均でした。ご存知のように日足チャートを見る時には値動きの傾向を掴む為によく、下図のように 5日・25日・75日の移動平均線を重ね描きします。それによって短期・中期・長期のトレンドをを把握する訳です。この3本の移動平均線を何らかの方法で束ねることで、期間に縛られることなくトレンドを判断できる指標が作れないものかな、と考えた訳です。

f:id:mariyudu:20190602133700p:plain

移動平均は短期である程、値動きに敏感に反応するので、値動きの上昇傾向がハッキリするにつれてチャートの上から短期線・中期線・長期線の順で描かれる傾向が強くなります(上図緑色帯)。下降傾向ならばこの逆で(上図赤色帯)、ボックストレンドだとそれぞれの平均線が順不同で絡み合った状態(上図青色帯)になります。この特質を利用して、完全な上昇トレンド時の順位を[5日線:(位置が上から数えて)1番目, 25日線:2番目、75日線:3番目]と想定し、日々の移動平均線群の位置順位との相関係数を取ってみれば、その計算結果は上昇が明確である程1(100%)に近づき、下降なら-1(-100%)に、ボックストレンドなら0前後をフラフラするのではないでしょうか? 移動平均の相対順位を RCI 計算するということで、指標名は「MA-RCI(仮)」ですなw

さらに、これを実際に試してみるなら移動平均線も3種類では計算結果のバリエーションが乏しくなりそうなので、10本くらいに増やしたいところです。平均日数を、[5,10,15...] と等差数列的にするか、[5,7,10...] と等比数列的にするか判断に迷いますが、どうせなら面白いほうがいいじゃん、と特に根拠もなくフィボナッチ数列の [2, 3, 5...] でやってみることにしました。ということでコーディング開始!

今回も日経平均プロフィルさんから日経平均をげとして使います。CSV ファイルを読み出して Pandas データフレームに格納する処理のコードはシリーズ第1回目を参照ください。

まず、データフレームに9種類の移動平均を計算して列追加します。Pandas ならとても楽々〜。

MA_DAYS = (2, 3, 5, 8, 13, 21, 34, 55, 89) # 平均日数
for d in MA_DAYS:
    df[f'ma_{d}'] = df.close.rolling(d).mean()

ここでいったん、計算した移動平均を直近250日間のローソク足チャートに重ね書きしてみます。枯れ木に蜘蛛の巣が絡まっているみたいだなー。

f:id:mariyudu:20190601184421p:plain

さて、9本の移動平均を相対順序づけして、基準ベクトルと相関係数を取ってみます。

ma_cnt = len(MA_DAYS)
trd_rank = np.arange(len(MA_DAYS), 0, -1) # 基準用の順位配列
ma_rank = np.array(df[[f'ma_{d}' for d in MA_DAYS]].rank(axis=1)) # 各移動平均の値を昇順に順位づけ
df['ma_rci'] = 1 - (6 * ((trd_rank - ma_rank)**2).sum(axis=1)) / (ma_cnt * (ma_cnt**2 - 1))

で、得られた列 ma_rci を日足と二段で可視化してみるとこんな感じ。

f:id:mariyudu:20190601184556p:plain

どうでしょうか? なんだかそれなりにトレンドが表現できている気がします。これを使ってトレンドフォロー型売買のバックテストでもしてみますかね。

ちなみに、結果は割愛しますが、(5, 15, 25, 35, 45, 55, 65, 75, 85) という等差、および (3, 5, 7, 10, 15, 23, 34, 51, 77) の等比での移動平均でも同様に計算・可視化してみたのですが、前者は比較的ダマシの多そうな単純な線に、後者は上記のフィボナッチ日数に近いシェイプの線になりました。興味のある方はご自身で計算してみて下さい。

今回も、コード一式と実行結果は Google Colaboratory でシェアしてあります。↓

colab.research.google.com

それから、本シリーズでのテクニカル指標計算方法は、下記の書籍を参考にしました。様々な指標を紹介するだけでなく計算方法・活用方法・組み合わせ例に至るまで解説されており、良書だと思います。

Pandas 演習としてのテクニカル指標計算 〜 ストキャスティクスの巻

今回のお題はユーザの多いストキャスティクスを。直近の一定期間における値幅(最高値-最安値)中の終値相対位置を元に、「売られ過ぎ・買われ過ぎ」を表現するオシレータ指標です。詳細は下記を参照下さい。

www.sevendata.co.jp

計算に使う株価データは毎度お馴染み、日経平均プロフィルさんから。CSV ファイルを読み出して Pandas データフレームに格納する処理のコードはシリーズ第1回目を参照ください。

指標のなかでは比較的計算が面倒な部類かと想いますが、pandas のパワフルな集計機能のおかげで関数ひとつに纏められました。尚、%K・%D・Slow%D はいずれも単位が[%]として使われるのが一般的ですが、データフレームには生の値を格納しておき、視覚化の際にパーセント値に補正するというのは個人的な嗜好ですw

# ストキャスティクス計算(%K, %D, Slow%D を返す)
def calc_stochastic(close, high, low, length):
  # 期間内の最高値・最安値
  highest = high.rolling(length).max()
  lowest = low.rolling(length).min()
  # 終値 - 最安値
  d = close - lowest
  # 最高値 - 最安値
  n = highest - lowest
  # %K
  pc_k = d / n
  # %D
  pc_d = d.rolling(3).sum() / n.rolling(3).sum()
  # Slow%D
  slow_pc_d = pc_d.rolling(3).mean()
  
  return pc_k, pc_d, slow_pc_d
  
# ストキャスティクス計算期間[日]
STOCHASTIC_DAYS = 9

# ストキャスティクスを計算してデータフレーム列に加える
df['pc_k'], df['pc_d'], df['slow_pc_d'] = calc_stochastic(df.close, df.high, df.low, STOCHASTIC_DAYS)

という訳で、いつものように日足と上下段で見える化を。

f:id:mariyudu:20190525143836p:plain
株価日足とストキャスティクス

指標の使い方や評価にまで踏み込んでしまうとエンドレスになりそうなので、それはまたの機会に。今回もコード一式と実行結果は Google Colaboratory でどうぞ↓。

colab.research.google.com

Pandas 演習としてのテクニカル指標計算 〜 平均足の巻

今回は指標とはちょっと違うかもしれませんが、平均足をお題に。ローソク足にした時にトレンドが把握しやすいよう始値・高値・安値・終値それぞれを調整したもので、国産の指標だそうです。詳しい説明は下記あたりを参照して下さい。

www.sevendata.co.jp

計算に使う株価データは毎度お馴染み、日経平均プロフィルさんから。CSV ファイルを読み出して Pandas データフレームに格納する処理のコードはシリーズ第1回目を参照ください。

平均足の計算方法は下記のとおりです。

平均足始値 = 前日始値と前日終値の中間値
平均足終値 = 当日の始値・高値・安値・終値の平均値
平均足高値 = 当日の平均足始値・平均足終値・高値の最大値
平均足安値 = 当日の平均足始値・平均足終値・安値の最小値

これだけなので、算出プログラムも今回は簡単ですな。

def calc_heikinashi(open, high, low, close):
  h_open = ((open + close) / 2).shift()
  h_close = (open + high + low + close) / 4
  h_high = pd.concat([h_open, h_close, high], axis='columns').max(axis='columns')
  h_low = pd.concat([h_open, h_close, low], axis='columns').min(axis='columns')
  return h_open, h_high, h_low, h_close
  
df['h_open'], df['h_high'], df['h_low'], df['h_close'] = calc_heikinashi(df.open, df.high, df.low, df.close)

では両者の直近250日ローソク足チャートを作画して比べてみましょー。上段が通常の日足、下段が平均足です。ご覧のとおり、平均足ではローソク本体のギャップが埋められるとともに、陽線・陰線が連続しやすくなっているのでトレンド途中の「踊り場」が視覚的に軽減され、判りやすくなった感じですね。

f:id:mariyudu:20190518140936p:plain
通常日足と平均足のローソク足チャート

今回もコード一式と実行結果は Google Colaboratory でどうぞ↓。

https://colab.research.google.com/drive/10jXCUWqaYbyCCZmFiLsqON7u_gWWKMe6

Pandas 演習としてのテクニカル指標計算 〜 DMI の巻

シリーズも7回目なので今回は少し難易度を上げて、DMI という七面倒な計算が必要なオシレータ系指標にチャレンジ。それに、これまでは日足の終値を材料として計算する指標ばかりでしたが、DMI は高値・安値を積極的に活用するという特色があります。DMI については下記が詳しいので参照下さい。

www.sevendata.co.jp

計算に使う株価データは毎度お馴染み、日経平均プロフィルさんから。CSV ファイルを読み出して Pandas データフレームに格納する処理のコードはシリーズ第1回目を参照ください。

まずは、真の値幅を計算しておきます。

def calc_tr(close, high, low):
  close_prv = close.shift() # 前日終値
  thigh = np.maximum(high, close_prv) # 真の高値
  tlow = np.minimum(low, close_prv) # 真の安値
  return thigh - tlow
  
df['tr'] = calc_tr(df.close, df.high, df.low)

次に、指標計算の元になる、上昇幅(+DM)と下降幅(-DM)を計算するのですが、ただ計算するだけでなく +DM と -DM を比較した際の状況に応じてゼロに補正するという処理も必要なので、これが面倒。プログラムではこの補正ロジックを calc_dm_scalar という関数にしておき、それを NumPy のユニバーサル関数に登録してベクトル演算する、という手法を取っています。

def adjust_dm_scalar(d_high, d_low):
  if np.isnan(d_high) or np.isnan(d_low):
    return np.nan, np.nan
  elif d_high < 0 and d_low < 0:
    return 0, 0
  elif d_high > d_low:
    return d_high, 0
  elif d_high < d_low:
    return 0, d_low
  else:
    return 0, 0
    
def calc_dm(high, low):
  d_high = high - high.shift() # 当日の高値 - 前日の高値
  d_low = low.shift() - low # 前日の安値 - 本日の安値
  adjust_dm_vec = np.frompyfunc(adjust_dm_scalar, 2, 2) # adjust_dm_scalar をユニバーサル関数登録
  return adjust_dm_vec(d_high, d_low)
  
df['dm_high'], df['dm_low'] = calc_dm(df.high, df.low)

上記で算出した +DM/-DM を直近の日数において合計し、さらに真の値幅合計で割ると、+DI/-DI という指標が導出されます。

DI_DAYS = 14

df['di_high'] = df.dm_high.rolling(DI_DAYS).sum() / df.tr.rolling(DI_DAYS).sum()
df['di_low'] = df.dm_low.rolling(DI_DAYS).sum() / df.tr.rolling(DI_DAYS).sum()

さらに補助的な指標として、+DI と -DI の差分を表す DX を計算し、さらに移動平均をとって ADX とします。

ADX_DAYS = 14

def calc_dx(di_high, di_low):
  a = np.abs(di_high - di_low)
  b = di_high + di_low
  return a / b
  
df['dx'] = calc_dx(df.di_high, df.di_low)
df['adx'] = df.dx.rolling(ADX_DAYS).mean()

ふー、やっと計算が終わったので可視化してみます。今回は高値・安値が計算材料となっているので、直近250日の日足ローソク足チャートと、+DI/-DI/ADX の折れ線グラフを上下に作画してみます。

f:id:mariyudu:20190512193118p:plain
日足ローソク足チャートと DMI グラフ

指標の使い方は、+DI が -DI を上抜けたら買いサイン、その逆が売りサインとするのが標準的らしいですが、どうも上のグラフを見る限りではダマシの連続ですね… ADX も線の上昇・下降がトレンドを表す筈なのですが、ここ1年くらいはボックス相場的な様相のせいか、うまく追従できてない印象。自分もこの指標は今回が初めてなので、もうちょっと付き合ってみる必要がありそうです。

という訳で、今回もコード一式と実行結果は Google Colaboratory でどうぞ↓。

colab.research.google.com

Pandas 演習としてのテクニカル指標計算 〜 コポック買い指標の巻

10連休短けぇ… もっと色々やりたかったのに… と萎える連休最終日、ブログでも書いて締めますかね。今回は「コポック買い指標」というちょっと聞き慣れないやつを取り上げてみます。これは、月の平均株価を前年同月と比較して騰落率を算出しておき、さらにその移動平均をとって売買サインとする、というものです。月単位なので当然、中長期な売買向けの指標となりますね。

今回の株価データは10年以上程度の長期データが必要なので、いつもの日経平均プロフィルさんではなく、FRED(セントルイス連邦準備銀行)から2000年以降の日経平均を取り寄せることにします。有名所のデータソースなら Pandas DataReader が使えるので、こいつでサクッと。

df = pdt.DataReader('NIKKEI225', 'fred', '2000/1/1',  '2019/04/30')
df.dropna(how='any', inplace=True) # 欠損値を含む行を削除

取得したデータは日足(といっても終値だけですが)なので、月単位の平均に加工します。

df['year'], df['month'] = df.index.year, df.index.month # 年と月の列を追加
df_monthly = df.groupby(['year', 'month']).mean() # 年月単位で終値平均を算出
df_monthly.sort_values(by=['year', 'month'], inplace=True) # この後の集計作業に備えて年・月でソートしておく

下ごしらえができたので、コポック買い指標を計算。対前年騰落率→加重移動平均算出、という流れです。

WMA_MONTHS = 10 # 対前年騰落率の移動平均月数
WMA_WEIGHTS = np.arange(WMA_MONTHS) + 1 # 加重平均用の重み

df_monthly['prev_year_rate'] = df_monthly.rolling(13).apply(lambda x: x[-1] / x[0] - 1, raw=True) # 対前年騰落率を計算
df_monthly['prev_year_rate_wma'] = df_monthly.prev_year_rate.rolling(WMA_MONTHS).apply(lambda x: np.average(x, weights=WMA_WEIGHTS), raw=True) # 加重移動平均を計算

この指標の使い方はシンプルで、上昇に転じたら買いサイン、下降に転じたら売りサインとされるようです。という訳でこのルールで売買サインも検出しておきます。

def detect_sign(vals):
  if vals[0] > vals[1] and vals[1] < vals[2]:
    # 上昇に転じたら買いサイン
    return 1
  elif vals[0] < vals[1] and vals[1] > vals[2]:
    # 下降に転じたら売りサイン
    return -1
  return 0

df_monthly['sign'] = df_monthly.prev_year_rate_wma.rolling(3).apply(detect_sign, raw=True)

ひととおり計算が終わったので可視化を。月株価とコポック指標を上下にグラフ作画し、それぞれに売買サインに挟まれた保持期間を青く塗り潰してみます。

f:id:mariyudu:20190506172723p:plain
月次株価とコポック指標

今回も、コード一式と実行結果は Google Colaboratory で公開してまつ↓。

colab.research.google.com

Pandas 演習としてのテクニカル指標計算 〜 RCI の巻

東証も前代未聞の長いお休みに入った10連休3日目、いかがお過ごしでしょうか。そんな今回は RCI を。順位相関指数と言うやつで、N 日間の株価について昇順に順位を採り、時系列に沿って 1, 2, … N と順当な上昇であれば 100% に、逆に N, N-1, … 1 と下降なら -100% に近づくというものです。直観的で分かりやすいっすよねー。

例によって、日経平均プロフィルさんから株価データ取得のコードは、シリーズ第1回目を参照ください。

株価(終値)の順位計算には、Pandas の Series/DataFrame に rank() というメソッドがあるので、これを活用します。

# 計算期間(日数)
RCI_DAYS = 9

# RCI 計算関数
def calc_rci(prices):
    day_cnt = len(prices)
    # 日付昇順ランク
    rank_day = np.arange(day_cnt) + 1
    # 株価昇順ランク
    rank_price = np.array(pd.Series(prices).rank())
    rci = 1 - (6 * ((rank_day - rank_price)**2).sum()) / (day_cnt * (day_cnt**2 - 1))
    return rci * 100 # パーセント値で返却

# RCI を計算して列追加
df['rci'] = df.close.rolling(RCI_DAYS).apply(calc_rci, raw=True)

直近250日について、株価と RCI のグラフを並べてみます。

f:id:mariyudu:20190429154118p:plain
直近250日の株価と RCI

ついでに、RCI が最小・ゼロ付近・最大の日付について、時系列と株価の順位相関も散布図で見える化して、「うんうん」と納得してみましょー。

f:id:mariyudu:20190429154206p:plain
RCI 散布図

うんうんw。今回も、コード一式と実行結果は Google Colaboratory でどうぞー↓。

colab.research.google.com