Equity factor basket#

The EquityFactorBasket building block provides a convenient way to build equity factor strategies. Factor scores can use historical and fundamental data at the instrument level, and incorporate custom signals and methods.

Prerequisites#

The examples provided on this page utilise the EquityUniverseFilter, ReinvestmentStrategy, and FactorExposures methods.

Before working through these examples, you should familiarise yourself with the following pages:

Environment#

Import the relevant Python libraries and initialise your environment:

import numpy as np
import pandas as pd
import datetime as dtm
from uuid import uuid4

import sigtech.framework as sig
from sigtech.framework.infra import cal

if not sig.config.is_initialised():
    env = sig.config.init(env_date='2022-02-28')
    sig.config.set('ALLOWED_MISSING_CA_DATA', True)

Build equity universe#

The following code block describes the creation of the equity universe:

CURRENCY='USD'
START_DATE = pd.Timestamp(2021, 1, 4)

You can define a custom rebalance schedule and apply it to the SPX using the EquityUniverseFilter:

rebal_schedule = sig.SchedulePeriodic(START_DATE, env.asofdate, 'NYSE(T) CALENDAR', frequency='EOM', bdc=cal.BDC_FOLLOWING).all_data_dates()

equity_filter = sig.EquityUniverseFilter('SPX INDEX')

UNIVERSE = equity_filter.apply_filter(rebal_schedule)

A reinvestment strategy can be created for every stock in your universe and mapped to the stock names:

unique_stocks = UNIVERSE.explode().unique()

rs = sig.get_single_stock_strategy(unique_stocks, build=True)

UNIVERSE_MAPPING = {s: f'{s} REINV STRATEGY' for s in unique_stocks}

Equity factor basket class#

The EquityFactorBasket inherits from SignalStrategy and requires these additional inputs:

  • A universe_filter input, either the EquityUniverseFilter or its output.

  • A FactorExposures object.

  • A factors_to_weight_function to transform the input factor data into allocations.

During construction, the strategy:

  • Evaluates the universe on each rebalance day.

  • Calculates each defined factor for each unique stock as a time series.

  • Proceeds through the rebalance dates, combining the factors as defined in the factors_to_weight_function, and allocating through the allocation_function.

Factor exposures#

You can use the sig.FactorExposures object to define the factors for your strategy.

Different factors can be added to this object via the add_raw_factor_timeseries method:

factor_definitions = sig.FactorExposures()

Adding a raw history field#

Total market cap is defined as a size factor:

factor_definitions.add_raw_factor_timeseries(
    'size',
    history_field='MARKETCAP'
)

Adding a raw fundamental field#

Quarterly dividend yield is defined as a value factor:

factor_definitions.add_raw_factor_timeseries(
    'value',
    fundamental_field='Dividend Yield',
    fundamental_freq='Quarterly'
)

Add a custom method#

This method can be run for each unique stock in your universe. The trailing 12 month returns is computed with a one month lag as a momentum factor. The method input rs is the name of the reinvestment strategy that the method runs:

def get_momentum_signal(rs, mtm_period=256, lag_period=21):

    price = sig.obj.get(rs).history()

    momentum = price.pct_change(mtm_period - 1).shift(lag_period).dropna()

    return momentum

factor_definitions.add_raw_factor_timeseries(
    'momentum',
    method=get_momentum_signal
)

Factors to weights#

This method is called on each rebalance date with the calculated individual factor values and defines how the factors are combined to select the stocks for your portfolio.

The method accepts a pd.DataFrame with factor values for each reinvestment strategy indexed by factor name:

Index

STOCK 1 REINV STRATEGY

STOCK 2 REINV STRATEGY

leverage

0.5

0.5

liquidity

100

100

And returns a pd.Series indexed by stocks with signal values:

Index

Values

STOCK 1 REINV STRATEGY

1

STOCK 2 REINV STRATEGY

1

Default factors to weights#

If no argument for the factors_to_weight_function is supplied, the default_factors_to_weight_function is used:

Input:

from sigtech.framework.strategies.equity_factor_basket import default_factors_to_weight_function

default_factors_to_weight_function?

Output:

Signature:
default_factors_to_weight_function(
    factors,
    factor_weights=None,
    proportion=None,
)
Docstring:
Default factor to weight function - This creates a linear combination of factors.

:param factors: factor dataframe.
:param factor_weights: Optional dictionary of weights.
:param proportion: Optional, proportion for long/short allocations.
:return: Series of weights.
File:      ~/sig-env/lib/python3.7/site-packages/sigtech/framework/strategies/equity_factor_basket.py
Type:      function

