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.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:
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)
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}
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
.
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()
Total market cap is defined as a size factor:
factor_definitions.add_raw_factor_timeseries(
'size',
history_field='MARKETCAP'
)
Quarterly dividend yield is defined as a value factor:
factor_definitions.add_raw_factor_timeseries(
'value',
fundamental_field='Dividend Yield',
fundamental_freq='Quarterly'
)
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
)
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 |
If no argument for the
factors_to_weight_function
is supplied, the default_factors_to_weight_function
is used:Input
Output
from sigtech.framework.strategies.equity_factor_basket import default_factors_to_weight_function
default_factors_to_weight_function?
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
Output
default_factors_to_weight_function??
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
Output
df.sum().sort_values()
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
Output
default_factors_to_weight_function(df, proportion=0.2)
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
Output
factor_weights = {
'Factor.1': 0.1,
'Factor.2': 0.9
}
default_factors_to_weight_function(df, factor_weights, proportion=0.2)
Stock.6 1
Stock.2 1
Stock.3 -1
Stock.7 -1
dtype: int64
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
Output
long_only_selection(df)
Stock.4 1
Stock.9 1
dtype: int64
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()

Factor data can be queried for your strategy:
efbs.factor_exposures_on_date(dtm.date(2022, 1, 4))

Last modified 2mo ago