予測できるかを予測してみよう

吐きだめのような正確性のない文章

仮想通貨の銘柄ごとの値動きをクラスタリングしてみた話

モチベーション

株価時系列に基づく企業クラスタリングという論文があることを知ったので,それを仮想通貨に適用してなにかしらの気づきを得られないかを調査した。

株価時系列の企業クラスタリング

まず,論文と同じような結果となるかを確認した。

Yahoo Finance API2を使用してデータを取得した。インストールは以下で可能。

!pip install yahoo-finance-api2

以下からデータ取得&グラフの描画が可能となる。データのダウンロードに少々時間がかかるため注意。 ソースの内容について一部補足をすると

  • 使用する株価時系列は東証株価指数33業種のうち,電気・ガス業(業種コード4050)となる。構成銘柄の番号については東証のサイトから確認できる。
  • UMAPの設定値については論文と同様(n_neighbors=2, min_dist=0.01)とした。この変更により,クラスタリングがより顕著に形成される傾向となる。
from yahoo_finance_api2 import share
from yahoo_finance_api2.exceptions import YahooFinanceError
import pandas as pd
import umap
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
import seaborn as sns
import colorcet as cc
%matplotlib inline
 
S_year = 5 #取得年数
S_day = 1 #取得単位
company_dic = { 
'9501':'東電力HD',
'9502':'中部電力',
'9503':'関西電力',
'9504':'中国電力',
'9505':'北陸電力',
'9506':'東北電力',
'9507':'四国電力',
'9508':'九州電力',
'9509':'北海電力',
'9511':'沖縄電力',
'9513':'Jパワー',
'9514':'EFON',
'9517':'イーレックス',
'9519':'レノバ',
'9531':'東瓦斯',
'9532':'大瓦斯',
'9533':'東邦瓦斯',
'9534':'北海瓦斯',
'9535':'広島ガス',
'9536':'西部ガスHD',
'9543':'静岡ガス',
'9551':'メタウォーター',
}

def get_pctchange(code):
    company_code = str(code) + '.T'
    my_share = share.Share(company_code)
    symbol_data = my_share.get_historical(share.PERIOD_TYPE_YEAR,
                                              S_year,
                                              share.FREQUENCY_TYPE_DAY,
                                              S_day)
    df = pd.DataFrame(symbol_data)
    df['timestamp'] = pd.to_datetime(df['timestamp']*1e6)
    df = df.set_index('timestamp', drop=True)
    return (df['close']).pct_change()

if __name__ == '__main__':
    # 株価時系列の結合
    df = pd.concat([
        get_pctchange(c).rename(f'{company_dic[c]}') 
        for c in company_dic.keys()
    ]
    ,axis=1)

    # 欠損値削除と標準化
    df = df.dropna()
    target = (df - df.mean()) / df.std()

    # UMAPでの時限削減
    um = umap.UMAP(n_neighbors=2, min_dist=0.01)
    um_out = um.fit_transform(target.T.values)

    # 散布図として描画
    fig = plt.figure(figsize=(6,4))
    palette = sns.color_palette(cc.glasbey, n_colors=24)

    for i in range(um_out.shape[0]):
        plt.scatter(um_out[:,0][i], um_out[:,1][i], 
                    color=palette[i],
                    label=df.columns[i]
                   )

    plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0)
    plt.show()

出力された結果を見てみると,元論文の結果と概ね一致していることが確認できる。一部,クラスタリングの構成要素が変化しているが,これは元論文との株価時系列の取得期間の違いによるものと考えられる。

f:id:mcakiyama:20220217035606p:plain
株価時系列クラスタリング出力

仮想通貨価格時系列のクラスタリング

仮想通貨のクラスタリングについては以下でそれなりの長さのOHLCVデータを取得できそうだったため,少し手直しして使用している。 APIで取得したOHLCVデータから任意の時間足を作成する

