FunctionKellyCriterion
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © RicardoSantos

//@version=5

// @description Kelly criterion methods.
//  the kelly criterion helps with the decision of how much one should invest in
//  a asset as long as you know the odds and expected return of said asset.
library(title='FunctionKellyCriterion')

// reference:
//      https://quantpedia.com/beware-of-excessive-leverage-introduction-to-kelly-and-optimal-f/?a=6080
//      https://masters.donntu.org/2009/fvti/kravchenko/library/article5.htm
//      https://towardsdatascience.com/python-risk-management-kelly-criterion-526e8fb6d6fd
//      Kelly's Criterion in Portfolio Optimization: A Decoupled Problem: https://arxiv.org/abs/1710.00431

//      https://catalogimages.wiley.com/images/db/pdf/9780470455951.excerpt.pdf
//    The Kelly criterion states that we should bet that fixed fraction of our
//    stake ( f ) that maximizes the growth function G( f ):
//    G( f ) = P ∗ ln(1 + B ∗ f ) + (1 − P) ∗ ln(1 − f ) (1.02)
//    where: f = the optimal fixed fraction
//    P = the probability of a winning bet/trade
//    B = the ratio of amount won on a winning bet to amount lost on
//    a losing bet
//    ln( ) = the natural logarithm function


// @function simplified version of the kelly criterion formula.
// @param win_p float, probability of winning.
// @param rr    float, reward to risk rate.
// @returns 
//      float, optimal fraction to risk.
// usage:
//      simplified(0.55, 1.0)
export simplified (
  float win_p, 
  float rr
  ) => //{
    float _loss_p = 1.0 - win_p
    win_p - (_loss_p / rr)
// example:
// k = simplified(math.sum(close > open ? 1.0 : 0.0, 10) / (10 + 1), 2.0)
plot(
  series = simplified(0.55, 1.0), 
  title  = 'F', 
  color  = color.aqua
  )
// reference:
//      https://quantpedia.com/beware-of-excessive-leverage-introduction-to-kelly-and-optimal-f/?a=6080
//}

// @function general form of the kelly criterion formula.
// @param win_p   float, probability of the investment returns a positive outcome.
// @param loss_p  float, probability of the investment returns a negative outcome.
// @param win_rr  float, reward on a positive outcome.
// @param loss_rr float, reward on a negative outcome.
// @returns 
//      float, optimal fraction to risk.
// usage:
//      partial(0.6, 0.4, 0.6, 0.1)
export partial (
  float win_p   , 
  float loss_p  , 
  float win_rr  , 
  float loss_rr
  ) => //{
    (win_p / loss_rr) - (loss_p / win_rr)
// example:
plot(
  series = partial(0.6, 0.4, 0.6, 0.1), 
  title  = 'Partial', 
  color  = color.purple
  )
// reference:
//      https://quantpedia.com/beware-of-excessive-leverage-introduction-to-kelly-and-optimal-f/?a=6080
//}

// @function Calculate the fraction to invest from a array of returns.
// @param returns array<float> trade/asset/strategy returns.
// @returns
//      float, optimal fraction to risk.
// usage:
//      from_returns(array.from(0.1,0.2,0.1,-0.1,-0.05,0.05))
export from_returns (
  float[] returns
  ) => //{
    int _size = array.size(returns)
    array<float> _wins   = array.new<float>(0)
    array<float> _losses = array.new<float>(0)
    for _i = 0 to _size - 1
        float _ri = array.get(returns, _i)
        if _ri > 0.0
            array.push(_wins, _ri)
        else
            array.push(_losses, _ri)
    float _win_p = array.size(_wins) / _size
    float _rr = array.avg(_wins) / math.abs(array.avg(_losses))
    simplified(_win_p, _rr)
// example:
plot(
  series = from_returns(array.from(0.25,-0.15,0.25,-0.15,0.25,-0.05,0.25,-0.15,0.25,-0.05)), 
  title  = 'from returns'
  )
//}

