見出し画像

Quantopian Contest に参加してみる

こんにちは。Quantopian Tokyo User Groupの主催の shinseitaro です。

Quantopianのコンテストに応募してみようと思い、今回はその過程をまとめた note を書きました。

Quantopianのコンテストに参加するためには、Cross-Sectional Equity Strategy であるアルゴリズムを提出することが求められます。Cross-Sectional Equity Strategyとは、将来の値上がりを予測できそうな指標(アルファ)を株価分析して探し出し、アルファが高い銘柄群をロング、低い銘柄群をショートするというストラテジーです。

( https://www.quantopian.com/lectures/long-short-equity より。一部編集。)

つまり、このAlphaを見つけて順位をつけるがコンテストのキモということになります。

またこのアルファ戦略以外にも、以下の条件を満たしたAlgorithmでなくては応募できません。

Positive returns.
Leverage between 0.8x-1.1x.
Low position concentration.
Low beta-to-SPY.
Mid-range turnover.
Long and short positions.
Trades liquid stocks.
Low exposure to sector risk.
Low exposure to style risk.
Place orders with Optimize API.

今回は初めて参加しますので、まずはそれなりのAlphaを見つけ、上記の条件をクリアして、参加できるアルゴリズムを作ること、を目標にします。上位に入る!なんて後回しです。

(ところで、Quantopianは良いアルゴリズムを彼らのFundに組み込むという事業もやっています。組み込まれるようなアルゴリズムを作れる人が羨ましいです。)

それなりのAlphaを見つける

Quantopianには、バックテストを実行する Algorithm と データを使ってストラテジーを考える Research という2つの機能があります。

分析するためのライブラリーも豊富に用意してあります。例えば、Alphalens。これは 先程説明した、Alpha を見つけることに特化したライブラリです。

Alphaを見つけるためにユーザーは様々な指標(Factor)をテストします。

将来の収益と、Factorの関係性を調べ、どのくらいPredictable(予測可能な)ファクターであるかを調べて、Alpha として使えるかどうか分析するのです。

Alphalens の ソースはGithubに公開されています。

移動平均からの乖離

今回はコンテストに参加するのが目的なので、簡単に考えられるロングショート戦略として、単純な移動平均からの乖離を考えてみます。

青色の線が株価の動き、緑が20日移動平均線です。平均よりずっと低い位置い現在の価格があれば平均の方に上がって行くように見えます。逆も然りです。

ここから、20日移動平均と現在の終値を比べて乖離が大きい銘柄をランキングして,ロングショートしたいと思います。この乖離をFactorとしてAlphaになり得るか、調べたいと思います。

注意しなくては行けないのは、

現在価格 / 移動平均がより大きい時にショート 
現在価格 / 移動平均がより小さい時にロング

というストラテジーを組むと、コンテストの趣旨とは逆(より大きいファクターをロング、より小さいファクターをショート)になってしまします。ですので、

移動平均 / 現在価格 がより大きい時にロング 
移動平均 / 現在価格 がより小さい時にショート

というストラテジーにします。

ここは重要です。というのも、Alphalensも、条件にあるOptimize APIでの注文も、大きいファクターをロング、小さいファクターをショートというロジックが組まれているからです。

Research

まずは20日移動平均と当日終値でFactorを作り、収益やセクターごとの結果を見てみます。

# at Research 
from quantopian.research import run_pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline import Pipeline

from quantopian.pipeline.filters import QTradableStocksUS
from quantopian.pipeline.factors import SimpleMovingAverage
from quantopian.pipeline.classifiers.fundamentals import Sector  

import alphalens as al 

MORNINGSTAR_SECTOR_CODES = {
     -1: 'Misc',
    101: 'Basic Materials',
    102: 'Consumer Cyclical',
    103: 'Financial Services',
    104: 'Real Estate',
    205: 'Consumer Defensive',
    206: 'Healthcare',
    207: 'Utilities',
    308: 'Communication Services',
    309: 'Energy',
    310: 'Industrials',
    311: 'Technology' ,    
}

def my_sma():
    
    # contest 用 ユニバース
    base_universe= QTradableStocksUS()
    
    sma = SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=20)
    adj_close = USEquityPricing.close.latest 
    sector = Sector()

    myfactor = sma /adj_close
    
    # 各日、各銘柄毎のmyfactorをzscoreでランキングする
    # その時使う銘柄はbase_universeに入っている銘柄だけ
    zscored_ratio = myfactor.zscore(mask=base_universe)
    
    return Pipeline(
        columns = {
            'zscored_ratio':zscored_ratio, 
            'adj_close':adj_close,
            'sma':sma,
            'Sector' : Sector(),
            'myfactor':myfactor,
        },
        screen=zscored_ratio.notnull() 
    )

