Notebook

Enter your backtest ID.

Note: the backtest needs to be longer than 2 years in order to receive a score.

In [1]:
# Replace the string below with your backtest ID.
bt = get_backtest('5aa25c8f66a63f41f2f66151')
100% Time: 0:00:11|###########################################################|
In [2]:
import empyrical as ep
import pyfolio as pf
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
from quantopian.research import returns
In [3]:
from quantopian.pipeline import Pipeline
from quantopian.research import run_pipeline
from quantopian.pipeline.filters import QTradableStocksUS

def get_tradable_universe(start, end):
    """
    Gets the tradable universe in a format that can be compared to the positions
    of a backtest.
    """
    pipe = Pipeline(
        columns={'qtu':QTradableStocksUS()}
    )
    df = run_pipeline(pipe, start, end)
    df = df.unstack()
    df.columns = df.columns.droplevel()
    df = df.astype(float).replace(0, np.nan)
    return df
In [4]:
def volatility_adjusted_daily_return(trailing_algorithm_returns):
    """
    Normalize the last daily return in `trailing_algorithm_returns` by the annualized
    volatility of `trailing_algorithm_returns`.
    """
    
    todays_return = trailing_algorithm_returns[-1]
    # Volatility is floored at 2%.
    volatility = max(ep.annual_volatility(trailing_algorithm_returns), 0.02)
    score = (todays_return / volatility)
    
    return score
In [5]:
def compute_score(algorithm_returns):
    """
    Compute the score of a backtest from its algorithm_returns.
    """
    
    result = []
    
    cumulative_score = 0
    count = 0
    
    daily_scores = roll(
        algorithm_returns,
        function=volatility_adjusted_daily_return,
        window=63
    )
    
    cumulative_score = np.cumsum(daily_scores[441:])
    latest_score = cumulative_score[-1]
    
    print ''
    print 'Score computed between %s and %s.' % (cumulative_score.index[0].date(), daily_scores.index[-1].date())
    
    plt.plot(cumulative_score)
    plt.title('Out-of-Sample Score Over Time')
    print 'Cumulative Score: %f' % latest_score
    
    return cumulative_score
In [6]:
# This code is copied from the empyrical repository.
# Source: https://github.com/quantopian/empyrical/blob/master/empyrical/utils.py#L49
# Includes a fix to the bug reported here: https://github.com/quantopian/empyrical/issues/79
def roll(*args, **kwargs):
    """
    Calculates a given statistic across a rolling time period.
    Parameters
    ----------
    returns : pd.Series or np.ndarray
        Daily returns of the strategy, noncumulative.
        - See full explanation in :func:`~empyrical.stats.cum_returns`.
    factor_returns (optional): float / series
        Benchmark return to compare returns against.
    function:
        the function to run for each rolling window.
    window (keyword): int
        the number of periods included in each calculation.
    (other keywords): other keywords that are required to be passed to the
        function in the 'function' argument may also be passed in.
    Returns
    -------
    np.ndarray, pd.Series
        depends on input type
        ndarray(s) ==> ndarray
        Series(s) ==> pd.Series
        A Series or ndarray of the results of the stat across the rolling
        window.
    """
    func = kwargs.pop('function')
    window = kwargs.pop('window')
    if len(args) > 2:
        raise ValueError("Cannot pass more than 2 return sets")

    if len(args) == 2:
        if not isinstance(args[0], type(args[1])):
            raise ValueError("The two returns arguments are not the same.")

    if isinstance(args[0], np.ndarray):
        return _roll_numpy(func, window, *args, **kwargs)
    return _roll_pandas(func, window, *args, **kwargs)

def _roll_ndarray(func, window, *args, **kwargs):
    data = []
    for i in range(window, len(args[0]) + 1):
        rets = [s[i-window:i] for s in args]
        data.append(func(*rets, **kwargs))
    return np.array(data)


def _roll_pandas(func, window, *args, **kwargs):
    data = {}
    for i in range(window, len(args[0]) + 1):
        rets = [s.iloc[i-window:i] for s in args]
        data[args[0].index[i - 1]] = func(*rets, **kwargs)
    return pd.Series(data)
In [7]:
SECTORS = [
    'basic_materials', 'consumer_cyclical', 'financial_services',
    'real_estate', 'consumer_defensive', 'health_care', 'utilities',
    'communication_services', 'energy', 'industrials', 'technology'
]

STYLES = [
    'momentum', 'size', 'value', 'short_term_reversal', 'volatility'
]

