scubamut
6/23/2019 - 12:46 PM

Q_Z MSMP 6.00

# ZIPLINE IMPORTS

import pandas as pd
import numpy as np
import re
import scipy
from collections import OrderedDict
from cvxopt import solvers, matrix, spdiag
import talib
from zipline.api import attach_pipeline, pipeline_output, get_datetime
from zipline import run_algorithm
from zipline.api import set_symbol_lookup_date, order_target_percent, get_open_orders
from zipline.api import order, record, set_commission
from zipline.api import symbol, symbols, get_datetime, schedule_function, get_environment
from zipline.finance import commission
from zipline.utils.events import date_rules, time_rules
from zipline.pipeline import Pipeline
from zipline.pipeline.data import USEquityPricing
from zipline.pipeline.filters import StaticAssets
from datetime import datetime, timezone, timedelta
import pytz

# CONSTANTS

GTC_LIMIT = 10
VALID_PORTFOLIO_ALLOCATION_MODES = ['EW', 'FIXED', 'PROPORTIONAL', 'MIN_VARIANCE', 'MAX_SHARPE',
                                    'BY_FORMULA', 'RISK_PARITY', 'VOLATILITY_WEIGHTED', 'RISK_TARGET',
                                    'MIN_CORRELATION']
VALID_STRATEGY_ALLOCATION_MODES = ['EW', 'FIXED', 'MIN_VARIANCE', 'MAX_SHARPE', 'BRUTE_FORCE_SHARPE',
                                   'BY_FORMULA', 'RISK_PARITY', 'VOLATILITY_WEIGHTED', 'RISK_TARGET', 'MIN_CORRELATION']
VALID_PORTFOLIO_ALLOCATION_FORMULAS = [None]
VALID_SECURITY_SCORING_METHODS = [None, 'RS', 'EAA']
VALID_PORTFOLIO_SCORING_METHODS = [None, 'RS']
VALID_PROTECTION_MODES = [None, 'BY_RULE', 'RAA', 'BY_FORMULA']
VALID_PROTECTION_FORMULAS = [None, 'DPF']
VALID_ALGO_ALLOCATION_MODES = ['EW', 'FIXED', 'PROPORTIONAL', 'MIN_VARIANCE', 'MAX_SHARPE',
                               'BY_FORMULA', 'RISK_PARITY', 'VOLATILITY_WEIGHTED', 'RISK_TARGET', 'MIN_CORRELATION']
VALID_STRATEGY_ALLOCATION_FORMULAS = [None, 'PAA']
VALID_STRATEGY_ALLOCATION_RULES = [None]
NONE_NOT_ALLOWED = ['portfolios', 'portfolio_allocation_modes', 'cash_proxies', 'strategy_allocation_mode']

from talib import BBANDS, DEMA, EMA, HT_TRENDLINE, KAMA, MA, MAMA, MAVP, MIDPOINT, MIDPRICE, SAR, \
    SAREXT, SMA, T3, TEMA, TRIMA, WMA, ADD, DIV, MAX, MAXINDEX, MIN, MININDEX, MINMAX, \
    MINMAXINDEX, MULT, SUB, SUM, BETA, CORREL, LINEARREG, LINEARREG_ANGLE, \
    LINEARREG_INTERCEPT, LINEARREG_SLOPE, STDDEV, TSF, VAR, ADX, ADXR, APO, AROON, \
    AROONOSC, BOP, CCI, CMO, DX, MACD, MACDEXT, MACDFIX, MFI, MINUS_DI, MINUS_DM, MOM, \
    PLUS_DI, PLUS_DM, PPO, ROC, ROCP, ROCR, ROCR100, RSI, STOCH, STOCHF, STOCHRSI, \
    TRIX, ULTOSC, WILLR, ATR, NATR, TRANGE, ACOS, ASIN, ATAN, CEIL, COS, COSH, EXP, \
    FLOOR, LN, LOG10, SIN, SINH, SQRT, TAN, TANH, AD, ADOSC, OBV, AVGPRICE, MEDPRICE, \
    TYPPRICE, WCLPRICE, HT_DCPERIOD, HT_DCPHASE, HT_PHASOR, HT_SINE, HT_TRENDMODE

