Python
AWS
Bitcoin
lambda
linebot
OriginalTISDay 17

海外の仮想通貨取引所が使いにくいので、LINEで話せる美少女コンシェルジュを作る

背景

みなさん、最近仮想通貨が熱いですね。
BitcoinだけでなくEthereumやRipple等のアルトコインと呼ばれるものも
たくさん値上がりしています。

kinds.png

【2017年10月】仮想通貨の種類は1000種類!

上記によるともう1000種類あるんですね。はえ~すごい。
が、しかし、国内の取引所を見てみるとどこも数が少なく(20種類弱くらい:2017/12時点)、
金の卵を見つけるには母数が少ないと感じてしまいました。

なので、
今回は有名な通貨もあり、アルトコインもたくさん(200種類以上ある)
97DIMDFd.pngBittrexというサイトを使ってみました。

【使ってみた結果】
- 全部英語...(当たり前だ)
- ログインがとてもめんどくさい
- (ID/Pw -> ワンタイムパスワード -> メール認証 -> ID/Pw -> ワンタイムパスワード)

→心理的障壁が大きく、「買うのめんどくさい、売るのめんどくさい」で機会を逃すことがよくありました。

日頃から気軽に使ってるLINEで取引できたら、いいのになと思ったので、
今回「LINEで話せる美少女コンシェルジュ」を作るに至りました。

作ったもの

特定のメッセージを送ると、情報の照会や取引を行ってくれるLINE Botを作成しました。
美少女要素は画像だけです(*゚ー゚)

【実装した機能一覧】
1. トレード履歴を取得
2. 買い注文を出す
3. 売り注文を出す
4. 注文をキャンセルする
5. 注文一覧を取得する
6. 所有している通貨一覧を取得する

Screenshot.png

何がどう変わったか

価格をチェックしてから取引を行うまでの面倒な一連の処理がLINEのメッセージを送るだけでできるようになりました!!!
Presentation1.jpg

構成

Presentation1.png

  1. Messaging API (通称 LINE BOT) でメッセージを受け、
  2. 受けたメッセージをAWS API gatewayに転送し、
  3. AWS lambdaで処理を判断し、
  4. BittexのAPI経由で、仮想通貨取引・取引情報照会を行います。
  5. 受け取った結果をAWS lambdaで処理し、
  6. Messaging API(通称 LINE BOT)でメッセージをユーザ(スマフォ)に返します。

作り方

1. 環境設定

1-1. LINE Bot用アカウントを作成する

Line Bot用のアカウントを作成します。
- ページにアクセスし、キャプチャの手順でBotを作成していきます。

Opera スナップショット_2017-12-26_194834_developers.line.me.png

Opera スナップショット_2017-12-26_194858_access.line.me.png

Opera スナップショット_2017-12-26_193642_developers.line.me.png

Opera スナップショット_2017-12-26_193831_developers.line.me.png

Opera スナップショット_2017-12-26_194120_developers.line.me.png

Opera スナップショット_2017-12-26_194257_developers.line.me.png
ちょっとだけ下にスクロールする
Opera スナップショット_2017-12-26_194644_developers.line.me.png

1-2. Bittrexでアカウントを作成する

(直接の入金は手間がかかるので、日本の取引所に入金し、送金することをおすすめします。)

Opera スナップショット_2017-12-26_200639_bittrex.com.png

Opera スナップショット_2017-12-26_200705_bittrex.com.png

->必要事項を入力し、アカウントを作成する

アカウントページへアクセスし、keyとsecretをコピーし、bittrex_keys.jsonへ貼り付ける
WITHDRAWは出金なので、今回は必要でない。
Opera スナップショット_2017-12-26_195207_bittrex.com.png

2. プログラミング

下記構成で作りました。

root/
 ├ [[pipしてきたライブラリたち]]
 ├ bittrex_utils/
 │ ├bittrex_public.py --- bittrexの公開情報を扱うクラス
 │ └bittrex_private.py --- bittrexの個人情報を扱うクラス
 ├ config/
 │ ├bittrex_keys.json --- bittrexのAPIを扱うためのキー
 │ └line_keys.json --- LINEのAPIを扱うためのキー
 ├ define/
 │ ├common_define.py --- 汎用的な単語を管理
 │ ├key_word_define.py --- チャットで使用するキーワードを管理
 │ └message_define.py --- チャットで使用するメッセージを管理
 ├ line_utils/
 │ └line_utils.py --- LINEのメッセージ送信を行うクラス
 ├ utils/
 │ └reply.py --- bittrexの取引を処理し、メッセージを返すクラス
 └ main.py --- 受け取ったメッセージを判別し、適切な処理へ振り分ける
