特定の銘柄の現在株価を取得して定期的に通知するSlackAppを作った話

Page content

タイトルの通りが、趣味と勉強を兼ねて、株価を取得して通知するSlackAppを作った。ので、その話を整理する。

はじめに

そもそもの始まりは、(実質的に)口数購入しかできない個別銘柄の積立自動入金にSBI証券が対応していないこと にあった。投資信託銘柄の積み立て自動入金は、「○○銘柄に毎月10000円」とか設定しておくと、合計で必要になる購入額を連携口座から自動引き落としで入金してくれる設定がある。(「銀行引落サービス」という)最初勘違いしていたんだが、個別株も、積み立て設定しておけば、その設定に従って毎月勝手に計算して指定銀行からお金引き落として入金してくれる、と思っていたのだが、そんなものはなかった。いや正確に言うと金額指定して積み立て設定してる場合は動く(らしい、買ったことないからしらない)のだが、(って設定画面↓に書いてある)
SBI証券の「銀行引落サービス」設定画面の一部スクリーンショット、「引落対象」に「日株」「米株」があるがどちらも「金額指定」(米株の場合は「円決済」)が対象になっている

私が保有しているのは全部「単元未満株取扱対象外銘柄」というやつで、金額指定で端数が出るような買い方ができない(例えば1口550円だとして、1000円分購入するとなると、1.8口くらいになってしまうので購入できない)。その時点の単価×口数と完全一致する金額指定ならできるのかもしれないが、日々価格が変動するからその時点で積み立て設定できても意味がない。必然的に口数指定で積み立て設定するしかなくなるが、そうなると上の通りで自動入金が動かない。

苦肉の策として、以下の対応を行うこととした。

  1. ティッカーシンボルと購入予定口数を保存
  2. 購入予定日に合わせて1.の設定から現在株価×口数で購入価格を計算(米株の場合は為替レートでJPYに換算)
  3. 2.を自分に通知
  4. 3.の通知を受けて、購入予定金額を手動でSBI証券に入金

この記事の本旨は1.~3.である。4.は手動運用で(できるなら4.まで含めて全部自動化したい…けどAPIがないようなのでおそらく無理)Slack云々とは別の話である。3.の「通知」というところが肝で、なんらか自分にわかる形で自動的に連絡してくれれば、例えばメールとかでもなんでも良かったのだが、

  • 通知があったときにすぐ気づけそう、という点だとSlackが一番適していた
  • 1.~3.の過程でどうせなにかプログラムというか仕組みを作ることになるわけだから、そういう意味でもSlackAppがやりやすそうだと思った
  • そういえば個人的にSlackAppをちょっと勉強してみたかった

ということでSlackAppを作ることにした。

構成要素

  • SlackAppはslack-edgeで作った。あとHono。Hono好きです。
  • 上記1.の設定を保存するためにDatabaseが必要で、これはHeroku Postgresqlにした。ただ、これ以前からもともと個人プロダクトで使ってるので、今回なわせて新規に建てたというより間借りである。後述するがSlackApp自体のホスティングもHerokuにしているためである。
  • SlackApp(アプリケーション)からDatabaseに接続・操作するためのクライアントライブラリとしてDrizzleを採用。Drizzle好きです。
  • 上に書いた通りで、SlackAppのホスティングはHeroku。これには(一応)理由がある。詳細後述。
  • 株価の取得はyahoo-finance2ライブラリを使っている。詳細後述。
  • 為替レートの取得はhttps://open.er-api.com/v6/latest/USDを使っている。ほかにも色々あるみたいなのでここは個人の趣味で決めてよいだろう。

yahoo-finance2(とHeroku)

