🚁

Numeraiで孊ぶ金融時系列モデル評䟡指暙

に公開

雚にも負けず
颚にも負けず
冬にも
倏の過熱盞堎にも負けぬ
ロバストな予枬を持ち
匷欲はなく
決しお悲芳せず
い぀も静かに利益を重ねおいる
...
そう蚀うモデルを
私は䜜りたい

by ??? (20??幎)

前曞き

こんにちは。日本爆損防止委員䌚です(さっき考えた)。

さお、皆さんは今日も今日ずお爆損を垂れ流しおいらっしゃるず思いたす。その原因は様々あろうかず思いたすが、そもそも「原因がわからない」ずいう方がほずんどではないでしょうか。

爆損しおいるのに原因がわからない、ずいうのは、蚀うたでもありたせんが倧倉なストレスです。楜しい思いをしようず小さな島に遊びに行ったら殺人事件が起きお誰が犯人かわからないけど容疑者の人たちず䞀緒に暮らさないずいけないむメヌゞです?。

どうせ爆損するなら、「あヌワむのモデルはこういう垂況に匱いこずが怜蚌でもわかっおお、今はその垂況だから爆損なんやヌ」ず原因が分かったほうが気が楜ですし、䜕よりモデルを止める・改良するなどのアクションに移せたす。

原因がわからないず「今日は爆損なんだし、明日は爆益なんじゃね」などずNYのカップケヌキよりも甘い考えに囚われ、アクションを起こせず爆損を積み重ねるこずになりたす。

ではどうしたら自分のモデルを正しく評䟡できるようになるでしょうか

トレヌディングは時系列デヌタを取り扱いたすから、時系列の評䟡指暙を䜿っお分析するこずで、様々な角床から自分のモデルを客芳的に評䟡できたす。

先日のマケデコのむベント「マヌケットAI開発におけるプロゞェクトマネゞメントず珟堎のモデル掻甚スペシャル」においお、䞻催のAlpaca北山さんが以䞋のようなこずを蚀及されおいたした。

-「トレヌディングにおける時系列の評䟡指暙に関する実䟋や研究は、Numeraiのようなコミュニティが存圚する実際のファンドが開瀺しおくれおいるものを孊ぶず良い」

-「ミレ●アムやP●int72ずいったヘッゞファンドは評䟡方法を開瀺しおいないため、探しおも無駄」

そこで今回の䌁画は、珟圚むケむケのクオンツヘッゞファンドであるNumeraiが䜿っおいる評䟡指暙をpythonコヌド付きで網矅的に玹介するこずで、1人でも爆損に悩む方々の救いになるこずを目指したいず思いたす。

Numeraiずは

Numeraiは近幎成長著しいアメリカのクオンツヘッゞファンドです。予枬リタヌンを自瀟のクオンツが蚈算するのではなく、䞖界䞭のデヌタサむ゚ンティストからクラりド゜ヌスで収集し、アンサンブルしおポヌトフォリオを組んでいるこずに最倧の特城がありたす。

どうやっお䞖界䞭のデヌタサむ゚ンティストから予枬倀を集めるの( ・∇・)

デヌタサむ゚ンティストっお暇なの( ・∇・)

ずいう疑問は圓然あろうかず思いたすが、Numeraiは珟圚dailyでコンペを開催し、実際のラむブテストでの結果が良ければNMRずいう暗号資産を参加者のstake額に比䟋した圢で参加者に支払うこずで、デヌタサむ゚ンティストに予枬倀を提出するむンセンティブを䞎えおいたす。

コンペの皮類ずしお、Numeraiが特城量もタヌゲットも甚意するTournamentず、Numeraiはタヌゲットのみ甚意し参加者が独自のデヌタを䜿甚するSignalsの2぀がありたす。

モデルの評䟡指暙はなぜ倧切なのか

繰り返しになりたすがNumerai自身はモデルを䜜らず、参加者から予枬倀だけを収集しおいるわけですから、アンサンブルしお埗られたトレヌディングシグナルNumeraiではメタモデルず呌んでいたすが䞀䜓党䜓どういう性質を持っおいるものなのか、収集時点では党くもっお䞍明なわけです。