init.pyは省略

pipするパッケージはこちら

python-bittrex==0.2.2
line-bot-sdk
flask

【詳細はGithubをご覧ください】
https://github.com/speedkingg/Bittrex_LineBot

2-1.ソースコード

bittrex_utils/

Bittrex apiをラップして作ったクラス。
取引で使用するメソッドを作成し、まとめてある。

bittrex_public.py
# -*- coding: utf-8 -*-

from bittrex import Bittrex  # Bittrexで取引するためのAPI

class bittrex_public:

    def __init__(self):
        self.bittrex_public = Bittrex(None, None)  # 公開情報を扱うBittrexオブジェクト

    # 取引可能通貨サマリ一覧をList型で返す
    def get_coin_summery_list(self):
        coin_summery_list = []
        response = self.bittrex_public.get_markets()

        for item in response['result']:
            coin_summery = str(item['MarketCurrencyLong'])
            coin_summery_list.append(coin_summery)

        return coin_summery_list

    # 通貨の最小取引単位取得
    def get_min_trade_size(self,market):
        response = self.bittrex_public.get_markets()
        for item in response['result']:
            if item['MarketCurrency'] == market:
                return item['MinTradeSize']

        return False

    # 通貨の終値取得
    def get_last_price(self,market):
        response = self.bittrex_public.get_marketsummary(market)
        return response['result'][0]['Last']

bittrex_private.py
# -*- coding: utf-8 -*-

import json  # jsonを使用するため
from pprint import pprint  # 表示用(jsonをきれいに表示してくれる)
from bittrex import Bittrex  # Bittrexで取引するためのAPI

# 設定ファイル読み込み--------------
bittrex_keys_json = open('config/bittrex_keys.json', 'r')
bittrex_keys = json.load(bittrex_keys_json)

KEY = bittrex_keys["key"]  # アクセスキー読み込み
SECRET = bittrex_keys["secret"]  # シークレットキー読み込み
# -------------------------------

class bittrex_private:
    def __init__(self):
        # self.bittrex_public = Bittrex(None, None)  # 公開情報を扱うBittrexオブジェクト
        self.bittrex_private = Bittrex(KEY, SECRET)  # 個人の情報を扱うBittrexオブジェクト

    # トレード履歴を取得
    def get_order_history(self):
        response = self.bittrex_private.get_order_history()
        pprint(response)
        return response

    # 買い注文を出す
    # marketに通貨ペア、quantityに注文する量、rateに価格を指定
    def buy_alt_coin(self, market, quantity, rate):
        response = self.bittrex_private.buy_limit(market=market, quantity=quantity, rate=rate)
        # 成功なら注文id、失敗ならfalseを返す
        if response['success'] is False:
            print(response)
            return False
        else:
            return response['result']['uuid']

    # 売り注文を出す
    # marketに通貨ペア、quantityに注文する量、rateに価格を指定
    def sell_alt_coin(self, market, quantity, rate):
        response = self.bittrex_private.sell_limit(market=market, quantity=quantity, rate=rate)
        # 成功なら注文id、失敗ならfalseを返す
        if response['success'] is False:
            print(response)
            return False
        else:
            return response['result']['uuid']

    # 注文をキャンセルする
    def order_cancel(self, uuid):
        response = self.bittrex_private.cancel(uuid=uuid)
        # 成功ならtrue、失敗ならfalseを返す
        print(response)
        return response['success']

    # 注文一覧を取得する
    def get_orders(self):
        order_list = []
        response = self.bittrex_private.get_open_orders()
        if response['success'] is False:
            print(response)
            return False
        else:
            # 注文が1件もない場合
            if len(response['result']) == 0:
                return None

            for item in response['result']:
                # 通貨の種類と量、注文IDを抜き出す
                balance = {}
                balance['market'] = item['Exchange']
                balance['quantity'] = item['Quantity']
                balance['uuid'] = item['OrderUuid']
                order_list.append(balance)

        return order_list

    # 所有している通貨をList型で返す
    def get_balances(self):
        balance_list = []
        response = self.bittrex_private.get_balances()
        if response['success'] is False:
            print(response)
            return False
        else:
            for item in response['result']:
                # 利用可能な通貨量が0の場合はスキップする
                if item['Available'] == 0:
                    continue
                # 通貨の種類と量を抜き出す
                balance = {}
                balance['currency'] = item['Currency']
                balance['available'] = item['Available']
                balance_list.append(balance)

        return balance_list

config/

各種APIを利用するためのキーを保存するための設定ファイル

