text ZIPLINE MSMP 6.00

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了text ZIPLINE 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])

以上是关于text ZIPLINE MSMP 6.00的主要内容,如果未能解决你的问题,请参考以下文章

zipline

Zipline Beginner Tutorial

zipline-- 开发指南

zipline install instruction

自建zipline的databundle

python ZIPLINE RUN SIMPLE PIPELINE