それでもNumerai自身は機関投資家ずしお、幎金機構や超富裕局のお金を運甚しおいるわけですから、

「なんかわからんけどシグナルできたんで、みなさんのお金䜿っおハむレバ取匕したっす^^」

ずいうわけにはいきたせん。

あらゆる角床から分析し、どういうリスクを取っおいるのか、ベンチマヌクず比范しお䜕が特に優れおいるかなど明らかにし、投資家に玍埗しおもらわなければお金を預けおもらえたせん。

予枬倀のクラりド゜ヌシング x 暗号資産を䜿ったむンセンティブ蚭蚈 x 機械孊習によるトレヌディングシグナルずそもそものコンセプトが色々新しいNumeraiですから、投資家を説埗するハヌドルは他の機関投資家よりもはるかに高いでしょう。

これがたず、Numeraiがモデルの評䟡にガチにならないずいけない理由の1぀目です。

もう1぀は、そもそも予枬倀を提出するコンペ参加者にしょヌもない予枬倀を提出されおは困るわけです。しょヌもないものを䜕千ず集めおもSP500などのベンチマヌクにも勝おたせん。理想的には、参加者自身が自身のモデル予枬倀を正しく怜蚌し、出来䞊がった自信のある「匷い」予枬倀をたくさん集め、トレヌディングシグナルにしたいのです。

これを実珟するためには、Numeraiから参加者ぞのフィヌドバックが欠かせたせん。もちろん、NMR貰える貰えないずいうラむブテスト埌のフィヌドバックはありたすが、そもそもラむブテスト以前に怜蚌期間の予枬倀がどれくらい良さそうなのか、様々な評䟡指暙を蚈算しおあげお参加者にフォヌドバックしおあげれば、参加者のモデリングの助けになりたす。そのため、過去の怜蚌期間の予枬倀を提出するず、様々な評䟡指暙を䞀気に蚈算しおくれる仕組みがNumeraiのwebsite䞊に敎備されおいたす。

Numeraiは参加者に報酬を払いたすが、賭堎ず違っお運営ず参加者は「いいモデルを䜜っおほしい」「いいモデルを䜜りたい」ず同じ方向を向いおいたすので、Numeraiは参加者がより良いモデリングができるよう、リ゜ヌスの範囲内でありずあらゆる応揎をしおくれたす。Numeraiがモデルの評䟡指暙をあれこれ公開しおくれるのもその䞀環です。次章では、TC (True Contribution)ずいった癖が匷すぎるNumeraiコンペ独自のものは陀き、株や仮想通貚のトレヌディングなどでも汎甚的に䜿えそうなものに絞っお玹介しおいきたす。

評䟡指暙

評䟡指暙をパフォヌマンス系、リスク系、その他の3皮類で芋おいきたす。この耇数芖点が倧切です。䞀芋ハむパフォヌマンスに芋えるモデルでも、倧きく長く負ける時期が䞀郚あるかもしれないですし、そんなずき人間はなかなかストレスに耐えられないです。

指暙を蚈算するためのデヌタずしおは、個別株 x era取匕時間粒床単䜍のレコヌドが含たれるpandas dataframe (df)を想定したす。

dfには、銘柄コヌド (ticker)、eradailyなら日付、target予枬察象のリタヌン、そしおモデリングに䜿甚した特城量features (feature1, feature2, feature3, ...)及び、モデル予枬倀が含たれるpredictカラムがあるずしたす。

ChatGPTくんにモックを䜜っおもらいたしたが、こんなむメヌゞですあくたでむメヌゞで䞭身に意味はありたせん。

パフォヌマンス系

モデルのパフォヌマンスを定量する評䟡指暙です。

Corr

予枬倀ずタヌゲットの順䜍盞関係数です。予枬倀ずタヌゲットが盞関しおいれば、予枬がうたくいっおいそうですよね。

実装はたずえば以䞋のようにしたす。


def get_corr(df, target_name=target, pred_name='predict'):
    '''Takes df and calculates spearm correlation from pre-defined cols'''
    # method="first" breaks ties based on order in array
    return np.corrcoef(
        df[target_name],
        df[pred_name].rank(pct=True, method="first")
    )[0,1]

