断クリプト日記(n日ぶりm回目)

暗号通貨ガチホしつつbotトレード極めます

コイン〇ェックのチャットを感情分析によるテキストマイニングしてみた

どうもみなさん、ガチホしていますか?

私はBTC-FXのSFDが発動しているのでbotトレは少しお休みして、
短期分のアルトをころころ転がしています。

5月はどのぐらいお祭り相場になるのかはわかりませんが、
私は基本的に直近でリリースがある通貨が利確のタイミングを計りやすいので
仕手戦を転々としていこうかと思います。(もちろん余剰資金だけですが。。)

さて、つい数時間前に仮想通貨の春ならぬTwitterの春が私のもとに訪れまして、
ゴールデンウィークの自由研究としてやっていたコインチェックのチャット分析を
かの有名な仮想NISHIさんに取り上げていただきました。(ありがとうございます。)
気づけばフォロワーさんが数時間で2倍になりました。仮想通貨だったら利確したい

私自身はしがない技術好きなオタクの一人でしかなく、データサイエンスのプロではないのですが
予想以上に反響が大きく、また今後発展してほしい分野だと思っていますので
今後の発展に期待を込めて、現時点での成果物を共有させていただけたらと思います。

今回の概要

今回作るパッケージはこのような構成になります。
f:id:cryptocurrency_chudoku:20180430015739p:plain
簡単に説明しますと、以下の2フォルダ、2ファイルでできます。
 [フォルダ] Log_CoinCheck :ログの格納フォルダ
 [フォルダ] phantomjs-2.1.1-windowsGUIなしでwebにアクセスできるツール(※要ダウンロード)
 [module] EmotionAnalysisBot.py:テキストマイニング用ファイル
 [module] GetChatLog.py:コインチェックのチャットログ収集用ファイル
 [module] GetChatLog_Zaif.py:Zaifのチャットログ収集用ファイルですが、Zaifのチャットはプロトコルがwebsocketになっていて上手く接続できないので教えてください

コインチェックのチャットからログを収集しよう

コインチェック自体はチャットのAPIとかないのですが、
下記のサイト様を参考にチャットログ収集用のクラスを作ってみました。
[参考サイト]
【コインチェック】APIが存在しないはずのチャットの自動読み上げシステムを作る

GetChatLog.py

import requests
import time
import datetime
from selenium import webdriver


class EXEC_GET_CHAT_LOG:

    def get_coincheck(self):

        # pjs_pathは今回フルパスで指定していますが、phantomjsのフォルダの中で「bin/phantomjs」が指定できればOKです。
        pjs_path = 'C:/pleiades/workspace/DataScienceBot/EmotionAnalysisBot/phantomjs-2.1.1-windows/bin/phantomjs'
        access_url = 'https://coincheck.com/ja/chats'
        l_coin_session2 = ""
        l_ga = ""
        l_gid = ""
        driver=webdriver.PhantomJS(executable_path = pjs_path)


        # CoinCheckにアクセス
        driver.get(access_url)

        # クッキー情報の取得
        cookies = driver.get_cookies()

        # クッキーのチェック
        print("check cookies:")

        for c in cookies:
            if "name" in c and c["name"] == "_coin_session2":
                l_coin_session2 = c["value"]
            elif "name" in c and c["name"] == "_ga":
                l_ga = c["value"]
            elif "name" in c and c["name"] == "_gid:":
                l_gid = c["value"]




        while True:

            # APIのエンドポイント
            url = 'https://coincheck.com/api/chats/list'

            header = {'accept': 'application/json, text/plain, */*'
                    ,'accept-encoding': 'gzip, deflate, br'
                    ,'accept-language': 'ja,en-US;q=0.9,en;q=0.8'
                    ,'cookie': '_coin_session2=' + l_coin_session2 + '; _ga=' + l_ga + ';  _gid=' + l_gid
                    ,'referer': 'https://coincheck.com/ja/chats'
                    ,'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36'
                    ,'x-csrf-token': ' ' # Chromeのコンソールから直接抜いてくるか、webdriverの処理で取得して代入してください。
                    ,'x-requested-with': 'XMLHttpRequest'
                      }


            # json形式で結果を受け取る
            response = requests.get(url, headers=header).json()

            # 現在の日付
            YYYYMMDD=datetime.date.today()

            # ファイル名の命名
            file_nm='Log_CoinCheck/' + str(YYYYMMDD) + '.txt'

            # ファイルOPEN
            f = open(file_nm, 'a',encoding='utf-8')

            print('**************************************************************************************************')
            # ログの出力
            for res in response['chats']:
                print("Time:{} message:    {}".format(res['created_at'] ,res['content'].replace('\n','')))
                f.write("Time:{} message:    {}".format(res['created_at'] ,res['content'].replace('\n','')))
                f.write("\n")

            f.close()
            # Sleep
            time.sleep(180)

        # ドライバーのクローズ
        driver.quit()





if __name__ == '__main__':
    # ロード
    exec_get_log = EXEC_GET_CHAT_LOG()

    # 実働
    exec_get_log.get_coincheck()