TALIB_FUNCTIONS = [BBANDS, DEMA, EMA, HT_TRENDLINE, KAMA, MA, MAMA, MAVP, MIDPOINT, MIDPRICE, SAR, \
                   SAREXT, SMA, T3, TEMA, TRIMA, WMA, ADD, DIV, MAX, MAXINDEX, MIN, MININDEX, MINMAX, \
                   MINMAXINDEX, MULT, SUB, SUM, BETA, CORREL, LINEARREG, LINEARREG_ANGLE, \
                   LINEARREG_INTERCEPT, LINEARREG_SLOPE, STDDEV, TSF, VAR, ADX, ADXR, APO, AROON, \
                   AROONOSC, BOP, CCI, CMO, DX, MACD, MACDEXT, MACDFIX, MFI, MINUS_DI, MINUS_DM, MOM, \
                   PLUS_DI, PLUS_DM, PPO, ROC, ROCP, ROCR, ROCR100, RSI, STOCH, STOCHF, STOCHRSI, TRIX, \
                   ULTOSC, WILLR, ATR, NATR, TRANGE, ACOS, ASIN, ATAN, CEIL, COS, COSH, EXP, FLOOR, LN, \
                   LOG10, SIN, SINH, SQRT, TAN, TANH, AD, ADOSC, OBV, AVGPRICE, MEDPRICE, TYPPRICE, \
                   WCLPRICE, HT_DCPERIOD, HT_DCPHASE, HT_PHASOR, HT_SINE, HT_TRENDMODE]


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class Algo():

    def __init__(self, context, strategies=[], allocation_model=None,
                 scoring_model=None, regime=None):

        if get_environment('platform') == 'zipline':
            context.day_no = 0

        self.ID = 'algo'
        self.type = 'Algorithm'

        self.strategies = strategies
        self.allocation_model = allocation_model
        self.regime = regime

        context.strategies = self.strategies

        context.max_lookback = self._compute_max_lookback(context)
        log.info('MAX_LOOKBACK = {}'.format(context.max_lookback))

        self.weights = [0. for s in self.strategies]
        context.strategy_weights = self.weights
        self.strategy_IDs = [s.ID for s in self.strategies]
        self.active = [s.ID for s in self.strategies] + [p.ID for s in self.strategies for p in s.portfolios]

        if self.allocation_model == None:
            raise ValueError('\n *** FATAL ERROR : ALGO ALLOCATION MODEL CANNOT BE NONE ***\n')

        context.prices = pd.Series()
        context.returns = pd.Series()
        context.log_returns = pd.Series()
        context.covariances = dict()
        context.sharpe_ratio = pd.Series()

        self.all_assets = self._set_all_assets()
        context.all_assets = self.all_assets[:]
        self.allocations = pd.Series(0, index=context.all_assets)
        self.previous_allocations = pd.Series(0, index=context.all_assets)
        context.scoring_model = scoring_model
        self.score = 0.

        context.data = Data(self.all_assets)
        context.algo_data = context.data

        set_symbol_lookup_date('2016-01-01')

        self._instantiate_rules(context)

        context.securities = []  # placeholder securities in portfolio

        if get_environment('platform') == 'zipline':
            context.count = context.max_lookback
        else:
            context.count = 0

        self.rebalance_count = 1  # default rebalance interval = 1
        self.first_time = True

        return

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # looks for any 'lookback' kwargs
    def _compute_max_lookback(self, context):

        kwargs_list = self._get_all_kwargs(context)
        for kwargs in kwargs_list:
            if 'lookback' in kwargs:
                lookback = kwargs['lookback']
                try:
                    period = kwargs['period']
                except:
                    period = 'D'
                # add additional days to cater for 'sip_period'
                if period == 'D':
                    lookback_days = 5 + lookback
                elif period == 'W':
                    lookback_days = 6 + lookback * 5
                elif period == 'M':
                    lookback_days = 25 + lookback * 25
                else:
                    raise RuntimeError('UNKNOWN LOOKBACK PERIOD TYPE {} for strategy {}'.format(period, self.ID))

                context.max_lookback = max(context.max_lookback, lookback_days)

        return context.max_lookback

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_all_kwargs(self, context):
        # creates a list of all kwargs containing 'lookback' labels
        kwargs_list = self._get_portfolio_and_strategy_kwargs(context)
        kwargs_list = kwargs_list + self._get_transform_kwargs(context)
        return kwargs_list

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_portfolio_and_strategy_kwargs(self, context):
        kwargs_list = []
        for strategy in context.strategies:
            kwargs_list = kwargs_list + [strategy.allocation_model.kwargs]
            for pfolio in strategy.portfolios:
                kwargs_list = kwargs_list + [pfolio.allocation_model.kwargs]
        non_trivial_kwargs_list = [kwargs for kwargs in kwargs_list if kwargs not in [None, [], {}, [{}]]]
        return non_trivial_kwargs_list

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_transform_kwargs(self, context):
        kwargs_list = []
        for transform in context.transforms:
            if transform.kwargs not in [None, [], {}, [{}]]:
                kwargs_list = kwargs_list + [transform.kwargs]

        return kwargs_list

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _instantiate_rules(self, context):
        context.rules = {}
        for r in context.algo_rules:
            context.rules[r.name] = r
            # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _set_all_assets(self):
        all_assets = [s.all_assets for s in self.strategies]
        self.all_assets = list(set([i for sublist in all_assets for i in sublist]))
        return self.all_assets

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _allocate_assets(self, context):
        log.debug('STRATEGY WEIGHTS = {}\n'.format(self.weights))
        for i, s in enumerate(self.strategies):
            self.allocations = self.allocations.add(self.weights[i] * s.allocations,
                                                    fill_value=0)
        if self.allocations.sum() == 0:
            # not enough price data yet
            return self.allocations

        # if 1. - sum(self.allocations) > 1.e-15 :
        #     raise RuntimeError ('SUM OF ALLOCATIONS = {} - SHOULD ALWAYS BE 1'.format(sum(self.allocations)))

        return self.allocations

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def check_signal_trigger(self, context, data):

        holdings = context.portfolio.positions
        if self.first_time or context.rules['rebalance_rule'].apply_rule(context)[holdings].any():
            # force rebalance
            self.rebalance(context, data)
            self.first_time = False

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def rebalance(self, context, data):

        # log.info('REBALANCE >> REBALANCE INTERVAL = ' + str(context.rebalance_interval))

        # make sure there's algo data
        # if not isinstance(context.algo_data, dict):
        if not context.data:
            return
        elif not self.first_time:
            if self.rebalance_count != context.rebalance_interval:
                self.rebalance_count += 1
                return

        self.first_time = False

        self.rebalance_count = 1

        log.info('----------------------------------------------------------------------------')

        self.allocations = pd.Series(0., index=context.all_assets)
        self.elligible = pd.Index(self.strategy_IDs)

        # if self.scoring_model != None:
        #     self.scoring_model.caller = self
        #     context.symbols = self.strategy_IDs[:]
        #     self.score = self.scoring_model.compute_score (context)
        #     self.elligible =  self.scoring_model.apply_ntop ()

        self.allocation_model.caller = self
        if self.regime == None:
            self._get_strategy_and_portfolio_allocations(context)
        else:
            self._check_for_regime_change_and_set_active(context)

        self.weights = self.allocation_model.get_weights(context)
        self.allocations = self._allocate_assets(context)

        self._execute_orders(context, data)

        return self.allocations
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _get_strategy_and_portfolio_allocations(self, context):
        for s_no, s in enumerate(self.strategies):
            s.allocations = pd.Series(0., index=s.all_assets)
            for p_no, p in enumerate(s.portfolios):
                p.allocations = pd.Series(0., index=p.all_assets)
                p.allocations = p.reallocate(context)
            s.allocations = s.reallocate(context)
        return

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _check_for_regime_change_and_set_active(self, context):
        self.current_regime = self.regime.get_current(context)
        log.debug('REGIME : {} \n'.format(self.current_regime))
        if self.regime.detect_change(context):
            self.regime.set_new_regime()
            self.active = self.regime.get_active()
        else:
            log.info('REGIME UNCHANGED. JUST REBALANCE\n')
        return
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _execute_orders(self, context, data):

        for security in self.allocations.index:
            if context.portfolio.positions[security].amount > 0 and self.allocations[security] == 0:
                order_target_percent(security, 0)
            elif self.allocations[security] != 0:
                if get_open_orders(security):
                    continue

                current_value = context.portfolio.positions[security].amount * data.current(security, 'price')
                portfolio_value = context.portfolio.portfolio_value
                if portfolio_value == 0:  # before first purchases
                    portfolio_value = context.account.available_funds
                target_value = portfolio_value * self.allocations[security]

                if np.abs(target_value / current_value - 1) < context.threshold:
                    continue

                order_target_percent(security, self.allocations[security] * context.leverage)
                qty = int(
                    context.account.net_liquidation * self.allocations[security] / data.current(security, 'price'))
                log.debug('ORDERING {} : {}%  QTY = {}'.format(security.symbol,
                                                               self.allocations[security] * 100, qty))

        context.gtc_count = GTC_LIMIT

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def check_for_unfilled_orders(self, context, data):
        unfilled = {o.sid: o.amount - o.filled for oo in get_open_orders() for o in get_open_orders(oo)}
        context.outstanding = {u: unfilled[u] for u in unfilled if unfilled[u] != 0}
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def fill_outstanding_orders(self, context, data):
        if context.outstanding == {}:
            context.show_positions = False
            return
        elif context.gtc_count > 0:
            for s in context.outstanding:
                order(s, context.outstanding[s])
                log.debug('ORDER {} OUTSTANDING {} SHARES'.format(context.outstanding[s], s.symbol))

            context.gtc_count -= 1
        else:
            log.info('GTC_COUNT EXPIRED')
            # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def show_records(self, context, data):
        record('LEVERAGE', context.account.leverage)
        # record('CONTEXT_LEVERAGE', context.leverage)
        # record('PV', context.account.total_positions_value)
        # record('PV1',context.portfolio.positions_value)
        # record('TOTAL', context.portfolio.portfolio_value)
        # record('CASH', context.portfolio.cash)
        # for s in context.strategies:
        #     # record(s.ID + '_prices', s.prices.iloc[-1])
        #     for p in s.portfolios:
        #         record(p.ID + '_prices', p..ilocprices[-1])

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def show_positions(self, context, data):

        if context.portfolio.positions == {}:
            return

        log.info('\nPOSITIONS\n')
        for asset in self.all_assets:
            if context.portfolio.positions[asset].amount > 0:
                log.info(
                    '{0} : QTY = {1}, COST BASIS {2:3.2f}, CASH = {3:7.2f}, POSITIONS VALUE = {4:7.2f}, TOTAL = {5:7.2f}'
                        .format(asset.symbol, context.portfolio.positions[asset].amount,
                                context.portfolio.positions[asset].cost_basis,
                                context.portfolio.cash,
                                context.portfolio.positions[asset].amount * data.current(asset, 'price'),
                                context.portfolio.portfolio_value))


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class Strategy():

    def __init__(self, context, ID='', portfolios=[], allocation_model=None,
                 scoring_model=None):

        self.ID = ID
        self.type = 'Strategy'
        self.portfolios = portfolios
        self.portfolio_IDs = [p.ID for p in self.portfolios]
        self.weights = [0. for p in portfolios]

        self.prices = pd.Series()
        self.returns = pd.Series()
        self.covariances = dict()
        self.sharpe_ratio = pd.Series()

        if allocation_model == None:
            self.allocation_model = AllocationModel(context, mode='EW')
        else:
            self.allocation_model = allocation_model
        self.scoring_model = scoring_model
        self.score = 0.

        self._set_all_assets()
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _set_all_assets(self):
        all_assets = [p.all_assets for p in self.portfolios]
        self.all_assets = set([i for sublist in all_assets for i in sublist])
        return self.all_assets
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def allocate_assets(self, context):
        self.allocations = pd.Series(0., index=self.all_assets)
        log.debug('STRATEGY {} PORTFOLIO WEIGHTS = {}\n'.format(self.ID, [round(w, 2) for w in self.weights]))
        for i, p in enumerate(self.portfolios):
            self.allocations = self.allocations.add(self.weights[i] * p.allocations,
                                                    fill_value=0)
        log.debug('SECURITY ALLOCATIONS for {} \n{}\n'.format(self.ID, self.allocations.round(2)))
        return self.allocations
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def reallocate(self, context):
        self.elligible = pd.Index(self.portfolio_IDs)

        if self.scoring_model != None:
            self.scoring_model.caller = self
            context.symbols = self.portfolio_IDs[:]
            self.score = self.scoring_model.compute_score(context)
            self.elligible = self.scoring_model.apply_ntop()

        self.allocation_model.caller = self
        self.weights = self.allocation_model.get_weights(context)
        self.allocations = self.allocate_assets(context)
        self.holdings = (self.allocations * context.portfolio.portfolio_value).divide(
            context.algo_data['price'][self.all_assets]).round(0)
        return self.allocations
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class Portfolio():

    def __init__(self, context, ID='',
                 securities=[], allocation_model=None,
                 scoring_model=None,
                 downside_protection_model=None,
                 cash_proxy=None, allow_shorts=False):

        self.ID = ID
        self.type = 'Portfolio'
        self.securities = securities
        self.weights = [0. for s in securities]
        self.allocation_model = allocation_model
        self.scoring_model = scoring_model
        self.score = 0.
        self.downside_protection_model = downside_protection_model
        if cash_proxy == None:
            log.info('NO CASH_PROXY SPECIFIED FOR PORTFOLIO {}'.format(self.ID))
            raise ValueError('INITIALIZATION ERROR')
        self.cash_proxy = cash_proxy

        self.prices = pd.Series()
        self.returns = pd.Series()
        self.covariances = dict()
        self.sharpe_ratios = pd.Series()

        for s in [context.market_proxy, self.cash_proxy, context.risk_free]:
            if s in self.securities:
                log.warn('{} is included in the portfolio'.format(s.symbol))

        self.all_assets = list(set(self.securities + [context.market_proxy, self.cash_proxy, context.risk_free]))

        self.allocations = pd.Series([0.0] * len(self.all_assets), index=self.all_assets)

        return

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def reallocate(self, context):

        self.allocations = pd.Series(0., index=self.all_assets)
        self.elligible = pd.Index(self.securities)

        if self.scoring_model != None:
            self.scoring_model.caller = self
            context.symbols = self.securities[:]
            self.score = self.scoring_model.compute_score(context)
            self.elligible = self.scoring_model.apply_ntop()

        self.allocation_model.caller = self
        self.weights = self.allocation_model.get_weights(context)
        self.allocations[self.elligible] = self.weights

        log.debug('ALLOCATIONS FOR {} : {}\n'.format(self.ID,
                                                     [(self.allocations.index[i].symbol, round(v, 2))
                                                      for i, v in enumerate(self.allocations)
                                                      if v > 0]))

        if self.downside_protection_model != None:
            self.downside_protection_model.caller = self
            self.allocations = self.downside_protection_model.apply_protection(context,
                                                                               self.allocations,
                                                                               self.cash_proxy,
                                                                               [self.securities, self.score])
            log.debug('AFTER DOWNSIDE PROTECTION {} : {}\n'.format(self.ID,
                                                                   [(self.allocations.index[i].symbol, round(v, 2))
                                                                    for i, v in enumerate(self.allocations)
                                                                    if v > 0]))

        self.holdings = (self.allocations * context.portfolio.portfolio_value).divide(
            context.algo_data['price'][self.all_assets]).round(0)

        return self.allocations


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class Regime():

    def __init__(self, transitions):
        """Initialize Regime object. Set init state and transition table."""
        self.transitions = transitions
        # set current != new to always detect change on first reallocation
        self.current_regime = 0
        self.new_regime = 1

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def detect_change(self, context):
        self.new_regime = self.get_current(context)
        return [False if self.current_regime == self.new_regime else True][0]
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def get_current(self, context):
        for k in self.transitions.keys():
            rule_name = self.transitions[k][0]
            rule = context.rules[rule_name]
            if rule.apply_rule(context):
                return k
                # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def set_new_regime(self):
        self.current_regime = self.new_regime
        record('REGIME', self.current_regime)
        return self.current_regime
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def get_active(self):
        return self.transitions[self.current_regime][1]


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class Data():

    def __init__(self, assets):
        self.all_assets = assets
        # self.fallbacks = {'EDV' : symbol('TLT')}
        return

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def update(self, context, data):

        ''' generates context.raw_data (dictionary of context.max_lookback values)  and context.algo_data (dictioanary current values) for  'high', 'open', 'low', 'close', 'volume', 'price' and all transforms '''

        # log.info('\n{} GENERATING ALGO_DATA...'.format(get_datetime().date()))

        # dataframe for each of 'high', 'open', 'low', 'close', 'volume', 'price'
        context.raw_data = self.get_raw_data(context, data)

        # add a dataframe for each transform
        context.raw_data = self.generate_frame_for_each_transform(context, data)

        # only need the current value for each security (Series)
        context.algo_data = self.current_algo_data(context, data)

        return context.algo_data
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def get_tradeable_assets(self, data):
        tradeable_assets = [asset for asset in self.all_assets if data.can_trade(asset)]
        if len(self.all_assets) > len(tradeable_assets):
            non_tradeable = [s.symbol for s in self.all_assets if data.can_trade(s) == False]
            log.error('*** FATAL ERROR : MISSING DATA for securities {}'.format(non_tradeable))
            print(tradeable_assets, self.all_assets)
            raise ValueError('FATAL ERROR: SEE LOG FOR MISSING DATA')
        return tradeable_assets

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def get_raw_data(self, context, data):

        context.raw_data = dict()

        tradeable_assets = self.get_tradeable_assets(data)

        for item in ['high', 'open', 'low', 'close', 'volume', 'price']:
            try:
                context.raw_data[item] = data.history(tradeable_assets, item, context.max_lookback, '1d')
            except:
                log.warn('FATAL ERROR: UNABLE TO LOAD HISTORY DATA FOR {}'.format(item))
                # force exit
                raise ValueError(' *** FATAL ERROR : INSUFFICIENT DATA - SEE LOG *** ')

            if np.isnan(context.raw_data[item].values).any():
                # log.warn ('\n WARNING : THERE ARE NaNs IN THE DATA FOR {} \n FILL BACKWARDS.......'
                #           .format([k.symbol for k in context.raw_data[item].keys() if
                #                    np.isnan(context.raw_data[item][k][0])]))
                context.raw_data[item] = context.raw_data[item].bfill()

        return context.raw_data

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def generate_frame_for_each_transform(self, context, data):

        for transform in context.transforms:
            # result = apply_transform(context, transform)
            result = transform.apply_transform(context)
            outputs = transform.outputs
            if type(result) == pd.Panel:
                context.raw_data.update(dict([(o, result[o]) for o in outputs]))
            elif type(result) == pd.DataFrame:
                context.raw_data[outputs[0]] = result
            else:
                log.error('\n INVALID TRANSFORM RESULT\n')

        return context.raw_data
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def current_algo_data(self, context, data):

        context.algo_data = dict()
        for k in [key for key in context.raw_data.keys()
                  if type(context.raw_data[key]) == pd.DataFrame]:
            context.algo_data[k] = context.raw_data[k].ix[-1]
            if np.isnan(context.algo_data[k].values).any():
                security = [s.symbol for s in context.raw_data[k].ix[-1].index
                            if np.isnan(context.raw_data[k][s].ix[-1])][0]
                log.warn('*** WARNING: FOR ITEM {} THERE IS A NAN IN THE DATA FOR {}'.format(k, security))
        return context.algo_data
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    # prices are NOMINAL prices used for individual portfolio/strategy variance/cov calculations
    def update_portfolio_and_strategy_metrics(self, context, data):
        for s_no, s in enumerate(context.strategies):
            self._update_strategy_metrics(context, data, s, s_no)
            self._update_portfolio_metrics(context, data, s)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _update_strategy_metrics(self, context, data, s, s_no):
        ''' calculate and store current price of strategies used by algo '''
        strategy_price = s.holdings.multiply(context.algo_data['price'][s.all_assets]).sum()
        s.prices[get_datetime()] = strategy_price
        s.sharpe_ratio[get_datetime()] = self._calculate_sharpe_ratio(context, data, s)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _update_portfolio_metrics(self, context, data, s):
        for p_no, p in enumerate(s.portfolios):
            portfolio_price = p.holdings.multiply(context.algo_data['price'][p.all_assets]).sum()
            p.prices[get_datetime()] = portfolio_price
            p.sharpe_ratios[get_datetime()] = self._calculate_sharpe_ratio(context, data, p)
            # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _calculate_sharpe_ratio(self, context, data, s_or_p):
        if len(s_or_p.prices) <= context.SR_lookback:
            # not enought data yet
            return 0
        rets = s_or_p.prices.pct_change()[-context.SR_lookback:]
        # s_or_p_rets = (rets * s_or_p.allocation_model.weights).sum(axis=1)[-context.SR_lookback:]
        risk_free_rets = data.history(context.risk_free, 'price', context.SR_lookback, '1d').pct_change()
        excess_returns = rets[1:].values - risk_free_rets[1:].values
        return excess_returns.mean() / rets.std()


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class ScoringModel():

    def __init__(self, context, factors=None, method=None, n_top=1):
        self.factors = factors
        self.method = method
        if self.factors == None:
            raise ValueError('Unable to score model with no factors')
        # if self.method == None :
        #     raise ValueError ('Unable to score model with no method')
        self.n_top = n_top
        self.score = 0
        self.methods = {'RS': self._relative_strength,
                        'EAA': self._eaa
                        }

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def compute_score(self, context):
        self.symbols = context.symbols
        self.score = self.methods[self.method](context)
        # log.debug ('\nSCORE\n\n{}\n'.format(self.score))
        return self.score

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _relative_strength(self, context):
        self.score = 0.
        for name in self.factors.keys():

            if np.isnan(context.algo_data[name[1:]][self.symbols]).any():
                if isinstance(self.symbols[0], str):
                    sym = [(self.symbols[s], v)
                           for s, v in enumerate(context.algo_data[name[1:]][self.symbols]) if np.isnan(v)][0][0]
                else:
                    sym = [(self.symbols[s].symbol, v)
                           for s, v in enumerate(context.algo_data[name[1:]][self.symbols]) if np.isnan(v)][0][0]
                print('SCORING ERROR : FACTOR {} VALUE FOR {} IS nan'.format(name, sym))
                raise RuntimeError()

            if name[0] == '+':
                # log.debug('Values for factor {} :\n\{}\nRANKS : \n{}'.format(name[1:],
                #                                                              [(s.symbol, context.algo_data[name[1:]][s]) for s in self.securities],
                #                                                              [(s.symbol, context.algo_data[name[1:]][self.securities].rank(ascending=False)[s])
                #                                                               for s in self.securities]))

                try:
                    # highest value gets highest rank / score
                    self.score = self.score + context.algo_data[name[1:]][self.symbols].rank(ascending=True) \
                                 * self.factors[name]
                except:
                    raise RuntimeError(
                        '\n *** FATAL ERROR : UNABLE TO SCORE FACTOR {}. CHECK TRANSFORM & FACTOR DEFINITIONS\n'
                            .format(name[1:]))

            elif name[0] == '-':
                # log.debug('Values for factor {} :\n\{}\nRANKS : \n{}'.format(name[1:],
                #                                                              [(s.symbol, context.algo_data[name[1:]][s]) for s in self.securities],
                #                                                              [(s.symbol, context.algo_data[name[1:]][self.securities].rank(ascending=True)[s])
                #                                                               for s in self.securities]))

                try:
                    # lowest value gets highest rank /score
                    self.score = self.score + context.algo_data[name[1:]][self.symbols].rank(ascending=False) \
                                 * self.factors[name]
                except:
                    raise RuntimeError('\n UNABLE TO SCORE FACTOR {}. CHECK TRANSFORM & FACTOR DEFINITIONS\n'
                                       .format(name[1:]))

        # log.debug('Scores for factor {} :\n\n{}'.format(name[1:],
        #                                                 [(s.symbol, self.score[s]) for s in self.securities]))

        return self.score
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _eaa(self, context):

        # only valid for securities, not portfolios or strategies (?)

        if self.caller.type != 'Portfolio':
            raise RuntimeError('SCORING MODEL EAA ONLY VALID FOR PORTFOLIO, NOT {}'.format(self.method))

        # prices = data.history(self.securities, 'price', 280, '1d')
        prices = context.raw_data['price'][self.symbols]

        monthly_prices = prices.resample('M').last()[self.symbols]
        monthly_returns = monthly_prices.pct_change().ix[-12:]

        # nominal return correlation to equi-weight portfolio
        N = len(self.symbols)
        equal_weighted_index = monthly_returns.mean(axis=1)
        C = pd.Series([0.0] * N, index=self.symbols)
        for s in C.index:
            C[s] = monthly_returns[s].corr(equal_weighted_index)

        R = context.algo_data['R'][self.symbols]
        V = monthly_returns.std()

        # Apply factor weights
        # wi ~ zi = ( ri^wR * (1-ci)^wC / vi^wV )^wS
        wR = self.factors['R']
        wC = self.factors['C']
        wV = self.factors['V']
        wS = self.factors['S']
        eps = self.factors['eps']

        # Generalized Momentum Score
        self.score = ((R ** wR) * ((1 - C) ** wC) / (V ** wV)) ** (wS + eps)

        return self.score

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def apply_ntop(self):

        N = len(self.symbols)
        if self.method == 'EAA':
            self.n_top = int(min(np.ceil(N ** 0.5) + 1, N / 2))
            elligible = self.score.sort_values().index[-self.n_top:]
        else:
            # best score gets lowest rank
            ranks = self.score.rank(ascending=False, method='dense')
            elligible = ranks[ranks <= self.n_top].index

        return elligible


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class AllocationModel():

    def __init__(self, context, mode='EW', weights=None, rule=None, formula=None, kwargs={}):
        self.mode = mode
        self.formula = formula
        self.weights = weights
        self.rule = rule
        self.kwargs = kwargs

        self.modes = {'EW': self._equal_weight_allocation,
                      'FIXED': self._fixed_allocation,
                      'PROPORTIONAL': self._proportional_allocation,
                      'MIN_VARIANCE': self._min_variance_allocation,
                      'BRUTE_FORCE_SHARPE': self._brute_force_sharpe_allocation,
                      'MAX_SHARPE': self._max_sharpe_allocation,
                      'BY_FORMULA': self._allocation_by_formula,
                      'REGIME_EW': self.allocate_by_regime_EW,
                      'RISK_PARITY': self._risk_parity_allocation,
                      'VOLATILITY_WEIGHTED': self._volatility_weighted_allocation,
                      'RISK_TARGET': self._risk_targeted_allocation,
                      'MIN_CORRELATION': self._get_reduced_correlation_weights,
                      }

        if mode not in self.modes.keys():
            raise ValueError('UNKNOWN MODE "{}"'.format(mode))

        self.caller = None  # portfolio or strategy object calling the model

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def get_weights(self, context):
        self.prices = self._get_caller_prices(context)
        if self.mode not in ['EW', 'FIXED', 'PROPORTIONAL']:
            # all other modes need prices for at least 'lookback' period
            if self.kwargs is not None and 'lookback' in self.kwargs:
                # unable to allocate weights until more than 'lookback' prices
                if len(self.prices) <= self.kwargs['lookback']:
                    # default to 'EW' to be able to generate prices
                    self.caller_weights = [1. / len(self.caller.elligible) for i in self.caller.elligible]
                    return self.caller_weights
        if self.mode.startswith('REGIME') and self.caller.ID != 'algo':
            raise ValueError('ILLEGAL REGIME ALLOCATION : REGIME ALLOCATION MODEL ONLY ALLOWED AT ALGO LEVEL')
        return self.modes[self.mode](context)
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _get_caller_prices(self, context):
        if self.caller.type == 'Portfolio':
            prices = context.raw_data['price'][self.caller.elligible]
        elif self.caller.type == 'Strategy':
            # portfolio prices for portfolios in strategy
            prices = self._get_strategy_prices(context)

        elif self.caller.type == 'Algorithm':
            # strategy prices for strategies in algorithm
            prices = self._get_algo_prices(context)

        return prices

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_strategy_prices(self, context):
        prices_dict = OrderedDict({p.ID: p.prices for s in context.strategies for p in s.portfolios})
        index = context.strategies[0].portfolios[0].prices.index
        columns = [p.ID for s in context.strategies for p in s.portfolios]
        return pd.DataFrame(prices_dict, index=index, columns=columns)
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _get_algo_prices(self, context):
        prices_dict = OrderedDict({s.ID: s.prices for s in context.strategies})
        index = context.strategies[0].prices.index
        columns = [s.ID for s in context.strategies]
        return pd.DataFrame(prices_dict, index=index, columns=columns)
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _equal_weight_allocation(self, context):
        elligible = self.caller.elligible
        if len(elligible) > 0:
            self.caller.weights = [1. / len(elligible) for i in elligible]
        return self.caller.weights
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _fixed_allocation(self, context):
        # we are going to change these weights, so be careful to keep a copy!
        self.caller.weights = self.caller.allocation_model.weights[:]
        return self.caller.weights

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _proportional_allocation(self, context):
        elligible = self.caller.elligible
        score = self.caller.score
        self.caller.weights = score[elligible] / score[elligible].sum()
        return self.caller.weights
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _risk_parity_allocation(self, context):
        lookback = self.kwargs['lookback']
        prices = self.prices[-lookback:]
        ret_log = np.log(1. + prices.pct_change())[1:]
        hist_vol = ret_log.std(ddof=0)

        adj_vol = 1. / hist_vol

        self.caller.weights = adj_vol.div(adj_vol.sum(), axis=0)
        return self.caller.weights
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _volatility_weighted_allocation(self, context):

        elligible = self.caller.elligible
        lookback = self.kwargs['lookback']
        ret_log = np.log(1. + self.prices.pct_change())
        hist_vol = ret_log.rolling(window=lookback, center=False).std()[elligible]

        adj_vol = 1. / hist_vol

        self.caller.weights = adj_vol.div(adj_vol.sum())
        return self.caller.weights
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _risk_targeted_allocation(self, context):
        lookback = self.kwargs['lookback']
        target_risk = self.kwargs['target_risk']
        shorts = self.kwargs['shorts']
        prices = self.prices[self.caller.elligible][-lookback:]
        sigma_mat = self._compute_covariance_matrix(prices)
        mu_vec = self._compute_expected_returns(prices)
        risk_free = context.raw_data['price'][context.risk_free].pct_change()[-lookback:].mean()
        self.caller.weights = self._compute_target_risk_portfolio(mu_vec, sigma_mat,
                                                                  target_risk=target_risk,
                                                                  risk_free=risk_free,
                                                                  shorts=shorts)[0]
        return self.caller.weights
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _min_variance_allocation(self, context):
        lookback = self.kwargs['lookback']
        shorts = self.kwargs['shorts']
        prices = self.prices[self.caller.elligible][-lookback:]
        sigma_mat = self._compute_covariance_matrix(prices)
        mu_vec = self._compute_expected_returns(prices)
        self.caller.weights = self._compute_global_min_portfolio(mu_vec=mu_vec,
                                                                 sigma_mat=sigma_mat,
                                                                 shorts=shorts)[0]
        return self.caller.weights
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _max_sharpe_allocation(self, context):
        # calculate security weights for max sharpe portfolio
        elligible = self.caller.elligible
        lookback = self.kwargs['lookback']
        shorts = self.kwargs['shorts']
        prices = self.prices[elligible][-lookback:]
        sigma_mat = self._compute_covariance_matrix(prices)
        mu_vec = self._compute_expected_returns(prices)
        risk_free = context.raw_data['price'][context.risk_free].pct_change()[-lookback:].mean()
        self.caller.weights = self._compute_tangency_portfolio(mu_vec=mu_vec,
                                                               sigma_mat=sigma_mat,
                                                               risk_free=risk_free,
                                                               shorts=shorts)[0]
        return self.caller.weights

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # this only works at strategy level
    # it could feasibly work at algo level too
    def _brute_force_sharpe_allocation(self, context):
        if isinstance(self.caller, Strategy):
            portfolio_SRs = [p.sharpe_ratios[-1] for p in self.caller.portfolios]
            # select the portfolio(s) with the highest SR - could be more than 1
            self.caller.weights = [1. if s == np.max(portfolio_SRs) else 0 for s in portfolio_SRs]
            # in case there are more than 1, normalize
            return self.caller.weights / np.sum(self.caller.weights)
        else:
            raise RuntimeError('BRUTE_FORCE_SHARPE_ALLOCATION ONLY WORKS AT STRATEGY LEVEL')

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_reduced_correlation_weights(self, context):
        """
        Implementation of minimum correlation algorithm.
        ref: http://cssanalytics.com/doc/MCA%20Paper.pdf

        :Params:
            :returns <Pandas DataFrame>:Timeseries of asset returns
            :risk_adjusted <boolean>: If True, asset weights are scaled
                                      by their standard deviations
        """
        elligible = self.caller.elligible
        lookback = self.kwargs['lookback']
        risk_adjusted = self.kwargs['risk_adjusted']

        prices = self.prices[elligible][-lookback:]
        returns = prices.pct_change()[1:]

        correlations = returns.corr()
        adj_correlations = self._get_adjusted_cor_matrix(correlations)
        initial_weights = adj_correlations.T.mean()

        ranks = initial_weights.rank()
        ranks /= ranks.sum()

        weights = adj_correlations.dot(ranks)
        weights /= weights.sum()

        if risk_adjusted:
            weights = weights / returns.std()
            weights /= weights.sum()
        return weights

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_adjusted_cor_matrix(self, cor):
        values = cor.values.flatten()
        mu = np.mean(values)
        sigma = np.std(values)
        distribution = scipy.stats.norm(mu, sigma)
        return 1 - cor.apply(lambda x: distribution.cdf(x))

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _allocation_by_formula(self, context):
        # for Protective Asset Allocation (PAA), strategy assumed to have 2 portfolios
        if self.formula == 'PAA':
            if len(self.caller.elligible) != 2:
                raise ValueError('Protective Asset Allocation (PAA) Srategy has {} Portfolio; must have 2')
            else:
                self.caller.allocations = self._allocate_by_PAA_formula(context)
        return self.caller.allocations

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _allocate_by_PAA_formula(self, context):
        try:
            protection_factor = self.kwargs['protection_factor']
        except:
            raise RuntimeError(
                'MISSING STRATEGY ALLOCATION KWARG "protection_factor" FOR STRATEGY {}'.format(self.caller.ID))
        securities = self.caller.portfolios[0].securities
        N = len(securities)
        n = context.rules[self.rule].apply_rule(context)[securities].sum()
        dpf = (N - n) / (N - protection_factor * n / 4.)
        # log.debug ('For portfolio {}, n = {}, N = {}, dpf = {}'.format(self.caller.ID, n, N, dpf))
        record('DPF', dpf)
        self.caller.weights = [1. - dpf, dpf]
        return self.caller.weights

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def allocate_by_regime_EW(self, context):

        # log.debug('\nACTIVE : {} \n'.format(self.caller.active))

        if self.caller.type != 'Algorithm':
            raise RuntimeError('REGIME SWITCHING ONLY ALLOWED AT ALGO LEVEL')

        self._reset_strategy_and_portfolio_weights(context)

        for s in self.caller.strategies:
            s.allocations = pd.Series(0, index=s.all_assets)

            for p in s.portfolios:
                if s.ID in self.caller.active:
                    p_weight = 1. / len(s.portfolios)
                elif p.ID in self.caller.active:
                    p_weight = 1. / np.sum([1 if pfolio.ID in self.caller.active else 0 for pfolio in s.portfolios])
                elif s.ID not in self.caller.active and p.ID not in self.caller.active:
                    continue

                p.allocations = p.reallocate(context)
                s.allocations = s.allocations.add(p_weight * p.allocations, fill_value=0)

        active_strategies = set([s.ID for s in context.strategies
                                 for p in s.portfolios if s.ID in self.caller.active
                                 or p.ID in self.caller.active])
        self.caller.weights = [1. / len(active_strategies) if s.ID in active_strategies else 0 for s in
                               context.strategies]
        context.strategy_weights = self.caller.weights

        return self.caller.weights
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _reset_strategy_and_portfolio_weights(self, context):

        for s_no, s in enumerate(self.caller.strategies):
            self.caller.weights[s_no] = 0
            context.strategy_weights[s_no] = 0
            for p_no, p in enumerate(s.portfolios):
                s.weights[p_no] = 0
        return
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _get_no_of_active_portfolios(self):
        # Note : if strategy in active, all its portfolios are active
        number = 0
        for s in self.caller.strategies:
            if s.ID in self.caller.active:
                # all portfolios are active
                for p in s.portfolios:
                    number += 1
            for p in s.portfolios:
                if p.ID in self.caller.active:
                    number += 1

        return number

    # Portfolio Helper Functions

    # Functions:
    #    1. compute_efficient_portfolio        compute minimum variance portfolio
    #                                            subject to target return
    #    2. compute_global_min_portfolio       compute global minimum variance portfolio
    #    3. compute_tangency_portfolio         compute tangency portfolio
    #    4. compute_efficient_frontier         compute Markowitz bullet
    #    5. compute_portfolio_mu               compute portfolio expected return
    #    6. compute_portfolio_sigma            compute portfolio standard deviation
    #    7. compute_covariance_matrix          compute covariance matrix
    #    8. compute_expected_returns           compute expected returns vector

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _compute_covariance_matrix(self, prices):
        # calculates the cov matrix for the period defined by prices
        returns = np.log(1 + prices.pct_change())[1:]
        excess_returns_matrix = returns - returns.mean()
        return 1. / len(returns) * (excess_returns_matrix.T).dot(excess_returns_matrix)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _compute_expected_returns(self, prices):
        mu_vec = np.log(1 + prices.pct_change(1))[1:].mean()
        return mu_vec

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _compute_portfolio_mu(self, mu_vec, weights_vec):
        if len(mu_vec) != len(weights_vec):
            raise RuntimeError('mu_vec and weights_vec must have same length')
        return mu_vec.T.dot(weights_vec)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _compute_portfolio_sigma(self, sigma_mat, weights_vec):

        if len(sigma_mat) != len(sigma_mat.columns):
            raise RuntimeError('sigma_mat must be square\nlen(sigma_mat) = {}\nlen(sigma_mat.columns) ={}'.
                               format(len(sigma_mat), len(sigma_mat.columns)))
        return np.sqrt(weights_vec.T.dot(sigma_mat).dot(weights_vec))

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _compute_efficient_portfolio(self, mu_vec, sigma_mat, target_return, shorts=True):

        # compute minimum variance portfolio subject to target return
        #
        # inputs:
        # mu_vec                  N x 1 DataFrame expected returns
        #                         with index = asset names
        # sigma_mat               N x N DataFrame covariance matrix of returns
        #                         with index = columns = asset names
        # target_return           scalar, target expected return
        # shorts                  logical, allow shorts is TRUE
        #
        # output is portfolio object with the following elements
        #
        # mu_p                   portfolio expected return
        # sig_p                  portfolio standard deviation
        # weights                N x 1 DataFrame vector of portfolio weights
        #                        with index = asset names

        # check for valid inputs
        #

        if len(mu_vec) != len(sigma_mat):
            print("dimensions of mu_vec and sigma_mat do not match")
            raise ValueError
        if np.matrix([sigma_mat.ix[i][i] for i in range(len(sigma_mat))]).any() <= 0:
            print('Covariance matrix not positive definite')
            raise TypeError

        #
        # compute efficient portfolio
        #

        solvers.options['show_progress'] = False
        P = 2 * matrix(sigma_mat.values)
        q = matrix(0., (len(sigma_mat), 1))
        G = spdiag([-1. for i in range(len(sigma_mat))])
        A = matrix(1., (1, len(sigma_mat)))
        A = matrix([A, matrix(mu_vec.T.values).T], (2, len(sigma_mat)))
        b = matrix([1.0, target_return], (2, 1))

        if shorts == True:
            h = matrix(1., (len(sigma_mat), 1))

        else:
            h = matrix(0., (len(sigma_mat), 1))

        # weights_vec = pd.DataFrame(np.array(solvers.qp(P, q, G, h, A, b)['x']),\
        #                                     sigma_mat.columns)
        try:
            weights_vec = pd.Series(list(solvers.qp(P, q, G, h, A, b)['x']), index=sigma_mat.columns)
        except:
            log.info('W A R N I N G : unable to compute optimal weights; setting to equal weights')
            weights_vec = pd.Series(1. / len(sigma_mat), index=sigma_mat.columns)

            #
        # compute portfolio expected returns and variance
        #
        # print ('*** Debug ***\n_compute_efficient_portfolio:\nmu_vec:\n', self.mu_vec, '\nsigma_mat:\n',
        #        self.sigma_mat, '\nweights:\n', self.weights_vec )
        weights_vec.index = mu_vec.index
        mu_p = self._compute_portfolio_mu(mu_vec, weights_vec)
        sigma_p = self._compute_portfolio_sigma(sigma_mat, weights_vec)

        return weights_vec, mu_p, sigma_p
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _compute_global_min_portfolio(self, mu_vec, sigma_mat, shorts=True):

        solvers.options['show_progress'] = False
        P = 2 * matrix(sigma_mat.values)
        q = matrix(0., (len(sigma_mat), 1))
        G = spdiag([-1. for i in range(len(sigma_mat))])
        A = matrix(1., (1, len(sigma_mat)))
        b = matrix(1.0)

        if shorts == True:
            h = matrix(1., (len(sigma_mat), 1))
        else:
            h = matrix(0., (len(sigma_mat), 1))

        # print ('\nP\n\n{}\n\nq\n\n{}\n\nG\n\n{}\n\nh\n\n{}\n\nA\n\n{}\n\nb\n\n{}\n\n'.format(P,q,G,h,A,b))
        # weights_vec = pd.DataFrame(np.array(solvers.qp(P, q, G, h, A, b)['x']),\
        #                                     index=sigma_mat.columns)
        weights_vec = pd.Series(list(solvers.qp(P, q, G, h, A, b)['x']), index=sigma_mat.columns)

        #
        # compute portfolio expected returns and variance
        #
        # print ('*** Debug ***\n_Global Min Portfolio:\nmu_vec:\n', mu_vec, '\nsigma_mat:\n',
        #        sigma_mat, '\nweights:\n', weights_vec)

        mu_p = self._compute_portfolio_mu(mu_vec, weights_vec)
        sigma_p = self._compute_portfolio_sigma(sigma_mat, weights_vec)

        return weights_vec, mu_p, sigma_p
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _compute_efficient_frontier(self, mu_vec, sigma_mat, risk_free=0, points=100, shorts=True):

        efficient_frontier = pd.DataFrame(index=range(points), dtype=object, columns=['mu_p', 'sig_p', 'sr_p', 'wts_p'])

        gmin_wts, gmin_mu, gmin_sigma = self._compute_global_min_portfolio(mu_vec, sigma_mat, shorts=shorts)

        xmax = mu_vec.max()
        if shorts == True:
            xmax = 2 * mu_vec.max()
        for i, mu in enumerate(np.linspace(gmin_mu, xmax, points)):
            w_vec, portfolio_mu, portfolio_sigma = self._compute_efficient_portfolio(mu_vec, sigma_mat, mu,
                                                                                     shorts=shorts)
            efficient_frontier.ix[i]['mu_p'] = w_vec.dot(mu_vec)
            efficient_frontier.ix[i]['sig_p'] = np.sqrt(w_vec.T.dot(sigma_mat.dot(w_vec)))
            efficient_frontier.ix[i]['sr_p'] = (efficient_frontier.ix[i]['mu_p'] - risk_free) / \
                                               efficient_frontier.ix[i]['sig_p']
            efficient_frontier.ix[i]['wts_p'] = w_vec

        return efficient_frontier

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _compute_tangency_portfolio(self, mu_vec, sigma_mat, risk_free=0, shorts=True):

        efficient_frontier = self._compute_efficient_frontier(mu_vec, sigma_mat, risk_free, shorts=shorts)
        index = efficient_frontier.index[efficient_frontier['sr_p'] == efficient_frontier['sr_p'].max()]

        wts = efficient_frontier['wts_p'][index].values[0]
        mu_p = efficient_frontier['mu_p'][index].values[0]
        sigma_p = efficient_frontier['sig_p'][index].values[0]
        sharpe_p = efficient_frontier['sr_p'][index].values[0]

        return wts, mu_p, sigma_p, sharpe_p

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _compute_target_risk_portfolio(self, mu_vec, sigma_mat, target_risk, risk_free=0, shorts=True):

        efficient_frontier = self._compute_efficient_frontier(mu_vec, sigma_mat, risk_free, shorts=shorts)
        if efficient_frontier['sig_p'].max() <= target_risk:
            log.warn('TARGET_RISK {} > EFFICIENT FRONTIER MAXIMUM {}; SETTING IT TO MAXIMUM'.
                     format(target_risk, efficient_frontier['sig_p'].max()))
            index = len(efficient_frontier) - 1
        elif efficient_frontier['sig_p'].min() >= target_risk:
            log.warn('TARGET RISK {} < GLOBAL MINIMUM {}; SETTING IT TO GLOBAL MINIMUM'.
                     format(target_risk, efficient_frontier['sig_p'].max()))
            index = 1
        else:
            index = efficient_frontier.index[efficient_frontier['sig_p'] >= target_risk][0]

        wts = efficient_frontier['wts_p'][index]
        mu_p = efficient_frontier['mu_p'][index]
        sigma_p = efficient_frontier['sig_p'][index]
        sharpe_p = efficient_frontier['sr_p'][index]

        return wts, mu_p, sigma_p, sharpe_p


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class DownsideProtectionModel():

    def __init__(self, context, mode=None, rule=None, formula=None, *args):

        self.mode = mode
        self.rule = rule
        self.formula = formula
        self.args = args

        self.modes = {'BY_RULE': self._by_rule,
                      'RAA': self._apply_RAA,
                      'BY_FORMULA': self._by_formula
                      }

        self.caller = None  # portfolio or strategy object calling the model

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def apply_protection(self, context, allocations, cash_proxy=None, *args):

        # apply downside protection model to cash_proxy, if it fails, set cash_proxy to risk_free

        if context.allow_cash_proxy_replacement:
            if context.raw_data['price'][cash_proxy][-1] < context.algo_data['price'][-43:].mean():
                cash_proxy = context.risk_free

        new_allocations = self.modes[self.mode](context, allocations, cash_proxy, *args)

        return new_allocations

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _by_rule(self, context, allocations, cash_proxy, *args):
        try:
            triggers = context.rules[self.rule].apply_rule(context)[allocations.index]
        except:
            raise RuntimeError('UNABLE TO APPLY RULE {} FOR {}'.format(self.rule, self.caller.ID))

        new_allocations = pd.Series([0 if triggers[a] else allocations[a] for a in allocations.index],
                                    index=allocations.index)
        new_allocations[cash_proxy] = new_allocations[cash_proxy] + (1 - new_allocations.sum())

        return new_allocations

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _apply_RAA(self, context, allocations, cash_proxy, *args):
        excess_returns = context.algo_data['EMOM']

        tmp1 = [0.5 if excess_returns[asset] > 0 else 0. for asset in allocations.index]

        prices = context.algo_data['price']
        MA = context.algo_data['smma']

        tmp2 = [0.5 if prices[asset] > MA[asset] else 0. for asset in allocations.index]

        dpf = pd.Series([x + y for x, y in zip(tmp1, tmp2)], index=allocations.index)

        new_allocations = allocations * dpf
        new_allocations[cash_proxy] = new_allocations[cash_proxy] + (1 - np.sum(new_allocations))

        record('BOND EXPOSURE', new_allocations[cash_proxy])

        return new_allocations

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _by_formula(self, context, allocations, cash_proxy, *args):
        if self.formula == 'DPF':
            try:
                new_allocations = self._apply_DPF(context, allocations, cash_proxy, *args)
            except:
                raise ValueError('FORMULA "{}" DOES NOT EXIST OR ERROR CALCULATING FORMULA'.formmat(self.formula))
        return new_allocations

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _apply_DPF(self, context, allocations, cash_proxy, *args):
        securities = args[0][0]
        N = len(securities)
        try:
            triggers = context.rules[self.rule].apply_rule(context)[securities]
        except:
            raise ValueError('UNABLE TO APPLY RULE {}'.format(self.rule))

        num_neg = triggers[triggers == True].count()
        dpf = float(num_neg) / N
        log.info("DOWNSIDE PROTECTION FACTOR = {}".format(dpf))

        new_allocations = (1. - dpf) * allocations
        new_allocations[cash_proxy] += dpf

        return new_allocations


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class Rule():
    functions = {'EQ': lambda x, y: x == y,
                 'LT': lambda x, y: x < y,
                 'GT': lambda x, y: x > y,
                 'LE': lambda x, y: x <= y,
                 'GE': lambda x, y: x >= y,
                 'NE': lambda x, y: x != y,
                 'AND': lambda x, y: x & y,
                 'OR': lambda x, y: x | y,
                 }

    def __init__(self, context, name='', rule='', apply_to='all'):

        self.name = name
        # remove spaces
        self.rule = rule.replace(' ', '')
        self.temp = ''
        self.apply_to = apply_to

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def apply_rule(self, context):

        ''' routine to evaluate a rule consisting of a string formatted as 'conditional [AND|OR conditional]'
            where conditionals are logical expressions, pandas series of logical expressions
            or pandas dataframes of logical expressions. Returns True or False,
            pandas series of True/False or pandas dataframe of True/False respectively.
        '''

        if self.rule == 'always_true':
            return True

        self.temp = self._replace_operators(self.rule)
        # get the first condition of the rule and evaluate it
        condition, result, cjoin = self._get_next_conditional(context)

        # log.debug ('result = {}'.format(result))

        while cjoin != None:
            # get the rest of the rule
            self.temp = self.temp[len(condition) + len(cjoin):]
            # get the next conditional of the rule and evaluate it
            func = Rule.functions[cjoin]
            condition, tmp_result, cjoin = self._get_next_conditional(context)

            result = func(result, tmp_result)

            # log.debug ('intermediate result = {}'.format(result))

        # log.debug ('final result = {}'.format(result))
        return result

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_next_conditional(self, context):
        condition, cjoin = self._get_conditional(self.temp)
        result = self._evaluate_condition(context, condition)
        if self.apply_to != 'all':
            result = result[self.apply_to]
        return condition, result, cjoin
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _replace_operators(self, s):

        ''' to make it easy to find operators in the rule s, replace ['=', '>', '<', '>=', '<=', '!=', 'and', 'or']
            with ['EQ', 'GT', 'LT', 'GE', 'LE', 'NE', 'AND', 'OR'] respectively
        '''

        s1 = s.replace('and', 'AND').replace('or', 'OR').replace('!=', 'NE').replace('<=', 'LE').replace('>=', 'GE')
        s1 = s1.replace('=', 'EQ').replace('<', 'LT').replace('>', 'GT')
        return s1

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_conditional(self, s):

        ''' routine to find first ocurrence of "AND" or "OR" in rule s. Returns
        conditional to the left of AND/OR and either 'AND', 'OR' or None '''

        pos_AND = [s.find('AND') if s.find('AND') != -1 else len(s)][0]
        pos_OR = [s.find('OR') if s.find('OR') != -1 else len(s)][0]
        condition, cjoin = [(s.split('AND')[0], 'AND') if pos_AND < pos_OR else (s.split('OR')[0], 'OR')][0]
        if pos_AND == len(s) and pos_OR == len(s):
            cjoin = None
        return condition, cjoin

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_operator(self, condition):

        '''routine to extract the operator and its position from the conditional expression
        '''
        for o in ['EQ', 'GT', 'LT', 'GE', 'LE', 'NE', 'AND', 'OR']:
            if condition.find(o) > 0:
                return o, condition.find(o)
        raise ('UNKNOWN OPERATOR')

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_operand_value(self, context, operand):
        if operand.startswith('('):
            tuple_0 = operand[1:operand.find(',')].strip("'").strip('"')
            tuple_1 = operand[operand.find(',') + 1:-1]
            return context.algo_data[tuple_0][tuple_1]
        if operand[0].isdigit() or operand.startswith('.') or operand.startswith('-'):
            return float(operand)
        elif isinstance(operand, str):
            return context.algo_data[operand.strip("'").strip('"')]
        else:
            op = context.algo_data[operand[0]]
            if operand[1] != None:
                op = context.algo_data[operand[0].strip("'").strip('"')][operand[1]]
            return op

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _evaluate_condition(self, context, condition):
        operator, position = self._get_operator(condition)
        x = self._get_operand_value(context, condition[:position])
        y = self._get_operand_value(context, condition[position + 2:])
        # log.debug ('x = {}, y = {}, operator = {}'.format(x, y, operator))
        func = Rule.functions[operator]

        return func(x, y)


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class Transform():

    def __init__(self, context, name='', function='', inputs=[], kwargs={}, outputs=[]):

        self.name = name
        self.function = function
        self.inputs = inputs
        self.kwargs = kwargs
        self.outputs = outputs

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def apply_transform(self, context):

        # transform format [([<data_items>], function, <data_item>, args)]

        context.dp = pd.Panel(context.raw_data)

        if self.function in TALIB_FUNCTIONS:
            return self._apply_talib_function(context)

        elif self.function.__name__.startswith('roll') or self.function.__name__.startswith(
                'expand') or self.function.__name__ == '<lambda>':
            return self._apply_pandas_function(context)

        else:
            return self.function(self, context)

        raise ValueError('UNKNOWN TRANSFORM {}'.format(self.function))

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _apply_talib_function(self, context):

        '''
        Routine to apply transform to data provided as a pandas Panel.
        Inputs:
        dp: pandas dataPanel consisting of a DataFrame for each item in ['open', 'high', 'low', 'close', 'volume',
            'price']; each DataFrame has column names = asset names
        inputs : list of dp items to be used as inputs. If empty (=[]), routine will use default input
                        names from the talib function DOC string
        function : talib function name (e.g. RSI, MACD, ADX etc.) - see list of imported functions above
        output_names : list of names for the tranforms DataFrames
        NOTE: names must be unique and there must be a name for each output (some transforms produce more than
                one output e.g MACD produces 3 outputs)
        args : empty list (=[]), in which case default values are obtained from talib function DOC string.
                otherwise, custom parameters may be provided as a list of integers, the parameters matching
                the FULL parameter list, as per the function DOC string

        Outputs:
            pandas DataPanel with new items (DataFrames) appended for each transform output.

        '''

        # parameters = [a for a in self.args]
        parameters = [self.kwargs[key] for key in iter(self.kwargs)]
        if parameters == []:
            parameters = [int(s) for s in re.findall('\d+', self.function.__doc__)]
        data_items = re.findall("(?<=\')\w+", self.function.__doc__)
        if data_items == []:
            inputs = self.inputs
        else:
            inputs = data_items

        for output in self.outputs:
            context.dp[output] = pd.DataFrame(0, index=context.dp.major_axis, columns=context.dp.minor_axis)

        for asset in context.dp.minor_axis:
            data = [context.dp.transpose(2, 1, 0)[asset][i].values for i in inputs]
            args = data + parameters
            transform = self.function(*args)
            if len(transform) == len(self.outputs) or len(transform) > 3:
                pass
            else:
                raise ValueError('** ERROR : must be output_names for each output')

            if len(self.outputs) == 1:
                context.dp[self.outputs[0]][asset] = transform
            else:
                for i, output in enumerate(self.outputs):
                    context.dp[output][asset] = transform[i]

        # for some reason, if you don't do this, then dp.transpose(2,1,0) gives dp[output][asset] as 0 !!
        for name in self.outputs:
            context.dp[name] = context.dp[name]

        return context.dp
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _apply_pandas_function(self, context):

        '''
        Routine to apply pandas function to column(s) of data provided as Pandas DataFrame.
        Allowed functions include all the pandas.rolling_ and pandas.expanding_ functions.
        NOTE: corr and cov are NOT allowed here, but must be implemented as CUSTOM FUNCTIONS
        Inputs:
            dp = Pandas DataPanel with data to be transformed in one (or more) panel items
            NOTE: in the case of CORR or COV, columns contain price data for each stock.
            inputs = name(s) of item(s) containing data to be transformed (DataFrames with columns = asset names)
            function = name of pandas function provided by user (pd.rolling_  or pd.expanding_ )
            args = list of arguments required by function
        Output:
            Pandas DataPanel with appended items containing the transformed data as DataFrames, or,
            as in the case of CORR and COV functions, the item is a DataPanel of correlations/covariances

        '''
        if 'corr' in self.function.__name__ or 'cov' in self.function.__name__:
            raise ValueError('** ERROR: Correlation and Covariance must be implemented as CUSTOM FUNCTIONS')

        for asset in context.dp.minor_axis:
            context.dp[self.outputs[0]] = self.function(context.dp[self.inputs[0]], *self.args)

        return context.dp

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # Custom Transforms

    def n_period_return(self, context):

        '''
        percentage return (optionally, excess return) over n periods
        most recent period can optionally be skipped

        kwargs[0] = 'no of periods'
        kwargs[1] = 'period' : 'D'|'W'|'M' (day|week||month)
        kwargs[2] = 'skip_period' (optional = False)

        '''
        try:
            skip_period = self.kwargs['skip_period']
        except:
            skip_period = False

        # TODO : need to return excess_return, depending on risk_free

        prices = context.dp[self.inputs[0]]

        no_of_periods = self.kwargs['lookback']
        # if no 'period' kwarg, assume 'D'
        try:
            period = self.kwargs['period']
        except:
            period = 'D'

        if period in ['W', 'M']:
            returns = prices.resample(period).last().pct_change(no_of_periods)
        elif period == 'D':
            returns = prices.pct_change(no_of_periods)

        idx = -1
        if skip_period:
            idx = - 2

        df = pd.DataFrame(0, index=context.dp.major_axis,
                          columns=context.dp.minor_axis)
        if not isinstance(context.risk_free, int):
            returns = returns.sub(returns[context.risk_free], axis=0)

        ds = returns.iloc[idx]
        df.iloc[-1] = ds

        context.dp[self.outputs[0]] = df

        return context.dp
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def simple_mean_monthly_average(self, context):

        h = context.dp[self.inputs[0]]
        lookback = self.kwargs['lookback']
        ds = h.resample('M').last()[-lookback - 1:-1].mean()

        df = pd.DataFrame(0, index=h.index, columns=h.columns)
        df.iloc[-1] = ds

        context.dp[self.outputs[0]] = df

        return context.dp

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def momentum(self, context):

        lookback = self.kwargs['lookback']
        ds = context.dp[self.inputs[0]].iloc[-1] / context.dp[self.inputs[0]].iloc[-lookback] - 1

        df = pd.DataFrame(0, index=context.dp.major_axis, columns=context.dp.minor_axis)
        df.iloc[-1] = ds

        context.dp[self.outputs[0]] = df

        return context.dp

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def daily_returns(self, context):

        context.dp[self.outputs[0]] = context.dp['price'].pct_change(1)

        return context.dp

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def excess_momentum(self, context):

        lookback = self.kwargs['lookback']
        ds = context.dp['price'].pct_change(lookback).iloc[-1] - \
             context.dp['price'][context.risk_free].pct_change(lookback).iloc[-1]

        df = pd.DataFrame(0, index=context.dp.major_axis, columns=context.dp.minor_axis)
        df.iloc[-1] = ds

        context.dp[self.outputs[0]] = df

        return context.dp

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def log_returns(self, context):

        try:
            context.dp[self.outputs[0]] = np.log(1. + context.dp['price'].pct_change(1))
        except:
            raise RuntimeError("Inputs must be ['price']")

        return context.dp

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def historic_volatility(self, context):

        lookback = self.kwargs['lookback']
        try:
            ret_log = np.log(1. + context.dp['price'].pct_change())
        except:
            raise RuntimeError("Inputs must be ['price']")

        # this is for pandas < 0.18
        # hist_vol = pd.rolling_std(ret_log, lookback)

        # this is for pandas ver > 0.18
        hist_vol = ret_log.rolling(window=lookback, center=False).std()

        context.dp[self.outputs[0]] = hist_vol * np.sqrt(252 / lookback)

        return context.dp
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def average_excess_return_momentum(self, context):

        '''
        Average Excess Return Momentum

        average_excess_return_momentum is the average of monthly returns in excess of the risk_free rate for multiple
        periods (1,3,6,12 months). In addtion, average momenta < 0 are set to 0.

        '''
        h = context.dp[self.inputs[0]].copy()
        hm = h.resample('M').last()
        hb = h.resample('M').last()[context.risk_free]

        ds = (hm.ix[-1] / hm.ix[-2] - hb.ix[-1] / hb.ix[-2] + hm.ix[-1] / hm.ix[-4]
              - hb.ix[-1] / hb.ix[-4] + hm.ix[-1] / hm.ix[-7] - hb.ix[-1] / hb.ix[-7]
              + hm.ix[-1] / hm.ix[-13] - hb.ix[-1] / hb.ix[-13]) / 22
        ds[ds < 0] = 0
        df = pd.DataFrame(0, index=h.index, columns=h.columns)
        df.iloc[-1] = ds

        context.dp[self.outputs[0]] = df

        return context.dp
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def paa_momentum(self, context):

        ds = context.dp[self.inputs[0]].iloc[-1] / context.dp[self.inputs[1]].iloc[-1] - 1

        df = pd.DataFrame(0, index=context.dp.major_axis, columns=context.dp.minor_axis)
        df.iloc[-1] = ds

        context.dp[self.outputs[0]] = df

        return context.dp

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def crossovers(self, context):
        df1 = context.dp[self.inputs[0]]
        df2 = context.dp[self.inputs[1]]
        down = (df1 > df2) & (df1.shift(1) < df2.shift(1)).astype(int)
        up = (df1 < df2) & (df1.shift(1) > df2.shift(1)).astype(int)
        # returns +1 for crosses above and -1 for crosses below
        return down * (-1) + up
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def slope(self, context):

        lookback = self.kwargs['lookback']
        ds = pd.Series(index=context.dp.minor_axis)
        for asset in context.dp.minor_axis:
            ds[asset] = talib.LINEARREG_SLOPE(context.dp[self.inputs[0]][asset].values, lookback)[-1]
        df = pd.DataFrame(0, index=context.dp.major_axis, columns=context.dp.minor_axis)
        df.iloc[-1] = ds
        return df


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class Configurator():
    '''
    The Configurator uses the Strategy Parameters set up by the StrategyParameters Class and dictionary of global
    parameters to create all the objects required for the algorithm.

    '''

    # def __init__ (self, context, strategies, global_parameters=None) :
    def __init__(self, context, strategies):
        self.strategies = strategies
        # self.global_parameters = global_parameters
        # self._set_global_parameters (context)
        context.tranforms = define_transforms(context)

        context.algo_rules = define_rules(context)
        self._configure_algo_strategies(context)
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _configure_algo_strategies(self, context):
        for s in self.strategies:
            self._check_valid_parameters(context, s)
            self._configure_strategy(context, s)
            # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    # TODO: would be better to make this table-driven
    # TODO : check strategy names are unique
    # TODO : compute context.max_lookback

    def _check_valid_parameters(self, context, strategy):
        N = len(strategy.portfolios)
        s = strategy
        self._check_valid_parameter(context, s, strategy.portfolios, 'portfolios', list, N, list, ''),
        self._check_valid_parameter(context, s, strategy.portfolio_allocation_modes, 'portfolio_allocation_modes',
                                    list, N, str, VALID_PORTFOLIO_ALLOCATION_MODES),
        self._check_valid_parameter(context, s, strategy.security_weights, 'security_weights', list, N, list, ''),
        self._check_valid_parameter(context, s, strategy.portfolio_allocation_formulas, 'portfolio_allocation_formulas',
                                    list,
                                    N, str, VALID_PORTFOLIO_ALLOCATION_FORMULAS),
        self._check_valid_parameter(context, s, strategy.security_scoring_methods, 'security_scoring_methods', list, N,
                                    str, VALID_SECURITY_SCORING_METHODS),
        self._check_valid_parameter(context, s, strategy.security_scoring_factors, 'security_scoring_factors', list, N,
                                    dict, ''),
        self._check_valid_parameter(context, s, strategy.security_n_tops, 'security_n_tops', list, N, int, '')
        self._check_valid_parameter(context, s, strategy.portfolio_scoring_method, 'portfolio_scoring_method', list, 1,
                                    str, VALID_PORTFOLIO_SCORING_METHODS),
        self._check_valid_parameter(context, s, strategy.portfolio_scoring_factors, 'portfolio_scoring_factors', list,
                                    1, dict, ''),
        self._check_valid_parameter(context, s, strategy.portfolio_n_top, 'portfolio_n_top', list, 1, int, '')
        self._check_valid_parameter(context, s, strategy.protection_modes, 'protection_modes', list, N,
                                    str, VALID_PROTECTION_MODES),
        self._check_valid_parameter(context, s, strategy.protection_rules, 'protection_rules', list, N, str, ''),
        self._check_valid_parameter(context, s, strategy.protection_formulas, 'protection_formulas', list, N,
                                    str, VALID_PROTECTION_FORMULAS),
        self._check_valid_parameter(context, s, strategy.cash_proxies, 'cash_proxies', list, N, type(symbols('SPY')[0]),
                                    ''),
        self._check_valid_parameter(context, s, strategy.strategy_allocation_mode, 'strategy_allocation_mode', str,
                                    1, str, VALID_STRATEGY_ALLOCATION_MODES)
        self._check_valid_parameter(context, s, strategy.portfolio_weights, 'portfolio_weights', list, N, float, ''),
        self._check_valid_parameter(context, s, strategy.strategy_allocation_formula, 'strategy_allocation_formula',
                                    str,
                                    1, str, VALID_STRATEGY_ALLOCATION_FORMULAS)
        self._check_valid_parameter(context, s, strategy.strategy_allocation_rule, 'strategy_allocation_rule', str,
                                    1, str, '')
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _check_valid_parameter(self, context, s, param, name, param_type, param_length, item_type, valid_params):

        if name in ['strategy_allocation_mode', 'portfolio_weights', 'strategy_allocation_formula',
                    'strategy_parameters', 'strategy_allocation_rule', 'portfolio_scoring_method',
                    'portfolio_scoring_factors', 'portfolio_n_top']:
            self._check_strategy_parameters(context, s, param, name, param_type, param_length, item_type, valid_params)
        else:
            # if param is None and name in NONE_NOT_ALLOWED :
            #     raise RuntimeError ('"None" not allowed for parameter {}'.format(name))
            # if param is None and 'FIXED' in s.portfolio_allocation_modes:
            #     raise RuntimeError ('Parameter {} cannot be None for portfolio_allocation_mode "FIXED"'.format(name))
            # else:
            #     # valid None parameter
            #     return

            self._check_param_type(name, param, param_type)

            if len(param) != param_length:
                raise RuntimeError('Parameter {} must be of length {} not {}'.format(name, param_length, len(param)))
            for n in range(param_length):
                if param[n] == None and name in NONE_NOT_ALLOWED:
                    raise RuntimeError('"None" not allowed for parameter {}'.format(name))
                elif param[n] == None:
                    if name == 'scoring_factors' and s.protection_modes == 'RS':
                        self._check_valid_scoring_factors(name, param[n])
                    # if name == 'security_n_tops' and s.portfolio_allocation_modes[n] == 'FIXED' :
                    #     if param[n] != len(s.security_weights[n]) :
                    #         raise RuntimeError ('For portfolio_allocation_mode = "FIXED", n_tops must equal no of security weights')
                    continue
                if valid_params != "":
                    if param[n] not in valid_params:
                        raise RuntimeError('Invalid {} {}'.format(name, param[n]))
                if not isinstance(param[n], item_type):
                    raise RuntimeError('Items of {} must be of type {} not {}'.format(name, item_type, type(param[n])))
                if name == 'portfolios':
                    self._check_valid_portfolio(param[n])

                if name.endswith('_weights') and np.sum(param[n]) != 1.:
                    raise RuntimeError('Sum of {} must equal 1, not {}'.format(name, np.sum(param)))

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _check_strategy_parameters(self, context, s, param, name, param_type, param_length, item_type, valid_params):
        if name == 'strategy_allocation_mode':
            if param not in valid_params:
                raise RuntimeError('Invalid strategy_allocation_mode {}'.format(param))
        elif name == 'portfolio_weights' and s.strategy_allocation_mode == 'FIXED':
            if np.sum(param) != 1.:
                raise RuntimeError('portfolio_weights must be a list of floating point numbers with sum = 1')
        elif name == 'strategy_allocation_formula':
            if param not in valid_params:
                raise RuntimeError('Invalid strategy_allocation_formula {}'.format(param))
        elif name == 'strategy_allocation_rule' and s.strategy_allocation_rule != None:
            valid_rules = [rule.name for rule in context.algo_rules]
            if s.strategy_allocation_rule not in valid_rules:
                raise RuntimeError(
                    'Strategy rule {} not found. Check rule definitions'.format(s.strategy_allocation_rule))
        elif name == 'portfolio_scoring_method':
            if param not in valid_params:
                raise RuntimeError('Invalid strategy_allocation_formula {}'.format(param))
                # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _check_param_type(self, name, param, param_type):
        if not isinstance(param, param_type):
            raise RuntimeError('Parameter {} must be of type {} not {}'.format(name, param_type, type(param)))

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _check_valid_scoring_factors(self, name, factors):
        sum_of_weights = 0.

        for key in factors.keys():
            if not key[0] in ['+', '-']:
                raise RuntimeError('First character of scoring factor {}, must be "+" or "-"'.format(key))
            sum_of_weights += factors[key]
        if sum_of_weights != 1.:
            raise RuntimeError('Sum of {} weights must equal 1, not {}'.format(name, sum_of_weights))

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _check_valid_portfolio(self, pfolio):
        if len(pfolio) < 1:
            raise RuntimeError('Portfolio must have at least one item')
        for n in range(len(pfolio)):
            if not isinstance(pfolio[n], type(symbols('SPY')[0])):
                raise RuntimeError('portfolio item {} must be of type '.format(type(symbols('SPY')[0])))
                # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _configure_strategy(self, context, s):

        portfolios = []

        for n in range(len(s.portfolios)):
            if s.security_scoring_methods[n] is None:
                scoring_model = None
            else:
                scoring_model = ScoringModel(context,
                                             method=s.security_scoring_methods[n],
                                             factors=s.security_scoring_factors[n],
                                             n_top=s.security_n_tops[n])

            if s.protection_modes[n] == None:
                downside_protection_model = None
            else:
                downside_protection_model = DownsideProtectionModel(context,
                                                                    mode=s.protection_modes[n],
                                                                    rule=s.protection_rules[n],
                                                                    formula=s.protection_formulas[n])

            portfolios = portfolios + \
                         [Portfolio(context,
                                    ID=s.ID + '_p' + str(n + 1),
                                    securities=s.portfolios[n],
                                    allocation_model=AllocationModel(context,
                                                                     mode=s.portfolio_allocation_modes[n],
                                                                     weights=s.security_weights[n],
                                                                     formula=s.portfolio_allocation_formulas[n],
                                                                     kwargs=s.portfolio_allocation_kwargs[n]
                                                                     ),
                                    scoring_model=scoring_model,
                                    downside_protection_model=downside_protection_model,
                                    cash_proxy=s.cash_proxies[n]
                                    )]

        if s.portfolio_scoring_method is None:
            scoring_model = None
        else:
            scoring_model = ScoringModel(context,
                                         method=s.portfolio_scoring_method,
                                         factors=s.portfolio_scoring_factors,
                                         n_top=s.portfolio_n_top)
        s.strategy = Strategy(context,
                              ID=s.ID,
                              allocation_model=AllocationModel(context,
                                                               mode=s.strategy_allocation_mode,
                                                               weights=s.portfolio_weights,
                                                               formula=s.strategy_allocation_formula,
                                                               kwargs=s.strategy_allocation_kwargs,
                                                               rule=s.strategy_allocation_rule),
                              scoring_model=scoring_model,
                              portfolios=portfolios
                              )


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class StrategyParameters():
    '''
    StrategyParameters hold the parameters for each strategy for a single or multistrategy algoritm

    calling:

    strategy = StrategyParameters(context, portfolios, portfolio_allocation_modes, security_weights,
                         portfolio_allocation_formulas,
                         scoring_methods, scoring_factors, n_tops,
                         protection_modes, protection_rules, protection_formulas,
                         cash_proxies, strategy_allocation_mode, portfolio_weights=None,
                         strategy_allocation_formula, strategy_allocation_rule)

    see below for definition of each parameter

    '''

    # NOTE: kwarg label 'lookback' should be ALWAYS be used for timeseries lookback periods!
    def __init__(self, context, ID, portfolios=[],
                 portfolio_allocation_modes=[], security_weights=None,
                 portfolio_allocation_formulas=None,
                 portfolio_allocation_kwargs=None,
                 security_scoring_methods=None, security_scoring_factors=None,
                 security_n_tops=None,
                 protection_modes=None, protection_rules=None, protection_formulas=None,
                 cash_proxies=[],
                 strategy_allocation_mode='EW', portfolio_weights=None,
                 portfolio_scoring_method=None, portfolio_scoring_factors=None,
                 portfolio_n_top=None,
                 strategy_allocation_formula=None,
                 strategy_allocation_kwargs=None,
                 strategy_allocation_rule=None):

        # strategy ID, must be unique str
        # eg 'strat1'
        self.ID = ID
        # list of n valid security lists (must be at least one security list)
        # eg [symbols('SPY','EEM')] or [symbols('SPY','EEM'), symbols('TLT','JNK','SHY'),....]
        self.portfolios = portfolios
        n = len(portfolios)
        # list of n VALID_PORTFOLIO_ALLOCATION_MODES, one for each portfolio
        # eg ['EW'] or ['EW', 'PROPORTIONAL',.....]
        self.portfolio_allocation_modes = portfolio_allocation_modes
        # either None or list of n kwargs each containing kwargs matching porfolio_allocation_modes
        # eg None or [kwargs1] or [kwargs1, kwargs2, ....] where kargsn = dict of kwargs relevant to modes
        self.portfolio_allocation_kwargs = portfolio_allocation_kwargs
        if portfolio_allocation_kwargs is None:
            self.portfolio_allocation_kwargs = [None for i in range(n)]
        # None or list of n lists of security weights for 'FIXED' portfolio_allocation_modes, else None
        # eg None or [[0.2,0.8]] or [[0.5,0.5],[0.1,0.7,0.2]...] where sum of each list = 1
        self.security_weights = security_weights
        if security_weights is None:
            self.security_weights = [None for i in range(n)]
            # None or list of n VALID_PORTFOLIO_ALLOCATION_FORMULAS for 'BY_FORMULA'
        # portfolio_allocation_modes, else None
        # eg None or [valid formula] or [None, valid formula, ...] for each portfolio where allocation 'BY_FORMULA'
        self.portfolio_allocation_formulas = portfolio_allocation_formulas
        if portfolio_allocation_formulas is None:
            self.portfolio_allocation_formulas = [None for i in range(n)]
            # None or list of n VALID_SECURITY_SCORING_METHODS, one for each portfolio
        # eg None or ['RS'] or [None, 'EAA', ....]
        self.security_scoring_methods = security_scoring_methods
        if security_scoring_methods is None:
            self.security_scoring_methods = [None for i in range(n)]
            # None or list of n dicts of scoring factors, relevant to each scoring method
        # eg None or [factors1] or [None, factors2, ...] where factorsn = dict of factors relevant to scoring methods
        self.security_scoring_factors = security_scoring_factors
        if security_scoring_factors is None:
            self.security_scoring_factors = [None for i in range(n)]
            # None or list of n_tops, one for each ranked portfolio, else None; n_top <= len(portfolio) - 1
        # eg None or [1], [1,2,...]
        self.security_n_tops = security_n_tops
        if security_n_tops is None:
            self.security_n_tops = [None for i in range(n)]
            # None or list of n VALID_PROTECTION_MODES, one for each portfolio
        # eg None or ['RAA'] or [None, 'BY_RULE','BY_FORMULA', ....]
        self.protection_modes = protection_modes
        if protection_modes is None:
            self.protection_modes = [None for i in range(n)]
            # None or list of n valid rules for portfolios with protection mode 'BY_RULE', else None
        # eg None or [valid rule] or [None, valid rule, ...] for each portfolio where allocation 'BY_RULE'
        self.protection_rules = protection_rules
        if protection_rules is None:
            self.protection_rules = [None for i in range(n)]
            # None or list of n VALID_PROTECTION_FORMULAS for portfolios with protection mode 'BY_FORMULA', else None
        # eg None or [valid formula] or [None, valid formula, ...] for each portfolio where allocation 'BY_FORMULA'
        self.protection_formulas = protection_formulas
        if protection_formulas is None:
            self.protection_formulas = [None for i in range(n)]
            # list of n valid securities to be used as cash proxies, one for each portfolio
        # eg [symbol('SHY')] or [symbol('SHY'), symbol('TLT'),...]  NOTE: symbol NOT symbols!!
        self.cash_proxies = cash_proxies
        # any one of VALID_STRATEGY_ALLOCATION_MODES
        # eg 'RISK_TARGET'
        self.strategy_allocation_mode = strategy_allocation_mode
        # None or any kwargs relevant to the strategy_allocation_mode
        # eg {'lookback': 100, 'target_risk': 0.01}
        self.strategy_allocation_kwargs = strategy_allocation_kwargs
        # None or list of n portfolio weights (sum = 1) if strategy_allocation_mode is 'FIXED'
        self.portfolio_weights = portfolio_weights
        if portfolio_weights is None:
            self.portfolio_weights = [None for i in range(n)]
            # None or one of VALID_STRATEGY_ALLOCATION_FORMULAS, if strategy_allocation_mode is 'BY_FORMULA'
        # eg 'PAA'
        self.strategy_allocation_formula = strategy_allocation_formula
        # None or one of VALID_PORTFOLIO_SCORING_METHODS
        # eg 'RS'
        self.portfolio_scoring_method = portfolio_scoring_method
        # None or dict of factors to be used for scoring (ranking) portfolios
        # eg {'+factor1': 10, '-factor2':20} - NOTE that factor names must be prefixed by '+' or '-'
        # to indicate whether to rank factor ascending (+) or descending (-)
        self.portfolio_scoring_factors = portfolio_scoring_factors
        # None or integer <= no of portfolios - 1
        # eg 2
        self.portfolio_n_top = portfolio_n_top
        # None or one of VALID_STRATEGY_ALLOCATION_RULES
        # eg None
        self.strategy_allocation_rule = strategy_allocation_rule
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# def handle_data(context, data):

