/ Metrics / src / trades_metrics.jl
trades_metrics.jl
  1  using .OrderTypes: LiquidationTrade, LongTrade, ShortTrade
  2  using .Data: default_value
  3  using .Data.Misc: OrderedDict
  4  
  5  @doc """ Computes the average duration of trades for an asset instance.
  6  
  7  $(TYPEDSIGNATURES)
  8  
  9  Calculates the average duration between trades for an `AssetInstance` `ai`.
 10  The `raw` parameter determines whether the result should be in raw format (in milliseconds) or a compact time format.
 11  The function `f` is used to aggregate the durations and defaults to the mean.
 12  
 13  """
 14  function trades_duration(ai::AssetInstance; raw=false, f=mean)
 15      periods = getproperty.(ai.history, :date) |> diff
 16      periods_num = getproperty.(periods, :value) # milliseconds
 17      μ = if length(ai.history) > 1
 18          f(periods_num)
 19      else
 20          Millisecond(lastdate(ai) - first(ai.history).date).value
 21      end
 22      raw ? μ : compact(Millisecond(trunc(μ)))
 23  end
 24  
 25  @doc """ Computes the average duration of trades for a strategy.
 26  
 27  $(TYPEDSIGNATURES)
 28  
 29  Calculates the average duration between trades for a `Strategy` `s`.
 30  The function `f` is used to aggregate the durations and defaults to the mean.
 31  
 32  """
 33  function trades_duration(s::Strategy; f=mean)
 34      [trades_duration(ai; raw=true, f) for ai in s.universe] |>
 35      mean |>
 36      trunc |>
 37      Millisecond |>
 38      compact
 39  end
 40  
 41  @doc """ Computes the average trade size for an asset instance.
 42  
 43  $(TYPEDSIGNATURES)
 44  
 45  Calculates the average size of trades for an `AssetInstance` `ai`.
 46  The function `f` is used to aggregate the sizes and defaults to the mean.
 47  
 48  """
 49  function trades_size(ai::AssetInstance; f=mean)
 50      vals = getproperty.(ai.history, :size)
 51      f(abs.(vals))
 52  end
 53  
 54  @doc """ Computes the average trade size for a strategy.
 55  
 56  $(TYPEDSIGNATURES)
 57  
 58  Calculates the average size of trades for a `Strategy` `s`.
 59  The function `f` is used to aggregate the sizes and defaults to the mean.
 60  
 61  """
 62  function trades_size(s::Strategy; f=mean)
 63      [trades_size(ai; f) for ai in s.universe] |> f
 64  end
 65  
 66  @doc """ Computes the average leverage for trades of an asset instance.
 67  
 68  $(TYPEDSIGNATURES)
 69  
 70  Calculates the average leverage of trades for an `AssetInstance` `ai`.
 71  The function `f` is used to aggregate the leverages and defaults to the mean.
 72  
 73  """
 74  function trades_leverage(ai::AssetInstance; f=mean)
 75      vals = getproperty.(ai.history, :leverage)
 76      f(abs.(vals))
 77  end
 78  
 79  @doc """ Computes the average hour of trades for an asset instance.
 80  
 81  $(TYPEDSIGNATURES)
 82  
 83  Calculates the average hour of trades for an `AssetInstance` `ai`.
 84  The function `f` is used to aggregate the hours and defaults to the mean.
 85  
 86  """
 87  function trades_hour(ai::AssetInstance; f=mean)
 88      h = Hour.(getproperty.(ai.history, :date))
 89      h = getproperty.(h, :value)
 90      f(h) |> trunc |> Hour
 91  end
 92  
 93  @doc """ Computes the average weekday of trades for an asset instance.
 94  
 95  $(TYPEDSIGNATURES)
 96  
 97  Calculates the average weekday of trades for an `AssetInstance` `ai`.
 98  The function `f` is used to aggregate the weekdays and defaults to the mean.
 99  
100  """
101  function trades_weekday(ai::AssetInstance; f=mean)
102      w = dayofweek.(getproperty.(ai.history, :date))
103      f(w) |> trunc |> Int |> dayname
104  end
105  
106  @doc """ Computes the average day of the month for trades of an asset instance.
107  
108  $(TYPEDSIGNATURES)
109  
110  Calculates the average day of the month for trades for an `AssetInstance` `ai`.
111  The function `f` is used to aggregate the days and defaults to the mean.
112  
113  """
114  function trades_monthday(ai::AssetInstance; f=mean)
115      w = dayofmonth.(getproperty.(ai.history, :date))
116      f(w) |> trunc |> Int
117  end
118  
119  macro cumbal()
120      ex = quote
121          returns = let
122              tf = first(keys(ohlcv_dict(ai)))
123              trades_balance(ai; tf).cum_total
124          end
125      end
126      esc(ex)
127  end
128  
129  function trades_drawdown(ai::AssetInstance; cum_bal=@cumbal(), kwargs...)
130      length(cum_bal) == 1 && return zero(DFT)
131      ath = atl = first(cum_bal)
132      dd = typemax(eltype(cum_bal))
133      for v in cum_bal
134          if v > ath
135              ath = v
136          elseif v < atl
137              atl = v
138          end
139          aatl = abs(atl)
140          shifted_ath = aatl + abs(ath)
141          this_dd = aatl / shifted_ath
142          if aatl > zero(aatl) && this_dd < dd
143              dd = this_dd
144          end
145      end
146      (; dd=isfinite(dd) ? dd : 1.0 - 1.0, atl, ath)
147  end
148  
149  function trades_pnl(returns; f=mean)
150      losses = (v for v in returns if isfinite(v) && v <= 0.0)
151      gains = (v for v in returns if isfinite(v) && v > 0.0)
152      NamedTuple((
153          Symbol(nameof(f), :_, :loss) => isempty(losses) ? default_value(f) : f(losses),
154          Symbol(nameof(f), :_, :profit) => isempty(gains) ? default_value(f) : f(gains),
155      ))
156  end
157  function trades_pnl(ai::AssetInstance; returns=_returns_arr(@cumbal()), kwargs...)
158      trades_pnl(returns; kwargs...)
159  end
160  
161  function asset_stats!(res::DataFrame, ai::AssetInstance; extended=false)
162      # Basic stats that are always computed
163      avg_dur = trades_duration(ai; f=mean)
164      avg_size = trades_size(ai; f=mean)
165      avg_leverage = trades_leverage(ai; f=mean)
166  
167      trades = length(ai.history)
168      liquidations = count(x -> x isa LiquidationTrade, ai.history)
169      longs = count(x -> x isa LongTrade, ai.history)
170      shorts = count(x -> x isa ShortTrade, ai.history)
171      weekday = trades_weekday(ai; f=mean)
172      monthday = trades_monthday(ai; f=mean)
173  
174      cum_bal = @cumbal()
175      drawdown, atl, ATH = trades_drawdown(ai; cum_bal)
176      returns = _returns_arr(cum_bal)
177      avg_loss, avg_profit = trades_pnl(ai; returns, f=mean)
178      end_balance = cum_bal[end]
179  
180      # Create base stats dictionary with ordered keys
181      stats = OrderedDict(
182          :asset => ai.asset.raw,
183          :trades => trades,
184          :liquidations => liquidations,
185          :longs => longs,
186          :shorts => shorts,
187          :avg_dur => avg_dur,
188          :weekday => weekday,
189          :monthday => monthday,
190          :avg_size => avg_size,
191          :avg_leverage => avg_leverage,
192          :drawdown => drawdown,
193          :ATH => ATH,
194          :avg_loss => avg_loss,
195          :avg_profit => avg_profit,
196          :end_balance => end_balance,
197      )
198  
199      # Add extended stats if requested
200      if extended
201          med_dur = trades_duration(ai; f=median)
202          min_dur = trades_duration(ai; f=minimum)
203          max_dur = trades_duration(ai; f=maximum)
204  
205          med_size = trades_size(ai; f=median)
206          min_size = trades_size(ai; f=minimum)
207          max_size = trades_size(ai; f=maximum)
208  
209          med_leverage = trades_leverage(ai; f=median)
210          min_leverage = trades_leverage(ai; f=minimum)
211          max_leverage = trades_leverage(ai; f=maximum)
212  
213          med_loss, med_profit = trades_pnl(ai; returns, f=median)
214          loss_ext, profit_ext = trades_pnl(ai; returns, f=extrema)
215          max_loss = loss_ext[1]
216          max_profit = profit_ext[2]
217  
218          # Create new OrderedDict with all stats in the desired order
219          stats = OrderedDict(
220              :asset => ai.asset.raw,
221              :trades => trades,
222              :liquidations => liquidations,
223              :longs => longs,
224              :shorts => shorts,
225              :avg_dur => avg_dur,
226              :med_dur => med_dur,
227              :min_dur => min_dur,
228              :max_dur => max_dur,
229              :weekday => weekday,
230              :monthday => monthday,
231              :avg_size => avg_size,
232              :med_size => med_size,
233              :min_size => min_size,
234              :max_size => max_size,
235              :avg_leverage => avg_leverage,
236              :med_leverage => med_leverage,
237              :min_leverage => min_leverage,
238              :max_leverage => max_leverage,
239              :drawdown => drawdown,
240              :ATH => ATH,
241              :avg_loss => avg_loss,
242              :med_loss => med_loss,
243              :avg_profit => avg_profit,
244              :med_profit => med_profit,
245              :max_loss => max_loss,
246              :max_profit => max_profit,
247              :end_balance => end_balance,
248          )
249      end
250  
251      push!(res, NamedTuple(stats); promote=false)
252      
253      # upcast periods for pretty print
254      if nrow(res) == 1
255          for prop in (:avg, :med, :min, :max)
256              prop = Symbol("$(prop)_dur")
257              haskey(stats, prop) || continue
258              arr = getproperty(res, prop)
259              setproperty!(res, prop, convert(Vector{Period}, arr))
260          end
261      end
262  end
263  
264  function trades_stats(s::Strategy; extended=false, since=DateTime(0))
265      res = DataFrame()
266      for ai in s.universe
267          isempty(ai.history) && continue
268          if since >= first(ai.history).date
269              hist = ai.history
270              full_hist = copy(hist)
271              try
272                  filter!(x -> x.date >= since, hist)
273                  asset_stats!(res, ai; extended)
274              finally
275                  empty!(hist)
276                  append!(hist, full_hist)
277              end
278          else
279              asset_stats!(res, ai; extended)
280          end
281      end
282      res
283  end
284  
285  function trades_perf(s::Strategy; sortby=[:drawdown])
286      df = trades_stats(s)
287      perf = select(df, occursin.(r"asset|drawdown|ATH|loss|profit|end_balance", names(df)))
288      sort!(perf, sortby)
289  end