POSITION_CONCENTRATION_98TH_MAX = 0.05
POSITION_CONCENTRATION_100TH_MAX = 0.1
LEVERAGE_0TH_MIN = 0.7
LEVERAGE_2ND_MIN = 0.8
LEVERAGE_98TH_MAX = 1.1
LEVERAGE_100TH_MAX = 1.2
DAILY_TURNOVER_0TH_MIN = 0.03
DAILY_TURNOVER_2ND_MIN = 0.05
DAILY_TURNOVER_98TH_MAX = 0.65
DAILY_TURNOVER_100TH_MAX = 0.8
NET_EXPOSURE_LIMIT_98TH_MAX = 0.1
NET_EXPOSURE_LIMIT_100TH_MAX = 0.2
BETA_TO_SPY_98TH_MAX = 0.3
BETA_TO_SPY_100TH_MAX = 0.4
SECTOR_EXPOSURE_98TH_MAX = 0.2
SECTOR_EXPOSURE_100TH_MAX = 0.25
STYLE_EXPOSURE_98TH_MAX = 0.4
STYLE_EXPOSURE_100TH_MAX = 0.5
TRADABLE_UNIVERSE_0TH_MIN = 0.9
TRADABLE_UNIVERSE_2ND_MIN = 0.95


def check_constraints(positions, transactions, algorithm_returns, risk_exposures):
    
    sector_constraints = True
    style_constraints = True
    constraints_met = 0
    num_constraints = 9
    
    # Position Concentration Constraint
    print 'Checking positions concentration limit...'
    try:
        percent_allocations = pf.pos.get_percent_alloc(positions[5:])
        daily_absolute_percent_allocations = percent_allocations.abs().drop('cash', axis=1)
        daily_max_absolute_position = daily_absolute_percent_allocations.max(axis=1)
        
        position_concentration_98 = daily_max_absolute_position.quantile(0.98)
        position_concentration_100 = daily_max_absolute_position.max()
        
    except IndexError:
        position_concentration_98 = -1
        position_concentration_100 = -1
        
    if (position_concentration_98 > POSITION_CONCENTRATION_98TH_MAX):
        print 'FAIL: 98th percentile position concentration of %.2f > %.1f.' % (
        position_concentration_98*100,
        POSITION_CONCENTRATION_98TH_MAX*100
    )
    elif (position_concentration_100 > POSITION_CONCENTRATION_100TH_MAX):
        print 'FAIL: 100th percentile position concentration of %.2f > %.1f.' % (
        position_concentration_100*100,
        POSITION_CONCENTRATION_100TH_MAX*100
    )
    else:
        print 'PASS: Max position concentration of %.2f%% <= %.1f%%.' % (
            position_concentration_98*100,
            POSITION_CONCENTRATION_98TH_MAX*100
        )
        constraints_met += 1

        
    # Leverage Constraint
    print ''
    print 'Checking leverage limits...'
    leverage = pf.timeseries.gross_lev(positions[5:])
    leverage_0 = leverage.min()
    leverage_2 = leverage.quantile(0.02)
    leverage_98 = leverage.quantile(0.98)
    leverage_100 = leverage.max()
    leverage_passed = True
    
    if (leverage_0 < LEVERAGE_0TH_MIN):
        print 'FAIL: Minimum leverage of %.2fx is below %.1fx' % (
            leverage_0,
            LEVERAGE_0TH_MIN
        )
        leverage_passed = False
    if (leverage_2 < LEVERAGE_2ND_MIN):
        print 'FAIL: 2nd percentile leverage of %.2fx is below %.1fx' % (
            leverage_2,
            LEVERAGE_2ND_MIN
        )
        leverage_passed = False
    if (leverage_98 > LEVERAGE_98TH_MAX):
        print 'FAIL: 98th percentile leverage of %.2fx is above %.1fx' % (
            leverage_98,
            LEVERAGE_98TH_MAX
        )
        leverage_passed = False
    if (leverage_100 > LEVERAGE_100TH_MAX):
        print 'FAIL: Maximum leverage of %.2fx is above %.1fx' % (
            leverage_0,
            LEVERAGE_0TH_MAX
        )
        leverage_passed = False
    if leverage_passed:
        print 'PASS: Leverage range of %.2fx-%.2fx is between %.1fx-%.1fx.' % (
            leverage_2,
            leverage_98,
            LEVERAGE_2ND_MIN,
            LEVERAGE_98TH_MAX
        )
        constraints_met += 1
      
    # Turnover Constraint
    print ''
    print 'Checking turnover limits...'
    turnover = pf.txn.get_turnover(positions, transactions, denominator='portfolio_value')
    # Compute mean rolling 63 trading day turnover.
    rolling_mean_turnover = roll(
        turnover, 
        function=pd.Series.mean,
        window=63)[62:]
    rolling_mean_turnover_0 = rolling_mean_turnover.min()
    rolling_mean_turnover_2 = rolling_mean_turnover.quantile(0.02)
    rolling_mean_turnover_98 = rolling_mean_turnover.quantile(0.98)
    rolling_mean_turnover_100 = rolling_mean_turnover.max()  
    rolling_mean_turnover_passed = True
    
    if (rolling_mean_turnover_0 < DAILY_TURNOVER_0TH_MIN):
        print 'FAIL: Minimum turnover of %.2f%% is below %.1f%%.' % (
            rolling_mean_turnover_0*100,
            DAILY_TURNOVER_0TH_MIN*100
        )
        rolling_mean_turnover_passed = False
    if (rolling_mean_turnover_2 < DAILY_TURNOVER_2ND_MIN):
        print 'FAIL: 2nd percentile turnover of %.2f%% is below %.1fx' % (
            rolling_mean_turnover_2*100,
            DAILY_TURNOVER_2ND_MIN*100
        )
        rolling_mean_turnover_passed = False
    if (rolling_mean_turnover_98 > DAILY_TURNOVER_98TH_MAX):
        print 'FAIL: 98th percentile turnover of %.2f%% is above %.1fx' % (
            rolling_mean_turnover_98*100,
            DAILY_TURNOVER_98TH_MAX*100
        )
        rolling_mean_turnover_passed = False
    if (rolling_mean_turnover_100 > DAILY_TURNOVER_100TH_MAX):
        print 'FAIL: Maximum turnover of %.2f%% is above %.1fx' % (
            rolling_mean_turnover_100*100,
            DAILY_TURNOVER_100TH_MAX*100
        )
        rolling_mean_turnover_passed = False
    if rolling_mean_turnover_passed:
        print 'PASS: Mean turnover range of %.2f%%-%.2f%% is between %.1f%%-%.1f%%.' % (
            rolling_mean_turnover_2*100,
            rolling_mean_turnover_98*100,
            DAILY_TURNOVER_2ND_MIN*100,
            DAILY_TURNOVER_98TH_MAX*100
        )
        constraints_met += 1

        
    # Net Exposure Constraint
    print ''
    print 'Checking net exposure limit...'
    net_exposure = pf.pos.get_long_short_pos(positions[5:])['net exposure'].abs()
    net_exposure_98 = net_exposure.quantile(0.98)
    net_exposure_100 = net_exposure.max()
    
    if (net_exposure_98 > NET_EXPOSURE_LIMIT_98TH_MAX):
        print 'FAIL: 98th percentile net exposure (absolute value) of %.2f > %.1f.' % (
        net_exposure_98*100,
        NET_EXPOSURE_LIMIT_98TH_MAX*100
    )
    elif (net_exposure_100 > NET_EXPOSURE_LIMIT_100TH_MAX):
        print 'FAIL: 100th percentile net exposure (absolute value) of %.2f > %.1f.' % (
        net_exposure_100*100,
        NET_EXPOSURE_LIMIT_100TH_MAX*100
    )
    else:
        print 'PASS: Net exposure (absolute value) of %.2f%% <= %.1f%%.' % (
            net_exposure_98*100,
            NET_EXPOSURE_LIMIT_98TH_MAX*100
        )
        constraints_met += 1
    
        
    # Beta Constraint
    print ''
    print 'Checking beta-to-SPY limit...'
    spy_returns = returns(
        symbols('SPY'),
        algorithm_returns.index[0],
        algorithm_returns.index[-1],
    )
    beta = roll(
        algorithm_returns,
        spy_returns,
        function=ep.beta,
        window=126
    ).reindex_like(algorithm_returns).fillna(0).abs()
    beta_98 = beta.quantile(0.98)
    beta_100 = beta.max()
    if (beta_98 > BETA_TO_SPY_98TH_MAX):
            print 'FAIL: 98th percentile absolute beta of %.3f > %.1f.' % (
            beta_98,
            BETA_TO_SPY_98TH_MAX
        )
    elif (beta_100 > BETA_TO_SPY_100TH_MAX):
        print 'FAIL: 100th percentile absolute beta of %.3f > %.1f.' % (
            beta_100,
            BETA_TO_SPY_100TH_MAX
        )
    else:
        print 'PASS: Max absolute beta of %.3f <= %.1f.' % (
            beta_98,
            BETA_TO_SPY_98TH_MAX
        )
        constraints_met += 1
        
    # Risk Exposures
    rolling_mean_risk_exposures = risk_exposures.rolling(63, axis=0).mean()[62:].fillna(0)
    
    # Sector Exposures
    print ''
    print 'Checking sector exposure limits...'
    for sector in SECTORS:
        absolute_mean_sector_exposure = rolling_mean_risk_exposures[sector].abs()
        abs_mean_sector_exposure_98 = absolute_mean_sector_exposure.quantile(0.98)
        abs_mean_sector_exposure_100 = absolute_mean_sector_exposure.max()
        if (abs_mean_sector_exposure_98 > SECTOR_EXPOSURE_98TH_MAX):
            print 'FAIL: 98th percentile %s exposure of %.3f (absolute value) is greater than %.2f.' % (
                sector,
                abs_mean_sector_exposure_98,
                SECTOR_EXPOSURE_98TH_MAX
            )
            sector_constraints = False
        elif (abs_mean_sector_exposure_100 > SECTOR_EXPOSURE_100TH_MAX):
            max_sector_exposure_day = absolute_mean_sector_exposure.idxmax()
            print 'FAIL: Max %s exposure of %.3f (absolute value) on %s is greater than %.2f.' % (
                sector,
                abs_mean_sector_exposure_100,
                max_sector_exposure_day,
                SECTOR_EXPOSURE_100TH_MAX
            )
            sector_constraints = False
    if sector_constraints:
        print 'PASS: All sector exposures were between +/-%.2f.' % SECTOR_EXPOSURE_98TH_MAX
        constraints_met += 1
        
    # Style Exposures
    print ''
    print 'Checking style exposure limits...'
    for style in STYLES:
        absolute_mean_style_exposure = rolling_mean_risk_exposures[style].abs()
        abs_mean_style_exposure_98 = absolute_mean_style_exposure.quantile(0.98)
        abs_mean_style_exposure_100 = absolute_mean_style_exposure.max()
        if (abs_mean_style_exposure_98 > STYLE_EXPOSURE_98TH_MAX):
            print 'FAIL: 98th percentile %s exposure of %.3f (absolute value) is greater than %.2f.' % (
                style, 
                abs_mean_style_exposure_98, 
                STYLE_EXPOSURE_98TH_MAX
            )
            style_constraints = False
        elif (abs_mean_style_exposure_100 > STYLE_EXPOSURE_100TH_MAX):
            max_style_exposure_day = absolute_mean_style_exposure.idxmax()
            print 'FAIL: Max %s exposure of %.3f (absolute value) on %s is greater than %.2f.' % (
                style, 
                abs_mean_style_exposure_100, 
                max_style_exposure_day.date(),
                STYLE_EXPOSURE_100TH_MAX
            )
            style_constraints = False
    if style_constraints:
        print 'PASS: All style exposures were between +/-%.2f.' % STYLE_EXPOSURE_98TH_MAX
        constraints_met += 1
    
    
    # Tradable Universe
    print ''
    print 'Checking investment in tradable universe...'
    positions_wo_cash = positions.drop('cash', axis=1)
    positions_wo_cash = positions_wo_cash.abs()
    total_investment = positions_wo_cash.fillna(0).sum(axis=1)
    daily_qtu_investment = universe.multiply(positions_wo_cash).fillna(0).sum(axis=1)
    percent_in_qtu = daily_qtu_investment / total_investment
    percent_in_qtu = percent_in_qtu[5:].fillna(0)
    
    percent_in_qtu_0 = percent_in_qtu.min()
    percent_in_qtu_2 = percent_in_qtu.quantile(0.02)
        
    if percent_in_qtu_0 < TRADABLE_UNIVERSE_0TH_MIN:
        min_percent_in_qtu_date = percent_in_qtu.argmin()
        print 'FAIL: Minimum investment in QTradableStocksUS of %.2f%% on %s is < %.1f%%.' % (
            percent_in_qtu_0*100, 
            min_percent_in_qtu_date.date(),
            TRADABLE_UNIVERSE_0TH_MIN*100
        )
    elif percent_in_qtu_2 < TRADABLE_UNIVERSE_2ND_MIN:
        print 'FAIL: Investment in QTradableStocksUS (2nd percentile) of %.2f%% is < %.1f%%.' % (
            percent_in_qtu_2*100, 
            TRADABLE_UNIVERSE_2ND_MIN*100
        )
    else:
        print 'PASS: Investment in QTradableStocksUS is >= %.1f%%.' % (
            TRADABLE_UNIVERSE_2ND_MIN*100
        )
        constraints_met += 1
        
        
    # Total algorithm_returns Constraint
    print ''
    print 'Checking that algorithm has positive algorithm_returns...'
    cumulative_algorithm_returns = ep.cum_returns_final(algorithm_returns)
    if (cumulative_algorithm_returns > 0):
        print 'PASS: Cumulative algorithm_returns of %.2f is positive.' % (
            cumulative_algorithm_returns
        )
        constraints_met += 1
    else:
        print 'FAIL: Cumulative algorithm_returns of %.2f is negative.' % (
            cumulative_algorithm_returns
        )
    
    print ''
    print 'Results:'
    if constraints_met == num_constraints:
        print 'All constraints met!'
    else:
        print '%d/%d tests passed.' % (constraints_met, num_constraints)