bittrex_keys.json
{
  "key":"<Bittrex APIのアカウントページからコピーする>",
  "secret":"<Bittrex APIのアカウントページからコピーする>"
}
line_keys.json
{
  "channel_access_token" : "<LINEの個人ページからコピーする>"
}

define/

返信用メッセージや処理分岐に使用するキーワードなど、直接処理内容に関係ない部分を抜き出して管理する。

common_define.py
# -*- coding: utf-8 -*-
class COMMON_DEFINE:
    def __init__(self):
        pass

    # 通貨ペアをBTCにするための接頭語
    PREFIX_BTC = "BTC-"

    # 通貨の単位(BTC)
    CURRENCY_UNIT_BTC = "btc"

key_word_define.py
# -*- coding: utf-8 -*-
class KeyWordDefine:
    def __init__(self):
        pass

    # line_search_keyword

    # 買い注文を出す
    ORDER_BUY = ['買う', '買い', '買って']
    # 売り注文を出す
    ORDER_SELL = ['売る', '売り' , '売って']
    # 注文をキャンセルする
    ORDER_CANCEL = ['キャンセル']
    # 注文一覧を取得する
    ORDERS_LIST = ['注文一覧', 'オーダー一覧']
    # 所有している通貨一覧を取得する
    WALLET = ['所有', 'もってるやつ']
    # ヘルプを出す
    HELP = ['ヘルプ', 'へるぷ']

message_define.py
# -*- coding: utf-8 -*-

class MessageDefine:
    def __init__(self):
        pass

    # line_reply_key_word

    TRADE_ID = "取引ID"

    # line_reply_message

    # ヘルプメッセージ
    HELP_MESSAGE = "メッセージフォーマットだよ\n\n"\
                    + "[所有しているコイン一覧]\nkey_word: 所有、もってるやつ\n\n"\
                    + "[買い注文]\nformat: 買い <通貨略称> <btc量>\n\n"\
                    + "[売り注文]\nformat: 売り <通貨略称> <alt_coin量>\n\n" \
                    + "[キャンセル]\nformat: キャンセル<注文ID>\n\n" \
                    + "[注文一覧]\nkey_word: 注文一覧、オーダー一覧\n\n" \
                    + "[ヘルプを出す]\nkey_word: へるぷ"

    # 所有しているコイン一覧を返すときのメッセージ
    OWNED_COIN_SUMMARY_MESSAGE = "今所有しているコインの一覧だよ"

    # 処理に失敗した際に返すメッセージ
    FAILED_TRADE_MESSAGE = "取引に失敗しちゃった。。。"

    # コインの購入申請をしたときに返すメッセージ
    APPLY_FOR_PURCHASE_MESSAGE = "指定したコインを購入申請したよ"

    # 取引中の注文がないときに返すメッセージ
    NO_ORDER_MESSAGE = "取引中の注文はないよ"

    #  取引中の注文を返すメッセージ
    ORDER_LIST_MESSAGE = "取引中の注文一覧だよ"

    # 取引をキャンセルしたときに返すメッセージ
    CANCEL_MESSAGE = "取引をキャンセルしたよ"

    # 取引のキャンセルに失敗したときに返すメッセージ
    FAILED_CANCEL_MESSAGE = "取引のキャンセルに失敗しちゃった。。。"

line_utils/

LINEで、メッセージを返信するときに使用するクラス。
LINE APIに合わせたフォーマットの整形やトークンの付与等を行う。

line_utils.py
# -*- coding: utf-8 -*-

import json
from linebot.api import LineBotApi
from linebot.models import TextSendMessage


# 設定ファイル読み込み--------------
line_keys_json = open('config/line_keys.json', 'r')
line_keys = json.load(line_keys_json)

channel_access_token = line_keys["channel_access_token"]  # シークレットキー読み込み
# -------------------------------


class line_utils:
    def __init__(self):
        # Line返信用オブジェクト作成
        self.line_bot_api = LineBotApi(channel_access_token)

    def line_reply(self,reply_token, reply_message):
        self.line_bot_api.reply_message(reply_token, messages=TextSendMessage(reply_message))

utils/

main処理から呼び出される
Bitrtrexの取引メソッドとLINEのメッセージ返信メソッドを呼び出し、仮想通貨の取引とLINEの返信を行うクラス

reply.py
# -*- coding: utf-8 -*-


from line_utils.line_utils import line_utils # lineでメッセージを送る
from bittrex_utils.bittrex_private import bittrex_private  # bittrexの個人情報を扱う
from bittrex_utils.bittrex_public import bittrex_public  # bittrexの公開情報を扱う
from define.common_define import COMMON_DEFINE
from define.message_define import MessageDefine