// @function Final fraction, eg. if fraction is 0.2 and expected max loss is 10% 
//  then you should size your position as 0.2/0.1=2 (leverage, 200% position size).
// @param fraction          float, aproximate percent fraction invested.
// @param max_expected_loss float, maximum expected percent on a loss (ex 10% = 0.1).
// @returns 
//      float, final fraction to invest.
//  usage:
//      final_f(0.2, 0.5)
export final_f (
  float fraction         , 
  float max_expected_loss
  ) => //{
    fraction / max_expected_loss
// example:
plot(
  series = final_f(0.2, 0.5), 
  title  = 'Ff', 
  color  = color.red
  )
// reference:
//      https://quantpedia.com/beware-of-excessive-leverage-introduction-to-kelly-and-optimal-f/?a=6080
//}


// @function Holding Period Return function
// @param fraction      float, aproximate percent fraction invested.
// @param trade         float, profit or loss in a trade.
// @param biggest_loss  float, value of the biggest loss on record.
// @returns 
//      float, multiplier of effect on equity so that a win of 5% is 1.05 and loss of 5% is 0.95.
//  usage:
//      hpr(fraction=0.05, trade=0.1, biggest_loss=-0.2)
export hpr (
  float fraction    , 
  float trade       , 
  float biggest_loss
  ) => //{
    1.0 + fraction * final_f(-trade, biggest_loss)
// example:
plot(
  series = hpr(0.05, 0.1, -0.2), 
  title  = 'HPR', 
  color  = color.yellow
  )
//}

// @function Terminal Wealth Relative, returns a multiplier that can be applied 
//  to the initial capital that leadds to the final balance.
// @param returns   array<float>, list of trade returns.
// @param rr        float       , reward to risk rate.
// @param eps       float       , minimum resolution to void zero division.
// @returns 
//      float, optimal fraction to invest.
// usage:
//      twr(returns=array.from(0.1,-0.2,0.3), rr=0.6)
export twr (
  float[] returns           , 
  float   rr                , 
  float   eps     = -1.0e-12
  ) => //{
    int _size = array.size(returns)
    // error handling:
    switch
        _size < 1 => runtime.error('FunctionKellyCriterion -> twr(): "returns" parameter array must have atleast one element!')
    //    
    float _biggest_loss = math.min(eps, array.min(returns))
    int _wins = 0
    for _i = 0 to _size - 1
        float _ti = array.get(returns, _i)
        if _ti > 0.0
            _wins += 1
    float _win_p = _wins / _size
    float _f = simplified(_win_p, rr)
    _twr = 1.0
    for _i = 0 to _size - 1
        _twr *= hpr(_f, array.get(returns, _i), _biggest_loss)
    _twr
// example:
r = array.from(+9.0, +18, +7, +1, +10,-5,-3,-17,-7)
t = twr(r, 0.50)
plot(
  series = t, 
  title  = 'TWR', 
  color  = color.teal
  )
// reference:
//      https://quantpedia.com/beware-of-excessive-leverage-introduction-to-kelly-and-optimal-f/?a=6080
//}


////////////////////////////////////////////////////////////////////////////////
// the following functions are just accessory and are included for convenience
//      sake only.
////////////////////////////////////////////////////////////////////////////////

// @function Geometric mean Holding Period Return, represents the average multiple made on the stake.
// @param returns   array<float>, list of trade returns.
// @param rr        float       , reward to risk rate.
// @param eps       float       , minimum resolution to void zero division.
// @returns 
//      float, multiplier of effect on equity so that a win of 5% is 1.05 and loss of 5% is 0.95.
//  usage:
//      ghpr(returns=array.from(0.1,-0.2,0.3), rr=0.6)
export ghpr (
  float[] returns           , 
  float   rr                , 
  float   eps     = -1.0e-12
  ) => //{
    int _size_r = array.size(returns)
    math.pow(twr(returns, rr, eps), 1.0 / _size_r)
// example:
plot(
  series = ghpr(r, 0.50), 
  title  = 'GHPR', 
  color  = color.yellow
  )
//}


