Multiple Strategies in a Single Algo

Sometimes we may want to run multiple strategies with different logic and order/ positions management within a single algo. This maybe useful when we are running an ensemble of strategies (with a capital allocation rule) in a single algorithm. There are other cases too. Blueshift API support such use cases by allowing to dynamically add or remove sub-strategies in a single algo.

Sub-Strategies

An algo run on Blueshift with the main context as defined by the context variable available through the main callback functions. This is the main context of the algo run. However, by using the sub-strategies API, we can add a sub-strategy to the algo (and remove it when required). Each such sub-strategy runs within its own context - also known as sub-context. Since each such sub-contexts are maintained independently, a sub-strategy can maintain its independent orders and positions tracking. We can generally use the API functions as is in either in the context of the main strategy or any sub-strategies. Blueshift will automatically apply the correct context. This allows the main and the sub-strategies to work independently from each other, without being aware of each others presence.

Defining a sub-strategy

A sub-strategy can be defined by sub-classing the Strategy class and then overriding the event handlers as required.

class blueshift.core.algorithm.strategy.Strategy

Interface for class based strategy on Blueshift. Subclass this strategy to create a sub-strategy. This class can also be used to create the main strategy (instead of defining the main callback functions directly in the strategy code).

Parameters:
  • name (str) – Name of the strategy.

  • initial_capital (float) – Initial capital allocation.

Sub-strategy methods and attributes

Strategy.name

Name (and the context name) of this strategy.

Strategy.initialize(context)

override this function to run at initialization.

Strategy.before_trading_start(context, data)

override this function to run at start of the day.

Strategy.handle_data(context, data)

override this function to run at every cycle.

Strategy.on_data(context, data)

override this function to run at new data arrival.

Strategy.on_trade(context, data)

override this function to run at any order fill event.

Strategy.after_trading_hours(context, data)

override this function to run at end of the day.

Strategy.analyze(context, perf)

override this function to run at end of the run.

Strategy.on_error(context, error)

override this function to run before the strategy exit on error.

Strategy.on_cancel(context)

override this function to run before the strategy exit on user cancel.

The callbacks are similar in functionality as the main event callbacks. These will be automatically called (on appropriate events) once a sub-strategy is added in the algo run. Once a sub-strategy is removed, its callbacks will not be invoked anymore.

Adding a sub-strategy

TradingAlgorithm.add_strategy(strategy)

Add a sub strategy to the current algo. Sub strategies allow a modular approach to incorporate multiple independent rules in a single strategy. For more see :ref: sub-strategies<Sub-Strategies>.

Note

Sub strategies can only be added in regular modes (not in EXECUTION mode).

Parameters:

strategy (Strategy.) – The strategy to add.

This API method must be called from the main strategy, as a sub-strategy can be added from the main context only.

Fetching sub-strategy

TradingAlgorithm.get_context(name=None)

The the algo context by name. If the current algo is running with sub-strategies added, fetch the corresponding sub context by the name. Omitting the name argument will return the main context.

Parameters:

name (str) – The name of the sub strategy to fetch.

Cancelling a sub-strategy

TradingAlgorithm.cancel_strategy(name, cancel_orders=True, square_off=False)

Cancel (remove) a sub strategy previously added. For more on sub-strategies, see :ref: sub-strategies<Sub-Strategies>.

Parameters:
  • name (str) – Name of the sub-strategy to remove.

  • cancel_orders (bool) – Cancel open orders on removal.

  • square_off (bool) – Square off on removal.

This API method must be called from the main strategy, or from the sub-strategy itself that is being cancelled.

Sub-strategy order and position tracking

Each sub-strategies, and the main strategy, will maintain their independent version of orders and positions tracking. That means order APIs (trading APIs), including order placement, order management, algo order and stoploss/ take-profit APIs will maintain and track their own contexts. Simulation APIs (simulation APIs) will also affect behaviour of only the respective contexts. For example, calling get_open_orders from a sub-context will return only the open orders placed from the corresponding sub-strategy.

However, a few sets of APIs are global, in the sense that they will affect all contexts - the main context as well as any sub-contexts created. This includes the squareoff API, the pipeline APIs (pipeline) and some risk management APIs). This means, for example, if we call square-off from any context, all positions in all active contexts will be squared-off by default. See more in the documentation of the respective APIs.

Below code snippet shows how to add and cancel of sub-strategies in an algo run. Sub-strategies are added in the main context, but can be removed from itself or from the main context.

