/ ExchangeTypes / src / exchange.jl
exchange.jl
  1  using .Python: pyschedule, pytask, Python, pyisinstance, pygetattr, @pystr, pybuiltins, pydict
  2  using Ccxt: _issupported
  3  using Ccxt.Misc.Lang: @lget!
  4  using Base: with_logger, NullLogger
  5  using OrderedCollections: OrderedSet
  6  
  7  @doc "Same as ccxt precision mode enums."
  8  @enum ExcPrecisionMode excDecimalPlaces = 2 excSignificantDigits = 3 excTickSize = 4
  9  
 10  @doc "Functions `f(::Exchange)` to call when an exchange is loaded"
 11  const HOOKS = Dict{Symbol,Vector{Function}}()
 12  
 13  @doc """Abstract exchange type.
 14  
 15  Defines the interface for interacting with crypto exchanges. Implemented for CCXT in CcxtExchange.
 16  """
 17  abstract type Exchange{I} end
 18  const OptionsDict = Dict{String,Dict{String,Any}}
 19  
 20  @doc """The `CcxtExchange` type wraps a ccxt exchange instance. Some attributes frequently accessed
 21  are copied over to avoid round tripping python. More attributes might be added in the future.
 22  To instantiate an exchange call `getexchange!` or `setexchange!`.
 23  
 24  """
 25  mutable struct CcxtExchange{I<:ExchangeID} <: Exchange{I}
 26      const py::Py
 27      const id::I
 28      const name::String
 29      const account::String
 30      const timeframes::OrderedSet{String}
 31      const markets::OptionsDict
 32      const types::Set{Symbol}
 33      const fees::Dict{Symbol,Union{Symbol,<:Number,<:AbstractDict}}
 34      const has::Dict{Symbol,Bool}
 35      const params::Py
 36      precision::ExcPrecisionMode
 37      _trace::Any
 38  end
 39  
 40  @doc """ Closes the given exchange.
 41  
 42  $(TYPEDSIGNATURES)
 43  
 44  This function attempts to close the given exchange if it exists. It checks if the exchange has a 'close' attribute and if so, it schedules the 'close' coroutine for execution.
 45  """
 46  function close_exc(exc::CcxtExchange)
 47      try
 48          k = (Symbol(exc.id), account(exc))
 49          if !haskey(exchanges, k) && !haskey(sb_exchanges, k) || pyisnull(e.py)
 50              return nothing
 51          end
 52          close_func = pygetattr(e, "close", nothing)
 53          if !isnothing(close_func)
 54              co = close_func()
 55              if !pyisnull(co) && pyisinstance(co, Python.gpa.pycoro_type)
 56                  task = pytask(co)
 57                  # block during precomp
 58                  if ccall(:jl_generating_output, Cint, ()) == 1
 59                      wait(task)
 60                  else
 61                      @async try
 62                          wait(task)
 63                      catch
 64                      end
 65                  end
 66              end
 67          end
 68      catch e
 69          @debug e
 70      end
 71  end
 72  
 73  Exchange() = Exchange(pybuiltins.None)
 74  @doc """ Instantiates a new `Exchange` wrapper for the provided `x` Python object.
 75  
 76  This constructs a `CcxtExchange` struct with the provided Python object.
 77  It extracts the exchange ID, name, and other metadata.
 78  It runs any registered hook functions for that exchange.
 79  It sets a finalizer to close the exchange when garbage collected.
 80  
 81  Returns the new `Exchange` instance, or an empty one if `x` is None.
 82  """
 83  function Exchange(x::Py, params=nothing, account="")
 84      id = ExchangeID(x)
 85      isnone = pyisnone(x)
 86      name = isnone ? "" : pyconvert(String, pygetattr(x, "name"))
 87      e = CcxtExchange{typeof(id)}(
 88          x,
 89          id,
 90          name,
 91          account,
 92          OrderedSet{String}(),
 93          OptionsDict(),
 94          Set{Symbol}(),
 95          Dict{Symbol,Union{Symbol,<:Number}}(),
 96          Dict{Symbol,Bool}(),
 97          @something(params, pydict()),
 98          excTickSize,
 99          nothing,
100      )
101      funcs = get(HOOKS, Symbol(id), ())::Union{Tuple{},Vector{Function}}
102      for f in funcs
103          f(e)
104      end
105      isnone ? e : finalizer(close_exc, e)
106  end
107  
108  @doc """ Converts value v to integer size with precision p.
109   $(TYPEDSIGNATURES)
110  
111  Used when converting exchange API responses to integer sizes for orders.
112  """
113  decimal_to_size(v, p::ExcPrecisionMode; exc=nothing) = begin
114      if p == excDecimalPlaces
115          if pyisinstance(v, pybuiltins.int)
116              convert(Int, v)
117          else
118              @warn "exchanges: wrong precision mode" v p exc
119              v
120          end
121      else
122          v
123      end
124  end
125  
126  Base.isempty(e::Exchange) = Symbol(e.id) === Symbol()
127  
128  @doc "The hash of an exchange object is reduced to its symbol (the function used to instantiate the object from ccxt)."
129  Base.hash(e::Exchange, u::UInt) = Base.hash(e.id, u)
130  
131  @doc "Attributes not matching the `Exchange` struct fields are forwarded to the wrapped ccxt class instance."
132  function Base.getproperty(e::E, k::Symbol) where {E<:Exchange}
133      if hasfield(E, k)
134          getfield(e, k)
135      else
136          !isempty(e) || throw("Can't access non instantiated exchange object.")
137          pyk = @pystr(k)
138          pyv = getfield(e, :py)
139          pygetattr(pyv, pyk)
140      end
141  end
142  function Base.propertynames(e::E) where {E<:Exchange}
143      (fieldnames(E)..., propertynames(e.py)...)
144  end
145  
146  _has(exc::Exchange, syms::Vararg{Symbol}) = begin
147      h = getfield(exc, :has)
148      any(s -> get(h, s, false), syms)
149  end
150  
151  _has(exc::Exchange, s::Symbol) = begin
152      h = getfield(exc, :has)
153      get(h, s, false)
154  end
155  
156  @doc """
157  Checks if the specified feature `feat` is supported by any of the exchanges available through the ccxt library.
158  
159  # Arguments
160  - `s::Symbol`: The feature to check for support across exchanges.
161  - `full::Bool=true`: If `true`, checks both static and instantiated properties of the exchange for support.
162  
163  # Returns
164  - `Vector{String}`: A list of exchange names that support the specified feature.
165  """
166  function _has(feat::Symbol; full=true)
167      supported = String[]
168      ccxt = Ccxt.ccxtws()
169      feat = string(feat)
170      for e in ccxt_exchange_names()
171          name = string(e)
172          if hasproperty(ccxt, name)
173              cls = getproperty(ccxt, name)
174              if (full && (_issupported(cls.has, feat) || _issupported(cls().has, feat))) ||
175                  _issupported(cls.has, feat)
176                  push!(supported, name)
177              end
178          end
179      end
180      supported
181  end
182  # NOTE: wrap the function here to quickly overlay methods
183  has(args...; kwargs...) = _has(args...; kwargs...)
184  _has_all(exc, what; kwargs...) = all((_has(exc, v; kwargs...)) for v in what)
185  # NOTE: wrap the function here to quickly overlay methods
186  has(exc, what::Tuple{Vararg{Symbol}}; kwargs...) = _has_all(exc, what; kwargs...)
187  
188  account(exc::Exchange) = getfield(exc, :account)
189  params(exc::Exchange) = getfield(exc, :params)
190  
191  function _first(exc::Exchange, args::Vararg{Symbol})
192      for name in args
193          if has(exc, name)
194              py = getfield(exc, :py)
195              return getproperty(py, name)
196          end
197      end
198  end
199  
200  @doc """Return the first available property from a variable number of Symbol arguments in the given Exchange.
201  
202  $(TYPEDSIGNATURES)
203  
204  This function iterates through the provided Symbols and returns the value of the first property that exists in the Exchange object."""
205  Base.first(exc::Exchange, args::Vararg{Symbol}) = _first(exc, args...)
206  
207  @doc "Global var holding Exchange instances. Used as a cache."
208  const exchanges = Dict{Tuple{Symbol,String},Exchange}()
209  @doc "Global var holding Sandbox Exchange instances. Used as a cache."
210  const sb_exchanges = Dict{Tuple{Symbol,String},Exchange}()
211  
212  _closeall() = begin
213      @sync begin
214          excs = []
215          while !isempty(exchanges)
216              _, e = pop!(exchanges)
217              push!(excs, e)
218              @async finalize(e)
219          end
220          while !isempty(sb_exchanges)
221              _, e = pop!(sb_exchanges)
222              push!(excs, e)
223              @async finalize(e)
224          end
225      end
226  end
227  
228  # atexit(_closeall)
229  Base.nameof(e::CcxtExchange) = Symbol(getfield(e, :id))
230  
231  exchange(e::Exchange, args...; kwargs...) = e
232  exchangeid(e::E) where {E<:Exchange} = getfield(e, :id)
233  
234  Base.print(out::IO, exc::Exchange) = begin
235      write(out, "Exchange: ")
236      write(out, exc.name)
237      write(out, " | ")
238      write(out, "$(length(exc.markets)) markets")
239      write(out, " | ")
240      tfs = collect(exc.timeframes)
241      write(out, "$(length(tfs)) timeframes")
242  end
243  Base.display(exc::Exchange) = print(exc)
244  Base.show(out::IO, exc::Exchange) = print(out, ":", nameof(exc))