Overall Statistics
Total Trades
693
Average Win
1.00%
Average Loss
-0.64%
Compounding Annual Return
10.239%
Drawdown
29.400%
Expectancy
0.946
Net Profit
465.713%
Sharpe Ratio
0.854
Probabilistic Sharpe Ratio
18.865%
Loss Rate
24%
Win Rate
76%
Profit-Loss Ratio
1.55
Alpha
0.055
Beta
0.267
Annual Standard Deviation
0.086
Annual Variance
0.007
Information Ratio
0.017
Tracking Error
0.139
Treynor Ratio
0.276
Total Fees
$877.89
Estimated Strategy Capacity
$7300000.00
Lowest Capacity Asset
TLT SGNKIKYGE9NP
#region imports
from AlgorithmImports import *
#endregion
"""
Adaptive Volatility (position sizing)

credit attribution:
    David Varadi
    https://cssanalytics.wordpress.com/2017/11/15/adaptive-volatility/

Aim: Get a better position sizing than  [target_vol / realized_vol_{t-1}]
     (where the realized_vol is calculated over a fixed lookback period, e.g. past 20 days)
     using a more 'adaptive' volatility that varies its lookback period according to market conditions.

The simplest method is to use the R-squared of the regression of prices vs time:
 1. high R-squared indicates a trending market
            -> use short lookback periods to capture sudden changes in volatilities;
 2. low R-squared instead iimplies a rangebound/mean-reverting market
            -> lengthen lookbacks since vol will revert to historical means.

To translate the R_squared value into the alpha for an exponential moving average,
the following exponential function is used (motivation: returns supposed lognormal):

    raw_alpha =  exp[-10. * (1 - R_squared(price vs. time, period=20)]
    alpha = min(raw_alpha, 0.5)

    the 0.5 lower bound effectively  limits the lookback to 3 days, since alpha := 2 / (1 + lookback).

Such a capped aplha is used in an EMA of the squared returns for the past 20 days.

Finally the (theoretical) daily exposure is:

    target_vol / sqrt( EMA_{t-1}(squared rturns, alpha) * 252)

    and target_vol is an annualised target vol, say 20%.

To limit excessive trading, I only rebalace if theoretical exposure changes above a certain threshold (say 5%).

Application hereby:
 long SPY (or similar) with a daily position sizing

A more interesting use of this position sizing scheme is when using algorithms with
long periodical rebalacings, say monthly or quarterly.
"""
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
from scipy.stats import linregress
import decimal as d

class AdaptiveVolatility(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2005, 1, 1)
        self.SetCash(100000)

        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage,
                               AccountType.Margin)

        symbols = [self.AddEquity(ticker, Resolution.Minute).Symbol
            for ticker in ['SPY','TLT']]

        self.SetBenchmark('SPY')

        # schedule: rebalance
        dateRule = self.DateRules.EveryDay(symbols[0])
        self.Schedule.On(dateRule, self.TimeRules.AfterMarketOpen(symbols[0], -90),
                         self.rebalance)
                         
        # schedule: email for recap at around close
        self.Schedule.On(dateRule, self.TimeRules.BeforeMarketClose(symbols[0], 0), 
                         self.JustBeforeMarketClose)

        self.back_period = 21 * 3 + 1     # 3 months
        self.vol_period = 21    # days for calc vol
        self.target_vol = 0.2
        self.lev = 1.5          # max lev from ratio targ_vol / real_vol
        
        self.delta = 0.05       # min rebalancing
        
        self.w = 1. / len(symbols)
        self.x = np.asarray(range(self.vol_period))

    ###################################### 
    def rebalance(self):

        # get all weights
        try:
            pos_sizing = self.pos_sizing() 
        except Exception as e:
            msg = f'Exception: {e}'
            self.Log(msg)
            return

        tot_port = self.Portfolio.TotalPortfolioValue
        
        for symbol, info in pos_sizing.items():
            new_weight = info[0]
            yesterdayClose = info[1]

            security = self.Securities[symbol]
            quantity = security.Holdings.Quantity
            price = security.Price

            if price == 0: price = yesterdayClose

            # gauge if needs to trade (new weight vs. current one > self.delta)
            curr_weight = quantity * price / tot_port
            shall_trade = abs(new_weight - curr_weight) > self.delta

            if shall_trade: 
                # self.SetHoldings(symbol, new_weight)

                delta_shares = int(new_weight * tot_port/ price) - quantity
                self.MarketOnOpenOrder(symbol, delta_shares)

                msg = f"{symbol} -- weight: {new_weight:.2f} (old weight was: {curr_weight:.2f}) -- last price: {price}"
                #self.Log(msg)

    def pos_sizing(self):

        # get daily returns for period = self.back_period
        allPrices = self.History(self.Securities.Keys, self.back_period, Resolution.Daily).close.unstack(level=0)
        
        pos = {}

        # calculate alpha for EWM
        for symbol in self.Securities.Keys:
            prices = allPrices[symbol]
            change = prices.pct_change().dropna()
            last = np.float(prices[-1])
            
            rsq = self.rsquared(self.x, prices[-self.vol_period:])
                
            alpha = min(0.5, np.exp(-10. * (1. - rsq)))
               
            vol = change.ewm(alpha=alpha).std() # alpha = 2/(span+1) = 1-exp(log(0.5)/halflife)
            ann_vol = np.float(vol.tail(1)) * np.sqrt(252)
            
            weight = (self.target_vol / ann_vol).clip(0.0, self.lev)  * self.w  # NB: self.w = 1/no_assets
            pos[symbol] =  (weight, last)

            msg = f"{symbol}: {pos[symbol][0]}, rsqr: {rsq}, alpha: {alpha}, ann_vol = {ann_vol}"
            #self.Log(msg)

        return pos
   
    ###################################### 
    def rsquared(self, x, y):
        # slope, intercept, r_value, p_value, std_err
        _, _, r_value, _, _ = linregress(x, y)
        return r_value**2
    
    
    ###################################### ###################################### 
    def OnMarginCallWarning(self):
        msg = f"{self.Time} : check warning margin call! Fast"
        self.Log(msg)

    ######################################    
    def JustBeforeMarketClose(self):
        msg = f"End of day: {self.Time} \nPortfolio value is {self.Portfolio.TotalPortfolioValue:.2f} and Margin Remaining is: {self.Portfolio.MarginRemaining:.2f}  (Total Holdings Value: {self.Portfolio.TotalHoldingsValue:.2f})"
        self.Log(msg)

    ######################################            
    def OnOrderEvent(self, orderEvent):
        order = self.Transactions.GetOrderById(orderEvent.OrderId)
        self.Log(f"{self.Time}: {order.Type}: {orderEvent}")
        
    ###################################### 
    def TimeIs(self, day, hour, minute):
        return self.Time.day == day and self.Time.hour == hour and self.Time.minute == minute