/ Ccxt / src / module.jl
module.jl
  1  using Python
  2  using Misc: DATA_PATH, Misc
  3  using Misc.ConcurrentCollections: ConcurrentDict
  4  using Misc.Lang: @lget!, Option
  5  using Misc.DocStringExtensions
  6  using Python: pynew, pyisnone, islist, isdict
  7  using Python.PythonCall: pyisnull, pycopy!, pybuiltins
  8  using Python: py_except_name
  9  
 10  @doc "The ccxt python module reference"
 11  const ccxt = Ref{Option{Py}}(nothing)
 12  @doc "The ccxt.pro (websockets) python module reference"
 13  const ccxt_ws = Ref{Option{Py}}(nothing)
 14  @doc "Ccxt exception names"
 15  const ccxt_errors = Set{String}()
 16  
 17  @doc """ Checks if the ccxt object is initialized.
 18  
 19  $(TYPEDSIGNATURES)
 20  
 21  This function checks if the global variable `ccxt` is initialized by checking if it's not `nothing` and not `null` in the Python context.
 22  """
 23  function isinitialized()
 24      val = ccxt[]
 25      !isnothing(val) && !pyisnull(val)
 26  end
 27  
 28  @doc """ Populates the `ccxt_errors` array with error names from the ccxt library.
 29  
 30  $(TYPEDSIGNATURES)
 31  
 32  This function checks if the `ccxt_errors` array is empty. If it is, it imports the `ccxt.base.errors` module from the ccxt library, retrieves the directory of the module, and iterates over each error. It then checks if the first character of the error name is uppercase. If it is, the error name is added to the `ccxt_errors` array.
 33  """
 34  function _ccxt_errors!()
 35      if isempty(ccxt_errors)
 36          for err in pyimport("ccxt.base.errors") |> pydir
 37              name = string(err)
 38              if isuppercase(first(name))
 39                  push!(ccxt_errors, name)
 40              end
 41          end
 42      end
 43  end
 44  
 45  @doc """ Determines if a Python exception is a ccxt error.
 46  
 47  $(TYPEDSIGNATURES)
 48  """
 49  function isccxterror(err::PyException)
 50      _ccxt_errors!()
 51      py_except_name(err) ∈ ccxt_errors
 52  end
 53  @doc " The path to the markets data directory."
 54  const MARKETS_PATH = joinpath(DATA_PATH, "markets")
 55  
 56  @doc """ Initializes the Python environment and creates the markets data directory.
 57  
 58  $(TYPEDSIGNATURES)
 59  """
 60  function _init()
 61      clearpypath!()
 62      if !isinitialized()
 63          try
 64              Python._async_init(Python.PythonAsync())
 65              mkpath(MARKETS_PATH)
 66              if ccall(:jl_generating_output, Cint, ()) != 0
 67                  Python.py_stop_loop()
 68              end
 69          catch e
 70              @error e Python.PY_V ENV["PYTHONPATH"] syspath = pyimport("sys").path vinfo =
 71                  pyimport("sys").version_info
 72          end
 73      end
 74  end
 75  
 76  function _doinit()
 77      isinitialized() && return nothing
 78      if Python.isinitialized()
 79          _init()
 80      else
 81          push!(Python.CALLBACKS, _init)
 82      end
 83  end
 84  
 85  include("exchange_funcs.jl")
 86  
 87  @doc "Choose correct ccxt function according to what the exchange supports."
 88  function _multifunc(exc, suffix, hasinputs=false)
 89      py = exc.py
 90      fname = "fetch" * suffix * "sWs"
 91      if issupported(exc, fname) || begin
 92          fname = "fetch" * suffix * "s"
 93          issupported(exc, fname)
 94      end
 95          getproperty(py, fname), :multi
 96      else
 97          fname = "fetch" * suffix * "Ws"
 98          if !issupported(exc, fname)
 99              fname = "fetch" * suffix
100          end
101          @assert issupported(exc, fname) "Exchange $(exc.name) does not support $fname"
102          @assert hasinputs "Single function needs inputs."
103          getproperty(py, fname), :single
104      end
105  end
106  
107  @doc """
108  A dictionary for storing function wrappers with their unique identifiers.
109  """
110  function _out_as_input(inputs, data; elkey=nothing)
111      if islist(data)
112          if length(data) == length(inputs)
113              Dict(i => v for (v, i) in zip(data, inputs))
114          else
115              @assert !isnothing(elkey) "Functions returned a list, but element key not provided."
116              Dict(v[elkey] => v for v in data)
117          end
118      elseif isdict(data)
119          Dict(i => data[i] for i in inputs if haskey(data, i))
120      else
121          Dict(i => data for i in inputs)
122      end
123  end
124  
125  # NOTE: watch_tickers([...]) returns empty sometimes...
126  # so call without args, and select the input
127  @doc """ Chooses a function based on the provided parameters and executes it.
128  
129  $(TYPEDSIGNATURES)
130  
131  This function selects a function based on the provided exception, suffix, and inputs. It then executes the chosen function with the provided inputs and keyword arguments. The function can handle multiple types of inputs and can execute multiple functions concurrently if necessary.
132  """
133  function choosefunc(exc, suffix, inputs::AbstractVector; elkey=nothing, kwargs...)
134      hasinputs = length(inputs) > 0
135      f, kind = _multifunc(exc, suffix, hasinputs)
136      if hasinputs
137          if kind == :multi
138              function multi_func()
139                  args = isempty(inputs) ? () : (inputs,)
140                  data = pyfetch(f, args...; kwargs...)
141                  _out_as_input(inputs, data; elkey)
142              end
143          else
144              function single_func()
145                  out = Dict{eltype(inputs),Union{Task,Py}}()
146                  try
147                      for i in inputs
148                          out[i] = pytask(f(i); kwargs...)
149                      end
150                      for (i, task) in out
151                          out[i] = fetch(task)
152                      end
153                  catch e
154                      @sync for v in values(out)
155                          if v isa Task && !istaskdone(v)
156                              pycancel(v)
157                          end
158                      end
159                      e isa PyException && rethrow(e)
160                      filter!(p -> p isa Task, out)
161                  end
162                  _out_as_input(inputs, out; elkey)
163              end
164          end
165      else
166          args = isempty(inputs) ? () : (inputs,)
167          default_func() = pyfetch(f, args...; kwargs...)
168      end
169  end
170  
171  function choosefunc(exc, suffix, inputs...; kwargs...)
172      choosefunc(exc, suffix, [inputs...]; kwargs...)
173  end
174  
175  @doc """ Upgrades the ccxt library to the latest version.
176  
177  $(TYPEDSIGNATURES)
178  
179  This function upgrades the ccxt library to the latest version available. It checks the current version of the ccxt library, and if a newer version is available, it upgrades the library using pip.
180  """
181  function upgrade()
182      @eval begin
183          version = pyimport("ccxt").__version__
184          using Python.PythonCall.C.CondaPkg: CondaPkg
185          try
186              CondaPkg.add_pip("ccxt"; version=">$version")
187          catch
188              # if the version is latest than we have to adjust
189              # the version to GTE
190              CondaPkg.add_pip("ccxt"; version=">=$version")
191          end
192      end
193      Python.pyimport("ccxt").__version__
194  end
195  
196  export ccxt, ccxt_ws, isccxterror, ccxt_exchange, choosefunc