// @function run multiple coin flipping (binary outcome) simulations.
// @param fraction        float, fraction of capital to bet.
// @param initial_capital float, capital at the start of simulation.
// @param n_series        int  , number of simulation series.
// @param n_periods       int  , number of periods in each simulation series.
// @returns
//      matrix<float>(n_series, n_periods), matrix with simulation results per row.
// usage:
//      run_coin_simulation(fraction=0.1)
export run_coin_simulation(
  float fraction               , 
  float initial_capital = 100.0,
  int   n_series        =  10  ,
  int   n_periods       = 100
  ) => //{
    //
    _S = matrix.new<float>(10, 100)
    for _i = 0 to n_series - 1
        matrix.set(_S, _i, 0, initial_capital)
        for _t = 1 to n_periods - 1
            bool _heads = math.round(math.random()) > 0
            if _heads
                // add the win to the capital
                matrix.set(_S, _i, _t, (1.0 + fraction) * matrix.get(_S, _i, _t - 1) )
            else
                // take the loss from the capital
                matrix.set(_S, _i, _t, (1.0 - fraction) * matrix.get(_S, _i, _t - 1) )
    _S
// example:
// var S = run_coin_simulation(0.1)
// plot(matrix.get(S, 0, bar_index%100))
// plot(array.avg(matrix.col(S, bar_index%100)))
// reference: 
//      https://towardsdatascience.com/python-risk-management-kelly-criterion-526e8fb6d6fd
//}

// @function run a simulation over provided returns.
// @param returns         array<float>, trade, asset or strategy percent returns.
// @param fraction        float       , fraction of capital to bet.
// @param initial_capital float       , capital at the start of simulation.
// @returns
//      array<float>, array with simulation results.
// usage:
//      run_asset_simulation(returns=array.from(0.1,-0.2,0.-3,0.4), fraction=0.1)
export run_asset_simulation(
  array<float>  returns                 ,
  float         fraction                , 
  float         initial_capital = 100.0
  ) => //{
    //
    int _size = array.size(returns)
    _S = array.new<float>(_size)
    array.set(_S, 0, initial_capital)
    for _t = 1 to _size - 1
        float _r = math.exp(array.get(returns, _t)) // center returns around 1 instead of 0
        float _eq = array.get(_S, _t - 1)
        if _r >= 1.0
            // add the win to the capital
            array.set(_S, _t, math.max(0.0, (1.0 + (fraction * _r)) * _eq))
        else
            // take the loss from the capital
            array.set(_S, _t, math.max(0.0, (1.0 - (fraction * _r)) * _eq))
    _S
// example:
var rs = array.new<float>(100, 0.0)
if barstate.isfirst
    for _i = 1 to 99
        array.set(rs, _i, math.random(-0.01, 0.01) * math.random())
float optf = math.max(0.001, from_returns(rs))
var A = run_asset_simulation(rs, 0.1)
var B = run_asset_simulation(rs, optf)
plot(array.get(A, bar_index%100), title='Asset sim. A', color=color.blue)
plot(array.get(B, bar_index%100), title='Asset sim. B', color=color.red)
plotshape(series=bar_index%100==0?100:na, title='marker', style=shape.diamond, location=location.absolute, size=size.small)
// reference: 
//      https://towardsdatascience.com/python-risk-management-kelly-criterion-526e8fb6d6fd
//}


// @function calculate strategy() current probability of positive outcome in a trade.
export strategy_win_probability() => //{
    strategy.wintrades / strategy.closedtrades
// reference: 
//      https://www.tradingview.com/script/bFXf4IXh-Built-in-Kelly-ratio-for-dynamic-position-sizing/
//}

// @function calculate strategy() current average won on a trade with positive outcome.
export strategy_avg_won() => //{
    strategy.grossprofit / strategy.wintrades
// reference: 
//      https://www.tradingview.com/script/bFXf4IXh-Built-in-Kelly-ratio-for-dynamic-position-sizing/
//}

// @function calculate strategy() current average lost on a trade with negative outcome.
export strategy_avg_loss() => //{
    strategy.grossloss / strategy.losstrades
// reference: 
//      https://www.tradingview.com/script/bFXf4IXh-Built-in-Kelly-ratio-for-dynamic-position-sizing/
//}