In [8]:
def evaluate_backtest(positions, transactions, algorithm_returns, risk_exposures):
    if len(positions.index) > 504:
        check_constraints(positions, transactions, algorithm_returns, risk_exposures)
        score = compute_score(algorithm_returns[start:end])
    else:
        print 'ERROR: Backtest must be longer than 2 years to be evaluated.'

Transform some of the data.

In [9]:
positions = bt.pyfolio_positions
transactions = bt.pyfolio_transactions
algorithm_returns = bt.daily_performance.returns
factor_exposures = bt.factor_exposures

start = positions.index[0]
end = positions.index[-1]
universe = get_tradable_universe(start, end)
universe.columns = universe.columns.map(lambda x: '%s-%s' % (x.symbol, x.sid))

Run this to evaluate your algorithm. Note that the new contest will require all filters to pass before a submission is eligible to participate.

In [10]:
evaluate_backtest(positions, transactions, algorithm_returns, factor_exposures)
Checking positions concentration limit...
PASS: Max position concentration of 4.89% <= 5.0%.

Checking leverage limits...
PASS: Leverage range of 0.96x-1.03x is between 0.8x-1.1x.

Checking turnover limits...
PASS: Mean turnover range of 14.14%-14.69% is between 5.0%-65.0%.

Checking net exposure limit...
PASS: Net exposure (absolute value) of 2.88% <= 10.0%.