簡単に処理の内容だけ説明させていただきますと、
コインチェックにはチャット専用のURLがありまして、
そのページを開くと最新のチャットログを50件取得するようになっています。

そのページを開いたときにAPIが呼ばれているのですが、
この非公式APIを3分間隔で叩いてログを収集しています。
※2018/4/30現在では3分で50件ほどログが生成。ただし、多少抜けていても誤差の範疇だと思います。

以下はその実例です。GoogleChromeの開発者コンソール画面(F12)から内容を確認できます。

f:id:cryptocurrency_chudoku:20180430031638p:plain

また、PGでは上記画像のクッキーも自動で取得していますが、
「'x-csrf-token'」というパラメタが私の作成したファイルではハードコーディングになっていたので
PGで自動取得させるか、Chromeの画面から手動で抜いてきて、コーディングをお願いします。。

実際のログは下記のようなフォーマットで作成されます。(メッセージの前だけTAB区切り)
f:id:cryptocurrency_chudoku:20180430042632p:plain

テキストマイニングにかけてみよう


実際にデータをこねくりまわす部分です。
今回はGoogle Cloud Platformの感情分析APIというものを使ってみました。
一定回数以上APIを使うと課金制になりますが、無料である程度楽しめるようです。

こちらのサイト様を参考にさせていただきましたので、説明はこちらから引用とさせていただきます。
to-kei.net
※後日追記(下記のリンクもわかりやすいので、ご参考まで)

Natural Language API でエンティティと感情を分析する

EmotionAnalysisBot.py

import requests
import unicodedata


class EMOTIONA_ANALYSIS_BOT:

    def get_east_asian_width_count(self,text):

        count = 0
        # ifブロックは本来2byte以上の文字を計算するのに使うが、Googleがマルチバイト文字を1文字で換算するため同一処理で可
        for c in text:
            if unicodedata.east_asian_width(c) in 'FWA':
                count += 1
            else:
                count += 1
        return count


    def exec_read_text(self,file_nm):

        f = open(file_nm, 'r', encoding='utf-8')

        # 1行毎にファイル終端まで全て読む(改行文字も含まれる)
        lines = f.readlines()

        f.close()

        msg_list=[]
        for line in lines:
            # TAB区切りで文字列を分割し、メッセージ部分を改行コード抜きで配列に追加
            msg=line.split('    ')[1].replace('\n','')
            # 重複するログは除外
            if msg not in msg_list:
                msg_list.append(msg)


        # API連携用テキストレコードリスト
        api_text=[]
        str=""

        for now_msg in msg_list:

            # 1000文字分のテキストを作成
            if self.get_east_asian_width_count(str) + self.get_east_asian_width_count(now_msg) < 998:
                str += now_msg + "。"

            # 1000文字を超えた場合
            else:
                api_text.append(str)
                str = now_msg + "。"

        # 最終行をレコードに追加
        api_text.append(str)

        return api_text


    def exec_analysis_text(self,api_text):

        #APIキーを入力(Google Cloud Platformの画面から有効化したうえで入力)
        key = ""

        total_score=0
        total_magnitude=0
        all_text=""

        for analysis_text in api_text:

            # 感情分析したいテキスト
            text = analysis_text
            # テキストの総和
            all_text+=text

            #APIエンドポイント
            url = 'https://language.googleapis.com/v1/documents:analyzeSentiment?key=' + key

            #基本情報の設定 JAをENにすれば英語のテキストを解析可能
            header = {'Content-Type': 'application/json'}
            body = {
                "document": {
                    "type": "PLAIN_TEXT",
                    "language": "JA",
                    "content": text
                },
                "encodingType": "UTF8"
            }

            #json形式で結果を受け取る。
            response = requests.post(url, headers=header, json=body).json()

            total_magnitude+=float(response["documentSentiment"]["magnitude"])
            total_score+=float(response["documentSentiment"]["score"])

            #分析の結果をコンソール画面で見やすく表示
            print("**********************************総合magnitude**********************************")
            print(response["documentSentiment"]["magnitude"])
            print("**********************************総合score**********************************")
            print(response["documentSentiment"]["score"])
            for i in response["sentences"]:
                print("magnitude:",i["sentiment"]["magnitude"],"  score:",i["sentiment"]["score"],"  text:",i["text"]["content"])

        print("***************************************本日のtotalマグニチュード***************************************")
        print(total_magnitude)
        print("***************************************本日のtotalスコア***************************************")
        print(total_score)
        print("***************************************本日のtotal文字数***************************************")
        print(self.get_east_asian_width_count(all_text))

        return

if __name__ == '__main__':

    # ロード
    emotionmal_analysis = EMOTIONA_ANALYSIS_BOT()

    # 実行
    api_text=emotionmal_analysis.exec_read_text('Log_CoinCheck/2018-04-28.txt')

    # 解析
    emotionmal_analysis.exec_analysis_text(api_text)