# lineメッセージを受け取って起動する
class reply():
    def __init__(self):
        self.line_utils = line_utils()
        self.bittrex_private = bittrex_private()
        self.bittrex_public = bittrex_public()


    # フォーマットをメッセージにして返す
    def match_keyword_help(self,reply_token):
        # 定義からメッセージを読み込む
        reply_message = MessageDefine.HELP_MESSAGE

        # lineメッセージを返す
        self.line_utils.line_reply(reply_token, reply_message=reply_message)


    # 所有しているコイン一覧をメッセージにして返す
    def match_keyword_wallet(self,reply_token):
        # 定義からメッセージを読み込む
        reply_message = MessageDefine.OWNED_COIN_SUMMARY_MESSAGE + "\n\n"
        # 所有している通貨リストを取得する
        balance_list = self.bittrex_private.get_balances()

        # 所有しているコイン一覧を1行ずつメッセージに追記する
        for item in balance_list:
            reply_message += str(item['currency']) + ": " + str(item['available']) + "\n"

        # lineメッセージを返す
        self.line_utils.line_reply(reply_token,reply_message=reply_message)


    # 指定したコインを買う
    # line_message = "買い <通貨略称> <btc量>"
    def match_keyword_buy(self,reply_token, line_message):
        # 受け取ったlineメッセージを要素に分解する
        line_message_list = line_message.split()
        market_name = line_message_list[1]
        btc_quantity = line_message_list[2]

        # 通貨ペアをBTCにする
        currency_pair = COMMON_DEFINE.PREFIX_BTC + market_name

        # 最小取引量取得
        min_trade_size = self.bittrex_public.get_min_trade_size(market_name)

        # 終値取得
        alt_last_price = self.bittrex_public.get_last_price(currency_pair)

        #注文量計算
        order_quantity = float(btc_quantity)/alt_last_price

        # 最小取引量に合わせる
        order_quantity = int(order_quantity/min_trade_size)*min_trade_size

        # アルトコインを購入し、取引IDを受け取る
        uuid = self.bittrex_private.buy_alt_coin(market=currency_pair ,
                                                 quantity=order_quantity,
                                                 rate=alt_last_price)
        # メッセージ作成
        # 取引失敗の場合
        if uuid is False:
            reply_message = MessageDefine.FAILED_TRADE_MESSAGE

        # 取引成功でトレードが完了している場合
        elif uuid == "":
            reply_message = MessageDefine.APPLY_FOR_PURCHASE_MESSAGE + "\n\n" \
                            + currency_pair + ": " + str(btc_quantity) + COMMON_DEFINE.CURRENCY_UNIT_BTC

        # 取引成功でトレードが完了していない場合
        else:
            reply_message = MessageDefine.APPLY_FOR_PURCHASE_MESSAGE + "\n\n" \
                            + currency_pair + ": " + str(btc_quantity) + COMMON_DEFINE.CURRENCY_UNIT_BTC\
                            + "\n" + MessageDefine.TRADE_ID + " : " + uuid

        # lineメッセージを返す
        self.line_utils.line_reply(reply_token, reply_message=reply_message)



    # 指定したコインを売る
    # line_message = "売り <通貨略称> <alt_coin量>"
    def match_keyword_sell(self,reply_token, line_message):
        # 受け取ったlineメッセージを要素に分解する
        line_message_list = line_message.split()
        market_name = line_message_list[1]
        alt_quantity = line_message_list[2]

        # 通貨ペアをBTCにする
        currency_pair = COMMON_DEFINE.PREFIX_BTC + market_name

        # 最小取引量取得
        min_trade_size = self.bittrex_public.get_min_trade_size(market_name)

        # 終値取得
        alt_last_price = self.bittrex_public.get_last_price(currency_pair)

        # 最小取引量に合わせる
        order_quantity = int(float(alt_quantity) / min_trade_size) * min_trade_size

        # アルトコインを購入し、取引IDを受け取る
        uuid = self.bittrex_private.sell_alt_coin(market=currency_pair ,
                                                 quantity=order_quantity,
                                                 rate=alt_last_price)
        # メッセージ作成
        # 取引失敗の場合
        if uuid is False:
            reply_message = MessageDefine.FAILED_TRADE_MESSAGE

        # 取引成功でトレードが完了している場合
        elif uuid == "":
            reply_message = MessageDefine.APPLY_FOR_PURCHASE_MESSAGE + "\n\n" \
                            + currency_pair + ": " + str(alt_quantity) + market_name.lower()

        # 取引成功でトレードが完了していない場合
        else:
            reply_message = MessageDefine.APPLY_FOR_PURCHASE_MESSAGE + "\n\n" \
                            + currency_pair + ": " + str(alt_quantity) + market_name.lower()\
                            + "\n" + MessageDefine.TRADE_ID + " : " + uuid

        # lineメッセージを返す
        self.line_utils.line_reply(reply_token, reply_message=reply_message)


    # 取引中の注文一覧をメッセージにして返す
    def match_keyword_orders_list(self,reply_token):
        order_list = self.bittrex_private.get_orders()

        if order_list is None:
            reply_message = MessageDefine.NO_ORDER_MESSAGE

        else:
            reply_message = MessageDefine.ORDER_LIST_MESSAGE + "\n\n"
            # 所有しているコイン一覧を1行ずつメッセージに詰める
            for item in order_list:
                reply_message += str(item['market']) + ": " + str(item['quantity']) + "\n"\
                                 + "取引ID: " + str(item['uuid']) + "\n\n"

        # lineメッセージを返す
        self.line_utils.line_reply(reply_token,reply_message=reply_message)


    # 取引中の注文一覧をメッセージにして返す
    # line_message = "キャンセル <取引ID>"
    def match_keyword_cancel(self, reply_token, line_message):
        line_message_list = line_message.split()
        uuid = line_message_list[1]

        # 注文が存在するか確認
        order_list = self.bittrex_private.get_orders()
        if order_list is None:
            reply_message = MessageDefine.NO_ORDER_MESSAGE
            # lineメッセージを返す
            self.line_utils.line_reply(reply_token, reply_message=reply_message)


        else:
            # 注文をキャンセルする
            response = self.bittrex_private.order_cancel(uuid)
            if response:
                reply_message = MessageDefine.CANCEL_MESSAGE
            else:
                reply_message = MessageDefine.FAILED_CANCEL_MESSAGE

            reply_message += "\n" + MessageDefine.TRADE_ID + " : " + uuid

            # lineメッセージを返す
            self.line_utils.line_reply(reply_token, reply_message=reply_message)