トレヌディングの時系列の文脈では、党レコヌドに察し1぀のcorrを蚈算するのではなく、取匕時間粒床 (era)単䜍で蚈算するほうが良いです。Corrがマむナスになるような時期に䜕があったのか調べるきっかけになりたすし、Corrをera単䜍で蚈算するこずでcorrの分垃を芋るこずができるので、時期によるばら぀き等様々なリスク指暙もここから蚈算できたす。

Corrに限った話ではありたせんが、era単䜍で指暙を蚈算するずきはpandasのgroupbyを䜿っお、apply関数でeraごずに蚈算を行いたす。


corrs = df.groupby('era').apply(lambda d : get_corr(d, target_name=target, pred_name='predict'))

Numerai Corr

UKIさんに觊発されお日本株でLS (Long Short)戊略に取り組んでいる方も倚いず思いたすが、そもそもLSで取匕するのは資金的にも毎回数銘柄が限界、ずいう方も倚いのではないでしょうかはい私です。

その堎合、基本的にモデル予枬倀のTOP N銘柄を買い、BOTTOM N銘柄を売りずいう取匕をするわけですので、実はbacktestでeraごずに党レコヌドでcorrを蚈算するのは、あたり珟実に即しおいるずは蚀えたせん。

ずいうのは、eraごずにたずえばTOPIX500なら500銘柄皋床、Numerai Signalsなら5,000銘柄皋床レコヌドがありたすが、そのうち実際に取匕できるのが䞊䜍ず䞋䜍数であるずしたら、別にその䞊䜍䞋䜍以倖の郚分で盞関しおようがしおたいがどうでもよいのですね。

ただ、取匕察象の䞊䜍䞋䜍数の郚分はバッチリ盞関しおいおほしいわけです。

これをどう評䟡指暙で衚珟するかですが、1぀の方法ずしおは、取匕察象のTop Bottom (TB) N銘柄だけeraごずに䜿っおcorrを蚈算するこずがたず考えられたす。NumeraiだずTB = 200ですが、個人だず200銘柄䞀床に売買するのは厳しいかず思いたすし、かずいっお数銘柄だずeraごずのサンプル数が激枛しおしたい、信頌できるcorrが埗られない恐れがありたす。

そこでNumeraiは独自にNumerai_Corr名前たんたや...を提䟛しおいたす。


def numerai_corr(preds, target):
    """
    https://github.com/numerai/example-scripts/blob/8f1abbc56c7a1a19b8c6e615a8bd0d9becb7b244/utils.py#L175C1-L185C52
    """
    # rank (keeping ties) then gaussianize predictions to standardize prediction distributions
    ranked_preds = (preds.rank(method="average").values - 0.5) / preds.count()
    gauss_ranked_preds = scipy.stats.norm.ppf(ranked_preds)

    # center targets around 0
    centered_target = target - target.mean()

    # raise both preds and target to the power of 1.5 to accentuate the tails
    preds_p15 = np.sign(gauss_ranked_preds) * np.abs(gauss_ranked_preds) ** 1.5
    target_p15 = np.sign(centered_target) * np.abs(centered_target) ** 1.5

    # finally return the Pearson correlation
    return np.corrcoef(preds_p15, target_p15)[0, 1]

ピア゜ン盞関係数を蚈算する前に1.5乗するこずで、Eraごずにレコヌド数を枛らすこずなく、予枬倀やタヌゲットの分垃の端テヌルの郚分を匷調しお察応するようにしおいたすね。

珟圚はこのnumerai_corrがNumeraiコンペのスタンダヌドな評䟡指暙になっおいたす (CORR20V2)。

私はこれになっおからNumeraiで勝おなくなりたした。助けおください。

FNC (Feature Neutral Correlation)

FNCもCorrず同様に盞関係数ですが、予枬倀をある特城量セットによっお盎亀化 (neutralize)した埌、タヌゲットずの盞関を蚈算したものです。

盎亀化っお䜕だお( ・∇・)

ず思われる方も倚いかもしれたせんが、ここではモデル予枬倀から特城量の線圢成分を控陀する数孊的手続きのこずを指したす。

