つたはすのブログ

獲得した知識をアウトプットしていきます

バーコードリーダーを使ってみた

書籍管理アプリを作りたくて、その入力デバイスとしてバーコードリーダーを探していたところ、秋葉原で見つけたので買いました。

箱の中には本体の他に設定用のバーコード一覧が入っていました。

ここに書かれたバーコードを読み込むことで設定を適用するようです。
バーコード読み込み時のON/OFFやスキャンモードの設定などができるみたいです。

メモ帳を開いて、ちょうど目の前にあったバーコードを読み込ませてみました。

読み込めてる!!

書籍のISBNコードは読み込めるのか。

読み込めてる!!!

これで書籍管理アプリが作れる!....かもしれない。

【tsfresh】列名から特徴量生成用の辞書を作成する

1. 概要

tsfreshで生成したデータの列名から特徴量生成用の辞書を作成する方法を解説する。

2. 背景

tsfreshはデフォルトの設定だと、800種類近くの特徴量を生成する。
これらに対してFilter MethodやLasso回帰を使って特徴量選択を行い、次回以降は選択した特徴量のみを生成するようにしたかった。

3. tsfreshによる特徴量生成

3-1. データの作成

特徴量作成対象となる時系列データを作成する。今回は正弦波のデータを用意する。

import numpy as np
import pandas as pd

t = np.linspace(0, 1, 100)
sin_data = np.sin(2*np.pi*t)
data_group = ["sin_data"] * len(t)
data_df = pd.DataFrame({"sin": sin_data , "group":data_group })

data_dfが今回の特徴量作成対象となる時系列データである。tsfreshの特徴量生成関数tsfresh.extract_featuresではデータのまとまりを表す列を指定する必要があるため、"group"という列を用意した。このソースコードでは準備した100レコードは1つの時系列データとみなすことになる。

例えば次のようにすれば、2つの時系列データと解釈される。

import numpy as np
import pandas as pd

t = np.linspace(0, 1, 100)
sin_data = np.sin(2*np.pi*t)
# 時系列データを前半と後半に分割
data_group = ["sin_data1"] * len(t)//2 + ["sin_data2"] * len(t)//2
data_df = pd.DataFrame({"sin": sin_data , "group":data_group })

3-2. 特徴量生成

tsfreshを使って特徴量を生成するには次のようなコードを実行すれば良い。

import tsfresh
from tsfresh.feature_extraction import ComprehensiveFCParameters

# 作成対象の特徴量
fcparams_dict = ComprehensiveFCParameters()

# 特徴量作成
ts_features = tsfresh.extract_features(
    timeseries_container=data_df,
    column_id="group",
    default_fc_parameters=fcparams_dict
)

tsfresh.extract_featuresのパラメータdefault_fc_parametersは省略することができるが、省略するとComprehensiveFCParameters()が設定されるので上記のソースコードの場合指定しなくても結果は同じである。このComprehensiveFCParameters()は作成対象の特徴量名とパラメータを指定する辞書である。

これは特徴量名をキーとして、パラメータを値とする辞書で内容は以下のようになっている。

{'variance_larger_than_standard_deviation': None, 
'has_duplicate_max': None, 
'has_duplicate_min': None, 
(省略)
'cid_ce': [{'normalize': True}, {'normalize': False}], 
'symmetry_looking': [{'r': 0.0}, {'r': 0.05}, {'r': 0.1}, {'r': 0.15000000000000002}, {'r': 0.2}, {'r': 0.25}, {'r': 0.30000000000000004}, {'r': 0.35000000000000003}, {'r': 0.4}, {'r': 0.45}, {'r': 0.5}, {'r': 0.55}, {'r': 0.6000000000000001}, {'r': 0.65}, {'r': 0.7000000000000001}, {'r': 0.75}, {'r': 0.8}, {'r': 0.8500000000000001}, {'r': 0.9}, {'r': 0.9500000000000001}], 
(省略)
'mean_n_absolute_max': [{'number_of_maxima': 7}]}

ノンパラメトリックな特徴量はNoneが、パラメトリックな特徴量はパラメータのリストが設定されている。

3-3. 生成された特徴量

ここではtsfreshで生成された特徴量の名前を確認する。

特徴量を生成する関数tsfresh.extract_featuresの返り値はpandas.DataFrameであるから、その列名を確認することでどのような特徴量が作成されたか確認することができる。

# 生成した特徴量の列名を取得
ts_cols = list(ts_features.columns)

ts_colsは次のようになっている。

['sin__variance_larger_than_standard_deviation', 
'sin__has_duplicate_max', 
'sin__has_duplicate_min', 
'sin__has_duplicate', 
(省略)
'sin__mean_n_absolute_max__number_of_maxima_7']

今回はこの列名から特徴量生成用の辞書を作成することが目的である。

4. 特徴量列名から辞書へ

4-1. 特徴量列名の構成

tsfresh.extract_featuresで生成された特徴量DataFrameの列名は次のような構成になっている。

ノンパラメトリック特徴量:データ名__特徴量名
パラメトリック特徴量:データ名__特徴量名__パラメータ名1_パラメータ値1__パラメータ名2_パラメータ値2....

今回作成した特徴量の列名を見ると「sin__variance_larger_than_standard_deviation」があるが、最初の「sin」は特徴量作成対象のdata_dfの「sin」列から特徴量を作成したことを表す。後半の「variance_larger_than_standard_deviation」は特徴量名である。これはノンパラメトリックな特徴量の例である。この場合

"variance_larger_than_standard_deviation": None

という辞書の要素に変換できれば良い。

一方パラメトリックな特徴量の例として「sin__mean_n_absolute_max__number_of_maxima_7」では、「mean_n_absolute_max」が特徴量名でパラメータ「number_of_maxima」に7を指定していることを意味する。この場合では

"mean_n_absolute_max": [{"number_of_maxima": 7}]

という辞書の要素に変換できれば良い。

4-2. 辞書の作成1(不要な部分の削除)

最初に列名の最初の部分(今回の場合の「sin__」の部分)は不要なので、取り除く処理をする。

cols_list = ["__".join(x.split("__")[1:]) for x in ts_cols]

ts_colsは生成した特徴量の列からなるリストである。これの要素を「__」で分割して最初の要素を削除(x.split("__")[1:]の部分)して、最初以外の要素を「__」で結合している。("__".join(...)の部分)