#   TRAILING STOPS
# if trailing stops not required, this can be commented out

#     if not context.use_trailing_stops:
#         return

#     # see https://www.quantopian.com/posts/trailing-stop-loss-with-multiple-securities
#     for security in context.portfolio.positions:
#         current_position = context.portfolio.positions[security].amount
#         context.stop_price[security] = max(context.stop_price[security] if security in context.stop_price
#                                         else 0, context.stop_pct * data.current(security, 'price'))
#         if (data.current(security, 'price') < context.stop_price[security]) and (current_position > 0):
#             order_target_percent(security, 0)
#             del context.stop_price[security]
#             log.info("Trail Selling {} at {}".format(security.symbol, data.current(security, 'price')))
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def before_trading_start(context, data):
    """
    Called every day before market open.
    """

    # log.info('PLATFORM = ' + get_environment('platform') + str(context.day_no))

    # ONLY IF WE REQUIRE TO FILL THE PIPELINE WITH DATA (IE NOT REQUIRED FOR QUANTOPIUAN)
    # if get_environment('platform') == 'zipline':
    #     # allow data buffer to fill in the ZIPLINE ENVIRONMENT
    #     if context.day_no <= context.max_lookback:
    #         context.day_no += 1
    #         return

    # generate updated algo data
    # log.info('GENERATE DATA')

    context.algo_data = context.data.update(context, data)

    if np.sum(context.strategies[0].weights) > 1.e-07:
        # wait until first allocation to generate portfolio and strategy metrics
        context.data.update_portfolio_and_strategy_metrics(context, data)

    return context.algo_data


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# define transforms
#########################################################################################################
#########################################################################################################
# the following routines contain all the configuration details
# any transform which relies on lookback data MUST have a 'lookback' kwarg
# and, optionally, 'period' = <no. of days> |'W'| 'M'
# NOTE: kwarg label 'lookback' should be ALWAYS be used for timeseries lookback periods!

