Ignite Inspiration

個人的な備忘録多め

睡眠についての調査

書いたきっかけ

興が乗って朝方まで分析をしていた次の日のパフォーマンスが著しく低下したため, 戒めのために夜更かしおよび生活習慣のズレのデメリットをまとめる。 今後長く自分の頭で考えていく知的活動を行っていくにあたり,睡眠習慣を正すメリットを認知することで 長期で見てパフォーマンスを向上させることが目的になる。 文献は一応参照しているが,個人的な感想といった内容といったほうが正しい。

自分の考え

人間の生物学的な進化に対し,科学の進歩は圧倒的にハイスピードで進行してきた。 本来の人間があるべき姿としては野生生物として過ごしていたときの生活が近いものであり, こと睡眠に関しては日の入・日の出に合わせて行うのが本来あるべき姿だろう。

参考文献1によると, 人間は時間の手がかりがない状況では25時間周期の生活リズムになるが, 日光や社会活動によって24時間に同調するようになっているとのこと。 そのため,その要因を排除することにより生活リズムが狂っていくことになる。

今後のアクション

自分の睡眠不足を助長しているのはほぼ確定的にスマートフォンであるため, これを封印すればよいと考える。 - 寝るときには枕元にはスマートフォンを置かずに別の部屋に置くことにする - ベッドにある充電器を移動させる

睡眠不足のデメリット

  • 参考文献1では,睡眠の質が自己効力感(やればできると感じられること)に有意に相関しているとの記載がある。

  • 体内リズムのズレは認知機能への影響がある。 有名なのが,6時間睡眠を2週間続けると2徹,丸二日寝ない状態と同じになるとのこと(参考文献2,3)。

  • 短時間睡眠による死亡リスク増加 国立がん研究センターが睡眠時間と死亡リスクとの関連を調査し論文にまとめている(参考文献4)。 さっくりまとめると,平均睡眠時間は7.4時間,長時間睡眠でも短時間睡眠でも死亡リスクは増加するというものである。

  • 脳の萎縮・認知症など 短時間睡眠者はそうでないものに対し,脳の萎縮スピードが増加し,認知症リスクが高まるとのこと(参考文献5)。

参考文献・サイト

  1. 交代制勤務者の食生活に関する留意点 | e-ヘルスネット(厚生労働省)
  2. The cumulative cost of additional wakefulness: dose-response effects on neurobehavioral functions and sleep physiology from chronic sleep restriction and total sleep deprivation
  3. 6時間睡眠は2日徹夜と同じ?|長時間労働を防ぐ労務管理とは | 人事部から企業成長を応援するメディアHR NOTE
  4. The Association Between Habitual Sleep Duration and Mortality According to Sex and Age: The Japan Public Health Center-based Prospective Study
  5. Sleep Duration and Age-Related Changes in Brain Structure and Cognitive Performance - PMC

感想:タートル流投資の魔術

はじめに

この本は過去のタートルズと呼ばれる投資家育成プログラムで卓越したパフォーマンスをあげた人物のストーリーを語るだけでなく システムトレードによる投資を行うものに対して基本的でありながらも重要な内容を記載しています。少なくとも私はそう思いました。

細かい内容は本を読むことをお勧めします。(Amazonでは中古が1円から出品されています)

概要

この本では,投資家を育成するプログラムにおいて,同じ手法に基づいて投資するように教えられたメンバーの中で パフォーマンスが出たもの,出なかったものの差を説明していました。 手法自体はトレンドフォロー戦略という古典的でありながらも, その手法が人間の認知の性質に基いて利益が出ること,手法の統計的性質などが記載されています。

どういった人におすすめか