「sin__mean_n_absolute_max__number_of_maxima_7」を例に挙げると、

  • sin
  • mean_n_absolute_max
  • number_of_maxima_7

に分割して、最初の「sin」を捨てて

  • mean_n_absolute_max
  • number_of_maxima_7

にする。これらを「__」で結合して「mean_n_absolute_max__number_of_maxima_7」にしている。

4-3. 辞書の作成2(ノンパラメトリック特徴量)

目的の辞書の要素の内でノンパラメトリックな特徴量の部分を作成する。

# 作成する辞書
my_params_dict = {}

for col in cols_list:
    tmp = col.split("__")
    name = tmp[0]  # 特徴量名

    # ノンパラメトリック
    if len(tmp) == 1:
        my_params_dict[name] = None
    # パラメトリック
    else:
       後述

特徴量の列名からデータ名の部分は削除済みなので、

ノンパラメトリック特徴量:特徴量名
パラメトリック特徴量:特徴量名__パラメータ名1_パラメータ値1__パラメータ名2_パラメータ値2....

という構成になっているので、「__」で分割したリストの最初の要素は特徴量名となっている。(変数nameに格納)

また、リストの長さは次のようになる。

ノンパラメトリック特徴量:1 
パラメトリック特徴量:パラメータの数 + 1 

ノンパラメトリックな特徴量については

特徴量名: None

という辞書の要素に変換できれば良いため

my_params_dict[name] = None

としている。

4-4. 辞書の作成3(パラメトリック特徴量)

ここがこの記事の本題である。考慮するべき要素が多いため内容を分割することにする。

4-4-1. パラメータ名と設定値の分割

パラメトリックな特徴量の列名を「__」で分割すると

特徴量名
パラメータ名1_パラメータ値1
パラメータ名2_パラメータ値2
パラメータ名3_パラメータ値3
...

という要素からなるリストになるので、最初の要素以外を「_」で分割して

{
パラメータ名1: パラメータ値1
パラメータ名2: パラメータ値2
パラメータ名3: パラメータ値3
...
}

という辞書を作成すれば良さそうである。そこで次の処理を考える。

# 作成する辞書
my_params_dict = {}

for col in cols_list:
    tmp = col.split("__")
    name = tmp[0]  # 特徴量名
    
    # ノンパラメトリック
    if len(tmp) == 1:
        my_params_dict[name] = None
    # パラメトリック
    else:
        for param_value in tmp[1:]:
            param, value = param_value.split("_")

最後のparam, value = param_value.split("_")の部分がここでのポイントである。上で説明したとおり、「_」で分割してパラメータ名と設定値に分割して、それぞれparamとvalueに入れようとしているが、これではエラーが発生する。

原因の一つは「f_agg_"mean"」という要素である。これは「f_agg」がパラメータ名で「mean」が設定値である。しかし「_」で分割すると

f
agg
"mean"

の3要素となってしまい、これらを2つの変数param, valueに代入しようとしてエラーとなってしまう。

このようにパラメータの中には名前に「_」が含まれるものが存在しているので、これを考慮する必要がある。
一方で設定値の方には「_」は含まれていない(ComprehensiveFCParameters()の内容を確認)ので、「_」で分割した上で最後の要素を設定値として、それ以前をパラメータ名の一部と判定すれば良い。そこで上の処理を修正して次のようにする。

# 作成する辞書
my_params_dict = {}

for col in cols_list:
    tmp = col.split("__")
    name = tmp[0]  # 特徴量名
    
    # ノンパラメトリック
    if len(tmp) == 1:
        my_params_dict[name] = None
    # パラメトリック
    else:
        for param_value in tmp[1:]:
            # 最後の要素をvalueに、それ以外をparam_listに格納
            *param_list, value = param_value.split("_")
            param = "_".join(param_list)

修正点はparam_listの前に「*」をつけて「_」で結合する処理を加えただけである。変数の前に「*」をつけることで最後の要素だけvalueに格納でき、残りはリストとしてparam_listに格納される。リストparam_listを再び「_」で結合することでパラメータ名にすることができる。

4-4-2. 設定値の編集(整数以外)

列名はすべて文字列型であるが、パラメータの中には整数型や不動点小数型で指定しなければならないものが存在しているため、適切な型変換を行う必要がある。
ここで対処するものについては次に列挙する。

  • 不要な「"」を削除
  • タプル型への変換
  • Noneの対応
  • 真偽型への変換
  • 小数型への変換

まず、不要な「"」を削除することについて、例えば「f_agg_"mean"」というものがあり、これまでの処理では設定値が「"mean"」となるが、設定用の辞書では「mean」となっているので、「"」を削除する処理を追加する。
次にタプル型への変換について、例えば「widths_(2, 5, 10, 20)」という要素があり、これは設定値としては(2, 5, 10, 20)というタプルであるが、現在の処理だと(2, 5, 10, 20)という文字列である。そこでこれをタプル型に変換する必要がある。
真偽型や小数型についても、文字列型を適切な型への変換が必要である。
整数型についても同様の変換が必要であるが、これは他よりも複雑な問題があるため分けて説明をする。

設定値の変換については関数として定義することにした。

def get_value(str_value):
    # 不要な「"」を削除
    value = str_value.replace('\"', '')
    value = str_value
    
    # タプル型への変換
    if "(" in value:
        value_list = value.replace("(", "").replace(")", "")
        value_list = value_list.split(",")
        # タプル型設定値はすべて整数型
        value = tuple(map(int, value_list))

    # Noneの対応
    elif value == "None":
        value = None

    # 小数型への変換(小数点を含む場合)
    elif "." in value:
        value = float(value)
        
    # 真偽型への変換
    elif value == "True":
        value = True
    elif value == "False":
        value = False

    return value

この関数を使って辞書作成処理を修正する。

# 作成する辞書
my_params_dict = {}

for col in cols_list:
    tmp = col.split("__")
    name = tmp[0]  # 特徴量名
    
    # ノンパラメトリック
    if len(tmp) == 1:
        my_params_dict[name] = None
    # パラメトリック
    else:
        for param_value in tmp[1:]:
            # 最後の要素をvalueに、それ以外をparam_listに格納
            *param_list, value = param_value.split("_")
            param = "_".join(param_list)
            # 設定値を編集
            value = get_value(value)

