Moving Regression Band Breakout strategy
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © tbiktag
//
// Moving Regression Band Breakout strategy
//
// The strategy based on the Moving Regression Prediction Bands indicator:
//
// https://www.tradingview.com/script/zOaMXJ65-Moving-Regression-Prediction-Bands/
//
// Entry condition: 
//    Long: the price crosses the Upper Band from below. 
//    Short: the price crosses the Lower Band from above. 
// Exit condition Long: 
//    the price crosses the Lower Band (the Lower Band - ATR, or Central Line)
// Exit condition Short: 
//    the price crosses the Upper Band (the Upper Band + ATR, or Central Line)
//
//
//@version=4
strategy("Moving Regression Band Breakout strategy", shorttitle = "MRBand Strat", overlay=true)

matrix_get(_A,_i,_j,_nrows) =>
    // Get the value of the element of an implied 2d matrix
    //input: 
    // _A :: array: pseudo 2d matrix _A = [[column_0],[column_1],...,[column_(n-1)]]
    // _i :: integer: row number
    // _j :: integer: column number
    // _nrows :: integer: number of rows in the implied 2d matrix
    array.get(_A,_i+_nrows*_j)

matrix_set(_A,_value,_i,_j,_nrows) =>
    // Set a value to the element of an implied 2d matrix
    //input: 
    // _A :: array, changed on output: pseudo 2d matrix _A = [[column_0],[column_1],...,[column_(n-1)]]
    // _value :: float: the new value to be set
    // _i :: integer: row number
    // _j :: integer: column number
    // _nrows :: integer: number of rows in the implied 2d matrix
    array.set(_A,_i+_nrows*_j,_value)

transpose(_A,_nrows,_ncolumns) =>
    // Transpose an implied 2d matrix
    // input:
    // _A :: array: pseudo 2d matrix _A = [[column_0],[column_1],...,[column_(n-1)]]
    // _nrows :: integer: number of rows in _A
    // _ncolumns :: integer: number of columns in _A
    // output:
    // _AT :: array: pseudo 2d matrix with implied dimensions: _ncolums x _nrows
    var _AT = array.new_float(_nrows*_ncolumns,0)
    for i = 0 to _nrows-1
        for j = 0 to _ncolumns-1
            matrix_set(_AT, matrix_get(_A,i,j,_nrows),j,i,_ncolumns)
    _AT

multiply(_A,_B,_nrowsA,_ncolumnsA,_ncolumnsB) => 
    // Calculate scalar product of two matrices
    // input: 
    // _A :: array: pseudo 2d matrix
    // _B :: array: pseudo 2d matrix
    // _nrowsA :: integer: number of rows in _A
    // _ncolumnsA :: integer: number of columns in _A
    // _ncolumnsB :: integer: number of columns in _B
    // output:
    // _C:: array: pseudo 2d matrix with implied dimensions _nrowsA x _ncolumnsB
    var _C = array.new_float(_nrowsA*_ncolumnsB,0)
    int _nrowsB = _ncolumnsA
    float elementC= 0.0
    for i = 0 to _nrowsA-1
        for j = 0 to _ncolumnsB-1
            elementC := 0
            for k = 0 to _ncolumnsA-1
                elementC := elementC + matrix_get(_A,i,k,_nrowsA)*matrix_get(_B,k,j,_nrowsB)
            matrix_set(_C,elementC,i,j,_nrowsA)
    _C

vnorm(_X,_n) =>
    //Square norm of vector _X with size _n
    float _norm = 0.0
    for i = 0 to _n-1
        _norm := _norm + pow(array.get(_X,i),2)
    sqrt(_norm)