def define_transforms(context):
    # Define transforms
    # select transforms required and make sure correct parameters are used
    # no need to comment out unused transforms, but they will slow algo down

    log.info('DEFINE TRANSFORMS')

    context.transforms = [
        Transform(context, name='momentum', function=Transform.n_period_return, inputs=['price'],
                  kwargs={'lookback': 45, 'risk_free': 0, 'skip_period': False}, outputs=['momentum']),
        Transform(context, name='mom_A', function=talib.ROCP, inputs=['price'],
                  kwargs={'lookback': 43}, outputs=['mom_A']),
        Transform(context, name='mom_B', function=talib.ROCP, inputs=['price'],
                  kwargs={'lookback': 21}, outputs=['mom_B']),
        Transform(context, name='daily_returns', function=Transform.daily_returns, inputs=['price'],
                  kwargs={}, outputs=['daily_returns']),
        Transform(context, name='vol_C', function=talib.STDDEV, inputs=['daily_returns'],
                  kwargs={'lookback': 20}, outputs=['vol_C']),
        Transform(context, name='hist_vol', function=Transform.historic_volatility, inputs=['price'],
                  kwargs={'lookback': 45}, outputs=['hist_vol']),
        Transform(context, name='slope', function=Transform.slope, inputs=['price'],
                  kwargs={'lookback': 100}, outputs=['slope']),
        Transform(context, name='TMOM', function=Transform.momentum, inputs=['price'],
                  kwargs={'lookback': 43}, outputs=['TMOM']),
        Transform(context, name='MA', function=talib.SMA, inputs=['price'],
                  kwargs={'lookback': 100}, outputs=['MA']),
        Transform(context, name='R', function=Transform.average_excess_return_momentum, inputs=['price'],
                  kwargs={'lookback': 13, 'period': 'M'}, outputs=['R']),
        Transform(context, name='RMOM', function=Transform.momentum, inputs=['price'],
                  kwargs={'lookback': 43}, outputs=['RMOM']),
        Transform(context, name='TMOM', function=Transform.excess_momentum, inputs=['price'],
                  kwargs={'lookback': 43}, outputs=['TMOM']),
        Transform(context, name='EMOM', function=Transform.momentum, inputs=['price'],
                  kwargs={'lookback': 43}, outputs=['EMOM']),
        Transform(context, name='volatility', function=talib.STDDEV, inputs=['daily_returns'],
                  kwargs={'lookback': 43}, outputs=['volatility']),
        Transform(context, name='smma', function=Transform.simple_mean_monthly_average, inputs=['price'],
                  kwargs={'lookback': 1, 'period': 'M'}, outputs=['smma']),
        Transform(context, name='mom', function=Transform.paa_momentum, inputs=['price', 'smma'],
                  kwargs={'lookback': 2, 'period': 'M'}, outputs=['mom']),
        Transform(context, name='smma_12', function=Transform.simple_mean_monthly_average, inputs=['price'],
                  kwargs={'lookback': 12, 'period': 'M'}, outputs=['smma_12']),
        Transform(context, name='rebalance_signal', function=Transform.crossovers, inputs=['price', 'MA'],
                  kwargs={'timeperiods': 100}, outputs=['rebalance_signal']),
    ]

    return context.transforms


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def define_rules(context):  # Define rules
    # select rules required and make sure correct transform names are used
    # no need to comment out unused rules

    log.info('DEFINE RULES')

    context.algo_rules = [
        # Rule(context, name='absolute_momentum_rule', rule="'price' < 'smma' "),
        # Rule(context, name='dual_momentum_rule', rule="'TMOM' < 0"),
        Rule(context, name='smma_rule', rule="'price' < 'smma'"),
        # Rule(context, name='complex_rule', rule="'price' < smma or 'TMOM' < 0"),
        Rule(context, name='momentum_rule', rule="'price' < 'MA'"),
        Rule(context, name='EAA_rule', rule="'R' <= 0"),
        Rule(context, name='paa_rule', rule="'mom' <= 0"),
        Rule(context, name='paa_filter', rule="'mom' > 0"),
        Rule(context, name='momentum_rule1', rule="'price' < 'smma_12'"),
        Rule(context, name='riskon', rule="'price' > 'smma_12'", apply_to=context.market_proxy),
        Rule(context, name='riskoff', rule="'price' <= 'smma_12'", apply_to=context.market_proxy),
        Rule(context, name='neutral', rule="'slope' <= 0.1 and 'slope' >= -0.1",
             apply_to=context.market_proxy),
        Rule(context, name='bull', rule="'slope' > 0.1", apply_to=context.market_proxy),
        Rule(context, name='bear', rule="'slope' < -0.1", apply_to=context.market_proxy)
        # Rule(context, name='rebalance_rule', rule="'rebalance_signal' != 0"),
    ]

    return context.algo_rules


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def set_global_parameters(context):
    # set the following parameters as required

    context.show_positions = True
    # select records to show in algo.show_records()
    context.show_records = True

    # replace cash_proxy with risk_free if context.allow_cash_proxY_replacement is True
    # and cash_proxy price is <= average cash_proxy price over last context.cash_proxy_lookback days
    context.allow_cash_proxy_replacement = False
    context.cash_proxy_lookback = 43  # must be <= context.max_lookback

    context.use_trailing_stops = False
    context.stop_pct = 0.92
    context.stop_price = {}

    # to calculate portfolio and strategy Sharpe ratios
    context.SR_lookback = 63
    context.SD_factor = 0

    # position only changed if percentage change > threshold
    context.threshold = 0.01

    # the following can be changed
    context.market_proxy = symbol('SPY')
    context.risk_free = symbol('SHY')

    set_commission(commission.PerTrade(cost=10.0))
    context.leverage = 1.0

    # parameters for rebalance period
    context.rebalance_period = 'ME'  # 'D'|'WS'|'WE'|'MS'|'ME'|'A'
    context.days_offset = 2
    context.on_open = True  # if false, then market_close
    context.hours = 0
    context.minutes = 1

    context.rebalance_interval = 1  # rebalancing will occur every balance_interval * balance_period


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def set_strategy_parameters(context):
    # If not required, parameters may be omitted
    # no need to comment out unused strategies
    # strategies used by the algo selected in set_algo_parameters

    # configure strategies below
    # ####################################################################################################
    #     # TEST STRATEGY TO USE A PIPELINE
    # s0 = StrategyParameters(context, ID='s0',
    #                 portfolios=[symbols('SPY','IEF')],
    #                 portfolio_allocation_modes=['FIXED'],
    #                 security_weights=[[0.6,0.4]],
    #                 # security_scoring_methods=[None],
    #                 # security_scoring_factors=[None],
    #                 # security_n_tops=[2],
    #                 # protection_modes=[None],
    #                 # protection_rules=[None],
    #                 cash_proxies=[symbol('TLT')],
    #                 strategy_allocation_mode='EW',
    #                )
    # ####################################################################################################
    #     # single RS portfolio with downside protection
    # s1 = StrategyParameters(context, ID='s1',
    #                 portfolios=[symbols( 'IVV', 'IJH', 'IJR', 'VEA',
    #                                     'VWO', 'VNQ', 'AGG')],
    #                 portfolio_allocation_modes=['EW'],
    #                 security_scoring_methods=['RS'],
    #                 security_scoring_factors=[{'+momentum': 1.0}],
    #                 security_n_tops=[2],
    #                 protection_modes=['BY_RULE'],
    #                 protection_rules=['smma_rule'],
    #                 cash_proxies=[symbol('TLT')],
    #                 strategy_allocation_mode='EW',
    #                )
    # ####################################################################################################
    #     # RAA - Robust Asset Allocation (4 portfolios)
    #     #
    # s2 = StrategyParameters(context, ID='s2',
    #                         portfolios=[symbols('MDY', 'EFA'), symbols('VNQ', 'RWX'),
    #                                     symbols('GLD', 'AGG'),
    #                                     symbols('EDV', 'EMB')],
    #                         portfolio_allocation_modes=['EW', 'EW', 'EW', 'EW'],
    #                         security_scoring_methods=['RS', 'RS', 'RS', 'RS'],
    #                         security_scoring_factors=[{'+EMOM': 1.}, {'+EMOM': 1.},
    #                                                   {'+EMOM': 1.}, {'+EMOM': 1.}],
    #                         security_n_tops=[1, 1, 1, 1],
    #                         protection_modes=['RAA', 'RAA', 'RAA', 'RAA'],
    #                         cash_proxies=[symbol('TLT'), symbol('TLT'), symbol('TLT'), symbol('TLT')],
    #                         strategy_allocation_mode='MAX_SHARPE',
    #                         strategy_allocation_kwargs={'lookback': 21, 'shorts': False},
    #                         )
    # ####################################################################################################
    #     # Strategy 3 - minimumn correlation strategy
    # s3 = StrategyParameters(context, ID='s3',
    #                 portfolios=[symbols( 'IVV', 'IJH', 'IJR', 'VEA',
    #                                     'VWO', 'VNQ', 'AGG')],
    #                 portfolio_allocation_modes=['MIN_CORRELATION'],
    #                 portfolio_allocation_kwargs=[{'lookback': 21, 'risk_adjusted': True}],
    #                 protection_modes=['BY_RULE'],
    #                 protection_rules=['smma_rule'],
    #                 protection_formulas=None,
    #                 cash_proxies=[symbol('SHY')],
    #                 strategy_allocation_mode='EW'
    #                )
    # ####################################################################################################
    #     # sdp_1 - downside protection strategy based on Alpha Architect DPM Rule: 50% TMOM, 50% MA
    #     # http://blog.alphaarchitect.com/2015/08/13/avoiding-the-big-drawdown-downside-protection-investment-strategies/#gs.qtrlStY
    # sdp_1 = StrategyParameters(context, ID='sdp_1',
    #                  portfolios=[symbols( 'XLY', 'XLF', 'XLK', 'XLE', 'XLV',  'XLI',
    #                                      'XLP', 'XLB', 'XLU')],
    #                  portfolio_allocation_modes=['EW'],
    #                  protection_modes=['RAA'],
    #                  # protection_modes=['BY_RULE'],
    #                  # protection_rules=['smma_rule'],
    #                  # protection_rules=['momentum_rule'],
    #                  cash_proxies=[symbol('SHY')],
    #                 strategy_allocation_mode='EW'
    #                 )
    # ####################################################################################################
    #     # RS with downside protection, single portfolio, EtfReplay-like ranking formula
    # rs_1 = StrategyParameters(context, ID='rs_1',
    #                 portfolios=[symbols( 'MDY', 'EFA')],
    #                 portfolio_allocation_modes=['EW'],
    #                 security_scoring_methods=['RS'],
    #                 security_scoring_factors=[{'+mom_A': 0.65, '+mom_B' : 0.35, '-vol_C' : 0.}],
    #                 security_n_tops=[1],
    #                 protection_modes=['BY_RULE'],
    #                 protection_rules=['smma_rule'],
    #                 cash_proxies=[symbol('TLT')],
    #                 strategy_allocation_mode='EW'
    #                )
    # ####################################################################################################
    #     # RS with 2 portfolios based on EtfReplay ranking model
    # rs_2 = StrategyParameters(context, ID='rs_2',
    #                 portfolios=[symbols( 'MDY', 'EFA'), symbols('IHF', 'EFA')],
    #                 portfolio_allocation_modes=['EW', 'FIXED'],
    #                 security_weights=[None, [0.8, 0.2]],
    #                 security_scoring_methods=['RS', 'RS'],
    #                 security_scoring_factors=[{'+mom_A': 0.65, '+mom_B' : 0.35, '-vol_C' : 0.},
    #                                           {'+mom_A': 0.65, '+mom_B' : 0.35, '-vol_C' : 0.}],
    #                 security_n_tops=[1, 2],
    #                 protection_modes=['BY_RULE', 'BY_RULE'],
    #                 protection_rules=['smma_rule', 'smma_rule'],
    #                 cash_proxies=[symbol('TLT'), symbol('TLT')],
    #                 strategy_allocation_mode='FIXED',
    #                 portfolio_weights=[0.6, 0.4]
    #                )
    # ####################################################################################################
    #     # EAA - Elastic Asset Allocation
    #     # http://indexswingtrader.blogspot.co.za/2015/01/a-primer-on-elastic-asset-allocation.html
    eaa_1 = StrategyParameters (context, ID='eaa_1',
                    portfolios=[symbols('EEM', 'IEF', 'IEV', 'MDY', 'QQQ', 'TLT', 'XLV')],
                    portfolio_allocation_modes=['PROPORTIONAL'],
                    security_scoring_methods=['EAA'],
                    # Golden Defensive EAA: wi ~ zi = squareroot( ri * (1-ci) )
                    security_scoring_factors = [{'R': 1.0, 'C' : 1.0, 'V' : 0.0, 'S' : 0.5, 'eps' : 1e-6}],
                    protection_modes=['BY_FORMULA'], protection_rules=['EAA_rule'],
                    protection_formulas=['DPF'], cash_proxies=[symbol('TLT')], strategy_allocation_mode='EW')
    # ####################################################################################################
    #     # Risk_on Risk_off
    # roo_1 = StrategyParameters(context, ID='roo_1',
    #                  portfolios=[symbols('SPY', 'QQQ', 'IWM', 'VGK', 'EWJ', 'EEM',
    #                                      'IYR', 'GSG', 'GLD'), symbols('TLT', 'TIP', 'LQD', 'SHY')],
    #                  portfolio_allocation_modes=['EW', 'EW'],
    #                  security_scoring_methods=['RS', 'RS'],
    #                  security_scoring_factors=[{'+smma': 1}, {'+smma': 1}],
    #                  security_n_tops=[3, 1],
    #                  protection_modes=['BY_RULE', None],
    #                  protection_rules=['momentum_rule1', None],
    #                  cash_proxies=[symbol('IEF'), symbol('SHY')], strategy_allocation_mode='EW')
    # ####################################################################################################
    #     # Adaptive Asset Allocation
    # aaa_1 = StrategyParameters(context, ID='aaa_1',
    #                 portfolios=[symbols( 'SPY', 'IWM', 'EFA', 'EEM', 'VNQ', 'GLD', 'GSG',
    #                                     'JNK', 'AGG', 'TIP', 'IEF', 'TLT')],
    #                 portfolio_allocation_modes=['VOLATILITY_WEIGHTED'],
    #                 security_scoring_methods=['RS'],
    #                 security_scoring_factors=[{'+mom': 1.0}],
    #                 security_n_tops=[3],
    #                 protection_modes=['BY_RULE'],
    #                 protection_rules=['smma_rule'],
    #                 cash_proxies=[symbol('TLT')],
    #                 strategy_allocation_mode='EW')
    ####################################################################################################
    # Protective Asset Allocation
    # http://indexswingtrader.blogspot.co.za/2016/04/introducing-protective-asset-allocation.html
    # paa_1 = StrategyParameters(context, ID='paa_1',
    #                  portfolios=[symbols('SPY', 'QQQ', 'IWM', 'VGK', 'EWJ', 'EEM',
    #                                      'IYR', 'GSG', 'GLD', 'LQD', 'TLT', 'HYG'),
    #                              symbols('IEF', 'TLT')],
    #                  portfolio_allocation_modes=['EW', 'EW'],
    #                  security_scoring_methods=['RS', 'RS'],
    #                  security_scoring_factors=[{'+mom': 1}, {'+mom': 1}],
    #                  security_n_tops=[3, 1],
    #                  protection_modes=['BY_RULE', None],
    #                  protection_rules=['paa_rule', None],
    #                  cash_proxies=[symbol('TLT'), symbol('TLT')],
    #                  strategy_allocation_mode='BY_FORMULA',
    #                  strategy_allocation_formula='PAA',
    #                  strategy_allocation_rule='paa_filter',
    #                  strategy_allocation_kwargs={'protection_factor': 1})
    ####################################################################################################
    # brs_1 = StrategyParameters(context, ID='brs_1',
    #                 portfolios=[symbols('CWB', 'JNK'), symbols('CWB', 'JNK'), symbols('CWB', 'JNK'),
    #                             symbols('CWB', 'PCY'), symbols('CWB', 'PCY'), symbols('CWB', 'PCY'),
    #                             symbols('CWB', 'TLT'), symbols('CWB', 'TLT'), symbols('CWB', 'TLT'),
    #                             symbols('JNK', 'PCY'), symbols('JNK', 'PCY'), symbols('JNK', 'PCY'),
    #                             symbols('JNK', 'TLT'), symbols('JNK', 'TLT'), symbols('JNK', 'TLT'),
    #                             symbols('PCY', 'TLT'), symbols('PCY', 'TLT'), symbols('PCY', 'TLT')],
    #                 portfolio_allocation_modes=['FIXED', 'FIXED', 'FIXED',
    #                                             'FIXED', 'FIXED', 'FIXED',
    #                                             'FIXED', 'FIXED', 'FIXED',
    #                                             'FIXED', 'FIXED', 'FIXED',
    #                                             'FIXED', 'FIXED', 'FIXED',
    #                                             'FIXED', 'FIXED', 'FIXED'],
    #                 security_weights=[[0.6, 0.4], [0.5, 0.5], [0.4, 0.6],
    #                                   [0.6, 0.4], [0.5, 0.5], [0.4, 0.6],
    #                                   [0.6, 0.4], [0.5, 0.5], [0.4, 0.6],
    #                                   [0.6, 0.4], [0.5, 0.5], [0.4, 0.6],
    #                                   [0.6, 0.4], [0.5, 0.5], [0.4, 0.6],
    #                                  [0.6, 0.4], [0.5, 0.5], [0.4, 0.6]],
    #                 cash_proxies=[symbol('TLT'), symbol('TLT'), symbol('TLT'),
    #                               symbol('TLT'), symbol('TLT'), symbol('TLT'),
    #                               symbol('TLT'), symbol('TLT'), symbol('TLT'),
    #                               symbol('TLT'), symbol('TLT'), symbol('TLT'),
    #                               symbol('TLT'), symbol('TLT'), symbol('TLT'),
    #                               symbol('TLT'), symbol('TLT'), symbol('TLT')],
    #                 strategy_allocation_mode='BRUTE_FORCE_SHARPE',
    #                 strategy_allocation_kwargs={'lookback' : 73})
    # ####################################################################################################
    # brs_2 = StrategyParameters(context, ID='brs_2',
    #                 portfolios=[symbols('CWB', 'JNK'), symbols('CWB', 'PCY'), symbols('CWB', 'TLT'),
    #                             symbols('JNK', 'PCY'), symbols('JNK', 'TLT'), symbols('PCY', 'TLT')],
    #                 portfolio_allocation_modes=['MAX_SHARPE', 'MAX_SHARPE', 'MAX_SHARPE',
    #                                             'MAX_SHARPE', 'MAX_SHARPE', 'MAX_SHARPE'],
    #                 portfolio_allocation_kwargs=[
    #                            {'lookback' : 73, 'shorts' : False},{'lookback' : 73, 'shorts' : False},
    #                            {'lookback' : 73, 'shorts' : False},{'lookback' : 73, 'shorts' : False},
    #                            {'lookback' : 73, 'shorts' : False},{'lookback' : 73, 'shorts' : False}],
    #                 cash_proxies=[symbol('TLT'), symbol('TLT'), symbol('TLT'),
    #                               symbol('TLT'), symbol('TLT'), symbol('TLT')],
    #                 strategy_allocation_mode='BRUTE_FORCE_SHARPE',
    #                 strategy_allocation_kwargs={'lookback' : 73, 'SD_factor' : 2})
    ####################################################################################################
    # context.strategy_parameters = [s1, s2, s3, sdp_1, rs_1, rs_2, eaa_1, roo_1, aaa_1, paa_1, brs_1, brs_2]
    context.strategy_parameters = [eaa_1]

    return context.strategy_parameters

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def set_algo_parameters(context, strategies):
    # UNCOMMENT ONLY ONE ALGO BELOW
    ###############################
    # simple downside protection algorithm
    # http://blog.alphaarchitect.com/2015/08/13/avoiding-the-big-drawdown-downside-protection-investment-            strategies/#gs.qtrlStY

    # strategy_ID = 'sdp_1'

    # algo = Algo (context, [s for s in strategies if s.ID == strategy_ID],
    #              allocation_model=AllocationModel(context, mode='EW', weights=None, formula=None),
    #             )
    ###############################
    # EAA - Elastic Asset Allocation
    # http://indexswingtrader.blogspot.co.za/2015/01/a-primer-on-elastic-asset-allocation.html

    # strategy_ID = 'eaa_1'

    # algo = Algo (context, [s for s in strategies if s.ID == strategy_ID],
    #              allocation_model=AllocationModel(context, mode='EW', weights=None, formula=None),
    #             )
    ###############################
    # multiple strategies, equally weighted

    # list of strategies by ID
    # strategy_IDs = ['s1', 's2', 's3', 'sdp_1']

    # algo = Algo (context, strategies=[s for s in strategies if s.ID in strategy_IDs],
    #              allocation_model=AllocationModel(context, mode='EW', weights=None, formula=None),
    #             )
    ###############################
    # run all uncommented strategies (other than regime-switching strategies)

    algo = Algo(context, strategies=[s for s in strategies],
                allocation_model=AllocationModel(context, mode='EW'), scoring_model=None,
                # allocation_model=AllocationModel(context, mode='RISK_PARITY', kwargs={'lookback':21}),     scoring_model=ScoringModel(context, method='RS', factors={'+EMOM':1.}, n_top=1),
                regime=None,
                )
    ########################
    # 2 regimes: riskon riskoff RS ; riskon=market_proxy price > sma, riskoff=market_proxy price <= sma
    # algo = Algo (context, [s for s in strategies if s.ID == 'roo_1'],
    #              allocation_model=AllocationModel(context, mode='REGIME_EW'),
    #              regime=Regime( transitions={'1' : ('riskon', ['roo_1_p1']),
    #                                          '0' : ('riskoff', ['roo_1_p2']),
    #                                   }
    #                            )
    #             )
    ########################
    # 3 regimes : 'bull', 'bear', 'neutral'
    # strategy_IDs = ['rs_2', 'eaa_1']
    # algo = Algo (context, strategies = [s for s in strategies if s.ID in strategy_IDs],
    #              allocation_model=AllocationModel(context, mode='REGIME_EW', weights=None, formula=None),
    #              regime=Regime(
    #                                   transitions={'0' : ('neutral', ['eaa_1']),
    #                                   '1' : ('bull', ['rs_2_p1']),
    #                                   '-1' : ('bear', ['rs_2_p2', 'eaa_1'])
    #                                          }
    #                                  )
    #             )
    ############################
    # AAA - Adaptive Asset Allocation
    # http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2359011

    # algo = Algo (context, strategies = [s for s in strategies if s.ID == 'aaa_1']
    #              allocation_model=AllocationModel(context, mode='EW'),
    #             )
    ############################
    # PAA - Protective Asset Allocation
    # http://indexswingtrader.blogspot.co.za/2016/04/introducing-protective-asset-allocation.html
    # algo = Algo (context, strategies = [s for s in strategies if s.ID == 'paa_1'],
    #              allocation_model=AllocationModel(context, mode='EW'),
    #             )
    ############################
    # BRS - Bond Rotation Strategy
    # https://logical-invest.com/portfolio-items/bond-rotation-sleep-well/
    # https://www.quantopian.com/posts/the-logical-invest-enhanced-bond-rotation-strategy

    # Algo-specific parameters
    # context.calculate_SR = True
    # context.SR_lookback = 73
    # context.SD_factor = 2
    # algo = Algo (context, strategies = [s for s in strategies if s.ID == 'brs_1'],
    #              allocation_model=AllocationModel(context, mode='EW'),
    #             )
    ###############################

    return algo


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# dummy logger
class Log():
    pass

    def info(self, s):
        print('{} INFO : {}'.format(get_datetime().tz_convert('US/Eastern'), s))
        pass

    def debug(self, s):
        print('{} DEBUG : {}'.format(get_datetime().tz_convert('US/Eastern'), s))
        pass

    def warn(self, s):
        print('{} WARNING : {}'.format(get_datetime().tz_convert('US/Eastern'), s))
        pass

    def error(self, s):
        print('{} ERROR : {}'.format(get_datetime().tz_convert('US/Eastern'), s))
        pass


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
############################################
# HELPER FUNCTIONS
##################
# NOTE: as pandas panel has been deprecated, need to fix this!!
# THE ALGORITHM PARAMETERS ARE DEFINED IN THIS SECTION:

# ENVIRONMENT can be set for 'ZIPLINE', 'RESEARCH' or 'IDE'
ENVIRONMENT = 'ZIPLINE'

# the following 3 lines must be commented out for use with RESEARCH or IDE
if ENVIRONMENT == 'ZIPLINE' and ENVIRONMENT != 'IDE':
    from zipline.api import symbol, symbols


#     from zipline.utils.factory import load_bars_from_yahoo, load_from_yahoo
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# this routine will not used for ENVIRONMENT == 'IDE'

# def get_data(ENVIRONMENT, tickers, start, end, benchmark, risk_free, cash_proxy):
#     if ENVIRONMENT == 'ZIPLINE':
#         benchmark_symbol = benchmark
#         cash_proxy_symbol = cash_proxy
#         risk_free_symbol = risk_free
#     elif ENVIRONMENT == 'RESEARCH':
#         if benchmark is None:
#             benchmark_symbol = None
#         else:
#             benchmark_symbol = symbols(benchmark)
#         if cash_proxy is None:
#             cash_proxy_symbol = None
#         else:
#             cash_proxy_symbol = symbols(cash_proxy)
#         if risk_free is None:
#             risk_free_symbol = None
#         else:
#             risk_free_symbol = symbols(risk_free)
#
#     # data is a Panel of DataFrames, one for each security
#     if ENVIRONMENT == 'ZIPLINE':
#         stocks = list(set(tickers + [benchmark_symbol, cash_proxy_symbol, risk_free_symbol]))
#         stocks = [s for s in stocks if s != None]
#         #         data = load_bars_from_yahoo(
#         #             stocks,
#         #             start = start,
#         #             end = end,
#         #             adjusted=False).transpose(2,1,0)
#
#         # User pandas_reader.data.DataReader to load the desired data. As simple as that.
#         d = web.DataReader(stocks, "yahoo", start, end)
#         data = pd.DataFrame(columns=['high', 'low', 'price', 'volume', 'open'], index=d.index)
#         data.high = d.High.copy()
#         data.low = d.Low.copy()
#         data.price = d['Adj Close'].copy()  # use this for comparing to Quantopian 'get_pricing'
#         data.volume = d.Volume.copy()
#         data.open = d.Open.copy()
#
#     elif ENVIRONMENT == 'RESEARCH':
#         stocks = set([symbols(t) for t in tickers] + [benchmark_symbol, cash_proxy_symbol, risk_free_symbol])
#         stocks = [s for s in stocks if s != None]
#         data = get_pricing(
#             stocks,
#             start_date=start,
#             end_date=end,
#             frequency='daily'
#         )
#
#         # repair unusable data
#     # BE CAREFUL!! dropna doesn't change the Panel's Major Index, so NA may still remain!
#     # safer to use ffill
#
#     #     for security in data.transpose(2,1,0):
#     #         data.transpose(2,1,0)[security] = data.transpose(2,1,0)[security].ffill()
#
#     # for
#
#     if benchmark is None:
#         stocks = []
#     else:
#         stocks = [benchmark_symbol]
#
#     if ENVIRONMENT == 'ZIPLINE':
#         stocks = stocks + [cash_proxy_symbol]
#         other_data = load_bars_from_yahoo(
#             stocks=stocks,
#             start=start,
#             end=end,
#             adjusted=False)  # use this for comparing to Quantopian 'get_pricing'
#         other_data.transpose(2, 1, 0).price = other_data.transpose(2, 1,
#                                                                    0).close  # use this for comparing to Quantopian 'get_pricing'
#     elif ENVIRONMENT == 'RESEARCH':
#         other_data = get_pricing(
#             stocks + [cash_proxy_symbol],
#             fields='price',
#             start_date=data.major_axis[0],
#             end_date=data.major_axis[-1],
#             frequency='daily',
#         )
#
#     other_data = other_data.ffill()
#
#     if benchmark is not None:
#         # need to add benchmark (eg SPY) and cash proxy to data panel
#         benchmark = other_data[benchmark_symbol]
#         benchmark_rets = benchmark.pct_change().dropna()
#
#         benchmark2 = other_data[cash_proxy_symbol]
#         benchmark2_rets = benchmark2.pct_change().dropna()
#
#     # make sure we have all the data we need
#     inception_dates = pd.DataFrame([data.transpose(2, 1, 0)[security].dropna().index[0].date() \
#                                     for security in data.transpose(2, 1, 0)], \
#                                    index=data.transpose(2, 1, 0).items, columns=['inception'])
#     if benchmark is not None:
#         inception_dates.loc['benchmark'] = benchmark.index[0].date()
#         inception_dates.loc['benchmark2'] = benchmark2.index[0].date()
#     print(inception_dates)
#
#     # check that the end dates coincide
#     end_dates = pd.DataFrame([data.transpose(2, 1, 0)[security].dropna().index[-1].date() \
#                               for security in data.transpose(2, 1, 0)], \
#                              index=data.transpose(2, 1, 0).items, columns=['end_date'])
#     if benchmark is not None:
#         end_dates.loc['benchmark'] = benchmark.index[-1].date()
#         end_dates.loc['benchmark2'] = benchmark2.index[-1].date()
#     print(end_dates)
#
#     # this will ensure that the strat and end dates are aligned
#     data = data[:, inception_dates.values.max(): end_dates.values.min(), :]
#     if benchmark is not None:
#         benchmark_rets = benchmark_rets[inception_dates.values.max(): end_dates.values.min()]
#         benchmark2_rets = benchmark2_rets[inception_dates.values.max(): end_dates.values.min()]
#
#     print('\n\nBACKTEST DATA IS FROM {} UNTIL {} \n*************************************************'
#           .format(inception_dates.values.max(), end_dates.values.min()))
#
#     # DATA FROM ZIPLINE LOAD_YAHOO_BARS DIFFERS FROM RESEARCH ENVIRONMENT!
#     data.items = ['open_price', 'high', 'low', 'close_price', 'volume', 'price']
#
#     print('\n\n{}'.format(data))
#
#     return data


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# symbol_set =['SPY', 'QQQ', 'IWM', 'VGK', 'EWJ', 'EEM','IYR', 'GSG', 'GLD', 'LQD', 'TLT', 'HYG','IEF', 'TLT','SHY']
# symbol_set = ['MDY', 'EFA','VNQ', 'RWX','GLD', 'AGG','EDV', 'EMB', 'TLT', 'SPY', 'SHY']
# tickers = list(set(symbol_set))

