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))