Basket Strategy

A simple building block for creating strategies, whose underlying assets or strategies are rebalanced to fixed weights, on a fixed rebalancing schedule.

Sections

Setup

This section will import relevant internal and external libraries, as well as setting up the platform environment.

Learn more: for information on setting up the environment, see Environment setup.

import pandas as pd
import datetime as dtm
import seaborn as sns
sns.set(rc={'figure.figsize': (18, 6)})

import sigtech.framework as sig

end_date = dtm.date(2021, 11, 30)
sig.init(env_date=end_date)

Building block overview

Overview of the BasketStrategy class.

sig.BasketStrategy?

Rebalancing frequencies

Although it is possible to build custom rebalancing schedules, the majority of common schedules are already supported.

The following AVAILABLE_REBALANCE_FREQS object can help with finding the correct string to supply to a BasketStrategy, and other strategies more generally, to achieve the desired rebalancing behaviour.

freqs = sig.BasketStrategy.AVAILABLE_REBALANCE_FREQS
frequencies = [f for f in dir(freqs) if not f.startswith('_')]
frequency_strings = sorted([f for f in frequencies if f == f.upper()])
frequency_strings

Any of the attributes above, represented by upper-case strings, yield the correct input string directly:

print(freqs.END_OF_MONTH)
print(freqs.IMM)
print(freqs.START_OF_MONTH)
print(freqs.YEARLY)
frequency_methods = sorted([f for f in frequencies if f == f.lower()])
frequency_methods

Any of the above attributes are methods that take an input and return the correct string format. The thursdays method is an example:

freqs.thursdays?

The input n is how often, in terms of weeks, to rebalance.

So freqs.thursdays(2) returns the string to input when a strategy should rebalance every second Thursday.

The following cell cycles through some of these methods and shows sample output (the strings to supply as the rebalancing_frequency argument):

print(freqs.business_days(2))
print(freqs.day_of_month(2))
print(freqs.fridays(2))
print(freqs.thursdays(2))
print(freqs.weeks(2))

Examples

currency = 'USD'

Basket of instruments

This basket will trade the March 2022 Crude Oil (CL) and Natural Gas (NG) futures contracts.

Fetch objects and combine their histories on a shared calendar.

cl = sig.obj.get('CLH22 COMDTY') 
ng = sig.obj.get('NGH22 COMDTY')
prices = pd.DataFrame({'CL': cl.history(), 'NG': ng.history()}).dropna()
prices.head()

The shared history begins in 2016. Examine the volume being traded to identify a realistic start date.

volumes = pd.DataFrame({
    'CL': cl.history('Volume'), 
    'NG': ng.history('Volume')
}).dropna()
volumes.sum(axis=1).plot();

Based on the above, choose a start date from the beginning of 2021.

start_date = dtm.date(2021, 1, 3)

Create a basket that allocates 70% to the CL contract and 40% to the NG contract, rebalancing on Wednesday every three weeks. There is no requirement for weights to sum to 100% and short positions are also possible.

If the scheduled rebalancing day falls on a market holiday, then rebalance on the following business day.

weights = [0.7, 0.4]
# list of names of constituents, not constituent objects
constituent_names = [cl.name, ng.name]
rebalance_frequency = freqs.wednesdays(3) # '3W-WED'
rebalance_bdc = 'FOLLOWING'
futures_basket = sig.BasketStrategy(
    currency=currency,
    start_date=start_date,
    end_date=end_date,
    constituent_names=constituent_names,
    weights=weights,
    rebalance_frequency=rebalance_frequency,
    rebalance_bdc=rebalance_bdc
)
futures_basket.build()
futures_basket.plot.portfolio_table(
    dts='TOP_ORDER_PTS',
    end_dt=dtm.date(2021, 1, 31)
)

Basket of strategies

The BasketStrategy building block is also able to take strategies as constituents. The following example creates two RollingFutureStrategy objects trading crude oil, a 'front month' version and a 'back month' version, and then constructs a basket from these two strategies.

The relevant information needed to construct a RollingFutureStrategy (RFS) is obtained directly from the relevant FuturesContractGroup.

# utilise a longer history
start_date = dtm.date(2018, 1, 3)

cl_group = cl.group()
contract_code = cl_group.contract_code
contract_sector = cl_group.contract_sector

# specifics of how rolling is to be done
rolling_rule = 'front' 
front_offset = '-4:-2'
rf_front = sig.RollingFutureStrategy(
    currency=currency,
    start_date=start_date, 
    contract_code=contract_code,
    contract_sector=contract_sector,
    rolling_rule=rolling_rule,
    front_offset=front_offset,

    # determine which contract to trade
    contract_offset=0,
)
rf_front.build()
rf_back = sig.RollingFutureStrategy(
    currency=currency,
    start_date=start_date, 
    contract_code=contract_code,
    contract_sector=contract_sector,
    rolling_rule=rolling_rule,
    front_offset=front_offset,

    # determine which contract to trade
    contract_offset=1,
)
rf_back.build()