# # # data is a Panel of DataFrames, one for each security
# # data = get_pricing(
# #     tickers,
# #     start_date='2009-12-01',
# #     end_date = '2016-11-1',
# #     frequency='daily'
# # )

# # Define which online source one should use
# data_source = 'yahoo'

# # We would like all available data from 01/01/2000 until today.
# start_date = '2009-12-01'
# end_date = datetime.today().strftime('%Y-%m-%d')

# # User pandas_reader.data.DataReader to load the desired data. As simple as that.
# panel_data = web.DataReader(tickers, data_source, start_date, end_date)
# data = panel_data.sort_index(ascending=True)

# inception_dates = pd.DataFrame([data[ticker].first_valid_index() for ticker in data.columns],
#                                index=data.keys(), columns=['inception'])

# print (inception_dates)

# data = data.ffill()
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def initialize(context):
    # this routine should not require changes

    print('PLATFORM  ', get_environment('platform'))

    context.transforms = []
    context.algo_rules = []
    context.max_lookback = 64  # minimum value for max_lookback
    context.outstanding = {}  # orders which span multiple days

    context.raw_data = {}

    context.trading_day_no = 0

    #############################################################
    set_global_parameters(context)
    log.info('GLOBAL PARAMETERS CONFIGURED')
    #############################################################
    context.strategy_parameters = set_strategy_parameters(context)
    # strategy_params = [context.strategy_parameters[p] for p in context.strategy_parameters]
    log.info('STRATEGY PARAMETERS CONFIGURED')
    #############################################################
    # configure strategies
    Configurator(context, strategies=context.strategy_parameters)
    log.info('STRATEGIES CONFIGURED')
    #############################################################
    strategies = [s.strategy for s in context.strategy_parameters]
    algo = set_algo_parameters(context, strategies)
    #############################################################

    print('SET DAILY FUNCTIONS')

    # daily functions to handle GTC orders
    # note: GTC_LIMIT=10 (default) set as global
    schedule_function(algo.check_for_unfilled_orders, date_rules.every_day(), time_rules.market_close())
    schedule_function(algo.fill_outstanding_orders, date_rules.every_day(), time_rules.market_open())

    if context.show_positions:
        schedule_function(algo.show_positions, date_rules.month_start(days_offset=0), time_rules.market_open())

    if context.show_records:
        # show records every day
        # edit the show_records function to include records required
        schedule_function(algo.show_records, date_rules.every_day(), time_rules.market_close())

    if context.rebalance_period == 'A':
        schedule_function(algo.check_signal_trigger, date_rules.every_day(), time_rules.market_open())

    else:
        periods = {'D': date_rules.every_day(),
                   'WS': date_rules.week_start(days_offset=context.days_offset),
                   'WE': date_rules.week_end(days_offset=context.days_offset),
                   'MS': date_rules.month_start(days_offset=context.days_offset),
                   'ME': date_rules.month_end(days_offset=context.days_offset)}

        period = periods[context.rebalance_period]

        if context.on_open:
            time_rule = time_rules.market_open(hours=context.hours, minutes=context.minutes)
        else:
            time_rule = time_rules.market_close(hours=context.hours, minutes=context.minutes)

        schedule_function(algo.rebalance, period, time_rule)

    log.info('REBALANCE INTERVAL = ' + str(period))

    log.info('INITIALIZATION DONE!')


#########################################################################################################
if __name__ == "__main__":
    log = Log()

    start = datetime(2015, 1, 1, 0, 0, 0, 0, pytz.utc)
    #     end = datetime(2014, 1, 10, 0, 0, 0, 0, pytz.utc)
    #     end = datetime.today().replace(tzinfo=timezone.utc) - timedelta(1)
    end = datetime(2019, 3, 1, 0, 0, 0, 0, pytz.utc)
    capital_base = 100000

    result = run_algorithm(start=start, end=end, initialize=initialize, \
                           capital_base=capital_base, \
                           before_trading_start=before_trading_start,
                           bundle='etfs_bundle')

    print(result[:3])
"""
MSMP v6.00 Major Upgrade

- single RS momentum portfolio with downside protection
- RAA - Robust Asset Allocation (4 portfolios)
- minimumn correlation strategy
- downside protection strategy based on Alpha Architect DPM Rule: 50% TMOM, 50% MA
- RS with downside protection, single portfolio, EtfReplay-like ranking formula
- RS with 2 portfolios based on EtfReplay ranking model
- EAA - Elastic Asset Allocation
- multiple strategies, equally weighted
- 2 regimes: riskon riskoff RS
- Adaptive Asset Allocation
- PAA - Protective Aseet Allocation
- BRS - Bond Rotation Strategy

"""

# IMPORTS
#

import datetime as dt
import pytz

import numpy as np
import pandas as pd
import math
import talib
import re
from collections import OrderedDict
from cvxopt import matrix, solvers, spdiag
import scipy

########################################
GTC_LIMIT = 10
VALID_PORTFOLIO_ALLOCATION_MODES = ['EW', 'FIXED', 'PROPORTIONAL', 'MIN_VARIANCE', 'MAX_SHARPE',
                                    'BY_FORMULA', 'RISK_PARITY', 'VOLATILITY_WEIGHTED', 'RISK_TARGET',
                                    'MIN_CORRELATION']
VALID_STRATEGY_ALLOCATION_MODES = ['EW', 'FIXED', 'MIN_VARIANCE', 'MAX_SHARPE', 'BRUTE_FORCE_SHARPE',
                                   'BY_FORMULA', 'RISK_PARITY', 'VOLATILITY_WEIGHTED', 'RISK_TARGET', 'MIN_CORRELATION']
VALID_PORTFOLIO_ALLOCATION_FORMULAS = [None]
VALID_SECURITY_SCORING_METHODS = [None, 'RS', 'EAA']
VALID_PORTFOLIO_SCORING_METHODS = [None, 'RS']
VALID_PROTECTION_MODES = [None, 'BY_RULE', 'RAA', 'BY_FORMULA']
VALID_PROTECTION_FORMULAS = [None, 'DPF']
VALID_ALGO_ALLOCATION_MODES = ['EW', 'FIXED', 'PROPORTIONAL', 'MIN_VARIANCE', 'MAX_SHARPE',
                               'BY_FORMULA', 'RISK_PARITY', 'VOLATILITY_WEIGHTED', 'RISK_TARGET', 'MIN_CORRELATION']
VALID_STRATEGY_ALLOCATION_FORMULAS = [None, 'PAA']
VALID_STRATEGY_ALLOCATION_RULES = [None]
NONE_NOT_ALLOWED = ['portfolios', 'portfolio_allocation_modes', 'cash_proxies', 'strategy_allocation_mode']
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from talib import BBANDS, DEMA, EMA, HT_TRENDLINE, KAMA, MA, MAMA, MAVP, MIDPOINT, MIDPRICE, SAR, \
    SAREXT, SMA, T3, TEMA, TRIMA, WMA, ADD, DIV, MAX, MAXINDEX, MIN, MININDEX, MINMAX, \
    MINMAXINDEX, MULT, SUB, SUM, BETA, CORREL, LINEARREG, LINEARREG_ANGLE, \
    LINEARREG_INTERCEPT, LINEARREG_SLOPE, STDDEV, TSF, VAR, ADX, ADXR, APO, AROON, \
    AROONOSC, BOP, CCI, CMO, DX, MACD, MACDEXT, MACDFIX, MFI, MINUS_DI, MINUS_DM, MOM, \
    PLUS_DI, PLUS_DM, PPO, ROC, ROCP, ROCR, ROCR100, RSI, STOCH, STOCHF, STOCHRSI, \
    TRIX, ULTOSC, WILLR, ATR, NATR, TRANGE, ACOS, ASIN, ATAN, CEIL, COS, COSH, EXP, \
    FLOOR, LN, LOG10, SIN, SINH, SQRT, TAN, TANH, AD, ADOSC, OBV, AVGPRICE, MEDPRICE, \
    TYPPRICE, WCLPRICE, HT_DCPERIOD, HT_DCPHASE, HT_PHASOR, HT_SINE, HT_TRENDMODE

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
TALIB_FUNCTIONS = [BBANDS, DEMA, EMA, HT_TRENDLINE, KAMA, MA, MAMA, MAVP, MIDPOINT, MIDPRICE, SAR, \
                   SAREXT, SMA, T3, TEMA, TRIMA, WMA, ADD, DIV, MAX, MAXINDEX, MIN, MININDEX, MINMAX, \
                   MINMAXINDEX, MULT, SUB, SUM, BETA, CORREL, LINEARREG, LINEARREG_ANGLE, \
                   LINEARREG_INTERCEPT, LINEARREG_SLOPE, STDDEV, TSF, VAR, ADX, ADXR, APO, AROON, \
                   AROONOSC, BOP, CCI, CMO, DX, MACD, MACDEXT, MACDFIX, MFI, MINUS_DI, MINUS_DM, MOM, \
                   PLUS_DI, PLUS_DM, PPO, ROC, ROCP, ROCR, ROCR100, RSI, STOCH, STOCHF, STOCHRSI, TRIX, \
                   ULTOSC, WILLR, ATR, NATR, TRANGE, ACOS, ASIN, ATAN, CEIL, COS, COSH, EXP, FLOOR, LN, \
                   LOG10, SIN, SINH, SQRT, TAN, TANH, AD, ADOSC, OBV, AVGPRICE, MEDPRICE, TYPPRICE, \
                   WCLPRICE, HT_DCPERIOD, HT_DCPHASE, HT_PHASOR, HT_SINE, HT_TRENDMODE]


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class Algo():

    def __init__(self, context, strategies=[], allocation_model=None,
                 scoring_model=None, regime=None):

        if get_environment('platform') == 'zipline':
            context.day_no = 0

        self.ID = 'algo'
        self.type = 'Algorithm'

        self.strategies = strategies
        self.allocation_model = allocation_model
        self.regime = regime

        context.strategies = self.strategies

        context.max_lookback = self._compute_max_lookback(context)
        log.info('MAX_LOOKBACK = {}'.format(context.max_lookback))

        self.weights = [0. for s in self.strategies]
        context.strategy_weights = self.weights
        self.strategy_IDs = [s.ID for s in self.strategies]
        self.active = [s.ID for s in self.strategies] + [p.ID for s in self.strategies for p in s.portfolios]

        if self.allocation_model == None:
            raise ValueError('\n *** FATAL ERROR : ALGO ALLOCATION MODEL CANNOT BE NONE ***\n')

        context.prices = pd.Series()
        context.returns = pd.Series()
        context.log_returns = pd.Series()
        context.covariances = dict()
        context.sharpe_ratio = pd.Series()

        self.all_assets = self._set_all_assets()
        context.all_assets = self.all_assets[:]
        self.allocations = pd.Series(0, index=context.all_assets)
        self.previous_allocations = pd.Series(0, index=context.all_assets)
        context.scoring_model = scoring_model
        self.score = 0.

        context.data = Data(self.all_assets)
        # context.algo_data = context.data

        set_symbol_lookup_date('2016-01-01')

        self._instantiate_rules(context)

        context.securities = []  # placeholder securities in portfolio

        if get_environment('platform') == 'zipline':
            context.count = context.max_lookback
        else:
            context.count = 0

        self.rebalance_count = 1  # default rebalance interval = 1
        self.first_time = True

        return

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # looks for any 'lookback' kwargs
    def _compute_max_lookback(self, context):

        kwargs_list = self._get_all_kwargs(context)
        for kwargs in kwargs_list:
            if 'lookback' in kwargs:
                lookback = kwargs['lookback']
                try:
                    period = kwargs['period']
                except:
                    period = 'D'
                # add additional days to cater for 'sip_period'
                if period == 'D':
                    lookback_days = 5 + lookback
                elif period == 'W':
                    lookback_days = 6 + lookback * 5
                elif period == 'M':
                    lookback_days = 25 + lookback * 25
                else:
                    raise RuntimeError('UNKNOWN LOOKBACK PERIOD TYPE {} for strategy {}'.format(period, self.ID))

                context.max_lookback = max(context.max_lookback, lookback_days)

        return context.max_lookback

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_all_kwargs(self, context):
        # creates a list of all kwargs containing 'lookback' labels
        kwargs_list = self._get_portfolio_and_strategy_kwargs(context)
        kwargs_list = kwargs_list + self._get_transform_kwargs(context)
        return kwargs_list

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_portfolio_and_strategy_kwargs(self, context):
        kwargs_list = []
        for strategy in context.strategies:
            kwargs_list = kwargs_list + [strategy.allocation_model.kwargs]
            for pfolio in strategy.portfolios:
                kwargs_list = kwargs_list + [pfolio.allocation_model.kwargs]
        non_trivial_kwargs_list = [kwargs for kwargs in kwargs_list if kwargs not in [None, [], {}, [{}]]]
        return non_trivial_kwargs_list

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_transform_kwargs(self, context):
        kwargs_list = []
        for transform in context.transforms:
            if transform.kwargs not in [None, [], {}, [{}]]:
                kwargs_list = kwargs_list + [transform.kwargs]

        return kwargs_list

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _instantiate_rules(self, context):
        context.rules = {}
        for r in context.algo_rules:
            context.rules[r.name] = r
            # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _set_all_assets(self):
        all_assets = [s.all_assets for s in self.strategies]
        self.all_assets = list(set([i for sublist in all_assets for i in sublist]))
        return self.all_assets

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _allocate_assets(self, context):
        log.debug('STRATEGY WEIGHTS = {}\n'.format(self.weights))
        for i, s in enumerate(self.strategies):
            self.allocations = self.allocations.add(self.weights[i] * s.allocations,
                                                    fill_value=0)
        if self.allocations.sum() == 0:
            # not enough price data yet
            return self.allocations

        # if 1. - sum(self.allocations) > 1.e-15 :
        #     raise RuntimeError ('SUM OF ALLOCATIONS = {} - SHOULD ALWAYS BE 1'.format(sum(self.allocations)))

        return self.allocations

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def check_signal_trigger(self, context, data):

        holdings = context.portfolio.positions
        if self.first_time or context.rules['rebalance_rule'].apply_rule(context)[holdings].any():
            # force rebalance
            self.rebalance(context, data)
            self.first_time = False

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def rebalance(self, context, data):

        # make sure there's algo data
        if not isinstance(context.algo_data, dict):
            return
        elif not self.first_time:
            if self.rebalance_count != context.rebalance_interval:
                self.rebalance_count += 1
                return

        self.first_time = False

        self.rebalance_count = 1

        log.info('----------------------------------------------------------------------------')

        self.allocations = pd.Series(0., index=context.all_assets)
        self.elligible = pd.Index(self.strategy_IDs)

        # if self.scoring_model != None:
        #     self.scoring_model.caller = self
        #     context.symbols = self.strategy_IDs[:]
        #     self.score = self.scoring_model.compute_score (context)
        #     self.elligible =  self.scoring_model.apply_ntop ()

        self.allocation_model.caller = self
        if self.regime == None:
            self._get_strategy_and_portfolio_allocations(context)
        else:
            self._check_for_regime_change_and_set_active(context)

        self.weights = self.allocation_model.get_weights(context)
        self.allocations = self._allocate_assets(context)

        self._execute_orders(context, data)

        return self.allocations
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _get_strategy_and_portfolio_allocations(self, context):
        for s_no, s in enumerate(self.strategies):
            s.allocations = pd.Series(0., index=s.all_assets)
            for p_no, p in enumerate(s.portfolios):
                p.allocations = pd.Series(0., index=p.all_assets)
                p.allocations = p.reallocate(context)
            s.allocations = s.reallocate(context)
        return

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _check_for_regime_change_and_set_active(self, context):
        self.current_regime = self.regime.get_current(context)
        log.debug('REGIME : {} \n'.format(self.current_regime))
        if self.regime.detect_change(context):
            self.regime.set_new_regime()
            self.active = self.regime.get_active()
        else:
            log.info('REGIME UNCHANGED. JUST REBALANCE\n')
        return
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _execute_orders(self, context, data):

        for security in self.allocations.index:
            if context.portfolio.positions[security].amount > 0 and self.allocations[security] == 0:
                order_target_percent(security, 0)
            elif self.allocations[security] != 0:
                if get_open_orders(security):
                    continue

                current_value = context.portfolio.positions[security].amount * data.current(security, 'price')
                portfolio_value = context.portfolio.portfolio_value
                if portfolio_value == 0:  # before first purchases
                    portfolio_value = context.account.available_funds
                target_value = portfolio_value * self.allocations[security]

                if np.abs(target_value / current_value - 1) < context.threshold:
                    continue

                order_target_percent(security, self.allocations[security] * context.leverage)
                qty = int(
                    context.account.net_liquidation * self.allocations[security] / data.current(security, 'price'))
                log.debug('ORDERING {} : {}%  QTY = {}'.format(security.symbol,
                                                               self.allocations[security] * 100, qty))

        context.gtc_count = GTC_LIMIT

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def check_for_unfilled_orders(self, context, data):
        unfilled = {o.sid: o.amount - o.filled for oo in get_open_orders() for o in get_open_orders(oo)}
        context.outstanding = {u: unfilled[u] for u in unfilled if unfilled[u] != 0}
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def fill_outstanding_orders(self, context, data):
        if context.outstanding == {}:
            context.show_positions = False
            return
        elif context.gtc_count > 0:
            for s in context.outstanding:
                order(s, context.outstanding[s])
                log.debug('ORDER {} OUTSTANDING {} SHARES'.format(context.outstanding[s], s.symbol))

            context.gtc_count -= 1
        else:
            log.info('GTC_COUNT EXPIRED')
            # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def show_records(self, context, data):
        record('LEVERAGE', context.account.leverage)
        # record('CONTEXT_LEVERAGE', context.leverage)
        # record('PV', context.account.total_positions_value)
        # record('PV1',context.portfolio.positions_value)
        # record('TOTAL', context.portfolio.portfolio_value)
        # record('CASH', context.portfolio.cash)
        # for s in context.strategies:
        #     # record(s.ID + '_prices', s.prices.iloc[-1])
        #     for p in s.portfolios:
        #         record(p.ID + '_prices', p..ilocprices[-1])

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def show_positions(self, context, data):

        if context.portfolio.positions == {}:
            return

        log.info('\nPOSITIONS\n')
        for asset in self.all_assets:
            if context.portfolio.positions[asset].amount > 0:
                log.info(
                    '{0} : QTY = {1}, COST BASIS {2:3.2f}, CASH = {3:7.2f}, POSITIONS VALUE = {4:7.2f}, TOTAL = {5:7.2f}'
                    .format(asset.symbol, context.portfolio.positions[asset].amount,
                            context.portfolio.positions[asset].cost_basis,
                            context.portfolio.cash,
                            context.portfolio.positions[asset].amount * data.current(asset, 'price'),
                            context.portfolio.portfolio_value))
            # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


class Strategy():

    def __init__(self, context, ID='', portfolios=[], allocation_model=None,
                 scoring_model=None):

        self.ID = ID
        self.type = 'Strategy'
        self.portfolios = portfolios
        self.portfolio_IDs = [p.ID for p in self.portfolios]
        self.weights = [0. for p in portfolios]

        self.prices = pd.Series()
        self.returns = pd.Series()
        self.covariances = dict()
        self.sharpe_ratio = pd.Series()

        if allocation_model == None:
            self.allocation_model = AllocationModel(context, mode='EW')
        else:
            self.allocation_model = allocation_model
        self.scoring_model = scoring_model
        self.score = 0.

        self._set_all_assets()
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _set_all_assets(self):
        all_assets = [p.all_assets for p in self.portfolios]
        self.all_assets = set([i for sublist in all_assets for i in sublist])
        return self.all_assets
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def allocate_assets(self, context):
        self.allocations = pd.Series(0., index=self.all_assets)
        log.debug('STRATEGY {} PORTFOLIO WEIGHTS = {}\n'.format(self.ID, [round(w, 2) for w in self.weights]))
        for i, p in enumerate(self.portfolios):
            self.allocations = self.allocations.add(self.weights[i] * p.allocations,
                                                    fill_value=0)
        log.debug('SECURITY ALLOCATIONS for {} \n{}\n'.format(self.ID, self.allocations.round(2)))
        return self.allocations
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def reallocate(self, context):
        self.elligible = pd.Index(self.portfolio_IDs)

        if self.scoring_model != None:
            self.scoring_model.caller = self
            context.symbols = self.portfolio_IDs[:]
            self.score = self.scoring_model.compute_score(context)
            self.elligible = self.scoring_model.apply_ntop()

        self.allocation_model.caller = self
        self.weights = self.allocation_model.get_weights(context)
        self.allocations = self.allocate_assets(context)
        self.holdings = (self.allocations * context.portfolio.portfolio_value).divide(
            context.algo_data['price'][self.all_assets]).round(0)
        return self.allocations
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


class Portfolio():

    def __init__(self, context, ID='',
                 securities=[], allocation_model=None,
                 scoring_model=None,
                 downside_protection_model=None,
                 cash_proxy=None, allow_shorts=False):

        self.ID = ID
        self.type = 'Portfolio'
        self.securities = securities
        self.weights = [0. for s in securities]
        self.allocation_model = allocation_model
        self.scoring_model = scoring_model
        self.score = 0.
        self.downside_protection_model = downside_protection_model
        if cash_proxy == None:
            log.info('NO CASH_PROXY SPECIFIED FOR PORTFOLIO {}'.format(self.ID))
            raise ValueError('INITIALIZATION ERROR')
        self.cash_proxy = cash_proxy

        self.prices = pd.Series()
        self.returns = pd.Series()
        self.covariances = dict()
        self.sharpe_ratios = pd.Series()

        for s in [context.market_proxy, self.cash_proxy, context.risk_free]:
            if s in self.securities:
                log.warn('{} is included in the portfolio'.format(s.symbol))

        self.all_assets = list(set(self.securities + [context.market_proxy, self.cash_proxy, context.risk_free]))

        self.allocations = pd.Series([0.0] * len(self.all_assets), index=self.all_assets)

        return

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def reallocate(self, context):

        self.allocations = pd.Series(0., index=self.all_assets)
        self.elligible = pd.Index(self.securities)

        if self.scoring_model != None:
            self.scoring_model.caller = self
            context.symbols = self.securities[:]
            self.score = self.scoring_model.compute_score(context)
            self.elligible = self.scoring_model.apply_ntop()

        self.allocation_model.caller = self
        self.weights = self.allocation_model.get_weights(context)
        self.allocations[self.elligible] = self.weights

        log.debug('ALLOCATIONS FOR {} : {}\n'.format(self.ID,
                                                     [(self.allocations.index[i].symbol, round(v, 2))
                                                      for i, v in enumerate(self.allocations)
                                                      if v > 0]))

        if self.downside_protection_model != None:
            self.downside_protection_model.caller = self
            self.allocations = self.downside_protection_model.apply_protection(context,
                                                                               self.allocations,
                                                                               self.cash_proxy,
                                                                               [self.securities, self.score])
            log.debug('AFTER DOWNSIDE PROTECTION {} : {}\n'.format(self.ID,
                                                                   [(self.allocations.index[i].symbol, round(v, 2))
                                                                    for i, v in enumerate(self.allocations)
                                                                    if v > 0]))

        self.holdings = (self.allocations * context.portfolio.portfolio_value).divide(
            context.algo_data['price'][self.all_assets]).round(0)

        return self.allocations
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


class Regime():

    def __init__(self, transitions):
        """Initialize Regime object. Set init state and transition table."""
        self.transitions = transitions
        # set current != new to always detect change on first reallocation
        self.current_regime = 0
        self.new_regime = 1

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def detect_change(self, context):
        self.new_regime = self.get_current(context)
        return [False if self.current_regime == self.new_regime else True][0]
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def get_current(self, context):
        for k in self.transitions.keys():
            rule_name = self.transitions[k][0]
            rule = context.rules[rule_name]
            if rule.apply_rule(context):
                return k
                # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def set_new_regime(self):
        self.current_regime = self.new_regime
        record('REGIME', self.current_regime)
        return self.current_regime
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def get_active(self):
        return self.transitions[self.current_regime][1]
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


class Data():

    def __init__(self, assets):
        self.all_assets = assets
        # self.fallbacks = {'EDV' : symbol('TLT')}
        return

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def update(self, context, data):

        ''' generates context.raw_data (dictionary of context.max_lookback values)  and context.algo_data (dictioanary current values) for  'high', 'open', 'low', 'close', 'volume', 'price' and all transforms '''

        # dataframe for each of 'high', 'open', 'low', 'close', 'volume', 'price'
        context.raw_data = self.get_raw_data(context, data)

        # log.info ('\n{} GENERATING ALGO_DATA...'.format(get_datetime().date()))

        # add a dataframe for each transform
        context.raw_data = self.generate_frame_for_each_transform(context, data)

        # only need the current value for each security (Series)
        context.algo_data = self.current_algo_data(context, data)

        return context.algo_data
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def get_tradeable_assets(self, data):
        tradeable_assets = [asset for asset in self.all_assets if data.can_trade(asset)]
        if len(self.all_assets) > len(tradeable_assets):
            non_tradeable = [s.symbol for s in self.all_assets if data.can_trade(s) == False]
            log.error('*** FATAL ERROR : MISSING DATA for securities {}'.format(non_tradeable))
            raise ValueError('FATAL ERROR: SEE LOG FOR MISSING DATA')
        return tradeable_assets

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def get_raw_data(self, context, data):

        context.raw_data = dict()

        tradeable_assets = self.get_tradeable_assets(data)

        for item in ['high', 'open', 'low', 'close', 'volume', 'price']:
            try:
                context.raw_data[item] = data.history(tradeable_assets, item, context.max_lookback, '1d')
            except:
                log.warn('FATAL ERROR: UNABLE TO LOAD HISTORY DATA FOR {}'.format(item))
                # force exit
                raise ValueError(' *** FATAL ERROR : INSUFFICIENT DATA - SEE LOG *** ')

            if np.isnan(context.raw_data[item].values).any():
                # log.warn ('\n WARNING : THERE ARE NaNs IN THE DATA FOR {} \n FILL BACKWARDS.......'
                #           .format([k.symbol for k in context.raw_data[item].keys() if
                #                    np.isnan(context.raw_data[item][k][0])]))
                context.raw_data[item] = context.raw_data[item].bfill()

        return context.raw_data

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def generate_frame_for_each_transform(self, context, data):

        for transform in context.transforms:
            # result = apply_transform(context, transform)
            result = transform.apply_transform(context)
            outputs = transform.outputs
            if type(result) == pd.Panel:
                context.raw_data.update(dict([(o, result[o]) for o in outputs]))
            elif type(result) == pd.DataFrame:
                context.raw_data[outputs[0]] = result
            else:
                log.error('\n INVALID TRANSFORM RESULT\n')

        return context.raw_data
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def current_algo_data(self, context, data):

        context.algo_data = dict()
        for k in [key for key in context.raw_data.keys()
                  if type(context.raw_data[key]) == pd.DataFrame]:
            context.algo_data[k] = context.raw_data[k].ix[-1]
            if np.isnan(context.algo_data[k].values).any():
                security = [s.symbol for s in context.raw_data[k].ix[-1].index
                            if np.isnan(context.raw_data[k][s].ix[-1])][0]
                log.warn('*** WARNING: FOR ITEM {} THERE IS A NAN IN THE DATA FOR {}'.format(k, security))
        return context.algo_data
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    # prices are NOMINAL prices used for individual portfolio/strategy variance/cov calculations
    def update_portfolio_and_strategy_metrics(self, context, data):
        for s_no, s in enumerate(context.strategies):
            self._update_strategy_metrics(context, data, s, s_no)
            self._update_portfolio_metrics(context, data, s)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _update_strategy_metrics(self, context, data, s, s_no):
        ''' calculate and store current price of strategies used by algo '''
        strategy_price = s.holdings.multiply(context.algo_data['price'][s.all_assets]).sum()
        s.prices[get_datetime()] = strategy_price
        s.sharpe_ratio[get_datetime()] = self._calculate_sharpe_ratio(context, data, s)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _update_portfolio_metrics(self, context, data, s):
        for p_no, p in enumerate(s.portfolios):
            portfolio_price = p.holdings.multiply(context.algo_data['price'][p.all_assets]).sum()
            p.prices[get_datetime()] = portfolio_price
            p.sharpe_ratios[get_datetime()] = self._calculate_sharpe_ratio(context, data, p)
            # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _calculate_sharpe_ratio(self, context, data, s_or_p):
        if len(s_or_p.prices) <= context.SR_lookback:
            # not enought data yet
            return 0
        rets = s_or_p.prices.pct_change()[-context.SR_lookback:]
        # s_or_p_rets = (rets * s_or_p.allocation_model.weights).sum(axis=1)[-context.SR_lookback:]
        risk_free_rets = data.history(context.risk_free, 'price', context.SR_lookback, '1d').pct_change()
        excess_returns = rets[1:].values - risk_free_rets[1:].values
        return excess_returns.mean() / rets.std()
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


