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).
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¶
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>.
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.