To retrieve all the API documentation and source code associated with an object, append ?? to the object name:

Input:

default_factors_to_weight_function??

Output:

Signature:
default_factors_to_weight_function(
    factors,
    factor_weights=None,
    proportion=None,
)
Source:
def default_factors_to_weight_function(factors, factor_weights=None, proportion=None):
    """
    Default factor to weight function - This creates a linear combination of factors.

    :param factors: factor dataframe.
    :param factor_weights: Optional dictionary of weights.
    :param proportion: Optional, proportion for long/short allocations.
    :return: Series of weights.
    """

    if factor_weights is None:
        combined_factors = factors.sum(axis=0)
    else:
        combined_factors = (factors.multiply(pd.Series(factor_weights), axis=0)).sum(axis=0)

    if proportion is None:
        return combined_factors

    ts_row = combined_factors.dropna()
    n = int(len(ts_row) * proportion)
    row_output = pd.concat([pd.Series(1, index=ts_row.nlargest(n).index),
                            pd.Series(-1, index=ts_row.nsmallest(n).index)])

    return row_output.loc[row_output.index.drop_duplicates(keep=False)]
File:      ~/sig-env/lib/python3.7/site-packages/sigtech/framework/strategies/equity_factor_basket.py
Type:      function

This method takes a weighted sum of the factor data across the universe of securities, ranks each constituent based on its factor score and assigns +/-1 signals for the top/bottom proportion of the universe.

This logic is visible in the following implementation. The process begins by providing the necessary factor data:

df = pd.DataFrame({
    'Stock.1': [1, 3], 'Stock.2': [-2, 4.7],  'Stock.3': [9, -2], 'Stock.4': [4, 4],
    'Stock.5': [0.3, 2], 'Stock.6': [-2.2, 6], 'Stock.7': [1, 0], 'Stock.8': [0, 4],
    'Stock.9': [3.5, 4], 'Stock.10': [2, 1]
}, index=['Factor.1', 'Factor.2'])

df

The factor scores are then summed and sorted:

Input:

df.sum().sort_values()

Output:

Stock.7     1.0
Stock.5     2.3
Stock.2     2.7
Stock.10    3.0
Stock.6     3.8
Stock.1     4.0
Stock.8     4.0
Stock.3     7.0
Stock.9     7.5
Stock.4     8.0
dtype: float64

This is followed by assigning +1 to the top 20% and -1 to the bottom 20%.

Input:

default_factors_to_weight_function(df, proportion=0.2)

Output:

Stock.4    1
Stock.9    1
Stock.7   -1
Stock.5   -1
dtype: int64

The following code performs the same function as above but on a weighted sum of factor scores:

Input:

factor_weights = {
    'Factor.1': 0.1,
    'Factor.2': 0.9
}

default_factors_to_weight_function(df, factor_weights, proportion=0.2)

Output:

Stock.6    1
Stock.2    1
Stock.3   -1
Stock.7   -1
dtype: int64

Custom factors to weights#

The sigtech.framework.strategies.equity_factor_basket class contains a suite of available factors to weights functions. If additional methods are required you can define your own. Arguments can be passed through the factors_to_weight_kwargs parameter in EquityFactorBasket.

A simple sum combination and long only allocation is defined:

def long_only_selection(factors, **kwargs):

    factors = factors.sum(axis=0)

    n = int(len(factors) * 0.2)

    long = pd.Series(1, index=factors.nlargest(n).index)

    return long

Input:

long_only_selection(df)

Output:

Stock.4    1
Stock.9    1
dtype: int64

Building the strategy#

The allocation_function parameter, inherited from SignalStrategy, defines how you allocate to the selected stocks from the factors_to_weight_function.

In the following example, NORMALIZE_ZERO_FILLED is used to equally weight your long and short stocks:

efbs = sig.EquityFactorBasket(
    currency=CURRENCY,
    start_date=START_DATE,
    universe_filter=UNIVERSE,
    universe_mapping=UNIVERSE_MAPPING,
    factor_exposure_generator=factor_definitions,
    factors_to_weight_function=default_factors_to_weight_function,
    rebalance_frequency='EOM',
    allocation_function=sig.EquityFactorBasket.AVAILABLE_ALLOCATION_FUNCTIONS.NORMALIZE_ZERO_FILLED
)

efbs.history().plot()

Querying factor data#

Factor data can be queried for your strategy:

efbs.factor_exposures_on_date(dtm.date(2022, 1, 4))