Replica of TradingView's Backtesting Engine with Arrays
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © OztheWoz

//@version=5
library("OzzyBacktestEngine")

// quality of life user function that help when working with array data usage
_indexArray(_array, _index) =>
    array.size(_array) > _index ? _index : na

_clearArray(_array, _oversize) =>
    if array.size(_array) >= _oversize
        array.pop(_array)


// _backtest() function
export _backtest(bool _enter, bool _exit, float _startQty, float _tradeQty) =>
    var profit_array        = array.new_float()
    var percentProfit_array = array.new_float()
    var entryPrice_array    = array.new_float()
    var entryQty_array      = array.new_float()
    var exitPrice_array     = array.new_float()
    var tradeReturn_array   = array.new_float()
    var trade      = _tradeQty / close
    var tradeAmt          = 0
    var tradeProfitable   = 0
    var grossProfit       = 0.0
    var grossLoss         = 0.0
    var netProfit         = 0.0
    var netPercentProfit  = 0.0
    var percentProfitable = 0.0
    var profitFactor      = 0.0
    var entryPrice        = 0.0
    var entryQty          = 0.0
    var exitPrice         = 0.0
    var tradeOpen         = 0
    var maxDrawdown       = 0.0
    var maxPercentDrawdown= 0.0
    var avgTrade          = 0.0
    var avgPercentTrade   = 0.0
    
    if barstate.isconfirmed
        if barstate.isfirst
            array.unshift(profit_array, 0)
    
        if _enter[1] and (tradeOpen == 0)
            tradeOpen := 1
            array.unshift(entryPrice_array, open)
            entryPrice := array.get(entryPrice_array, _indexArray(entryPrice_array, 0))
            array.unshift(entryQty_array,   _tradeQty/entryPrice)
            entryQty   := array.get(entryQty_array,   _indexArray(entryQty_array,   0))
            netProfit := netProfit
            
            _clearArray(entryPrice_array, 3)
            _clearArray(entryQty_array, 3)
    
        if _exit[1] and (tradeOpen == 1)
            tradeOpen := 0
            tradeAmt += 1
            array.unshift(exitPrice_array, open)
            exitPrice := array.get(exitPrice_array, _indexArray(exitPrice_array, 0))
            _clearArray(exitPrice_array, 3)
            tradeReturn = ((entryQty * exitPrice) - (entryPrice * entryQty))
            netProfit += tradeReturn
            netPercentProfit := (((_startQty + netProfit) - _startQty) / _startQty) * 100
            array.unshift(tradeReturn_array, tradeReturn)
            array.unshift(profit_array, netProfit)
            array.unshift(percentProfit_array, netPercentProfit)
            curr_net = array.get(profit_array, _indexArray(profit_array, 0))
            last_net = array.get(profit_array, _indexArray(profit_array, 1))
            
            
            if curr_net > last_net
                tradeProfitable += 1
                grossProfit += math.abs(tradeReturn)
            if curr_net <= last_net
                grossLoss += math.abs(tradeReturn)
            
            percentProfitable := (tradeProfitable / tradeAmt) * 100
            profitFactor := grossProfit / grossLoss
            
            // Max Drawdown
            indexMaxProfit = array.indexof(profit_array, array.max(profit_array, 0))
            var maxProfit = 0.0
            maxProfit := array.size(profit_array) > 2 ? array.get(profit_array, indexMaxProfit > 0 ? indexMaxProfit : na) : array.get(profit_array, _indexArray(profit_array, 0))
            drawdown = maxProfit - curr_net
            var drawdown_array = array.new_float()
            array.unshift(drawdown_array, drawdown)
            indexMaxDrawdown = array.indexof(drawdown_array, array.max(drawdown_array, 0))
            maxDrawdown := array.get(drawdown_array, indexMaxDrawdown >= 0 ? indexMaxDrawdown : na)
            maxPercentDrawdown := (maxDrawdown / (_startQty + array.get(profit_array, indexMaxDrawdown))) * 100
            // "THE PERCENTAGE AND ABBSOLUTE VALUES OF A DRAWDOWN ARE TWO DIFFERENT METRICS. THEY ARE TRICKED INDEPENDENTLY."
            // This takes the percent of the current Max Drawdown, not the max percent drawdown
            
            // Average Trade
            avgTrade := array.avg(tradeReturn_array)
            avgPercentTrade := (avgTrade / _startQty) * 100
            
    
    [netProfit, netPercentProfit, tradeAmt, percentProfitable, profitFactor, maxDrawdown, maxPercentDrawdown, avgTrade, avgPercentTrade]



////////////////////////////////////////////
////  DEMO STRATEGY (PLOTTED ON CHART)  ////
////////////////////////////////////////////


// //@version=5
// strategy("DEMO STRATEGY", overlay=true, default_qty_type = strategy.cash, default_qty_value = 1000, initial_capital = 10000)

// sma_fast = ta.sma(close, 9)
// sma_slow = ta.sma(close, 26)
// enterLong = ta.crossover(sma_fast, sma_slow)
// exitLong = ta.crossunder(sma_fast, sma_slow)

// if enterLong
//     strategy.entry("Long", strategy.long)
// if exitLong
//     strategy.close("Long")



sma_fast = ta.sma(close, 9)
sma_slow = ta.sma(close, 26)
enterLong = ta.crossover(sma_fast, sma_slow)
exitLong = ta.crossunder(sma_fast, sma_slow)

plot(sma_fast, color=color.lime)
plot(sma_slow, color=color.blue, linewidth=2)


// example of _backtest() function being used
[netProfit, netPercentProfit, tradeAmt, percentProfitable, profitFactor, maxDrawdown, maxPercentDrawdown, avgTrade, avgPercentTrade] = _backtest(enterLong, exitLong, 10000, 1000)


// table showing data
backtest_table = table.new(position.top_right, 7, 2, bgcolor=color.new(#CFCFCF, 80), border_color=na, border_width=1)
_addCell(_table) =>
    table.cell(_table, 1, 0, text="Net Profit", text_color=color.white)
    table.cell(_table, 2, 0, text="Total Trades", text_color=color.white)
    table.cell(_table, 3, 0, text="Percent of Trades \nProfitable", text_color=color.white)
    table.cell(_table, 4, 0, text="Profit Factor", text_color=color.white)
    table.cell(_table, 5, 0, text="Max Drawdown", text_color=color.white)
    table.cell(_table, 6, 0, text="Average Trade", text_color=color.white)
    
    table.cell(_table, 1, 1, text="$" + str.tostring(math.round(netProfit, 2)) + "\n" + str.tostring(math.round(netPercentProfit, 2)) + "%", text_color=color.white)
    table.cell(_table, 2, 1, text=str.tostring(math.round(tradeAmt)), text_color=color.white)
    table.cell(_table, 3, 1, text=str.tostring(math.round(percentProfitable, 2)) + "%", text_color=color.white)
    table.cell(_table, 4, 1, text=str.tostring(math.round(profitFactor, 3)), text_color=color.white)
    table.cell(_table, 5, 1, text=str.tostring(math.round(maxDrawdown, 2)) + "\n" + str.tostring(math.round(maxPercentDrawdown, 2)) + "%", text_color=color.white)
    table.cell(_table, 6, 1, text=str.tostring(math.round(avgTrade, 2)) + "\n" + str.tostring(math.round(avgPercentTrade, 2)) + "%", text_color=color.white)

_addCell(backtest_table)