qr_diag(_A,_nrows,_ncolumns) => 
    //QR Decomposition with Modified Gram-Schmidt Algorithm (Column-Oriented)
    // input:
    // _A :: array: pseudo 2d matrix _A = [[column_0],[column_1],...,[column_(n-1)]]
    // _nrows :: integer: number of rows in _A
    // _ncolumns :: integer: number of columns in _A
    // output:
    // _Q: unitary matrix, implied dimenstions _nrows x _ncolumns
    // _R: upper triangular matrix, implied dimansions _ncolumns x _ncolumns
    var _Q = array.new_float(_nrows*_ncolumns,0)
    var _R = array.new_float(_ncolumns*_ncolumns,0)
    var _a = array.new_float(_nrows,0)
    var _q = array.new_float(_nrows,0)
    float _r = 0.0
    float _aux = 0.0
    //get first column of _A and its norm:
    for i = 0 to _nrows-1
        array.set(_a,i,matrix_get(_A,i,0,_nrows))
    _r := vnorm(_a,_nrows)
    //assign first diagonal element of R and first column of Q
    matrix_set(_R,_r,0,0,_ncolumns)
    for i = 0 to _nrows-1
        matrix_set(_Q,array.get(_a,i)/_r,i,0,_nrows)
    if _ncolumns != 1
        //repeat for the rest of the columns
        for k = 1 to _ncolumns-1
            for i = 0 to _nrows-1
                array.set(_a,i,matrix_get(_A,i,k,_nrows))
            for j = 0 to k-1
                //get R_jk as scalar product of Q_j column and A_k column:
                _r := 0
                for i = 0 to _nrows-1
                    _r := _r + matrix_get(_Q,i,j,_nrows)*array.get(_a,i)
                matrix_set(_R,_r,j,k,_ncolumns)
                //update vector _a
                for i = 0 to _nrows-1
                    _aux := array.get(_a,i) - _r*matrix_get(_Q,i,j,_nrows)
                    array.set(_a,i,_aux)
            //get diagonal R_kk and Q_k column
            _r := vnorm(_a,_nrows)
            matrix_set(_R,_r,k,k,_ncolumns)
            for i = 0 to _nrows-1
                matrix_set(_Q,array.get(_a,i)/_r,i,k,_nrows)
    [_Q,_R]

pinv(_A,_nrows,_ncolumns) =>
//Pseudoinverse of matrix _A calculated using QR decomposition
// Input: 
// _A:: array: implied as a (_nrows x _ncolumns) matrix _A = [[column_0],[column_1],...,[column_(_ncolumns-1)]]
// Output: 
// _Ainv:: array implied as a (_ncolumns x _nrows) matrix _A = [[row_0],[row_1],...,[row_(_nrows-1)]]
// ----
// First find the QR factorization of A: A = QR,
// where R is upper triangular matrix.
// Then _Ainv = R^-1*Q^T.
// ----
    [_Q,_R] = qr_diag(_A,_nrows,_ncolumns)
    _QT = transpose(_Q,_nrows,_ncolumns)
    // Calculate Rinv:
    var _Rinv = array.new_float(_ncolumns*_ncolumns,0)
    float _r = 0.0
    matrix_set(_Rinv,1/matrix_get(_R,0,0,_ncolumns),0,0,_ncolumns)
    if _ncolumns != 1
        for j = 1 to _ncolumns-1
            for i = 0 to j-1
                _r := 0.0
                for k = i to j-1
                    _r := _r + matrix_get(_Rinv,i,k,_ncolumns)*matrix_get(_R,k,j,_ncolumns)
                matrix_set(_Rinv,_r,i,j,_ncolumns)
            for k = 0 to j-1
                matrix_set(_Rinv,-matrix_get(_Rinv,k,j,_ncolumns)/matrix_get(_R,j,j,_ncolumns),k,j,_ncolumns)
            matrix_set(_Rinv,1/matrix_get(_R,j,j,_ncolumns),j,j,_ncolumns)
    //
    _Ainv = multiply(_Rinv,_QT,_ncolumns,_ncolumns,_nrows)
    _Ainv

mae(_x, _xhat) =>
    // Mean Average Error
    // _x.   :: array float, original data
    // _xhat :: array float, model estimate
    // output
    // _nrmse:: float
    float _mae = 0.0
    if array.size(_x) != array.size(_xhat)
        _mae := na
    else
        _N = array.size(_x)
        for i = 0 to _N-1
            _mae := _mae + abs(array.get(_x,i) - array.get(_xhat,i))/_N
    _mae