results = run_pipeline(my_sma(), '2016-01-01', '2018-01-01')



myfactor = sma /adj_close が、今回のFactorです。現在の株価と20日移動平均を比較しています。

ユニバースには、QTradableStocksUSを指定します。ユニバースを指定しないと、Quantopianにデータが存在する全銘柄(約8000社)に対してを計算を行います。コンテストでは、彼らが指定したユニバースである QTradableStocksUS を使わなくてはいけません。

QTradableStocksUS を パイプラインに渡してを実行すると、QTradableStocksUSに入っている銘柄だけ計算します。返り値 results を見てみましょう。

results.head()

上記コードのPipeline()で columns = で指定したデータを持つDataFrameが返ります。 index は日付と銘柄のマルチインデックスです。

次に、同期間の株価を取得します。

## 全期間で株式データを取得する。
asset_list = results.index.levels[1]
prices = get_pricing(asset_list, start_date='2016-01-01', end_date='2018-01-01', fields='close_price')
prices.head()

取得した株式データを Alphalens に渡して、Factorと収益率の関係を調べます。

groupbyを指定すると、グループごとの結果を返します。groupby_labelsでグループの名前を差し替える事が出来ます。quantilesを10にして、同日に取得した全銘柄から得られたFactorの大きさ毎に10のグループに分けます。

## alphalens のテストに使う収益率の日数。
periods = (1,5,10)
## factor をアルファレンズにかける。
factor_data = al.utils.get_clean_factor_and_forward_returns(factor=results["myfactor"],
                                                           prices=prices,
                                                           groupby=results["Sector"],
                                                           groupby_labels=MORNINGSTAR_SECTOR_CODES,
                                                           periods=periods,
                                                           quantiles=10)
factor_data.head()

factor_quantile を見てください。 quantiles=10を指定したので、factorの大きさ毎に10のグループに分けられました。

では、収益とFactorの関係を見てみましょう。結果がたくさん出力されますが、一部だけ画像を貼り付けておきます。

al.tears.create_returns_tear_sheet(factor_data, by_group=True)

最初のテーブルは、先に指定した、1日、5日、10日の収益率です。棒グラフは、ファクターを1-10にランキングして、各グループに入った銘柄の収益平均をプロットしたものです。いい感じですね。私のファクターはアルファになれそうです。

条件をクリアするアルゴリズムを作る

コンテストの基準を満たすアルゴリズムを1から書くのはとても大変なので、Quantopian Lectures にあるアルゴリズムを改造しながら作ってみたいと思います.使うのはこちら

実際書いたアルゴリズムがこちら。

from quantopian.pipeline.data.builtin import USEquityPricing

import quantopian.algorithm as algo
import quantopian.optimize as opt
from quantopian.pipeline import Pipeline
from quantopian.pipeline.factors import SimpleMovingAverage

from quantopian.pipeline.filters import QTradableStocksUS
from quantopian.pipeline.experimental import risk_loading_pipeline

# レバレッジはコンテストの規則で1.1までだがバッファで1.0に。
MAX_GROSS_LEVERAGE = 1.0
# 全ポジションは600まで。投入資金が少なければMaxに届かない事もある。
TOTAL_POSITIONS = 600

