会社の同僚に触発され、Rustの勉強も兼ねてSlackボットを作ってみました。
使ったライブラリなどを含めてご紹介させて頂きます。
※ 作成時のRustのバージョンは1.15.1です。
全体像
構成はシンプルです。
cronで30分毎にSlackボット(Rust CLI)を実行します。
Slackボットは起動すると指定株価一覧が記載されたCSVを読み込み、そこに記載された株の価格情報をGoogle Finance APIに取得しにいきます。
前日の終値から3%以上上昇もしくは3%以上下落した株の価格をSlackにメッセージとして送信します。
ソースコードはこちらに公開しています。
https://github.com/x1-/finance
※ Google Finance APIの詳細はこちらの記事をどうぞ。
プロジェクトの作成
Cargoを使ってプロジェクトのテンプレートを作成します。
CLIなのでバイナリ実行可能なように –bin オプションを付与します。
cargo new finance --bin
financeという名称で作成しました。
cd finance/ tree . ├── Cargo.toml └── src └── main.rs 1 directory, 2 files
Cargo.toml に依存を書きます。
[dependencies] chrono = "0.3" csv = "0.14" docopt = "0.7" env_logger = "0.3" futures = "^0.1.7" hyper = "0.10" hyper-openssl = "^0.2.1" lazy_static = "^0.2.2" regex = "^0.2.1" rustc-serialize = "*" slack-hook = "0.3" time = "0.1" tokio-core = "^0.1.3"
多くのバージョンに ^ を付けて、パッチ・バージョンまで指定していますが、このように書くと「マイナー・バージョンが同じで、記載したパッチ・バージョンより高いバージョンのものがあればそちらを使う」ようになります。
メイン・プログラム
プログラムはシンプルで、APIのクライアントを記述した api_client.rs と main.rsの2ファイルのみとなっています。
全容は GitHub掲載のとおりですが、 メイン・プログラムを記述した main.rs を順に説明させて頂こうと思います。
いろいろな構造体の定義
はじめに、構造体をたくさん定義しています。
docoptで実行時引数を受け取るための構造体、CSVの各レコードを表す構造体、APIのレスポンスを受け取る構造体などです。
下記はCSVを解釈するための構造体です。
#[derive(Debug, RustcDecodable)] struct Record { code: String, name: String, market: String }
Debug を付与しないと printできません。
RustcDecodable を付与すると csv でパースできるようになります。
csv は内部的に rustc_serialize を使っているのですね。
docopt用usage文字列の作成
const USAGE: &'static str = r" to notice kabu rate of up or down at slack-channel. Usage: finance --tick=<tick> --ratio=<ratio> [--webhook=<url>] [--term=<term>] [--data=<csv>] finance --version Options: -h --help Show this message. --version Show version. --tick=<tick> candle tick interval by seconds [default 86400]. --ratio=<ratio> the threshold ratio of price up or down [default 0.1]. --webhook=<url> webhook url of slack integration. if empty, do not send slack [default empty]. --term=<term> the term of measuring price [default 7d]. --data=<csv> the csv listed the stocks [default ./data/stocks.csv]. ";
ここの部分はdocoptに渡すusage文字列を定義しています。
pythonのdocoptと似ていますが、pythonのものほど柔軟性はありません。。。
tick などに default値を定義していますが、これが解釈されてデフォルト値として扱われるわけではありません。
引数が渡されなかった場合、空文字として扱われます。
よって数値引数が渡されなかった場合、エラーとなります。
clapだとどうなのか気になるところ。
各クライアントの生成
Google FinanceのAPIクライアントは hyper のHTTPクライアントを使って作成しました。
SSLを解釈できるHTTPクライアントが hyper くらいしかありませんでした。
let client = api_client::Ssl::new();
Slack投稿クライアントにはslack-hookを使いました。
今回は株の上昇幅を投稿するだけだったのでシンプルなslack-hookを選択したのですが、会話するなら Yobotとかなのかな。
let slack = Slack::new( args.flag_webhook.as_str() );
値動きを追う株
Slackに投稿する対象の株を data/stocks.csvに定義しています。
※ 将来的にはDB管理もありかなと。
CSVファイルの読み込みには便利なcsvライブラリを使っています。
これを使うとCSVファイルを RustcDecodable な構造体のベクターにマッピングできます。
let mut file = csv::Reader::from_file( args.flag_data ).unwrap();
データ・ファイルのパスも実行時引数としていますが、これはデフォルトを data/stocks.csv にしています。
データを取得して条件に該当する株価をSlackにポスト
CSVから取得したレコードを1行つ処理し、 Google Finance APIに現在の株価を問い合わせます。
前日の終値から3%以上上昇もしくは3%以上下落した株の価格をSlackにメッセージとして送信します。
for r in file.decode() { let r: Record = r.unwrap(); let url = format!( "{uri}&p={term}&i={tick}&x={market}&q={code}", uri = URL_BASE, term = args.flag_term, tick = args.flag_tick, market = "TYO", code = r.code ); let res = &client.sync_get( &url ); let data: Result<Vec<Stock>, String> = data_to_struct( res, args.flag_tick ); let rprice: Result<ComparedPrice, String> = data.and_then( |d| close_rate( &d ) ); match rprice { Ok( ref p ) if p.ratio >= args.flag_ratio || p.ratio < -(args.flag_ratio) => { let payload = slack_payload( r.code, r.name, p.current, p.previous, p.ratio ); if let Ok(ref s) = slack { let res = s.send( &payload ); println!("res: {:?}", res); } println!("found: {:?}", payload); }, Ok( ref p ) => println!( "rate is less than {th}. ratio:{ratio:.3}, [{code}:{name}, now:{current}]", th = args.flag_ratio, ratio = p.ratio, code = r.code, name = r.name, current = p.current ), _ => println!( "cannot calculate ratio" ) } }
投稿アイコンに絵文字を使えるので、上昇した株は :chart_with_upwards_trend: 、下降した株は :chart_with_downwards_trend: を使って上昇/下降をわかりやすくしています。
ざっとこんな感じのプログラム構成になっています。
私はこれを使って仕事中に注目している株の値動きをウォッチしています。
使ったライブラリまとめ
ライブラリ | 用途 |
---|---|
chrono | 日時文字列を変換したり、日時計算したり。 |
csv | APIから取得したデータをパース。 |
docopt | コマンドライン引数のパースに。clapにしておけばよかったかなあ… |
env_logger | ログを出力。 |
hyper | HTTPクライアントを作成。 |
hyper-openssl | APIがSSLだったので。 |
lazy_static | グローバル変数の実行時初期化に。 |
regex | 正規表現に。 |
slack-hook | Slackにメッセージを送信。 |