/ SimMode / src / backtest.jl
backtest.jl
  1  using Executors: orderscount
  2  using Executors: isoutof_orders
  3  using .Instances.Data.DFUtils: lastdate
  4  using .Misc.LoggingExtras
  5  using Base: with_logger
  6  using .st: universe, current_total, trades_count
  7  using Pbar: @withpbar!, @pbupdate!, ProgressBar, addjob!, ProgressJob, pbar!, Progress, pbar
  8  using .Progress: DescriptionColumn, CompletedColumn, SeparatorColumn, ProgressColumn, AbstractColumn
  9  using Pbar.Term.Segments: Segment
 10  using Pbar.Term.Measures: Measure
 11  using Pbar.Term.Progress: Progress
 12  
 13  import .Misc: start!, stop!
 14  
 15  # Custom column to display trades and balance
 16  struct StatsColumn <: AbstractColumn
 17      job::ProgressJob
 18      segments::Vector{Segment}
 19      measure::Measure
 20      style::String
 21      trades::Ref{Int}
 22      balance::Ref{DFT}
 23  
 24      function StatsColumn(job::ProgressJob; style="blue", trades=Ref{Int}(), balance=Ref{DFT}())
 25          txt = Segment("Trades: 0 | Balance: 0.0", style)
 26          return new(job, [txt], txt.measure, style, trades, balance)
 27      end
 28  end
 29  
 30  function Progress.update!(col::StatsColumn, color::String, args...)
 31      txt = Segment("Trades: $(col.trades[]) | Balance: $(col.balance[])", col.style)
 32      return txt.text
 33  end
 34  
 35  @doc """Backtest a strategy `strat` using context `ctx` iterating according to the specified timeframe.
 36  
 37  $(TYPEDSIGNATURES)
 38  
 39  On every iteration, the strategy is queried for the _current_ timestamp.
 40  The strategy should only access data up to this point.
 41  Example:
 42  - Timeframe iteration: `1s`
 43  - Strategy minimum available timeframe `1m`
 44  Iteration gives time `1999-12-31T23:59:59` to the strategy:
 45  The strategy (that can only lookup up to `1m` precision)
 46  looks-up data until the timestamp `1999-12-31T23:58:00` which represents the
 47  time until `23:59:00`.
 48  Therefore we have to shift by one period down, the timestamp returned by `apply`:
 49  ```julia
 50  julia> t = TimeTicks.apply(tf"1m", dt"1999-12-31T23:59:59")
 51  1999-12-31T23:59:00 # we should not access this timestamp
 52  julia> t - tf"1m".period
 53  1999-12-31T23:58:00 # this is the correct candle timestamp that we can access
 54  ```
 55  To avoid this mistake, use the function `available(::TimeFrame, ::DateTime)`, instead of apply.
 56  """
 57  function start!(
 58      s::Strategy{Sim}, ctx::Context; trim_universe=false, doreset=true, resetctx=true, show_progress=:off
 59  )
 60      # ensure that universe data start at the same time
 61      @ifdebug _resetglobals!(s)
 62      if trim_universe
 63          let data = st.coll.flatten(st.universe(s))
 64              !check_alignment(data) && trim!(data)
 65          end
 66      end
 67      if resetctx
 68          tt.current!(ctx.range, ctx.range.start + call!(s, WarmupPeriod()))
 69      end
 70      if doreset
 71          st.reset!(s)
 72      end
 73      update_mode = s.attrs[:sim_update_mode]::ExecAction
 74      logger = if s[:sim_debug]
 75          current_logger()
 76      else
 77          MinLevelLogger(current_logger(), s[:log_level])
 78      end
 79      
 80      with_logger(logger) do
 81          if show_progress !== :off
 82              # Create custom columns for the progress bar
 83              mycols = [DescriptionColumn, CompletedColumn, SeparatorColumn, ProgressColumn]
 84              trades = Ref{Int}()
 85              balance = Ref{DFT}()
 86              cols_kwargs = Dict()
 87              
 88              # Add stats columns if show_progress is :full
 89              if show_progress === :full
 90                  push!(mycols, StatsColumn)
 91                  cols_kwargs[:StatsColumn] = Dict(:style=>"blue bold", :trades=>trades, :balance=>balance)
 92              end
 93              
 94              wp = call!(s, WarmupPeriod())
 95              wp_steps = trunc(Int, wp / ctx.range.step)
 96              trimmed_start = min(ctx.range.stop, ctx.range.start + wp_steps * ctx.range.step)
 97              trimmed_range = trimmed_start:ctx.range.step:ctx.range.stop
 98              pbar!(; columns=mycols, columns_kwargs=cols_kwargs, width=140)
 99              balance[] = current_total(s)