修正点は最後のvalue = get_value(value)である。これで設定値のほとんどは適切な型へ変換された。

4-4-3. 設定値の編集(整数)

ここでは設定値が整数である場合の編集をおこなうが、ややこしいことに特徴量によっては文字列型で渡す必要がある。
例えば「matrix_profile」という特徴量のパラメータ「feature」に25という値を設定するときには文字列型で設定する必要がある。(他にもmeanやmedianなどの文字を設定することができるようにするためと思われる。)

対処方法としては、とりあえず整数型にしておいて、tsfreshで用意されている設定用辞書を参考に文字列型だった場合は文字列型に変換する方法を採用する。

まずは設定値編集用の関数を修正する。

def get_value(str_value):
    # 不要な「"」を削除
    value = str_value.replace('\"', '')
    # タプル型への変換
    if "(" in value:
        value_list = value.replace("(", "").replace(")", "")
        value_list = value_list.split(",")
        # タプル型設定値はすべて整数型
        value = tuple(map(int, value_list))
        
    # Noneの対応
    elif value == "None":
        value = None
        
    # 小数型への変換(小数点を含む場合)
    elif "." in value:
        value = float(value)
        
    # 真偽型への変換
    elif value == "True":
        value = True
    elif value == "False":
        value = False

    else:
        # とりあえず整数型に
        try:
            value = int(value)
        finally:
            return value
        
    return value

修正箇所は最後のelseの処理でとりあえず整数型に変換している。

次に辞書作成処理の修正であるが、手本の辞書を参照して、それが文字列型だった場合は型変換をするようにしている。

from tsfresh.feature_extraction import ComprehensiveFCParameters

# 手本の辞書
fcparams_dict = ComprehensiveFCParameters()
# 手本の辞書にないパラメータ
ng_param = ["autolag"]

# 作成する辞書
my_params_dict = {}

for col in cols_list:
    tmp = col.split("__")
    name = tmp[0]  # 特徴量名
    
    # ノンパラメトリック
    if len(tmp) == 1:
        my_params_dict[name] = None
    # パラメトリック
    else:
        for param_value in tmp[1:]:
            # 最後の要素をvalueに、それ以外をparam_listに格納
            *param_list, value = param_value.split("_")
            param = "_".join(param_list)
            # 手本にないパラメータは処理をしない
            if param in ng_param:
                continue

            # 設定値を編集
            value = get_value(value)

            # 手本が文字列型の場合は型変換
            if type(fcparams_dict[name][0][param]) == str:
                value = str(value)

手本の辞書は特徴量生成の際に生成対象を指定しなかった場合に使われる辞書である。これまでの処理をして得られたパラメータの中に手本の辞書に存在しないもの(autolag)があったので、そのパラメータについては処理をしないようにしている。
そして最後の処理が今回のポイントで、手本のパラメータの設定値が文字列型の場合は処理中の設定値を文字列型に変換している。

以上で設定値の編集は完了した。

4-4-4. 辞書の作成

これまでの処理でパラメトリックな特徴量について、特徴量の名前・パラメータ・設定値に分解した。ここではこれらを使って特徴量生成用辞書を作成する。
ここでのポイントは特徴量の中には複数パターンのパラメータ設定値が存在しており、リストで格納する必要があるということである。

例えば「cid_ce」という特徴量は「normalize」というパラメータが存在しているが、設定値にはTrueとFalseを選択することができる。このとき、辞書の要素としては次のような形になる。

'cid_ce': [{'normalize': True}, {'normalize': False}]

処理の方針としては、上記の例の場合、{'normalize': True}と{'normalize': False}という設定値パターンの辞書を作り、これらを要素とするリストを作成する。
ソースコードは次の通りである。

from tsfresh.feature_extraction import ComprehensiveFCParameters

# 手本の辞書
fcparams_dict = ComprehensiveFCParameters()
# 手本の辞書にないパラメータ
ng_param = ["autolag"]

# 作成する辞書
my_params_dict = {}

for col in cols_list:
    tmp = col.split("__")
    name = tmp[0]  # 特徴量名
    
    # ノンパラメトリック
    if len(tmp) == 1:
        my_params_dict[name] = None
    # パラメトリック
    else:
        # 設定値パターン辞書
        param_dict_tmp = {}

        for param_value in tmp[1:]:
            # 最後の要素をvalueに、それ以外をparam_listに格納
            *param_list, value = param_value.split("_")
            param = "_".join(param_list)
            if param in ng_param:
                continue
            
            # 設定値を編集
            value = get_value(value)

            # 手本が文字列型の場合は型変換
            if type(fcparams_dict[name][0][param]) == str:
                value = str(value)
            # 設定値パターン辞書設定
            param_dict_tmp[param] = value
        
        # 設定値パターン辞書を追加
        if name in my_params_dict:
            my_params_dict[name].append(param_dict_tmp)
        else:
            my_params_dict[name] = [param_dict_tmp]

パラメトリック特徴量の処理の最初に設定値パターンの辞書を定義して、これまでの処理をしたあとに設定値パターン辞書を設定する。
その後に特徴量に対応する設定値パターンの辞書を要素とするリストを作成している。1つ以上設定値パターンを作成している場合はappendして、そうでなければ新しくリストを定義している。

4-5. 作成した辞書で特徴量を生成

上記の処理により作成した辞書my_params_dictを使って特徴量を生成してみることにする。
比較のために、デフォルト設定での特徴量生成も実施する。

ts_my_features = tsfresh.extract_features(
    timeseries_container=data_df,
    column_id="group",
    default_fc_parameters=my_params_dict
)
ts_ori_features = tsfresh.extract_features(
    timeseries_container=data_df,
    column_id="group",
)

print(len(ts_my_features.columns))
print(len(ts_ori_features.columns))

これの結果はどちらも789となり生成される特徴量の数は同じとなった。

生成された特徴量の列名が同じになるかも確認する。

print((ts_my_features.columns == ts_ori_features.columns).sum())

結果は789となり列数と同じとなった。

5. 選択した特徴量を再生成

ここでは一度デフォルト設定で特徴量を生成してから部分的に列を指定して、同じ特徴量を再生成できることを確認する。

まずはデフォルト設定で789個の特徴量を作成して、その中から200種類の特徴量を選択する。

