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