AWSLambdaでLine通知を実装する

注意(2021/2/3追記・2021/2/5追記)

本記事ではYahooファイナンスのスクレイピングを行っているコードがありましたが、Yahooファイナンスのポリシーを確認したところ禁止されていることが分かりました。(Yahooファイナンスヘルプ

そのため、最新の記事の通りALPHA VANTAGEのAPIを使用するように仕様を変更しました。

本記事はLine APIの使い方、BeautifulSoupを使ったスクレイピング、バイナンスAPIの使い方など参考になる箇所があるため残しますが、Yahooファイナンスのスクレイピングは実施しないようにしてください。

下記でGoogleスプレッドシートのGoogleFinance関数を利用する手段を代替として記載しました。

はじめに

なにかしらのサイトを監視し、何かあった場合にLineに通知したい場合はよくあると思う。

今回はFXを題材として、チャートがある一定の値段に到達した際にLineに通知する仕組みを作る。これはループイフダンなどで、レンジ相場を想定していたのにレンジ外にチャートが出てしまった場合に対応する際に有益である。

仕組みとしては、AWS Lambdaで定期的にチャートを監視し、閾値を超えた場合にLineに通知するものである。

今回の内容は以前書いた以下に近い部分もあるので、参考として下さい。

Line Developerに登録する

今回の仕組みはLineの通知に Line APIを使用するため、以下よりLine Developerへの登録が必要である。既にLineを利用しているユーザであればすぐに開始できると思う。

※ 登録自体はある程度すんなりできると思うので特に説明はありません。

登録が完了したら適当な名前でチャンネルを開設する。今回は「FX-Alart」とした。

アクセストークンを取得する

Line APIを使用するにはアクセストークンが必要となる。チャンネルの「Messaging API設定」タブからチャンネルアクセストークンを発行する。

自分のLineからチャンネルを友達登録する

自分のLineアカウントに通知を送るためには作成したチャンネルを友達登録する必要がある。チャンネルの「Messaging API設定」タブにQRコードがあるのでLineアプリのQRコードによる友達追加で読み込んで友達登録する。

Line APIを試してみる

APIを叩いてメッセージが送れるか確認する。今回はLinuxのCURLを使って下記コマンドを実行する。

curl -v -X POST https://api.line.me/v2/bot/message/push \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {アクセストークン}' \
-d '{
    "to": "ユーザーID",
    "messages":[
        {
            "type":"text",
            "text":"Hello, world1"
        },
        {
            "type":"text",
            "text":"Hello, world2"
        }
    ]
}'