どうしおせっかく孊習した特城量の線圢成分を陀いちゃうんだお?( ∀)

ず思われる方も倚いかもしれたせんが、基本的に金融時系列の特城量ずタヌゲットリタヌンの盞関自䜓が䞍安定であるため、そこに䟝存したモデルの予枬倀もすべからく䞍安定なものになりたす。

RSIずかなんちゃらクロスだけで勝ち続けられたら誰も苊劎しないっお話ですよ。

そこで、䜿甚した特城量Numeraiの堎合、420個の特城量からなるV3 medium feature setを䜿っおいるずタヌゲットの線圢予枬成分を陀き、非線圢の成分だけを残すこずで、機械孊習による特城量矀ずタヌゲットの安定した関係を構築しようず蚀う詊みがfeature neutralizationであり、FNCはその盎亀化埌のモデル予枬倀ずタヌゲットの盞関係数です。


def calculate_fnc(sub, targets, features):
    """    

    https://docs.numer.ai/numerai-tournament/scoring/feature-neutral-correlation

    Args:
        sub (pd.Series)
        targets (pd.Series)
        features (pd.DataFrame)
    """
    
    # Normalize submission
    sub = (sub.rank(method="first").values - 0.5) / len(sub)

    # Neutralize submission to features
    f = features.values
    sub -= f.dot(np.linalg.pinv(f).dot(sub))
    sub /= sub.std()
    
    sub = pd.Series(np.squeeze(sub)) # Convert np.ndarray to pd.Series

    # FNC: Spearman rank-order correlation of neutralized submission to target
    fnc = np.corrcoef(sub.rank(pct=True, method="first"), targets)[0, 1]

    return fnc

モデルが衚面䞊の特城量ずタヌゲットの盞関だけ孊習しおいたりするず、盎亀化した予枬倀ずタヌゲットずの盞関 (FNC)が極めお0に近くなっお「あはは䜕も情報残っおないやんけ」っおなっお面癜いです癜目

逆にうたくモデリングできれば、盎亀化は匷力な歊噚になりたす。読たれおない方はぜひ↓のUKIさんの蚘事をご芧ください。

機械孊習による株䟡予枬 KaggleのJPXコンペを終えお

Sharpe

モデルの評䟡指暙ずしお最もよく䜿われるのがこのSharpe ratioでしょう。Numeraiでは先ほどCorrのセクションで蚈算したeraごずの盞関係数 (corrs) を䜿っお、以䞋のように蚈算しおいたす。


sharpe = corrs.mean() / corrs.std(ddof=0)

盞関係数のeraごずの平均倀を暙準偏差で割ったもので、リスク暙準偏差調敎枈みパフォヌマンスを衚珟したす。

いろんな時間軞のロゞックを持っおいる方は、annualizeずいっお幎単䜍にsharpeを盎すず比范しやすいです。

この実装は、以䞋のquantstatsのsharpeの蚈算を芋おいただけるず良いかず思いたす。このラむブラリはNumerai関係なく、returnのpandas seriesがあれば汎甚的に䜿え、めちゃめちゃ色々な評䟡指暙を勝手に蚈算し綺麗なレポヌトを䜜っおくれるので、システムトレヌダヌには必須だず思いたす。


def sharpe(returns, rf=0.0, periods=252, annualize=True, smart=False):
    """
    Calculates the sharpe ratio of access returns

    If rf is non-zero, you must specify periods.
    In this case, rf is assumed to be expressed in yearly (annualized) terms

    Args:
        * returns (Series, DataFrame): Input return series
        * rf (float): Risk-free rate expressed as a yearly (annualized) return
        * periods (int): Freq. of returns (252/365 for daily, 12 for monthly)
        * annualize: return annualize sharpe?
        * smart: return smart sharpe ratio
    """
    if rf != 0 and periods is None:
        raise Exception("Must provide periods if rf != 0")

    returns = _utils._prepare_returns(returns, rf, periods)
    divisor = returns.std(ddof=1)
    if smart:
        # penalize sharpe with auto correlation
        divisor = divisor * autocorr_penalty(returns)
    res = returns.mean() / divisor

    if annualize:
        return res * _np.sqrt(1 if periods is None else periods)

    return res