特定銘柄の株価取得のAPIは、いくつか探したのだが、こういう用途で個人が比較的自由にアクセスできるようになっているものがないようだ。探した限りではyahoo-finance2ライブラリがTypescript対応・型定義も充実していて使いやすく、その時はあまり深く考えずに「これでいいや」と思って採用した。だが、このライブラリは内部的にnode:fsnode:cryptoなど、Node.jsのAPIに依存している部分があるようで、workerdだと動かせないことがわかった。実はこれがホスティングにHerokuを採用した理由になっている。本当はCloudflare Workersにしようと思ってた(DatabaseもD1にしようと思ってた)のだが、wranglerで起動したらエラーになって、調べたらyahoo-finance2にその辺を含むということがわかり、じゃあまあCloudflare Workersは諦めるかと思ってHerokuに至った。こういうところがCloudflare Workersの辛いところだったりする。そして逆にサーバーを丸ごと使えるHerokuの強みでもある。

ただ、その後になって色々調べてみたところでは、yahoo-finance2ライブラリは、内部的にはYahoo Financeが公開している(?)エンドポイントhttps://query1.finance.yahoo.com/に、User-Agentのヘッダーいれて投げてるだけだ。似たようなのでyf-importっていうのもあったが、これも同じである。実際、ヘッダ入れずにリクエスト投げると429: Too Many Requestがレスポンスされてエラーになるが

$ curl https://query1.finance.yahoo.com/v8/finance/chart/GOOGL
Edge: Too Many Request

User-Agentいれると回避できる。