from blueshift.api import order, symbol, schedule_once, cancel_strategy
from blueshift.api import add_strategy, exit_when_done, get_context
from blueshift.protocol import Strategy

class Strategy1(Strategy):

    def initialize(self, context):
        # this context refers to the sub-context for this sub-strategy
        schedule_once(self.strategy)

    def strategy(self, context, data):
        order(symbol('MSFT'), 1)
        for oid in context.orders:
            # prints only MSFT order
            print(context.orders[oid].to_dict())
        # cancelling sub-strategy from itself
        cancel_strategy(self.name)

class Strategy2(Strategy):

    def initialize(self, context):
        # this context refers to the sub-context for this sub-strategy
        schedule_once(self.strategy)

    def strategy(self, context, data):
        order(symbol('AAPL'), 1)
        for oid in context.orders:
            # prints only AAPL order
            print(context.orders[oid].to_dict())

def initialize(context):
    # this context refers to the main context
    # add the sub-strategies
    add_strategy(Strategy1('strategy1', 15000))
    add_strategy(Strategy2('strategy2', 25000))
    schedule_once(strategy)

def strategy(context, data):
    order(symbol('AMZN'), 1)
    for oid in context.orders:
        # prints only AMZN order
        print(context.orders[oid].to_dict())

    schedule_once(wrap_up)

def wrap_up(context, data):
    # cancelling sub-strategy from main context
    cancel_strategy('strategy2')
    # exit after all orders, (AAPL, MSFT and AMZN) are done
    exit_when_done()

def analyze(context, perf):
    # print the performance for AMZN from the main context
    print(context.blotter.performance)
    # print the performance for AAPL from the sub-context "strategy1"
    ctx_1 = get_context('strategy1')
    print(ctx1.blotter.performance)

The sub-strategy APIs provide a powerful way to create ensemble strategies, run multiple algos more efficiently and otherwise write clean modular algos and trading logic. Below snippet is an example.

Example - Capital allocation

Below snippet gives an example of strategy that uses sub-strategy APIs to run multiple strategy logic in parallel, and uses the main context to manage dynamic capital allocation (in this case equal capital set at the beginning of every trading day). The CapitalAllocator class implements the capital allocation logic. Each of the sub-strategies (AdvisorStrategy) trades independently based on trading signals generated by an advisor function. At the beginning of the trading day, it checks for capital allocation updates and transfer fund in or out. For positive cash transfer, it is done right away. For capital withdrawal, it waits till the market open and places unwinding trades and action the withdrawal after a while.

from blueshift.api import order_target_percent, symbol, get_context
from blueshift.api import schedule_function, date_rules, time_rules
from blueshift.api import set_commission, set_slippage
from blueshift.api import fund_transfer, add_strategy
from blueshift.protocol import Strategy
from blueshift.finance import commission, slippage
from blueshift.library.technicals.indicators import bbands, ema, rsi

class CapitalAllocator:
    """ sub-strategies capital allocation logic. """
    def __init__(self, context):
        self.context = context
        self.changes = {}
        self.cash = 0
        self.initialize()

    def initialize(self):
        """ at start, transfer out the capital from the main context. """
        init_cap = self.context.portfolio.starting_cash
        transferred = fund_transfer(-init_cap)
        self.cash -= round(transferred, 2)

    def compute(self):
        """ compute the allocation every day. """
        contexts = {s.name:get_context(s.name) for s in self.context.strategies}
        contexts = {k:v for k,v in contexts.items() if v is not None}

        self.changes = self.compute_allocation(contexts)

    def compute_allocation(self, contexts):
        n = len(contexts)

        total_value = sum(
                [contexts[s].portfolio.portfolio_value for s in contexts])
        capital = total_value + self.cash

        allocations = {k:round(capital/n) for k,v in contexts.items()}
        changes = {}

        for k in contexts:
            changes[k] = allocations[k] - contexts[k].portfolio.portfolio_value

        return changes

    def allocate(self, strategy):
        """ update the capital allocation for a given strategy. """
        capital_to_add = self.changes.get(strategy.name, 0)
        if capital_to_add == 0:
            return

        strategy.capital_change = capital_to_add