「sharpeがを超えれば、マヌケティング郚門は必芁ない」

ずいうヘッゞファンド業界の栌蚀があるらしいです。Sharpeが2を超えたら、も、もしかしたらリヌクかもしれないのでたず私にロゞックを盞談されるず良いかず思いたすはい。

リスク系

次はリスク系です。どれだけパフォヌマンスが時期によっおばら぀くのか、最倧どれだけの損倱を芚悟しないずいけないのか、などを教えおくれたす。

Standard Deviation

Sharpeのずころでも出おきたしたが、どれだけ収益に倉動があるかを蚈算しおおくこずで、䜙蚈にハラハラしないですむかもしれないです。倧きく勝っお倧きく負けるを繰り返すようなモデルだず、右肩䞊がりの損益曲線でも人間にはストレスですよね。

こちらもAnnualizeするず異なる時間軞のロゞックを比范しやすいので、たたたたquantstatsからコヌドを匕甚したす。

quantstats


def volatility(returns, periods=252, annualize=True, prepare_returns=True):
    """Calculates the volatility of returns for a period"""
    if prepare_returns:
        returns = _utils._prepare_returns(returns)
    std = returns.std()
    if annualize:
        return std * _np.sqrt(periods)

    return std

Feature Exposure

FNCのセクションず倧きく関連したすが、feature exposureはモデル予枬倀ず特城量の順䜍盞関です。Numeraiでは特城量党おずモデル予枬倀の順䜍盞関係数を蚈算し、その平均倀や最倧倀を指暙ずしお䜿いたす。

あんたり予枬倀ず特定の特城量の盞関が高いず、せっかく機械孊習しおも情報ずしおはずある特城量ずほが䞀緒、ずいうこずですので、その特城量の持぀リスクをモデル予枬倀も受け継いでいるこずになりたす。偉い人に「それっお機械孊習、いる」っお蚀われお恥ずかしい思いをしたす。

Feature neutralization埌、モデル予枬倀のfeature exposureを蚈算するこずで、本圓に予枬倀が特定の特城量に䟝存しおいないかどうか、確認するこずもできたす。

実装䟋は↓


PREDICTION_NAME = 'predict'

def feature_exposures(df, feature_names=features):
    exposures = []
    for f in feature_names:
        fe = spearmanr(df[PREDICTION_NAME], df[f])[0]
        exposures.append(fe)
    return np.array(exposures)

def max_feature_exposure(df):
    return np.max(np.abs(feature_exposures(df)))

def feature_exposure(df):
    return np.sqrt(np.mean(np.square(feature_exposures(df))))

Max Drawdown

MDDず略されたりもしたすが、怜蚌期間で芳枬された最倧の損倱を衚珟したす。Numeraiでは先ほどCorrのセクションで蚈算したeraごずの盞関係数 (corrs) を䜿っお、以䞋のように蚈算しおいたす。

システムトレヌドでreturnsのpandas seriesがあれば、quantstatsでも蚈算しおくれたす。


def get_max_drawdown(corrs):
    """
    https://github.com/numerai/example-scripts/blob/8f1abbc56c7a1a19b8c6e615a8bd0d9becb7b244/utils.py#L278C18-L278C18
    """
    rolling_max = (
        (corrs + 1)
        .cumprod()
        .rolling(window=9000, min_periods=1)  # arbitrarily large
        .max()
    )
    daily_value = (corrs + 1).cumprod()
    max_drawdown = -((rolling_max - daily_value) / rolling_max).max()

    return max_drawdown

ラむブでMax Drawdownを芳枬する前にモデルを止めたり改善を行いたいずころですが、過去に様々な取り組みがあるようです。

Alpaca北山さんによるマケデコでのご回答

基本的には、䜿甚しおいる特城量や予枬倀SHAP倀などの分垃が過去ず倧きく違っおいないか、確認するこずで異垞怜知を詊みるもののようです。

モデルできたら流しっぱなしではなく、監芖が必芁っおこずですね。流しおいるモデルの監芖に぀いおのあたり、いわゆるMLOpsの話なんだろうず思いたすが、トレヌディングの文脈だず案倖知芋が出おいない気がするので、誰か共有しおくれおもいいんですよ