APIの解析単位が1000文字だと思いますので、PG上では1000文字単位以内で
チャットのログを分割してAPIを実行するようにしています。
あとはトータルスコアなども出力するようにしていますが、バグっていたらすみませんorz

実行して普通に動かせば下記のような結果が出ると思います。
f:id:cryptocurrency_chudoku:20180430044757j:plain

まとめ

最後にまとめです。
現在は機械学習ディープラーニングなどのライブラリが非常に充実しているため、
プログラムさえ書ければ個人でもこのようにデータサイエンティスト気分を簡単に味わえます。

ただ実際のデータサイエンスの分野というのはここまでシンプルなものではなくて、
3段階に分けられるのではないかと思います。(1が難しく、3が易しい)

  1. モデルを構築できる
  2. パフォーマンスチューニングできる
  3. モデルを実装できる(使える)


今回実践したのは3~3.5というレベルなのですが、
この場合、使うモデル自体が間違っている・もしくはモデルが最適な結果を返してくれない場合、
意味のない結果しか返ってこずあまり役に立ちません。

ただ、1.ができる人というのはその道のプロであるためハードルはかなり高いですが、
そうでなくとも、2.ができる、もしくは3.ができる人でも複数のモデルを使いこなせることができれば
何か違う結果が見えてくるのかもしれません。

※ブログもPGも急ピッチで書いてしまったので、品質についてはご容赦下さいm(_ _)m

最後まで読んでいただきありがとうございました。

日次で価格データをDBに自動保存してもらおう

どうもみなさん、ガチホしていますか?

4/12にBTCの価格がわずか1時間程度の間に10万円以上値上がりしたのは記憶に新しいと思います。
そんな中、私は2017年の12/8の朝8時ぐらいにCoinCheckのチャートを見ながら、
ものの数分で40万近く値上がりしていく姿を見て、
"いよいよBTCも終わりか"と悟っていたことを思い出しました。

★      ★      ★

さて、私はこのごろずっと"Cryptowatchのデータ少なすぎ、バックテストの意味ないじゃん!"
と思っていまして、自動でDBにデータを保存してくれるスクリプトを作成してみました。

エンジニアでないとなかなかDBを触ることもないと思いますので、
DBなんてわからないよ!という方にも取り組んでいただけるように説明させていただきます。

手順

そもそもDBとは?

簡単に言えば、データを保存しておくストレージというところでしょうか。
そんなこと知ってるよ(゚Д゚)ゴルァ!という方はいったん落ち着いてくださいw

CryptowatchのAPIで価格データを取得するときも実はDBからデータを取得しているのですが、
一定期間しか価格データを取得することができません。
それに対して、自分のローカルもしくはAWS上にDBを構築してしまえば、
好きなように保存したデータを使い倒すことができてしまうのです!
下のような図をイメージしていただけるとわかりやすいのではないのでしょうか。

f:id:cryptocurrency_chudoku:20180414165357p:plain
すでに私がこのブログに投稿しているbotもそうなんですが、
①のAPIでデータを取得しただけでは、データは保存されません。
メモリに一時的に保存されているだけで、プログラムが停止すればデータは消えます。
一方、➁の処理まで実行すればプログラムを停止してもPCの電源を切ってもデータは残っています。

➁のデータを登録する部分が今回のメインテーマとなります。
DBにも、OracleSQLServerMySQLHadoop...etcとさまざまなものがあるのですが、
個人でbot制作をするレベルであれば特に気にする必要はないと考えているため、
自作のアプリケーションと相性の良いものを使っていただければと思います。

今回はPostgresSQLを使って説明したいと思います。


DBの環境構築をしよう

DB自体の環境構築については、すでにまとめてくださっている記事も多いので
引用させていただく形で説明したいと思います。

qiita.com

今回紹介するプログラムの都合上、PostgreSQL9.5以上のバージョンを対象としています。
上記リンクの方法に従って、PostgreSQL9.5以上のバージョンで環境構築をお願いいたします。

環境構築が終わりましたら、デフォルトで「pgAdmin3」という
GUIのクライアントツールが入っていると思いますのでそちらを使って操作していきましょう。
「C:\Program Files\PostgreSQL\9.5\bin」の配下の"pgAdmin3.exe"というファイルを実行します。



DBを使ってみよう

1.DBの構造

さて、実際にDBをつかってみましょうか。
DBにはテーブルという表領域を確保しなければなりません。
このテーブルの中にさまざまなデータを登録していきます。

f:id:cryptocurrency_chudoku:20180414204741p:plain
テーブルはそれぞれ属性の異なるデータを保持していくのが定石です。
例えば、上図のように5分足のデータと1時間足のデータは分けてテーブルに持ちます。

テーブルでは日付をPK(特定の行を一意に特定できる制約)を設定しなければなりません。
もし仮に、5分足と1時間足の価格データを同じテーブルに持ってしまうと、
"2018-04-13 15:00:00"の行が2つあるのに、1時間足なのか5分足なのか識別できなくなるからです。

テーブルを作るというのは、
図にあるような"日付","始値","高値","安値","終値","取引高"という項目(カラム)を設定したり、
"日付"がPKですよー。という定義を作ってあげることを言います。

