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