/ Exchanges / src / leverage.jl
leverage.jl
  1  using .Python: PyException, pyisTrue, pygetitem, pyeq, @py, pyfetch_timeout
  2  using Data: Cache, tobytes, todata
  3  using Data.DataStructures: SortedDict
  4  using Instruments: splitpair
  5  using .Misc: IsolatedMargin, CrossMargin, Long, Short
  6  import .Misc.marginmode
  7  
  8  # TODO: export to livemode
  9  resp_code(resp, ::Type{<:ExchangeID}) = pygetitem(resp, @pyconst("code"), @pyconst(""))
 10  function _handle_leverage(e::Exchange, resp)
 11      if resp isa PyException
 12          if occursin("not modified", string(resp))
 13              true
 14          else
 15              @warn "exchanges: set leverage error" e resp
 16              false
 17          end
 18      else
 19          resptobool(e, resp)
 20      end
 21  end
 22  
 23  leverage_value(::Exchange, val, sym) = string(round(float(val), digits=2))
 24  @doc "Update the leverage for a specific symbol.
 25  
 26  $(TYPEDSIGNATURES)
 27  
 28  - `exc`: an Exchange object to update the leverage on.
 29  - `v`: a Real number representing the new leverage value.
 30  - `sym`: a string representing the symbol to update the leverage for.
 31  "
 32  function leverage!(exc::Exchange, v, sym; side=Long(), timeout=Second(5))
 33      lev = leverage_value(exc, v, sym)
 34      set_func = first(exc, :setLeverage)
 35      if isnothing(set_func)
 36          @warn "exchanges: set leverage not supported" exc
 37          return false
 38      end
 39      resp = pyfetch_timeout(set_func, Returns(nothing), timeout, lev, sym)
 40      if isnothing(resp)
 41          @warn "exchanges: set leverage timedout" sym lev = v exc
 42          false
 43      else
 44          success = _handle_leverage(exc, resp)
 45          if !success
 46              # TODO: support `fetchLeverages` with caching?
 47              fetch_func = first(exc, :fetchLeverage)
 48              if isnothing(fetch_func)
 49                  @warn "exchange: can't check leverage" exc
 50                  return false
 51              end
 52              resp_lev = pyfetch_timeout(fetch_func, Returns(nothing), timeout, sym)
 53              if isnothing(resp_lev)
 54                  false
 55              elseif resp_lev isa Exception
 56                  @error "exchanges: set leverage" exception = resp_lev
 57                  false
 58              else
 59                  side_key = ifelse(side == Long(), "longLeverage", "shortLeverage")
 60                  resp_val = pytofloat(get(resp_lev, side_key, Base.NaN))
 61                  pytofloat(lev) == resp_val
 62              end
 63          else
 64              true
 65          end
 66      end
 67  end
 68  
 69  @doc """A type representing a tier of leverage.
 70  
 71  $(FIELDS)
 72  
 73  This type is used to store and manage information about a specific leverage tier. Each tier is defined by its minimum and maximum notional values, maximum leverage, tier number, and maintenance margin requirement.
 74  """
 75  @kwdef struct LeverageTier{T<:Real}
 76      min_notional::T
 77      max_notional::T
 78      max_leverage::T
 79      tier::Int
 80      mmr::T
 81      bc::Symbol
 82  end
 83  LeverageTier(args...; kwargs...) = LeverageTier{Float64}(args...; kwargs...)
 84  @doc "Every asset has a list of leverage tiers, that are stored in a SortedDict, if the exchange supports them."
 85  const LeverageTiersDict = SortedDict{Int,LeverageTier}
 86  @doc "Leverage tiers are cached both in RAM and storage."
 87  const leverageTiersCache = Dict{String,LeverageTiersDict}()
 88  @doc """Returns a default leverage tier for a specific symbol.
 89  
 90  $(TYPEDSIGNATURES)
 91  
 92  The default leverage tier has generous limits.
 93  """
 94  function default_leverage_tier(sym)
 95      SortedDict(
 96          1 => LeverageTier(;
 97              min_notional=1e-8,
 98              max_notional=2e6,
 99              max_leverage=100.0,
100              tier=1,
101              mmr=0.005,
102              bc=Symbol(string(splitpair(sym)[1])),
103          ),
104      )
105  end
106  
107  _tierskey(exc, sym) = "$(exc.name)/$(sym)"
108  @doc """Fetch the leverage tiers for a specific symbol from an exchange.
109  
110  $(TYPEDSIGNATURES)
111  
112  - `exc`: an Exchange object to fetch the leverage tiers from.
113  - `sym`: a string representing the symbol to fetch the leverage tiers for.
114  """
115  function leverage_tiers(exc::Exchange, sym::AbstractString)
116      k = _tierskey(exc, sym)
117      @lget! leverageTiersCache k begin
118          ans = Cache.load_cache(k; raise=false)
119          if isnothing(ans)
120              pytiers = pyfetch(exc.fetchMarketLeverageTiers, Val(:try), sym)
121              if pytiers isa PyException || isnothing(pytiers)
122                  @warn "Couldn't fetch leverage tiers for $sym from $(exc.name). Using defaults. ($pytiers)"
123                  ans = default_leverage_tier(sym)
124              else
125                  tiers = pyconvert(Vector{Dict{String,Any}}, pytiers)
126                  ans = SortedDict{Int,LeverageTier}(
127                      let tier = pyconvert(Int, t["tier"])
128                          delete!(t, "info")
129                          tier => LeverageTier(;
130                              min_notional=t["minNotional"],
131                              max_notional=t["maxNotional"],
132                              max_leverage=something(t["maxLeverage"], 100.0),
133                              tier,
134                              mmr=t["maintenanceMarginRate"],
135                              bc=Symbol(t["currency"]),
136                          )
137                      end for t in tiers
138                  )
139              end
140              Cache.save_cache(k, ans)
141          end
142          ans
143      end
144  end
145  
146  @doc """Get the leverage tier for a specific size from a sorted dictionary of tiers.
147  
148  $(TYPEDSIGNATURES)
149  
150  - `tiers`: a SortedDict where the keys are integers representing the size thresholds and the values are LeverageTier objects.
151  - `size`: a Real number representing the size to fetch the tier for.
152  
153  """
154  function tier(tiers::SortedDict{Int,LeverageTier}, size::Real)
155      idx = findfirst(t -> t.max_notional > abs(size), tiers)
156      idx, tiers[@something idx lastindex(tiers)]
157  end
158  
159  @doc """Get the maximum leverage for a specific size and symbol from an exchange.
160  
161  $(TYPEDSIGNATURES)
162  
163  - `exc`: an Exchange object to fetch the maximum leverage from.
164  - `sym`: a string representing the symbol to fetch the maximum leverage for.
165  - `size`: a Real number representing the size to fetch the maximum leverage for.
166  
167  """
168  function maxleverage(exc::Exchange, sym::AbstractString, size::Real)
169      tiers = leverage_tiers(exc, sym)
170      _, t = tier(tiers, size)
171      t.max_leverage
172  end
173  
174  # CCXT strings
175  Base.string(::Union{T,Type{T}}) where {T<:IsolatedMargin} = "isolated"
176  Base.string(::Union{T,Type{T}}) where {T<:CrossMargin} = "cross"
177  Base.string(::Union{T,Type{T}}) where {T<:NoMargin} = "nomargin"
178  
179  function dosetmargin(exc::Exchange, mode_str, symbol; kwargs...)
180      resp = pyfetch(exc.setMarginMode, mode_str, symbol)
181      resptobool(exc, resp)
182  end
183  
184  @doc "Update margin mode for a specific symbol on the exchange.
185  
186  Also sets if the position is hedged or one sided.
187  For customizations, dispatch to `dosetmargin`.
188  
189  $(TYPEDSIGNATURES)
190  "
191  function marginmode!(exc::Exchange, mode, symbol; hedged=false, kwargs...)
192      mode_str = string(mode)
193      if mode_str in ("isolated", "cross")
194          exc.options["defaultMarginMode"] = mode_str
195          if !isempty(symbol)
196              ans = dosetmargin(exc, mode_str, symbol; hedged, kwargs...)
197              if ans isa Bool
198                  return ans
199              else
200                  @error "failed to set margin mode" exc = nameof(exc) err = ans
201                  return false
202              end
203          else
204              return true
205          end
206      elseif mode_str == "nomargin"
207          return true
208      else
209          error("Invalid margin mode $mode")
210      end
211  end
212  
213  marginmode(exc::Exchange) = get(exc.options, "defaultMarginMode", NoMargin())