Comparison of holdings for the two rolling futures strategies.

As expected, whilst the 'front' version of the strategy is rolling out of Gs and into Hs, the back version is rolling out of Hs and into Js.

rf_front.plot.portfolio_table(
    dts='TOP_ORDER_PTS',
    end_dt=dtm.date(2018, 1, 31)
)
rf_back.plot.portfolio_table(
    dts='TOP_ORDER_PTS',
    end_dt=dtm.date(2018, 1, 31)
)

Create a dollar neutral basket that goes long the back month version of the RFS and short the front month version, rebalancing at the end of each month.

The default value of rebalance_bdc is taken as no argument is supplied. This results in the preceding business day being used to rebalance if that dictated by the schedule falls on a market holiday.

strategy_weights = [-0.5, 0.5]
strategy_constituent_names = [rf_front.name, rf_back.name]
rebalance_frequency = freqs.END_OF_MONTH # 'EOM'

term_basket = sig.BasketStrategy(
    currency=currency,
    start_date=start_date,
    end_date=end_date,
    constituent_names=strategy_constituent_names,
    weights=strategy_weights,
    rebalance_frequency=rebalance_frequency,
)
term_basket.build()

The history can be fetched and plotted directly by calling history or the PlotWrapper. It can be used to create an interactive performance plot.

term_basket.plot.performance()

Basket of baskets

A basket strategy, like any strategy built within the SigTech platform, is itself a Strategy, i.e. it inherits from the sig.Strategy class. This means it is possible to create a BasketStrategy that allocates amongst other BasketStrategy objects.

This example creates a basket that allocates between the crude term structure basket built above and a EURO STOXX 50 (VG) Rolling Futures Strategy.

The same rolling rules as above are used with a contract_offset of zero (the front quarterly contract is held).

group = sig.obj.get('VG INDEX FUTURES GROUP')

rfs = sig.RollingFutureStrategy(
    currency=currency,
    start_date=start_date, 
    contract_code=group.contract_code,
    contract_sector=group.contract_sector,
    rolling_rule=rolling_rule,
    front_offset=front_offset,
    contract_offset=0,
)
rfs.build()
basket_weights = [0.5, 0.5]
basket_constituent_names = [term_basket.name, rfs.name]
rebalance_frequency = freqs.day_of_month(2) # '2DOM'

The business day calendars of the two strategies do not precisely agree, seen in the symmetric difference of the dates for which they have valuations.

set(rfs.history().index).symmetric_difference(
    set(term_basket.history().index))

The intersecting_business_days parameter can be used to ensure that the intersection of the business days used for each strategy is used for the resulting strategy.

It is easy to check that this is true for baskets_basket.

baskets_basket = sig.BasketStrategy(
    currency=currency,
    start_date=start_date,
    end_date=end_date,
    constituent_names=basket_constituent_names,
    weights=basket_weights,
    rebalance_frequency=rebalance_frequency,
    intersecting_business_days=True
)
baskets_basket.build()

ClickExpand All in the following tree plot to view a visualisation of how the top level basket is trading both a lower level basket and a Rolling Futures Strategy. It also shows the lower level constituents that each of these strategies is holding.

baskets_basket.plot.tree(dtm.datetime(2021, 11, 30))

Strategy details & parameters

Custom Rebalancing schedules

BasketStrategy admits a parameter called rebalance_dates, which takes the form of a list of dtm.date or dtm.datetime objects and superseded rebalance_frequency in determining the dates on which the strategy should rebalance.

The example below creates a custom schedule using the SchedulePeriodic class and then passes this to BasketStrategy

start_date = dtm.date(2018, 7, 2)
_end_date = dtm.date(2019, 12, 31)

weekly_fridays = sig.SchedulePeriodic(
    start_date=start_date,
    end_date=_end_date,
    holidays=cl.exchange().holidays,
    frequency=freqs.fridays(1)
).all_data_dates()

schedule = [d for d in weekly_fridays if d.month in [3, 6, 9, 12]]
schedule[:5]
custom_rebalance_basket = sig.BasketStrategy(
    currency=currency,
    start_date=start_date,
    end_date=_end_date,
    weights=[0.9, 0.1],
    constituent_names=[rf_front.name, rfs.name],

    # provide custom schedule
    rebalance_dates=schedule,
    # custom schedule supersedes
    rebalance_frequency=freqs.END_OF_MONTH
)
custom_rebalance_basket.build()


custom_rebalance_basket.plot.portfolio_table(
    dts='TOP_ORDER_PTS',
    end_dt=dtm.date(2018, 9, 14),
    flatten=True
)

Trade units instead of weights

The baskets constructed in the examples above have a list of target weights that they aim to rebalance.

It is also useful to specify the number of units to target, such as shares and contracts.

Note: this is completed with the use of the unit_type parameter.

Using the futures contracts introduced above, the following example creates a basket that buys one CL contract and sells one NG contract:

