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