# デフォルト設定で特徴量を生成
ts_ori_features = tsfresh.extract_features(
    timeseries_container=data_df,
    column_id="group",
)

# ランダムに200個特徴量を選択
sample_features = np.random.choice(list(ts_ori_features.columns), size=200, replace=False)
# 前処理
cols_list = ["__".join(x.split("__")[1:]) for x in sample_features]

次に前処理済の特徴量列から辞書を作成する。(ソースコードは上記のものと同じ)

# 作成する辞書
my_params_dict = {}
ng_param = ["autolag"]


for col in cols_list:
    tmp = col.split("__")
    name = tmp[0]  # 特徴量名
    
    # ノンパラメトリック
    if len(tmp) == 1:
        my_params_dict[name] = None
    # パラメトリック
    else:
        # 設定値パターン辞書
        param_dict_tmp = {}
        for param_value in tmp[1:]:
            # 最後の要素をvalueに、それ以外をparam_listに格納
            *param_list, value = param_value.split("_")
            param = "_".join(param_list)
            if param in ng_param:
                continue
            
            # 設定値を編集
            value = get_value(value)

            # 手本が文字列型の場合は型変換
            if type(fcparams_dict[name][0][param]) == str:
                value = str(value)
            # 設定値パターン辞書設定
            param_dict_tmp[param] = value
        
        # 設定値パターン辞書を追加
        if name in my_params_dict:
            my_params_dict[name].append(param_dict_tmp)
        else:
            my_params_dict[name] = [param_dict_tmp]

最後に作成した辞書から特徴量を生成する。

ts_my_features = tsfresh.extract_features(
    timeseries_container=data_df,
    column_id="group",
    default_fc_parameters=my_params_dict
)

print(len(ts_my_features.columns))

結果は200となり、生成された特徴量の数は正しい。生成された特徴量の種類も同じか確認しておく。

sample_features = list(sample_features)  # ランダムに選択した特徴量
my_features = list(ts_my_features.columns)  # 再生成した特徴量

# 比較のためにソートしておく
sample_features.sort()
my_features.sort()

 # 同じ列名の数
print((np.array(sample_features) == np.array(my_features)).sum())

結果は200となり、選択したものと同じ列が生成されている。

6. ソースコード全体

import tsfresh
from tsfresh.feature_extraction import ComprehensiveFCParameters

import numpy as np
import pandas as pd

# データ作成
t = np.linspace(0, 1, 100)
sin_data = np.sin(2*np.pi*t)
data_df = pd.DataFrame({"sin": sin_data , "type": ["sin data"]*len(sin_data)})

# 特徴量作成
ts_features = tsfresh.extract_features(
    timeseries_container=data_df,
    column_id="group",
    default_fc_parameters=fcparams_dict
)

def get_value(str_value):
    # 不要な「"」を削除
    value = str_value.replace('\"', '')
    # タプル型への変換
    if "(" in value:
        value_list = value.replace("(", "").replace(")", "")
        value_list = value_list.split(",")
        # タプル型設定値はすべて整数型
        value = tuple(map(int, value_list))
        
    # Noneの対応
    elif value == "None":
        value = None
        
    # 小数型への変換(小数点を含む場合)
    elif "." in value:
        value = float(value)
        
    # 真偽型への変換
    elif value == "True":
        value = True
    elif value == "False":
        value = False

    else:
        # とりあえず整数型に
        try:
            value = int(value)
        finally:
            return value
        
    return value

# 辞書作成
# 作成する辞書
my_params_dict = {}
ng_param = ["autolag"]


for col in cols_list:
    tmp = col.split("__")
    name = tmp[0]  # 特徴量名
    
    # ノンパラメトリック
    if len(tmp) == 1:
        my_params_dict[name] = None
    # パラメトリック
    else:
        # 設定値パターン辞書
        param_dict_tmp = {}
        for param_value in tmp[1:]:
            # 最後の要素をvalueに、それ以外をparam_listに格納
            *param_list, value = param_value.split("_")
            param = "_".join(param_list)
            if param in ng_param:
                continue
            
            # 設定値を編集
            value = get_value(value)

            # 手本が文字列型の場合は型変換
            if type(fcparams_dict[name][0][param]) == str:
                value = str(value)
            # 設定値パターン辞書設定
            param_dict_tmp[param] = value
        
        # 設定値パターン辞書を追加
        if name in my_params_dict:
            my_params_dict[name].append(param_dict_tmp)
        else:
            my_params_dict[name] = [param_dict_tmp]

# 特徴量再生成
ts_my_features = tsfresh.extract_features(
    timeseries_container=data_df,
    column_id="group",
    default_fc_parameters=my_params_dict
)

やったこととやりたいこと

ここでは私のブログで扱った内容と今後書きたい(勉強したい)内容を書いていこうと思います。
リンクがあるものはすでに書いた内容で、ないものはこれから書きたい内容です。

優先的に記事を書いてほしい内容や、ここには書いてないけど関連する内容で解説してほしいものがあれば教えてください!

テクニカル分析

ローソク足の説明とbitbank APIを使ってローソク足の取得方法について書いてます。

注文板と指値注文・成行注文について書いてます。また、bitbank APIを使って板情報を取得する方法も解説しています。

  • テクニカル指標

仮想通貨取引に使えそうなテクニカル指標の実装を紹介する記事を書きたいですね。

  • 時系列データ分析

時系列モデルの解説とチャートのモデルへの当てはめについての記事を書きたいですね。
時系列モデルについては勉強中です。

機械学習

800種類近くの特徴量を生成してから特徴量選択をして、選択された特徴量だけを再生成できるか検証しました。

  • Triple Barriers戦略

書籍「ファイナンス機械学習(Amazon)」で紹介されていた手法です。

仮想通貨取引Botに使えそうな強化学習の手法を解説する記事を書いてみたいですね。

bitbank API使い方

bitbank APIを使った仮想通貨の取引をするのに必要なシークレットキーについて説明をしています。

bitbank APIを使って仮想通貨の売買注文をする方法を解説しています。

仮想通貨取引Bot作成

仮想通貨取引Botの基礎となる抽象クラスの実装について解説しています。

  • Bollinger Bandsを使ったBotの実装

