【AWS】Lambdaでbitflyer約定通知
【きっかけ】
元々AWSを勉強していたことと、3月12日のコロナショックによりbitflyerにて、今年の利益を全て吹き飛ばし、損失街道まっしぐらになったことがきっかけです。(現在損失更新中)
というのも、暴落中にbitflyerのサイトへのアクセスが非常に悪くなり、取引がWebから全く出来なくなったところに敗因があります。(STOP入れてましたが現物で死にました)
しかし、その間でもAPIからならアクセスが可能とのことを風の噂で聞き、実際に試そうといったのが今回の経緯です。
備忘録的なものでコード等、お粗末なところがありますが、同じような考えの人の参考になればと思います。
1. 使用するサービス
(AWS)
- Lambda
- IAM
- Cloudwatch Event
- SSM
(外部サービス)
- LINE API
2.概要
Cloudwatch Eventで約定を確認するLambdaを定期的に実行することで実現していきます。
一つのLambdaでbitflyerAPIとの通信とLINE通知を行っています。
3. 実装
3-1.Lambda作成
以下コード入力
const ssm = new (require('aws-sdk/clients/ssm'))(); const request = require('request'); const crypto = require('crypto'); const momentTimezone = require('moment-timezone'); exports.handler = (event, context, callback) => { const id = process.env['id']; console.log('id: ' + id); //ssmからbitflyerのAPIキーを取得します getParameterFromSystemManager('bitflyer-keys',callback) .then(function(data){ //ここら辺はbitflyerAPIのリファレンスのテンプレ通りです const apikey = data.split(",")[0]; const sercretKey = data.split(",")[1]; var timestamp = Date.now().toString(); var method = 'GET'; var path = '/v1/me/getchildorders?product_code=FX_BTC_JPY &child_order_state=COMPLETED'; //約定ID指定(これで最新の約定を取得する) var afterID = '&after=' if(id){ path += afterID + id; } var text = timestamp + method + path; var sign = crypto.createHmac('sha256' , sercretKey).update(text).digest('hex'); var option = { url: 'https://api.bitflyer.jp' + path, method: method, headers: { 'ACCESS-KEY': apikey, 'ACCESS-TIMESTAMP': timestamp, 'ACCESS-SIGN': sign, 'Content-Type': 'application/json' } } //bitflyerAPIにリクエストを送ります return sendRequest(option, callback); }).then(function(data){ if(data.response.statusCode != 200){ console.error("Error:",data.response); callback(null, { statusCode: data.response.statusCode, body: JSON.stringify({message: data.response}), headers: {"Content-type": "application/json"} }); return; } data = JSON.parse(data.body); if(data.length == 0){ console.log('No data Found'); callback(null,{ statusCode: 200, body: JSON.stringify({message: 'No data Found'}), headers: {"Content-type": "application/json"} }); return; } console.log(data) const dateTimeUtc = momentTimezone.tz(data[0].child_order_date.split(" ")[0], 'UTC'); const dateTimeJst = momentTimezone(dateTimeUtc) .tz('Asia/Tokyo').format('YYYY/MM/DD HH:mm:ss'); const date = new Date(dateTimeJst); const toDay = new Date(momentTimezone(new Date()) .tz('Asia/Tokyo').format('YYYY/MM/DD HH:mm:ss')); //Lambdaの再起動判定です。インスタンスが新たに生成されると環境変数がリセットされてしまいます。 //現在時刻と約定時刻から判断してます if(toDay.getTime() - date.getTime() > 100000){ process.env['id'] = data[0].id; console.log('Lambda Restarted'); callback(null,{ statusCode: 500, body: JSON.stringify({message: 'Lambda Restarted'}), headers: {"Content-type": "application/json"} }); return; } process.env['id'] = data[0].id; var message = '\n'; //約定情報を回していきます data.forEach(function(value){ const dateTimeUtc = momentTimezone.tz(value.child_order_date.split(" ")[0], 'UTC'); const dateTimeJst = momentTimezone(dateTimeUtc) .tz('Asia/Tokyo').format('YYYY/MM/DD HH:mm:ss'); //Lineに送るメッセージ作成 message += "[" + value.side + ": " + value.child_order_type + "]\n" + "Date " + dateTimeJst + "\n" + "price " + value.price + "\n" + "size: " + value.size + "\n" + "average price: " + value.average_price + "\n" + "executed size: " + value.executed_size + "\n" + "----------------------\n"; }) console.log(message); sendLine(message,callback); }); function getParameterFromSystemManager(apikey_name, callback) { return new Promise(function (resolve) { var apikey = process.env[apikey_name]; if(!apikey || typeof apikey == undefined){ var params = { Name: apikey_name, /* required */ WithDecryption:true }; ssm.getParameter(params, function(err, apikey) { if (err){ console.error(err.stack); callback(null,{ statusCode: 500, body: JSON.stringify({message: err.toString()}), headers: {"Content-type": "application/json"} }); resolve(null); return; } process.env[apikey_name] = apikey.Parameter.Value; resolve(apikey.Parameter.Value); }); }else resolve(apikey); }); } function sendLine(message, callback){ getParameterFromSystemManager('line-access-key', callback) .then(function(lineKey){ var option = { url: 'https://notify-api.line.me/api/notify', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + lineKey }, method: 'POST', form :{ message: message } }; return sendRequest(option,callback); }) .then(function(data){ if(data.response.statusCode != 200){ console.error(data) } callback(null, { statusCode: data.response.statusCode, body: data.body, headers: {"Content-type": "application/json"} }); }); } function sendRequest(option, callback){ return new Promise(function (resolve) { request(option, function(error, response, body){ if(error){ console.error(error); callback(null,{ statusCode: 500, body: JSON.stringify({message: error.toString()}), headers: {"Content-type": "application/json"} }); resolve(null); } var data = {response, body} resolve(data); }); }); } };
3-2. Lambdaにssm取得ロール付与
LambdaのPermissionsタブから既存ロールを選択します
以下ポリシーを作成して付与(フルポリシーでもいいです)
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "sts:AssumeRole", "ssm:GetParameter" ], "Resource": "*" } ] }
3-3. ssmのパラメータストアにキーを登録
bitflyerAPIとLINEAPIのキーをそれぞれ設定します。
(bitflyer-keyはapi,sercretの順になっています)
3-4. Cloudwatch EventのRoleに作成したラムダを設定
EventSourceはScheduleにし実行タイミングを設定し、TargetにLambdaを選択し、作成したLambda Functionを設定する
3-5. 最後に
以上で設定した時間おきに約定を確認し、約定してあればLINEが飛ぶようになっています。
利確時は朗報ですが、損失確定のお知らせの時はただムカつくだけです。
目的としては仕事中とかポジションを持っている時にわざわざサイトを見て確認する必要がないところです。
また、今後Lambdaを使って価格通知とかも載せていこうかと思います。
冒頭でいっていたRealTimeAPIも実装してみようかと思います。(こちらはnode or Pythonのオンプレの予定)