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 :ref:`main callback
` functions. This is the main :ref:`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. .. py:module:: blueshift.core.algorithm.strategy .. autoclass:: Strategy() :noindex: Sub-strategy methods and attributes ++++++++++++++++++++++++++++++++++++ .. autoattribute:: Strategy.name .. automethod:: Strategy.initialize .. automethod:: Strategy.before_trading_start .. automethod:: Strategy.handle_data .. automethod:: Strategy.on_data .. automethod:: Strategy.on_trade .. automethod:: Strategy.after_trading_hours .. automethod:: Strategy.analyze .. automethod:: Strategy.on_error .. automethod:: Strategy.on_cancel The callbacks are similar in functionality as the :ref:`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 ---------------------- .. py:module:: blueshift.core.algorithm.algorithm :noindex: .. automethod:: TradingAlgorithm.add_strategy 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 ---------------------- .. py:module:: blueshift.core.algorithm.algorithm :noindex: .. automethod:: TradingAlgorithm.get_context Cancelling a sub-strategy -------------------------- .. automethod:: TradingAlgorithm.cancel_strategy 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 (:ref:`trading APIs`), including order placement, order management, algo order and stoploss/ take-profit APIs will maintain and track their own contexts. Simulation APIs (:ref:`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 :ref:`squareoff` API, the pipeline APIs (:ref:`pipeline`) and some :ref:`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. .. code-block:: python 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. .. code-block:: python 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.