わたねこコーリング

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

lightweight-charts でローソク足チャートをサクサク生成

以前、TechanJS というフィナンシャルチャート作画ライブラリの紹介をしましたが、今日はまた別のチャート作画ライブラリ↓の紹介を。

github.com

上記にあるように lightweight-charts は、トレーダー向けに各種ツールを提供している TradingView さんが OSS として公開しているものです。流石にその筋のプロが作ったというべきか、軽量・コンパクトながらツボを押さえた機能設計がなされていて、使いやすい印象を持ちました。ドキュメント類はリファレンスが見つからなかったのですが、サンプル利用ガイドを読みながら見様見真似で、何とか欲しかった機能が実現できました。CodePen に日経平均データを使ったサンプルを置いたので、どうぞ↓。


See the Pen
lightweight-charts でお手軽ローソク足チャート作画
by まりゆどぅ (@Mariyudu)
on CodePen.

そもそもの発端は、ある条件でスクリーニングした銘柄の値動きを一定期間ウォッチする環境を作ろう、というものでした。さらには、過去のデータを使って値動きを予想する訓練にも使いたいので、指定日付以降の日足はグラフ表示させずに一日単位でスクロール表示させたいな、と。TechanJS でももちろん実現出来ますが、lightweight-charts のほうが少ないコード量で書けました。よく出来たライブラリなので、もっとユーザが増えるといいですねー。

PHP 5.3 プログラムの保守環境を1分で用意する

オトナの事情で PHP 5.3 系のシステムなんてものを保守する必要に迫られて DockerHub を漁ってたんですが、流石にサポート切れのバージョンはオフィシャルは勿論、草の根ベースでもなかなか厳しいですな。そんな中、なんとかちょうど良さげなのがあったので備忘録を。こいつです↓

hub.docker.com

Alpine ベースなので貧弱貧弱ゥな鯖でもサクサク動いて助かります(EC2 t3.nano で確認)。色々種類があるのですが、最初からよく使う PHP モジュールが同梱された Apache2 版の cespi/php-5.3:modules-apache が良さげ。Dockerfile の公開は無いので history をざっと見たけど、素性も特に悪くなさそう。以下、docker run するまでの簡単な例です。

まず、作業ディレクトリを作って PHP ファイルをデプロイ。

$ mkdir -p /foo/bar/htdocs
$ cat <<EOS > /foo/bar/htdocs/info.php
<?php
phpinfo();
EOS

あとは、そのディレクトリを指定してコンテナを起動するだけ。

$ docker container run \
--rm -itd \
-p 8000:80 -v "/foo/bar/htdocs:/var/www/htdocs" \
-e "APACHE_DOCUMENT_ROOT=/foo/bar/htdocs" \
cespi/php-5.3:modules-apache-latest

curl http://localhost:8000/info.php などとすれば動作確認できます。Cake1.2 のアプリという、目眩がしそうな古いプログラムを稼働させて見ましたが問題無いようです。尚、PHP の設定を調整したい場合は、/foo/bar/php.ini を作っておいて、パラメタに

-v /foo/bar/php.ini:/usr/local/etc/php/conf.d/custom.ini:ro

を指定してやれば OK です。楽ちん!

※ちなみに用意されている PHP モジュールですが、コンテナの中に入って確認したところ以下のとおりでした。

# php -m
[PHP Modules]
apc
bcmath
Core
ctype
curl
date
dom
ereg
fileinfo
filter
ftp
gd
hash
iconv
json
ldap
libxml
mbstring
mcrypt
memcache
memcached
mysql
mysqlnd
openssl
pcre
PDO
pdo_mysql
pdo_sqlite
Phar
posix
readline
Reflection
session
SimpleXML
soap
SPL
SQLite
sqlite3
standard
tokenizer
xml
xmlreader
xmlwriter
xsl
zip
zlib

[Zend Modules]

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