Bollinger Bands(ボリンジャーバンド)を使った仮想通貨取引Botの実装について解説したいですね。
たまに私のラズパイで動かしたりしています。

  • その他テクニカル指標を使ったBotの実装

いくつかテクニカル指標の実装を紹介した後に仮想通貨取引Botに実装して紹介したいですね。

強化学習で仮想通貨の取引を学習させる記事を書きたいですね。
現在、実装を頑張ってます。

  • Botの設定や動作状況を確認するGUI

仮想通貨取引Botが取り扱う通貨の切り替えや取引成績の確認ができるGUIを作って紹介したいですね。

  • チャットアプリとの連携

仮想通貨取引Botの提示報告をTelegramで受け取るシステムを解説したいですね。
これは実際に私が使っているものです。TelegramはLINEと連携するよりかなり簡単でいいですね。

音声合成ソフト制御

  • VOICEROID2をPythonで制御

ちょっと前にチャットボットと連携させて会話内容をしゃべらせてたのですが、制御部分を汎用的に使えるようにして解説したいですね。

現在VOICEROID2と同じようにして制御できないか模索中です。制御できるようになったら調べ方を含めて解説記事書きたいですね。
(追記 11/23)CeVIO CS7はC#APIを介してPythonで制御することができました。記事を書くときはこっちの方法を解説することになるかも。

自作ライブラリの紹介

LiquidのAPIを使用して取引を行う自作ライブラリを紹介
github.com

bitbank APIで仮想通貨を取引しよう(Bot抽象クラス 編)

 この記事では仮想通貨取引Botの素となる抽象クラスの実装について備忘録を兼ねて解説します。

Botを動かす環境

 Botを動かす環境の設計思想として、使用するBotのクラスを変えるだけで簡単に戦略を変えることができるようにしています。そのために売買判断など、どのBotに共通する機能は抽象クラスで定義します。環境上ではその抽象クラスを継承したBotを使い、抽象クラスで定義したメソッドだけを呼び出すようにします。

 処理のイメージは次の通りです。

bot = BollingerBandBot()  # Botを初期化

while True:
    today_str = datetime.today().strftime("%Y%m%d")  # 今日の日付を取得
    ohlc = get_ohlc(pair, yyyymmdd)  # OHLC情報(始値, 高値, 低値, 終値)を取得
    bot.update_info(*ohlc)  # 取得した情報でBotの内部情報を更新
    action = bot.get_action()  # 更新したBotの内部情報に基づいて行動を選択
    transaction(action)  # 選択した行動に従って売買処理
    time.sleep(5*60)  # 5分待つ

 この記事ではこの環境で動かすBotが継承する抽象クラスについて説明します。

抽象クラス- AbstractBot-

 ここではBotに共通するメソッドや変数を抽象クラス「AbstractBot」に定義します。取引判断に使用するBotはこのクラスを継承して使用することになります。以下で詳細な説明をします。

インスタンス変数

 抽象クラスAbstractBotにはbitbankのAPIから取得できる基本的な値を保持する変数を持たせます。具体的には次の通りです。

  • open_prices: 始値のリスト
  • high_prices: 高値のリスト
  • low_prices: 低値のリスト
  • close_prices: 終値のリスト
  • window_size: 上記のリストの長さの最大値(インスタンス生成時に設定)

 始値や高値のリストにはAPIから情報を取得するたびに値を追加していきます。

 説明した部分の実装は次の通りです。

from abc import ABCMeta, abstractmethod


# 抽象クラス
class AbstractBot(metaclass=ABCMeta):
    def __init__(self, window_size=10):
        self.window_size = window_size
        
        self.open_prices = []
        self.high_prices = []
        self.low_prices = []
        self.close_prices = []

クラスメソッド

 抽象クラスAbstractBotには次のメソッドを持たせます。

 これらのメソッドの詳細説明および実装は次で行います。

情報更新メソッド-update_info-

 ここではAPIから取得した情報をインスタンス変数に格納するメソッド「update_info」の説明を行います。

 このメソッドでは、APIから取得した始値、高値、低値、終値を受け取って、上で紹介したそれぞれのリストに格納します。その際にリストの最大長を超えた場合には一番古い情報を捨てます。

 リストの長さが最大長に達したときに、戦略に使用する値を計算するメソッド「calc_values」(詳細は後述)を呼び出します。

class AbstractBot(metaclass=ABCMeta):
    def __init__(self, window_size=10):
        # 省略

    def update_info(self, open_price, high, low, close_price):
        """
        OHLCデータを取り込み、情報を更新する
        :param open_price: 始値
        :param high: 高値
        :param low: 低値
        :param close_price: 終値
        """
        self.open_prices.append(open_price)
        self.high_prices.append(high)
        self.low_prices.append(low)
        self.close_prices.append(close_price)

        if len(self.high_prices) > self.window_size:
            # window sizeになったら最初の値は捨てる
            self.high_prices = self.high_prices[1:]
            self.low_prices = self.low_prices[1:]
            self.open_prices = self.open_prices[1:]
            self.close_prices = self.close_prices[1:]

            # 戦略用の値を計算
            self.calc_values()
売買判断指標計算メソッド-calc_values-

 ここでは売買判断を行うための指標を計算するメソッド「calc_values」について説明を行います。

 このメソッドはインスタンス変数に格納されたOHLCからテクニカル指標など、戦略に使用する値を計算します。また、このメソッドはOHLCの長さがwindow_sizeに達したときに呼ばれる(「情報更新メソッド-update_info-」を参照)ので、例えばローソク足10本文の移動平均を使った戦略を使いたい場合はwindow_sizeに10を設定して、このメソッドには価格の平均値を計算する処理を書くことになります。ここでは指標を計算するだけなので、計算結果を格納するインスタンス変数を適宜用意しておく必要があります。

 この抽象クラスには具体的な戦略を持たせないので、何もしないメソッドを抽象メソッドとして定義します。具体的なBotクラスを定義する際には、戦略に応じた値を計算するメソッドでオーバーライドさせます。

class AbstractBot(metaclass=ABCMeta):
    def __init__(self, window_size=10):
        # 省略

    def update_info(self, open_price, high, low, close_price):
        # 省略

    @abstractmethod
    def calc_values(self):
        """
        戦略に応じた値を計算する
        """
        pass

 例えば、Bollinger Bandによる戦略を採用する場合には、売買判断に価格の平均値と標準偏差を使用するので、このメソッドでそれらの計算を行うことになります。実装例は次の通りです。(そのうち詳細を説明した記事を書く予定)