main.py

最初に呼び出され、受け取ったメッセージを判別し、適切な処理へ振り分ける

main.py
# -*- coding: utf-8 -*-

from define.key_word_define import KeyWordDefine # キーワード定義情報一覧
from utils.reply import reply #受け取ったメッセージを処理し、LINEでリプライを行うクラス

reply = reply()

# lineメッセージを受け取って起動する
def lambda_handler(event, context):

    # debug
    print(event)
    print(event['body-json']['events'][0]['replyToken'])
    print(event['body-json']['events'][0]['message']['text'].encode('utf-8'))

    # lineイベントの取り出し
    line_event = event['body-json']['events'][0]

    # lineメッセージを返す用のトークン取得
    reply_token = line_event['replyToken']

    # 送られてきたlineメッセージ取り出し
    line_message = line_event['message']['text'].encode('utf-8')

    # [ヘルプ]のキーワードとマッチした場合
    for word in KeyWordDefine.HELP:
        if line_message.find(word) != -1:
            reply.match_keyword_help(reply_token)
            exit()

    # [所有している通貨一覧を取得する]のキーワードとマッチした場合
    for word in KeyWordDefine.WALLET:
        if line_message.find(word) != -1:
            reply.match_keyword_wallet(reply_token)
            exit()

    # [買い]のキーワードとマッチした場合
    for word in KeyWordDefine.ORDER_BUY:
        if line_message.find(word) != -1:
            reply.match_keyword_buy(reply_token, line_message)
            exit()

    # [売り]のキーワードとマッチした場合
    for word in KeyWordDefine.ORDER_SELL:
        if line_message.find(word) != -1:
            reply.match_keyword_sell(reply_token, line_message)
            exit()

    # [注文一覧]のキーワードとマッチした場合
    for word in KeyWordDefine.ORDERS_LIST:
        if line_message.find(word) != -1:
            reply.match_keyword_orders_list(reply_token)
            exit()

    # [キャンセル]のキーワードとマッチした場合
    for word in KeyWordDefine.ORDER_CANCEL:
        if line_message.find(word) != -1:
           reply.match_keyword_cancel(reply_token, line_message)
           exit()

3. デプロイ

  1. 上記で作成したファイルをaws Lamdaへアップロード
  2. API gatewayとaws Lamdaを関連付け、デプロイ
  3. デプロイしたAPI gatewayのURLをMessaging APIと紐付け
  4. 完成\(^o^)/

作ってみて

メッセージ1つで取引できるのは、とても気分的に楽ですね。
このチャットのお陰で、ストレスフリーな売買ができるようになりました!
これからはチャートを見て、気軽に売買ができます!(๑•̀ㅁ•́๑)✧

更新