class AdvisorStrategy(Strategy):
    """ sub strategy that accepts a signal function to trade. """
    def __init__(self, name, advisor, allocator):
        self.advisor = advisor
        self.allocator = allocator
        self.can_trade = False
        self.target_position = {}
        self.signals = {}
        self.capital_change = 0
        super().__init__(name, 0)

    def initialize(self, context):
        # you must set the slippage/ commissions for each sub-strategies
        set_commission(commission.PerShare(cost=0.0, min_trade_cost=0.0))
        set_slippage(slippage.FixedSlippage(0.00))
        self.securities = [symbol('RELIANCE'),symbol('INFY')]

        schedule_function(self.release_capital, date_rules.everyday(), time_rules.at('09:15'))
        schedule_function(self.start_trading, date_rules.everyday(), time_rules.at('09:30'))
        schedule_function(self.run_strategy, date_rules.everyday(), time_rules.every_nth_minute(5))
        schedule_function(self.stop_trading, date_rules.everyday(), time_rules.at('15:00'))

    def before_trading_start(self, context, data):
        """ capture change in allocation. """
        self.allocator.allocate(self)
        self.can_trade = False
        self.target_position = {}
        self.signals = {}

        if self.capital_change > 0:
            # positive capital transfer can be done right away
            transferred = fund_transfer(self.capital_change)
            self.allocator.cash -= round(transferred, 2)
            self.capital_change = 0

    def release_capital(self, context, data):
        """ capital withdrawal requires winding down positions. """
        if self.capital_change < 0:
            weight = self.get_weight()
            for asset in context.portfolio.positions:
                pos = context.portfolio.positions[asset]
                sign = 1 if pos.quantity >= 0 else -1
                net = context.portfolio.portfolio_value
                frac = (net + self.capital_change)*weight*sign/net
                order_target_percent(asset, frac)

    def run_strategy(self, context, data):
        """ the main strategy logic. """
        if self.can_trade:
            self.generate_signals(context, data)
            self.generate_target_position(context, data)
            self.rebalance(context, data)

    def rebalance(self, context,data):
        for security in self.securities:
            if security in self.target_position:
                order_target_percent(
                    security, self.target_position[security])

    def generate_target_position(self, context, data):
        weight = self.get_weight()

        for security in self.securities:
            if self.signals[security] == 999:
                continue
            elif self.signals[security] > 0.5:
                self.target_position[security] = weight
            elif self.signals[security] < -0.5:
                self.target_position[security] = -weight
            else:
                self.target_position[security] = 0

    def generate_signals(self, context, data):
        try:
            price_data = data.history(self.securities, 'close', 375, '1m')
        except:
            return

        for security in self.securities:
            px = price_data.loc[:,security].values
            self.signals[security] = self.advisor(px)

    def start_trading(self, context, data):
        """ transfer any capital for the day. """
        self.can_trade = True

        if self.capital_change != 0:
            transferred = fund_transfer(self.capital_change)
            self.allocator.cash -= round(transferred, 2)

        self.capital_change = 0

    def stop_trading(self, context, data):
        self.can_trade = False

    def get_weight(self):
        num_secs = len(self.securities)
        return round(1.0/num_secs,2)

def advisor_bbands(px):
    upper, mid, lower = bbands(px, 300)
    if upper - lower == 0:
        return 0

    last_px = px[-1]
    dist_to_upper = 100*(upper - last_px)/(upper - lower)

    if dist_to_upper > 95:
        return -1
    elif dist_to_upper < 5:
        return 1
    elif dist_to_upper > 40 and dist_to_upper < 60:
        return 0
    else:
        return 999

def advisor_rsi(px):
    sig = rsi(px)

    if sig > 70:
        return -1
    elif sig < 30:
        return 1
    elif sig > 45 and sig < 55:
        return 0
    else:
        return 999

def advisor_ma(px):
    sig1 = ema(px, 5)
    sig2 = ema(px, 20)

    if sig1 > sig2:
        return 1
    else:
        return -1


def initialize(context):
    context.allocator = CapitalAllocator(context) # allocation logic

    # create the sub-strategies
    context.strategies = [
            AdvisorStrategy('bbands', advisor_bbands, context.allocator),
            AdvisorStrategy('rsi', advisor_rsi, context.allocator),
            AdvisorStrategy('xma', advisor_ma, context.allocator),
            ]

    # add them to the main context
    for s in context.strategies:
        add_strategy(s)

def before_trading_start(context, data):
    # update allocation every day
    context.allocator.compute()

Note in the compute method above, we are explicitly checking if any sub-strategy context is None. This is to make sure the strategy does not fail in case a sub-strategy exits on error and the corresponding context is removed and no longer available.