tickers.jl
1 using .Misc.Lang: @get, @multiget, @lget!, Option, safenotify, safewait 2 using .Misc: config, NoMargin, DFT 3 using .Misc.ConcurrentCollections: ConcurrentDict 4 using .Misc: waitforcond 5 using Instruments: isfiatquote, spotpair 6 using .Python: @pystr, @pyconst, pyfetch_timeout, pylist, pytruth 7 using ExchangeTypes: decimal_to_size, excDecimalPlaces, excSignificantDigits, excTickSize 8 9 @doc """A leveraged pair is a pair like `BTC3L/USD`. 10 - `:yes` : Leveraged pairs will not be filtered. 11 - `:only` : ONLY leveraged will be kept. 12 - `:from` : Selects non leveraged pairs, that also have at least one leveraged sibling. 13 """ 14 const LEVERAGED_PAIR_OPTIONS = (:yes, :only, :from) 15 16 @doc """Quote id of the market.""" 17 quoteid(mkt) = @multiget mkt "quoteId" "quote" "n/a" 18 @doc "True if `id` is a quote id." 19 isquote(id, qc) = lowercase(id) == qc 20 @doc "True if `mkt` is a leveraged market." 21 ismargin(mkt) = Bool(@get mkt "margin" false) 22 23 @doc "True if `pair` is a leveraged pair." 24 function has_leverage(pair, pairs_with_leverage) 25 !isleveragedpair(pair) && pair ∈ pairs_with_leverage 26 end 27 @doc "Constructor that returns a function that checks if a pair is leveraged." 28 function leverage_func(exc, with_leveraged, verbose=true) 29 # Leveraged `:from` filters the pairlist taking non leveraged pairs, IF 30 # they have a leveraged counterpart 31 if with_leveraged == :from 32 verbose && @warn "Filtering by leveraged, are you sure?" 33 pairs_with_leverage = Set() 34 for k in keys(exc.markets) 35 dlv = deleverage_pair(k) 36 k !== dlv && push!(pairs_with_leverage, dlv) 37 end 38 (pair) -> has_leverage(pair, pairs_with_leverage) 39 else 40 Returns(true) 41 end 42 end 43 @doc "True if symbol `sym` has a quote volume less than `min_vol`." 44 function hasvolume(sym, spot; tickers, min_vol) 45 if spot ∈ keys(tickers) 46 quotevol(tickers[spot]) <= min_vol 47 else 48 quotevol(tickers[sym]) <= min_vol 49 end 50 end 51 52 marketsid(args...; kwargs...) = error("not implemented") 53 @doc "Get the exchange market ids." 54 marketsid(exc::Exchange, args...; kwargs...) = keys(tickers(exc, args...; kwargs...)) 55 @doc "Get the tickers matching quote currency `quot`." 56 tickers(quot::Symbol, args...; kwargs...) = tickers(exc, quot, args...; kwargs...) 57 58 aspair(k, v) = k => v 59 askey(k, _) = k 60 asvalue(_, v) = v 61 62 @doc """Get the exchange tickers. 63 64 $(TYPEDSIGNATURES) 65 66 - `exc`: an Exchange object to fetch the tickers from. 67 - `quot`: only choose pairs where the quote currency equals `quot`. 68 - `min_vol`: the minimum volume of each pair. 69 - `skip_fiat` (optional, default is true): ignore fiat/fiat pairs. 70 - `with_margin` (optional, default is the result of `config.margin != NoMargin()`): only choose pairs enabled for margin trading. 71 - `with_leverage` (optional, default is `:no`): if `:no`, skip all pairs where the base currency matches the `leverage_pair_rgx` regex. 72 - `as_vec` (optional, default is false): return the pair list as a Vector instead of as a Dict. 73 - `verbose` (optional, default is true): print detailed output about the operation. 74 - `type` (optional, default is the result of `markettype(exc)`): the type of markets to fetch tickers for. 75 - `cross_match` list of other exchanges where the filter pairs must also be present in 76 77 """ 78 function tickers( 79 exc::Exchange, 80 quot; 81 min_vol, 82 skip_fiat=true, 83 with_margin=config.margin != NoMargin(), 84 with_leverage=:no, 85 as_vec=false, 86 verbose=true, 87 type=markettype(exc), 88 cross_match::Tuple{Vararg{Symbol}}=(), 89 ) # ::Union{Dict,Vector} 90 # swap exchange in case of futures 91 @tickers! type 92 pairlist = [] 93 quot = string(quot) 94 95 lquot = lowercase(quot) 96 97 as = ifelse(as_vec, askey, aspair) 98 pushas(p, k, v, _) = push!(p, as(k, v)) 99 pushifquote(p, k, v, q) = isquote(quoteid(v), q) && pushas(p, k, v, nothing) 100 addto = ifelse(isempty(quot), pushas, pushifquote) 101 leverage_check = leverage_func(exc, with_leverage, verbose) 102 notinmarket(sym) = any(sym ∉ keys(getexchange!(e).markets) for e in cross_match) 103 # TODO: all this checks should be decomposed into functions transducer style 104 function skip_check(sym, spot, islev, mkt) 105 notinmarket(sym) || 106 (with_leverage == :no && islev) || 107 (with_leverage == :only && !islev) || 108 !leverage_check(spot) || 109 hasvolume(sym, spot; tickers, min_vol) || 110 (skip_fiat && isfiatpair(spot)) || 111 (with_margin && Bool(@get(mkt, "margin", false))) 112 end 113 114 let markets = exc.markets 115 for (sym, mkt) in tickers 116 mkt = get(markets, sym, nothing) 117 isnothing(mkt) && continue 118 spot = spotsymbol(sym, mkt) 119 islev = isleveragedpair(spot) 120 skip_check(sym, spot, islev, mkt) && continue 121 addto(pairlist, sym, mkt, lquot) 122 end 123 end 124 125 function result(pairlist, as_vec) 126 isempty(pairlist) && 127 verbose && 128 @warn "No pairs found, check quote currency ($quot) and min volume parameters ($min_vol)." 129 isempty(quot) && return pairlist 130 if as_vec 131 unique!(pairlist) 132 return sort!(pairlist; by=k -> quotevol(tickers[k])) 133 end 134 Dict(pairlist) 135 end 136 result(pairlist, as_vec) 137 end 138 139 @doc "Caches markets (1minute)." 140 const marketsCache1Min = safettl(String, Py, Minute(1)) 141 @doc "Caches tickers (10seconds)." 142 const tickersCache10Sec = safettl(String, Py, Second(10)) 143 @doc "Caches active states (1minute)." 144 const activeCache1Min = safettl(String, Bool, Minute(1)) 145 @doc "Lock held when fetching tickers (per ticker)." 146 const tickersLockDict = ConcurrentDict(Dict{String,ReentrantLock}()) 147 @doc "Retrieves a cached market (1minute) or fetches it from exchange. 148 149 $(TYPEDSIGNATURES) 150 " 151 function market!(pair, exc::Exchange) 152 @lget! marketsCache1Min pair exc.py.market(pair) 153 end 154 market!(a::AbstractAsset, args...) = market!(a.raw, args...) 155 156 _tickerfunc(exc) = first(exc, :fetchTickerWs, :fetchTicker) 157 @doc """Fetch the ticker for a specific pair from an exchange. 158 159 $(TYPEDSIGNATURES) 160 161 The `ticker!` function takes the following parameters: 162 163 - `pair`: a string representing the currency pair to fetch the ticker for. 164 - `exc`: an Exchange object to fetch the ticker from. 165 - `timeout` (optional, default is 3 seconds): the maximum time to wait for the ticker fetch operation. 166 - `func` (optional, default is the result of `_tickerfunc(exc)`): the function to use to fetch the ticker. 167 """ 168 function ticker!( 169 pair, exc::Exchange; timeout=Second(3), func=_tickerfunc(exc), delay=Second(1) 170 ) 171 l = @lget!(tickersLockDict, pair, ReentrantLock()) 172 waitforcond(l.cond_wait, timeout) 173 if islocked(l) 174 waitforcond(l.cond_wait, timeout) 175 return @get tickersCache10Sec pair pydict() 176 else 177 @lock l begin 178 fetch_func = first(exc, :fetchTicker) 179 @lget! tickersCache10Sec pair begin 180 v = nothing::Option{Py} 181 tries = 0 182 while tries < 3 183 tries += 1 184 def_func = pyisTrue(func == fetch_func) ? Returns(missing) : fetch_func 185 v = pyfetch_timeout(func, exc.fetchTicker, timeout, pair) 186 if v isa PyException 187 @error "Fetch ticker error: $v" offline = isoffline() func pair 188 v = pylist() 189 isoffline() && break 190 else 191 break 192 end 193 sleep(delay) 194 end 195 safenotify(l.cond_wait) 196 v 197 end 198 end 199 end 200 end 201 ticker!(a::AbstractAsset, args...; kwargs...) = ticker!(a.raw, args...; kwargs...) 202 @doc """Fetch the latest price for a specific pair from an exchange. 203 204 $(TYPEDSIGNATURES) 205 206 - `pair`: a string representing the currency pair to fetch the latest price for. 207 - `exc`: an Exchange object to fetch the latest price from. 208 - `kwargs` (optional): any additional keyword arguments are passed on to the underlying fetch operation. 209 """ 210 function lastprice(pair::AbstractString, exc::Exchange; kwargs...) 211 tick = ticker!(pair, exc; kwargs...) 212 lastprice(exc, tick, pair) 213 end 214 215 function lastprice(exc::Exchange, tick, pair="") 216 if !pytruth(tick) 217 sym = try 218 @coalesce get(tick, "symbol", missing) pair 219 catch 220 end 221 @warn "exchanges: failed to fetch ticker" pair nameof(exc) 222 0.0 223 else 224 lp = tick["last"] 225 if !pytruth(lp) 226 ask = tick["ask"] 227 bid = tick["bid"] 228 if pytruth(ask) && pytruth(bid) 229 (pytofloat(ask) + pytofloat(bid)) / 2 230 else 231 close = tick["close"] 232 if pytruth(close) 233 pytofloat(close) 234 else 235 vwap = tick["vwap"] 236 if pytruth(vwap) 237 pytofloat(vwap) 238 else 239 high = tick["high"] 240 low = tick["low"] 241 if pytruth(high) && pytruth(low) 242 (pytofloat(high) + pytofloat(low)) / 2 243 else 244 @warn "lastprice failed" nameof(exc) get(tick, "symbol", "") 245 0.0 246 end 247 end 248 end 249 end 250 else 251 lp |> pytofloat 252 end 253 end 254 end 255 256 function default_amount_precision(exc) 257 if exc.precision == excDecimalPlaces 258 8 259 elseif exc.precision == excSignificantDigits 260 9 261 elseif exc.precision == excTickSize 262 1e-8 263 end 264 end 265 266 function default_price_precision(exc) 267 if exc.precision == excDecimalPlaces 268 2 269 elseif exc.precision == excSignificantDigits 270 3 271 elseif exc.precision == excTickSize 272 1e-2 273 end 274 end 275 276 function _get_precision(exc, mkt, k) 277 v = mkt[k] 278 if !pyisnone(v) 279 pytofloat(v) 280 elseif k in ("amount", "base") 281 default_amount_precision(exc) 282 else # price cost quote 283 default_price_precision(exc) 284 end 285 end 286 287 @doc "Precision of the (base, quote) currencies of the market. 288 289 $(TYPEDSIGNATURES) 290 " 291 function market_precision(pair::AbstractString, exc::Exchange) 292 mkt = exc.markets[pair]["precision"] 293 p_amount = decimal_to_size(_get_precision(exc, mkt, "amount"), exc.precision; exc) 294 p_price = decimal_to_size(_get_precision(exc, mkt, "price"), exc.precision; exc) 295 (; amount=p_amount, price=p_price) 296 end 297 market_precision(a::AbstractAsset, args...) = market_precision(a.raw, args...) 298 299 py_str_to_float(n::DFT) = n 300 function py_str_to_float(py::Py) 301 (x -> Base.parse(Float64, x))(pyconvert(String, py)) 302 end 303 304 const DEFAULT_LEVERAGE = (; min=0.0, max=100.0) 305 const DEFAULT_AMOUNT = (; min=1e-15, max=Inf) 306 const DEFAULT_PRICE = (; min=1e-15, max=Inf) 307 const DEFAULT_COST = (; min=1e-15, max=Inf) 308 const DEFAULT_FIAT_COST = (; min=1e-8, max=Inf) 309 310 _min_from_precision(::Nothing) = nothing 311 _min_from_precision(v::Int) = 1.0 / 10.0^v 312 _min_from_precision(v::Real) = v 313 function _minmax_pair(mkt, l, prec, default) 314 k = string(l) 315 Symbol(l) => (; 316 min=(@something pyconvert(Option{DFT}, get(mkt[k], "min", nothing)) _min_from_precision( 317 prec 318 ) default.min), 319 max=(@something pyconvert(Option{DFT}, get(mkt[k], "max", nothing)) default.max), 320 ) 321 end 322 323 @doc """Fetch the market limits for a specific pair from an exchange. 324 325 $(TYPEDSIGNATURES) 326 327 - `pair`: a string representing the currency pair to fetch the market limits for. 328 - `exc`: an Exchange object to fetch the market limits from. 329 - `precision` (optional, default is `price=nothing, amount=nothing`): a named tuple specifying the precision for price and amount. 330 - `default_leverage` (optional, default is `DEFAULT_LEVERAGE`): the default leverage to use if not specified in the market data. 331 - `default_amount` (optional, default is `DEFAULT_AMOUNT`): the default amount to use if not specified in the market data. 332 - `default_price` (optional, default is `DEFAULT_PRICE`): the default price to use if not specified in the market data. 333 - `default_cost` (optional, default is `DEFAULT_COST` for non-fiat quote pairs and `DEFAULT_FIAT_COST` for fiat quote pairs): the default cost to use if not specified in the market data. 334 """ 335 function market_limits( 336 pair::AbstractString, 337 exc::Exchange; 338 precision=(; price=nothing, amount=nothing), 339 default_leverage=DEFAULT_LEVERAGE, 340 default_amount=DEFAULT_AMOUNT, 341 default_price=DEFAULT_PRICE, 342 default_cost=(isfiatquote(pair) ? DEFAULT_FIAT_COST : DEFAULT_COST), 343 ) 344 mkt = exc.markets[pair]["limits"] 345 (; 346 ( 347 _minmax_pair(mkt, "leverage", nothing, default_leverage), 348 _minmax_pair(mkt, "amount", precision.amount, default_amount), 349 _minmax_pair(mkt, "price", precision.price, default_price), 350 _minmax_pair(mkt, "cost", nothing, default_cost), 351 )... 352 ) 353 end 354 function market_limits(a::AbstractAsset, args...; kwargs...) 355 market_limits(a.raw, args...; kwargs...) 356 end 357 358 @doc """Check if a currency pair is active on an exchange. 359 360 $(TYPEDSIGNATURES) 361 """ 362 function is_pair_active(pair::AbstractString, exc::Exchange) 363 @lget! activeCache1Min pair begin 364 pyconvert(Bool, market!(pair, exc)["active"]) 365 end 366 end 367 is_pair_active(a::AbstractAsset, args...) = is_pair_active(a.raw, args...) 368 369 _default_fees(exc, side) = @something get(exc.fees, side, nothing) 0.01 370 function _fees_byside(exc, mkt, side) 371 @something get(mkt, string(side), nothing) _default_fees(exc, Symbol(side)) 372 end 373 @doc """Fetch the market fees for a specific pair from an exchange. 374 375 $(TYPEDSIGNATURES) 376 377 - `pair`: a string representing the currency pair to fetch the market fees for. 378 - `exc` (optional, default is the current exchange): an Exchange object to fetch the market fees from. 379 - `only_taker` (optional, default is `nothing`): a boolean indicating whether to fetch only the taker fee. If `nothing`, both maker and taker fees are fetched. 380 381 """ 382 function market_fees( 383 pair::AbstractString, exc::Exchange; only_taker::Union{Bool,Nothing}=nothing 384 ) 385 m = exc.markets[pair] 386 if isnothing(only_taker) 387 taker = get(m, "taker", nothing) 388 if isnothing(taker) 389 # Fall back to fees from spot market 390 m = get(exc.markets, spotpair(pair), nothing) 391 if isnothing(m) 392 # always ensure 393 @warn "Failed to fetch $pair fees from $(exc.name), using default fees." 394 taker = _default_fees(exc, :taker) 395 maker = _default_fees(exc, :maker) 396 else 397 taker = _fees_byside(exc, m, :taker) 398 maker = _fees_byside(exc, m, :maker) 399 end 400 else 401 maker = _fees_byside(exc, m, :maker) 402 end 403 (; taker, maker, min=min(taker, maker), max=max(taker, maker)) 404 elseif only_taker 405 _fees_byside(exc, m, :taker) 406 else 407 _fees_byside(exc, m, :maker) 408 end 409 end 410 market_fees(a::AbstractAsset, args...; kwargs...) = market_fees(a.raw, args...; kwargs...)