2.空のテーブルを作成する

説明がかなりしんどいので、スライドで解説させてくださいw f:id:cryptocurrency_chudoku:20180414213554p:plain
f:id:cryptocurrency_chudoku:20180414213613p:plain
DDLと呼ばれるSQLを実行するとデータが空っぽのテーブルが作成されます。
このSQLを実行すると暗黙的にテーブルが保存されます。

※ちなみにSQLとはDBにあるデータを取り出したり更新・登録・削除などする言語のことです。
SQLは上記の④で表示しているウインドウに張り付けて実行していきます。

DDL:"T_BTC_FX_5MIN_BITFLYER"(5分足データ専用のテーブルになります)

CREATE TABLE public."T_BTC_FX_5MIN_BITFLYER"
(
  "DATA_DATE" timestamp without time zone NOT NULL,
  "OPEN" integer, -- 始値
  "HIGH" integer, -- 高値
  "LOW" integer, -- 安値
  "CLOSE" integer, -- 終値
  "VOLUME" double precision, -- 出来高
  CONSTRAINT "PK_T_BTC_FX_5MIN_BITFLYER" PRIMARY KEY ("DATA_DATE")
)
WITH (
  OIDS=FALSE
);
ALTER TABLE public."T_BTC_FX_5MIN_BITFLYER"
  OWNER TO postgres;
COMMENT ON COLUMN public."T_BTC_FX_5MIN_BITFLYER"."OPEN" IS '始値';
COMMENT ON COLUMN public."T_BTC_FX_5MIN_BITFLYER"."HIGH" IS '高値';
COMMENT ON COLUMN public."T_BTC_FX_5MIN_BITFLYER"."LOW" IS '安値';
COMMENT ON COLUMN public."T_BTC_FX_5MIN_BITFLYER"."CLOSE" IS '終値';
COMMENT ON COLUMN public."T_BTC_FX_5MIN_BITFLYER"."VOLUME" IS '出来高';


3.データを入れてみる、取得してみる、削除してみる

A.登録(INSERT文)

INSERT INTO "T_BTC_FX_5MIN_BITFLYER" 
       ("DATA_DATE", "OPEN", "HIGH", "LOW", "CLOSE", "VOLUME") 
VALUES ('2018-04-18 21:55:00', 70000, 800, 30, 20, 10)

B.参照(SELECT文)

select *
from "T_BTC_FX_5MIN_BITFLYER"
where "DATA_DATE" = '2018-04-18 21:55:00'

C.削除(DELETE文)

delete
from "T_BTC_FX_5MIN_BITFLYER"
where "DATA_DATE" = '2018-04-18 21:55:00'

サンプルのSQLを掲載しておきます。
A.を実行して、B.を実行すると結果が返ってきます。
C.を実行するとA.で登録したデータは消えます。

SQLの文法についてはいろいろ調べてみてください!(適当)

また、A.やC.のようなテーブルのデータを登録・削除する処理のあとには
"commit"というSQLを単独で実行します。
人間の目にはデータが入っているように見えますが、
実際にはメモリに一時的に保存されているだけで保存されていません。
"commit"をしなければDBを停止した時にデータが消えます。

また、"rollback"というSQLもあり、これは前回"commit"した時点までデータの状態を戻します。
※なお、DBの設定状態によってはオートコミットというものがあり、
"commit"を実行しなくてもデータが保存されてしまいますので事前に確認して下さい!
私のPostgreSQLのDBはオートコミットになっていました。


pythonでDBにデータをインポートしよう

さあ、お待たせしました。
いよいよpythonのプログラムを使ってデータをDBに登録する作業です。
以下にコードを掲載します。

OneFileGetBitFlyerTicker.py

import psycopg2
import datetime as dt
import requests
import json
import time
from datetime import timedelta as td