もしあなたが初めてシステムトレードを始めようとする場合,この本はその辺のSNSで宣伝されている内容に比べて 示唆に富んだ内容であることは間違いないでしょう。 (ただし,この本に記載されている内容は古く使いされているものが多いため,この本に記載されている内容をそのまま利用したとて利益はでるとは限りません。

具体的な内容についてはざっと以下の通りです。 - 投資に対する人間の「認知のひずみ」の説明 - 認知の歪みによってトレンドフォロー戦略がワークする理由 - 値動きを「予測」せずに利益を得られる理由 - 異なる銘柄間での指標の統一(標準化) - リターン,リスクの定量化 - 資金管理・破産リスク - 最適化の必要性と限界

特に,市場を予測するのではなく,過去の統計的な性質を元にトレードをすること, カーブフィッティングなどに陥らないように単純なロジックを適用し,分散させることがロバストにつながるということを 当時の時代に理解していたことが印象的でした。

個人的な課題として,今後のアクションにつなげたいのは以下の内容でした。 - 各指標やオシレータはトレンドなのか平均回帰なのかを考えて組み合わせること - リターンの安定度≠リスクであること。ネガティブスキューとなっていて,無視できない未知のリスクが存在していないか - 手法を比較する評価指標は投資期間やサンプルに含まれる外れ値によって影響されずらいものを使用すること - 母集団から選んだサンプルが将来の分布を代表していないと,統計値は役に立たないことを理解すること

以上になります。

【Ubuntu18.04】lightGBMをGPUで処理させて速度を比較した

概要

タイトルの通りでlightGBMをGPU経由で動作させる方法を自分の備忘録のためにまとめました。 ビルドしてインストールする方法は弊環境ではうまくいかなったため、pip経由でインストールする方法になります。

最後にまとめておりますが、lightGBMをGPUで動作させた場合、処理時間を25%短縮することができました。 XGBoostやcatboostなど他のGBDT系のライブラリに比べるとGPU化の恩恵は少ないですが、25%の短縮は大きいと思います。

環境構築

実行環境

環境は以下の通り。 - OS: Ubuntu 18.04.6 LTS - CPU: AMD Ryzen 9 5950X - GPU: RTX2070

NVIDIAドライバのインストール

以下のコマンドでNVIDIAのドライバをapt install経由でダウンロードできるようにします。 ppa追加時にENTERを押します。

$ sudo add-apt-repository ppa:graphics-drivers/ppa 
$ sudo apt update 

以下のコマンドでダウンロードできるドライバの一覧を確認します。 今回は推奨(recommended)されているnvidia-driver-470をインストールすることにしました。

$ ubuntu-drivers devices

WARNING:root:_pkg_get_support nvidia-driver-515: package has invalid Support PBheader, cannot determine support level
WARNING:root:_pkg_get_support nvidia-driver-510: package has invalid Support PBheader, cannot determine support level
== /sys/devices/pci0000:00/0000:00:03.1/0000:08:00.0 ==
modalias : pci:v000010DEd00001F02sv00001462sd00003734bc03sc00i00
vendor   : NVIDIA Corporation
driver   : nvidia-driver-515 - third-party non-free
driver   : nvidia-driver-470 - third-party non-free recommended
driver   : nvidia-driver-510 - third-party non-free
driver   : xserver-xorg-video-nouveau - distro free builtin

以下のコマンドでドライバをインストールし.再起動します。

$ sudo apt install nvidia-drivers-470
$ sudo reboot

lightGBM(GPU版)のインストール

再起動した後,再度ターミナルを開いて必要なパッケージをインストールしていきます。 以下のコマンドを入力します。

$ sudo apt install --no-install-recommends cmake build-essential clinfo opencl-headers libboost-all-dev

今回,python3.7&仮想環境で動作確認をしたいため,以下のコマンドを入力してpython3.7をインストールしておきます。 また,インストールした後に仮想環境を作って立ち上げておきます。 不要な方は、以下のコマンドは飛ばしてください。

$ sudo apt install python3.7 python3.7-venv
$ python3.7 -m venv venv
$ source ./venv/bin/activate

以下のコマンドで必要なpythonパッケージをインストールします。

$ pip install -U pip setuptools wheel
$ pip install numpy scipy scikit-learn 

ここまできて,ようやくGPU版のlightGBMをインストールすることができます。 以下のコマンドを入力してインストールします。

$ pip install lightgbm --install-option=--gpu --install-option="--opencl-library=/usr/lib/x86_64-linux-gnu/libOpenCL.so.1"

以上でGPUに対応したlightGBMをインストールすることができました。

GPU認識確認

システム上からGPUが認識されているかを確認します。 GPUの認識にcatboostを使うので、追加でインストールしておきます。

$ pip install catboost

以下のコマンドを入力します。。 GPUが認識されていれば「1」が表示されます。1が表示されない場合は再起動をしてみてください。

$ python -c "from catboost.utils import get_gpu_device_count; print(get_gpu_device_count())"

動作確認

実際にCPUを利用した場合とGPUで動作させた場合で比較をしてみました。 ソースは以下のサイトのものを参考にさせていただきました。この場を借りてお礼を申し上げます。

lightGBM CPU動作

CPU版は以下のソース参照。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
import time
import logging
from contextlib import contextmanager

import lightgbm as lgb
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import log_loss


LOGGER = logging.getLogger(__name__)


@contextmanager
def timeit():
    """処理にかかった時間を計測してログに出力するコンテキストマネージャ"""
    start = time.time()
    yield
    end = time.time()
    elapsed = end - start
    LOGGER.info(f'Elapsed Time: {elapsed:.2f} sec')


def main():
    logging.basicConfig(level=logging.INFO,
                        stream=sys.stderr,
                        )

    # 疑似的な教師信号を作るためのパラメータ
    dist_args = {
        # データ点数
        'n_samples': 100_000,
        # 次元数
        'n_features': 1_000,
        # その中で意味のあるもの
        'n_informative': 100,
        # 重複や繰り返しはなし
        'n_redundant': 0,
        'n_repeated': 0,
        # タスクの難易度
        'class_sep': 0.65,
        # 二値分類問題
        'n_classes': 2,
        # 生成に用いる乱数
        'random_state': 42,
        # 特徴の順序をシャッフルしない (先頭の次元が informative になる)
        'shuffle': False,
    }
    # 教師データを作る
    train_x, train_y = make_classification(**dist_args)
    # データセットを学習用と検証用に分割する
    x_tr, x_val, y_tr, y_val = train_test_split(train_x, train_y,
                                                test_size=0.3,
                                                shuffle=True,
                                                random_state=42,
                                                stratify=train_y)
    # CatBoost が扱うデータセットの形式に直す
    train_pool = lgb.Dataset(x_tr, label=y_tr)
    valid_pool = lgb.Dataset(x_val, label=y_val)
    # 学習用のパラメータ
    params = {
        # タスク設定と損失関数
        'objective': 'binary',
        # 学習率
        'learning_rate': 0.02,
        # 学習ラウンド数
        'num_boost_round': 5_000,
        # 検証用データの損失が既定ラウンド数減らなかったら学習を打ち切る
        # NOTE: ラウンド数を揃えたいので今回は使わない
        # 'early_stopping_rounds': 100,
        # 乱数シード
        'random_state': 42,
        # 学習に GPU を使う場合
        # 'device': 'gpu',
    }
    # モデルを学習する
    with timeit():
        model = lgb.train(params, 
                          train_pool,
                          valid_sets=[valid_pool], 
                          verbose_eval=100
                          )

    # 検証用データを分類する
    y_pred = model.predict(x_val)
    # ロジスティック損失を確認する
    metric = log_loss(y_val, y_pred)
    LOGGER.info(f'Validation Metric: {metric}')


if __name__ == '__main__':
    main()

lightGBM GPU動作

GPU版は以下の通り。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
import time
import logging
from contextlib import contextmanager

import lightgbm as lgb
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import log_loss


LOGGER = logging.getLogger(__name__)


@contextmanager
def timeit():
    """処理にかかった時間を計測してログに出力するコンテキストマネージャ"""
    start = time.time()
    yield
    end = time.time()
    elapsed = end - start
    LOGGER.info(f'Elapsed Time: {elapsed:.2f} sec')


def main():
    logging.basicConfig(level=logging.INFO,
                        stream=sys.stderr,
                        )

    # 疑似的な教師信号を作るためのパラメータ
    dist_args = {
        # データ点数
        'n_samples': 100_000,
        # 次元数
        'n_features': 1_000,
        # その中で意味のあるもの
        'n_informative': 100,
        # 重複や繰り返しはなし
        'n_redundant': 0,
        'n_repeated': 0,
        # タスクの難易度
        'class_sep': 0.65,
        # 二値分類問題
        'n_classes': 2,
        # 生成に用いる乱数
        'random_state': 42,
        # 特徴の順序をシャッフルしない (先頭の次元が informative になる)
        'shuffle': False,
    }
    # 教師データを作る
    train_x, train_y = make_classification(**dist_args)
    # データセットを学習用と検証用に分割する
    x_tr, x_val, y_tr, y_val = train_test_split(train_x, train_y,
                                                test_size=0.3,
                                                shuffle=True,
                                                random_state=42,
                                                stratify=train_y)
    # CatBoost が扱うデータセットの形式に直す
    train_pool = lgb.Dataset(x_tr, label=y_tr)
    valid_pool = lgb.Dataset(x_val, label=y_val)
    # 学習用のパラメータ
    params = {
        # タスク設定と損失関数
        'objective': 'binary',
        # 学習率
        'learning_rate': 0.02,
        # 学習ラウンド数
        'num_boost_round': 5_000,
        # 検証用データの損失が既定ラウンド数減らなかったら学習を打ち切る
        # NOTE: ラウンド数を揃えたいので今回は使わない
        # 'early_stopping_rounds': 100,
        # 乱数シード
        'random_state': 42,
        # 学習に GPU を使う場合
        'device': 'gpu',
    }
    # モデルを学習する
    #model = lgb.train(params, train_pool,valid_sets=[valid_pool])
    with timeit():
        model = lgb.train(params, 
                          train_pool,
                          valid_sets=[valid_pool], 
                          verbose_eval=100
                          )
    model.save_model('model_gpu.txt'
                     #, num_iteration=model.best_iteration
                     )
        #model.fit(train_pool,
        #          eval_set=valid_pool,
        #          verbose_eval=100,
        #          use_best_model=True,
        #          )
    # 検証用データを分類する
    y_pred = model.predict(x_val)
    # ロジスティック損失を確認する
    metric = log_loss(y_val, y_pred)
    LOGGER.info(f'Validation Metric: {metric}')


if __name__ == '__main__':
    main()

実行時間の比較

それぞれ実行すると、以下のような結果となりました。

time log loss
CPU 203.67 sec 0.114378
GPU 154.56 sec 0.113432
# CPU
[4900] valid_0's binary_logloss: 0.116022
[5000]    valid_0's binary_logloss: 0.114378
INFO:__main__:Elapsed Time: 203.67 sec
INFO:__main__:Validation Metric: 0.11437848050873242
# GPU
[4900] valid_0's binary_logloss: 0.115171
[5000]    valid_0's binary_logloss: 0.113433
INFO:__main__:Elapsed Time: 154.56 sec
INFO:__main__:Validation Metric: 0.11343276824400272

まとめ

多少調べたことがある方は解ると思いますが、lightGBMはツリー系のなかでもあまりGPU処理の恩恵が得られないというライブラリです。タスクによってはCPUとあまり差異がなかったり、他のGBDT系ライブラリのほうが高速に動作します。 (この論文このページを参照)

弊環境・今回のタスクではDNNのような劇的な高速化は得られませんでしたが、およそ25%の処理スピードアップとなりました。

lightGBMはあまりGPUの利用率が高くないため、高価で高性能なGPUを利用しても処理速度の向上には寄与しないと考えれられます。 一方で、弊環境のAMD Ryzen9 5950Xの16コアCPUよりGPUを利用したほうが高速で処理させることができたため、コア数の少ないCPUを利用している場合は、GPU利用の恩恵があるかもしれません。

少しでもトレーニング時間の短縮を目指す方はぜひ利用してみてください!

参考文献

  1. LightGBM ~GPU Tuning Guide and Performance Comparison

  2. Python: CatBoost を GPU で学習させる

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

モチベーション

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

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

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

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データから任意の時間足を作成する