【AWS】Lambdaでbitflyer約定通知

【きっかけ】

元々AWSを勉強していたことと、3月12日のコロナショックによりbitflyerにて、今年の利益を全て吹き飛ばし、損失街道まっしぐらになったことがきっかけです。(現在損失更新中)
というのも、暴落中にbitflyerのサイトへのアクセスが非常に悪くなり、取引がWebから全く出来なくなったところに敗因があります。(STOP入れてましたが現物で死にました)
しかし、その間でもAPIからならアクセスが可能とのことを風の噂で聞き、実際に試そうといったのが今回の経緯です。
備忘録的なものでコード等、お粗末なところがありますが、同じような考えの人の参考になればと思います。

 

1. 使用するサービス

(AWS)

  • Lambda
  • IAM
  • Cloudwatch Event
  • SSM   

 
(外部サービス)

  • Bitflyer Lightning API  (今回はHTTP APIです。いずれRealTime APIでもやろうと思います)

 

2.概要

Cloudwatch Eventで約定を確認するLambdaを定期的に実行することで実現していきます。
一つのLambdaでbitflyerAPIとの通信とLINE通知を行っています。

architecture
構成図

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タブから既存ロールを選択します

lambda-role
ロール付与

以下ポリシーを作成して付与(フルポリシーでもいいです)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "sts:AssumeRole",
                "ssm:GetParameter"
            ],
            "Resource": "*"
        }
    ]
}


3-3. ssmのパラメータストアにキーを登録
bitflyerAPIとLINEAPIのキーをそれぞれ設定します。

名前はそれぞれbitflyer-keysline-access-keyです。
TypeはSecureStringにし、一つのキーに対して複数値がある場合は、Valueの中を図のようにカンマ区切りにします。
(bitflyer-keyはapi,sercretの順になっています)
parameter-store
APIキー登録

3-4. Cloudwatch EventのRoleに作成したラムダを設定
EventSourceはScheduleにし実行タイミングを設定し、TargetにLambdaを選択し、作成したLambda Functionを設定する
  

cloudwatchEvent
Eventの設定

3-5. 最後に
以上で設定した時間おきに約定を確認し、約定してあればLINEが飛ぶようになっています。
利確時は朗報ですが、損失確定のお知らせの時はただムカつくだけです。
目的としては仕事中とかポジションを持っている時にわざわざサイトを見て確認する必要がないところです。
また、今後Lambdaを使って価格通知とかも載せていこうかと思います。
冒頭でいっていたRealTimeAPIも実装してみようかと思います。(こちらはnode or Pythonのオンプレの予定)