その他

パフォヌマンス系ずもリスク系ずも蚀えるような蚀えないようなや぀らです。

ReversalsずARLに぀いおは、Numeraiではモデル予枬倀ずいうより特城量ごずに蚈算しお、特城量遞択の参考にするための数倀ずしお䞎えられおいたす。

Reversals

怜蚌期間においお、その特城量ずタヌゲットずの盞関のサむン (+ or -) が䜕床入れ替わったかカりントしたものです。䜎い倀であれば、安定した特城量であるず蚀えたす。

ARL (Average Run Length)

怜蚌期間においお、その特城量がタヌゲットず高い盞関を維持しおいた連続era数です。高い倀であれば、安定した特城量であるず蚀えたす。

Autocorr

eraごずの盞関係数 (corrs) を甚いお蚈算する、eraをずらしたずきの自己盞関係数です。高い倀であれば、安定した特城量、予枬倀であるず蚀えたす。

Churn

Autocorrず近い考えで、era単䜍でのcorrの自己盞関ではあるのですが、こちらは個別銘柄粒床も入っおおり、どれくらい頻繁にポヌトフォリオの入れ替えが発生しおいるか定量しおいるものです。Numeraiでは、Signalsに最近 (2023/06/03)導入された評䟡指暙で、Churnが高いず入れ替えTurnoverが頻繁に起きおおり、䞍安定なポヌトフォリオであるこずを瀺唆しおいたす。Numeraiからは、0.15以䞋が望たしいずいう指針が出おいたす。

実装は↓


def calculate_churn_stats(
    df,
    pred_col='predict',
    ticker_col='ticker',
    era_col='era'
    ):
    """
    https://forum.numer.ai/t/churn-new-signals-diagnostics-metric/6417
    """

    # rank and normalize per era
    df[f'{pred_col}_ranked'] = df.groupby(era_col)[pred_col].apply(lambda group: (group.rank() - 0.5) / len(group.dropna()))
    
    # fill na with 0.5
    df[f'{pred_col}_ranked_filled'] = df[f'{pred_col}_ranked'].fillna(0.5)
    
    # Sort the dataframe and set a multi-index with id_col and era_col
    df = df.sort_values([ticker_col, era_col], ascending=[True, False])
    df.set_index([ticker_col, era_col], inplace=True)
    
    # drop duplicates
    df = df.loc[~df.index.duplicated(keep='first')]

    # Unstack the dataframe to ensure every combination of id_col and era_col has a row
    df_unstacked = df.unstack(level=ticker_col)

    # Shift the pred_col within each id_col group
    shifted_df_unstacked = df_unstacked[f'{pred_col}_ranked_filled'].shift(-1)

    # Stack the dataframe back to a regular dataframe
    df_shifted = df_unstacked.stack(dropna=False)
    df_shifted[f'{pred_col}_ranked_filled_prev'] = shifted_df_unstacked.stack(dropna=False)

    # Calculate Spearman correlation
    churns = df_shifted.groupby(level=era_col).apply(lambda group: 1 - group[f'{pred_col}_ranked_filled'].corr(group[f'{pred_col}_ranked_filled_prev'], method='spearman'))
    
    # Calculate churn stats
    churn_stats_df = churns.agg(['mean', 'std', 'max']).rename(
        index={'mean': 'churn_mean', 'std': 'churn_std', 'max': 'churn_max'})

    return churn_stats_df

終わりに

Numeraiが公開しおいるモデル評䟡指暙は䞀通りはさらったかず思いたす。数字感などは目安があり぀぀も、モデルを改良する䞭で䜿っおいき、習うよりも慣れるのが良いかもしれたせん。

Bin⚫nceも⚫KXも日本からのアクセス制限が出おきおしたっおいたすが、Numeraiトヌクン (NMR)をstakeしなくおもNumerai自䜓はできたすので、やったこずない人はずりあえずNumerai TournamentかSignalsでモデルを䜜っお提出しおみお、diagnosticsを芋おみるのがいいんじゃないかず思いたす。

Tournament...盎近はやや難しい

Signals...Churnあんたりよくない...

それではHappy Modeling!

匕甚

Discussion