trades.jl
1 import .SimMode: maketrade, trade! 2 using .SimMode: @maketrade, iscashenough, cost 3 using .Misc.TimeToLive: safettl 4 using .Misc: toprecision 5 6 @doc """ Checks and filters trades based on a timestamp. 7 8 $(TYPEDSIGNATURES) 9 10 The function checks if the response is a list. If not, it issues a warning and returns `nothing`. 11 If a `since` timestamp is provided, it filters out trades that occurred before this timestamp. 12 The function returns the filtered list of trades or the original response if no `since` timestamp is provided. 13 14 """ 15 function _check_and_filter(resp; ai, since, kind="") 16 if resp isa Vector 17 if isnothing(since) 18 resp 19 else 20 filter(t -> trade_timestamp(t, exchangeid(ai)) >= since, resp) 21 end 22 elseif pyisinstance(resp, pybuiltins.list) 23 if isnothing(since) 24 resp 25 else 26 out = pylist() 27 for t in resp 28 trade_timestamp(t, exchangeid(ai)) >= since && out.append(t) 29 end 30 out 31 end 32 else 33 @warn "Couldn't fetch $kind trades for $(raw(ai))" 34 return nothing 35 end 36 end 37 38 function trade_timestamp(v, eid::EIDType) 39 pyconvert(Int, resp_trade_timestamp(v, eid)) |> TimeTicks.dtstamp 40 end 41 42 @doc """ Fetches and filters the user's trades. 43 44 $(TYPEDSIGNATURES) 45 46 The function fetches the user's trades using the `fetch_my_trades` function. 47 If a `since` timestamp is provided, it filters out trades that occurred before this timestamp using the `_check_and_filter` function. 48 The function returns the filtered list of trades or the original response if no `since` timestamp is provided. 49 50 """ 51 function live_my_trades(s::LiveStrategy, ai; since=nothing, kwargs...) 52 resp = fetch_my_trades(s, ai; since, kwargs...) 53 _check_and_filter(resp; ai, since) 54 end 55 56 @doc """ Fetches and filters trades for a specific order 57 58 $(TYPEDSIGNATURES) 59 60 This function fetches the trades associated with a specific order using the `fetch_order_trades` function. 61 If a `since` timestamp is provided, it filters out trades that occurred before this timestamp using the `_check_and_filter` function. 62 The function returns the filtered list of trades or the original response if no `since` timestamp is provided. 63 64 """ 65 function live_order_trades(s::LiveStrategy, ai, id; since=nothing, kwargs...) 66 resp = fetch_order_trades(s, ai, id; since, kwargs...) 67 _check_and_filter(resp; ai, since, kind="order") 68 end 69 70 @doc "A named tuple representing the ccxt fields of a trade." 71 const Trf = NamedTuple( 72 Symbol(f) => f for f in ( 73 "id", 74 "timestamp", 75 "datetime", 76 "symbol", 77 "order", 78 "type", 79 "side", 80 "takerOrMaker", 81 "price", 82 "amount", 83 "cost", 84 "fee", 85 "fees", 86 "currency", 87 "rate", 88 ) 89 ) 90 91 function inlimits(v, ai, lim_sym) 92 lims = ai.limits 93 min = getproperty(lims, lim_sym).min 94 max = getproperty(lims, lim_sym).max 95 96 if v < min || v > max 97 @warn "create trade: value outside bounds" lim_sym v min max 98 false 99 else 100 true 101 end 102 end 103 104 @doc "A cache for storing market symbols by currency with a time-to-live of 360 seconds." 105 const MARKETS_BY_CUR = safettl(Tuple{ExchangeID,String}, Vector{String}, Second(360)) 106 function anyprice(cur::String, sym, exc) 107 try 108 v = lastprice(sym, exc) 109 v <= zero(v) || return v 110 for sym in @lget! MARKETS_BY_CUR (exchangeid(exc), cur) [ 111 k for k in Iterators.reverse(collect(keys(exc.markets))) if startswith(k, cur) 112 ] 113 v = lastprice(sym, exc) 114 v <= zero(v) || return v 115 end 116 return 0.0 117 catch 118 return 0.0 119 end 120 end 121 122 _feebysign(rate, cost) = rate >= 0.0 ? cost : -cost 123 @doc """ Calculates the fee from a fee dictionary 124 125 $(TYPEDSIGNATURES) 126 127 This function calculates the fee from a fee dictionary. 128 It retrieves the rate and cost from the fee dictionary and then uses the `_feebysign` function to calculate the fee based on the rate and cost. 129 130 """ 131 function _getfee(fee_dict, cost=get_float(fee_dict, "cost")) 132 rate = get_float(fee_dict, "rate") 133 _feebysign(rate, cost) 134 end 135 136 @doc """ Determines the fee cost based on the currency 137 138 $(TYPEDSIGNATURES) 139 140 This function determines the fee cost based on the currency specified in the fee dictionary. 141 If the currency matches the quote currency, it returns the fee in quote currency. 142 If the currency matches the base currency, it returns the fee in base currency. 143 If the currency doesn't match either, it returns zero for both. 144 145 """ 146 function _feecost( 147 fee_dict, ai, ::EIDType=exchangeid(ai); qc_py=@pystr(qc(ai)), bc_py=@pystr(bc(ai)) 148 ) 149 cur = get_py(fee_dict, "currency") 150 @debug "live fee cost" _module = LogCreateTrade cur qc_py bc_py 151 if pyeq(Bool, cur, qc_py) 152 @debug "live fee cost: quote currency" _module = LogCreateTrade _getfee(fee_dict) 153 (_getfee(fee_dict), 0.0) 154 elseif pyeq(Bool, cur, bc_py) 155 @debug "live fee cost: base currency" _module = LogCreateTrade _getfee(fee_dict) 156 (0.0, _getfee(fee_dict)) 157 else 158 (0.0, 0.0) 159 end 160 end 161 162 # This tries to always convert the fees in quote currency 163 # function _feecost_quote(s, ai, bc_price, date, qc_py=@pystr(qc(ai)), bc_py=@pystr(bc(ai))) 164 # if pyeq(Bool, cur, bc_py) 165 # _feebysign(rate, cost * bc_price) 166 # else 167 # # Fee currency is neither quote nor base, fetch the price from the candle 168 # # of the related spot pair with the trade quote currency at the trade date 169 # try 170 # spot_pair = "$cur/$qc_py" 171 # price = @something priceat(s, ai, date; sym=spot_pair, step=:close) anyprice( 172 # string(cur), spot_pair, exchange(ai) 173 # ) 174 # _feebysign(rate, cost * price) 175 # catch 176 # end 177 # end 178 # end 179 180 @doc """ Determines the currency of the fee based on the order side 181 182 $(TYPEDSIGNATURES) 183 184 This function determines the currency of the fee based on the side of the order. 185 It uses the `feeSide` property of the market associated with the order. 186 The function returns `:base` if the fee is in the base currency and `:quote` if the fee is in the quote currency. 187 188 """ 189 function trade_feecur(ai, side::Type{<:OrderSide}) 190 # Default to get since it should be the most common 191 feeside = get(market(ai), "feeSide", "get") 192 if feeside == "get" 193 if side == Buy 194 :base 195 else 196 :quote 197 end 198 elseif feeside == "give" 199 if side == Sell 200 :base 201 else 202 :quote 203 end 204 elseif feeside == "quote" 205 :quote 206 elseif feeside == "base" 207 :base 208 else 209 :quote 210 end 211 end 212 213 @doc """ Calculates the default trade fees based on the order side 214 215 $(TYPEDSIGNATURES) 216 217 This function calculates the default trade fees based on the side of the order and the current market conditions. 218 It uses the `trade_feecur` function to determine the currency of the fee and then calculates the fee based on the amount and cost of the trade. 219 220 """ 221 function _default_trade_fees( 222 ai, side::Type{<:OrderSide}; fees_base, fees_quote, actual_amount, net_cost 223 ) 224 feecur = trade_feecur(ai, side) 225 default_fees = maxfees(ai) 226 if feecur == :base 227 fees_base += actual_amount * default_fees 228 else 229 fees_quote += net_cost * default_fees 230 end 231 (fees_quote, fees_base) 232 end 233 234 market(ai) = exchange(ai).markets[raw(ai)] 235 @doc """ Determines the trade fees based on the response and side of the order 236 237 $(TYPEDSIGNATURES) 238 239 This function determines the trade fees based on the response from the exchange and the side of the order. 240 It first checks if the response contains a fee dictionary. If it does, it calculates the fee cost based on the dictionary. 241 If the response does not contain a fee dictionary but contains a list of fees, it calculates the total fee cost from the list. 242 If the response does not contain either, it calculates the default trade fees. 243 244 """ 245 function _tradefees(resp, side, ai; actual_amount, net_cost) 246 eid = exchangeid(ai) 247 v = resp_trade_fee(resp, eid) 248 if pyisinstance(v, pybuiltins.dict) 249 @debug "live trade fees: " _module = LogCreateTrade _feecost(v, ai, eid) 250 return _feecost(v, ai, eid) 251 end 252 v = resp_trade_fees(resp, eid) 253 fees_quote, fees_base = 0.0, 0.0 254 if pyisinstance(v, pybuiltins.list) && !isempty(v) 255 qc_py = @pystr(qc(ai)) 256 bc_py = @pystr(bc(ai)) 257 for fee in v 258 (q, b) = _feecost(fee, ai, eid; qc_py, bc_py) 259 fees_quote += q 260 fees_base += b 261 end 262 end 263 if iszero(fees_quote) && iszero(fees_base) 264 (fees_quote, fees_base) = _default_trade_fees( 265 ai, side; fees_base, fees_quote, actual_amount, net_cost 266 ) 267 end 268 @debug "live trade fees" _module = LogCreateTrade fees_quote fees_base 269 return (fees_quote, fees_base) 270 end 271 272 _addfees(net_cost, fees_quote, ::IncreaseOrder) = net_cost + fees_quote 273 _addfees(net_cost, fees_quote, ::ReduceOrder) = net_cost - fees_quote 274 275 @doc """ Checks if the trade symbol matches the order symbol 276 277 $(TYPEDSIGNATURES) 278 279 This function checks if the trade symbol from the response matches the symbol of the order. 280 If they do not match, it issues a warning and returns `false`. 281 282 """ 283 function isordersymbol(ai, o, resp, eid::EIDType; getter=resp_trade_symbol)::Bool 284 pyeq(Bool, getter(resp, eid), @pystr(raw(ai))) || begin 285 @warn "Mismatching trade for $(raw(ai))($(resp_trade_symbol(resp, eid))), order: $(o.asset), refusing construction." 286 return false 287 end 288 end 289 290 @doc """ Checks if the response is of the expected type 291 292 $(TYPEDSIGNATURES) 293 294 This function checks if the response from the exchange is of the expected type. 295 If the response is not of the expected type, it issues a warning and returns `false`. 296 297 """ 298 function isordertype(ai, o, resp, ::EIDType; type=pybuiltins.dict)::Bool 299 if !pyisinstance(resp, type) 300 @warn "Invalid response for order $(raw(ai)), order: $o, refusing construction." 301 false 302 else 303 true 304 end 305 end 306 307 @doc """ Checks if the trade id matches the order id 308 309 $(TYPEDSIGNATURES) 310 311 This function checks if the trade id from the response matches the id of the order. 312 If they do not match, it issues a warning and returns `false`. 313 314 """ 315 function isorderid(ai, o, resp, eid::EIDType; getter=resp_trade_order)::Bool 316 if string(getter(resp, eid)) != o.id 317 @warn "Mismatching id $(raw(ai))($(resp_trade_order(resp, eid))), order: $(o.id), refusing construction." 318 false 319 else 320 true 321 end 322 end 323 324 @doc """ Checks if the trade side matches the order side 325 326 $(TYPEDSIGNATURES) 327 328 This function checks if the side of the trade from the response matches the side of the order. 329 If they do not match, it issues a warning and returns `false`. 330 331 """ 332 function isorderside(side, o)::Bool 333 if side != orderside(o) 334 @warn "Mismatching trade side $side and order side $(orderside(o)), refusing construction." 335 false 336 else 337 true 338 end 339 end 340 341 function divergentprice(o::AnyMarketOrder, actual_price) 342 false 343 end 344 345 function divergentprice(o::AnyBuyOrder, actual_price; rtol=0.05) 346 !isapprox(actual_price, o.price; rtol) && actual_price > o.price 347 end 348 349 function divergentprice(o::AnySellOrder, actual_price; rtol=0.05) 350 !isapprox(actual_price, o.price; rtol) && actual_price < o.price 351 end 352 353 @doc """ Checks if the trade price is valid 354 355 $(TYPEDSIGNATURES) 356 357 This function checks if the trade price from the response is approximately equal to the order price or if the order is a market order. 358 If the price is far off from the order price, it issues a warning. 359 The function also checks if the price is greater than zero, issuing a warning and returning `false` if it's not. 360 361 """ 362 function isorderprice(s, ai, actual_price, o; rtol=0.05, resp)::Bool 363 if divergentprice(o, actual_price; rtol) 364 @warn "create trade: trade price far off from order price" o.price exc_price = 365 actual_price ai nameof(s) o o.id @caller(20) 366 false 367 elseif actual_price <= 0.0 || !isfinite(actual_price) 368 @warn "create trade: invalid price" nameof(s) ai tradeid = resp_trade_id( 369 resp, exchangeid(ai) 370 ) o 371 false 372 else 373 true 374 end 375 end 376 377 @doc """ Checks if the trade amount is valid 378 379 $(TYPEDSIGNATURES) 380 381 This function checks if the trade amount from the response is greater than zero. 382 If it's not, it issues a warning and returns `false`. 383 384 """ 385 function isorderamount(s, ai, actual_amount; resp)::Bool 386 if actual_amount <= 0.0 || !isfinite(actual_amount) 387 @warn "create trade: invalid amount" nameof(s) ai tradeid = resp_trade_id( 388 resp, exchangeid(ai) 389 ) 390 false 391 else 392 true 393 end 394 end 395 396 @doc """ Warns if the local cash is not enough for the trade 397 398 $(TYPEDSIGNATURES) 399 400 This function checks if the local cash is enough for the trade. 401 If it's not, it issues a warning. 402 403 """ 404 function _warn_cash(s, ai, o; actual_amount) 405 if !iscashenough(s, ai, actual_amount, o) 406 @warn "make trade: creating trade but local cash is not enough" cash(ai) o.id actual_amount 407 end 408 end 409 410 @doc """ Constructs a trade based on the order and response 411 412 $(TYPEDSIGNATURES) 413 414 This function constructs a trade based on the order and the response from the exchange. 415 It performs several checks on the response, such as checking the type, symbol, id, side, price, and amount. 416 If any of these checks fail, the function returns `nothing`. 417 Otherwise, it calculates the fees, warns if the local cash is not enough for the trade, and constructs the trade. 418 419 """ 420 function maketrade(s::LiveStrategy, o, ai; resp, trade::Option{Trade}=nothing, kwargs...) 421 eid = exchangeid(ai) 422 if trade isa Trade 423 return trade 424 end 425 if !isordertype(ai, o, resp, eid) || 426 !isordersymbol(ai, o, resp, eid) || 427 !isorderid(ai, o, resp, eid) 428 @debug "maketrade: failed" _module = LogCreateTrade ai isordertype(ai, o, resp, eid) isordersymbol(ai, o, resp, eid) isorderid(ai, o, resp, eid) 429 return nothing 430 end 431 side = _ccxt_sidetype(resp, eid; o) 432 if !isorderside(side, o) 433 @debug "maketrade: wrong side" _module = LogCreateTrade ai side o 434 return nothing 435 end 436 actual_amount = resp_trade_amount(resp, eid) 437 actual_price = resp_trade_price(resp, eid) 438 439 isorderprice(s, ai, actual_price, o; resp) 440 inlimits(actual_price, ai, :price) 441 442 if actual_amount <= 0.0 || !isfinite(actual_amount) 443 @debug "make trade: amount value absent from trade or wrong ($actual_amount)), using cost." _module = 444 LogCreateTrade ai actual_amount resp 445 net_cost = resp_trade_cost(resp, eid) 446 actual_amount = toprecision(net_cost / actual_price, ai.precision.amount) 447 if !isorderamount(s, ai, actual_amount; resp) 448 @debug "make trade: wrong amount" _module = LogCreateTrade ai actual_amount 449 return nothing 450 end 451 else 452 net_cost = let c = cost(actual_price, actual_amount) 453 toprecision(c, ai.precision.price) 454 end 455 end 456 inlimits(net_cost, ai, :cost) 457 inlimits(actual_amount, ai, :amount) 458 459 _warn_cash(s, ai, o; actual_amount) 460 date = @something pytodate(resp, eid) now() 461 462 fees_quote, fees_base = _tradefees(resp, side, ai; actual_amount, net_cost) 463 size = _addfees(net_cost, fees_quote, o) 464 465 @debug "Constructing trade" _module = LogCreateTrade cash = cash(ai, posside(o)) ai = raw( 466 ai 467 ) s = nameof(s) 468 @maketrade 469 end