futures_basket_units = sig.BasketStrategy(
    currency=currency,
    start_date=start_date,
    end_date=end_date,
    constituent_names=[cl.name, ng.name],
    weights=[1, -1],
    rebalance_frequency=freqs.START_OF_MONTH,
    unit_type='TRADE'
)
futures_basket_units.build()

Note: specifying unit_type=TRADE, when viewing the corresponding portfolio_table,clearly demonstrates the desired behaviour.

futures_basket_units.plot.portfolio_table(
    dts='TOP_ORDER_PTS',
    unit_type='TRADE'
)

Precise weights using instant sizing

In the first futures basket example, the position weights achieved differ slightly from the targets.

Example: the weight accorded to NGH22 COMDTY at 2021/01/05 21:30:00 is 72.001%, whilst the target weight is exactly 70%.

futures_basket.plot.portfolio_table(
    dts='TOP_ORDER_PTS',
    end_dt=dtm.date(2021, 1, 5)
)

One of the main reasons for this difference is that the number of units to trade, or sizing, is done using the previous day's closing price for both of the futures contracts that underlie the basket. This is to more accurately simulate the experience of a portfolio manager with access to EOD prices.

The instant_sizing parameter can be used to execute orders on the same day that they are sized.

# basket parameters
weights = [0.7, 0.4]
constituent_names = [cl.name, ng.name]
rebalance_frequency = freqs.wednesdays(3)
rebalance_bdc = 'FOLLOWING'

Useful data on exchange close times and timezones:

cl.exchange().exchange_close
cl.exchange().timezone

To achieve the exact weights specified:

  • Set instant_sizing to True to ensure orders are executed on the day they are generated.

  • Ignore transaction costs, since the slippage here can alter the resulting weights.

  • Set the sizing time and the execution time to agree.

  • Although not completely necessary in this example, set this shared action time to be the exchange close.

action_time = dtm.time(16, 30)
timezone = cl.exchange().timezone
futures_basket_instant = sig.BasketStrategy(
    currency=currency,
    start_date=dtm.date(2021, 1, 3),
    end_date=end_date,
    constituent_names=constituent_names,
    weights=weights,
    rebalance_frequency=rebalance_frequency,
    rebalance_bdc=rebalance_bdc, 

    # sizing and trading time parameters
    instant_sizing=True,
    
    # remove trading costs to avoid slippage
    include_trading_costs=False,
    
    # sizing time and execution time must agree
    size_time_input=action_time,
    execution_time_input=action_time,
    size_timezone_input=timezone,
    execution_timezone_input=timezone
)
futures_basket_instant.build()

The precise target weights are now achieved:

futures_basket_instant.plot.portfolio_table(
    dts='TOP_ORDER_PTS',
    end_dt=dtm.date(2021, 1, 31)
)

CashBasketStrategy

The CashBasketStrategy is an extension of the BasketStrategy that enables FX spot to be traded.

sig.CashBasketStrategy?

Create a basket that is long EURUSD and short GBPUSD.

fx_basket = sig.CashBasketStrategy(
    currency='USD',
    start_date=dtm.date(2016, 1, 4),
    end_date=dtm.date(2017, 1, 4),
    constituent_names=['EUR CASH','GBP CASH'],
    weights=[0.8, -0.8],
    rebalance_frequency=freqs.fridays(1), # '1W-FRI'
)
fx_basket.build()
fx_basket.history().plot()
fx_basket.plot.portfolio_table(
    dts='TOP_POSITION_CHG_PTS',
    end_dt=dtm.date(2016, 1, 31)
)

SizeVaryingBasket

The SizeVaryingBasket building block is a basket strategy with the additional functionality to support the handling of fund subscriptions and redemptions.

from sigtech.framework.strategies.basket_strategies import SizeVaryingBasketStrategy
SizeVaryingBasketStrategy?

This example uses the front and back month CL rolling futures strategies from above, together with a dictionary of 'subs' and 'reds'. There is an option to trigger rebalancing at a subscription or redemption event, which is used in this case. The initial_cash is also altered.

sv_basket = SizeVaryingBasketStrategy(
    currency=currency,
    start_date=start_date,
    constituent_names=[rf_front.name, rf_back.name],
    weights=[0.5, 0.5],
    rebalance_frequency=freqs.END_OF_MONTH,
    
    # subs/reds parameters
    initial_cash=500,
    subs_reds_data={
        dtm.date(2019, 2, 10): 200,
        dtm.date(2019, 4, 17): -400,
        dtm.date(2021, 2, 3): 300,
    },
    use_subs_reds_to_rebalance=True,
)
sv_basket.build()
sv_basket.history().plot();

In addition to the usual portfolio_table, the timeline method of a strategy's PlotWrapper is an excellent way to get an overview of the details of a strategy.

Selecting the period 01/02/2021 - 15/02/2021, displaying 'POSITIONS' and selecting all boxes except 'Flatten' clearly displays the cash inflow and the associated _add_cash method being called.

Note: clicking on the top level strategy displays the substrategies.

sv_basket.plot.timeline()

Last updated

© 2023 SIG Technologies Limited