【シストレ】特定のユーザーのツイートに反応する仮想通貨自動売買Botを作る

## 概要

仮想通貨の世界においては、とある1人の人物が相場の動きに対して強大な影響力を持っていたりする事もザラです。

![10-elon-musk-0309-restricted-super-169.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/2d91d84a-3be4-628e-35a7-f8f20ce3a5cb.jpeg)


特に最近だとイーロン・マスク氏などはその主な例であり、巷では「イーロン砲」などと呼ばれて多くの投資家から注目されていたりします。

> ![スクリーンショット 2021-07-04 1.16.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/9636f9d9-760e-b4d1-e31e-467948d4779c.png)

> 出典: [ビットコイン急騰、イーロン・マスク砲が炸裂か](https://coinpost.jp/?p=216800)

もし瞬時にこれに気付いて波に乗る事ができれば、さぞ儲かった事でしょう。

そこで今回は、そういったいわゆるインフルエンサー的な人物のTwitterアカウントを監視し、相場に影響を与えそうなツイートがされた際に素早く反応して自動売買を行うBotを作ってみたいと思います。

![Untitled Diagram(16).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/cb0571c3-b46d-22dd-431a-4ed859056076.png)

<font color="Red">※全体的にお遊びレベルというか、こんなのができたら良いなくらいの軽いノリで作ったのでこのまま実運用できるほどの代物ではないと思います。あくまでアイデアの一環程度に捉えて欲しいです。</font>

## 完成イメージ

![マイ-ムービー(8).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/3587ca7e-daf7-ecd3-10d0-12ec49aae8c5.gif)

特定のユーザーの仮想通貨に関するツイートに反応して注文→決済まで行います。

## 仕様

- 自動売買Bot
  - Docker
  - Python3
- 仮想通貨取引所
  - Bybit testnet（デモ版）
- Twitter API
  - キー
  - トークン

環境構築においては、再現性を考慮してDockerを使用します。Dockerが嫌な場合は各々のやり方に任せますが、仮に不具合などが起きても保証はできないのでご注意ください。

仮想通貨取引所はBybit testnetを選択。アカウント作成→APIキー作成→デモ口座への流れがめちゃくちゃ簡単にできるのでおすすめです。

あと、ツイートを拾うためのTwitter APIも必要になるのでこちらもキーやトークンなどの発行も必須です。

## 下準備

具体的なコードを書いていく前に、Bybit testnetの口座開設やTwitter APIの各種キー・トークンなどが必要になるのであらかじめ準備しておきます。

### Bybit testnet

Bybit testnetの口座開設に関しては、[僕が以前書いた記事](https://qiita.com/kazama1209/items/96587619e85335a74b3a#bybit%E3%83%87%E3%83%A2%E7%89%88%E3%81%AE%E5%8F%A3%E5%BA%A7%E9%96%8B%E8%A8%AD) に詳しく手順が書いてあるのでそちらを参考に進めてください。

※口座開設

### Twitter API

Twitter API関連に関しては、以下の記事を参考に進めてください。

[2021年度版 Twitter API利用申請の例文からAPIキーの取得まで詳しく解説](https://www.itti.jp/web-direction/how-to-apply-for-twitter-api/)

## 実装

下準備が完了したのでいよいよコードを書いていきます。

### 環境構築

まずはPythonが動く環境を作るところから。

#### 各種ディレクトリ・ファイルを作成

```
$ mkdir tweet-reaction-trading-bot
$ cd tweet-reaction-trading-bot
$ touch Dockerfile
$ touch docker-compose.yml
$ touch requirements.txt
$ mkdir opt
$ touch opt/test.py
```

次のような構成になっていればOK。

```
├── opt
│   ├── test.py
├── Dockerfile
├── docker-compose.yml
└── requirements.txt
```

```dockerfile:./Dockerfile
FROM python:3
USER root

RUN apt-get update
RUN apt-get -y install locales && \
    localedef -f UTF-8 -i ja_JP ja_JP.UTF-8

ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ JST-9
ENV TERM xterm

ADD . /codes
WORKDIR /codes

RUN apt-get install -y vim less
RUN pip install --upgrade pip
RUN pip install --upgrade setuptools
RUN pip install -r requirements.txt
```

```yml:./docker-compose.yml
version: "3"
services:
  python3:
    restart: always
    build: .
    container_name: python3
    working_dir: /root/
    tty: true
    volumes:
      - ./opt:/root/opt
```

```:./requirements.txt
ccxt == 1.45.18
tweepy
python-dotenv
```

※ccxtはバージョンアップがかなりの高頻度で行われているため、既存のコードが動かなくなってしまうなんて事が多々あります。今回のコードは上記のバージョン（1.45.18）で動作確認済み。それ以外のバージョンだと正常な動作は保証できないのでなるべく固定してください。

```python:./opt/test.py
print('test')
```

### コンテナを起動

```
$ docker-compose up -d
```

### コンテナの中へ入る

```
$ docker exec -it python3 /bin/bash
```

### Pythonが動くかどうかテスト

```
$ cd opt
$ python test.py
```

上記コマンドを実行し、ターミナルに「test」と表示されれば無事環境構築に成功です。

### 売買ロジックを作成

```
$ touch bybit_api.py
$ touch twitter_api.py
$ touch main.py
$ touch .env
```

次のような構成になっていればOK。

```
├── opt
│   ├── .env
│   ├── bybit_api.py
│   ├── main.py
│   ├── test.py
│   └── twitter_api.py
├── Dockerfile
├── docker-compose.yml
└── requirements.txt
```

```:./opt/.env
TWITTER_API_KEY=APIキー
TWITTER_API_SECRET=APIシークレット
TWITTER_ACCESS_TOKEN=アクセストークン
TWITTER_ACCESS_TOKEN_SECRET=アクセストークンシークレット
TWITTER_SCREEN_NAME=監視対象のユーザーID
BYBIT_API_KEY=APIキー
BYBIT_API_SECRET=APIシークレット
```

下準備編のところで各値を取得していると思うので、それぞれセットしてください。

```python:./opt/bybit_api.py
import ccxt
import os
from dotenv import load_dotenv
load_dotenv()

class BybitAPI:
    def __init__(self):
        self.client = ccxt.bybit(
            {
                'apiKey': os.getenv('BYBIT_API_KEY'),
                'secret': os.getenv('BYBIT_API_SECRET'),
                'urls': {
                    'api': 'https://api-testnet.bybit.com/' # testnet（デモ版）を使用する場合はこの記述が必要
                }
            }
        )

    # 保有中のポジションを取得
    def get_position(self, symbol):
        self.client.load_markets()
        market = self.client.market(symbol)

        return self.client.v2_private_get_position_list({ 'symbol': market['id'] })['result']

    # 注文を作成する
    def create_order(self, symbol, order_type, side, amount):
        order = self.client.create_order(
            symbol,     # 通貨ペア
            order_type, # 成行注文か指値注文か（market: 成行注文、limit: 指値注文）
            side,       # 買いか売りか（buy: 買い、sell: 売り）
            amount,     # 注文量（USD）
            { 
              'qty': amount
            }
        )

        return order
```

```python:./opt/twitter_api.py
import tweepy
import os
from dotenv import load_dotenv
load_dotenv()

class TwitterApi:
    def __init__(self):
        self.api_key = os.getenv('TWITTER_API_KEY')
        self.api_secret = os.getenv('TWITTER_API_SECRET')
        self.access_token = os.getenv('TWITTER_ACCESS_TOKEN')
        self.access_token_secret = os.getenv('TWITTER_ACCESS_TOKEN_SECRET')
    
    def client(self):
        auth = tweepy.OAuthHandler(self.api_key, self.api_secret)
        auth.set_access_token(self.access_token, self.access_token_secret)

        # 認証
        return tweepy.API(auth)
    
    # 対象ユーザーのリツイートとリプライを除外した上で最新ツイートを1件取得
    def get_user_recent_tweet(self, screen_name):
        return self.client().user_timeline(screen_name = screen_name, count = 1, include_rts = False, exclude_replies = True)[0]
```

```python:./opt/main.py
import datetime
from time import sleep
import traceback
import sys
import os
from dotenv import load_dotenv
load_dotenv()
from bybit_api import BybitAPI
from twitter_api import TwitterApi

# 対象の通貨名（表記ゆれを考慮して複数指定）
target_coin_names = [
    'ビットコイン',
    'bitcoin',
    'btc'
]

# 買い要因になりそうな単語（表記ゆれを考慮して複数指定）
positive_words = [
    '買い',
    'buy',
    'moon'
]

# 売り要因になりそうな単語（表記ゆれを考慮して複数指定）
negative_words = [
    '売り',
    'sell'
]

# Twitter API
twitter_api = TwitterApi()

# ツイートを監視する相手のユーザーID
screen_name = os.getenv('TWITTER_SCREEN_NAME')

# Bybit API
bybit_api = BybitAPI()

# 取引したい通貨ペア
symbol = 'BTC/USD'

# 注文量（USD）
amount = 1

# エントリー中かどうかのフラグ
entry_flag = False

# 決済予定時刻
close_time = None

print('Start!')

while True:
    try:
        # 現在時刻を算出
        now = datetime.datetime.now()
        
        if entry_flag == False:
            # ツイートを取得
            tweet = twitter_api.get_user_recent_tweet(screen_name)

            # ツイートが直近（1分以内）のものかどうかを確認（時間差のある過去ツイートに引っ張られないようにするため）
            if (now - (tweet.created_at + datetime.timedelta(hours = 9))) < datetime.timedelta(minutes = 1):

                # ツイート内に対象の通貨名（条件1）＋買い要因or売り要因の単語（条件2）が含まれるかを確認（ex. ビットコイン 買い）
                # 条件1と条件2は必ずセットである必要があり、どちらか1つだけでは成立しない

                # 買い要因と売り要因、両方の単語が含まれていた場合は判断に困るのでパス
                if any([target_coin_name in tweet.text.lower() for target_coin_name in target_coin_names]) \
                    and any([positive_word in tweet.text.lower() for positive_word in positive_words]) \
                    and any([negative_word in tweet.text.lower() for negative_word in negative_words]):
                    
                    pass 
                
                # 買い要因の単語が含まれていた場合は買い注文を出す
                elif any([target_coin_name in tweet.text.lower() for target_coin_name in target_coin_names]) \
                    and any([positive_word in tweet.text.lower() for positive_word in positive_words]):
                    
                    # 成り行き買い注文
                    order = bybit_api.create_order(symbol, 'market', 'buy', amount)
                    print(f'Buy: {now}')
                    
                    # エントリーフラグをTrueに変更
                    entry_flag = True

                    # 決済予定時刻を設定（現在時刻から〇分後）※ minutesに指定する値は任意でOK
                    close_time = now + datetime.timedelta(minutes = 5)
                
                # 売り要因の単語が含まれていた場合は売り注文を出す
                elif any([target_coin_name in tweet.text.lower() for target_coin_name in target_coin_names]) \
                    and any([negative_word in tweet.text.lower() for negative_word in negative_words]):
                    
                    # 成り行き売り注文
                    order = bybit_api.create_order(symbol, 'market', 'sell', amount)
                    print(f'Sell: {now}')

                    # エントリーフラグをTrueに変更
                    entry_flag = True

                    # 決済予定時刻を設定（現在時刻から〇分後）※ minutesに指定する値は任意でOK
                    close_time = now + datetime.timedelta(minutes = 5)
                
                else:
                    # それらしい単語が含まれていなかった場合はパス
                    pass
           
            else:
                # 直近のものでなかった場合はパス
                pass

        else:
            # 現在時刻 > 決済予定時刻なら決済を行う
            if close_time != None and now > close_time:
                # 自分のポジション状況を確認
                position = bybit_api.get_position(symbol)

                # 買いポジションを持っていた場合は売り注文を出す
                if position['side'] == 'Buy':
                    order = bybit_api.create_order(symbol, 'market', 'sell', amount)
                    print(f'Close: {now}')

                    # 決済が済んだらシステム終了
                    sys.exit()
                
                # 売りポジションを持っていた場合は買い注文を出す
                elif position['side'] == 'Sell':
                    order = bybit_api.create_order(symbol, 'market', 'buy', amount)
                    print(f'Close: {now}')

                    # 決済が済んだらシステム終了
                    sys.exit()

                else:
                    # 何かの間違いで万が一ノーポジだった場合はパス
                    pass

            else:
                # 現在時刻 < 決済予定時刻ならパス
                pass
        
        # 10秒空ける（Twitter APIの制限値は15分で180回までなので少し余裕を持ちたい）
        sleep(10)

    except:
        # 異常が起きた場合はシステム終了
        print(traceback.format_exc())
        sys.exit()
```

Botが反応する条件となる

- target_coin_names
- positive_words
- negative_words

に関しては、その時々でホットなワードが異なると思うので各々自由に決めてください。（今回はあくまでもサンプルなのでかなり単純なものに設定しています。）

決済予定時刻の設定などもお好みで。

## 動作確認

```
$ python main.py
```

あとはBotを起動して監視対象のユーザーが狙いの仮想通貨に関するツイートをするのを待つだけです。

![スクリーンショット 2021-07-04 2.04.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/0e4f39b8-3503-d041-c281-0046819d7b23.png)

↑手っ取り早く確認したい場合は僕と同じように適当なテスト用Twitterアカウントを使って試してみてください。

![マイ-ムービー(8).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/851b2605-c2eb-b1c6-bc70-68f8310ba3db.gif)

## あとがき

以上、特定のユーザーのツイートに反応する仮想通貨自動売買Botを作ってみました。

実際の相場ではHFT（高頻度取引）と呼ばれるミリ秒単位の取引がひしめき合っているらしいので、こんな遊びじみたBotで上手くいくかどうかは微妙ですが、あくまでアイデアの1つとして考えていただければと思います。

https://github.com/kazama1209/tweet-reaction-trading-bot

今回作成したコードを載せておくので、手順通りに進めて万が一動かない箇所などがあった場合は間違っている部分が無いか確認してください。