currency.jl
1 using .Misc: MM, toprecision, DFT 2 using .Python: pybuiltins, pyisinstance, pyfloat, pyint 3 using .Instruments: AbstractCash, atleast!, Cash 4 import Instruments: value, addzero! 5 Instruments.@importcash! 6 import Base: ==, +, -, ÷, /, * 7 import .Misc: gtxzero, ltxzero, approxzero, ZERO 8 9 @doc "The cache for currencies which lasts for 1 hour by exchange." 10 const currenciesCache1Hour = safettl(ExchangeID, Py, Hour(1)) 11 @doc "This lock is only used during currency construction." 12 const currency_lock = ReentrantLock() 13 14 @doc "Convert a Python object to a float number." 15 function to_float(py::Py, T::Type{<:AbstractFloat}=DFT) 16 something(pyconvert(Option{T}, py), zero(T)) 17 end 18 to_float(v::Number) = v 19 20 @doc "Convert a Python object to a number." 21 function to_num(py::Py) 22 @something if pyisnone(py) 23 0.0 24 elseif pyisinstance(py, pybuiltins.int) 25 pyconvert(Option{Int}, py) 26 elseif pyisinstance(py, pybuiltins.float) 27 pyconvert(Option{DFT}, py) 28 elseif pyisinstance(py, (pybuiltins.tuple, pybuiltins.list)) && length(py) > 0 29 to_num(py[0]) 30 elseif pyisinstance(py, pybuiltins.str) 31 isempty(py) ? 0 : pyconvert(DFT, pyfloat(py)) 32 else 33 pyconvert(Option{DFT}, pyfloat(py)) 34 end 0.0 35 end 36 37 @doc "Returns the limits, precision, and fees for a currency as a named tuple. 38 39 $(TYPEDSIGNATURES) 40 41 The tuple fields can be nothing if the currency property is not provided. 42 " 43 function _lpf(exc, cur) 44 local limits, precision, fees 45 if isnothing(cur) || pyisnone(cur) 46 limits = (; min=1e-8, max=1e8) 47 precision = 8 48 fees = zero(DFT) 49 else 50 limits = let l = get(cur, "limits", nothing) 51 if isnothing(l) || pyisnone(l) 52 (min=1e-8, max=1e8) 53 elseif haskey(l, "amount") 54 MM{DFT}((to_float(l["amount"]["min"]), to_float(l["amount"]["max"]))) 55 else 56 (min=1e-8, max=1e8) 57 end 58 end 59 precision = let p = get(cur, "precision", nothing) 60 if isnothing(p) || pyisnone(p) 61 8 62 else 63 to_num(p) 64 end 65 end 66 fees = to_float(get(cur, "fee", nothing)) 67 end 68 (; limits, precision, fees) 69 end 70 71 @doc "Returns the currency from the exchange if found." 72 function _cur(exc, sym) 73 sym_str = uppercase(string(sym)) 74 curs = @lget! currenciesCache1Hour exc.id let v = pyfetch(exc.fetchCurrencies) 75 v isa PyException ? exc.currencies : v 76 end 77 (pyisnone(curs) || isempty(curs)) ? nothing : get(curs, sym_str, nothing) 78 end 79 80 @doc "A `CurrencyCash` contextualizes a `Cash` instance w.r.t. an exchange. 81 Operations are rounded to the currency precision. 82 83 $(FIELDS) 84 " 85 struct CurrencyCash{C<:Cash,E<:ExchangeID} <: AbstractCash 86 cash::C 87 limits::MM{DFT} 88 precision::T where {T<:Real} 89 fees::DFT 90 sandbox::Bool 91 @doc """Create a CurrencyCash object. 92 93 $(TYPEDSIGNATURES) 94 """ 95 function CurrencyCash(id::Type{<:ExchangeID}, cash_type::Type{<:Cash}, v; sandbox=false, account="") 96 @lock currency_lock begin 97 exc = getexchange!(Symbol(id); sandbox, account) 98 c = cash_type(v) 99 lpf = _lpf(exc, _cur(exc, nameof(c))) 100 Instruments.cash!(c, toprecision(c.value, lpf.precision)) 101 new{cash_type,id}(c, lpf..., issandbox(exc)) 102 end 103 end 104 function CurrencyCash(exc::Exchange, sym, v=0.0) 105 @lock currency_lock begin 106 cur = _cur(exc, sym) 107 pyisinstance(cur, pybuiltins.dict) || 108 @debug "$sym not found on $(exc.name) (using defaults)" 109 c = Cash(sym, v) 110 lpf = _lpf(exc, cur) 111 Instruments.cash!(c, toprecision(c.value, lpf.precision)) 112 new{typeof(c),typeof(exc.id)}(c, lpf..., issandbox(exc)) 113 end 114 end 115 end 116 117 function CurrencyCash{C,E}(v; sandbox=false) where {C<:Cash,E<:ExchangeID} 118 CurrencyCash(E, C, v; sandbox) 119 end 120 121 function CurrencyCash(c::CurrencyCash{C,E}, v) where {C<:Cash,E<:ExchangeID} 122 CurrencyCash(E, C, v; sandbox=c.sandbox) 123 end 124 125 @doc "The currency cash as a number." 126 value(cc::CurrencyCash) = value(cc.cash) 127 Base.getproperty(c::CurrencyCash, s::Symbol) = 128 if s == :value 129 getfield(_cash(c), :value)[] 130 elseif s == :id 131 nameof(_cash(c)) 132 else 133 getfield(c, s) 134 end 135 function Base.isapprox(cc::C, v::N) where {C<:CurrencyCash,N<:Number} 136 isapprox(value(cc), v; atol=_atol(cc)) 137 end 138 Base.isapprox(v::N, cc::C) where {C<:CurrencyCash,N<:Number} = isapprox(cc, v) 139 Base.setproperty!(::CurrencyCash, ::Symbol, v) = error("CurrencyCash is private.") 140 Base.zero(c::CurrencyCash) = zero(c.cash) 141 Base.zero(::Type{<:CurrencyCash{C}}) where {C} = zero(C) 142 function Base.iszero(c::CurrencyCash{Cash{S,T}}) where {S,T} 143 isapprox(value(c), zero(T); atol=_atol(c)) 144 end 145 146 function Base.show(io::IO, c::CurrencyCash{<:Cash,E}) where {E<:ExchangeID} 147 write(io, "$(c.cash) (on $(E.parameters[1]))") 148 end 149 Base.hash(c::CurrencyCash, args...) = hash(_cash(c), args...) 150 Base.nameof(c::CurrencyCash) = nameof(_cash(c)) 151 152 Base.promote(c::CurrencyCash, n) = promote(_cash(c), n) 153 Base.promote(n, c::CurrencyCash) = promote(n, _cash(c)) 154 155 Base.convert(::Type{T}, c::CurrencyCash) where {T<:Real} = convert(T, _cash(c)) 156 Base.isless(a::CurrencyCash, b::CurrencyCash) = isless(_cash(a), _cash(b)) 157 Base.isless(a::CurrencyCash, b::Number) = isless(promote(a, b)...) 158 Base.isless(b::Number, a::CurrencyCash) = isless(promote(b, a)...) 159 Base.isless(a::Cash, b::CurrencyCash) = isless(a, b.cash) 160 161 Base.abs(c::CurrencyCash) = _toprec(c, abs(_cash(c))) 162 Base.real(c::CurrencyCash) = _toprec(c, real(_cash(c))) 163 164 _cash(cc::CurrencyCash) = getfield(cc, :cash) 165 _prec(cc::CurrencyCash) = getfield(cc, :precision) 166 _toprec(cc::AbstractCash, v) = toprecision(v, _prec(cc)) 167 _toprec(cc::AbstractCash, c::C) where {C<:AbstractCash} = toprecision(value(c), _prec(cc)) 168 _asatol(v::F) where {F<:AbstractFloat} = v 169 _asatol(v::I) where {I<:Integer} = 1 / 10^v 170 _atol(cc::CurrencyCash) = _prec(cc) |> _asatol 171 172 -(a::CurrencyCash, b::Real) = _toprec(a, a.cash - b) 173 +(a::CurrencyCash, b::Real) = _toprec(a, a.cash + b) 174 *(a::CurrencyCash, b::Real) = _toprec(a, a.cash * b) 175 /(a::CurrencyCash, b::Real) = _toprec(a, a.cash / b) 176 ÷(a::CurrencyCash, b::Real) = a.cash ÷ b 177 178 ==(a::CurrencyCash{S}, b::CurrencyCash{S}) where {S} = _cash(b) == _cash(a) 179 ÷(a::CurrencyCash{S}, b::CurrencyCash{S}) where {S} = _cash(a) ÷ _cash(b) 180 *(a::CurrencyCash{S}, b::CurrencyCash{S}) where {S} = _toprec(a, _cash(a) * _cash(b)) 181 /(a::CurrencyCash{S}, b::CurrencyCash{S}) where {S} = _toprec(a, _cash(a) / _cash(b)) 182 +(a::CurrencyCash{S}, b::CurrencyCash{S}) where {S} = _toprec(a, _cash(a) + _cash(b)) 183 -(a::CurrencyCash{S}, b::CurrencyCash{S}) where {S} = _toprec(a, _cash(a) - _cash(b)) 184 185 _applyop!(op, c, v) = 186 let cv = getfield(_cash(c), :value) 187 cv[] = _toprec(c, op(cv[], v)) 188 end 189 190 add!(c::CurrencyCash, v) = _applyop!(+, c, v) 191 sub!(c::CurrencyCash, v) = _applyop!(-, c, v) 192 mul!(c::CurrencyCash, v) = _applyop!(*, c, v) 193 rdiv!(c::CurrencyCash, v) = _applyop!(/, c, v) 194 div!(c::CurrencyCash, v) = div!(_cash(c), v) 195 mod!(c::CurrencyCash, v) = mod!(_cash(c), v) 196 cash!(c::CurrencyCash, v) = cash!(_cash(c), _toprec(c, v)) 197 addzero!(c::CurrencyCash, v, args...; atol=_atol(c), kwargs...) = begin 198 add!(c, v) 199 atleast!(c; atol) 200 c 201 end 202 203 gtxzero(c::CurrencyCash) = gtxzero(value(c); atol=_atol(c)) 204 ltxzero(c::CurrencyCash) = ltxzero(value(c); atol=_atol(c)) 205 approxzero(c::CurrencyCash) = approxzero(value(c); atol=_atol(c))