/ Strategies / src / methods.jl
methods.jl
  1  using .Lang: @lget!, @deassert, MatchString, @caller, Option
  2  using .Instances: ohlcv_dict
  3  using .Data: propagate_ohlcv!
  4  import .Instances.ExchangeTypes: exchangeid, exchange
  5  import .Instances.Exchanges: marketsid
  6  import .Instruments: cash!, add!, sub!, addzero!, subzero!, freecash, cash
  7  using .Misc: attr, setattr!
  8  import .Misc: marginmode
  9  using .OrderTypes: IncreaseTrade, ReduceTrade, SellTrade, ShortBuyTrade
 10  
 11  baremodule LogStrategyLock end
 12  
 13  const _STR_SYMBOLS_CACHE = Dict{String,Symbol}()
 14  const _SYM_SYMBOLS_CACHE = Dict{Symbol,Symbol}()
 15  
 16  @doc """ Retrieves the market identifiers for a given strategy type.
 17  
 18  $(TYPEDSIGNATURES)
 19  
 20  The `marketsid` function invokes the `call!` function with the strategy type and `StrategyMarkets()` as arguments.
 21  This function is used to fetch the market identifiers associated with a specific strategy type.
 22  """
 23  marketsid(t::Type{<:S}) where {S<:Strategy} = invokelatest(call!, t, StrategyMarkets())
 24  marketsid(s::S) where {S<:Strategy} = begin
 25      call!(typeof(s), StrategyMarkets())
 26  end
 27  Base.Broadcast.broadcastable(s::Strategy) = Ref(s)
 28  @doc "Assets loaded by the strategy."
 29  assets(s::Strategy) = universe(s).data.asset
 30  inuniverse(a::AbstractAsset, s::Strategy) = a ∈ assets(s)
 31  inuniverse(ai::AssetInstance, s::Strategy) = ai.asset ∈ assets(s)
 32  inuniverse(sym::Symbol, s::Strategy) = begin
 33      for ai in s.universe
 34          if sym == bc(ai)
 35              return true
 36          end
 37      end
 38      return false
 39  end
 40  @doc "Strategy assets instance."
 41  instances(s::Strategy) = universe(s).data.instance
 42  # FIXME: this should return the Exchange, not the ExchangeID
 43  @doc "Strategy exchange."
 44  exchange(s::Strategy) = getexchange!(Symbol(exchangeid(s)); sandbox=s.sandbox, account=account(s))
 45  function exchangeid(
 46      ::Union{<:S,Type{<:S}} where {S<:Strategy{X,N,E,R,C} where {X,N,R,C}}
 47  ) where {E<:ExchangeID}
 48      E
 49  end
 50  Exchanges.account(s::Strategy) = getfield(getfield(s, :config), :account)
 51  Exchanges.accounts(s::Strategy) = Exchanges.accounts(exchange(s))
 52  Exchanges.current_account(s::Strategy) = Exchanges.current_account(exchange(s))
 53  function Exchanges.getexchange!(s::Type{<:Strategy})
 54      getexchange!(Symbol(exchangeid(s)); sandbox=issandbox(s), account=account(s))
 55  end
 56  Exchanges.issandbox(s::Strategy) = begin
 57      ans = s.sandbox
 58      @deassert ans == Exchanges.issandbox(exchange(s))
 59      ans
 60  end
 61  function Exchanges.issandbox(s::Type{<:Strategy})
 62      let mod = getproperty(Main, nameof(s))
 63          if hasproperty(mod, :SANDBOX)
 64              prop = getproperty(mod, :SANDBOX)
 65              if prop isa Bool
 66                  prop
 67              elseif prop isa Ref{Bool}
 68                  prop[]
 69              else
 70                  @error "strategy: expected `SANDBOX` to be a boolean ref" prop
 71                  execmode(s) != Paper()
 72              end
 73          else
 74              @warn "strategy: `SANDBOX` property not found"
 75              execmode(s) != Paper()
 76          end
 77      end
 78  end
 79  cash(s::Strategy) = getfield(s, :cash)
 80  Instances.committed(s::Strategy) = getfield(s, :cash_committed)
 81  @doc "Cash that is not committed, and therefore free to use for new orders."
 82  freecash(s::Strategy) = cash(s) - s.cash_committed
 83  @doc "Get the strategy margin mode."
 84  function marginmode(
 85      ::Union{<:T,<:Type{<:T}}
 86  ) where {T<:Strategy{X,N,E,M} where {X,N,E}} where {M<:MarginMode}
 87      M()
 88  end
 89  
 90  @doc "Returns the strategy execution mode."
 91  Misc.execmode(::Union{Type{S},S}) where {S<:Strategy{M}} where {M<:ExecMode} = M()
 92  
 93  @doc """ Checks if the strategy's cash matches its universe.
 94  
 95  $(TYPEDSIGNATURES)
 96  
 97  The `iscashable` function checks if the cash of the strategy is cashable within the universe of the strategy.
 98  It returns `true` if the cash is cashable, and `false` otherwise.
 99  """