mr(_src,_window,_degree) =>
    // Vandermonde matrix with implied dimensions (window x degree+1)
    // Linear form: J = [ [z]^0, [z]^1, ... [z]^degree], with z = [ (1-window)/2 to (window-1)/2 ] 
    var _J = array.new_float(_window*(_degree+1),0)
    for i = 0 to _window-1 
        for j = 0 to _degree
            matrix_set(_J,pow(i,j),i,j,_window)
    // Vector of raw datapoints:
    var _Y_raw = array.new_float(_window,na)
    for j = 0 to _window-1
        array.set(_Y_raw,j,_src[_window-1-j]) 
    // Calculate polynomial coefficients which minimize the loss function
    _C = pinv(_J,_window,_degree+1)
    _a_coef = multiply(_C,_Y_raw,_degree+1,_window,1)
    // For smoothing, approximate the last point (i.e. z=window-1) by a0
    float _Y = 0.0
    for i = 0 to _degree
        _Y := _Y + array.get(_a_coef,i)*pow(_window-1,i)
    // Trend Direction Forecast
    float _Y_f = 0.0
    for i = 0 to _degree
        _Y_f := _Y_f + array.get(_a_coef,i)*pow(_window,i)
    // Calculates data estimate (needed for rmse)
    _Y_hat = multiply(_J,_a_coef,_window,_degree+1,1)
    float _err = mae(_Y_raw,_Y_hat)
    [_Y,_Y_f,_err]

/// --- main ---
src = input(title="Source", defval=close, group = "Model Parameters:")
degree = input(title="Local Polynomial Degree", type = input.integer, defval=2, minval = 0,  group = "Model Parameters:")
window = input(title="Length (must be larger than degree)", type = input.integer, defval=80, minval = 2,  group = "Model Parameters:")
mult = input(title="Multiplier", type=input.float,defval=2.0,minval=0.0,
             group = "Model Parameters:", tooltip = "Defines the Band Width.")
doLong = input(title="Allow Long Entries", type=input.bool,defval=true, inline = "linealwd", group = "Allowed Entries:")
doShort = input(title="Allow Short Entries", type=input.bool,defval=false, inline = "linealwd", group = "Allowed Entries:")
stoplong = input(title="Exit Long At", defval="Lower Band - ATR", 
                   options = ["Lower Band","Lower Band - ATR","Central Line"],
                   inline = "lineexit", group = "Exit Conditions:")
stopshort = input(title="Exit Short At", defval="Central Line", 
                   options = ["Upper Band","Upper Band + ATR","Central Line"],
                   inline = "lineexit", group = "Exit Conditions:")
fixedstart =input(title="", group = "Fixed Backtest Period Start/End Dates:",
              inline = "linebac1", type = input.bool, defval = true)
backtest_start=input(title = "", type = input.time, inline = "linebac1", 
              group = "Fixed Backtest Period Start/End Dates:",
              defval = timestamp("01 Jan 2017 13:30 +0000"),
              tooltip="If deactivated, backtest staring from the first available price bar.")
fixedend =  input(title="", group = "Fixed Backtest Period Start/End Dates:",
              inline = "linebac2", type = input.bool, defval = false)
backtest_end =input(title = "", type = input.time, inline = "linebac2", 
              group = "Fixed Backtest Period Start/End Dates:",
              defval = timestamp("30 Dec 2080 23:30 +0000"),
              tooltip="If deactivated, backtesting ends at the last available price bar.")
istoploss = input(title="Show Stop-Loss Line", type=input.bool,defval=true, group="Additional Options")
slopefilter = input(title="Enter Only When MR Slope is Positive/Negative", type=input.bool,defval=false, group="Additional Options")
issignal = input(title="Show Breakout Signals Labels", type=input.bool,defval=false, group="Additional Options")
clinetype = input(title="Show Central Line As", defval="Previous-Period MR Prediction", 
                  options = ["Ribbon","MR","Previous-Period MR Prediction"], group="Additional Options")

[MR,MR_f,div] = mr(src,window,degree)
div := div*mult



