/ Metrics / src / metrics.jl
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