class ScoringModel():

    def __init__(self, context, factors=None, method=None, n_top=1):
        self.factors = factors
        self.method = method
        if self.factors == None:
            raise ValueError('Unable to score model with no factors')
        # if self.method == None :
        #     raise ValueError ('Unable to score model with no method')
        self.n_top = n_top
        self.score = 0
        self.methods = {'RS': self._relative_strength,
                        'EAA': self._eaa
                        }

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def compute_score(self, context):
        self.symbols = context.symbols
        self.score = self.methods[self.method](context)
        # log.debug ('\nSCORE\n\n{}\n'.format(self.score))
        return self.score

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _relative_strength(self, context):
        self.score = 0.
        for name in self.factors.keys():

            if np.isnan(context.algo_data[name[1:]][self.symbols]).any():
                if isinstance(self.symbols[0], str):
                    sym = [(self.symbols[s], v)
                           for s, v in enumerate(context.algo_data[name[1:]][self.symbols]) if np.isnan(v)][0][0]
                else:
                    sym = [(self.symbols[s].symbol, v)
                           for s, v in enumerate(context.algo_data[name[1:]][self.symbols]) if np.isnan(v)][0][0]
                print('SCORING ERROR : FACTOR {} VALUE FOR {} IS nan'.format(name, sym))
                raise RuntimeError()

            if name[0] == '+':
                # log.debug('Values for factor {} :\n\{}\nRANKS : \n{}'.format(name[1:],
                #                                                              [(s.symbol, context.algo_data[name[1:]][s]) for s in self.securities],
                #                                                              [(s.symbol, context.algo_data[name[1:]][self.securities].rank(ascending=False)[s])
                #                                                               for s in self.securities]))

                try:
                    # highest value gets highest rank / score
                    self.score = self.score + context.algo_data[name[1:]][self.symbols].rank(ascending=True) \
                                 * self.factors[name]
                except:
                    raise RuntimeError(
                        '\n *** FATAL ERROR : UNABLE TO SCORE FACTOR {}. CHECK TRANSFORM & FACTOR DEFINITIONS\n'
                        .format(name[1:]))

            elif name[0] == '-':
                # log.debug('Values for factor {} :\n\{}\nRANKS : \n{}'.format(name[1:],
                #                                                              [(s.symbol, context.algo_data[name[1:]][s]) for s in self.securities],
                #                                                              [(s.symbol, context.algo_data[name[1:]][self.securities].rank(ascending=True)[s])
                #                                                               for s in self.securities]))

                try:
                    # lowest value gets highest rank /score
                    self.score = self.score + context.algo_data[name[1:]][self.symbols].rank(ascending=False) \
                                 * self.factors[name]
                except:
                    raise RuntimeError('\n UNABLE TO SCORE FACTOR {}. CHECK TRANSFORM & FACTOR DEFINITIONS\n'
                                       .format(name[1:]))

        # log.debug('Scores for factor {} :\n\n{}'.format(name[1:],
        #                                                 [(s.symbol, self.score[s]) for s in self.securities]))

        return self.score
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _eaa(self, context):

        # only valid for securities, not portfolios or strategies (?)

        if self.caller.type != 'Portfolio':
            raise RuntimeError('SCORING MODEL EAA ONLY VALID FOR PORTFOLIO, NOT {}'.format(self.method))

        # prices = data.history(self.securities, 'price', 280, '1d')
        prices = context.raw_data['price'][self.symbols]

        monthly_prices = prices.resample('M').last()[self.symbols]
        monthly_returns = monthly_prices.pct_change().ix[-12:]

        # nominal return correlation to equi-weight portfolio
        N = len(self.symbols)
        equal_weighted_index = monthly_returns.mean(axis=1)
        C = pd.Series([0.0] * N, index=self.symbols)
        for s in C.index:
            C[s] = monthly_returns[s].corr(equal_weighted_index)

        R = context.algo_data['R'][self.symbols]
        V = monthly_returns.std()

        # Apply factor weights
        # wi ~ zi = ( ri^wR * (1-ci)^wC / vi^wV )^wS
        wR = self.factors['R']
        wC = self.factors['C']
        wV = self.factors['V']
        wS = self.factors['S']
        eps = self.factors['eps']

        # Generalized Momentum Score
        self.score = ((R ** wR) * ((1 - C) ** wC) / (V ** wV)) ** (wS + eps)

        return self.score

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def apply_ntop(self):

        N = len(self.symbols)
        if self.method == 'EAA':
            self.n_top = min(np.ceil(N ** 0.5) + 1, N / 2)
            elligible = self.score.sort_values().index[-self.n_top:]
        else:
            # best score gets lowest rank
            ranks = self.score.rank(ascending=False, method='dense')
            elligible = ranks[ranks <= self.n_top].index

        return elligible
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


class AllocationModel():

    def __init__(self, context, mode='EW', weights=None, rule=None, formula=None, kwargs={}):
        self.mode = mode
        self.formula = formula
        self.weights = weights
        self.rule = rule
        self.kwargs = kwargs

        self.modes = {'EW': self._equal_weight_allocation,
                      'FIXED': self._fixed_allocation,
                      'PROPORTIONAL': self._proportional_allocation,
                      'MIN_VARIANCE': self._min_variance_allocation,
                      'BRUTE_FORCE_SHARPE': self._brute_force_sharpe_allocation,
                      'MAX_SHARPE': self._max_sharpe_allocation,
                      'BY_FORMULA': self._allocation_by_formula,
                      'REGIME_EW': self.allocate_by_regime_EW,
                      'RISK_PARITY': self._risk_parity_allocation,
                      'VOLATILITY_WEIGHTED': self._volatility_weighted_allocation,
                      'RISK_TARGET': self._risk_targeted_allocation,
                      'MIN_CORRELATION': self._get_reduced_correlation_weights,
                      }

        if mode not in self.modes.keys():
            raise ValueError('UNKNOWN MODE "{}"'.format(mode))

        self.caller = None  # portfolio or strategy object calling the model

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def get_weights(self, context):
        self.prices = self._get_caller_prices(context)
        if self.mode not in ['EW', 'FIXED', 'PROPORTIONAL']:
            # all other modes need prices for at least 'lookback' period
            if self.kwargs is not None and 'lookback' in self.kwargs:
                # unable to allocate weights until more than 'lookback' prices
                if len(self.prices) <= self.kwargs['lookback']:
                    # default to 'EW' to be able to generate prices
                    self.caller_weights = [1. / len(self.caller.elligible) for i in self.caller.elligible]
                    return self.caller_weights
        if self.mode.startswith('REGIME') and self.caller.ID != 'algo':
            raise ValueError('ILLEGAL REGIME ALLOCATION : REGIME ALLOCATION MODEL ONLY ALLOWED AT ALGO LEVEL')
        return self.modes[self.mode](context)
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _get_caller_prices(self, context):
        if self.caller.type == 'Portfolio':
            prices = context.raw_data['price'][self.caller.elligible]
        elif self.caller.type == 'Strategy':
            # portfolio prices for portfolios in strategy
            prices = self._get_strategy_prices(context)

        elif self.caller.type == 'Algorithm':
            # strategy prices for strategies in algorithm
            prices = self._get_algo_prices(context)

        return prices

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_strategy_prices(self, context):
        prices_dict = OrderedDict({p.ID: p.prices for s in context.strategies for p in s.portfolios})
        index = context.strategies[0].portfolios[0].prices.index
        columns = [p.ID for s in context.strategies for p in s.portfolios]
        return pd.DataFrame(prices_dict, index=index, columns=columns)
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _get_algo_prices(self, context):
        prices_dict = OrderedDict({s.ID: s.prices for s in context.strategies})
        index = context.strategies[0].prices.index
        columns = [s.ID for s in context.strategies]
        return pd.DataFrame(prices_dict, index=index, columns=columns)
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _equal_weight_allocation(self, context):
        elligible = self.caller.elligible
        if len(elligible) > 0:
            self.caller.weights = [1. / len(elligible) for i in elligible]
        return self.caller.weights
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _fixed_allocation(self, context):
        # we are going to change these weights, so be careful to keep a copy!
        self.caller.weights = self.caller.allocation_model.weights[:]
        return self.caller.weights

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _proportional_allocation(self, context):
        elligible = self.caller.elligible
        score = self.caller.score
        self.caller.weights = score[elligible] / score[elligible].sum()
        return self.caller.weights
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _risk_parity_allocation(self, context):
        lookback = self.kwargs['lookback']
        prices = self.prices[-lookback:]
        ret_log = np.log(1. + prices.pct_change())[1:]
        hist_vol = ret_log.std(ddof=0)

        adj_vol = 1. / hist_vol

        self.caller.weights = adj_vol.div(adj_vol.sum(), axis=0)
        return self.caller.weights
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _volatility_weighted_allocation(self, context):

        elligible = self.caller.elligible
        lookback = self.kwargs['lookback']
        ret_log = np.log(1. + self.prices.pct_change())
        hist_vol = ret_log.rolling(window=lookback, center=False).std()[elligible]

        adj_vol = 1. / hist_vol

        self.caller.weights = adj_vol.div(adj_vol.sum())
        return self.caller.weights
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _risk_targeted_allocation(self, context):
        lookback = self.kwargs['lookback']
        target_risk = self.kwargs['target_risk']
        shorts = self.kwargs['shorts']
        prices = self.prices[self.caller.elligible][-lookback:]
        sigma_mat = self._compute_covariance_matrix(prices)
        mu_vec = self._compute_expected_returns(prices)
        risk_free = context.raw_data['price'][context.risk_free].pct_change()[-lookback:].mean()
        self.caller.weights = self._compute_target_risk_portfolio(mu_vec, sigma_mat,
                                                                  target_risk=target_risk,
                                                                  risk_free=risk_free,
                                                                  shorts=shorts)[0]
        return self.caller.weights
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _min_variance_allocation(self, context):
        lookback = self.kwargs['lookback']
        shorts = self.kwargs['shorts']
        prices = self.prices[self.caller.elligible][-lookback:]
        sigma_mat = self._compute_covariance_matrix(prices)
        mu_vec = self._compute_expected_returns(prices)
        self.caller.weights = self._compute_global_min_portfolio(mu_vec=mu_vec,
                                                                 sigma_mat=sigma_mat,
                                                                 shorts=shorts)[0]
        return self.caller.weights
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _max_sharpe_allocation(self, context):
        # calculate security weights for max sharpe portfolio
        elligible = self.caller.elligible
        lookback = self.kwargs['lookback']
        shorts = self.kwargs['shorts']
        prices = self.prices[elligible][-lookback:]
        sigma_mat = self._compute_covariance_matrix(prices)
        mu_vec = self._compute_expected_returns(prices)
        risk_free = context.raw_data['price'][context.risk_free].pct_change()[-lookback:].mean()
        self.caller.weights = self._compute_tangency_portfolio(mu_vec=mu_vec,
                                                               sigma_mat=sigma_mat,
                                                               risk_free=risk_free,
                                                               shorts=shorts)[0]
        return self.caller.weights

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # this only works at strategy level
    # it could feasibly work at algo level too
    def _brute_force_sharpe_allocation(self, context):
        if isinstance(self.caller, Strategy):
            portfolio_SRs = [p.sharpe_ratios[-1] for p in self.caller.portfolios]
            # select the portfolio(s) with the highest SR - could be more than 1
            self.caller.weights = [1. if s == np.max(portfolio_SRs) else 0 for s in portfolio_SRs]
            # in case there are more than 1, normalize
            return self.caller.weights / np.sum(self.caller.weights)
        else:
            raise RuntimeError('BRUTE_FORCE_SHARPE_ALLOCATION ONLY WORKS AT STRATEGY LEVEL')

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_reduced_correlation_weights(self, context):
        """
        Implementation of minimum correlation algorithm.
        ref: http://cssanalytics.com/doc/MCA%20Paper.pdf

        :Params:
            :returns <Pandas DataFrame>:Timeseries of asset returns
            :risk_adjusted <boolean>: If True, asset weights are scaled
                                      by their standard deviations
        """
        elligible = self.caller.elligible
        lookback = self.kwargs['lookback']
        risk_adjusted = self.kwargs['risk_adjusted']

        prices = self.prices[elligible][-lookback:]
        returns = prices.pct_change()[1:]

        correlations = returns.corr()
        adj_correlations = self._get_adjusted_cor_matrix(correlations)
        initial_weights = adj_correlations.T.mean()

        ranks = initial_weights.rank()
        ranks /= ranks.sum()

        weights = adj_correlations.dot(ranks)
        weights /= weights.sum()

        if risk_adjusted:
            weights = weights / returns.std()
            weights /= weights.sum()
        return weights

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_adjusted_cor_matrix(self, cor):
        values = cor.values.flatten()
        mu = np.mean(values)
        sigma = np.std(values)
        distribution = scipy.stats.norm(mu, sigma)
        return 1 - cor.apply(lambda x: distribution.cdf(x))

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _allocation_by_formula(self, context):
        # for Protective Asset Allocation (PAA), strategy assumed to have 2 portfolios
        if self.formula == 'PAA':
            if len(self.caller.elligible) != 2:
                raise ValueError('Protective Asset Allocation (PAA) Srategy has {} Portfolio; must have 2')
            else:
                self.caller.allocations = self._allocate_by_PAA_formula(context)
        return self.caller.allocations

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _allocate_by_PAA_formula(self, context):
        try:
            protection_factor = self.kwargs['protection_factor']
        except:
            raise RuntimeError(
                'MISSING STRATEGY ALLOCATION KWARG "protection_factor" FOR STRATEGY {}'.format(self.caller.ID))
        securities = self.caller.portfolios[0].securities
        N = len(securities)
        n = context.rules[self.rule].apply_rule(context)[securities].sum()
        dpf = (N - n) / (N - protection_factor * n / 4.)
        # log.debug ('For portfolio {}, n = {}, N = {}, dpf = {}'.format(self.caller.ID, n, N, dpf))
        record('DPF', dpf)
        self.caller.weights = [1. - dpf, dpf]
        return self.caller.weights

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def allocate_by_regime_EW(self, context):

        # log.debug('\nACTIVE : {} \n'.format(self.caller.active))

        if self.caller.type != 'Algorithm':
            raise RuntimeError('REGIME SWITCHING ONLY ALLOWED AT ALGO LEVEL')

        self._reset_strategy_and_portfolio_weights(context)

        for s in self.caller.strategies:
            s.allocations = pd.Series(0, index=s.all_assets)

            for p in s.portfolios:
                if s.ID in self.caller.active:
                    p_weight = 1. / len(s.portfolios)
                elif p.ID in self.caller.active:
                    p_weight = 1. / np.sum([1 if pfolio.ID in self.caller.active else 0 for pfolio in s.portfolios])
                elif s.ID not in self.caller.active and p.ID not in self.caller.active:
                    continue

                p.allocations = p.reallocate(context)
                s.allocations = s.allocations.add(p_weight * p.allocations, fill_value=0)

        active_strategies = set([s.ID for s in context.strategies
                                 for p in s.portfolios if s.ID in self.caller.active
                                 or p.ID in self.caller.active])
        self.caller.weights = [1. / len(active_strategies) if s.ID in active_strategies else 0 for s in
                               context.strategies]
        context.strategy_weights = self.caller.weights

        return self.caller.weights
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _reset_strategy_and_portfolio_weights(self, context):

        for s_no, s in enumerate(self.caller.strategies):
            self.caller.weights[s_no] = 0
            context.strategy_weights[s_no] = 0
            for p_no, p in enumerate(s.portfolios):
                s.weights[p_no] = 0
        return
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _get_no_of_active_portfolios(self):
        # Note : if strategy in active, all its portfolios are active
        number = 0
        for s in self.caller.strategies:
            if s.ID in self.caller.active:
                # all portfolios are active
                for p in s.portfolios:
                    number += 1
            for p in s.portfolios:
                if p.ID in self.caller.active:
                    number += 1

        return number

    # Portfolio Helper Functions

    # Functions:
    #    1. compute_efficient_portfolio        compute minimum variance portfolio
    #                                            subject to target return
    #    2. compute_global_min_portfolio       compute global minimum variance portfolio
    #    3. compute_tangency_portfolio         compute tangency portfolio
    #    4. compute_efficient_frontier         compute Markowitz bullet
    #    5. compute_portfolio_mu               compute portfolio expected return
    #    6. compute_portfolio_sigma            compute portfolio standard deviation
    #    7. compute_covariance_matrix          compute covariance matrix
    #    8. compute_expected_returns           compute expected returns vector

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _compute_covariance_matrix(self, prices):
        # calculates the cov matrix for the period defined by prices
        returns = np.log(1 + prices.pct_change())[1:]
        excess_returns_matrix = returns - returns.mean()
        return 1. / len(returns) * (excess_returns_matrix.T).dot(excess_returns_matrix)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _compute_expected_returns(self, prices):
        mu_vec = np.log(1 + prices.pct_change(1))[1:].mean()
        return mu_vec

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _compute_portfolio_mu(self, mu_vec, weights_vec):
        if len(mu_vec) != len(weights_vec):
            raise RuntimeError('mu_vec and weights_vec must have same length')
        return mu_vec.T.dot(weights_vec)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _compute_portfolio_sigma(self, sigma_mat, weights_vec):

        if len(sigma_mat) != len(sigma_mat.columns):
            raise RuntimeError('sigma_mat must be square\nlen(sigma_mat) = {}\nlen(sigma_mat.columns) ={}'.
                               format(len(sigma_mat), len(sigma_mat.columns)))
        return np.sqrt(weights_vec.T.dot(sigma_mat).dot(weights_vec))

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _compute_efficient_portfolio(self, mu_vec, sigma_mat, target_return, shorts=True):

        # compute minimum variance portfolio subject to target return
        #
        # inputs:
        # mu_vec                  N x 1 DataFrame expected returns
        #                         with index = asset names
        # sigma_mat               N x N DataFrame covariance matrix of returns
        #                         with index = columns = asset names
        # target_return           scalar, target expected return
        # shorts                  logical, allow shorts is TRUE
        #
        # output is portfolio object with the following elements
        #
        # mu_p                   portfolio expected return
        # sig_p                  portfolio standard deviation
        # weights                N x 1 DataFrame vector of portfolio weights
        #                        with index = asset names

        # check for valid inputs
        #

        if len(mu_vec) != len(sigma_mat):
            print("dimensions of mu_vec and sigma_mat do not match")
            raise
        if np.matrix([sigma_mat.ix[i][i] for i in range(len(sigma_mat))]).any() <= 0:
            print('Covariance matrix not positive definite')
            raise

        #
        # compute efficient portfolio
        #

        solvers.options['show_progress'] = False
        P = 2 * matrix(sigma_mat.values)
        q = matrix(0., (len(sigma_mat), 1))
        G = spdiag([-1. for i in range(len(sigma_mat))])
        A = matrix(1., (1, len(sigma_mat)))
        A = matrix([A, matrix(mu_vec.T.values).T], (2, len(sigma_mat)))
        b = matrix([1.0, target_return], (2, 1))

        if shorts == True:
            h = matrix(1., (len(sigma_mat), 1))

        else:
            h = matrix(0., (len(sigma_mat), 1))

        # weights_vec = pd.DataFrame(np.array(solvers.qp(P, q, G, h, A, b)['x']),\
        #                                     sigma_mat.columns)
        try:
            weights_vec = pd.Series(list(solvers.qp(P, q, G, h, A, b)['x']), index=sigma_mat.columns)
        except:
            log.info('W A R N I N G : unable to compute optimal weights; setting to equal weights')
            weights_vec = pd.Series(1. / len(sigma_mat), index=sigma_mat.columns)

            #
        # compute portfolio expected returns and variance
        #
        # print ('*** Debug ***\n_compute_efficient_portfolio:\nmu_vec:\n', self.mu_vec, '\nsigma_mat:\n',
        #        self.sigma_mat, '\nweights:\n', self.weights_vec )
        weights_vec.index = mu_vec.index
        mu_p = self._compute_portfolio_mu(mu_vec, weights_vec)
        sigma_p = self._compute_portfolio_sigma(sigma_mat, weights_vec)

        return weights_vec, mu_p, sigma_p
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _compute_global_min_portfolio(self, mu_vec, sigma_mat, shorts=True):

        solvers.options['show_progress'] = False
        P = 2 * matrix(sigma_mat.values)
        q = matrix(0., (len(sigma_mat), 1))
        G = spdiag([-1. for i in range(len(sigma_mat))])
        A = matrix(1., (1, len(sigma_mat)))
        b = matrix(1.0)

        if shorts == True:
            h = matrix(1., (len(sigma_mat), 1))
        else:
            h = matrix(0., (len(sigma_mat), 1))

        # print ('\nP\n\n{}\n\nq\n\n{}\n\nG\n\n{}\n\nh\n\n{}\n\nA\n\n{}\n\nb\n\n{}\n\n'.format(P,q,G,h,A,b))
        # weights_vec = pd.DataFrame(np.array(solvers.qp(P, q, G, h, A, b)['x']),\
        #                                     index=sigma_mat.columns)
        weights_vec = pd.Series(list(solvers.qp(P, q, G, h, A, b)['x']), index=sigma_mat.columns)

        #
        # compute portfolio expected returns and variance
        #
        # print ('*** Debug ***\n_Global Min Portfolio:\nmu_vec:\n', mu_vec, '\nsigma_mat:\n',
        #        sigma_mat, '\nweights:\n', weights_vec)

        mu_p = self._compute_portfolio_mu(mu_vec, weights_vec)
        sigma_p = self._compute_portfolio_sigma(sigma_mat, weights_vec)

        return weights_vec, mu_p, sigma_p

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _compute_efficient_frontier(self, mu_vec, sigma_mat, risk_free=0, points=100, shorts=True):

        efficient_frontier = pd.DataFrame(index=range(points), dtype=object, columns=['mu_p', 'sig_p', 'sr_p', 'wts_p'])

        gmin_wts, gmin_mu, gmin_sigma = self._compute_global_min_portfolio(mu_vec, sigma_mat, shorts=shorts)

        xmax = mu_vec.max()
        if shorts == True:
            xmax = 2 * mu_vec.max()
        for i, mu in enumerate(np.linspace(gmin_mu, xmax, points)):
            w_vec, portfolio_mu, portfolio_sigma = self._compute_efficient_portfolio(mu_vec, sigma_mat, mu,
                                                                                     shorts=shorts)
            efficient_frontier.ix[i]['mu_p'] = w_vec.dot(mu_vec)
            efficient_frontier.ix[i]['sig_p'] = np.sqrt(w_vec.T.dot(sigma_mat.dot(w_vec)))
            efficient_frontier.ix[i]['sr_p'] = (efficient_frontier.ix[i]['mu_p'] - risk_free) / \
                                               efficient_frontier.ix[i]['sig_p']
            efficient_frontier.ix[i]['wts_p'] = w_vec

        return efficient_frontier

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _compute_tangency_portfolio(self, mu_vec, sigma_mat, risk_free=0, shorts=True):

        efficient_frontier = self._compute_efficient_frontier(mu_vec, sigma_mat, risk_free, shorts=shorts)
        index = efficient_frontier.index[efficient_frontier['sr_p'] == efficient_frontier['sr_p'].max()]

        wts = efficient_frontier['wts_p'][index].values[0]
        mu_p = efficient_frontier['mu_p'][index].values[0]
        sigma_p = efficient_frontier['sig_p'][index].values[0]
        sharpe_p = efficient_frontier['sr_p'][index].values[0]

        return wts, mu_p, sigma_p, sharpe_p

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _compute_target_risk_portfolio(self, mu_vec, sigma_mat, target_risk, risk_free=0, shorts=True):

        efficient_frontier = self._compute_efficient_frontier(mu_vec, sigma_mat, risk_free, shorts=shorts)
        if efficient_frontier['sig_p'].max() <= target_risk:
            log.warn('TARGET_RISK {} > EFFICIENT FRONTIER MAXIMUM {}; SETTING IT TO MAXIMUM'.
                     format(target_risk, efficient_frontier['sig_p'].max()))
            index = len(efficient_frontier) - 1
        elif efficient_frontier['sig_p'].min() >= target_risk:
            log.warn('TARGET RISK {} < GLOBAL MINIMUM {}; SETTING IT TO GLOBAL MINIMUM'.
                     format(target_risk, efficient_frontier['sig_p'].max()))
            index = 1
        else:
            index = efficient_frontier.index[efficient_frontier['sig_p'] >= target_risk][0]

        wts = efficient_frontier['wts_p'][index]
        mu_p = efficient_frontier['mu_p'][index]
        sigma_p = efficient_frontier['sig_p'][index]
        sharpe_p = efficient_frontier['sr_p'][index]

        return wts, mu_p, sigma_p, sharpe_p


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class DownsideProtectionModel():

    def __init__(self, context, mode=None, rule=None, formula=None, *args):

        self.mode = mode
        self.rule = rule
        self.formula = formula
        self.args = args

        self.modes = {'BY_RULE': self._by_rule,
                      'RAA': self._apply_RAA,
                      'BY_FORMULA': self._by_formula
                      }

        self.caller = None  # portfolio or strategy object calling the model

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def apply_protection(self, context, allocations, cash_proxy=None, *args):

        # apply downside protection model to cash_proxy, if it fails, set cash_proxy to risk_free

        if context.allow_cash_proxy_replacement:
            if context.raw_data['price'][cash_proxy][-1] < context.algo_data['price'][-43:].mean():
                cash_proxy = context.risk_free

        new_allocations = self.modes[self.mode](context, allocations, cash_proxy, *args)

        return new_allocations

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _by_rule(self, context, allocations, cash_proxy, *args):
        try:
            triggers = context.rules[self.rule].apply_rule(context)[allocations.index]
        except:
            raise RuntimeError('UNABLE TO APPLY RULE {} FOR {}'.format(self.rule, self.caller.ID))

        new_allocations = pd.Series([0 if triggers[a] else allocations[a] for a in allocations.index],
                                    index=allocations.index)
        new_allocations[cash_proxy] = new_allocations[cash_proxy] + (1 - new_allocations.sum())

        return new_allocations

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _apply_RAA(self, context, allocations, cash_proxy, *args):
        excess_returns = context.algo_data['EMOM']

        tmp1 = [0.5 if excess_returns[asset] > 0 else 0. for asset in allocations.index]

        prices = context.algo_data['price']
        MA = context.algo_data['smma']

        tmp2 = [0.5 if prices[asset] > MA[asset] else 0. for asset in allocations.index]

        dpf = pd.Series([x + y for x, y in zip(tmp1, tmp2)], index=allocations.index)

        new_allocations = allocations * dpf
        new_allocations[cash_proxy] = new_allocations[cash_proxy] + (1 - np.sum(new_allocations))

        record('BOND EXPOSURE', new_allocations[cash_proxy])

        return new_allocations

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _by_formula(self, context, allocations, cash_proxy, *args):
        if self.formula == 'DPF':
            try:
                new_allocations = self._apply_DPF(context, allocations, cash_proxy, *args)
            except:
                raise ValueError('FORMULA "{}" DOES NOT EXIST OR ERROR CALCULATING FORMULA'.formmat(self.formula))
        return new_allocations

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _apply_DPF(self, context, allocations, cash_proxy, *args):
        securities = args[0][0]
        N = len(securities)
        try:
            triggers = context.rules[self.rule].apply_rule(context)[securities]
        except:
            raise ValueError('UNABLE TO APPLY RULE {}'.format(self.rule))

        num_neg = triggers[triggers == True].count()
        dpf = float(num_neg) / N
        log.info("DOWNSIDE PROTECTION FACTOR = {}".format(dpf))

        new_allocations = (1. - dpf) * allocations
        new_allocations[cash_proxy] += dpf

        return new_allocations
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