# 価格取得
def get_ticker(candle_span,data_span):

    # 時間足の指定
    candle_span=[candle_span]

    # 現在時刻から指定した時間間隔の時刻を取得
    endDate = dt.datetime.now()
    startDate = endDate + td(hours=data_span)

    # 時刻データのフォーマット変換
    startTimestamp = startDate.timestamp()
    endTimestamp = endDate.timestamp()

    # 価格データのAPIリクエスト@cryptowatch
    query = {"periods": candle_span, "after": str(int(startTimestamp)), "before": str(int(endTimestamp))}

    # 成功するまでリトライ
    while True:
        try:
            res = json.loads(requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc", params=query).text)["result"]
        except:
            # 失敗した場合sleep
            print('価格の取得に失敗したためリトライ処理を実行します。')
            time.sleep(15)
        else:
            # 成功した場合ループを抜ける
            break
    else:
        print('価格の取得処理においてリトライに失敗したので異常終了します。')

    # ローソク足のデータを入れる配列
    data = []
    for i in candle_span:
        row = res[i]
        for column in row[::-1]:
            # dataへローソク足データを追加
            if column[4] != 0:
                column = column[0:6]
                data.append(column)

    return data[::-1]



def connection_db(candleStick):

    # DBに接続する
    cnn = psycopg2.connect('dbname=XXXX host=localhost port=XXXX user=XXXX password=XXXX')
    cur = cnn.cursor()

    try:

        for row in candleStick:
            data_date = dt.datetime.fromtimestamp(row[0])
            open = row[1]
            high = row[2]
            low = row[3]
            close = row[4]
            volume = row[5]
            sql=(data_date, open, high, low, close, volume, open, high, low, close, volume)
            cur.execute(u"""INSERT INTO
                             "T_BTC_FX_5MIN_BITFLYER"
                             ("DATA_DATE", "OPEN", "HIGH", "LOW", "CLOSE", "VOLUME")
                             VALUES (%s, %s, %s, %s, %s, %s)
                             ON CONFLICT ON CONSTRAINT "PK_T_BTC_FX_5MIN_BITFLYER"
                             DO UPDATE SET
                                 "OPEN"   = %s
                               , "HIGH"   = %s
                               , "LOW"    = %s
                               , "CLOSE"  = %s
                               , "VOLUME" = %s """ , sql)

    except (psycopg2.OperationalError) as e:
        print (e)
    else:
        cnn.commit()
    finally:
        cur.close()
        cnn.close()


if __name__ == '__main__':

    # 価格の取得
    candleStick=get_ticker("300",-100)

    # DBへの保存
    connection_db(candleStick)



簡単に処理の解説だけします。
以下はメイン処理です。

if __name__ == '__main__':

    # 価格の取得
    candleStick=get_ticker("300",-100)

    # DBへの保存
    connection_db(candleStick)

get_tickerというファンクションでCryptoWatchのAPIを叩いて価格を取得していますが、
引数で渡している”300”というのは5分足のデータ、-100というのは100時間分のデータを
現在時刻からさかのぼって、指定して取得しています。

価格取得処理はいろいろなbotソースコードにも載っているので説明は割愛します。

続いてDBの登録処理です。

def connection_db(candleStick):

    # DBに接続する
    cnn = psycopg2.connect('dbname=XXXX host=localhost port=XXXX user=XXXX password=XXXX')
    cur = cnn.cursor()

    try:

        for row in candleStick:
            data_date = dt.datetime.fromtimestamp(row[0])
            open = row[1]
            high = row[2]
            low = row[3]
            close = row[4]
            volume = row[5]
            sql=(data_date, open, high, low, close, volume, open, high, low, close, volume)
            cur.execute(u"""INSERT INTO
                             "T_BTC_FX_5MIN_BITFLYER"
                             ("DATA_DATE", "OPEN", "HIGH", "LOW", "CLOSE", "VOLUME")
                             VALUES (%s, %s, %s, %s, %s, %s)
                             ON CONFLICT ON CONSTRAINT "PK_T_BTC_FX_5MIN_BITFLYER"
                             DO UPDATE SET
                                 "OPEN"   = %s
                               , "HIGH"   = %s
                               , "LOW"    = %s
                               , "CLOSE"  = %s
                               , "VOLUME" = %s """ , sql)

    except (psycopg2.OperationalError) as e:
        print (e)
    else:
        cnn.commit()
    finally:
        cur.close()
        cnn.close()

今回はpsycopg2というライブラリを使ってDBに接続しています。
他にもライブラリはあるのですが、postgreSQLに関して言えばインストール数が多く
ポピュラーだそうなので使用しています。

cnnという変数に接続情報を文字列で渡していますのでXXXXを書き換えてください。
localhostというのは利用者が現在操作している手元のコンピュータや端末を表します。
リモート環境にDBがある場合はIPを指定してください。

DB接続処理でのポイントは3つあります。

1. try-except-else-finallyで処理しよう

SQL以外のプログラムを介してDBとやり取りするときの定石なんですが、
接続が切れたり、処理に失敗したときの例外処理を丁寧に処理しなければなりません。
処理に成功したらelse:のブロックに入って登録したレコードをコミット(保存)していきます。

また、処理の成功・失敗に関わらずclose()を使って、接続を切断してあげなければなりません。
接続を切断しないと次にPGを実行しても、前のコネクションが次々とたまり続けて
DBが高負荷な状態になり、停止します。

2. バインドパラメータで変数を渡そう

INSERT文の中で”%s”と記載してありますがなんでしょう?
ここに実際に登録するデータが入るのですが「sql」という変数で渡している
パラメータの順にセットしています。

"pythonの文字列SQLを生成しているのなら+を使って変数と文字列結合すればいいじゃん!"
という意見があると思いますが、カッコいいだけでこのような書き方にしているわけではありません。
今回はあまり関係ないですが、SQLインジェクションというハッキング手法がありますので
これを機に覚えてみてください。
qiita.com

3. UPSERT文で処理する
INSERT INTO
             "T_BTC_FX_5MIN_BITFLYER"
             ("DATA_DATE", "OPEN", "HIGH", "LOW", "CLOSE", "VOLUME")
             VALUES (%s, %s, %s, %s, %s, %s)
             ON CONFLICT ON CONSTRAINT "PK_T_BTC_FX_5MIN_BITFLYER"
             DO UPDATE SET
                "OPEN"   = %s
              , "HIGH"   = %s
              , "LOW"    = %s
              , "CLOSE"  = %s
              , "VOLUME" = %s

実行SQLの部分を見てみましょう。
この構文はPostgreSQL9.5から採用されているUPSERT文というものです。
INSERT文を基本的に実行しますが、PK違反(一意制約違反)が発生した場合は
すでにDBに登録されているPKに合致する行の各項目に対してUPDATEをするという処理です。
OracleだとMerge文になります。

CryptowatchのAPIから取得するデータを調整して、同じデータを登録しないように制御するのは
かなり根気がいりますので、SQLで制御したほうが遥かに楽です!

インポートを自動化しよう

さぁ、いよいよ最後になります。
登録できるPGはあるものの、結局人間が1日1回とか手動で実行するの?(´・ω・`)
そんなことはありません。プログラムに自動でやってもらいましょう!
Windowsのタスクスケジューラというアプリを使います。Cortanaさんに聞いてみましょう。

詳しい使い方は以下のリンクにありますので、こちらからどうぞ。
www.atmarkit.co.jp
このアプリで設定したファイルを時間になったら定期実行してくれます。

Windowsのタスクスケジューラで実行するファイルは以下のように作りました。
テキストファイルを開いて、以下の内容を張り付けて、
拡張子「.bat」で保存すれば完成です。
"db_import_job.bat"のファイルは"OneFileGetBitFlyerTicker.py"と同じディレクトリに配置してください。
今回はCドライブの直下に配置しましたので、下記のようなコードになっています。

db_import_job.bat

python C:\OneFileGetBitFlyerTicker.py


では最後にタスクスケジューラが正常終了したらpgAdmin3から結果を見てみましょう。
f:id:cryptocurrency_chudoku:20180414195733p:plain
やったね^^

さて、今回はDBの基本からつらつらと書いてみましたが、いかがでしたでしょうか。
教養科目としてプログラムを習っていて、ソースを書ける学生さんはいても、
DBの取り扱いまで含めると数が少ないと思います。
なので、

”これからbot作成を通してITスキルを身に着けたい”

そんな風に思っている学生さんなどの皆様に読んでいただけたら幸いです。

TA-Libをwin64bit環境にインストールする方法をまとめてみた

どうもみなさん、ガチホしていますか?

私はトレードbotの実装・テスト・運用が順調すぎて、 気づいたら1分足のチャートしかみておらず
ガチホしている暗号通貨はずるずると購入額からFiat建てで下がっていました。

でも、気にしません! 特に価格が下がることに対して興味がないからです。

さて、先日寄稿させていただきました"ビットコインのトレードbotを書いてみた"の中で
windows10の64bit環境にTA-Libのライブラリをインストールするのに2日間かかったということを
強調していましたので、本日はその方法をまとめておきます。

GitHubに書いてあるんですが、英語だと抵抗ある人もいると思うので私なりにまとめます。
github.com
f:id:cryptocurrency_chudoku:20180401140231p:plain

インストール手順

1 ラッパーを手に入れよう

Qiitaを見ていると"TA-Lib"というテクニカルインディケーターの計算をやってくれる便利なライブラリがあるということで、コマンドプロンプトにて、"pip install TA-Lib"を実行してみるが入らない。。。
Google先生に聞いてみると下記のサイトからラッパーの準備が必要ということで、
まずはDownLoadしましょう。

ta-lib.org
"ta-lib-0.4.0-msvc.zip"を自分のローカルの"C:\"の直下にzip形式を解凍したしたうえで、
配置してください。

ラッパーってなんだYo!っていう人は個別に調べてください。

2 VisualStudioもしくはVisualStudioのビルドツールをインストールしよう

気を取り直してコマンドプロンプトにて、"pip install TA-Lib"を実行してみるが入らない。。。
コマンドプロンプトの内容を見るとC++のビルドツールがないので、怒られた。むりぽ(´・ω・`)

WindowsにはデフォルトでC++のビルドツールが入っていないので、
DownLoadしたラッパーを導入できない
コマンドプロンプトにも出ますが、下記URLにアクセスし対象のファイルをインストール。
landinghub.visualstudio.com
私はeclipseというIDEをつかっていますので"Build Tools for Visual Studio 2017 "(※2018/3時点)という
ツールのみインストールしましたが、VisualStudioをIDEとして使う場合はそのままインストールすれば
ビルドできるのだと思います。


3 VS2017用x64NativeToolsのコマンドプロンプトでビルドしよう

気を取り直してコマンドプロンプトにて、"pip install TA-Lib"を実行してみるが入らない。。。(3回目)
このあたりの時系列はあいまいなので、正確性に欠けますが、以下の方法で対応していたと思います。

Gitにも書いてありますが、32bit専用のラッパーということで/(^o^)\オワタ状態でしたが、
64bit専用コマンドプロンプトで実行すればよいとのこと

下図が対象ツールです。(赤枠)
f:id:cryptocurrency_chudoku:20180401133152p:plain

コマンドプロンプトが起動したらcdコマンドで"C:\ta-lib\c\make\cdr\win32\msvc"の
ディレクトリに移動します。
移動したら"nmake"とコマンドを打って終わりです。
このあとに再度、通常のコマンドプロンプトで"pip install TA-Lib"を実行したか覚えていないのですが、
再実行しても"already install"と出るような気がするので問題ないと思います。やったね!

4 その他に困ったら

1 ビルドしてもpipのバージョンが古くて変なエラーが出る問題

どうやったら再現できるのか忘れてしまいましたが、
ログをはきだすpyモジュールの型が合わなくて、ビルドが最後まで通らなかった気がしたので
Gitのpull requestどおりにコメントアウトするか、最新のpipにアップグレードしたほうがよさそうです。
※確かpython3.7系以降は大丈夫だったかな?

2 VS2017用x64NativeTools以外でビルドしてしまった・・・

上記の画像でも示しているようにコマンドプロンプトの種類が複数あってわかりにくいのですが
違うコマンドプロンプトで実行すると動きません。
"pip uninstall TA-Lib"を実行して、"C:\ta-lib"のファイルを再度新しいもので置きなおしてからやります。

3 それでもできない(涙目)

なんと裏技があります。笑
すでにコンパイル済みのソースを配置して使えるそうです。
下記ブログに書いてあったので、どうしても無理な方はこちらへどうぞ。

yasumonoe.hateblo.jp

4 eclipseでエラーでるんだけど?

いざ、TA-Libのライブラリをインストールしてpythonモジュールでfunctionを呼び出すと
importモジュールがない、みたいなエラーが出ます。
ただし、ラッパーを使っているのでおそらくpathが通っていないとかそのあたりの問題だと思います。
エラーが出たままでも動くので問題ありません。



さて、第2回目の記事はいかがでしたでしょうか。
1回目の記事を書いた時点で燃え尽きていたのですが、同じような悩みをもった人を見かけたので
やる気を振り絞って記事を書いてみました。
それではまた!

ビットコインのトレードbotを書いてみた

どうもみなさん、ガチホしていますか?

最近クリプト界隈で話題となっているビットコインのトレードbotを書いてみましたので、
備忘録として書き残しておきます。

botの作り方などはQiitaや有料noteなんかにもいくつか投稿がありますが、
テーマ別となっており局所的にソースコードが載っていたりと、全体像が分かりにくく、
プログラムを少しかじったことがある人でもモチベーションを維持しながら
最後まで作りきることが難しいのでは感じていました。

そこで、私の尊敬するbotトレーダーAKAGAMIさんとは真逆の路線で、
読者の皆様に"釣り竿"ではなく、"魚"そのものを与え、
まずはbotトレードに興味を持っていただこうというスタンスで記事の公開を始めました。

AKAGAMIさんがいなければ自分自身ここまで来ることはできなかったので、敬意を表します。

目次

1.本記事の対象読者

  • プログラミングの基礎知識がある(言語問わず、簡単な機能を実装できる)
  • pythonの環境構築ができる(anacondaやpipが分かる、eclipseなどのIDEが使える)
  • bitFlyerに口座を開設している

2.概要と技術テーマ

概要

1分ごとにRSIを取得し、設定した基準を割った場合(売られすぎor買われすぎ)にエントリーし、
RSIがニュートラルになったらポジションをクローズする平均回帰型のスキャルbotです。
公開したソースコードbitFlyerのBTC-FXで取引するための実装にしてあります。

技術テーマ
  • ccxt
     →暗号通貨取引所のAPIをall-in-oneで使えるライブラリ
  • TA-Lib
     →インディケーターの算出を自動でできるライブラリ
      pythonで利用するには、ラッパーのインストールが必要。
  • CryptowatchAPI
     →bitFlyerの価格を取得するために利用しています



3.パッケージの構成とソースコード

パッケージの構成

f:id:cryptocurrency_chudoku:20180321155157p:plain



TradingController.py (メインクラス)
import ccxt
import time
from config import Apiconfig    as  api
from config import Tradeconfig  as  tra
from ExecLogic import EXEC_LOGIC
from ExecTicker import EXEC_TICKER


# ロード処理

# 取引ペア
symbol=tra.symbol

# APIキー
bitflyer = ccxt.bitflyer({
'apiKey': api.bf_api_key,
'secret': api.bf_api_secret,
})

# 実行クラス
ticker = EXEC_TICKER()
logic  = EXEC_LOGIC()


while 1==1:
    print('sleep')
    time.sleep(tra.crawling_time)

    # 初期ポジション
    position=None

    try:
        # 終値の取得
        close_price=ticker.get_ticker()
        # ロジッククラスの判定
        position_flg=logic.exec_rsi(close_price)
        print(position_flg)
    except:
        print('価格の取得に失敗しました。APIエラーのため再実行します。')


    # ノーポジションからエントリー
    if position_flg == 1 or position_flg == -1 :

        try:
            # 注文指定
            if   position_flg ==  1: position='buy'
            elif position_flg == -1: position='sell'
            # 発注
            order = bitflyer.create_order(symbol, type='market',side=position, amount=tra.lot)
            # 注文id
            position=order['id']
            print('ポジションを取ります')
            print(order)
        except:
            # APIエラー
            print('ポジションの取得に失敗しました。APIエラーのため再実行します')
            position=None

    while position:
        print('sleep')
        time.sleep(tra.crawling_time)

        try:
            # 終値の取得
            close_price=ticker.get_ticker()
            # ロジッククラスの判定
            exit_position_flg=logic.exec_exit_rsi(close_price,position_flg)
            print(exit_position_flg)
        except:
            # APIエラー
            print('価格の取得に失敗しました。APIエラーのため再実行します')

        if exit_position_flg + position_flg == 0:

            try:
                # 注文指定
                if   exit_position_flg ==  1: position='buy'
                elif exit_position_flg == -1: position='sell'
                # 発注
                order = bitflyer.create_order(symbol, type='market', side=position, amount=tra.lot)
                print('ポジションをクローズします')
                print(order)
                position=None
                position_flg=0
                exit_position_flg=0
            except:
                # APIエラー
                print('ポジションのクローズに失敗しました。APIエラーのため再実行します')
        else :
            exit_position_flg=0



ExecLogic.py (売買ロジックの判定クラス)
import talib as ta



class EXEC_LOGIC:

    def exec_rsi(self,close_price):

        entryflg=0

        #ta-lib
        data=ta.RSI(close_price, timeperiod=14)

        print(data)
        RSI=data[120]
        print(RSI)

        if   RSI <= 35.0:
            print("成行買い")
            entryflg=1

        elif RSI >= 65.0:
            print("成行売り")
            entryflg=-1

        else:
            print("ノーポジション")

        return  entryflg


    def exec_exit_rsi(self,close_price,position_flg):

        exitflg=0

        #ta-lib
        data=ta.RSI(close_price, timeperiod=14)

        print(data)
        RSI=data[120]
        print(RSI)

        if   RSI <= 53.0 and position_flg == -1:
            print("手仕舞い_成行買い")
            exitflg=1

        elif RSI >= 47.0 and position_flg == 1:
            print("手仕舞い_成行売り")
            exitflg=-1

        else:
            print("HOLD")

        return  exitflg



ExecTicker.py (価格データ取得クラス)
import requests
import json
import datetime as dt
from datetime import timedelta as td
import numpy as np
from config import Tradeconfig as tra

class EXEC_TICKER:

    def get_ticker(self):


        # 現在時刻から指定した時間間隔の時刻を取得
        endDate = dt.datetime.now()
        startDate = endDate + td(hours=tra.span)

        # 時刻データのフォーマット変換
        startTimestamp = startDate.timestamp()
        endTimestamp = endDate.timestamp()

        # 価格データのAPIリクエスト@cryptowatch
        # 1分足を取得
        query = {"periods": "60", "after": str(int(startTimestamp)), "before": str(int(endTimestamp))}
        res = json.loads(requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc", params=query).text)["result"]["60"]
        res = np.array(res)

        #ta-libに渡す形式
        close_price = res[:, 4]

        return  close_price



Tradingconfig.py(トレードに関するパラメータファイル)
# 取引ペア
symbol='FX_BTC_JPY'

# データを取得する期間
span=-2 # 2hours

# ロット数(BTC)
lot=0.01

# クローリングTime
crawling_time=60    # 60sec



Apiconfig.py(アクセス情報をハードコーディングしたファイル)
# APIのアクセスキーをここに書く
bf_api_key=

# APIのシークレットキーをここに書く
bf_api_secret=

4.考察

バックテストをせずに即本番運用しましたが、パフォーマンスとしては"トントン"でした。
①→➁や①'→➁'のようなレンジ相場ではRSIと価格がほぼ連動してくれているので問題ないですが、
➂→④のようなトレンドが継続する場合になるとエントリーポイントからずるずると含み損になります。

f:id:cryptocurrency_chudoku:20180323091632p:plain
※ちなみに➂→④の時は、再び➂と④の間でRSIのラインを割ったときに
botが持っているポジションよりも多く、私が直接ナンピンしてエントリーして、
RSIがニュートラルに戻った時に利確します。

追加機能として上記のナンピンエントリーが実装できれば、
勝率がかなり高めのbotになるのではないかと考えています。

5.備考

Win10の64bit環境でTA-Libのラッパーをインストールするまでに
C#コンパイルツールがデフォルトで入っていなかったり、
64bit専用のVisual Studioコマンドプロンプトコンパイルしないといけないなど
地雷除去に2日かかりましたorz
※Gitの説明を見ればなんとか解決できますが、そもそもlinuxmac環境であれば瞬殺です。



本記事を最後まで読んでいただきありがとうございました。
botトレードをこれから始めたい人の何かきっかけになれたら幸いです。