Basket strategy#

In the SigTech platform, a basket strategy serves as a general framework for managing various types of assets or sub-strategies.

It can be used to trade multiple instruments, rebalancing them on a fixed schedule. These instruments can be assets (such as stocks, ETFs, indices, commodities, futures, options, and bonds) or other strategies; you can even trade basket strategies inside another basket strategy.

Initialize SigTech

Import Python libraries:

import datetime as dtm
import numpy as np
import pandas as pd

import sigtech.framework as sig

Initialize the SigTech framework environment:

env = sig.init()

Create a basket strategy#

Basket strategies can trade assets, strategies, or both.

For convenience, first define two helper functions with some constants for currency and start and end dates.

CURRENCY = "USD"
START_DATE = dtm.datetime(2023, 1, 1)
END_DATE = dtm.datetime(2024, 1, 1)

Define a helper function that creates basket strategies:

def create_basket_strat(
        currency=None,
        start_date=None,
        end_date=None,
        **params,
    ):
    currency = currency or CURRENCY
    start_date = start_date or START_DATE
    end_date = end_date or END_DATE
    strat = sig.BasketStrategy(
        currency=currency,
        start_date=start_date,
        end_date=end_date,
        **params,
    )
    print(strat)
    return strat

Define a helper function that creates rolling future strategies:

def create_rf_strat(
        currency=None,
        start_date=None,
        end_date=None,
        **params,
    ):
    currency = currency or CURRENCY
    start_date = start_date or START_DATE
    end_date = end_date or END_DATE
    return sig.RollingFutureStrategy(
        currency=currency,
        start_date=start_date,
        end_date=end_date,
        **params,
    )

Create a basket strategy of assets#

Create a basket strategy that trades two assets (frozen orange juice futures and coffee futures).

First, retrieve the futures contract instruments to trade:

jo_ct = sig.obj.get("JO COMDTY FUTURES GROUP").active_contract()
kc_ct = sig.obj.get("KC COMDTY FUTURES GROUP").active_contract()

Then, use the create_basket_strat helper function to create a basket strategy that trades the futures contract instruments:

jo_kc_asset_basket = create_basket_strat(
    constituent_names=[jo_ct.name, kc_ct.name],
    weights=[0.5, 0.5],
    rebalance_frequency="EOM",
    ticker="JO KC ASSET BASKET",
)
jo_kc_asset_basket.history().tail()

In the code cell above:

  • The constituent_names parameter is a list of instrument object names.

  • The weights parameter is a list of target weightings for the constituents specified in constituent_names.

    By default, constituent weights are percentages of the strategy’s total funds, expressed as a fraction of 100. For example, to specify constituent weights of 25%, 25%, and 50% for a strategy with three constituents, set weights=[0.25, 0.25, 0.50].

  • The rebalance_frequency parameter specifies when the basket strategy will rebalance its allocated funds between its constituents, using the weightings specified in weights as targets.

View the strategy’s holdings:

jo_kc_asset_basket.plot.portfolio_table(
    dts="TOP_ORDER_PTS",
)

Create a basket strategy of strategies#

Create a basket strategy that trades two rolling future strategies (one that trades frozen orange juice futures and one that trades coffee futures).

First, use the create_rf_strat helper function to create the rolling future strategies:

jo_rfs = create_rf_strat(contract_code="JO", contract_sector="COMDTY")
kc_rfs = create_rf_strat(contract_code="KC", contract_sector="COMDTY")

Then, use the create_basket_strat helper function to create a basket strategy that trades the rolling future strategies:

jo_kc_rfs_basket = create_basket_strat(
    constituent_names=[jo_rfs.name, kc_rfs.name],
    weights=[0.5, 0.5],
    rebalance_frequency="EOM",
    ticker="JO KC RFS BASKET",
)
jo_kc_rfs_basket.history().tail()

Create a basket strategy of assets and strategies#

Create a basket strategy that trades two assets and two rolling future strategies (one each for frozen orange juice futures and coffee futures):

jo_kc_asset_rfs_basket = create_basket_strat(
    constituent_names=[jo_ct.name, kc_ct.name, jo_rfs.name, kc_rfs.name],
    weights=[0.25, 0.25, 0.25, 0.25],
    rebalance_frequency="EOM",
    ticker="JO KC ASSET RFS BASKET",
)
jo_kc_asset_rfs_basket.history().tail()

Weight the constituents in a basket strategy#

When you create a basket strategy, you supply a list of constituent names (constituent_names), and a corresponding list of target weights (weights).

By default, SigTech interprets the weights parameter as a list of percentages as fractions of 100. On a schedule defined by the rebalance_frequency parameter, the basket strategy rebalances its allocated funds between constituents, aiming to match the specified weights as closely as possible.