ある程度,新興の銘柄でも十分なデータが取得できるように,5分足でのデータ取得とした。

# coding: utf-8
from datetime import datetime, timedelta
import time, calendar, pytz, requests
import pandas as pd
import colorcet as cc
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
import umap 
%matplotlib inline

ALLOWED_PERIOD = {
    "1m": ["1m", 1,    1],  "3m": ["1m",  3,   3],
    "5m": ["5m", 1,    5], "15m": ["5m",  3,  15], "30m": ["5m", 6, 30],
    "1h": ["1h", 1,   60],  "2h": ["1h",  2, 120],
    "3h": ["1h", 3,  180],  "4h": ["1h",  4, 240],
    "6h": ["1h", 6,  360], "12h": ["1h", 12, 720],
    "1d": ["1d", 1, 1440],
    # not support yet '3d', '1w', '2w', '1m'
}

# DataFrameでOHLCVを取得
def fetch_ohlcv_df(period="1m", symbol="XBTUSD", count=1000, reverse=True, partial=False, tstype="UTMS"):
    if period not in ALLOWED_PERIOD:
        return None
    period_params = ALLOWED_PERIOD[period]
    need_count = (count + 1) * period_params[1] # マージ状況により、不足が発生する可能性があるため、多めに取得

    # REST APIリクエストでOHLCVデータ取得
    df_ohlcv = __get_ohlcv_paged(symbol=symbol, period=period_params[0], count=need_count)

    # DataFrame化して指定時間にリサンプリング
    if period_params[1] > 1:
        minutes = ALLOWED_PERIOD[period][2]
        offset = str(minutes) + "T"
        if 60 <= minutes < 1440:
            offset = str(minutes / 60) + "H"
        elif 1440 <= minutes:
            offset = str(minutes / 1440) + "D"
        df_ohlcv = df_ohlcv.resample(offset).agg({
                        "timestamp": "first",
                        "open":      "first",
                        "high":      "max",
                        "low":       "min",
                        "close":     "last",
                        "volume":    "sum",
                    })
    # 未確定の最新足を除去
    if partial == False:
        df_ohlcv = df_ohlcv.iloc[:-1]
    # マージした結果、余分に取得している場合、古い足から除去
    if len(df_ohlcv) > count:
        df_ohlcv = df_ohlcv.iloc[len(df_ohlcv)-count:]
    # index解除
    df_ohlcv.reset_index(inplace=True)
    # timestampを期間終わり時刻にするため、datetimeをシフト
    df_ohlcv["datetime"] += timedelta(minutes=ALLOWED_PERIOD[period][2])
    # timestamp変換
    __convert_timestamp(df_ohlcv, tstype)
    # datetime列を削除
    df_ohlcv.drop("datetime", axis=1, inplace=True)
    # 並び順を反転
    if reverse == True:
        df_ohlcv = df_ohlcv.iloc[::-1]
    # indexリセット
    df_ohlcv.reset_index(inplace=True, drop=True)
    return df_ohlcv

