仮想通貨の銘柄ごとの値動きをクラスタリングしてみた話
モチベーション
株価時系列に基づく企業クラスタリングという論文があることを知ったので,それを仮想通貨に適用してなにかしらの気づきを得られないかを調査した。
株価時系列の企業クラスタリング
まず,論文と同じような結果となるかを確認した。
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()
出力された結果を見てみると,元論文の結果と概ね一致していることが確認できる。一部,クラスタリングの構成要素が変化しているが,これは元論文との株価時系列の取得期間の違いによるものと考えられる。
仮想通貨価格時系列のクラスタリング
仮想通貨のクラスタリングについては以下でそれなりの長さの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()
出力結果は以下のようになった。 いくつかのクラスタが確認できるが,ヒートマップの結果を見る限りは相関関係とクラスターの構成要素についてはあまり関係がないようだ。
なかなか面白い結果が得られたと思う。個人的な感想としては以下の通り。
これらの結果は取得するデータの期間,時間足によって異なる可能性がある。それらの傾向を見てみるのも面白いかもしれない。
今後について
以上の結果から収益につながる傾向が得られないかを検討する?