$ curl -H "User-Agent: Mozilla/5.0" https://query1.finance.yahoo.com/v8/finance/chart/GOOGL
{"chart":{"result":[{"meta":{"currency":"USD","symbol":"GOOGL","exchangeName":"NMS"
...

なんとなく正規の使い方かどうか怪しい気がするが、これでよければfetchで十分代替え可能なわけで、そうなるとCloudlfare Wokersでも動かせることになる。これはそのうち直すかもしれない。


なお今更いうことでもないが、このライブラリ(というよりYahoo FinanceのAPI)は、米国だけではなく日本の国内株式も対応している。ただ東証の場合は末尾に".T"を付けて呼び出す必要がある。こんな感じ↓

$ npx yahoo-finance2 quote 316A.T
Storing cookies in /home/ricemountainer/.yf2-cookies.json
{
  language: 'en-US',
  region: 'US',
  quoteType: 'ETF',
  typeDisp: 'ETF',
  quoteSourceName: 'Delayed Quote',
  triggerable: false,
  customPriceAlertConfidence: 'LOW',
  currency: 'JPY',
  ...
}

国内株式のIDも「ティッカーシンボル」って呼称していいのか知らないのだが、内部的には全部"symbol"で統一されているので本記事も「ティッカーシンボル」と呼ぶことにする。

slash commandの実装

上記1.にあたる部分はSlash Commandで実装した。この際「ティッカーシンボル」と「購入口数」を2つ、ユーザーから入力してもらうことになるわけだが、これはslack-edgeではreq.payload.textにまとめて入ってくる。例えば/hogehoge GOOGL 1と実行した場合はreq.payload.text"GOOGL 1"となっている。なので内部的にsplitして切り離して使う。

ティッカーシンボルのほうは、yahoo-finance2に投げて株価を取得する。これはconst quote = await yahooFinance.quote(symbol);で株価情報を取得できる。返却されるQuoteオブジェクトには非常に大量の情報を含むが、要するに「今の株価」って意味ではregularMarketPriceを見ればいいらしい。(Githubにはそのようなことが書いてある)

それと通貨情報としてcurrencyを取得して合わせて保存しておく。このフィールドにはJPYとかUSDとかっていう文字列値が入っている。米国株だとUSDになる。この値を基準に為替レートの適用を判定することになる。為替レートも日々変わっていくので、Slash Commandの実行時点で保存しておくのはここまで。実際の購入予定金額の計算時に使用する。と、ここまで書いててふと思ったが、銘柄の通貨が途中で変わることってあるんだろうか??そうなると登録時点で通貨も一緒に保存しておくのはナシだな…まあこれはそのうち調べておくか…

定期通知の実装

SlackAppはユーザーのアクションをもとに「受動的に」動く仕組みであり、SlackApp自身が「能動的に」動くものではない。なので「定期実行して結果を通知」というのをユーザーからの指示なしでSlackApp自身が動くことができない。なので「定期実行」を した後に、SlackAppが自動で動き出すようなトリガー的な仕組みが必要だった。

パっと思いついたのは/remindコマンドとの併用だった。例えば/remind #hogehoge "hey check stock @hogeapp" every day at 10:00みたいな感じでリマインダーを仕組んでおき、Slackから定期的にChannelにメッセージを投稿してもらう。SlackApp側はapp.event('app_mention', ...)app.message('check stock', ...)等でこれを待ち構えておき、このリスナー内で処理を実行、結果をreq.context.sayで通知する。という流れである。なおどちらにしてもEvent Subscriptionの有効化が必要になる。一応これで想定通りの動きはしてくれている。以下がそのスクリーンショット。
リマインダーが"check stock @hogehoge"の形でSlackAppをメンションして、その後Slack Appが動いて株価と為替レートをもとに合計の購入価格を計算して、Slackにメッセージ投稿している様子

今考えるとこれは色々やりかたあるはずで、トリガーが「自分へのメンション」や「特定のメッセージ文字列」なら、例えばIncoming Webhookchat.postMessage等でも可能で、これをホスティングサービスの定期実行の仕組み(Herokuなら例えばAdvanced Scheduler、WorkersならCron Triggers)からfetchすればいい。

「現在株価と予定株数に基づく購入金額の計算」処理自体を独立させて、適当にエンドポイントとして露出させ、スケジューラーからfetchし、処理の最後でIncoming Webhookで通知、という流れでもできなくはない。この場合だとSlackAppへの「指示」(にあたるメッセージの投稿)が不要となり、Channelにはその処理結果だけが通知されることになるので、見た目はちょっとシンプルになりそうだ。一方、任意のタイミングで確認しようとしたとき、外部からcurl等でコールしなければならず、運用面での制約が増えそうではある。(SlackAppのリスナーに用意しておくメリットは、SlackのChannelで特定のメッセージやメンションを投稿するだけで、いつでも動かせることにある)。まあこの辺は実装と運用の好みによるんだろう。/remindとの併用は個人的にはいいアイディアだと思っており、このまましばらくは継続するつもりである。

この後の話

例えばyahoo-finance2のQuoteオブジェクトにはregularMarketChangePercentっていうフィールドがあり、前回市場のCloseから今に至るまでの間の数値変動の割合がここで確認できる。これにより、「急激に下がった」「急激に上がった」銘柄をある程度監視して、発生次第Slackでメンション通知するような仕組みを構築することは、理論上可能である。そうなるとかなり柔軟に株の売買取引に対応できそうだと想定される。まあこの程度のアラート通知機能はSBI証券にも備わってそうだけど…(よくしらない)

これはさすがにある程度多頻度でやらなければならなくなりそうで、/remindとかで毎回メッセージ投稿してから動いてもらう作りだとChannelの視認性が大きく損なわれることが想定されるため、やるにしても別口で独立させた方が良さそうではある。それと、Yahoo Finance APIのレートリミット的なところも気になる。万が一IPごとBANされるようなことがあれば、この機能どころか全体としての運用が立ち行かなくなる。作るにしても慎重に進めていきたいところだ。

ただ、ここまでかいておいてなんだが、個人的に個社株ってそこまで積極的にやるつもりはなく、やるにしてもせいぜいETFちょいちょい買ってるレベルの日和見投資なので、そんなにガチって実装する気も(今の所は)あまり起きない。SlackAppは非常に出来ることが多い面白いプロダクトだと(今更)分かりつつあり、これを使って他に作りたいものが色々あるので、そっちに時間をかけて注力していきたい欲が今は強い。yahoo finance APIのfetchコール化なんかを含めてそのうち趣味で適当にいじくるかもしれないが、とりあえず自分がやりたい最低限のことは実装できたので、これでしばらく運用の様子見をすることにする。