// plot bands
highband = MR_f[1]+div[1]
lowband = MR_f[1]-div[1]
centralline = MR_f[1]
hbplt = plot(highband,title='Upper Band',color=#3C94B8,linewidth=2, transp = 40)
lbplt = plot(lowband,title='Lower Band',color=#B83C94,linewidth=2, transp = 40)
fill(hbplt,lbplt,color=#3C94B8)

// plot central line
var plt_color = #3C94B8
if MR_f[1] < MR and clinetype=="Ribbon"
    plt_color := #94B83C
else if MR_f[1] > MR and clinetype=="Ribbon"
    plt_color := #B83C71
mrplt = plot(MR,title='MR',color=plt_color,linewidth=1,
             transp = clinetype=="Previous-Period MR Prediction"?100:20)
mrfplt = plot(centralline,title='MR Prediction',color=plt_color,linewidth=1, 
             transp = clinetype=="MR"?100:20)
fill(mrplt,mrfplt,color=plt_color,transp = clinetype!="Ribbon"?100:20)


//  breakout signals
ATR = atr(14)
Breakout = crossover(src,highband)
Breakdown = crossunder(src,lowband)
ATRBreakout = crossover(src,highband + ATR)
ATRBreakdown = crossunder(src,lowband - ATR)
MRSlopesUp = MR>MR[1]
MRSlopesDw = MR<MR[1]
plotshape(Breakout and issignal ? high : na, location=location.abovebar, 
          style=shape.triangleup, color=#88d4ca, size=size.tiny, title='Breakout')
plotshape(Breakdown and issignal? low : na, location=location.belowbar, 
          style=shape.triangledown, color=#f67e7d, size=size.tiny,title='Breakdown')
goLong = Breakout and (slopefilter?MRSlopesUp:true)
goShort = Breakdown and (slopefilter?MRSlopesDw:true)


// trailing stop / stop loss
trailinglong = stoplong=="Central Line"?centralline:(stoplong=="Lower Band"?lowband:lowband-ATR)
trailingshort= stopshort=="Central Line"?centralline:(stopshort=="Upper Band"?highband:highband+ATR)
// if signal has been triggered at previous bar, set position open price and initial SL
var float opprice = 0.0
var float stoploss = 0.0
if (strategy.position_entry_name[1]=="short" or strategy.position_size[1]==0) and goLong[1] 
    opprice := open
    stoploss := trailinglong
else if (strategy.position_entry_name[1]=="long" or strategy.position_size[1]==0) and goShort[1] 
    opprice := open
    stoploss := trailingshort
// move SL to position opening price if lowband crosses the initial SL
if (strategy.position_entry_name=="long" and crossover(trailinglong, opprice)) or (strategy.position_entry_name=="short" and crossunder(trailingshort, opprice))
    stoploss := opprice
stoplosscolor = strategy.position_entry_name=="long"?#B83C94:(strategy.position_entry_name=="short"?#3C94B8:color.silver)
plot(istoploss?stoploss:na,title='sl',color=stoplosscolor,linewidth=1,transp=50)

endLong = crossunder(src,trailinglong)
endShort = crossover(src,trailingshort)

// backtest
isinrange = (fixedstart?time>=backtest_start:true) and (fixedend?time<=backtest_end:true)

if goLong and isinrange and doLong
    strategy.entry("long",   true)
    alert(syminfo.tickerid+" Long Signal Triggered",alert.freq_once_per_bar_close)
if goShort and isinrange and doShort
    strategy.entry("short",  false)
    alert(syminfo.tickerid+" Short Signal Triggered",alert.freq_once_per_bar_close)
if endLong or crossunder(src,stoploss)
    strategy.close("long")
    alert(syminfo.tickerid+" Exit Signal Triggered for Long Position",alert.freq_once_per_bar_close)
if endShort or crossover(src,stoploss)
    strategy.close("short")
    alert(syminfo.tickerid+" Exit Signal Triggered for Short Position",alert.freq_once_per_bar_close)
if (not isinrange)
    strategy.close_all()