Checking beta-to-SPY limit...
PASS: Max absolute beta of 0.245 <= 0.3.

Checking sector exposure limits...
PASS: All sector exposures were between +/-0.20.

Checking style exposure limits...
PASS: All style exposures were between +/-0.40.

Checking investment in tradable universe...
PASS: Investment in QTradableStocksUS is >= 95.0%.

Checking that algorithm has positive algorithm_returns...
PASS: Cumulative algorithm_returns of 0.66 is positive.

Results:
All constraints met!

Score computed between 2018-01-02 and 2018-03-07.
Cumulative Score: 0.052321
In [11]:
bt.create_full_tear_sheet()
Start date2016-01-06
End date2018-03-07
Total months26
Backtest
Annual return 27.4%
Cumulative returns 69.1%
Annual volatility 11.0%
Sharpe ratio 2.27
Calmar ratio 5.45
Stability 0.90
Max drawdown -5.0%
Omega ratio 1.48
Sortino ratio 3.87
Skew 0.66
Kurtosis 3.69
Tail ratio 1.41
Daily value at risk -1.3%
Gross leverage 1.00
Daily turnover 14.7%
Alpha 0.25
Beta -0.01
Worst drawdown periods Net drawdown in % Peak date Valley date Recovery date Duration
0 5.03 2017-11-06 2018-01-11 2018-02-07 68
1 4.87 2016-07-20 2016-08-05 2016-09-16 43
2 4.60 2016-01-15 2016-02-09 2016-02-26 31
3 3.96 2017-05-02 2017-06-08 2017-08-02 67
4 3.70 2017-08-03 2017-08-30 2017-10-13 52
Stress Events mean min max
New Normal 0.10% -2.58% 4.29%
Top 10 long positions of all time max
SNAP-50683 5.03%
ZEN-46918 4.96%
FGEN-48088 4.92%
BPT-1068 4.91%
YEXT-50781 4.87%
CFMS-49211 4.84%
BHVN-50839 4.82%
CPG-46206 4.81%
ACIA-33321 4.76%
LOCO-47382 4.75%
Top 10 short positions of all time max
SGYP-32331 -5.00%
AMD-351 -4.95%
ARRY-22192 -4.91%
P-41579 -4.91%
CSIQ-32856 -4.90%
GPRO-47208 -4.90%
PGNX-17908 -4.89%
USNA-15408 -4.88%
ACAD-26322 -4.87%
NFLX-23709 -4.86%
Top 10 positions of all time max
SNAP-50683 5.03%
SGYP-32331 5.00%
ZEN-46918 4.96%
AMD-351 4.95%
FGEN-48088 4.92%
ARRY-22192 4.91%
BPT-1068 4.91%
P-41579 4.91%
CSIQ-32856 4.90%
GPRO-47208 4.90%
All positions ever held max
SNAP-50683 5.03%
SGYP-32331 5.00%
ZEN-46918 4.96%
AMD-351 4.95%
FGEN-48088 4.92%
ARRY-22192 4.91%
BPT-1068 4.91%
P-41579 4.91%
CSIQ-32856 4.90%
GPRO-47208 4.90%
PGNX-17908 4.89%
USNA-15408 4.88%
YEXT-50781 4.87%
ACAD-26322 4.87%
NFLX-23709 4.86%
JCP-4118 4.86%
SCTY-43721 4.85%
CFMS-49211 4.84%
VRX-10908 4.83%
KNDI-32972 4.83%
TWTR-45815 4.82%
KITE-47169 4.82%
BHVN-50839 4.82%
CPG-46206 4.81%
CLF-1595 4.80%
MUX-7845 4.80%
OCLR-21366 4.79%
MACK-42735 4.79%
ARIA-11880 4.79%
INO-3150 4.79%
AKS-10897 4.78%
GLUU-33566 4.78%
ICPT-43505 4.78%
SQ-49610 4.77%
S-2938 4.76%
ASPS-38633 4.76%
FEYE-45451 4.76%
NVDA-19725 4.76%
AG-40607 4.76%
ACIA-33321 4.76%
LOCO-47382 4.75%
BB-19831 4.75%
LC-48220 4.75%
UPL-50782 4.75%
DERM-47845 4.74%
OPK-23120 4.74%
RNG-45521 4.74%
XOG-50368 4.74%
SHAK-48543 4.73%
HUBS-47872 4.73%
ADRO-48925 4.73%
ATRS-22418 4.73%
FNSR-20866 4.73%
TWOU-46648 4.73%
BETR-49318 4.72%
LTRP_A-47578 4.72%
ANET-47063 4.72%
KN-46369 4.71%
ADPT-47191 4.71%
CBPO-35846 4.71%
KLDX-49463 4.71%
MU-5121 4.71%
LILA_K-49206 4.70%
BOX-48486 4.70%
GRPN-42118 4.70%
QRVO-48384 4.70%
TXMD-28966 4.70%
ONDK-48290 4.70%
SWIR-21561 4.70%
EVHC-45269 4.69%
AMBA-43495 4.69%
ARWR-10417 4.69%
TDOC-49222 4.69%
PLUG-20776 4.69%
INOV-48629 4.69%
PMC-34241 4.69%
COTV-50002 4.69%
AVXS-49751 4.69%
ELGX-23769 4.68%
GKOS-49178 4.68%
EVBG-50264 4.68%
TPL-7551 4.68%
KORS-42270 4.68%
CHK-8461 4.68%
CACC-1216 4.68%
TROX-40530 4.68%
LDRH-45619 4.68%
TWLO-50077 4.67%
ELF-50320 4.67%
SHOP-49060 4.67%
EXEL-21383 4.67%
TREE-36742 4.67%
LNKD-41451 4.67%
FLOW-49434 4.66%
NVAX-14112 4.66%
FRAC-50612 4.66%
PIR-6000 4.66%
SD-50348 4.66%
YRCW-8370 4.66%
LITE-49288 4.66%
PSTG-49464 4.66%
AA-50428 4.65%
MIME-49606 4.65%
OMER-38827 4.65%
AMPH-47193 4.65%
ALKS-301 4.65%
STMP-26286 4.65%
LKSD-50312 4.65%
CLNE-33924 4.65%
WYNN-24124 4.65%
RH-43599 4.65%
ZIOP-31341 4.65%
CLDR-50810 4.64%
XENT-47373 4.64%
GSM-38638 4.64%
JUNO-48317 4.64%
DPLO-47883 4.64%
MELI-34525 4.64%
ZOES-46742 4.64%
UAA-27822 4.64%
FSLR-32902 4.64%
TNET-46633 4.64%
SUM-48746 4.64%
ATHN-34692 4.64%
SPWH-46777 4.63%
MNK-44917 4.63%
IBP-46365 4.63%
XPER-25705 4.63%
VRTX-8045 4.63%
BW-49208 4.63%
AXDX-15934 4.63%
BLUE-44935 4.63%
NMBL-46002 4.63%
OMF-45670 4.63%
ARCH-50357 4.63%
WAIR-41757 4.63%
CHRS-48026 4.63%
TCMD-50169 4.62%
SC-46215 4.62%
MEDP-50194 4.62%
FDC-49496 4.62%
CBI-1287 4.62%
MDCO-21906 4.62%
COHR-1751 4.62%
YELP-42596 4.62%
NXPI-39994 4.62%
TIME-46965 4.62%
URI-18113 4.62%
KTOS-20947 4.62%
TEAM-49655 4.61%
DFIN-50310 4.61%
AOBC-24519 4.61%
NOMD-47572 4.61%
AYX-50735 4.61%
EVHC-50499 4.61%
GOOS-50713 4.61%
WTI-26986 4.61%
OLED-14774 4.61%
ALXN-14328 4.61%
LB-4564 4.61%
OR-47173 4.61%
SKX-20284 4.60%
BOJA-49024 4.60%
HALO-26766 4.60%
HABT-48126 4.60%
FIT-49139 4.60%
TAHO-39938 4.60%
ILMN-21774 4.60%
BL-50418 4.60%
KEM-4265 4.60%
SINA-21448 4.60%
SYNH-48027 4.60%
MMI-45771 4.59%
QTNA-50417 4.59%
WEB-27762 4.59%
RIG-9038 4.59%
AERI-45733 4.59%
COUP-50350 4.59%
FND-50798 4.59%
SRPT-16999 4.59%
OKTA-50758 4.59%
MULE-50719 4.58%
ALRM-49192 4.58%
LNG-22096 4.58%
DLTH-49615 4.58%
CDEV-50376 4.58%
OPHT-45498 4.58%
WDC-8132 4.58%
FCX-13197 4.57%
STX-24518 4.57%
FWON_K-47272 4.57%
CMG-28016 4.57%
MRD-47126 4.57%
VSTO-48531 4.57%
SEDG-48823 4.57%
NYRT-46760 4.57%
CORT-26191 4.57%
GDDY-48863 4.57%
DB-23113 4.57%
BBY-754 4.57%
UNFI-16129 4.57%
OPB-46768 4.56%
SYNT-24790 4.56%
MYCC-45453 4.56%
BUFF-49279 4.56%
TBI-15165 4.56%
PANW-43202 4.56%
SCMP-34477 4.56%
BCC-44089 4.56%
FCPT-49543 4.55%
WTW-23269 4.55%
ICHR-50509 4.55%
ANIP-25565 4.55%
IVC-4084 4.55%
PEIX-27129 4.55%
GPS-3321 4.55%
BTG-44884 4.55%
IONS-4031 4.55%
PRTY-48933 4.55%
ASIX-50260 4.55%
ACHN-32790 4.54%
SUPN-42877 4.54%
ARNC-2 4.54%
RICE-46240 4.54%
LULU-34395 4.54%
DDD-12959 4.53%
PEN-49413 4.53%
LCI-23602 4.53%
AIMT-49323 4.53%
TTD-50288 4.53%
BIIB-3806 4.53%
ABMD-53 4.53%
SNBR-19559 4.53%
M-2754 4.52%
MBLY-47430 4.52%
NBIX-14972 4.52%
LZB-4621 4.52%
IRBT-27780 4.52%
AXON-49107 4.52%
MTCH-49608 4.52%
ABX-64 4.52%
HPQ-3735 4.52%
SAGE-47332 4.52%
SOI-50870 4.51%
NOV-24809 4.51%
ONVO-41829 4.51%
DHT-27705 4.51%
KERX-21789 4.51%
CSC-1898 4.51%
RUBI-46671 4.51%
SBLK-28112 4.51%
IRTC-50399 4.50%
INCY-10187 4.50%
HTZ-50070 4.50%
GLOG-42746 4.50%
DNOW-46949 4.50%
SDRL-39495 4.49%
EIGI-45735 4.49%
BANC-23943 4.49%
MDVN-28160 4.49%
NRE-49511 4.48%
CXO-34440 4.47%
RRD-2248 4.47%
ETSY-48934 4.45%
HES-216 4.45%
IOVA-36209 4.44%
KMI-40852 4.42%
DATA-44747 4.41%
INSY-44665 4.40%
FRO-22983 4.38%
WPM-27437 4.38%
HZNP-41766 4.36%
BNED-49310 4.31%
CLDX-19187 4.28%
CJ-50690 4.26%
FCAU-47888 4.23%
VRTS-37869 4.08%
IPHI-40399 4.07%
WTTR-50789 4.06%
CHS-8612 4.06%
KRNT-48872 4.03%
PE-46989 3.93%
CBL-9890 3.92%
KTWO-46870 3.92%
PAH-45531 3.89%
HSKA-17227 3.79%
DEPO-18010 3.68%
ACRS-49465 3.66%
OTIC-47495 3.64%
BKD-27830 3.61%
EVH-49100 3.58%
NTLA-49934 3.54%
NVMI-21427 3.53%
ATGE-2371 3.48%
AKRX-270 3.44%
GIMO-44892 3.38%
BPMC-49000 3.33%
PBI-5773 3.29%
W-47833 3.28%
GRUB-46693 3.27%
GEO-11710 3.26%
CHGG-45847 3.21%
AR-45618 3.20%
DDC-39 3.18%
SGRY-49456 3.15%
LGND-12200 3.11%
AGEN-21104 3.09%
DBD-2100 3.08%
NVRO-48025 3.07%
MDXG-34049 3.05%
MEET-30658 3.02%
RVNC-46315 2.92%
MOH-25349 2.87%
TLRD-7203 2.87%
PLCE-24789 2.87%
URBN-10303 2.85%
SPWR-27817 2.80%
SPKE-47372 2.78%
UCTT-26146 2.76%
CZZ-34560 2.75%
OSUR-22151 2.75%
OSIS-17718 2.73%
AFSI-32871 2.71%
ZLTQ-42037 2.70%
CPSI-23667 2.59%
CXW-22102 2.58%
NYLD-49043 2.46%
BRKS-12512 2.44%
HMHC-45861 2.40%
INGN-46370 2.36%
SSYS-12107 2.33%
OCSL-36371 2.33%
CO-33188 2.32%
ERII-36518 2.30%
DY-2385 2.29%
CORE-27864 2.29%
LL-35036 2.28%
VNDA-28326 2.27%
HA-3431 2.21%
INST-49594 2.21%
INVN-42165 2.19%
TRTN-50119 2.18%
RARE-46285 2.16%
SAH-24786 2.13%
ALDR-46869 2.11%
MMYT-40028 2.06%
AGI-44156 2.03%
RBA-18480 2.02%
PCRX-40815 2.00%
ANF-15622 1.98%
REN-34800 1.97%
ZUMZ-27229 1.96%
LXFT-44986 1.91%
UIS-7761 1.89%
TGI-15905 1.87%
X-8329 1.86%
SFS-47776 1.79%
IGT-48892 1.75%
ARNA-21724 1.74%
KSS-4313 1.74%
XON-45239 1.74%
SGMS-22637 1.73%
WIX-45800 1.72%
BEAT-35929 1.71%
AUY-25714 1.67%
CZR-42461 1.64%
ESND-7866 1.63%
DRII-45112 1.62%
BWLD-25642 1.60%
TIVO-16661 1.59%
GBX-11645 1.57%
CCJ-14479 1.53%
AEM-154 1.45%
AKAM-20680 1.42%
PTCT-44955 1.40%
MDR-4752 1.39%
HDP-48257 1.37%
PAAS-13083 1.28%
CF-27558 1.28%
EBAY-24819 1.28%
RUN-49321 1.27%
APTI-50318 1.27%
MGPI-24094 1.24%
FNBC-44709 1.20%
ATKR-50040 1.18%
ENVA-47979 1.17%
RPD-49275 1.17%
HRTX-22651 1.17%
TLN-49003 1.13%
HZN-49218 1.12%
WRD-50537 1.12%
PETS-25867 1.10%
NAV-5199 1.10%
BLCM-48304 1.09%
ENDP-21750 1.07%
JOY-22996 1.06%
HQY-47397 1.04%
BRCD-20061 0.99%
XNCR-45942 0.99%
BZH-10728 0.98%
SIG-9774 0.97%
MFRM-42184 0.97%
WLH-43733 0.94%
NPTN-40807 0.89%
ATEN-46598 0.79%
TRUE-46929 0.79%
OBE-32293 0.78%
AAXN-22846 0.78%
VRTV-47143 0.78%
AMAG-659 0.72%
SAND-43329 0.69%
CTRL-45212 0.67%
JWN-5382 0.63%
CASH-9700 0.63%
AEL-25710 0.60%
CSTM-44780 0.58%
AAC-47842 0.55%
EGO-24547 0.54%
ELLI-41243 0.54%
GG-22226 0.51%
MYGN-13698 0.49%
CPE-12011 0.47%
RGNX-49409 0.46%
CTMX-49470 0.44%
XTLY-49199 0.43%
ABCO-23176 0.43%
MBI-4684 0.41%
HCC-50780 0.38%
MOV-14762 0.37%
CCRN-24893 0.37%
EXTR-19973 0.34%
MKTO-44738 0.32%
ONCE-48547 0.32%
SHLD-26169 0.32%
HL-3585 0.31%
ESIO-2612 0.27%
MRO-5035 0.23%
BKS-9693 0.21%
TSE-47098 0.21%
HEES-28023 0.20%
RETA-49995 0.19%
PVG-42366 0.19%
EXTN-49560 0.18%
CSU-17639 0.18%
TMST-47162 0.18%
APA-448 0.17%
NE-5249 0.17%
ITGR-22015 0.14%
TTI-7633 0.13%
AMC-46027 0.11%
BLDP-13798 0.09%
TGTX-13984 0.07%
HOME-50187 0.06%
KLIC-4248 0.06%
YNDX-41484 0.03%
PUMP-50718 0.03%

Performance Relative to Common Risk Factors

Summary Statistics
Annualized Specific Return 27.67%
Annualized Common Return -0.98%
Annualized Total Return 26.41%
Specific Sharpe Ratio 2.41
Exposures Summary Average Risk Factor Exposure Annualized Return Cumulative Return
basic_materials 0.00 -0.19% -0.41%
consumer_cyclical 0.04 0.37% 0.81%
financial_services 0.01 0.04% 0.09%
real_estate 0.00 0.20% 0.43%
consumer_defensive 0.01 0.20% 0.44%
health_care -0.14 -0.89% -1.93%
utilities 0.00 0.07% 0.16%
communication_services -0.01 -0.11% -0.24%
energy 0.03 0.33% 0.71%
industrials 0.02 1.11% 2.42%
technology -0.12 -2.91% -6.22%
momentum -0.06 0.12% 0.27%
size -0.32 0.88% 1.93%
value 0.06 -0.42% -0.90%
short_term_reversal 0.24 0.93% 2.03%
volatility -0.18 -0.69% -1.48%