class BollingerBandBot(AbstractBot):
    def __init__(self, window_size):
        super(BollingerBandBot, self).__init__(window_size)

        self.sigma = 0.0    # 終値の標準偏差
        self.means = 0.0    # 終値の平均値

    def calc_values(self):
        """
        戦略に応じた値を計算する
        Bollinger Band用の標準偏差を計算する
        """
        self.sigma = statistics.stdev(self.close_prices)
        self.means = statistics.mean(self.close_prices)
売買判断メソッド-get_action-

 ここでは計算した指標から売買判断を行うメソッド「get_action」について説明を行います。

 このメソッドでは上記で説明したメソッドcalc_valuesで計算した値を参照して売買の判断を行い、売買判断結果と数量のペアを返します。売買判断結果は

  • buy: 購入する
  • sell: 売却する
  • pass: 見送る

の3種類のいずれかを設定します。例えばこのメソッドが["buy", 30.0]を返したときには数量30.0だけ購入することを表します。

 この抽象クラスでは特定の戦略を持たせないので何もしないメソッドを抽象メソッドとして定義しますが、具体的なBotクラスを定義する際には、戦略に応じた判断基準を書いたメソッドでオーバーライドさせます。

class AbstractBot(metaclass=ABCMeta):
    def __init__(self, window_size=10):
        # 省略

    def update_info(self, open_price, high, low, close_price):
        # 省略

    @abstractmethod
    def calc_values(self):
        # 省略

    @abstractmethod
    def get_action(self):
        """
        行動を出力する。形式は次の通り
        [行動, 数量]
        行動は"buy", "sell", "pass"のいずれか
        """
        pass

ソースコード

 ここでは今回説明した抽象クラス「AbstractBot」のソースコード全体を載せます。

from abc import ABCMeta, abstractmethod


# 抽象クラス
class AbstractBot(metaclass=ABCMeta):
    def __init__(self, window_size=10):
        self.window_size = window_size

        self.open_prices = []
        self.high_prices = []
        self.low_prices = []
        self.close_prices = []

    def update_info(self, open_price, high, low, close_price):
        """
        OHLCデータを取り込み、情報を更新する
        :param open_price: 始値
        :param high: 高値
        :param low: 低値
        :param close_price: 終値
        """
        self.open_prices.append(open_price)
        self.high_prices.append(high)
        self.low_prices.append(low)
        self.close_prices.append(close_price)

        if len(self.high_prices) > self.window_size:
            # window sizeになったら最初の値は捨てる
            self.high_prices = self.high_prices[1:]
            self.low_prices = self.low_prices[1:]
            self.open_prices = self.open_prices[1:]
            self.close_prices = self.close_prices[1:]

            # 戦略用の値を計算
            self.calc_values()

    @abstractmethod
    def calc_values(self):
        """
        戦略に応じた値を計算する
        """
        pass

    @abstractmethod
    def get_action(self):
        """
        行動を出力する。形式は次の通り
        [行動, 数量]
        行動は"buy", "sell", "pass"のいずれか
        """
        pass

 

bitbank APIで仮想通貨を取引しよう(注文 編)

 この記事ではbitbankAPIで注文を出す方法を備忘録を兼ねて解説します。

 bitbank APIで注文を出すにはbitbankの口座を作成して「シークレットキー」を発行する必要があります。シークレットキーはマイページのAPIタブから発行・確認ができます。

注文を出す関数

注文を出す関数の使い方と引数

 注文を出すにはプライベートAPIのorder関数を使います。使い方は次の通りです。
 ※order関数に渡している変数を用意する必要あり

import python_bitbankcc

# KEYS
API_KEY    =" xxxxxxxxxxxxxxxxx" # APIキー
API_SECRET = "xxxxxxxxxxxxxxxx" # シークレットキー

# プライベートAPI
prv = python_bitbankcc.private(API_KEY, API_SECRET)

# 注文
order = prv.order(pair, amount, price, side, order_type)

 プライベートAPIを使用するにはAPIキーとシークレットキーを用意してpython_bitbankcc.privateに渡します。ただし、上記の例のようにAPIキーとシークレットキーをソースコードに書くのはお勧めしません。これを回避する方法はbitbank APIで仮想通貨を取引しよう(シークレットキー 編)で紹介しています。

 ここではプライベートAPIを変数prvに保持しています。そしてprvが持つクラスメソッドorderで注文を出しています。orderに渡す引数は次の通りです。

引数名 意味
pair 取引する通貨のペアを指定します。
例えばビットコインを日本円で取引するにはbtc_jpyを指定する。
指定できる値についてはドキュメントを参照してください。
amount 注文量を指定します。
price 注文単価を指定します。
side 売り注文なのか、買い注文なのかを指定します。
売り注文であれば「sell」を買い注文であれば「buy」を指定します。
order_type 指値注文なのか成行注文なのかを指定します。
指値注文であれば「limit」を成行注文であれば「market」を指定します。
指値注文と成行注文の意味については過去の記事を参照してください。

 私の環境ではAPIのバージョンが古いため指定できませんでしたが、PostOnly注文の指定もできるようです。

注文を出す関数の返り値

 次にこの関数の返り値を確認してみましょう。

order = prv.order(pair="xlm_jpy", price=61.0, amount=0.1, order_type="limit", side="buy")
print(order)

 ここではステラルーメンを0.1だけ、単価61円で指値買い注文を出しました。つまり、6.1円払って0.1xlmを買おうとしています。

 この買い注文の返り値をorderに格納して次の行で出力しています。今回は次のように出力されました。

{'order_id': 14062042287, 'pair': 'xlm_jpy', 'side': 'buy', 'type': 'limit', 'start_amount': '0.1000', 'remaining_amount': '0.1000', 'executed_amount': '0.0000', 'price': '61.000', 'average_price': '0.000', 'ordered_at': 1620008701853, 'status': 'UNFILLED', 'expire_at': 1635560701853, 'post_only': False}

 返り値は辞書型で注文に関する情報が出力されました。意味は次の通りです。