class Rule():
    functions = {'EQ': lambda x, y: x == y,
                 'LT': lambda x, y: x < y,
                 'GT': lambda x, y: x > y,
                 'LE': lambda x, y: x <= y,
                 'GE': lambda x, y: x >= y,
                 'NE': lambda x, y: x != y,
                 'AND': lambda x, y: x & y,
                 'OR': lambda x, y: x | y,
                 }

    def __init__(self, context, name='', rule='', apply_to='all'):

        self.name = name
        # remove spaces
        self.rule = rule.replace(' ', '')
        self.temp = ''
        self.apply_to = apply_to

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def apply_rule(self, context):

        ''' routine to evaluate a rule consisting of a string formatted as 'conditional [AND|OR conditional]'
            where conditionals are logical expressions, pandas series of logical expressions
            or pandas dataframes of logical expressions. Returns True or False,
            pandas series of True/False or pandas dataframe of True/False respectively.
        '''

        if self.rule == 'always_true':
            return True

        self.temp = self._replace_operators(self.rule)
        # get the first condition of the rule and evaluate it
        condition, result, cjoin = self._get_next_conditional(context)

        # log.debug ('result = {}'.format(result))

        while cjoin != None:
            # get the rest of the rule
            self.temp = self.temp[len(condition) + len(cjoin):]
            # get the next conditional of the rule and evaluate it
            func = Rule.functions[cjoin]
            condition, tmp_result, cjoin = self._get_next_conditional(context)

            result = func(result, tmp_result)

            # log.debug ('intermediate result = {}'.format(result))

        # log.debug ('final result = {}'.format(result))
        return result

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_next_conditional(self, context):
        condition, cjoin = self._get_conditional(self.temp)
        result = self._evaluate_condition(context, condition)
        if self.apply_to != 'all':
            result = result[self.apply_to]
        return condition, result, cjoin
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _replace_operators(self, s):

        ''' to make it easy to find operators in the rule s, replace ['=', '>', '<', '>=', '<=', '!=', 'and', 'or']
            with ['EQ', 'GT', 'LT', 'GE', 'LE', 'NE', 'AND', 'OR'] respectively
        '''

        s1 = s.replace('and', 'AND').replace('or', 'OR').replace('!=', 'NE').replace('<=', 'LE').replace('>=', 'GE')
        s1 = s1.replace('=', 'EQ').replace('<', 'LT').replace('>', 'GT')
        return s1

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_conditional(self, s):

        ''' routine to find first ocurrence of "AND" or "OR" in rule s. Returns
        conditional to the left of AND/OR and either 'AND', 'OR' or None '''

        pos_AND = [s.find('AND') if s.find('AND') != -1 else len(s)][0]
        pos_OR = [s.find('OR') if s.find('OR') != -1 else len(s)][0]
        condition, cjoin = [(s.split('AND')[0], 'AND') if pos_AND < pos_OR else (s.split('OR')[0], 'OR')][0]
        if pos_AND == len(s) and pos_OR == len(s):
            cjoin = None
        return condition, cjoin

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_operator(self, condition):

        '''routine to extract the operator and its position from the conditional expression
        '''
        for o in ['EQ', 'GT', 'LT', 'GE', 'LE', 'NE', 'AND', 'OR']:
            if condition.find(o) > 0:
                return o, condition.find(o)
        raise ('UNKNOWN OPERATOR')

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_operand_value(self, context, operand):
        if operand.startswith('('):
            tuple_0 = operand[1:operand.find(',')].strip("'").strip('"')
            tuple_1 = operand[operand.find(',') + 1:-1]
            return context.algo_data[tuple_0][tuple_1]
        if operand[0].isdigit() or operand.startswith('.') or operand.startswith('-'):
            return float(operand)
        elif isinstance(operand, str):
            return context.algo_data[operand.strip("'").strip('"')]
        else:
            op = context.algo_data[operand[0]]
            if operand[1] != None:
                op = context.algo_data[operand[0].strip("'").strip('"')][operand[1]]
            return op

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _evaluate_condition(self, context, condition):
        operator, position = self._get_operator(condition)
        x = self._get_operand_value(context, condition[:position])
        y = self._get_operand_value(context, condition[position + 2:])
        # log.debug ('x = {}, y = {}, operator = {}'.format(x, y, operator))
        func = Rule.functions[operator]

        return func(x, y)
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


class Transform():

    def __init__(self, context, name='', function='', inputs=[], kwargs={}, outputs=[]):

        self.name = name
        self.function = function
        self.inputs = inputs
        self.kwargs = kwargs
        self.outputs = outputs

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def apply_transform(self, context):

        # transform format [([<data_items>], function, <data_item>, args)]

        context.dp = pd.Panel(context.raw_data)

        if self.function in TALIB_FUNCTIONS:
            return self._apply_talib_function(context)

        elif self.function.__name__.startswith('roll') or self.function.__name__.startswith(
                'expand') or self.function.__name__ == '<lambda>':
            return self._apply_pandas_function(context)

        else:
            return self.function(self, context)

        raise ValueError('UNKNOWN TRANSFORM {}'.format(self.function))

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _apply_talib_function(self, context):

        '''
        Routine to apply transform to data provided as a pandas Panel.
        Inputs:
        dp: pandas dataPanel consisting of a DataFrame for each item in ['open', 'high', 'low', 'close', 'volume',
            'price']; each DataFrame has column names = asset names
        inputs : list of dp items to be used as inputs. If empty (=[]), routine will use default input
                        names from the talib function DOC string
        function : talib function name (e.g. RSI, MACD, ADX etc.) - see list of imported functions above
        output_names : list of names for the tranforms DataFrames
        NOTE: names must be unique and there must be a name for each output (some transforms produce more than
                one output e.g MACD produces 3 outputs)
        args : empty list (=[]), in which case default values are obtained from talib function DOC string.
                otherwise, custom parameters may be provided as a list of integers, the parameters matching
                the FULL parameter list, as per the function DOC string

        Outputs:
            pandas DataPanel with new items (DataFrames) appended for each transform output.

        '''

        # parameters = [a for a in self.args]
        parameters = [self.kwargs[key] for key in iter(self.kwargs)]
        if parameters == []:
            parameters = [int(s) for s in re.findall('\d+', self.function.__doc__)]
        data_items = re.findall("(?<=\')\w+", self.function.__doc__)
        if data_items == []:
            inputs = self.inputs
        else:
            inputs = data_items

        for output in self.outputs:
            context.dp[output] = pd.DataFrame(0, index=context.dp.major_axis, columns=context.dp.minor_axis)

        for asset in context.dp.minor_axis:
            data = [context.dp.transpose(2, 1, 0)[asset][i].values for i in inputs]
            args = data + parameters
            transform = self.function(*args)
            if len(transform) == len(self.outputs) or len(transform) > 3:
                pass
            else:
                raise ValueError('** ERROR : must be output_names for each output')

            if len(self.outputs) == 1:
                context.dp[self.outputs[0]][asset] = transform
            else:
                for i, output in enumerate(self.outputs):
                    context.dp[output][asset] = transform[i]

        # for some reason, if you don't do this, then dp.transpose(2,1,0) gives dp[output][asset] as 0 !!
        for name in self.outputs:
            context.dp[name] = context.dp[name]

        return context.dp
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _apply_pandas_function(self, context):

        '''
        Routine to apply pandas function to column(s) of data provided as Pandas DataFrame.
        Allowed functions include all the pandas.rolling_ and pandas.expanding_ functions.
        NOTE: corr and cov are NOT allowed here, but must be implemented as CUSTOM FUNCTIONS
        Inputs:
            dp = Pandas DataPanel with data to be transformed in one (or more) panel items
            NOTE: in the case of CORR or COV, columns contain price data for each stock.
            inputs = name(s) of item(s) containing data to be transformed (DataFrames with columns = asset names)
            function = name of pandas function provided by user (pd.rolling_  or pd.expanding_ )
            args = list of arguments required by function
        Output:
            Pandas DataPanel with appended items containing the transformed data as DataFrames, or,
            as in the case of CORR and COV functions, the item is a DataPanel of correlations/covariances

        '''
        if 'corr' in self.function.__name__ or 'cov' in self.function.__name__:
            raise ValueError('** ERROR: Correlation and Covariance must be implemented as CUSTOM FUNCTIONS')

        for asset in context.dp.minor_axis:
            context.dp[self.outputs[0]] = self.function(context.dp[self.inputs[0]], *self.args)

        return context.dp

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # Custom Transforms

    def n_period_return(self, context):

        '''
        percentage return (optionally, excess return) over n periods
        most recent period can optionally be skipped

        kwargs[0] = 'no of periods'
        kwargs[1] = 'period' : 'D'|'W'|'M' (day|week||month)
        kwargs[2] = 'skip_period' (optional = False)

        '''
        try:
            skip_period = self.kwargs['skip_period']
        except:
            skip_period = False

        # TODO : need to return excess_return, depending on risk_free

        prices = context.dp[self.inputs[0]]

        no_of_periods = self.kwargs['lookback']
        # if no 'period' kwarg, assume 'D'
        try:
            period = self.kwargs['period']
        except:
            period = 'D'

        if period in ['W', 'M']:
            returns = prices.resample(period).last().pct_change(no_of_periods)
        elif period == 'D':
            returns = prices.pct_change(no_of_periods)

        idx = -1
        if skip_period:
            idx = - 2

        df = pd.DataFrame(0, index=context.dp.major_axis,
                          columns=context.dp.minor_axis)
        if not isinstance(context.risk_free, int):
            returns = returns.sub(returns[context.risk_free], axis=0)

        ds = returns.iloc[idx]
        df.iloc[-1] = ds

        context.dp[self.outputs[0]] = df

        return context.dp
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def simple_mean_monthly_average(self, context):

        h = context.dp[self.inputs[0]]
        lookback = self.kwargs['lookback']
        ds = h.resample('M').last()[-lookback - 1:-1].mean()

        df = pd.DataFrame(0, index=h.index, columns=h.columns)
        df.iloc[-1] = ds

        context.dp[self.outputs[0]] = df

        return context.dp

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def momentum(self, context):

        lookback = self.kwargs['lookback']
        ds = context.dp[self.inputs[0]].iloc[-1] / context.dp[self.inputs[0]].iloc[-lookback] - 1

        df = pd.DataFrame(0, index=context.dp.major_axis, columns=context.dp.minor_axis)
        df.iloc[-1] = ds

        context.dp[self.outputs[0]] = df

        return context.dp

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def daily_returns(self, context):

        context.dp[self.outputs[0]] = context.dp['price'].pct_change(1)

        return context.dp

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def excess_momentum(self, context):

        lookback = self.kwargs['lookback']
        ds = context.dp['price'].pct_change(lookback).iloc[-1] - \
             context.dp['price'][context.risk_free].pct_change(lookback).iloc[-1]

        df = pd.DataFrame(0, index=context.dp.major_axis, columns=context.dp.minor_axis)
        df.iloc[-1] = ds

        context.dp[self.outputs[0]] = df

        return context.dp

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def log_returns(self, context):

        try:
            context.dp[self.outputs[0]] = np.log(1. + context.dp['price'].pct_change(1))
        except:
            raise RuntimeError("Inputs must be ['price']")

        return context.dp

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def historic_volatility(self, context):

        lookback = self.kwargs['lookback']
        try:
            ret_log = np.log(1. + context.dp['price'].pct_change())
        except:
            raise RuntimeError("Inputs must be ['price']")

        # this is for pandas < 0.18
        # hist_vol = pd.rolling_std(ret_log, lookback)

        # this is for pandas ver > 0.18
        hist_vol = ret_log.rolling(window=lookback, center=False).std()

        context.dp[self.outputs[0]] = hist_vol * np.sqrt(252 / lookback)

        return context.dp
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def average_excess_return_momentum(self, context):

        '''
        Average Excess Return Momentum

        average_excess_return_momentum is the average of monthly returns in excess of the risk_free rate for multiple
        periods (1,3,6,12 months). In addtion, average momenta < 0 are set to 0.

        '''
        h = context.dp[self.inputs[0]].copy()
        hm = h.resample('M').last()
        hb = h.resample('M').last()[context.risk_free]

        ds = (hm.ix[-1] / hm.ix[-2] - hb.ix[-1] / hb.ix[-2] + hm.ix[-1] / hm.ix[-4]
              - hb.ix[-1] / hb.ix[-4] + hm.ix[-1] / hm.ix[-7] - hb.ix[-1] / hb.ix[-7]
              + hm.ix[-1] / hm.ix[-13] - hb.ix[-1] / hb.ix[-13]) / 22
        ds[ds < 0] = 0
        df = pd.DataFrame(0, index=h.index, columns=h.columns)
        df.iloc[-1] = ds

        context.dp[self.outputs[0]] = df

        return context.dp
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def paa_momentum(self, context):

        ds = context.dp[self.inputs[0]].iloc[-1] / context.dp[self.inputs[1]].iloc[-1] - 1

        df = pd.DataFrame(0, index=context.dp.major_axis, columns=context.dp.minor_axis)
        df.iloc[-1] = ds

        context.dp[self.outputs[0]] = df

        return context.dp

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def crossovers(self, context):
        df1 = context.dp[self.inputs[0]]
        df2 = context.dp[self.inputs[1]]
        down = (df1 > df2) & (df1.shift(1) < df2.shift(1)).astype(int)
        up = (df1 < df2) & (df1.shift(1) > df2.shift(1)).astype(int)
        # returns +1 for crosses above and -1 for crosses below
        return down * (-1) + up
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def slope(self, context):

        lookback = self.kwargs['lookback']
        ds = pd.Series(index=context.dp.minor_axis)
        for asset in context.dp.minor_axis:
            ds[asset] = talib.LINEARREG_SLOPE(context.dp[self.inputs[0]][asset].values, lookback)[-1]
        df = pd.DataFrame(0, index=context.dp.major_axis, columns=context.dp.minor_axis)
        df.iloc[-1] = ds
        return df


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class Configurator():
    '''
    The Configurator uses the Strategy Parameters set up by the StrategyParameters Class and dictionary of global
    parameters to create all the objects required for the algorithm.

    '''

    # def __init__ (self, context, strategies, global_parameters=None) :
    def __init__(self, context, strategies):
        self.strategies = strategies
        # self.global_parameters = global_parameters
        # self._set_global_parameters (context)
        context.tranforms = define_transforms(context)

        context.algo_rules = define_rules(context)
        self._configure_algo_strategies(context)
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _configure_algo_strategies(self, context):
        for s in self.strategies:
            self._check_valid_parameters(context, s)
            self._configure_strategy(context, s)
            # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    # TODO: would be better to make this table-driven
    # TODO : check strategy names are unique
    # TODO : compute context.max_lookback

    def _check_valid_parameters(self, context, strategy):
        N = len(strategy.portfolios)
        s = strategy
        self._check_valid_parameter(context, s, strategy.portfolios, 'portfolios', list, N, list, ''),
        self._check_valid_parameter(context, s, strategy.portfolio_allocation_modes, 'portfolio_allocation_modes',
                                    list, N, str, VALID_PORTFOLIO_ALLOCATION_MODES),
        self._check_valid_parameter(context, s, strategy.security_weights, 'security_weights', list, N, list, ''),
        self._check_valid_parameter(context, s, strategy.portfolio_allocation_formulas, 'portfolio_allocation_formulas',
                                    list,
                                    N, str, VALID_PORTFOLIO_ALLOCATION_FORMULAS),
        self._check_valid_parameter(context, s, strategy.security_scoring_methods, 'security_scoring_methods', list, N,
                                    str, VALID_SECURITY_SCORING_METHODS),
        self._check_valid_parameter(context, s, strategy.security_scoring_factors, 'security_scoring_factors', list, N,
                                    dict, ''),
        self._check_valid_parameter(context, s, strategy.security_n_tops, 'security_n_tops', list, N, int, '')
        self._check_valid_parameter(context, s, strategy.portfolio_scoring_method, 'portfolio_scoring_method', list, 1,
                                    str, VALID_PORTFOLIO_SCORING_METHODS),
        self._check_valid_parameter(context, s, strategy.portfolio_scoring_factors, 'portfolio_scoring_factors', list,
                                    1, dict, ''),
        self._check_valid_parameter(context, s, strategy.portfolio_n_top, 'portfolio_n_top', list, 1, int, '')
        self._check_valid_parameter(context, s, strategy.protection_modes, 'protection_modes', list, N,
                                    str, VALID_PROTECTION_MODES),
        self._check_valid_parameter(context, s, strategy.protection_rules, 'protection_rules', list, N, str, ''),
        self._check_valid_parameter(context, s, strategy.protection_formulas, 'protection_formulas', list, N,
                                    str, VALID_PROTECTION_FORMULAS),
        self._check_valid_parameter(context, s, strategy.cash_proxies, 'cash_proxies', list, N, type(symbols('SPY')[0]),
                                    ''),
        self._check_valid_parameter(context, s, strategy.strategy_allocation_mode, 'strategy_allocation_mode', str,
                                    1, str, VALID_STRATEGY_ALLOCATION_MODES)
        self._check_valid_parameter(context, s, strategy.portfolio_weights, 'portfolio_weights', list, N, float, ''),
        self._check_valid_parameter(context, s, strategy.strategy_allocation_formula, 'strategy_allocation_formula',
                                    str,
                                    1, str, VALID_STRATEGY_ALLOCATION_FORMULAS)
        self._check_valid_parameter(context, s, strategy.strategy_allocation_rule, 'strategy_allocation_rule', str,
                                    1, str, '')
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _check_valid_parameter(self, context, s, param, name, param_type, param_length, item_type, valid_params):

        if name in ['strategy_allocation_mode', 'portfolio_weights', 'strategy_allocation_formula',
                    'strategy_parameters', 'strategy_allocation_rule', 'portfolio_scoring_method',
                    'portfolio_scoring_factors', 'portfolio_n_top']:
            self._check_strategy_parameters(context, s, param, name, param_type, param_length, item_type, valid_params)
        else:
            # if param is None and name in NONE_NOT_ALLOWED :
            #     raise RuntimeError ('"None" not allowed for parameter {}'.format(name))
            # if param is None and 'FIXED' in s.portfolio_allocation_modes:
            #     raise RuntimeError ('Parameter {} cannot be None for portfolio_allocation_mode "FIXED"'.format(name))
            # else:
            #     # valid None parameter
            #     return

            self._check_param_type(name, param, param_type)

            if len(param) != param_length:
                raise RuntimeError('Parameter {} must be of length {} not {}'.format(name, param_length, len(param)))
            for n in range(param_length):
                if param[n] == None and name in NONE_NOT_ALLOWED:
                    raise RuntimeError('"None" not allowed for parameter {}'.format(name))
                elif param[n] == None:
                    if name == 'scoring_factors' and s.protection_modes == 'RS':
                        self._check_valid_scoring_factors(name, param[n])
                    # if name == 'security_n_tops' and s.portfolio_allocation_modes[n] == 'FIXED' :
                    #     if param[n] != len(s.security_weights[n]) :
                    #         raise RuntimeError ('For portfolio_allocation_mode = "FIXED", n_tops must equal no of security weights')
                    continue
                if valid_params != "":
                    if param[n] not in valid_params:
                        raise RuntimeError('Invalid {} {}'.format(name, param[n]))
                if not isinstance(param[n], item_type):
                    raise RuntimeError('Items of {} must be of type {} not {}'.format(name, item_type, type(param[n])))
                if name == 'portfolios':
                    self._check_valid_portfolio(param[n])

                if name.endswith('_weights') and np.sum(param[n]) != 1.:
                    raise RuntimeError('Sum of {} must equal 1, not {}'.format(name, np.sum(param)))

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _check_strategy_parameters(self, context, s, param, name, param_type, param_length, item_type, valid_params):
        if name == 'strategy_allocation_mode':
            if param not in valid_params:
                raise RuntimeError('Invalid strategy_allocation_mode {}'.format(param))
        elif name == 'portfolio_weights' and s.strategy_allocation_mode == 'FIXED':
            if np.sum(param) != 1.:
                raise RuntimeError('portfolio_weights must be a list of floating point numbers with sum = 1')
        elif name == 'strategy_allocation_formula':
            if param not in valid_params:
                raise RuntimeError('Invalid strategy_allocation_formula {}'.format(param))
        elif name == 'strategy_allocation_rule' and s.strategy_allocation_rule != None:
            valid_rules = [rule.name for rule in context.algo_rules]
            if s.strategy_allocation_rule not in valid_rules:
                raise RuntimeError(
                    'Strategy rule {} not found. Check rule definitions'.format(s.strategy_allocation_rule))
        elif name == 'portfolio_scoring_method':
            if param not in valid_params:
                raise RuntimeError('Invalid strategy_allocation_formula {}'.format(param))
                # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _check_param_type(self, name, param, param_type):
        if not isinstance(param, param_type):
            raise RuntimeError('Parameter {} must be of type {} not {}'.format(name, param_type, type(param)))

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _check_valid_scoring_factors(self, name, factors):
        sum_of_weights = 0.

        for key in factors.keys():
            if not key[0] in ['+', '-']:
                raise RuntimeError('First character of scoring factor {}, must be "+" or "-"'.format(key))
            sum_of_weights += factors[key]
        if sum_of_weights != 1.:
            raise RuntimeError('Sum of {} weights must equal 1, not {}'.format(name, sum_of_weights))

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _check_valid_portfolio(self, pfolio):
        if len(pfolio) < 1:
            raise RuntimeError('Portfolio must have at least one item')
        for n in range(len(pfolio)):
            if not isinstance(pfolio[n], type(symbols('SPY')[0])):
                raise RuntimeError('portfolio item {} must be of type '.format(type(symbols('SPY')[0])))
                # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    def _configure_strategy(self, context, s):

        portfolios = []

        for n in range(len(s.portfolios)):
            if s.security_scoring_methods[n] is None:
                scoring_model = None
            else:
                scoring_model = ScoringModel(context,
                                             method=s.security_scoring_methods[n],
                                             factors=s.security_scoring_factors[n],
                                             n_top=s.security_n_tops[n])

            if s.protection_modes[n] == None:
                downside_protection_model = None
            else:
                downside_protection_model = DownsideProtectionModel(context,
                                                                    mode=s.protection_modes[n],
                                                                    rule=s.protection_rules[n],
                                                                    formula=s.protection_formulas[n])

            portfolios = portfolios + \
                         [Portfolio(context,
                                    ID=s.ID + '_p' + str(n + 1),
                                    securities=s.portfolios[n],
                                    allocation_model=AllocationModel(context,
                                                                     mode=s.portfolio_allocation_modes[n],
                                                                     weights=s.security_weights[n],
                                                                     formula=s.portfolio_allocation_formulas[n],
                                                                     kwargs=s.portfolio_allocation_kwargs[n]
                                                                     ),
                                    scoring_model=scoring_model,
                                    downside_protection_model=downside_protection_model,
                                    cash_proxy=s.cash_proxies[n]
                                    )]

        if s.portfolio_scoring_method is None:
            scoring_model = None
        else:
            scoring_model = ScoringModel(context,
                                         method=s.portfolio_scoring_method,
                                         factors=s.portfolio_scoring_factors,
                                         n_top=s.portfolio_n_top)
        s.strategy = Strategy(context,
                              ID=s.ID,
                              allocation_model=AllocationModel(context,
                                                               mode=s.strategy_allocation_mode,
                                                               weights=s.portfolio_weights,
                                                               formula=s.strategy_allocation_formula,
                                                               kwargs=s.strategy_allocation_kwargs,
                                                               rule=s.strategy_allocation_rule),
                              scoring_model=scoring_model,
                              portfolios=portfolios
                              )


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class StrategyParameters():
    '''
    StrategyParameters hold the parameters for each strategy for a single or multistrategy algoritm

    calling:

    strategy = StrategyParameters(context, portfolios, portfolio_allocation_modes, security_weights,
                         portfolio_allocation_formulas,
                         scoring_methods, scoring_factors, n_tops,
                         protection_modes, protection_rules, protection_formulas,
                         cash_proxies, strategy_allocation_mode, portfolio_weights=None,
                         strategy_allocation_formula, strategy_allocation_rule)

    see below for definition of each parameter

    '''

    # NOTE: kwarg label 'lookback' should be ALWAYS be used for timeseries lookback periods!
    def __init__(self, context, ID, portfolios=[],
                 portfolio_allocation_modes=[], security_weights=None,
                 portfolio_allocation_formulas=None,
                 portfolio_allocation_kwargs=None,
                 security_scoring_methods=None, security_scoring_factors=None,
                 security_n_tops=None,
                 protection_modes=None, protection_rules=None, protection_formulas=None,
                 cash_proxies=[],
                 strategy_allocation_mode='EW', portfolio_weights=None,
                 portfolio_scoring_method=None, portfolio_scoring_factors=None,
                 portfolio_n_top=None,
                 strategy_allocation_formula=None,
                 strategy_allocation_kwargs=None,
                 strategy_allocation_rule=None):

        # strategy ID, must be unique str
        # eg 'strat1'
        self.ID = ID
        # list of n valid security lists (must be at least one security list)
        # eg [symbols('SPY','EEM')] or [symbols('SPY','EEM'), symbols('TLT','JNK','SHY'),....]
        self.portfolios = portfolios
        n = len(portfolios)
        # list of n VALID_PORTFOLIO_ALLOCATION_MODES, one for each portfolio
        # eg ['EW'] or ['EW', 'PROPORTIONAL',.....]
        self.portfolio_allocation_modes = portfolio_allocation_modes
        # either None or list of n kwargs each containing kwargs matching porfolio_allocation_modes
        # eg None or [kwargs1] or [kwargs1, kwargs2, ....] where kargsn = dict of kwargs relevant to modes
        self.portfolio_allocation_kwargs = portfolio_allocation_kwargs
        if portfolio_allocation_kwargs is None:
            self.portfolio_allocation_kwargs = [None for i in range(n)]
        # None or list of n lists of security weights for 'FIXED' portfolio_allocation_modes, else None
        # eg None or [[0.2,0.8]] or [[0.5,0.5],[0.1,0.7,0.2]...] where sum of each list = 1
        self.security_weights = security_weights
        if security_weights is None:
            self.security_weights = [None for i in range(n)]
            # None or list of n VALID_PORTFOLIO_ALLOCATION_FORMULAS for 'BY_FORMULA'
        # portfolio_allocation_modes, else None
        # eg None or [valid formula] or [None, valid formula, ...] for each portfolio where allocation 'BY_FORMULA'
        self.portfolio_allocation_formulas = portfolio_allocation_formulas
        if portfolio_allocation_formulas is None:
            self.portfolio_allocation_formulas = [None for i in range(n)]
            # None or list of n VALID_SECURITY_SCORING_METHODS, one for each portfolio
        # eg None or ['RS'] or [None, 'EAA', ....]
        self.security_scoring_methods = security_scoring_methods
        if security_scoring_methods is None:
            self.security_scoring_methods = [None for i in range(n)]
            # None or list of n dicts of scoring factors, relevant to each scoring method
        # eg None or [factors1] or [None, factors2, ...] where factorsn = dict of factors relevant to scoring methods
        self.security_scoring_factors = security_scoring_factors
        if security_scoring_factors is None:
            self.security_scoring_factors = [None for i in range(n)]
            # None or list of n_tops, one for each ranked portfolio, else None; n_top <= len(portfolio) - 1
        # eg None or [1], [1,2,...]
        self.security_n_tops = security_n_tops
        if security_n_tops is None:
            self.security_n_tops = [None for i in range(n)]
            # None or list of n VALID_PROTECTION_MODES, one for each portfolio
        # eg None or ['RAA'] or [None, 'BY_RULE','BY_FORMULA', ....]
        self.protection_modes = protection_modes
        if protection_modes is None:
            self.protection_modes = [None for i in range(n)]
            # None or list of n valid rules for portfolios with protection mode 'BY_RULE', else None
        # eg None or [valid rule] or [None, valid rule, ...] for each portfolio where allocation 'BY_RULE'
        self.protection_rules = protection_rules
        if protection_rules is None:
            self.protection_rules = [None for i in range(n)]
            # None or list of n VALID_PROTECTION_FORMULAS for portfolios with protection mode 'BY_FORMULA', else None
        # eg None or [valid formula] or [None, valid formula, ...] for each portfolio where allocation 'BY_FORMULA'
        self.protection_formulas = protection_formulas
        if protection_formulas is None:
            self.protection_formulas = [None for i in range(n)]
            # list of n valid securities to be used as cash proxies, one for each portfolio
        # eg [symbol('SHY')] or [symbol('SHY'), symbol('TLT'),...]  NOTE: symbol NOT symbols!!
        self.cash_proxies = cash_proxies
        # any one of VALID_STRATEGY_ALLOCATION_MODES
        # eg 'RISK_TARGET'
        self.strategy_allocation_mode = strategy_allocation_mode
        # None or any kwargs relevant to the strategy_allocation_mode
        # eg {'lookback': 100, 'target_risk': 0.01}
        self.strategy_allocation_kwargs = strategy_allocation_kwargs
        # None or list of n portfolio weights (sum = 1) if strategy_allocation_mode is 'FIXED'
        self.portfolio_weights = portfolio_weights
        if portfolio_weights is None:
            self.portfolio_weights = [None for i in range(n)]
            # None or one of VALID_STRATEGY_ALLOCATION_FORMULAS, if strategy_allocation_mode is 'BY_FORMULA'
        # eg 'PAA'
        self.strategy_allocation_formula = strategy_allocation_formula
        # None or one of VALID_PORTFOLIO_SCORING_METHODS
        # eg 'RS'
        self.portfolio_scoring_method = portfolio_scoring_method
        # None or dict of factors to be used for scoring (ranking) portfolios
        # eg {'+factor1': 10, '-factor2':20} - NOTE that factor names must be prefixed by '+' or '-'
        # to indicate whether to rank factor ascending (+) or descending (-)
        self.portfolio_scoring_factors = portfolio_scoring_factors
        # None or integer <= no of portfolios - 1
        # eg 2
        self.portfolio_n_top = portfolio_n_top
        # None or one of VALID_STRATEGY_ALLOCATION_RULES
        # eg None
        self.strategy_allocation_rule = strategy_allocation_rule
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


