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

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

Pandas 演習としてのテクニカル指標計算 〜 ボリンジャーバンドの巻

シリーズ3回めはボリンジャーバンドを。個人的には最も使用頻度が高い指標かも。

日経平均プロフィルさんから株価データ取得のコードはここでは割愛。シリーズ第1回目を参照ください。

まずバンドの中心ラインとなる単純移動平均を算出し、同日数での株価標準偏差を算出してプラスとマイナスそれぞれの3本バンドを算出、という流れです。

# 計算期間(日数)
BB_DAYS = 25

# 株価移動平均を計算
df['ma'] = df.close.rolling(BB_DAYS).mean()

# 株価標準偏差を計算
df['sigma'] = df.close.rolling(BB_DAYS).std()

# ボリンジャーバンドを計算
for n in range(1, 4):
  df[f"bb_h{n}"] = df.ma + df.sigma * n
  df[f"bb_l{n}"] = df.ma - df.sigma * n

株価(終値)とボリンジャーバンドを重ね書きした可視化(↓)。

f:id:mariyudu:20190414141548p:plain
ボリンジャーバンドの図(全期間)

全期間だとごちゃごちゃして分かりづらいので、2018年ぶんだけ作画してみます(↓)。

f:id:mariyudu:20190414141644p:plain
ボリンジャーバンドの図(2018年)

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

colab.research.google.com

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

珍しくやる気になってるので、鉄は熱いうちにという訳で今回は MACD の計算でつ。長短2本の指数平滑移動平均の差分をとり、さらにその移動平均をとったものとの差分が売買シグナルになるというオシレータ系の指標です。個人的にはどうも意味合いというか使い所がイメージできないのですが、一般的には広く利用されているポピュラーな手法ですね。

株価データは前回同様に日経平均プロフィルさんの日経平均日足データ CSV を使うのでコードは割愛。

前回書いた指数平滑移動平均計算関数を流用しつつ、MACD の計算はこんな感じ。

# 指数平滑移動平均計算
def calc_ema(prices, period):
    ema = np.zeros(len(prices))
    ema[:] = np.nan # NaN で初期化
    ema[period-1] = prices[:period].mean() # 最初だけ単純移動平均
    for d in range(period, len(prices)):
        ema[d] = ema[d-1] + (prices[d] - ema[d-1]) / (period + 1) * 2
    return ema

# MACD 計算
def calc_macd(prices, period_short, period_long, period_signal):
    ema_short = calc_ema(prices, period_short)
    ema_long = calc_ema(prices, period_long)
    macd = ema_short - ema_long # MACD = 短期移動平均 - 長期移動平均
    signal = pd.Series(macd).rolling(period_signal).mean() # シグナル = MACD の移動平均
    hist = macd - signal # ヒストグラム
    return macd, signal, hist

df['macd'], df['signal'], df['hist'] = calc_macd(df.close, 12, 26, 9)

MACD とシグナルの差分をとってヒストグラムとします。一般的にはこいつがプラスに転じれば買いサイン、マイナスに転じれば売りサイン、という訳ですな。株価、MACD とシグナル、ヒストグラムの順に可視化したグラフがこんな感じです。

f:id:mariyudu:20190406181835p:plain
株価・MACD とシグナル・ヒストグラム

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

colab.research.google.com

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

Jupyter Notebook 上で主にシストレをテーマにしたデータいぢりを始めて1年あまり経ちますが、いまいち NumPy・Pandas・Matplotlib がマスター出来た気になれないので、公開しながら演習することにしました。プライベートのスニペットバンクにちまちま書き溜めるよりは気合いが入るのじゃないかと…

Python には TA-Lib というテクニカル分析ライブラリがありますが、これを使わずに NumPy や Pandas の機能をフル活用して自前で計算することでデータ処理のスキルを磨こうという狙いです。てな訳で、手始めに移動平均の類から。

尚、計算の対象は日経平均にします。日経平均プロフィルさんが2016年からの日足データ CSV を提供されているので、これを有り難く使わせて頂きます。

df = pd.read_csv("https://indexes.nikkei.co.jp/nkave/historical/nikkei_stock_average_daily_jp.csv", encoding="SHIFT-JIS", names=['date', 'close', 'open', 'high', 'low'], header=1);
df = df.dropna() # 最後のコメント行を削除

データフレームの列名は ASCII のほうが何かとトラブルが少ないので、names= で列名を指定しておきます。では、この日足データフレームに、以下のように25日の各種移動平均を算出しながら列追加していきます。

まずは単純移動平均から。これは Pandas の窓関数平均で一発。

df['sma25'] = df.close.rolling(25).mean()

次に加重移動平均。これは関数を作って窓関数に apply してやります。

def calc_wma(prices):
    weights = np.arange(len(prices)) + 1
    wma = np.sum(weights * prices) / weights.sum()
    return wma

