初心者が一攫千金を目指してBitcoin自動取引botを作るよ！ その４【サーバレス化】

![サーバレスアーキテクチャ.png](https://qiita-image-store.s3.amazonaws.com/0/172701/b5cb13bd-be65-bcb7-df45-e54d1aae4024.png)
今回は[前回](http://qiita.com/speedkingg/items/7f122e1fd90558c531f4)作った「bitcoin自動取引Botもどき」を
AWSのLambdaを利用して今流行の**サーバレス構成**にしました！
クラウドにあるので、私のPCが止まっても半永久的に動き続けます。まさにBotですね！

クラウドの構成について、
サーバレスを選んだ理由は利用料が**すごく安い** + **管理が楽**と聞いたからです。[[参考]](https://aws.amazon.com/jp/serverless/)

![Picture６７.png](https://qiita-image-store.s3.amazonaws.com/0/172701/b71f4939-790f-f001-a81f-a60af6e8f747.png)


実際に今回使ったサービスを見てみると、、、
下記の構成で**月額１$!!**
![Presentation4.png](https://qiita-image-store.s3.amazonaws.com/0/172701/1f046ad1-27d6-c579-07dc-04adfdfd290d.png)

# 試算

| 対象サービス | 見積もり料金 |
|:-----------|------------:|
| Lambda | $0 |
| DynamoDB | $0 |
| cloudwatch | $1.00? |

>*Lambda*
>・Lambda では 1 か月に 1,000,000 件の無料リクエストおよび 400,000 GB-秒のコンピューティング時間が無料利用枠となっています。[[参考]](https://aws.amazon.com/jp/lambda/pricing/)
>>▶１分に１回１ヶ月実行すると、1 x 60m x 24h x 31day = 44,640回なので**余裕**

>*DynamoDB*
>・AWS 無料利用枠には、Amazon DynamoDB の 25 GB のストレージ、2 億リクエストまでの処理が含まれます。[[参考]](https://aws.amazon.com/jp/dynamodb/pricing/)
>>**2 億…（　゜Д゜）**それは個人で超えられるのか・・・
>> （1 時間あたりの書き込み、読み込みキャパシティーにもよるらしいのですが、
>> 　今回はそこまで激しい使い方をしないので気にしません。）

>*cloudwatch*
>  : 作成された100万回のカスタムイベントに対して$ 1.00[[参考]](https://aws.amazon.com/jp/cloudwatch/pricing/)
>>▶１分に１回１ヶ月実行すると、1 x 60m x 24h x 31day = 44,640回なので$ 1.00？
>>>▶１週間くらい動かしてみて、一向に増える気配がない…
　　１ヶ月立って０円だったら０円に直します。

# 仕様
動きは相変わらず、
**安くなったら買う、そして、高くなったら売る！**みたいな愚直なBotのままです。
早く直したいですが、いいアルゴリズムを思いつきません。。。。

>①前回の取引額をDBから取ってくる（この金額が判断の基準となる）
>②前回の取引が残っていた場合キャンセルする
>②Bitcoinを持っていてかつ値上がりした場合、売る！（そして、取引額をDBに保存する）
>③日本円を持っていてかつ値下がりした場合、買う！（そして、取引額をDBに保存する）

# 実装
上記アーキテクチャ図のAWS内の３つを実装します。（この３つを設定するだけで動きます！！）
![aaaaa.PNG](https://qiita-image-store.s3.amazonaws.com/0/172701/cd3c2a82-9dcc-0c94-fc4b-3e202c5bf846.png)

>1. DynamoDB
>  - テーブル作成
>2. Lambda
>  - コーディング
>  - コード実行環境へデプロイ
>3. CloudWach
>  - スケジューラに上記で設定したLambdaをセットする

## １．DynamoDB
![Picture1.png](https://qiita-image-store.s3.amazonaws.com/0/172701/74c38a9d-ca9c-4fd9-9722-84c7b60ed991.png)

今回使用するコード実行環境のLambdaは逐次実行で値を保存できないので、
データベースを使用します。

下記２つのテーブルを作成します。
>①last_transaction_price - 取引した価格を記録する
>②sequence　-　成立した取引回数を記録する

### **①last_transaction_price - 取引した価格を記録する**
①-１．［テーブルを作成］より、下記テーブルを作成
>・テーブル名：last_transaction_pric
>・プライマリキー：trade_number (数値型)

①-２．［項目の作成］よりレコード登録（最初の１件だけ手動で登録する）
![キャプチャ3.PNG](https://qiita-image-store.s3.amazonaws.com/0/172701/79489437-e797-67fd-dbbf-390d2fbc37ec.png)

※「last_transaction_price」には前回の取引額を入れる
　（覚えていないor取引したことない場合は、現在価格を入れる）

### **②sequence　-　成立した取引回数を記録する**
①-１．［テーブルを作成］より、下記テーブルを作成
>・テーブル名：sequence
>・プライマリキー：name (文字列)

①-２．［項目の作成］よりレコード登録（最初の１件だけ手動で登録する）
![キャプチャ4.PNG](https://qiita-image-store.s3.amazonaws.com/0/172701/7f5715b3-2a79-e3a6-9e85-1ad0d25deb62.png)


## ２．Lambda
![Picture2.png](https://qiita-image-store.s3.amazonaws.com/0/172701/6e59404b-2172-627e-50f7-c7c3ff66bf3c.png)
>①コーディング
>②ロール作成
>③Lambda作成
>④デプロイ
>⑤テスト

### **①コーディング**
**構成**
今回、色々増えています。
［］・・・フォルダ
> [zaif]
>  　|- *main.py*
>  　|- [config] - *zaif_keys.json*　-　zaifのkey,secretを保管
>  　|- [aws] - *dynamodb_utils.py*　-　awsのDynamoDBのAPIを使いやすいようにまとめたクラス
>  　|- [define] - *common_define.py*　- （主にDynamoDBで使う）文字列を定義してまとめたクラス
>  　|- [exception] - *error.py*　-　独自エラーをまとめたクラス群
>  　|- ...<略>　-　[その１](http://qiita.com/speedkingg/items/2d874beee26106ce9f9a#２apiインストール)でインストールしているので

※他で使うPythonファイルを作るときは、同じフォルダに「\_\_init__.py」を入れないと読み込めません。（ファイルの中身は空で問題ないです）
 (上記の構成では記載を割愛しています)
例：![キャプチャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/172701/a818ca39-a2c7-35be-be36-934a30c521fd.png)

■main.py

```python:main.py
# -*- coding: utf-8 -*-
import json
from zaifapi import ZaifPublicApi  # Zaifが公開している認証情報が要らないAPIを実行するクラス
from zaifapi import ZaifPrivateApi  # Zaifが公開している認証情報が必要なAPIを実行するクラス
from pprint import pprint  # 表示用(jsonをきれいに表示してくれる)
from aws.dynamodb_utils import DynamoDBUtils  # Dynamodbを操作するクラス
from define.common_define import CommonDefine  # テーブル名等定数を定義したクラス
from exception.error import *  # 独自定義のエラーを定義したクラス

dynamodb = DynamoDBUtils()

zaif_keys_json = open('config/zaif_keys.json', 'r')
zaif_keys = json.load(zaif_keys_json)

KEY = zaif_keys["key"]
SECRET = zaif_keys["secret"]

zaif_public = ZaifPublicApi()
zaif_private = ZaifPrivateApi(KEY, SECRET)


# Lamdaから呼び出したときにココが実行される
def lambda_handler(event, context):
    try:
        # 現在の情報取得
        info = _get_info()
        last_price = info["last_price"]
        funds_btc = info["funds_btc"]
        funds_jpy = info["funds_jpy"]
        open_orders = info["open_orders"]

        # 前回の取引額取得
        trade_count = _get_trade_count()
        last_transaction_data = _get_last_transaction_data(trade_count)
        last_transaction_price = last_transaction_data[CommonDefine.LAST_TRANSACTION_PRICE]
        order_id = last_transaction_data[CommonDefine.ORDER_ID]

        print('▼[info] last_transaction_price: ' + str(last_transaction_price) + ', order_id: ' + str(order_id))

        # 取引が保留になっていた場合キャンセルを行う
        if order_id > 0 and open_orders > 0:
            try:
                print('■キャンセルを行いました。')
                pprint(zaif_private.cancel_order(order_id=order_id))
            except Exception as e:
                print(e)
            else:
                # 前回の取引金額を削除する
                _delete_last_transaction_price(trade_count)
        elif open_orders == 0:
            dynamodb.update_by_partition_key(CommonDefine.LAST_TRANSACTION_TABLE,
                                             CommonDefine.TRADE_NUMBER, trade_count,
                                             CommonDefine.ORDER_ID, 0)

        # btcを持っていて値上がりしたとき
        if funds_btc != 0 and last_price > last_transaction_price:
            _sell_btc(last_price, funds_btc)

        # jpyを持っていて値下がりしたとき
        # （最小単位（0.0001btc分）の日本円以上を持っていたとき）
        elif funds_jpy > last_transaction_price / 10000 and last_price < last_transaction_price:
            _buy_btc(last_price, funds_jpy)

    except ResourceNotFoundError:
        raise
    except IllegalArgumentError:
        raise
    except Exception:
        raise


# 前回の取引額取得
def _get_last_transaction_data(trade_count):
    try:
        last_transaction_data = dynamodb.get_item_by_partition_key(CommonDefine.LAST_TRANSACTION_TABLE,
                                                                   CommonDefine.TRADE_NUMBER, trade_count)

        if "Item" not in last_transaction_data:
            raise ResourceNotFoundError("last_transaction_data is not found.")

        if CommonDefine.LAST_TRANSACTION_PRICE not in last_transaction_data["Item"]:
            raise ResourceNotFoundError(CommonDefine.LAST_TRANSACTION_PRICE + " is not found.")

        if CommonDefine.ORDER_ID not in last_transaction_data["Item"]:
            raise ResourceNotFoundError(CommonDefine.ORDER_ID + " is not found.")

    except ResourceNotFoundError:
        raise
    except IllegalArgumentError:
        raise
    except Exception:
        raise

    return last_transaction_data["Item"]


def _get_trade_count():
    trade_count_data = dynamodb.get_item_by_partition_key(CommonDefine.SEQUENCE,
                                                          CommonDefine.SEQUENCE_NAME, CommonDefine.TRADE_COUNT)
    if "Item" not in trade_count_data:
        raise ResourceNotFoundError("trade_count_data is not found.")

    if CommonDefine.COUNT_NUMBER not in trade_count_data["Item"]:
        raise ResourceNotFoundError(CommonDefine.COUNT_NUMBER + " is not found.")

    return trade_count_data["Item"].get(CommonDefine.COUNT_NUMBER)


def _get_info():
    info = {}

    info["last_price"] = int(zaif_public.last_price('btc_jpy')["last_price"])

    trade_info = zaif_private.get_info2()
    info["funds_btc"] = trade_info["funds"]["btc"]
    info["funds_jpy"] = trade_info["funds"]["jpy"]
    info["open_orders"] = trade_info["open_orders"]

    print('■ 現在の情報です。')
    print('last_price: ' + str(info["last_price"]))
    print('funds_btc: ' + str(info["funds_btc"]))
    print('funds_jpy: ' + str(info["funds_jpy"]))
    print('open_orders: ' + str(info["open_orders"]))

    return info


def _sell_btc(last_price, funds_btc):
    try:
        trade_result = zaif_private.trade(currency_pair="btc_jpy", action="ask",
                                          price=last_price, amount=funds_btc)
        print('■ Bitcoinの売却申請を行いました。')
        pprint(trade_result)
        if trade_result["order_id"] == 0:
            _put_last_transaction_price(last_price, 0)
            print('■ 取引が完了しました。')
        elif trade_result["order_id"] != 0:
            _put_last_transaction_price(last_price, trade_result["order_id"])
            print('■ 取引が保留になっています。')

    except ResourceNotFoundError:
        raise
    except IllegalArgumentError:
        raise
    except Exception:
        raise


def _buy_btc(last_price, funds_jpy):
    try:
        # APIが小数点以下４桁までの対応なのでround()
        amount = round(float(funds_jpy) / last_price, 4)
        print('▼[trace]_buy_btc')
        print('amount: ' + str(amount))
        # Bitcoinを買う
        trade_result = zaif_private.trade(currency_pair="btc_jpy", action="bid",
                                          price=last_price, amount=amount)
        print('■ Bitcoinの購入申請を行いました')
        pprint(trade_result)
        if trade_result["order_id"] == 0:
            _put_last_transaction_price(last_price, 0)
            print('■ 取引が完了しました。')
        elif trade_result["order_id"] != 0:
            _put_last_transaction_price(last_price, trade_result["order_id"])
            print('■ 取引が保留になっています。')

    except ResourceNotFoundError:
        raise
    except IllegalArgumentError:
        raise
    except Exception:
        raise


# 取引した情報をDBへ書き込み
def _put_last_transaction_price(last_transaction_price, order_id):
    try:
        trade_count = _get_trade_count()

        trade_count += 1
        put_record_value = {
            CommonDefine.TRADE_NUMBER: trade_count,
            CommonDefine.LAST_TRANSACTION_PRICE: last_transaction_price,
            CommonDefine.ORDER_ID: order_id
        }
        dynamodb.put_item(CommonDefine.LAST_TRANSACTION_TABLE, put_record_value)

        dynamodb.update_by_partition_key(CommonDefine.SEQUENCE,
                                         CommonDefine.SEQUENCE_NAME, CommonDefine.TRADE_COUNT,
                                         CommonDefine.COUNT_NUMBER, trade_count)
    except ResourceNotFoundError:
        raise
    except IllegalArgumentError:
        raise
    except Exception:
        raise


# 前回の取引情報を削除する
def _delete_last_transaction_price(trade_count):
    try:
        trade_count -= 1
        dynamodb.delete_by_partition_key(CommonDefine.LAST_TRANSACTION_TABLE,
                                         CommonDefine.TRADE_NUMBER, CommonDefine.SEQUENCE_NAME)

        dynamodb.update_by_partition_key(CommonDefine.SEQUENCE,
                                         CommonDefine.SEQUENCE_NAME, CommonDefine.TRADE_COUNT,
                                         CommonDefine.COUNT_NUMBER, trade_count)
    except ResourceNotFoundError:
        raise
    except IllegalArgumentError:
        raise
    except Exception:
        raise

```

■[config] - *zaif_keys.json*
・[初心者が一攫千金を目指してBitcoin自動取引botを作るよ！ その３](http://qiita.com/speedkingg/items/7f122e1fd90558c531f4#実装)参照

■[aws] - *dynamodb_utils.py*
・DynamoDBを操作する基本的なメソッドをまとめてます。
・*main.py*から引用して使用しています。

```python:dynamodb_utils.py
# coding:utf-8

import boto3
from exception.error import *  # 独自定義のエラーを定義したクラス


class DynamoDBUtils:
    def __init__(self):
        self.dynamodb = boto3.resource('dynamodb')

    # 指定したテーブルのレコードをすべて取得（上限 1 MB ）
    def scan(self, table_name):
        if isinstance(table_name, str) is False or isinstance(table_name, unicode):
            raise IllegalArgumentError('item is not str/unicode')

        table = self.dynamodb.Table(table_name)
        response = table.scan()

        print('▼[trace][DynamoDB access]scan')
        print('table_name: ' + table_name)
        print('response:')
        print(str(response))

        return response

    # テーブルを指定して受け取ったjson(item)をレコードとして挿入または上書き
    def put_item(self, table_name, item):
        if isinstance(table_name, str) is False or isinstance(table_name, unicode):
            raise IllegalArgumentError('item is not str/unicode')
        if isinstance(item, dict) is False:
            raise IllegalArgumentError('item is not dict')

        table = self.dynamodb.Table(table_name)
        response = table.put_item(
            Item=item
        )

        print('▼[trace][DynamoDB access]put_item')
        print('table_name: ' + table_name)
        print('put_item: ' + str(item))
        print('response:')
        print(str(response))

        return response

    # テーブルとプライマリーキーを指定してレコードを取得
    def get_item_by_partition_key(self, table_name, partition_key, partition_key_value):
        if isinstance(table_name, str) is False or isinstance(table_name, unicode):
            raise IllegalArgumentError('item is not str/unicode')

        table = self.dynamodb.Table(table_name)
        response = table.get_item(
            Key={partition_key: partition_key_value}
        )

        print('▼[trace][DynamoDB access]get_item_by_partition_key')
        print('table_name: ' + table_name)
        print('partition_key: ' + str(partition_key) + ', partition_key_value: ' + str(partition_key_value))
        print('response:')
        print(str(response))

        return response

    # テーブルとプライマリーキー・ソートキーを指定してレコードを取得
    def get_item_by_partition_key_and_sort_key(self, table_name, partition_key, partition_key_value,
                                               sort_key, sort_key_value):
        if isinstance(table_name, str) is False or isinstance(table_name, unicode):
            raise IllegalArgumentError('item is not str/unicode')

        table = self.dynamodb.Table(table_name)
        response = table.get_item(
            Key={partition_key: partition_key_value,
                 sort_key: sort_key_value}
        )

        print('▼[trace][DynamoDB access]get_item_by_partition_key_and_sort_key')
        print('table_name: ' + table_name)
        print('partition_key: ' + str(partition_key) + ', partition_key_value: ' + str(partition_key_value))
        print('sort_key: ' + str(sort_key) + ', sort_key_value: ' + str(sort_key_value))
        print('response:')
        print(str(response))

        return response

    # テーブルとプライマリーキーと更新項目を指定してレコードを更新
    def update_by_partition_key(self, table_name, partition_key, partition_key_value, update_key, update_value):
        if isinstance(table_name, str) is False or isinstance(table_name, unicode):
            raise IllegalArgumentError('item is not str/unicode')

        table = self.dynamodb.Table(table_name)
        response = table.update_item(
            Key={
                partition_key: partition_key_value
            },
            UpdateExpression='SET ' + update_key + ' = :uk',
            ExpressionAttributeValues={
                ':uk': update_value
            }
        )

        print('▼[trace][DynamoDB access]update_by_partition_key')
        print('table_name: ' + table_name)
        print('partition_key: ' + str(partition_key) + ', partition_key_value: ' + str(partition_key_value))
        print('update_key: ' + str(update_key) + ', update_value: ' + str(update_value))
        print('response:')
        print(str(response))

        return response

    # テーブルとプライマリーキーを指定してレコードを削除
    def delete_by_partition_key(self, table_name, partition_key, partition_key_value):
        if isinstance(table_name, str) is False or isinstance(table_name, unicode):
            raise IllegalArgumentError('item is not str/unicode')

        table = self.dynamodb.Table(table_name)
        response = table.delete_item(
            Key={partition_key: partition_key_value}
        )

        print('▼[trace][DynamoDB access]delete_by_partition_key')
        print('table_name: ' + table_name)
        print('partition_key: ' + str(partition_key) + ', partition_key_value: ' + str(partition_key_value))
        print('response:')
        print(str(response))

        return response

```



■[define] - *common_define.py*
・主にDynamoDBで使う文字列を定義してまとめたクラスです。
・あとからカラム名等を変更したくなったときに楽できるようにまとめて管理します。

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

    # dynamoDB table name
    LAST_TRANSACTION_TABLE = "last_transaction_price"
    SEQUENCE = "sequence"

    # dynamoDB key name
    TRADE_NUMBER = "trade_number"
    LAST_TRANSACTION_PRICE = "last_transaction_price"
    SEQUENCE_NAME = "name"
    COUNT_NUMBER = "count_number"
    ORDER_ID = "order_id"

    # other
    TRADE_COUNT = "trade_count"
```

■[exception] - *error.py*
・独自エラーをまとめたクラス群です。
・出したいエラー名が標準で用意されていないときに使用します。

```python:error.py
# -*- coding: utf-8 -*-
class ResourceNotFoundError(Exception):
    # 取得対象のデータが存在しない場合の例外

    def __init__(self, message):
        self.message = message

    def __str__(self):
        return "Requested resource " + self.message + " not found."


class IllegalArgumentError(Exception):
    # 入力された引数が不正な場合の例外

    def __init__(self, message):
        self.message = message

    def __str__(self):
        return "Illegal argument (" + self.message + ")"
```


### **②ロール作成**
・Lambdaを実行する上で必要な権限を割り振ったロールを作成します。
![キャプチャ33.PNG](https://qiita-image-store.s3.amazonaws.com/0/172701/cc48f8ba-f891-3d99-2f27-8be4fc6d7de8.png)

>１．IAM ▶ ロール ▶ 「新しいロールの作成」を選択
>２．ロールタイプの選択： AWS Lambda
>３．ポリシーのアタッチ：AmazonDynamoDBFullAccess , AWSLambdaFullAccess
>４．ロール名を入力して、「ロールの作成」

### **③Lambda作成**
・実際にコードを実行するLambdaを作ります。
![キャプチャaaaaa.PNG](https://qiita-image-store.s3.amazonaws.com/0/172701/1d98373e-4c08-7eed-3b2a-f24e97a589ae.png)

>１．Lambda ▶ 関数 ▶ 「Lambda関数の作成」を選択
>２．左側の「関数の設定」タブをクリック
>３．名前（なんでもよい）を入力する
>　　ランタイム*：python2.7を選択
>　　既存のロール*：「**②ロール作成**」で作成したロールを選択
>　　詳細設定 ▶ タイムアウト：10秒へ値を増やす
>４．「次へ」 ▶ 「作成」

### **④デプロイ**
・**③Lambda作成**で作成したLambdaにコードを乗せます。
>１．「設定」タブを開き、ハンドラを**main.lambda_handler**へ修正
>　　（main.pyを呼び出して実行するという意味）
![キャプチャaws.PNG](https://qiita-image-store.s3.amazonaws.com/0/172701/1fe81ec0-d705-320f-973c-fa264d235291.png)
>２．**①コーディング**で作成したコード群をzipで固める
>　　**※[zaif]-[zaif]-main.pyとならないように注意！（正：[zaif]-main.py）**
>３．「コード」タブを選択し、ファイルをアップロードする
>![キャプチqャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/172701/ef70c4be-72a2-52d5-9fee-a564e725ca4a.png)

### **⑤テスト**
>「保存してテスト」ボタンを押し、**実行結果: 成功**となればOK！
![aaaaキャプチャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/172701/4a56bb1b-e8d4-e02e-ba0f-6c20ad64bc98.png)

## ３．CloudWach
![Picture3.png](https://qiita-image-store.s3.amazonaws.com/0/172701/89d79ea1-136b-3293-03db-122c35aeeac6.png) 
・上記で作成したLambdaが定期実行されるように設定します。
![キャプチ.PNG](https://qiita-image-store.s3.amazonaws.com/0/172701/3cb75af6-4b91-d20f-0001-80f272f3b97e.png)

>１．CloudWatch ▶ ルール ▶ ルールの作成
>２．「スケジュール」▶ 値を１へ変更
>　　「ターゲットの追加」▶機能*：**上記で作成したLambda関数**
>![キャaプチャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/172701/e21bba59-1c6e-8e97-032d-20ef3df8cbe9.png)
>３．「詳細の設定」▶名前*を入力 ▶ 「ルールの設定」

# おわりに
今回は手順が長かったですね。。。お疲れ様でした！
今回の設定で、**半永久的にBitCoinを取引するBot**ができましたね！
さあ、あとは儲けるロジックを考えるだけ！！(๑•̀ㅂ•́)و✧
試行錯誤していきます٩(•౪• ٩)

# 引用
・[けものフレンズ ロゴジェネレータ](https://aratama.github.io/kemonogen/)
>▶世の中には素晴らしいものを作る方がいらっしゃいますね。
　ありがたく利用させていただきました。ありがとうございました。

# 修正履歴
・2017/4/27：いちばん大事な**main.py**を書き忘れていました。。。すみません。
・2017/4/27：MarkDown記法間違い修正（[alt](http://qiita.com/alt)さんご指摘ありがとうございます。）