項目 意味
order_id 注文ID
pair 取引通貨ペア
side 売り注文か買い注文か
売り注文なら「buy」、買い注文なら「sell」
type 指値注文か成行注文か
指値注文なら「limit」、成行注文なら「market」
start_amount 注文数量
remaining_amount 未約定の数量
executed_amount 約定済み数量
price 注文価格
average_price 平均約定価格
order_at 注文日時(UnixTime)
status 注文ステータス
注文中:「UNFILLED」
一部約定済:「PARTIALLY_FILLED」
すべて約定済:「FULLY_FILLED」
取消済:「CANCELED_UNFILLED」
注文取消済(一部約定):「CANCELED_PARTIALLY_FILLED」
expire_at 有効期限(UnixTime)
post_only PostOnly注文かどうか

bitbank APIで仮想通貨を取引しよう(シークレットキー 編)

 この記事ではbitbankAPIで仮想通貨取引をする際などに使用するシークレットキーを環境変数に登録してプログラムで参照する方法を備忘録を兼ねて解説します。

シークレットキー

 bitbankのAPIを使って自分の資産を確認したり、仮想通貨取引をしたりするためにはシークレットキーを発行する必要があります。シークレットキーはbitbankの口座を作成して、マイページから「API」のタブにアクセスすれば発行できます。

 ここで発行されたシークレットキーは第三者に知られてはなりません。これが流出してしまうと第三者によって勝手に仮想通貨の取引をされてしまうなど、悪用されてしまう可能性があります。

 シークレットキーはプログラムで使用するのですが、ソースコードに直接記述することはおすすめしません。例えば、シークレットキーが記述されたソースコードをGitHabなどインターネットにアップロードしてしまうと他の人が見れるようになり悪用されてしまうかもしれません。最初は公開するつもりがなくても、念のため直接書くことは避けた方がいいと思います。

 この記事ではシークレットキーを管理する方法として環境変数で管理する方法を紹介します。

環境変数の登録

 windowsではコントロールパネルから「システムとセキュリティ」→「システム」→「システムの詳細設定」→「環境変数」で環境変数のウィンドウを出すことができます。

 環境変数の「システム環境変数」の変数名に適当な値を、変数値にシークレットキーを入力して登録します。
 ここでは例として、変数名に「BITBANK」、変数値に「test_xxxxx」を登録します。

環境変数入力例
環境変数入力例

プログラムでの環境変数の参照

 上で登録した環境変数pythonで参照する方法を紹介します。

 python環境変数を参照するには「os」をインポートして、「os.environ」で登録した変数値を呼び出します。
 プログラムは以下の通りです。

import os

secret_key = os.environ["BITBANK"] #変数値の呼び出し

print(secret_key)

 今回は環境変数名を「BITBANK」としましたので「os.environ["BITBANK"]」で呼び出しています。
 このプログラムを実行すると、今回登録した変数値「test_xxxxx」が出力されます。

bitbank APIで仮想通貨を取引しよう(板情報 編)

 この記事ではbitbankAPIを使った板の取得方法を備忘録を兼ねて解説します。
 板の説明上、指値注文と成行注文の解説をしますが、APIを使った注文の出し方は別の記事で説明します。

 とは現在出ている注文の一覧表のことです。
 前回の記事で紹介したローソク足では過去の取引情報を確認することができますが、今回紹介する板ではこれから発生するであろう取引を確認することができます。

 板に書かれている情報は次の3つです。

  • 価格
  • 売り注文に出されている数量
  • 買い注文に出されている数量

 例えばビットコインの板が次のようになっていたとしましょう。

売り注文量 価格 買い注文量
3.0 6,100,000
5.2 6,050,000
3.0 6,040,000
0.1 6,000,000
5,988,000 3.2
5,987,000 2.8
5,986,000 0.1
5,985,000 3.3
5,979,000 1.4
5,975,000 4.8

 板のうちで売り注文量に値が入っている部分を売り板(赤字の部分)と言います。
 売り板の一番上を見ると、単価6,100,000円の売り注文が3.0ビットコイン分入っていることが読み取れます。

 一方で買い注文量に値が入っている部分を買い板(緑字の部分)と言います。
 買い板の一番下を見ると、単価5,975,000円の買い注文が4.8ビットコイン分入っていることが読み取れます。

 このように板を確認すると、どの価格にどれだけの量の注文が入っているかが確認できます。

指値注文

 上で紹介した板に注文を載せるには指値注文を出します。
 指値注文とは売る(買う)値段を指定する注文方式です。

 例えば、6,001,000円で1.0ビットコインの売り注文を指値注文で出すと、上の板は次のように更新されます。(黒字部分が今回の注文)

売り注文量 価格 買い注文量
3.0 6,100,000
5.2 6,050,000
3.0 6,040,000
1.0 6,001,000
0.1 6,000,000
5,988,000 3.2
5,987,000 2.8
5,986,000 0.1
5,985,000 3.3
5,979,000 1.4
5,975,000 4.8

 指値注文の値段(上の例だと6,001,000円)のことを指値と言います。

 指値注文の特徴は、売り注文なら指値よりも高くならないと取引は成立せず、買い注文なら指値より低くならないと取引は成立しないことです。
 今回の例ではビットコインの値段が6,001,000円より高くならないと取引が成立しないので、6,001,000円で売りたかったのに5,900,000円で売れてしまった...ということにはなりません。

 指値注文では思っていたよりも低い値段で売れてしまったり、高い値段で買ってしまうことはないのですが、値段が指値に到達しないと取引が成立しないため、なかなか取引が成立せず売買の機会を逃してしまう可能性があります。

成行注文

 成行注文とは値段を指定せず、注文量だけ指定する注文方式です。
 売り注文を成行注文で行うと買い板の最高値で注文が成立します。
 一方で買い注文を成行注文で行うと売り板の最安値で注文が成立します。

 例えば、板が次の状態だとします。

売り注文量 価格 買い注文量
3.0 6,100,000
5.2 6,050,000
3.0 6,040,000
0.1 6,000,000
5,988,000 3.2
5,987,000 2.8
5,986,000 0.1
5,985,000 3.3
5,979,000 1.4
5,975,000 4.8

 このとき0.5ビットコインの買い注文を成行注文で行うと、まず売り板の最安値である、単価6,000,000円の0.1ビットコインの取引が成立して、板が次のように更新されます。