df['wma25'] = df.close.rolling(25).apply(calc_wma)

指数平滑移動平均は、当日株価と前回平均を計算するので窓関数での逐次計算では処理できず、全終値からまるっと計算する手法にしました。ループは極力使いたくなかったのですが、無理に行列指向な処理にするとコードの可読性が落ちそうだったので仕方なく…

def calc_ema(prices, period):
    ema = np.zeros(len(prices))
    ema[:] = np.nan # NaN で初期化
    ema[period-1] = prices[:period].mean() # 最初だけ単純移動平均
    for d in range(period, len(prices)):
        ema[d] = ema[d-1] + (prices[d] - ema[d-1]) / (period + 1) * 2
    return ema

df['ema25'] = calc_ema(df.close, 25)

ついでに株価と移動平均の乖離率[%]も計算してみます。

def calc_devrate(prices):
    sma = prices.mean()
    return (prices[-1] - sma) / sma * 100

df['devrate'] = df.close.rolling(25).apply(calc_devrate)

以上、各種移動平均を計算・列追加されたデータフレームがこちらになりまつ。

f:id:mariyudu:20190330173612p:plain
移動平均を加えたデータフレーム

最後にお約束の可視化グラフを。計算した各種移動平均を株価に重ねて作画してみます。期間が長いのでちと重なりがちですが、それぞれの移動平均線の特色がわかる…ような気がしますw 乖離率もその下に並べて置いときます。

f:id:mariyudu:20190330173408p:plain
移動平均をグラフで可視化

上記のコードは Google Colaboratory で公開してあります(↓)。

https://colab.research.google.com/drive/1Od95PnZsuXXMHNA3HeKwRTsSui9XGqSacolab.research.google.com

「INSERT … SELECT … WHERE NOT EXIST …」を少し深掘りしてみた

MySQL で INSERT する際に、所定条件のレコードが既に存在しない時のみ行いたい、ってのはよくある要求のようです。調べたら、こんなハック↓で何とかなる模様。

stackoverflow.com

成る程、INSERT … SELECT 構文の SELECT を工夫する訳か。ただ自分の場合、上記記事の例に倣うと

INSERT INTO table_listnames (name, address, tele)
SELECT * FROM (SELECT 'Rupert', '', '') AS tmp
WHERE NOT EXISTS (
    SELECT name FROM table_listnames WHERE name = 'Rupert'
) LIMIT 1;


みたいに、複数列に同じ値をセットしたかったのですが、それだと「ERROR 1060 (42S21): Duplicate column name ''」と怒られちゃいます。これには

INSERT INTO table_listnames (name, address, tele)
SELECT * FROM (SELECT 'Rupert' AS `name`, '' AS `address`, '' AS `tele`) AS tmp
WHERE NOT EXISTS (
    SELECT name FROM table_listnames WHERE name = 'Rupert'
) LIMIT 1;

と、きっちり列名を付与してやれば良いみたいです。さて問題は解決したのですが、この SQL、SELECT 文が二重になってて何だか分かったような分かんないような。フツーに

SELECT 'Rupert' AS `name`, '' AS `address`, '' AS `tele`
WHERE NOT EXISTS (
    SELECT name FROM table_listnames WHERE name = 'Rupert'
) LIMIT 1;

じゃダメなの? と試してみるとエラー。FROM 句が無いと WHERE 句は使えないのか… それじゃ

SELECT 'Rupert' AS `name`, '' AS `address`, '' AS `tele` FROM tmp
WHERE NOT EXISTS (
    SELECT name FROM table_listnames WHERE name = 'Rupert'
) LIMIT 1;

では? 「tmp なんてテーブルねーぞ」と怒られるなぁ(当たり前)。ここで「MySQL テーブル ダミー」等とぐぐってみたら、MySQL には DUAL というダミーテーブルがあるようです↓。

rnk.mitelog.jp

という訳で

INSERT INTO table_listnames (name, address, tele)
SELECT 'Rupert' AS `name`, '' AS `address`, '' AS `tele` FROM DUAL
WHERE NOT EXISTS (
    SELECT name FROM table_listnames WHERE name = 'Rupert'
) LIMIT 1;

で上手く行きました! 少しシンプルになったぞいww

第6世代 iPod Classic のバッテリー交換したった件

愛用の10年モノ iPod Classic が、さすがにバッテリーがヘタってきて持続時間が2〜3時間程になってしまったので、自力でバッテリー交換してみたです。幸い、ネットにこの種の作業に関連した情報がたくさんあったのでそれらを参考に行いましたが、それでもハマってしまった箇所を少し記録に残しておこうかと。

当方 iPod の型番は MB565J/A。所謂第6世代というブツらしいので、Amazon からこいつを購入。

思ったより早く、2日で届いたので作業開始!

f:id:mariyudu:20190106200538p:plain
愛機とバッテリー交換キット

