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