100  
101              # Define update function based on show_progress mode
102              update_stats = if show_progress === :full
103                  () -> begin
104                      trades[] = trades_count(s)
105                      balance[] = current_total(s)
106                  end
107              else
108                  () -> nothing
109              end
110  
111              @withpbar! trimmed_range desc="Backtesting" begin
112                  # Iterate over the trimmed_range to respect warmup trimming when showing progress
113                  for date in trimmed_range
114                      isoutof_orders(s) && begin
115                          @deassert all(iszero(ai) for ai in universe(s))
116                          break
117                      end
118                      update!(s, date, update_mode)
119                      call!(s, date, ctx)
120                      update_stats()
121                      @debug "sim: iter" s.cash ltxzero(s.cash) isempty(s.holdings) orderscount(s)
122                      @pbupdate!
123                  end
124              end
125          else
126              for date in ctx.range
127                  isoutof_orders(s) && begin
128                      @deassert all(iszero(ai) for ai in universe(s))
129                      break
130                  end
131                  update!(s, date, update_mode)
132                  call!(s, date, ctx)
133                  @debug "sim: iter" s.cash ltxzero(s.cash) isempty(s.holdings) orderscount(s)
134              end
135          end
136      end
137      s
138  end
139  
140  @doc """
141  Backtest with context of all data loaded in the strategy universe.
142  
143  $(TYPEDSIGNATURES)
144  
145  Backtest the strategy with the context of all data loaded in the strategy universe. This function ensures that the universe data starts at the same time. If `trim_universe` is true, it trims the data to ensure alignment. If `doreset` is true, it resets the strategy before starting the backtest. The backtest is performed using the specified `ctx` context.
146  
147  """
148  start!(s::Strategy{Sim}; kwargs...) = start!(s, Context(s); kwargs...)
149  
150  @doc """
151  Starts the strategy with the given count.
152  
153  $(TYPEDSIGNATURES)
154  
155  Starts the strategy with the given count.
156  If `count` is greater than 0, it sets the start and end timestamps based on the count and the strategy's timeframe.
157  Otherwise, it sets the start and end timestamps based on the last timestamp in the strategy's universe.
158  
159  """
160  function start!(s::Strategy{Sim}, count::Integer; tf=s.timeframe, kwargs...)
161      if count > 0
162          from = ohlcv(first(s.universe)).timestamp[begin]
163          to = from + tf.period * count
164      else
165          to = ohlcv(last(s.universe)).timestamp[end]
166          from = to + tf.period * count
167      end
168      ctx = Context(Sim(), tf, from, to)
169      start!(s, ctx; kwargs...)
170  end
171  
172  @doc """Returns the latest date in the given strategy's universe.
173  
174  $(TYPEDSIGNATURES)
175  
176  Iterates over the strategy's universe to find the date of the last data point. Returns the latest date as a `DateTime` object.
177  
178  """
179  _todate(s) = begin
180      to = typemin(DateTime)
181      for ai in s.universe
182          this_date = lastdate(ai)
183          if this_date > to
184              to = this_date
185          end
186      end
187      return to
188  end
189  
190  @doc """ Starts the strategy simulation from a specific date to another.
191  
192  $(TYPEDSIGNATURES)
193  
194  This function initializes a simulation context with the given timeframe and date range, then starts the strategy with this context.
195  
196  """
197  function start!(s::Strategy{Sim}, from::DateTime, to::DateTime=_todate(s); kwargs...)
198      ctx = Context(Sim(), s.timeframe, from, to)
199      start!(s, ctx; kwargs...)
200  end
201  
202  stop!(::Strategy{Sim}) = nothing
203  
204  backtest!(s::Strategy{Sim}, args...; kwargs...) = begin
205      @warn "DEPRECATED: use `start!`"
206      start!(s, args...; kwargs...)
207  end
208