売り注文量 価格 買い注文量
3.0 6,100,000
5.2 6,050,000
3.0 6,040,000
5,988,000 3.2
5,987,000 2.8
5,986,000 0.1
5,985,000 3.3
5,979,000 1.4
5,975,000 4.8

 残り0.4ビットコイン分の注文は更新された売り板の最安値である、単価6,040,000円の取引が成立して、板が次のように更新されます。

売り注文量 価格 買い注文量
3.0 6,100,000
5.2 6,050,000
2.6 6,040,000
5,988,000 3.2
5,987,000 2.8
5,986,000 0.1
5,985,000 3.3
5,979,000 1.4
5,975,000 4.8

 以上により、今回の条件で0.5ビットコインの買い注文を成行注文を出すと、単価6,000,000円で0.1ビットコインを買い、さらに単価6,040,000円で0.4ビットコインを買ったので
(6,000,000×0.1) + (6,040,000×0.4) = 3,016,000
により、3,016,000円で0.5ビットコインを購入したことになります。

 成行注文では、注文を出すとすぐに取引が成立するということがメリットとなります。
 一方で想定外の値段で取引が成立してしまうという可能性があるので注意しなければなりません。

 例えば、板が次の状態だとします。

売り注文量 価格 買い注文量
3.0 6,100,000
2.6 6,040,000
6,000,000 0.1
5,500,000 5.0

 このとき1.0ビットコインの売り注文を成行注文で行うと、まず買い板の最高値で取引が成立するので、単価6,000,000円で0.1ビットコインが売れます。
 そして板が更新されて次のようになります。

売り注文量 価格 買い注文量
3.0 6,100,000
2.6 6,040,000
5,500,000 5.0

 残りの0.9ビットコイン分の注文は買い板の最高値で取引されるので、最初の取引よりも500,000円安い単価5,500,000円で取引されます。

 想定外の値段で取引が成立することを避けるために成り行き注文する際には板をしっかり確認しましょう。

指値注文と成行注文のメリット・デメリット

 指値注文と成行注文のメリットおよびデメリットをまとめておきました。

指値注文 成行注文
メリット 自分が指定した値段で取引可能
(想定外の値段で取引されない)
すぐに取引できる
デメリット 値段が指値に到達しないと取引が成立せず
売買の機会を逃す可能性あり
想定外の値段で取引が
成立してしまう可能性あり

APIを使った板情報の取得

 ここからbitbankのAPIを使って板情報を取得しましょう。
 そのためにはAPIライブラリをインストールする必要がありますので、導入していない場合はコンソールにて次を実行してインストールしてください。

$ pip install git+https://github.com/bitbankinc/python-bitbankcc.git

売り板を出力しよう

 ビットコインの現在の売り板を出力してみましょう。
 売り板情報は、板情報を取得して、「asks」にアクセスすると得られます。

# bitbank APIをインポート
import python_bitbankcc


# bitbankのパブリックAPIの呼び出し
pub = python_bitbankcc.public()

# 通貨ペア(ビットコイン/日本円)
pair = "btc_jpy"

# 板の取得
order_board = pub.get_depth(pair)

# 売り板の取得
asks_board = order_board["asks"]

# 売り板の出力
for ask in asks_board:
    print(ask)

 これを実行すると次の結果が得られます。(実行時の板情報が得られるので結果はタイミングによって実行結果が変わります。)

['6756039', '0.4463']
['6759204', '0.0370']
['6759997', '0.0150']
中略
['6929994', '0.0001']
['6930000', '0.0763']
['6930168', '0.0010']

 出力結果の各行に注文情報が出力されています。左が値段で右が注文量です。
 また、値段が安いものから順に出力されます。

売り板を部分的に出力しよう

 上のプログラムを実行すると、かなりたくさんの注文情報が出力されました。
 これでは少し見づらいので、売り板の一部だけを出力するようにしましょう。

 上で解説した通り、売り板は値段が安いものから取引されるので、取得した売り板情報のうちで値段が安いものから5つ出力するようにしましょう。
 そのプログラムは次の通りです。

import python_bitbankcc

pub = python_bitbankcc.public()
pair = "btc_jpy"
order_board = pub.get_depth(pair)

# 売り板のうち最初5つを取得
asks_board = order_board["asks"][:5]

# 売り板の出力
for ask in asks_board:
    print(ask)

 これを実行すると次のように出力されます。(実行時の板情報が得られるので結果はタイミングによって実行結果が変わります。)

['6773936', '0.0494']
['6773937', '0.0383']
['6774101', '0.0147']
['6775428', '0.0003']
['6775429', '0.0660']

買い板を出力しよう

 ビットコインの現在の買い板を出力してみましょう。
 買い板情報は、板情報を取得して、「bids」にアクセスすると得られます。
 プログラムは次の通りです。

import python_bitbankcc

pub = python_bitbankcc.public()
pair = "btc_jpy"
order_board = pub.get_depth(pair)

# 買い板の取得
bid_board = order_board["bids"]

# 買い板の出力
for bid in bids_board:
    print(bid)

 これを実行すると次のように出力されます。(実行時の板情報が得られるので結果はタイミングによって実行結果が変わります。)

['6767755', '0.4447']
['6767750', '0.0118']
['6767699', '0.4009']
中略
['6611000', '0.0338']
['6610001', '0.4000']
['6610000', '0.0067']

 出力結果の各行に注文情報が出力されています。左が値段で右が注文量です。
 買い板は売り板とは逆に値段が高いものから順に出力されます。

買い板を部分的に出力しよう

 売り板の場合と同様に注文情報を5つだけ出力しましょう。
 上で解説した通り、買い板は値段が高いものから順に取引されるので、値段が高い注文を5つ出力しましょう。
 処理としては売り板と同様にすればOKです。

import python_bitbankcc

pub = python_bitbankcc.public()
pair = "btc_jpy"
order_board = pub.get_depth(pair)

# 買い板のうち最初5つを取得
bids_board = order_board["bids"][:5]

# 買い板の出力
for bid in bids_board:
    print(bid)

 これを実行すると次のように出力されます。(実行時の板情報が得られるので結果はタイミングによって実行結果が変わります。)

['6761815', '0.4629']
['6760959', '0.0400']
['6760958', '0.1400']
['6760289', '0.0550']
['6760203', '0.0480']