# if traling stops not required, this can be commented out
def handle_data(context, data):
    if not context.use_trailing_stops:
        return

    # see https://www.quantopian.com/posts/trailing-stop-loss-with-multiple-securities
    for security in context.portfolio.positions:
        current_position = context.portfolio.positions[security].amount
        context.stop_price[security] = max(context.stop_price[security] if security in context.stop_price
                                           else 0, context.stop_pct * data.current(security, 'price'))
        if (data.current(security, 'price') < context.stop_price[security]) and (current_position > 0):
            order_target_percent(security, 0)
            del context.stop_price[security]
            log.info("Trail Selling {} at {}".format(security.symbol, data.current(security, 'price')))
        # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


def before_trading_start(context, data):
    """
    Called every day before market open.
    """

    if get_environment('platform') == 'zipline':
        # allow data buffer to fill in the ZIPLINE ENVIRONMENT
        if context.day_no <= context.max_lookback:
            context.day_no += 1
            return

    # generate updated algo data
    context.algo_data = context.data.update(context, data)

    if np.sum(context.strategies[0].weights) > 1.e-07:
        # wait until first allocation to generate portfolio and strategy metrics
        context.data.update_portfolio_and_strategy_metrics(context, data)

    return context.algo_data


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def initialize(context):
    # this routine should not require changes

    context.transforms = []
    context.algo_rules = []
    context.max_lookback = 64  # minimum value for max_lookback
    context.outstanding = {}  # orders which span multiple days

    context.raw_data = {}

    context.trading_day_no = 0

    #############################################################
    set_global_parameters(context)
    #############################################################
    context.strategy_parameters = set_strategy_parameters(context)
    # strategy_params = [context.strategy_parameters[p] for p in context.strategy_parameters]
    #############################################################
    # configure strategies
    Configurator(context, strategies=context.strategy_parameters)
    #############################################################
    strategies = [s.strategy for s in context.strategy_parameters]
    algo = set_algo_parameters(context, strategies)
    #############################################################
    # daily functions to handle GTC orders
    # note: GTC_LIMIT=10 (default) set as global
    schedule_function(algo.check_for_unfilled_orders, date_rules.every_day(), time_rules.market_close())
    schedule_function(algo.fill_outstanding_orders, date_rules.every_day(), time_rules.market_open())

    if context.show_positions:
        schedule_function(algo.show_positions, date_rules.month_start(days_offset=0), time_rules.market_open())

    if context.show_records:
        # show records every day
        # edit the show_records function to include records required
        schedule_function(algo.show_records, date_rules.every_day(), time_rules.market_close())

    if context.rebalance_period == 'A':
        schedule_function(algo.check_signal_trigger, date_rules.every_day(), time_rules.market_open())

    else:
        periods = {'D': date_rules.every_day(),
                   'WS': date_rules.week_start(days_offset=context.days_offset),
                   'WE': date_rules.week_end(days_offset=context.days_offset),
                   'MS': date_rules.month_start(days_offset=context.days_offset),
                   'ME': date_rules.month_end(days_offset=context.days_offset)}

        period = periods[context.rebalance_period]

        if context.on_open:
            time_rule = time_rules.market_open(hours=context.hours, minutes=context.minutes)
        else:
            time_rule = time_rules.market_close(hours=context.hours, minutes=context.minutes)

        schedule_function(algo.rebalance, period, time_rule)


#########################################################################################################
#########################################################################################################
# the following routines contain all the configuration details
# any transform which relies on lookback data MUST have a 'lookback' kwarg
# and, optionally, 'period' = <no. of days> |'W'| 'M'
# NOTE: kwarg label 'lookback' should be ALWAYS be used for timeseries lookback periods!
def define_transforms(context):  # Define transforms
    # select transforms required and make sure correct parameters are used
    # no need to comment out unused transforms, but they will slow algo down
    context.transforms = [
        Transform(context, name='momentum', function=Transform.n_period_return, inputs=['price'],
                  kwargs={'lookback': 45, 'risk_free': 0, 'skip_period': False}, outputs=['momentum']),
        Transform(context, name='mom_A', function=ROCP, inputs=['price'],
                  kwargs={'lookback': 43}, outputs=['mom_A']),
        Transform(context, name='mom_B', function=ROCP, inputs=['price'],
                  kwargs={'lookback': 21}, outputs=['mom_B']),
        Transform(context, name='daily_returns', function=Transform.daily_returns, inputs=['price'],
                  kwargs={}, outputs=['daily_returns']),
        Transform(context, name='vol_C', function=STDDEV, inputs=['daily_returns'],
                  kwargs={'lookback': 20}, outputs=['vol_C']),
        Transform(context, name='hist_vol', function=Transform.historic_volatility, inputs=['price'],
                  kwargs={'lookback': 45}, outputs=['hist_vol']),
        Transform(context, name='slope', function=Transform.slope, inputs=['price'],
                  kwargs={'lookback': 100}, outputs=['slope']),
        Transform(context, name='TMOM', function=Transform.momentum, inputs=['price'],
                  kwargs={'lookback': 43}, outputs=['TMOM']),
        Transform(context, name='MA', function=SMA, inputs=['price'],
                  kwargs={'lookback': 100}, outputs=['MA']),
        Transform(context, name='R', function=Transform.average_excess_return_momentum, inputs=['price'],
                  kwargs={'lookback': 13, 'period': 'M'}, outputs=['R']),
        Transform(context, name='RMOM', function=Transform.momentum, inputs=['price'],
                  kwargs={'lookback': 43}, outputs=['RMOM']),
        Transform(context, name='TMOM', function=Transform.excess_momentum, inputs=['price'],
                  kwargs={'lookback': 43}, outputs=['TMOM']),
        Transform(context, name='EMOM', function=Transform.momentum, inputs=['price'],
                  kwargs={'lookback': 43}, outputs=['EMOM']),
        Transform(context, name='volatility', function=STDDEV, inputs=['daily_returns'],
                  kwargs={'lookback': 43}, outputs=['volatility']),
        Transform(context, name='smma', function=Transform.simple_mean_monthly_average, inputs=['price'],
                  kwargs={'lookback': 1, 'period': 'M'}, outputs=['smma']),
        Transform(context, name='mom', function=Transform.paa_momentum, inputs=['price', 'smma'],
                  kwargs={'lookback': 2, 'period': 'M'}, outputs=['mom']),
        Transform(context, name='smma_12', function=Transform.simple_mean_monthly_average, inputs=['price'],
                  kwargs={'lookback': 12, 'period': 'M'}, outputs=['smma_12']),
        Transform(context, name='rebalance_signal', function=Transform.crossovers, inputs=['price', 'MA'],
                  kwargs={'timeperiods': 100}, outputs=['rebalance_signal']),
    ]

    return context.transforms


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def define_rules(context):  # Define rules
    # select rules required and make sure correct transform names are used
    # no need to comment out unused rules
    context.algo_rules = [
        # Rule(context, name='absolute_momentum_rule', rule="'price' < 'smma' "),
        # Rule(context, name='dual_momentum_rule', rule="'TMOM' < 0"),
        Rule(context, name='smma_rule', rule="'price' < 'smma'"),
        # Rule(context, name='complex_rule', rule="'price' < smma or 'TMOM' < 0"),
        Rule(context, name='momentum_rule', rule="'price' < 'MA'"),
        Rule(context, name='EAA_rule', rule="'R' <= 0"),
        Rule(context, name='paa_rule', rule="'mom' <= 0"),
        Rule(context, name='paa_filter', rule="'mom' > 0"),
        Rule(context, name='momentum_rule1', rule="'price' < 'smma_12'"),
        Rule(context, name='riskon', rule="'price' > 'smma_12'", apply_to=context.market_proxy),
        Rule(context, name='riskoff', rule="'price' <= 'smma_12'", apply_to=context.market_proxy),
        Rule(context, name='neutral', rule="'slope' <= 0.1 and 'slope' >= -0.1",
             apply_to=context.market_proxy),
        Rule(context, name='bull', rule="'slope' > 0.1", apply_to=context.market_proxy),
        Rule(context, name='bear', rule="'slope' < -0.1", apply_to=context.market_proxy)
        # Rule(context, name='rebalance_rule', rule="'rebalance_signal' != 0"),
    ]

    return context.algo_rules


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def set_global_parameters(context):
    # set the following parameters as required

    context.show_positions = True
    # select records to show in algo.show_records()
    context.show_records = True

    # replace cash_proxy with risk_free if context.allow_cash_proxY_replacement is True
    # and cash_proxy price is <= average cash_proxy price over last context.cash_proxy_lookback days
    context.allow_cash_proxy_replacement = False
    context.cash_proxy_lookback = 43  # must be <= context.max_lookback

    context.use_trailing_stops = False
    context.stop_pct = 0.92
    context.stop_price = {}

    # to calculate portfolio and strategy Sharpe ratios
    context.SR_lookback = 63
    context.SD_factor = 0

    # position only changed if percentage change > threshold
    context.threshold = 0.01

    # the following can be changed
    context.market_proxy = symbol('SPY')
    context.risk_free = symbol('SHY')

    set_commission(commission.PerTrade(cost=10.0))
    context.leverage = 1.0

    # parameters for rebalance period
    context.rebalance_period = 'ME'  # 'D'|'WS'|'WE'|'MS'|'ME'|'A'
    context.days_offset = 2
    context.on_open = True  # if false, then market_close
    context.hours = 0
    context.minutes = 1

    context.rebalance_interval = 1  # rebalancing will occur every balance_interval * balance_period


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def set_strategy_parameters(context):
    # If not required, parameters may be omitted
    # no need to comment out unused strategies
    # strategies used by the algo selected in set_algo_parameters

    # configure strategies below
    # ####################################################################################################
    # single RS portfolio with downside protection
    s1 = StrategyParameters(context, ID='s1',
                            portfolios=[symbols('IVV', 'IJH', 'IJR', 'VEA',
                                                'VWO', 'VNQ', 'AGG')],
                            portfolio_allocation_modes=['EW'],
                            security_scoring_methods=['RS'],
                            security_scoring_factors=[{'+momentum': 1.0}],
                            security_n_tops=[2],
                            protection_modes=['BY_RULE'],
                            protection_rules=['smma_rule'],
                            cash_proxies=[symbol('IEF')],
                            strategy_allocation_mode='EW',
                            )
    # ####################################################################################################
    # RAA - Robust Asset Allocation (4 portfolios
    # s2 = StrategyParameters(context, ID='s2',
    #                  portfolios=[symbols('MDY', 'EFA'), symbols('VNQ', 'RWX'),
    #                              symbols('GLD', 'AGG'),
    #                              symbols('EDV', 'EMB')],
    #                  portfolio_allocation_modes=['EW', 'EW', 'EW', 'EW'],
    #                  security_scoring_methods=['RS', 'RS', 'RS', 'RS'],
    #                  security_scoring_factors=[{'+EMOM':1.}, {'+EMOM':1.},
    #                                            {'+EMOM':1.}, {'+EMOM':1.}],
    #                  security_n_tops=[1, 1, 1, 1],
    #                  protection_modes=['RAA', 'RAA', 'RAA', 'RAA'],
    #                  cash_proxies=[symbol('TLT'), symbol('TLT'), symbol('TLT'), symbol('TLT')],
    #                  strategy_allocation_mode='MAX_SHARPE',
    #                  strategy_allocation_kwargs={'lookback': 21, 'shorts': False},
    #                  )
    # ####################################################################################################
    # Strategy 3 - minimumn correlation strategy
    # s3 = StrategyParameters(context, ID='s3',
    #                 portfolios=[symbols( 'IVV', 'IJH', 'IJR', 'VEA',
    #                                     'VWO', 'VNQ', 'AGG')],
    #                 portfolio_allocation_modes=['MIN_CORRELATION'],
    #                 portfolio_allocation_kwargs=[{'lookback': 21, 'risk_adjusted': True}],
    #                 protection_modes=['BY_RULE'],
    #                 protection_rules=['smma_rule'],
    #                 protection_formulas=None,
    #                 cash_proxies=[symbol('SHY')],
    #                 strategy_allocation_mode='EW'
    #                )
    # ####################################################################################################
    # sdp_1 - downside protection strategy based on Alpha Architect DPM Rule: 50% TMOM, 50% MA
    # http://blog.alphaarchitect.com/2015/08/13/avoiding-the-big-drawdown-downside-protection-investment-strategies/#gs.qtrlStY
    # sdp_1 = StrategyParameters(context, ID='sdp_1',
    #                  portfolios=[symbols( 'XLY', 'XLF', 'XLK', 'XLE', 'XLV',  'XLI',
    #                                      'XLP', 'XLB', 'XLU')],
    #                  portfolio_allocation_modes=['EW'],
    #                  protection_modes=['RAA'],
    #                  # protection_modes=['BY_RULE'],
    #                  # protection_rules=['smma_rule'],
    #                  # protection_rules=['momentum_rule'],
    #                  cash_proxies=[symbol('SHY')],
    #                 strategy_allocation_mode='EW'
    #                 )
    # ####################################################################################################
    # RS with downside protection, single portfolio, EtfReplay-like ranking formula
    # rs_1 = StrategyParameters(context, ID='rs_1',
    #                 portfolios=[symbols( 'MDY', 'EFA')],
    #                 portfolio_allocation_modes=['EW'],
    #                 security_scoring_methods=['RS'],
    #                 security_scoring_factors=[{'+mom_A': 0.65, '+mom_B' : 0.35, '-vol_C' : 0.}],
    #                 security_n_tops=[1],
    #                 protection_modes=['BY_RULE'],
    #                 protection_rules=['smma_rule'],
    #                 cash_proxies=[symbol('TLT')],
    #                 strategy_allocation_mode='EW')
    # ####################################################################################################
    # RS with 2 portfolios based on EtfReplay ranking model
    # rs_2 = StrategyParameters(context, ID='rs_2',
    #                 portfolios=[symbols( 'MDY', 'EFA'), symbols('IHF', 'EFA')],
    #                 portfolio_allocation_modes=['EW', 'FIXED'],
    #                 security_weights=[None, [0.8, 0.2]],
    #                 security_scoring_methods=['RS', 'RS'],
    #                 security_scoring_factors=[{'+mom_A': 0.65, '+mom_B' : 0.35, '-vol_C' : 0.},
    #                                           {'+mom_A': 0.65, '+mom_B' : 0.35, '-vol_C' : 0.}],
    #                 security_n_tops=[1, 2],
    #                 protection_modes=['BY_RULE', 'BY_RULE'],
    #                 protection_rules=['smma_rule', 'smma_rule'],
    #                 cash_proxies=[symbol('TLT'), symbol('TLT')],
    #                 strategy_allocation_mode='FIXED',
    #                 portfolio_weights=[0.6, 0.4]
    #                )
    # ####################################################################################################
    # EAA - Elastic Asset Allocation
    # http://indexswingtrader.blogspot.co.za/2015/01/a-primer-on-elastic-asset-allocation.html
    # eaa_1 = StrategyParameters (context, ID='eaa_1',
    #                 portfolios=[symbols('EEM', 'IEF', 'IEV', 'MDY', 'QQQ', 'TLT', 'XLV')],
    #                 portfolio_allocation_modes=['PROPORTIONAL'],
    #                 security_scoring_methods=['EAA'],
    #                 # Golden Defensive EAA: wi ~ zi = squareroot( ri * (1-ci) )
    #                 security_scoring_factors = [{'R': 1.0, 'C' : 1.0, 'V' : 0.0, 'S' : 0.5, 'eps' : 1e-6}],
    #                 protection_modes=['BY_FORMULA'], protection_rules=['EAA_rule'],
    #                 protection_formulas=['DPF'], cash_proxies=[symbol('TLT')], strategy_allocation_mode='EW')
    # ####################################################################################################
    # Risk_on Risk_off
    # roo_1 = StrategyParameters(context, ID='roo_1',
    #                  portfolios=[symbols('SPY', 'QQQ', 'IWM', 'VGK', 'EWJ', 'EEM',
    #                                      'IYR', 'GSG', 'GLD'), symbols('TLT', 'TIP', 'LQD', 'SHY')],
    #                  portfolio_allocation_modes=['EW', 'EW'],
    #                  security_scoring_methods=['RS', 'RS'],
    #                  security_scoring_factors=[{'+smma': 1}, {'+smma': 1}],
    #                  security_n_tops=[3, 1],
    #                  protection_modes=['BY_RULE', None],
    #                  protection_rules=['momentum_rule1', None],
    #                  cash_proxies=[symbol('IEF'), symbol('SHY')], strategy_allocation_mode='EW')
    #####################################################################################################
    # Adaptive Asset Allocation
    # aaa_1 = StrategyParameters(context, ID='aaa_1',
    #                 portfolios=[symbols( 'SPY', 'IWM', 'EFA', 'EEM', 'VNQ', 'GLD', 'GSG',
    #                                     'JNK', 'AGG', 'TIP', 'IEF', 'TLT')],
    #                 portfolio_allocation_modes=['VOLATILITY_WEIGHTED'],
    #                 security_scoring_methods=['RS'],
    #                 security_scoring_factors=[{'+mom': 1.0}],
    #                 security_n_tops=[3],
    #                 protection_modes=['BY_RULE'],
    #                 protection_rules=['smma_rule'],
    #                 cash_proxies=[symbol('TLT')],
    #                 strategy_allocation_mode='EW')
    ####################################################################################################
    # Protective Asset Allocation
    # http://indexswingtrader.blogspot.co.za/2016/04/introducing-protective-asset-allocation.html
    # paa_1 = StrategyParameters(context, ID='paa_1',
    #                  portfolios=[symbols('SPY', 'QQQ', 'IWM', 'VGK', 'EWJ', 'EEM',
    #                                      'IYR', 'GSG', 'GLD', 'LQD', 'TLT', 'HYG'),
    #                              symbols('IEF', 'TLT')],
    #                  portfolio_allocation_modes=['EW', 'EW'],
    #                  security_scoring_methods=['RS', 'RS'],
    #                  security_scoring_factors=[{'+mom': 1}, {'+mom': 1}],
    #                  security_n_tops=[3, 1],
    #                  protection_modes=['BY_RULE', None],
    #                  protection_rules=['paa_rule', None],
    #                  cash_proxies=[symbol('TLT'), symbol('TLT')],
    #                  strategy_allocation_mode='BY_FORMULA',
    #                  strategy_allocation_formula='PAA',
    #                  strategy_allocation_rule='paa_filter',
    #                  strategy_allocation_kwargs={'protection_factor': 1})
    ####################################################################################################
    # Bond Rotation Strategy
    # brs_1 = StrategyParameters(context, ID='brs_1',
    #                 portfolios=[symbols('CWB', 'JNK'), symbols('CWB', 'JNK'), symbols('CWB', 'JNK'),
    #                             symbols('CWB', 'PCY'), symbols('CWB', 'PCY'), symbols('CWB', 'PCY'),
    #                             symbols('CWB', 'TLT'), symbols('CWB', 'TLT'), symbols('CWB', 'TLT'),
    #                             symbols('JNK', 'PCY'), symbols('JNK', 'PCY'), symbols('JNK', 'PCY'),
    #                             symbols('JNK', 'TLT'), symbols('JNK', 'TLT'), symbols('JNK', 'TLT'),
    #                             symbols('PCY', 'TLT'), symbols('PCY', 'TLT'), symbols('PCY', 'TLT')],
    #                 portfolio_allocation_modes=['FIXED', 'FIXED', 'FIXED',
    #                                             'FIXED', 'FIXED', 'FIXED',
    #                                             'FIXED', 'FIXED', 'FIXED',
    #                                             'FIXED', 'FIXED', 'FIXED',
    #                                             'FIXED', 'FIXED', 'FIXED',
    #                                             'FIXED', 'FIXED', 'FIXED'],
    #                 security_weights=[[0.6, 0.4], [0.5, 0.5], [0.4, 0.6],
    #                                   [0.6, 0.4], [0.5, 0.5], [0.4, 0.6],
    #                                   [0.6, 0.4], [0.5, 0.5], [0.4, 0.6],
    #                                   [0.6, 0.4], [0.5, 0.5], [0.4, 0.6],
    #                                   [0.6, 0.4], [0.5, 0.5], [0.4, 0.6],
    #                                  [0.6, 0.4], [0.5, 0.5], [0.4, 0.6]],
    #                 cash_proxies=[symbol('TLT'), symbol('TLT'), symbol('TLT'),
    #                               symbol('TLT'), symbol('TLT'), symbol('TLT'),
    #                               symbol('TLT'), symbol('TLT'), symbol('TLT'),
    #                               symbol('TLT'), symbol('TLT'), symbol('TLT'),
    #                               symbol('TLT'), symbol('TLT'), symbol('TLT'),
    #                               symbol('TLT'), symbol('TLT'), symbol('TLT')],
    #                 strategy_allocation_mode='BRUTE_FORCE_SHARPE',
    #                 strategy_allocation_kwargs={'lookback' : 73})
    #####################################################################################################
    # brs_2 = StrategyParameters(context, ID='brs_2',
    #                 portfolios=[symbols('CWB', 'JNK'), symbols('CWB', 'PCY'), symbols('CWB', 'TLT'),
    #                             symbols('JNK', 'PCY'), symbols('JNK', 'TLT'), symbols('PCY', 'TLT')],
    #                 portfolio_allocation_modes=['MAX_SHARPE', 'MAX_SHARPE', 'MAX_SHARPE',
    #                                             'MAX_SHARPE', 'MAX_SHARPE', 'MAX_SHARPE'],
    #                 portfolio_allocation_kwargs=[
    #                            {'lookback' : 73, 'shorts' : False},{'lookback' : 73, 'shorts' : False},
    #                            {'lookback' : 73, 'shorts' : False},{'lookback' : 73, 'shorts' : False},
    #                            {'lookback' : 73, 'shorts' : False},{'lookback' : 73, 'shorts' : False}],
    #                 cash_proxies=[symbol('TLT'), symbol('TLT'), symbol('TLT'),
    #                               symbol('TLT'), symbol('TLT'), symbol('TLT')],
    #                 strategy_allocation_mode='BRUTE_FORCE_SHARPE',
    #                 strategy_allocation_kwargs={'lookback' : 73, 'SD_factor' : 2})
    ####################################################################################################
    # context.strategy_parameters = [s1, s2, s3, sdp_1, rs_1, rs_2, eaa_1, roo_1, aaa_1, paa_1, brs_1, brs_2]
    context.strategy_parameters = [s1]

    return context.strategy_parameters


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def set_algo_parameters(context, strategies):
    # UNCOMMENT ONLY ONE ALGO BELOW
    ###############################
    # simple downside protection algorithm
    # http://blog.alphaarchitect.com/2015/08/13/avoiding-the-big-drawdown-downside-protection-investment-            strategies/#gs.qtrlStY

    # strategy_ID = 'sdp_1'

    # algo = Algo (context, [s for s in strategies if s.ID == strategy_ID],
    #              allocation_model=AllocationModel(context, mode='EW', weights=None, formula=None),
    #             )
    ###############################
    # EAA - Elastic Asset Allocation
    # http://indexswingtrader.blogspot.co.za/2015/01/a-primer-on-elastic-asset-allocation.html

    # strategy_ID = 'eaa_1'

    # algo = Algo (context, [s for s in strategies if s.ID == strategy_ID],
    #              allocation_model=AllocationModel(context, mode='EW', weights=None, formula=None),
    #             )
    ###############################
    # multiple strategies, equally weighted

    # list of strategies by ID
    # strategy_IDs = ['s1', 's2', 's3', 'sdp_1']

    # algo = Algo (context, strategies=[s for s in strategies if s.ID in strategy_IDs],
    #              allocation_model=AllocationModel(context, mode='EW', weights=None, formula=None),
    #             )
    ###############################
    # run all uncommented strategies (other than regime-switching strategies)
    algo = Algo(context, strategies=[s for s in strategies],
                allocation_model=AllocationModel(context, mode='EW'), scoring_model=None,
                # allocation_model=AllocationModel(context, mode='RISK_PARITY', kwargs={'lookback':21}),     scoring_model=ScoringModel(context, method='RS', factors={'+EMOM':1.}, n_top=1),
                regime=None,
                )
    ########################
    # 2 regimes: riskon riskoff RS ; riskon=market_proxy price > sma, riskoff=market_proxy price <= sma
    # algo = Algo (context, [s for s in strategies if s.ID == 'roo_1'],
    #              allocation_model=AllocationModel(context, mode='REGIME_EW'),
    #              regime=Regime( transitions={'1' : ('riskon', ['roo_1_p1']),
    #                                          '0' : ('riskoff', ['roo_1_p2']),
    #                                   }
    #                            )
    #             )
    ########################
    # 3 regimes : 'bull', 'bear', 'neutral'
    # strategy_IDs = ['rs_2', 'eaa_1']
    # algo = Algo (context, strategies = [s for s in strategies if s.ID in strategy_IDs],
    #              allocation_model=AllocationModel(context, mode='REGIME_EW', weights=None, formula=None),
    #              regime=Regime(
    #                                   transitions={'0' : ('neutral', ['eaa_1']),
    #                                   '1' : ('bull', ['rs_2_p1']),
    #                                   '-1' : ('bear', ['rs_2_p2', 'eaa_1'])
    #                                          }
    #                                  )
    #             )
    ############################
    # AAA - Adaptive Asset Allocation
    # http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2359011

    # algo = Algo (context, strategies = [s for s in strategies if s.ID == 'aaa_1']
    #              allocation_model=AllocationModel(context, mode='EW'),
    #             )
    ############################
    # PAA - Protective Asset Allocation
    # http://indexswingtrader.blogspot.co.za/2016/04/introducing-protective-asset-allocation.html
    # algo = Algo (context, strategies = [s for s in strategies if s.ID == 'paa_1'],
    #              allocation_model=AllocationModel(context, mode='EW'),
    #             )
    ############################
    # BRS - Bond Rotation Strategy
    # https://logical-invest.com/portfolio-items/bond-rotation-sleep-well/
    # https://www.quantopian.com/posts/the-logical-invest-enhanced-bond-rotation-strategy

    # Algo-specific parameters
    # context.calculate_SR = True
    # context.SR_lookback = 73
    # context.SD_factor = 2
    # algo = Algo (context, strategies = [s for s in strategies if s.ID == 'brs_1'],
    #              allocation_model=AllocationModel(context, mode='EW'),
    #             )
    ###############################

    return algo


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
############################
# TODO
############################
'''
TODO:

1. Clean up the configurator - add validation to classes?
2. clean up transforms and rules 
3. add signal at portfolio level?
4. add rebalance period params to set_globa_params so need to change initaliaze
5. how to set GTC_LIMIT as global param
6. write tests!
7. Documentation
8. Fix apply_pandas_function (rolling, expanding)
9. Port the best algos to this template

11. move MODE IMPLEMENTATION ERROR OR DOWNSIDE PROTECTION MODE to give more meaningful error message
12. n_period_return - need to return excess_return, depending on risk_free

14. If rebalance period = 'A' must check that the necessary transforms and rules are present
15. another brute force SR idea - same portfolios as for brs_1, determine max_sharpe allocations for each portfolio, and calculate the SR for each. then select the portfolio  with the highest SR (brs_1 only uses very few fixed weights)
16. test for kwargs except EW, PROPORTIONAL, 
17. test StrategyPrarmeters.ID
18. Algo - check that there are valid parameters for all strategies
19. if mode = FIXED, no of weights must = no of securities

'''
############################