metrics.jl
1 using .Statistics: std 2 using Base: negate 3 using .st: trades_count 4 5 const DAYS_IN_YEAR = 365 6 @doc "All the metrics that supported." 7 const METRICS = Set(( 8 :total, :sharpe, :sortino, :calmar, :drawdown, :expectancy, :cagr, :trades 9 )) 10 11 @doc """ Generates code to calculate the cumulative total balance for a given set of trades over a given timeframe. 12 13 $(SIGNATURES) 14 15 This macro generates code that calculates the cumulative total balance for a given set of trades `s` over a given timeframe `tf`. 16 It first gets a DataFrame of balances for `s` and `tf` using the `trades_balance` function. 17 If this DataFrame is `nothing` (which means there are no trades), it immediately returns `-Inf`. 18 Otherwise, it extracts the `cum_total` column from the DataFrame, which represents the cumulative total balance, and assigns this to `balance`. 19 The generated code is then returned. 20 """ 21 macro balance_arr() 22 s = esc(:s) 23 tf = esc(:tf) 24 quote 25 $(esc(:balance)) = let balance_df = trades_balance($s; $tf) 26 isnothing(balance_df) && return -Inf 27 balance_df.cum_total 28 end 29 end 30 end 31 32 @doc """ Calculates the simple returns for an array of prices. 33 34 $(SIGNATURES) 35 36 This function takes an array of prices `arr` as a parameter, calculates the differences between successive prices, divides each difference by the corresponding previous price, and returns the resulting array of simple returns. 37 Please note that the first element of the return array would be `NaN` due to the lack of a previous price for the first element in `arr`. 38 """ 39 _returns_arr(arr) = begin 40 n_series = length(arr) 41 diff(arr) ./ replace(v -> iszero(v) ? one(v) : v, view(arr, 1:(n_series - 1))) 42 end 43 44 @doc """ Annualizes a volatility value. 45 46 $(SIGNATURES) 47 48 This function takes a volatility value `v` and a timeframe `tf` as parameters. 49 It multiplies `v` by the square root of the ratio of the number of days in a year times the period of `tf` to the period of a day. 50 This effectively converts `v` from a volatility per `tf` period to an annual volatility. 51 """ 52 _annualize(v, tf) = v * sqrt(DAYS_IN_YEAR * period(tf) / period(tf"1d")) 53 54 @doc """ Computes the non-annualized Sharpe ratio. 55 56 $(TYPEDSIGNATURES) 57 58 Calculates the Sharpe ratio given an array of `returns`. 59 The ratio is computed as the excess of the mean return over the risk-free rate `rfr`, divided by the standard deviation of the `returns`. 60 `tf` specifies the timeframe for the returns and defaults to one day. 61 62 """ 63 function _rawsharpe(returns; rfr=0.0, tf=tf"1d") 64 avg_returns = mean(returns) 65 ratio = (avg_returns - rfr) / std(returns) 66 _annualize(ratio, tf) 67 end 68 69 @doc """ Computes the Sharpe ratio for a given strategy. 70 71 $(TYPEDSIGNATURES) 72 73 Calculates the Sharpe ratio for a `Strategy` `s` over a specified timeframe `tf`, defaulting to one day. 74 The risk-free rate `rfr` can be specified, and defaults to 0.0. 75 76 """ 77 function sharpe(s::Strategy, tf=tf"1d"; rfr=0.0) 78 @balance_arr 79 returns = _returns_arr(balance) 80 _rawsharpe(returns; rfr, tf) 81 end 82 83 @doc """ Computes the non-annualized Sortino ratio. 84 85 $(TYPEDSIGNATURES) 86 87 Calculates the Sortino ratio given an array of `returns`. 88 The ratio is the excess of the mean return over the risk-free rate `rfr`, divided by the standard deviation of the negative `returns`. 89 `tf` specifies the timeframe for the returns and defaults to one day. 90 91 """ 92 function _rawsortino(returns; rfr=0.0, tf=tf"1d") 93 avg_returns = mean(returns) 94 downside_idx = returns .< 0.0 95 ratio = (avg_returns - rfr) / std(view(returns, downside_idx)) 96 _annualize(ratio, tf) 97 end 98 99 @doc """ Computes the Sortino ratio for a given strategy. 100 101 $(TYPEDSIGNATURES) 102 103 Calculates the Sortino ratio for a `Strategy` `s` over a specified timeframe `tf`, defaulting to one day. 104 The risk-free rate `rfr` can be specified, and defaults to 0.0. 105 106 """ 107 function sortino(s::Strategy, tf=tf"1d"; rfr=0.0) 108 @balance_arr 109 returns = _returns_arr(balance) 110 _rawsortino(returns; rfr, tf) 111 end 112 113 @doc """ Computes the non-annualized Calmar ratio. 114 115 $(TYPEDSIGNATURES) 116 117 Calculates the Calmar ratio given an array of `returns`. 118 The ratio is the annual return divided by the maximum drawdown. 119 `tf` specifies the timeframe for the returns and defaults to one day. 120 121 """ 122 function _rawcalmar(returns; tf=tf"1d") 123 max_drawdown = maxdd(returns).dd 124 annual_returns = mean(returns) * DAYS_IN_YEAR 125 negate(annual_returns) / max_drawdown 126 end 127 128 @doc """ Computes the maximum drawdown for a series of returns. 129 130 $(TYPEDSIGNATURES) 131 132 Calculates the maximum drawdown given an array of `returns`. 133 The drawdown is the largest percentage drop in the cumulative product of 1 plus the returns. 134 135 """ 136 function maxdd(returns) 137 length(returns) <= 1 && 138 return (; dd=0.0, ath=get(returns, 1, 0.0), cum_returns=returns) 139 @deassert all(x >= -1.0 for x in returns) 140 cum_returns = log1p.(v == -1.0 ? -1.0 + eps() : v for v in returns) 141 cumsum!(cum_returns, cum_returns) 142 replace!(expm1, cum_returns) 143 ath = one(eltype(cum_returns)) 144 dd = typemax(ath) 145 for n in eachindex(cum_returns) 146 bal = cum_returns[n] 147 if bal > ath 148 ath = bal 149 else 150 this_dd = bal / ath 151 if this_dd < dd 152 dd = this_dd 153 end 154 end 155 end 156 (; dd=-dd, ath, cum_returns) 157 end 158 159 @doc """ Computes the Calmar ratio for a given strategy. 160 161 $(TYPEDSIGNATURES) 162 163 Calculates the Calmar ratio for a `Strategy` `s` over a specified timeframe `tf`, defaulting to one day. 164 165 """ 166 function calmar(s::Strategy, tf=tf"1d") 167 @balance_arr 168 returns = _returns_arr(balance) 169 _rawcalmar(returns; tf) 170 end 171 172 @doc """ Computes the trading expectancy. 173 174 $(TYPEDSIGNATURES) 175 176 Calculates the trading expectancy given an array of `returns`. 177 This is a measure of the mean value of both winning and losing trades. 178 It takes into account both the probability and the average win/loss of trades. 179 180 """ 181 function _rawexpectancy(returns) 182 isempty(returns) && return 0.0 183 184 ups_idx = returns .> 0.0 185 ups = view(returns, ups_idx) 186 isempty(ups) && return 0.0 187 188 downs = view(returns, xor.(ups_idx, true)) 189 avg_up = mean(ups) 190 avg_down = mean(abs.(downs)) 191 192 risk_reward_ratio = avg_up / avg_down 193 up_rate = length(ups) / length(returns) 194 195 return ((1.0 + risk_reward_ratio) * up_rate) - 1.0 196 end 197 198 @doc """ Computes the trading expectancy for a given strategy. 199 200 $(TYPEDSIGNATURES) 201 202 Calculates the trading expectancy for a `Strategy` `s` over a specified timeframe `tf`, defaulting to one day. 203 204 """ 205 function expectancy(s::Strategy, tf=tf"1d") 206 @balance_arr 207 returns = _returns_arr(balance) 208 _rawexpectancy(returns) 209 end 210 211 @doc """ Computes the Compound Annual Growth Rate (CAGR) for a given strategy. 212 213 $(TYPEDSIGNATURES) 214 215 Calculates the CAGR for a `Strategy` `s` over a specified `Period` `prd`, defaulting to the period of the strategy's trades. 216 The initial cash amount `initial` and the pricing function `price_func` can also be specified. 217 218 """ 219 function cagr( 220 s::Strategy, 221 prd::Period=st.tradesperiod(s), 222 initial=s.initial_cash, 223 price_func=st.lasttrade_price_func, 224 ) 225 final = st.current_total(s, price_func) 226 (final / initial)^inv(prd / Day(DAYS_IN_YEAR)) - 1.0 227 end 228 229 @doc """ Returns a dict of calculated metrics for a given strategy. 230 231 $(TYPEDSIGNATURES) 232 233 For a `Strategy` `s`, calculates specified `metrics` over a specified timeframe `tf`, defaulting to one day. 234 If `normalize` is `true`, the metrics are normalized with respect to `norm_max`. 235 236 """ 237 function multi( 238 s::Strategy, metrics::Vararg{Symbol}; tf=tf"1d", normalize=false, norm_max=(;) 239 ) 240 balance = let df = trades_balance(s; tf) 241 isnothing(df) && 242 return Dict(m => ifelse(normalize, 0.0, typemin(DFT)) for m in metrics) 243 df.cum_total 244 end 245 returns = _returns_arr(balance) 246 if isempty(returns) 247 return [zero(DFT) for _ in metrics] 248 end 249 maybenorm = normalize ? normalize_metric : (x, _...) -> x 250 Dict((m => let v = if m == :sharpe 251 _rawsharpe(returns; tf) 252 elseif m == :sortino 253 _rawsortino(returns; tf) 254 elseif m == :calmar 255 _rawcalmar(returns; tf) 256 elseif m == :drawdown 257 maxdd(returns).dd 258 elseif m == :expectancy 259 _rawexpectancy(returns) 260 elseif m == :cagr 261 cagr(s) 262 elseif m == :total 263 balance[end] 264 elseif m == :trades 265 trades_count(s, Val(:liquidations))[1] 266 else 267 error("$m is not a valid metric") 268 end 269 norm_args = if m in norm_max 270 (norm_max[m],) 271 else 272 () 273 end 274 maybenorm(v, Val(m), norm_args...) 275 end for m in metrics)...) 276 end 277 278 _zeronan(v) = ifelse(isnan(v), 0.0, v) 279 _clamp_metric(v, max) = clamp(_zeronan(v / max), zero(v), one(v)) 280 @doc """ Normalize a metric. Based on the value of `max`. """ 281 normalize_metric(v, ::Val{:total}, max=1e6) = _clamp_metric(v, max) 282 normalize_metric(v, ::Val{:drawdown}, max=1e6) = _clamp_metric(v, max) 283 normalize_metric(v, ::Val{:sharpe}, max=1e1) = _clamp_metric(v, max) 284 normalize_metric(v, ::Val{:sortino}, max=1e1) = _clamp_metric(v, max) 285 normalize_metric(v, ::Val{:calmar}, max=1e1) = _clamp_metric(v, max) 286 normalize_metric(v, ::Val{:expectancy}) = v 287 normalize_metric(v, ::Val{:cagr}, max=1e2) = _clamp_metric(v, max) 288 normalize_metric(v, ::Val{:trades}, max=1e6) = _clamp_metric(v, max) 289 290 export sharpe, sortino, calmar, expectancy, cagr, multi