# Here we define the maximum position size that can be held for any
# given stock. If you have a different idea of what these maximum
# sizes should be, feel free to change them. Keep in mind that the
# optimizer needs some leeway in order to operate. Namely, if your
# maximum is too small, the optimizer may be overly-constrained.

# 保有銘柄数をコントロールするための定数。
# 最適なオーダーのために多少の余裕をもたせたほうがいいと書いてあるので、
# TOTAL_POSITIONS = 600のままにした。
MAX_SHORT_POSITION_SIZE = 2.0 / TOTAL_POSITIONS
MAX_LONG_POSITION_SIZE = 2.0 / TOTAL_POSITIONS

def initialize(context):
   
    algo.attach_pipeline(make_pipeline(), 'long_short_equity_template')
    algo.attach_pipeline(risk_loading_pipeline(), 'risk_factors')

    algo.schedule_function(func=rebalance,
                           date_rule=algo.date_rules.week_start(),
                           time_rule=algo.time_rules.market_close(),
                           half_days=False)

    algo.schedule_function(func=record_vars,
                           date_rule=algo.date_rules.every_day(),
                           time_rule=algo.time_rules.market_close(),
                           half_days=True)

def make_pipeline():
    sma = SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=20)
    adj_close = USEquityPricing.close.latest 
    myfactor = sma / adj_close
    quantiled_myfactor = myfactor.quantiles(10)
    
    universe = QTradableStocksUS()
    # 外れ値をはずしてみた。
    myfactor_windorized = myfactor.winsorize(min_percentile=0.10, max_percentile=0.90)

    # 今後、新しいFactorを追加してつかう事を想定してcombined_factorを作っている
    combined_factor = (
        myfactor_windorized.zscore() 
        # + another_myfactor.zscore() 例えば、こういうふうに、+でつなげて追加すればよい
    )

    # combined_factorの上からと下からTOTAL_POSITIONS//2銘柄取得。それをロングとショートする。
    longs= combined_factor.top(TOTAL_POSITIONS//2, mask=universe )
    shorts= combined_factor.bottom(TOTAL_POSITIONS//2, mask=universe )
    long_short_screen = (longs | shorts)

    # Create pipeline
    pipe = Pipeline(
        columns={
            'longs': longs,
            'shorts': shorts,
            'combined_factor': combined_factor,
            'myfactor':myfactor, 
            'quantiled_myfactor':quantiled_myfactor,
            
        },
        screen=long_short_screen
    )
    return pipe

def before_trading_start(context, data):
    # pipeline 作成
    context.pipeline_data = algo.pipeline_output('long_short_equity_template')
   
    # リスクファクターを計算するパイプラインを作成
    context.risk_loadings = algo.pipeline_output('risk_factors')

def record_vars(context, data):
    # ポジション数を描画する
    algo.record(num_positions=len(context.portfolio.positions))

def rebalance(context, data):
    """
    毎日のリバランスをする関数。
    """
    # パイプラインからデータを取得。 pandas DataFrame 型
    pipeline_data = context.pipeline_data
    risk_loadings = context.risk_loadings

    # ファクターを最大限に使いつつ最適化(factorが大きい銘柄群をLong、小さい銘柄群をShortする)して、
    # 銘柄を選ぶ関数を使いオブジェクトを取得。
    # Optimize API については、https://www.quantopian.com/help#optimize-title を参照してください。
    objective = opt.MaximizeAlpha(pipeline_data.combined_factor)

    # objective に渡す「制限リスト」を作成。制限を渡さないと、当てはまるすべての銘柄を取引してしまう。
    # constraint については、 https://www.quantopian.com/help#module-quantopian_optimize_constraints 
    constraints = []
    # レバレッジが1.0になるように制限
    constraints.append(opt.MaxGrossExposure(MAX_GROSS_LEVERAGE))

    # ダラーニュートラル(long/short両サイドが同じサイズ)になるように制限
    constraints.append(opt.DollarNeutral())

    # Add the RiskModelExposure constraint to make use of the
    # default risk model constraints
    # すみません。ここは勉強不足でわかりません
    neutralize_risk_factors = opt.experimental.RiskModelExposure(
        risk_model_loadings=risk_loadings,
        version=0
    )
    constraints.append(neutralize_risk_factors)

    # 設定したポジションサイズを超えない様にする。
    # かつ、一つもしくは少数の銘柄だけのポジションをつくらないようにする。
    constraints.append(
        opt.PositionConcentration.with_equal_bounds(
            min=-MAX_SHORT_POSITION_SIZE,
            max=MAX_LONG_POSITION_SIZE
        ))

    # objective と 制約リストを渡して注文する
    algo.order_optimal_portfolio(
        objective=objective,
        constraints=constraints
    )


バックテスト

Fullbacktestを行うと、アルゴリズムの収益だけではなく、リスク指標や、コンテストの基準を満たしているかどうかの結果も表示されます。

コンテストの基準を一つクリアしていないようです。何がどうクリアしていないのかも教えてくれます。

どうやら Short Term Reversal というスタイルリスクファクターで不合格になってようです。なにかしら?と思ってよくよく読んでみると、直近まで大きくポジティブ、もしくはネガティブだった銘柄が、急に戻して得られる収益は小さくなくてはいけないようです。

え?

そういう、ストラテジーなんですけど?

やばい。ストラテジーから見直しか?と思いましたが、このコンテストに参加した話をLTする!と約束した件があり、時間が全然ないので、なんとかアドホックに修正して、テストをパスできないかと、ゴニョゴニョしていたところ、移動平均を20日ではなく60日にしたらなんとかパスしましたε-(´∀`*)ホッ。収益はだいぶ減りましたが。

参加

あとはアルゴリズムを応募するだけです。Full Backtestが終わると、Enter Contestというボタンがでます。それを押すだけです。応募時の名前を聞かれますが、shinseitaro と適当に書いて応募しました。

応募すると、次の日のマーケットデータを使って最終のテストをすること、テストにパスしたかどうかはメールで知らせること、アルゴリズムは3つまで応募できるからがんばって!というメッセージが出ます。

結果

2018/11/29Submitしたんですが、2018/11/30現在、いまだメールは来ません・・・

感想

大変勉強になりました。

まず、Quantopianがどのようにマーケットに向き合っているかがよくわかりました。彼らは、投資家やアルゴリズム開発者を、突然吹き荒れるマーケットの「嵐」からできるだけ守りたいと思っていることが随所に見られます。

コンテストの基準、TutorialやLecture、リスクマネジメントモデルのホワイトペーパーなどのドキュメント、Forumへの返答など非常に丁寧に行われています。株価のQuantitative Analytics を学びたい人にはとてもよいツールです。

株価分析に関して、私に全然知識が無いなーと思い知らされました。

メモ

1. ここを見ると、( https://www.quantopian.com/posts/15-contest-entrants-invited-to-license-their-strategies-to-quantopian ) 

Many of our funded authors have relied upon price-driven strategies. As we continue to evaluate and add algorithms to our portfolio, we will be especially interested in new strategies that take advantage of a broader range of fundamental factors.

価格分析したアルゴが多いのでファンダメンタルアルゴがほしいんだよねーってQuantopian側がリクエストしています。テンプレも用意したんで頑張ってよー(超跳躍意訳)とのことです。ファンダメンタルが得意な方、ぜひ!

2. アルゴリズムネタに困ったら、Forumに用意されている Algo Template というタグを検索してみたらいいと思います。(ファンダメンタルなアルゴしかでてきませんが。)

3. 本気でFundedされたい人は,Quantopian Risk Modelを理解しなくてはいけないと思います.

4. 学生さんのみを対象にしたContestも開始したみたいです。Quantsを目指す、学生のみなさん、ぜひ!

5. 次回、Tokyo Quantopian User Groupは12月15日です。かなりガッツリとファンダメンタル分析をする予定です。満席ですが、キャンセルが出るかもしれませんので、興味がある方は登録だけでもされておくとHappyかもしれません。





この記事が気に入ったらサポートをしてみませんか?