参考:Line Developerリファレンス(https://developers.line.biz/ja/reference/messaging-api/#messages

ユーザーIDはLine Developersの「チャンネル基本設定」にある「あなたのユーザーID」を使用する。

Cloud9から試す場合は、CURLをyumインストールする必要がある。

$ sudo yum install curl

Cloud9でLambda Functionを用意する

AWS Cloud9でPythonのLambda Functionを作成する。操作の詳細は前回(Cloud 9 + lambdaでbot作成)触れているので今回は割愛する。

BeautifulSoupをインストール

FXの価格情報を得るためにYahooファイナンスをスクレイピングする。スクレイピングするために今回はBeautifulSoupを使用するためインストールが必要となる。

また、今回はLambdaファンクション名を「FXAlart」としたので同名のディレクトリが自動作成されているものとする。

$ cd ./FXAlart
$ python -m pip install --target=./ BeautifulSoup4

※ FXの価格情報について、本当はGoogle Finance APIから情報を得る予定だったが廃止となっていた。(2021年2月時点)また、YahooのYQLも使用できなくなっていたため泣く泣くYahooファイナンスの情報をスクレイピングすることにした。

AWS Lambdaのソースコード

ここではLambdaのソースコード(lambda_function.py)の中身のみ記載する。Lambdaの記載方法などが分からない方は、Lambdaは個人開発でコストを圧縮したいときには必ず必要な技術だと考えるため、興味がある方は勉強してみてほしい。

ただ、Cloud9にてLambda Functionを自動作成した際にlambda_function.pyも作成されていると思うので、下記コードをコピペしてもらえれば使用可能である。

from bs4 import BeautifulSoup
import urllib.request
import urllib.parse
import json
import os
    
#監視通貨ペアとアラート値(最低値・最大値)を定義
pairs=["AUDNZD","EURGBP"]
alartBase = {"AUDNZD":{"min":os.environ['AUDNZD_MIN'], "max":os.environ['AUDNZD_MAX']},"EURGBP":{"min":os.environ['EURGBP_MIN'], "max":os.environ['EURGBP_MAX']}}
    
#スクレイピング対象を定義
urlForm="https://stocks.finance.yahoo.co.jp/stocks/detail/?code={}=X"
htmlTarget=".stoksPrice"
    
#Line APIを定義
lineUserId="Line ユーザーID"
lineAccessToken="Line アクセストークン"

#Line通知関数を定義
def line_push(lineAccessToken, lineUserId, text):
    url = 'https://api.line.me/v2/bot/message/push'
    data = {
        "to": lineUserId,
	    "messages":[
            {
                "type":"text",
                "text":text
            }
        ]
    }
    headers = {
        'Content-Type': 'application/json',
	    'Authorization': 'Bearer {'+lineAccessToken+'}',
    }
    req = urllib.request.Request(url, json.dumps(data).encode(), headers)
    urllib.request.urlopen(req)

def lambda_handler(event, context):
    for pair in pairs:
        url = urlForm.format(pair)
        res = urllib.request.urlopen(url)
        soup = BeautifulSoup(res, 'html.parser');
        vals = soup.select_one(htmlTarget).findAll(text=True)

        if float(vals[0]) < float(alartBase[pair]["min"]):
            body = pair+"が【"+str(alartBase[pair]["min"])+"】を下まわりました。"
            line_push(lineAccessToken, lineUserId, body)
            body = pair+"は現在【"+str(vals[0])+"】です。"
            line_push(lineAccessToken, lineUserId, body)
        elif float(vals[0]) > float(alartBase[pair]["max"]):
            body = pair+"が【"+str(alartBase[pair]["max"])+"】を上まわりました。"
            line_push(lineAccessToken, lineUserId, body)
            body = pair+"は現在【"+str(vals[0])+"】です。"
            line_push(lineAccessToken, lineUserId, body)
    return ''

ソースコードのポイント

このコードでは、あらかじめ設定した通貨ペアの現在価格が閾値を超えていないか判定し、超えている場合は自分のLineに通知するようにしている。

下記の変更を行うことで自由度が増す。

Line API用のIDとトークンを設定する

ソースコードの中のlineUserIdとlineAccessTokenはLineDeveloperから各自のものを使用する。それぞれ確認方法は本記事の上部に記載している。

通貨ペアと閾値を設定する

通貨ペアはpair変数の中身を変更する。現在はpairs=[“AUDNZD”,”EURGBP”]となっているため、AUD/NZDとEUR/GBPのペアを設定している。この値はYahoo FinanceのURL部分(https://stocks.finance.yahoo.co.jp/stocks/detail/?code={}=X の{}の部分)で使用されるため、あらかじめURLが存在するか確認する必要がある。

閾値はalartBase変数の中身を変更する。上記のpair変数と辞書型のキーを一致させる必要がある。また、最大値と最小値をそれぞれminとmaxで設定できる。

AWS Lambda環境変数を使用する

上記のalartBase変数の最大値、最小値はos.environ[‘AUDNZD_MIN’]のように記載している。これはAWS Lambdaのコンソール画面から設定できる環境変数の値を参照するようにしている。これによりコンソール画面から値を設定でき、ソースコードに手を加える必要がなくなる。

下記のようにAWSコンソール画面からLambda > 関数 > 関数名と進んだ先に設定する項目があり、編集可能である。

Lambdaファンクションのデプロイと定期実行

デプロイはCloud9からならすぐにできる。操作の詳細は前回(Cloud 9 + lambdaでbot作成)触れているので今回は割愛する。

Lambdaトリガーの設定は下記のようにAWSコンソール画面からLambda > 関数 > 関数名と進んだ先に設定する項目があり、編集可能である。

定期実行する際はEventBridge(CloudWatch Events)を選択し、定期実行のための式を入力する。今回は30分に一回の実行としたので「rate(30 minutes)」と設定した。

おわりに

FXのチャートでアラート機能が欲しい場合、無料版アプリでは制限があったり自由度が低い。そんな時、Lambdaで自分好みにカスタマイズして動かすのはアリだと思う。

Lambdaはデフォルトのメモリ(128MB)で動かすとき、1ミリ秒当たり0.0000000021USDである。

今回の処理はCloudWatch Logsから確認すると2秒かからず終了していたので、一回の起動で0.000462円(1ドル110円換算)である。一か月で0.66528円しかかからないので、有料プランのFXチャートを使うよりはコストを抑えられる。

おまけ

Defiが注目されている中、USDTとDAIの運用を始めました。ステーブルコインの価値が大きくぶれると困るので、仮想通貨の監視も下記で始めました。Defi運用については今度記事を書くかもしれません。

from binance.client import Client
import urllib.request
import urllib.parse
import json

#変数を定義
pairs=["USDTDAI","BUSDDAI","BUSDUSDT"]
alartBase = {"min":0.99, "max":1.01}
API_KEY = 'XXXXXXX'
API_SECRET = 'XXXXX'

#Line APIを定義
lineUserId="XXXXXXX"
lineAccessToken="XXXXXXXX"

#Line通知関数を定義
def line_push(lineAccessToken, lineUserId, text):
    url = 'https://api.line.me/v2/bot/message/push'
    data = {
        "to": lineUserId,
	    "messages":[
            {
                "type":"text",
                "text":text
            }
        ]
    }
    headers = {
        'Content-Type': 'application/json',
	    'Authorization': 'Bearer {'+lineAccessToken+'}',
    }
    req = urllib.request.Request(url, json.dumps(data).encode(), headers)
    urllib.request.urlopen(req)

#バイナンスAPIから値を取得		
def get_avg_trades(pair):
    trade_sum = 0
    trade_num = 0
    trades = client.get_recent_trades(symbol=pair)
    for trade in trades:
        trade_sum = trade_sum + float(trade['price'])
        trade_num = trade_num + 1
    trade_avg = trade_sum / trade_num
    return trade_avg

#閾値を超えた場合にアラートを発報
def alart(trade_avg):
    if trade_avg > alartBase['max']:
        text=pair+"が【"+str(alartBase["max"])+"】を上まわりました。"
        line_push(lineAccessToken, lineUserId, text)
        text = pair+"は現在【"+str(trade_avg)+"】です。"
        line_push(lineAccessToken, lineUserId, text)
    elif trade_avg < alartBase['min']:
        text=pair+"が【"+str(alartBase["min"])+"】を下まわりました。"
        line_push(lineAccessToken, lineUserId, text)
        text = pair+"は現在【"+str(trade_avg)+"】です。"
        line_push(lineAccessToken, lineUserId, text)

#Main処理
def lambda_handler(event, context):
    client = Client(API_KEY, API_SECRET)
    for pair in pairs:
        trade_avg = get_avg_trades(pair)
        alart(trade_avg)
    return ''

ここではBinance APIを使って通貨情報を得ています。

YahooファイナンスのスクレイピングとBinance APIを合体したLambdaファンクションは以下。

from bs4 import BeautifulSoup
from binance.client import Client
import urllib.request
import urllib.parse
import json
import os
    
#監視通貨ペアとアラート値(最低値・最大値)を定義
pairs=["AUDNZD","EURGBP"]
alartBase = {"AUDNZD":{"min":os.environ['AUDNZD_MIN'], "max":os.environ['AUDNZD_MAX']},"EURGBP":{"min":os.environ['EURGBP_MIN'], "max":os.environ['EURGBP_MAX']}}

#バイナンス用設定
pairs_binance=["USDTDAI","BUSDDAI","BUSDUSDT"]
alartBase_binance = {"min":os.environ['STABLE_COIN_RATE_MIN'], "max":os.environ['STABLE_COIN_RATE_MAX']}
API_KEY = 'XXXXXXXX'
API_SECRET = 'XXXXXXXX'
client = Client(API_KEY, API_SECRET)
    
#スクレイピング対象を定義
urlForm="https://stocks.finance.yahoo.co.jp/stocks/detail/?code={}=X"
htmlTarget=".stoksPrice"
    
#Line APIを定義
lineUserId="XXXXXXXXXXXXXXXXXX"
lineAccessToken="XXXXXXXXXXX"

#Line通知関数を定義
def line_push(lineAccessToken, lineUserId, text):
    url = 'https://api.line.me/v2/bot/message/push'
    data = {
        "to": lineUserId,
	    "messages":[
            {
                "type":"text",
                "text":text
            }
        ]
    }
    headers = {
        'Content-Type': 'application/json',
	    'Authorization': 'Bearer {'+lineAccessToken+'}',
    }
    req = urllib.request.Request(url, json.dumps(data).encode(), headers)
    urllib.request.urlopen(req)

def get_avg_trades(pair):
    trade_sum = 0
    trade_num = 0
    trades = client.get_recent_trades(symbol=pair)
    for trade in trades:
        trade_sum = trade_sum + float(trade['price'])
        trade_num = trade_num + 1
    trade_avg = trade_sum / trade_num
    return trade_avg

def lambda_handler(event, context):
    # FXアラートのMain処理を実行
    for pair in pairs:
        url = urlForm.format(pair)
        res = urllib.request.urlopen(url)
        soup = BeautifulSoup(res, 'html.parser');
        vals = soup.select_one(htmlTarget).findAll(text=True)

        if float(vals[0]) < float(alartBase[pair]["min"]):
            body = pair+"が【"+str(alartBase[pair]["min"])+"】を下まわりました。"
            line_push(lineAccessToken, lineUserId, body)
            body = pair+"は現在【"+str(vals[0])+"】です。"
            line_push(lineAccessToken, lineUserId, body)
        elif float(vals[0]) > float(alartBase[pair]["max"]):
            body = pair+"が【"+str(alartBase[pair]["max"])+"】を上まわりました。"
            line_push(lineAccessToken, lineUserId, body)
            body = pair+"は現在【"+str(vals[0])+"】です。"
            line_push(lineAccessToken, lineUserId, body)
            
    # 仮想通貨アラートのMain処理を実行
    for pair_binance in pairs_binance:
        trade_avg = get_avg_trades(pair_binance)
        if trade_avg > float(alartBase_binance['max']):
            text=pair_binance+"が【"+alartBase_binance["max"]+"】を上まわりました。"
            line_push(lineAccessToken, lineUserId, text)
            text = pair_binance+"は現在【"+str(trade_avg)+"】です。"
            line_push(lineAccessToken, lineUserId, text)
        elif trade_avg < float(alartBase_binance['min']):
            text=pair_binance+"が【"+alartBase_binance["min"]+"】を下まわりました。"
            line_push(lineAccessToken, lineUserId, text)
            text = pair_binance+"は現在【"+str(trade_avg)+"】です。"
            line_push(lineAccessToken, lineUserId, text)
        
    return ''

環境変数

  • AUDNZD_MIN
  • AUDNZD_MAX
  • EURGBP_MIN
  • EURGBP_MAX
  • STABLE_COIN_RATE_MIN
  • STABLE_COIN_RATE_MAX

インストールモジュール

  • BeautifulSoup
  • binance

設定値(必須)

  • API_KEY
  • API_SECRET
  • lineUserId
  • lineAccessToken

参考

Spread the love