Use units to weight a basket strategy#

Instead of rebalancing the constituents of a basket strategy using targeted percentages, you can use a list of the number of units of each constituent to trade for constituents, weights, you can rebalance to a list of how many of each thing units such as a targeted number of shares or contracts.

To do this, use the unit_type parameter.

Create a basket strategy that buys one JO contract and sells one KC contract:

futures_basket_units = sig.BasketStrategy(
    currency=currency,
    start_date=start_date,
    end_date=end_date,
    constituent_names=[jo_rfs.name, kc_rfs.name],
    weights=[1, -1],
    rebalance_frequency="SOM",
    unit_type="TRADE",
)
futures_basket_units.history().tail()

Examine the holdings:

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

Use precise weights and instant sizing#

In the previous examples, the targeted weights sometimes don’t match up with the position weights actually traded in the strategies. This is because, by default, the number of units to trade is calculated using the previous day’s closing price, simulating the experience of a portfolio manager with access to EOD prices.

To achieve the exact weights specified in weights:

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

  • Set ignore_transaction_costs to True, since transaction costs can impact weighting.

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

  • Set the shared action time to be the exchange close.

jo_ct = sig.obj.get("JO COMDTY FUTURES GROUP").active_contract()
sig.BasketStrategy(
    currency=currency,
    start_date=start_date,
    end_date=end_date,
    constituent_names=[jo_rfs.name, kc_rfs.name],
    weights=[1, -1],
    rebalance_frequency="SOM",
    unit_type="TRADE",
    instant_sizing=True,
    include_trading_costs=False,
    size_time_input=dtm.time(16, 30),
    execution_time_input=dtm.time(16, 30),
    size_timezone_input=jo_ct.exchange().timezone,
    execution_timezone_input=jo_ct.exchange().timezone
).history().tail()

Examine the positions:

futures_basket_instant.plot.portfolio_table(
    dts="TOP_ORDER_PTS",
    end_dt=end_date,
)

Rebalance the constituents a basket strategy#

Basket strategies rebalance their constituents at specified intervals using the weights parameter, which is a list of target weightings corresponding to the instruments referenced by constituent_names.

To control when a basket strategy rebalances, specify rebalance_frequency, a frequency string, when you create the strategy.

Create a basket strategy that rebalances at the end of the month ("EOM"):

jo_kc_basket = sig.BasketStrategy(
    currency="USD",
    start_date=start_date,
    end_date=end_date,
    constituent_names=[jo_ct.name, kc_ct.name],
    weights=[0.5, 0.5],
    rebalance_frequency="EOM",
)
jo_kc_basket.history().tail()

Here are some example frequency strings:

Frequency string

Description

"EOM"

End of the month

"SOM"

Start of the month

"YEARLY"

Every year

"3M-IMM"

Quarterly, using the IMM dates

"3BD"

Every third business day

"15DOM"

On the 15th day of the month

"1W-THU"

Every week, on Thursdays

"2M"

Every second month

"6M-28DOM"

Every six months, on the 28th

List the static frequency strings:

freqs = sig.BasketStrategy.AVAILABLE_REBALANCE_FREQS
frequencies = [f for f in dir(freqs) if not f.startswith("_")]
frequency_strings = [
    eval(f"freqs.{f}") for f in frequencies if f == f.upper()
]
frequency_strings

List the methods for getting more granular frequency strings:

frequency_methods = [f for f in frequencies if f == f.lower()]
frequency_methods

Get some example frequency strings:

print(freqs.business_days(3))
print(freqs.day_of_month(15))
print(freqs.thursdays(1))
print(freqs.months(2))
print(freqs.day_of_month(28, 6))

Select a business day convention#

If a rebalancing date falls on a holiday, SigTech uses business day conventions to select the actual date to perform the rebalance. To select a business day convention to use for rebalancing dates that fall on holidays, specify rebalance_bdc, a business day convention string, when you create the strategy.

Reschedule to the following business day if the original rebalance date is a holiday:

jo_kc_basket = sig.BasketStrategy(
    currency="USD",
    start_date=start_date,
    end_date=end_date,
    constituent_names=[
        jo_ct.name,
        kc_ct.name,
    ],
    weights=[0.5, 0.5],
    rebalance_frequency="EOM",
    rebalance_bdc="FOLLOWING",
)
jo_kc_basket.history().tail()

List all of the business day convention strings:

bdc_strings = [
    eval(f"sig.calendar.{const}")
    for const in dir(sig.calendar)
    if const.startswith("BDC")
]
bdc_strings

All of the business day convention strings and what they do:

Business day convention string

Description