iPod Classic の裏ブタ開けは大変だと聞いてましたが、確かにここでハマりました(所要時間30分程)。隙間にカッターの刃を差し込んで云々というブログ記事を読んで真似したのだけど、さっぱりダメで、最終的には精密ドライバ(↓)の一番細いものを捩じ込んでスキマを作り、さらに太いドライバー→バッテリー付属のオープナー、という手順でこじ開けました。

f:id:mariyudu:20190106200713p:plain
精密ドライバ

最初は、下記のようなイメージでスマートに開けるのだと思ってたんですが、どうやらこれが間違ってたみたい。

f:id:mariyudu:20190106200745p:plain
裏ブタ開けイメージ(✗)

実際は裏ブタ縁をぐいっと変形させつつ、力ワザで多少強引にやっちゃうのが正解だったみたいです(↓)。

f:id:mariyudu:20190106200824p:plain
裏ブタ開けイメージ(○)

フタを開けちゃえばもう勝ったようなもの。バッテリーを裏ブタから剥がして新しいものを貼り付け。

f:id:mariyudu:20190106200926p:plain
バッテリ交換後の図

最後にバッテリーのコードがコネクタに挿さらない! と焦りましたが、参考記事をよく読むとコネクタ部の黒い凹の字型のストッパ(↓)を上に数ミリ引き上げてから挿すのが正解ですた(要ピンセット)。

f:id:mariyudu:20190106201017p:plain
バッテリコネクタ部

フタを閉める前に、起動確認を。おお、ニューバッテリーって7割程充電済みなのね。

f:id:mariyudu:20190106201108p:plain
バッテリ交換後の起動確認

以上、新年早々の iPod Classic 延命作戦という訳で現場からは以上です。尚、参考にさせて頂いた記事は以下のとおりです。

iPod Classic 160Gの液晶・バッテリ交換
iPod classic (Late 2009)の分解&HDD交換【前編】 | デカの日常
iPod Classic Rubber Bumpers Replacement - iFixit Repair Guide
【iPod Classic修理】ipod Classic分解方法 - YouTube

VSCode + Remote FS で netrw っぽくリモートマシンのファイルを編集する

仕事柄、ちょこちょこっとリモートマシンのファイルを確認・編集したいことが多いのですが、そんな時は主に MacVim + netrw で ssh 越し作業してます。ローカル・リモートともに特別な設定無しで、ファイラでディレクトリを渡り歩きながらの操作が楽ちんなのでつ。

こういう作業を最近流行りの AtomVSCode 等の Electron ベースなエディタで行おうとすると、お薦めとされている拡張機能のほとんどがリモートマシンに rmate が必要だったりプロジェクト毎に諸元設定しなきゃだったりで、多機能ではあっても小回りが効かないのが不満でした。そんな中、みつけたのが Remote FS という VSCode 拡張機能。これは設定で、

"remotefs.remote": {
  "dev": {
    "scheme": "sftp",
    "host": "host",
    "username": "username",
    "rootPath": "/path/to/somewhere"
  }
}

等とリモートサーバ諸元を設定しておけば、あとはコマンドラインから

code --folder-uri sftp://dev/

と叩くだけで下記のように、エクスプローラにファイラが表示されてリモートマシンのディレクトリやファイルを直に操作できます。イイと思いません?

f:id:mariyudu:20181201165526p:plain
Remote FS

残念なのは、~/.ssh/config に登録したホスト情報が使えないことでしょうか。ていうか設定さえも面倒なので更に netrw っぽく、コマンドラインから URI を叩くだけで済むようにならないかなーなんて思います。

CloudWatch コンソールログイン通知でメールが来ない件

小ハマり系小ネタでつ。先日 AWS セキュリティ強化の一環で今さらですが、コンソールログインのメール通知を

dev.classmethod.jp

を読んで実施したのですが、ログインしてみてもさっぱりメールが来ない。CloudTrail にはちゃんとイベントが記録されているのに… と、イベントをよく見たらリージョンが us-east-1 となっている?

f:id:mariyudu:20181115153633p:plain:w500

どゆこと? と、「ConsoleLogin イベント リージョン」あたりでぐぐるとまたもクラスメソッドさんのページがヒット(助かってますぅ)。

dev.classmethod.jp

これによるとコンソールのリージョン設定やサインイン時の URL に依存するようです。当方コンソールは東京にセットしてありますが、サインイン URL を https://ap-northeast-1.console.aws.amazon.com/ec2/v2/home にしても https://s3.console.aws.amazon.com/s3/home にしても CloudTrail にはリージョンは全て us-east-1 で記録されてます。うーむ…

結局、理由はよく分からないまま、メール通知設定での SNS トピック作成および CloudWatch ルール作成の時だけリージョンをバージニア北部に変えてやりなおしたところ、無事メールが届くようになりましたとさ。ちゃんちゃん。