100  coll.iscashable(s::Strategy) = coll.iscashable(s.cash, universe(s))
101  issim(::Strategy{M}) where {M<:ExecMode} = M == Sim
102  ispaper(::Strategy{M}) where {M<:ExecMode} = M == Paper
103  islive(::Strategy{M}) where {M<:ExecMode} = M == Live
104  @doc "The name of the strategy module."
105  Base.nameof(::Type{<:Strategy{<:ExecMode,N}}) where {N<:Symbol} = N
106  @doc "The name of the strategy module."
107  Base.nameof(s::Strategy) = typeof(s).parameters[2]
108  @doc "The strategy `AssetCollection`."
109  universe(s::Strategy) = getfield(s, :universe)
110  @doc "The `throttle` attribute determines the strategy polling interval."
111  throttle(s::Strategy) = attr(s, :throttle, Second(5))
112  @doc "The strategy `Config` attributes."
113  attrs(s::Strategy) = getfield(getfield(s, :config), :attrs)
114  @doc "`Symbol` representation of the strategy (name of the module)."
115  Base.Symbol(s::Strategy) = nameof(s)
116  Base.haskey(s::Strategy, k) = haskey(attrs(s), k)
117  
118  @doc """ Resets the state of a strategy.
119  
120  $(TYPEDSIGNATURES)
121  
122  The `reset!` function is used to reset the state of a given strategy.
123  It empties the buy and sell orders, resets the holdings and assets, and optionally re-applies the strategy configuration defaults.
124  If the strategy is currently running, the reset operation is aborted with a warning.
125  """
126  function reset!(s::Strategy, config=false)
127      let attrs = attrs(s)
128          if haskey(attrs, :is_running) && attrs[:is_running][]
129              @warn "Aborting reset because $(nameof(s)) is running in $(execmode(s)) mode!"
130              return nothing
131          end
132      end
133      for d in values(s.buyorders)
134          empty!(d)
135      end
136      for d in values(s.sellorders)
137          empty!(d)
138      end
139      empty!(s.holdings)
140      for ai in universe(s)
141          reset!(ai, Val(:full))
142      end
143      if config
144          cfg = s.config
145          # Reset only dynamic attributes dict to defaults
146          cfg.attrs = copy(cfg.defaults.attrs)
147      else
148          let cfg = s.config
149              nameof(exchange(s))
150              cfg.exchange = nameof(exchange(s))
151              cfg.mode = execmode(s)
152              cfg.margin = marginmode(s)
153              cfg.qc = nameof(cash(s))
154              cfg.min_timeframe = s.timeframe
155          end
156      end
157      default!(s)
158      cash!(s.cash, s.config.initial_cash)
159      cash!(s.cash_committed, 0.0)
160      abs = attr(s, :assets_bysym, nothing)
161      if !isnothing(abs)
162          empty!(abs)
163      end
164      call!(s, ResetStrategy())
165  end
166  @doc """ Reloads OHLCV data for assets in the strategy universe.
167  
168  $(TYPEDSIGNATURES)
169  
170  The `reload!` function empties the data for each asset instance in the strategy's universe and then loads new data.
171  This is useful for refreshing the strategy's knowledge of the market state.
172  """
173  reload!(s::Strategy) = begin
174      for inst in universe(s).data.instance
175          empty!(inst.data)
176          load!(inst; reset=true)
177      end
178  end
179  const config_fields = fieldnames(Config)
180  @doc "Set strategy defaults."
181  default!(s::Strategy) = nothing
182  
183  @doc """ Fills the strategy with the specified timeframes.
184  
185  $(TYPEDSIGNATURES)
186  
187  This function fills the strategy with the specified timeframes. It first creates a set of timeframes and adds the strategy's timeframe, the timeframes from the strategy's configuration, and the timeframe attribute of the strategy. It then fills the universe of the strategy with these timeframes.
188  """
189  Base.fill!(s::Strategy; kwargs...) = begin
190      tfs = Set{TimeFrame}()
191      push!(tfs, s.timeframe)
192      push!(tfs, s.config.timeframes...)
193      push!(tfs, attr(s, :timeframe, s.timeframe))
194      uni = universe(s)
195      coll.fill!(uni, tfs...; kwargs...)
196      for ai in uni
197          propagate_ohlcv!(ohlcv_dict(ai))
198      end
199  end
200  
201  _config_attr(s, k) = getfield(getfield(s, :config), k)
202  
203  @doc """ Retrieves a property of a strategy.
204  
205  $(TYPEDSIGNATURES)
206  
207  This function checks if the property is directly on the strategy or the strategy's configuration.
208  If the property is not found, it checks the configuration's attributes.
209  """
210  function Base.getproperty(s::Strategy, sym::Symbol)
211      if hasfield(Strategy, sym)
212          getfield(s, sym)
213      else
214          cfg = getfield(s, :config)
215          if hasfield(Config, sym)
216              getfield(cfg, sym)
217          else
218              getfield(cfg, :attrs)[sym]
219          end
220      end
221  end
222  
223  @doc """ Retrieves a property of a strategy using a string key.
224  
225  $(TYPEDSIGNATURES)
226  
227  This function first gets the universe of the strategy and then retrieves the property using the string key.
228  """
229  function Base.getproperty(s::Strategy, sym::String)
230      uni = getfield(s, :universe)
231      uni[MatchString(sym)]
232  end
233  
234  @doc """ Generates the path for strategy logs.
235  
236  $(TYPEDSIGNATURES)
237  
238  The `logpath` function generates a path for storing strategy logs.
239  It takes the strategy and optional parameters for the name of the log file and additional path nodes.
240  The function checks if the directory for the logs exists and creates it if necessary.
241  It then returns the full path to the log file.
242  """
243  function logpath(s::Strategy; name="events", path_nodes...)
244      dir = dirname(s.path)
245      dirpath = if dir == ""
246          pwd()
247      else
248          dirpath = joinpath(realpath(dirname(s.path)), "logs", path_nodes...)
249          isdir(dirpath) || mkpath(dirpath)
250          dirpath
251      end
252      joinpath(dirpath, string(replace(name, r".log$" => ""), ".log"))
253  end
254  
255  @doc """ Retrieves the logs for a strategy.
256  
257  $(TYPEDSIGNATURES)
258  
259  The `logs` function collects and returns all the logs associated with a given strategy.
260  It fetches the logs from the directory specified in the strategy's path.
261  """
262  function logs(s::Strategy)
263      dirpath = joinpath(realpath(dirname(s.path)), "logs")
264      collect(Iterators.flatten(walkdir(dirpath)))
265  end
266  
267  function Base.propertynames(::Strategy)
268      (
269          fieldnames(Strategy)...,
270          :attrs,
271          :exchange,
272          :path,
273          :initial_cash,
274          :min_size,
275          :min_vol,
276          :qc,
277          :mode,
278          :config,
279      )
280  end
281  
282  Base.getindex(s::Strategy, k::MatchString) = getindex(s.universe, k)
283  Base.getindex(s::Strategy, k) = attr(s, k)
284  Base.setindex!(s::Strategy, v, k...) = setattr!(s, v, k...)
285  Base.lock(s::Strategy) = begin
286      @debug "strategy: locking" _module = LogStrategyLock @caller
287      lock(getfield(s, :lock))
288      @debug "strategy: locked" _module = LogStrategyLock @caller
289  end
290  Base.lock(f, s::Strategy) = begin
291      @debug "strategy: locking" _module = LogStrategyLock @caller
292      lock(f, getfield(s, :lock))
293      @debug "strategy: locked" _module = LogStrategyLock @caller
294  end
295  Base.unlock(s::Strategy) = begin
296      unlock(getfield(s, :lock))
297      @debug "strategy: unlocked" _module = LogStrategyLock @caller
298  end
299  Base.islocked(s::Strategy) = islocked(getfield(s, :lock))
300  Base.float(s::Strategy) = cash(s).value
301  Base.get(s::Strategy, k::Symbol, def) = get(getfield(getfield(s, :config), :attrs), k, def)
302  
303  @doc """ Creates a similar strategy with optional changes.
304  
305  $(TYPEDSIGNATURES)
306  
307  The `similar` function creates a new strategy that is similar to the given one.
308  It allows for optional changes to the mode, timeframe, and exchange.
309  The new strategy is created with the same self, margin mode, and universe as the original, but with a copy of the original's configuration.
310  """
311  function Base.similar(s::Strategy; mode=s.mode, timeframe=s.timeframe, exc=exchange(s))
312      s = Strategy(
313          s.self,
314          mode,
315          marginmode(s),
316          timeframe,
317          exc,
318          similar(universe(s));
319          config=copy(s.config),
320      )
321  end
322  
323  function symsdict(s::Strategy)
324      @lock s @lget! attrs(s) :assets_bysym Dict{String,Option{AssetInstance}}()
325  end
326  
327  @doc """
328  Retrieves an asset instance by symbol.
329  
330  $(TYPEDSIGNATURES)
331  
332  This function retrieves an asset instance by symbol `sym` from a strategy `s`. It first checks if the asset instance is already cached in the strategy's attributes. If not, it retrieves the asset instance from the strategy's universe. If the asset instance is not found, it returns `nothing`.
333  
334  """
335  function asset_bysym(s::Strategy, sym, dict_bysim=symsdict(s))
336      k = string(sym)
337      ai = get(dict_bysim, k, nothing)
338      if isnothing(ai)
339          ai = s[MatchString(k)]
340          if ai isa AssetInstance
341              dict_bysim[k] = ai
342          end
343      else
344          ai
345      end
346  end