"FOLLOWING"

Following business day

"PRECEDE"

Preceding business day

"NONE"

Original rebalance date

"MOD_FOL"

Modified following business day

"MOD_PRE"

Modified preceding business day

The “modified” business day conventions ("MOD_FOL" and "MOD_PRE") ensure that the strategy is rebalanced in the same month in which the rebalance was originally scheduled.

  • When you set rebalance_bdc to "MOD_FOL", a rebalance that falls on a holiday is performed on the preceding business day if the following business day is in the next month. Otherwise, the rebalance is performed on the following business day.

  • Similarly, when you set rebalance_bdc to "MOD_PRE", a rebalance that falls on a holiday is performed on the following business day if the preceding business day is in the next month. Otherwise, the rebalance is performed on the preceding business day.

Create a custom rebalancing schedule#

To create a custom rebalancing schedule, use the rebalance_dates parameter.

The rebalance_dates parameter lets you supply a list of dates to override the rebalance dates scheduled by the rebalance_frequency parameter.

Create a list of rebalancing dates:

cl = sig.obj.get("CL COMDTY FUTURES GROUP").active_contract()
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]

Create a basket strategy using the custom rebalancing dates:

cl_active = sig.obj.get("JO COMDTY FUTURES GROUP").active_contract()
_basket_of_assets = sig.BasketStrategy(
    currency="USD",
    start_date=start_date,
    end_date=end_date,
    constituent_names=[jo_ct.name, kc_ct.name],
    weights=[0.5, 0.5],
    rebalance_frequency="EOM",
    rebalance_dates=schedule,
)
_basket_of_assets.history().tail()

Example basket strategies#

Trade front- and back-month rolling future strategies#

Create a basket strategy that combines two rolling future strategies trading crude oil. One trades the front-month contract; the other trades the back-month contract.

rf_front = sig.RollingFutureStrategy(
    currency=currency,
    start_date=start_date,
    end_date=end_date,
    contract_code="CL",
    contract_sector="COMDTY",
    rolling_rule="front",
    front_offset="-4:-2",
    contract_offset=0,
)
rf_back = sig.RollingFutureStrategy(
    currency=currency,
    start_date=start_date,
    end_date=end_date,
    contract_code="CL",
    contract_sector="COMDTY",
    rolling_rule="front",
    front_offset="-4:-2",
    contract_offset=1,
)

Compare the holdings for the two rolling future strategies:

rf_front.plot.portfolio_table(
    dts="TOP_ORDER_PTS",
    end_dt=end_date,
)
rf_back.plot.portfolio_table(
    dts="TOP_ORDER_PTS",
    end_dt=end_date,
)

Create a dollar-neutral basket that goes long the back-month version of the rolling future strategy, and short the front-month version, rebalancing at the end of each month:

term_basket = sig.BasketStrategy(
    currency=currency,
    start_date=start_date,
    end_date=end_date,
    constituent_names=[rf_front.name, rf_back.name],
    weights=[-0.5, 0.5],
    rebalance_frequency="EOM",
)
term_basket.history().tail()

Trade another basket#

Create a basket strategy that trades the basket strategy in the previous example, and another rolling future strategy.

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

rfs = sig.RollingFutureStrategy(
    currency=currency,
    start_date=start_date,
    contract_code="VG",
    contract_sector="INDEX",
    rolling_rule="front",
    front_offset="-4:-2",
    contract_offset=0,
)

Use symmetric difference to compare the business day calendars of the two strategies:

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

Since the calendars of the two strategies aren’t the same, use the intersecting_business_days parameter to create a calendar using only the trading dates that appear in both:

baskets_basket = sig.BasketStrategy(
    currency=currency,
    start_date=start_date,
    end_date=end_date,
    constituent_names=[term_basket.name, rfs.name],
    weights=[0.5, 0.5],
    rebalance_frequency="2DOM",
    intersecting_business_days=True,
)
baskets_basket.history().tail()

Click Expand All in the following tree plot to view a visualization of the hierarchy of the basket and its constituents:

baskets_basket.plot.tree(dtm.datetime(2023, 6, 1))

CashBasketStrategy#

The CashBasketStrategy is an extension of the BasketStrategy that enables the trading of foreign exchange (FX) spot instruments.

It focuses on cash equivalents in currency pairs, using parameters tailored for FX requirements and implementing specific logic for handling currency exposure and related risks.

Input:

sig.CashBasketStrategy?

This example creates 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.

Input:

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

The following example uses the front- and back-month CL rolling futures strategies from above, together with a dictionary of subscriptions (“subs”) and redemptions (“reds”). It uses the option to trigger rebalancing at a subscription or redemption event. 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()