# private
def __convert_timestamp(df_ohlcv, timestamp="UTMS"):
    if timestamp == "UTS":
        df_ohlcv["timestamp"] = pd.Series([int(dt.timestamp()) for dt in df_ohlcv["datetime"]])
    elif timestamp == "UTMS":
        df_ohlcv["timestamp"] = pd.Series([int(dt.timestamp()) * 1000 for dt in df_ohlcv["datetime"]])
    elif timestamp == "DT":
        df_ohlcv["timestamp"] = df_ohlcv["datetime"]
    elif timestamp == "STS":
        df_ohlcv["timestamp"] = pd.Series([dt.strftime("%Y-%m-%dT%H:%M:%S") for dt in df_ohlcv["datetime"]])
    elif timestamp == "STMS":
        df_ohlcv["timestamp"] = pd.Series([dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ") for dt in df_ohlcv["datetime"]])
    else:
        df_ohlcv["timestamp"] = df_ohlcv["datetime"]

def __get_ohlcv_paged(symbol="XBTUSD", period="1m", count=1000):
    ohlcv_list = []
    utc_now = datetime.now(pytz.utc)
    to_time = int(utc_now.timestamp())
    #to_time = int(time.mktime(utc_now.timetuple()))
    from_time = to_time - ALLOWED_PERIOD[period][2] * 60 * count
    start = from_time
    end = to_time
    if count > 10000:
        end = from_time + ALLOWED_PERIOD[period][2] * 60 * 10000
    while start <= to_time:
        ohlcv_list += __fetch_ohlcv_list(symbol=symbol, period=period, start=start, end=end)
        start = end + ALLOWED_PERIOD[period][2] * 60
        end = start + ALLOWED_PERIOD[period][2] * 60 * 10000
        if end > to_time:
            end = to_time
    df_ohlcv = pd.DataFrame(ohlcv_list,
                            columns=["timestamp", "open", "high", "low", "close", "volume"])
    df_ohlcv["datetime"] = pd.to_datetime(df_ohlcv["timestamp"], unit="s")
    df_ohlcv = df_ohlcv.set_index("datetime")
    df_ohlcv.index = df_ohlcv.index.tz_localize("UTC")
    return df_ohlcv

def __fetch_ohlcv_list(symbol="XBTUSD", period="1m", start=0, end=0):
    param = {"period": ALLOWED_PERIOD[period][2], "from": start, "to": end, "symbol":symbol}
    url = "https://www.bitmex.com/api/udf/history?symbol={symbol}&resolution={period}&from={from}&to={to}".format(**param)
    res = requests.get(url)
    data = res.json()
    return [list(ohlcv) for ohlcv in zip(data["t"], data["o"], data["h"], data["l"], data["c"], data["v"])]


if __name__ == '__main__':
    # データ取得
    dfs = []
    symbols = ["XBTUSD", "ETHUSD",'XRPUSD','LTCUSD', 'ADAUSD', 'DOGEUSD','DOTUSD','BNBUSD']
    for symbol in symbols:
        dfs.append(fetch_ohlcv_df(period="5m", symbol=symbol,  count=1000, reverse=False, partial=True, tstype="DT")['close'].pct_change().rename(f'{symbol}_pctchange'))
    df = pd.concat(dfs,axis=1)
    df.dropna(inplace=True)
    
    # 標準化
    target = (df - df.mean()) / df.std()
    #UMAPでの時限削減
    um = umap.UMAP(n_neighbors=2, min_dist=0.01)
    um_out = um.fit_transform(target.T.values)
    
    # グラフ描画
    palette = sns.color_palette(cc.glasbey, n_colors=24)    
    for i in range(um_out.shape[0]):
        plt.scatter(um_out[:,0][i], um_out[:,1][i], 
                    color=palette[i],  
                    label=symbols[i])
    plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0)
    plt.show()

    # ヒートマップ描画
    sns.heatmap(df.corr(), cmap='Blues')
    plt.show()

出力結果は以下のようになった。 いくつかのクラスタが確認できるが,ヒートマップの結果を見る限りは相関関係とクラスターの構成要素についてはあまり関係がないようだ。

f:id:mcakiyama:20220217042936p:plain
仮想通貨価格時系列のクラスタリング出力

f:id:mcakiyama:20220217043019p:plain
仮想通貨価格時系列のヒートマップ

なかなか面白い結果が得られたと思う。個人的な感想としては以下の通り。

これらの結果は取得するデータの期間,時間足によって異なる可能性がある。それらの傾向を見てみるのも面白いかもしれない。

今後について

以上の結果から収益につながる傾向が得られないかを検討する?

参考文献

  1. 株価時系列に基づく企業クラスタリング
  2. TOPIX(東証株価指数)
  3. APIで取得した株価データをcsvファイルに保存する方法
  4. APIで取得したOHLCVデータから任意の時間足を作成する