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:

IndexSTOCK 1 REINV STRATEGYSTOCK 2 REINV STRATEGY

leverage

0.5

0.5

liquidity

100

100

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

IndexValues

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:

from sigtech.framework.strategies.equity_factor_basket import default_factors_to_weight_function

default_factors_to_weight_function?

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

default_factors_to_weight_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:

df.sum().sort_values()

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

default_factors_to_weight_function(df, proportion=0.2)

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

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

default_factors_to_weight_function(df, factor_weights, proportion=0.2)

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
long_only_selection(df)

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))

Last updated

© 2023 SIG Technologies Limited