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