/ Instances / src / module.jl
module.jl
   1  using Exchanges
   2  using OrderTypes
   3  
   4  import Exchanges.ExchangeTypes: exchangeid, exchange
   5  using Exchanges: CurrencyCash, Data, TICKERS_CACHE10, markettype, @tickers!
   6  using OrderTypes: ByPos, AssetEvent, positionside, Instruments, ordertype
   7  using .Data: load, zi, empty_ohlcv, DataFrame, DataStructures
   8  using .Data.DFUtils: daterange, timeframe
   9  import .Data: stub!
  10  using .Data.DataFrames: metadata
  11  using .Instruments: Instruments, compactnum, AbstractAsset, Cash, add!, sub!, Misc
  12  import .Instruments: _hashtuple, cash!, cash, freecash, value, raw, bc, qc
  13  using .Misc: config, MarginMode, NoMargin, WithMargin, MM, DFT, toprecision, ZERO
  14  using .Misc: Lang, TimeTicks, SortedArray, SafeLock
  15  using .Misc: Isolated, Cross, Hedged, IsolatedHedged, CrossHedged, CrossMargin
  16  using .Misc: setattr!, attr, attr!, attrs, hasattr
  17  using .Misc.DocStringExtensions
  18  import .Misc: approxzero, gtxzero, ltxzero, marginmode, load!
  19  using .TimeTicks
  20  import .TimeTicks: timeframe
  21  using .DataStructures: SortedDict
  22  using .Lang: Option, @deassert, @lget!, @caller
  23  import Base: position, isopen
  24  import Exchanges: lastprice, leverage!
  25  import OrderTypes: trades
  26  
  27  baremodule InstancesLock end
  28  
  29  @doc """Defines the abstract type for an instance.
  30  
  31  The `AbstractInstance` type is a generic abstract type for an instance. It is parameterized by two types: `A`, which must be a subtype of `AbstractAsset`, and `E`, which must be a subtype of `ExchangeID`.
  32  """
  33  abstract type AbstractInstance{A<:AbstractAsset,E<:ExchangeID} end
  34  
  35  @doc "Defines a NamedTuple structure for limits, including leverage, amount, price, and cost, each of which is a subtype of Real."
  36  const Limits{T<:Real} = NamedTuple{(:leverage, :amount, :price, :cost),<:NTuple{4,MM{<:T}}}
  37  @doc "Defines a NamedTuple structure for precision, including amount and price, each of which is a subtype of Real."
  38  const Precision{T<:Real} = NamedTuple{(:amount, :price),<:Tuple{<:T,<:T}}
  39  @doc "Defines a NamedTuple structure for fees, including taker, maker, minimum, and maximum fees, each of which is a subtype of Real."
  40  const Fees{T<:Real} = NamedTuple{(:taker, :maker, :min, :max),<:NTuple{4,<:T}}
  41  @doc "Defines a type for currency cash, which is parameterized by an exchange `E` and a symbol `S`."
  42  const CCash{E} = CurrencyCash{Cash{S,DFT},E} where {S}
  43  const AnyTrade{T,E} = Trade{O,T,E} where {O<:OrderType}
  44  const DEFAULT_FIELDS = (;
  45      limits=(;
  46          leverage=(; min=1.0, max=10.0),
  47          amount=(; min=1e-8, max=1e8),
  48          price=(; min=1e-8, max=1e8),
  49          cost=(; min=1e-8, max=1e8),
  50      ),
  51      precision=(; amount=1e-8, price=1e-8),
  52      fees=(; taker=0.01, maker=0.01, min=0.01, max=0.01),
  53  )
  54  
  55  include("positions.jl")
  56  
  57  @doc """Defines a structure for an asset instance.
  58  
  59  $(FIELDS)
  60  
  61  An `AssetInstance` holds all known state about an exchange asset like `BTC/USDT`.
  62  """
  63  struct AssetInstance{T<:AbstractAsset,E<:ExchangeID,M<:MarginMode} <: AbstractInstance{T,E}
  64      "Genric dict for instance specific parameters."
  65      attrs::Dict{Symbol,Any}
  66      "The identifier of the asset."
  67      asset::T
  68      "The OHLCV (Open, High, Low, Close, Volume) series for the asset."
  69      data::SortedDict{TimeFrame,DataFrame}
  70      "The trade history of the pair."
  71      history::SortedArray{AnyTrade{T,E},1}
  72      "A lock for synchronizing access to the asset instance."
  73      lock::SafeLock
  74      _internal_lock::SafeLock
  75      "The amount of the asset currently held. This can be positive or negative (short)."
  76      cash::Option{CCash{E}{S1}} where {S1}
  77      "The amount of the asset currently committed for orders."
  78      cash_committed::Option{CCash{E}{S2}} where {S2}
  79      "The exchange instance that this asset instance belongs to."
  80      exchange::Exchange{E}
  81      "The long position of the asset."
  82      longpos::Option{Position{Long,E,M}}
  83      "The short position of the asset."
  84      shortpos::Option{Position{Short,E,M}}
  85      "The last position of the asset."
  86      lastpos::Vector{Option{Position{P,E,M} where {P<:PositionSide}}}
  87      "The minimum order size (from the exchange)."
  88      limits::Limits{DFT}
  89      "The number of decimal points (from the exchange)."
  90      precision::Precision{<:Union{Int,DFT}}
  91      "The fees associated with the asset (from the exchange)."
  92      fees::Fees{DFT}
  93      @doc """ Create an `AssetInstance` object.
  94  
  95      $(TYPEDSIGNATURES)
  96  
  97      This function constructs an `AssetInstance` with defined asset, data, exchange, margin, and optional parameters for limits, precision, and fees. It initializes long and short positions based on the provided margin and ensures that the margin is not hedged.
  98  
  99      """
 100      function AssetInstance(
 101          a::A, data, e::Exchange{E}, margin::M; limits, precision, fees
 102      ) where {A<:AbstractAsset,E<:ExchangeID,M<:MarginMode}
 103          @assert !ishedged(margin) "Hedged margin not yet supported."
 104          local longpos, shortpos
 105          longpos, shortpos = positions(M, a, limits, e)
 106          cash, comm = if M == NoMargin
 107              (CurrencyCash(e, a.bc, 0.0), CurrencyCash(e, a.bc, 0.0))
 108          else
 109              (nothing, nothing)
 110          end
 111          lastpos = Vector{Option{Position{<:PositionSide,E,M}}}()
 112          push!(lastpos, nothing)
 113          if !(ispercentage(e.markets[raw(a)]))
 114              @warn "Exchange uses fixed amount fees, fees calculation will not match!"
 115          end
 116          new{A,E,M}(
 117              Dict{Symbol,Any}(),
 118              a,
 119              data,
 120              SortedArray(AnyTrade{A,E}[]; by=trade -> trade.date),
 121              SafeLock(),
 122              SafeLock(),
 123              cash,
 124              comm,
 125              e,
 126              longpos, #::Option{Position{Long,E,<:WithMargin}},
 127              shortpos, #::Option{Position{Short,E,<:WithMargin}},
 128              lastpos,
 129              limits,
 130              precision,
 131              fees,
 132          )
 133      end
 134  end
 135  
 136  @doc "A type alias representing an asset instance with no margin."
 137  const NoMarginInstance = AssetInstance{<:AbstractAsset,<:ExchangeID,NoMargin}
 138  @doc "A type alias for an asset instance with either isolated or cross margin."
 139  const MarginInstance{M<:Union{Isolated,Cross}} = AssetInstance{
 140      <:AbstractAsset,<:ExchangeID,M
 141  }
 142  @doc "A type alias for an asset instance with either isolated or cross hedged margin."
 143  const HedgedInstance{M<:Union{IsolatedHedged,CrossHedged}} = AssetInstance{
 144      <:AbstractAsset,<:ExchangeID,M
 145  }
 146  @doc "A type alias representing an asset instance with cross margin."
 147  const CrossInstance{M<:CrossMargin} = AssetInstance{<:AbstractAsset,<:ExchangeID,M}
 148  @doc " Retrieve the margin mode of an `AssetInstance`. "
 149  marginmode(::AssetInstance{<:AbstractAsset,<:ExchangeID,M}, args...) where {M<:WithMargin} = M()
 150  marginmode(::NoMarginInstance, args...) = NoMargin()
 151  
 152  @doc """ Generate positions for a specific margin mode.
 153  
 154  $(TYPEDSIGNATURES)
 155  
 156  This function generates long and short positions for a given asset on a specific exchange. The number and size of the positions are determined by the `limits` argument and the margin mode `M`.
 157  
 158  """
 159  function positions(M::Type{<:MarginMode}, a::AbstractAsset, limits::Limits, e::Exchange)
 160      if M == NoMargin
 161          nothing, nothing
 162      else
 163          let tiers = leverage_tiers(e, a.raw)
 164              function pos_kwargs()
 165                  (;
 166                      asset=a,
 167                      min_size=limits.amount.min,
 168                      tiers=[tiers],
 169                      this_tier=[first(values(tiers))],
 170                      cash=CurrencyCash(e, a.bc, 0.0),
 171                      cash_committed=CurrencyCash(e, a.bc, 0.0),
 172                  )
 173              end
 174  
 175              LongPosition{typeof(e.id),M}(; pos_kwargs()...),
 176              ShortPosition{typeof(e.id),M}(; pos_kwargs()...)
 177          end
 178      end
 179  end
 180  
 181  _external_lock(ai::AssetInstance) = getfield(ai, :lock)
 182  _internal_lock(ai::AssetInstance) = getfield(ai, :_internal_lock)
 183  
 184  function _hashtuple(ai::AssetInstance)
 185      (
 186          Instruments._hashtuple(getfield(ai, :asset))...,
 187          getfield(getfield(ai, :exchange), :id),
 188      )
 189  end
 190  Base.hash(ai::AssetInstance) = hash(_hashtuple(ai))
 191  Base.hash(ai::AssetInstance, h::UInt) = hash(_hashtuple(ai), h)
 192  function Base.propertynames(ai::AssetInstance)
 193      (fieldnames(AssetInstance)..., :ohlcv, :funding, keys(attrs(ai))...)
 194  end
 195  Base.Broadcast.broadcastable(s::AssetInstance) = Ref(s)
 196  function Base.lock(ai::AssetInstance)
 197      @debug "instances: locking" _module = InstancesLock ai tid = Threads.threadid() f = @caller(10)
 198      lock(getfield(ai, :lock))
 199      @debug "instances: locked" _module = InstancesLock ai tid = Threads.threadid() f = @caller(10)
 200  end
 201  Base.lock(f, ai::AssetInstance) = begin
 202      l = getfield(ai, :lock)
 203      lock(f, getfield(ai, :lock))
 204  end
 205  function Base.unlock(ai::AssetInstance)
 206      unlock(getfield(ai, :lock))
 207      @debug "instances: unlocked" _module = InstancesLock ai tid = Threads.threadid() f = @caller(10)
 208  end
 209  Base.islocked(ai::AssetInstance) = islocked(getfield(ai, :lock))
 210  @doc " Get the cash value of a `AssetInstance`. "
 211  Base.float(ai::AssetInstance) = nothing
 212  Base.float(ai::NoMarginInstance) = cash(ai).value
 213  Base.float(ai::MarginInstance) =
 214      let c = cash(ai)
 215          if isnothing(c)
 216              0.0
 217          else
 218              c.value
 219          end
 220      end
 221  Base.abs(ai::MarginInstance) =
 222      let pos = position(ai)
 223          if isnothing(pos)
 224              0.0
 225          else
 226              abs(pos)
 227          end
 228      end
 229  Base.getindex(ai::AssetInstance, k::Symbol) = attr(ai, k)
 230  Base.get(ai::AssetInstance, keys::Tuple{Vararg{Symbol}}) = attr(ai, keys...)
 231  Base.get(ai::AssetInstance, k, v) = attr(ai, k, v)
 232  Base.setindex!(ai::AssetInstance, v, k::Symbol) = setattr!(ai, v, k)
 233  Base.setindex!(ai::AssetInstance, v, keys::Vararg{Symbol}) = setattr!(ai, v, keys...)
 234  Base.get!(ai::AssetInstance, k, v) = attr!(ai, k, v)
 235  Base.haskey(ai::AssetInstance, k::Symbol) = hasattr(ai, k)
 236  Base.keys(ai::AssetInstance) = keys(attrs(ai))
 237  Base.values(ai::AssetInstance) = values(attrs(ai))
 238  
 239  posside(::NoMarginInstance) = Long()
 240  @doc "Get the position side of an `AssetInstance`. "
 241  posside(ai::MarginInstance) =
 242      let pos = position(ai)
 243          isnothing(pos) ? nothing : posside(pos)
 244      end
 245  _ishedged(::Union{T,Type{T}}) where {T<:MarginMode{H}} where {H} = H == Hedged
 246  # NOTE: wrap the function here to quickly overlay methods
 247  @doc "Check if the margin mode is hedged."
 248  ishedged(args...; kwargs...) = _ishedged(args...; kwargs...)
 249  @doc "Check if the `AssetInstance` is hedged."
 250  ishedged(ai::AssetInstance) = marginmode(ai) |> ishedged
 251  @doc "Check if the `AssetInstance` is open."
 252  isopen(ai::NoMarginInstance) = !iszero(ai)
 253  isopen(ai::MarginInstance) =
 254      let po = position(ai)
 255          !isnothing(po) && isopen(po)
 256      end
 257  @doc "Check if the `AssetInstance` is long."
 258  islong(ai::NoMarginInstance) = true
 259  @doc "Check if the `AssetInstance` is short."
 260  isshort(ai::NoMarginInstance) = false
 261  islong(ai::MarginInstance) =
 262      let pos = position(ai)
 263          isnothing(pos) && return false
 264          islong(pos)
 265      end
 266  isshort(ai::MarginInstance) =
 267      let pos = position(ai)
 268          isnothing(pos) && return false
 269          isshort(pos)
 270      end
 271  
 272  @doc """ Check if the position value of the asset is below minimum quantity.
 273  
 274  $(TYPEDSIGNATURES)
 275  
 276  This function checks if the position value of a given `AssetInstance` at a specific price is below the minimum limit for that asset. The position side `p` determines if it's a long or short position.
 277  
 278  """
 279  function isdust(ai::MarginInstance, price::Number, p::PositionSide)
 280      pos = position(ai, p)
 281      if isnothing(pos)
 282          return true
 283      end
 284      this_cash = cash(pos) |> value |> abs
 285      if this_cash >= ai.limits.amount.min
 286          return false
 287      else
 288          this_cash * price * leverage(pos) < ai.limits.cost.min
 289      end
 290  end
 291  function isdust(ai::MarginInstance, price::Number)
 292      isdust(ai, price, Long()) && isdust(ai, price, Short())
 293  end
 294  function isdust(ai::NoMarginInstance, price::Number)
 295      this_cash = cash(ai) |> value |> abs
 296      if this_cash >= ai.limits.amount.min
 297          return false
 298      else
 299          this_cash * price < ai.limits.cost.min
 300      end
 301  end
 302  function isdust(ai::AssetInstance, o::Type{<:Order}, price::Number)
 303      if o <: ReduceOnlyOrder
 304          false
 305      else
 306          invoke(isdust, Tuple{MarginInstance,Number,PositionSide}, ai, price, posside(ai))
 307      end
 308  end
 309  @doc """ Get the asset cash rounded to precision.
 310  
 311  $(TYPEDSIGNATURES)
 312  
 313  This function returns the asset cash of a `MarginInstance` rounded according to the asset's precision. The position side `p` is determined by the `posside` function.
 314  
 315  """
 316  function nondust(ai::MarginInstance, price::Number, p=posside(ai))
 317      pos = position(ai, p)
 318      if isnothing(pos)
 319          return zero(price)
 320      end
 321      c = cash(pos)
 322      amt = c.value
 323      abs(amt * price * leverage(pos)) < ai.limits.cost.min ? zero(amt) : amt
 324  end
 325  
 326  function nondust(ai::MarginInstance, o::Type{<:Order}, price)
 327      if o <: ReduceOnlyOrder
 328          cash(ai, o).value
 329      else
 330          invoke(nondust, Tuple{MarginInstance,Number,PositionSide}, ai, price, posside(o))
 331      end
 332  end
 333  
 334  @doc """ Check if the amount is below the asset instance's minimum limit.
 335  
 336  $(TYPEDSIGNATURES)
 337  
 338  This function checks if a specified amount in base currency is considered zero with respect to an `AssetInstance`'s minimum limit. The amount is considered zero if it is less than the minimum limit minus a small epsilon value.
 339  
 340  """
 341  function Base.iszero(ai::AssetInstance, v; atol=ai.limits.amount.min - eps(DFT))
 342      isapprox(v, zero(DFT); atol)
 343  end
 344  @doc """ Check if the asset cash for a position side is zero.
 345  
 346  $(TYPEDSIGNATURES)
 347  
 348  This function checks if the cash value of an `AssetInstance` for a specific `PositionSide` is zero. This is used to determine if there are no funds in a certain position side (long or short).
 349  
 350  """
 351  function Base.iszero(ai::AssetInstance, p::PositionSide)
 352      isapprox(value(cash(ai, p)), zero(DFT); atol=ai.limits.amount.min - eps(DFT))
 353  end
 354  @doc """ Check if the asset cash is zero.
 355  
 356  $(TYPEDSIGNATURES)
 357  
 358  This function checks if the cash value of an `AssetInstance` is zero. This is used to determine if there are no funds in the asset.
 359  
 360  """
 361  function Base.iszero(ai::AssetInstance)
 362      iszero(ai, Long()) && iszero(ai, Short())
 363  end
 364  approxzero(ai::AssetInstance, args...; kwargs...) = iszero(ai, args...; kwargs...)
 365  @doc """ Check if an amount is greater than zero for an `AssetInstance`.
 366  
 367  $(TYPEDSIGNATURES)
 368  
 369  This function checks if a specified amount `v` is greater than zero for an `AssetInstance`. It's used to validate the amount before performing operations on the asset.
 370  
 371  """
 372  function gtxzero(ai::AssetInstance, v, ::Val{:amount})
 373      gtxzero(v; atol=ai.limits.amount.min + eps())
 374  end
 375  @doc """ Check if an amount is less than zero for an `AssetInstance`.
 376  
 377  $(TYPEDSIGNATURES)
 378  
 379  This function checks if a specified amount `v` is less than zero for an `AssetInstance`. It's used to validate the amount before performing operations on the asset.
 380  
 381  """
 382  function ltxzero(ai::AssetInstance, v, ::Val{:amount})
 383      ltxzero(v; atol=ai.limits.amount.min + eps())
 384  end
 385  @doc """ Check if a price is greater than zero for an `AssetInstance`.
 386  
 387  $(TYPEDSIGNATURES)
 388  
 389  This function checks if a specified price `v` is greater than zero for an `AssetInstance`. The price is considered greater than zero if it is above the minimum limit minus a small epsilon value.
 390  
 391  """
 392  gtxzero(ai::AssetInstance, v, ::Val{:price}) = gtxzero(v; atol=ai.limits.price.min + eps())
 393  @doc """ Check if a price is less than zero for an `AssetInstance`.
 394  
 395  $(TYPEDSIGNATURES)
 396  
 397  This function checks if a specified price `v` is less than zero for an `AssetInstance`. The price is considered less than zero if it is below the minimum limit minus a small epsilon value.
 398  
 399  """
 400  ltxzero(ai::AssetInstance, v, ::Val{:price}) = ltxzero(v; atol=ai.limits.price.min + eps())
 401  @doc """ Check if a cost is greater than zero for an `AssetInstance`.
 402  
 403  $(TYPEDSIGNATURES)
 404  
 405  This function checks if a specified cost `v` is greater than zero for an `AssetInstance`. The cost is considered greater than zero if it is above the minimum limit minus a small epsilon value.
 406  
 407  """
 408  gtxzero(ai::AssetInstance, v, ::Val{:cost}) = gtxzero(v; atol=ai.limits.cost.min + eps())
 409  @doc """ Check if a cost is less than zero for an `AssetInstance`.
 410  
 411  $(TYPEDSIGNATURES)
 412  
 413  This function checks if a specified cost `v` is less than zero for an `AssetInstance`. The cost is considered less than zero if it is below the minimum limit minus a small epsilon value.
 414  
 415  """
 416  ltxzero(ai::AssetInstance, v, ::Val{:cost}) = ltxzero(v; atol=ai.limits.cost.min + eps())
 417  @doc """ Check if two amounts are approximately equal for an `AssetInstance`.
 418  
 419  $(TYPEDSIGNATURES)
 420  
 421  This function checks if two specified amounts `v1` and `v2` are approximately equal for an `AssetInstance`. It's used to validate whether two amounts are similar considering small variations.
 422  
 423  """
 424  function Base.isapprox(
 425      ai::AssetInstance, v1, v2, ::Val{:amount}; atol=ai.precision.amount + eps(DFT)
 426  )
 427      isapprox(value(v1), value(v2); atol)
 428  end
 429  @doc """ Check if two prices are approximately equal for an `AssetInstance`.
 430  
 431  $(TYPEDSIGNATURES)
 432  
 433  This function checks if two specified prices `v1` and `v2` are approximately equal for an `AssetInstance`. It's used to validate whether two prices are similar considering small variations.
 434  
 435  """
 436  function Base.isapprox(
 437      ai::AssetInstance, v1, v2, ::Val{:price}; atol=ai.precision.price + eps(DFT)
 438  )
 439      isapprox(value(v1), value(v2); atol)
 440  end
 441  
 442  function Base.isequal(ai::AssetInstance, v1, v2, kind::Val{:amount})
 443      isapprox(ai, v1, v2, kind; atol=ai.limits.amount.min - eps(DFT))
 444  end
 445  
 446  function Base.isequal(ai::AssetInstance, v1, v2, kind::Val{:price})
 447      isapprox(ai, v1, v2, kind; atol=ai.limits.price.min - eps(DFT))
 448  end
 449  
 450  @doc """ Create an `AssetInstance` from a zarr instance.
 451  
 452  $(TYPEDSIGNATURES)
 453  
 454  This function constructs an `AssetInstance` by loading data from a zarr instance and requires an external constructor defined in `Engine`. The `MarginMode` can be specified, with `NoMargin` being the default.
 455  
 456  """
 457  function instance(exc::Exchange, a::AbstractAsset, m::MarginMode=NoMargin(); zi=zi)
 458      data = Dict()
 459      @assert a.raw ∈ keys(exc.markets) "Market $(a.raw) not found on exchange $(exc.name)."
 460      for tf in config.timeframes
 461          data[tf] = load(zi, exc.name, a.raw, string(tf))
 462      end
 463      AssetInstance(a; data, exc, margin=m)
 464  end
 465  instance(a) = instance(exc, a)
 466  
 467  @doc """ Load OHLCV data for an `AssetInstance`.
 468  
 469  $(TYPEDSIGNATURES)
 470  
 471  This function loads OHLCV (Open, High, Low, Close, Volume) data for a given `AssetInstance`. If `reset` is set to true, it will re-fetch the data even if it's already been loaded.
 472  
 473  """
 474  function load!(ai::AssetInstance; reset=true, zi=zi)
 475      for (tf, df) in ai.data
 476          reset && empty!(df)
 477          loaded = load(zi, ai.exchange.name, raw(ai), string(tf))
 478          append!(df, loaded)
 479      end
 480  end
 481  Base.getproperty(ai::AssetInstance, f::Symbol) = begin
 482      if f == :ohlcv
 483          ohlcv(ai)
 484      elseif f == :bc
 485          ai.asset.bc
 486      elseif f == :qc
 487          ai.asset.qc
 488      elseif f == :funding
 489          metadata(ohlcv(ai), "funding")
 490      elseif hasfield(AssetInstance, f)
 491          getfield(ai, f)
 492      else
 493          attr(ai, f)
 494      end
 495  end
 496  
 497  @doc " Get the parsed `AbstractAsset` of an `AssetInstance`. "
 498  function asset(ai::AssetInstance)
 499      getfield(ai, :asset)
 500  end
 501  
 502  @doc " Get the raw string id of an `AssetInstance`. "
 503  function raw(ai::AssetInstance)
 504      raw(asset(ai))
 505  end
 506  
 507  @doc " Get the base currency of an `AssetInstance`. "
 508  bc(ai::AssetInstance) = bc(asset(ai))
 509  @doc " Get the quote currency of an `AssetInstance`. "
 510  qc(ai::AssetInstance) = qc(asset(ai))
 511  
 512  @doc """ Round a value based on the `precision` field of the `ai` asset instance.
 513  
 514  $(TYPEDSIGNATURES)
 515  
 516  This macro rounds a value `v` based on the `precision` field of an `AssetInstance`. By default, it rounds the `amount`, but it can also round other fields like `price` or `cost` if specified.
 517  
 518  """
 519  macro _round(v, kind=:amount)
 520      @assert kind isa Symbol
 521      quote
 522          toprecision(
 523              $(esc(v)), getfield(getfield($(esc(esc(:ai))), :precision), $(QuoteNode(kind)))
 524          )
 525      end
 526  end
 527  
 528  @doc """ Round a value based on the `precision` (price) field of the `ai` asset instance.
 529  
 530  $(TYPEDSIGNATURES)
 531  
 532  This macro rounds a price value `v` based on the `precision` field of an `AssetInstance`.
 533  
 534  """
 535  macro rprice(v)
 536      quote
 537          $(@__MODULE__).@_round $(esc(v)) price
 538      end
 539  end
 540  
 541  @doc """ Round a value based on the `precision` (amount) field of the `ai` asset instance.
 542  
 543  $(TYPEDSIGNATURES)
 544  
 545  This macro rounds an amount value `v` based on the `precision` field of an `AssetInstance`.
 546  
 547  """
 548  macro ramount(v)
 549      quote
 550          $(@__MODULE__).@_round $(esc(v)) amount
 551      end
 552  end
 553  
 554  @doc """ Get the last available candle strictly lower than `apply(tf, date)`.
 555  
 556  $(TYPEDSIGNATURES)
 557  
 558  This function retrieves the last available candle (Open, High, Low, Close, Volume data for a specific time period) from the `AssetInstance` that is strictly lower than the date adjusted by the `TimeFrame` `tf`.
 559  
 560  """
 561  function Data.candlelast(ai::AssetInstance, tf::TimeFrame=first(keys(ohlcv_dict(ai))), args...)
 562      Data.candlelast(ai.data[tf])
 563  end
 564  
 565  function OrderTypes.Order(ai::AssetInstance, type; kwargs...)
 566      Order(ai.asset, ai.exchange.id, type; kwargs...)
 567  end
 568  
 569  @doc """ Create a similar `AssetInstance` with cash and orders reset.
 570  
 571  $(TYPEDSIGNATURES)
 572  
 573  This function returns a similar `AssetInstance` to the one provided, but resets the cash and orders. The limits, precision, and fees can be specified, and will default to those of the original instance.
 574  
 575  """
 576  function Base.similar(
 577      ai::AssetInstance;
 578      exc=ai.exchange,
 579      limits=ai.limits,
 580      precision=ai.precision,
 581      fees=ai.fees,
 582  )
 583      AssetInstance(ai.asset, ai.data, exc, marginmode(ai); limits, precision, fees)
 584  end
 585  
 586  @doc "Get the asset instance cash."
 587  cash(ai::NoMarginInstance) = getfield(ai, :cash)
 588  @doc "Get the asset instance cash for the long position."
 589  cash(ai::NoMarginInstance, ::ByPos{Long}) = cash(ai)
 590  @doc "Get the asset instance cash for the short position."
 591  cash(ai::NoMarginInstance, ::ByPos{Short}) = 0.0
 592  cash(ai::MarginInstance) =
 593      let pos = position(ai)
 594          isnothing(pos) && return nothing
 595          getfield((pos), :cash)
 596      end
 597  cash(ai::MarginInstance, ::ByPos{Long}) = getfield(position(ai, Long()), :cash)
 598  cash(ai::MarginInstance, ::ByPos{Short}) = getfield(position(ai, Short()), :cash)
 599  @doc "Get the asset instance committed cash."
 600  committed(ai::NoMarginInstance) = getfield(ai, :cash_committed)
 601  committed(ai::NoMarginInstance, ::ByPos{Long}) = committed(ai)
 602  committed(ai::NoMarginInstance, ::ByPos{Short}) = 0.0
 603  function committed(ai::MarginInstance, ::ByPos{P}) where {P}
 604      getfield(position(ai, P), :cash_committed)
 605  end
 606  committed(ai::MarginInstance) = getfield((@something position(ai) ai), :cash_committed)
 607  @doc "Get the asset instance ohlcv data for the smallest time frame."
 608  ohlcv(ai::AssetInstance) = getfield(first(getfield(ai, :data)), :second)
 609  ohlcv(ai::AssetInstance, tf::TimeFrame) = getfield(ai, :data)[tf]
 610  @doc "Get the asset instance ohlcv data dictionary."
 611  ohlcv_dict(ai::AssetInstance) = getfield(ai, :data)
 612  Instruments.add!(ai::NoMarginInstance, v, args...) = add!(cash(ai), v)
 613  Instruments.add!(ai::MarginInstance, v, p::PositionSide) = add!(cash(ai, p), v)
 614  Instruments.sub!(ai::NoMarginInstance, v, args...) = sub!(cash(ai), v)
 615  Instruments.sub!(ai::MarginInstance, v, p::PositionSide) = sub!(cash(ai, p), v)
 616  Instruments.cash!(ai::NoMarginInstance, v, args...) = cash!(cash(ai), v)
 617  Instruments.cash!(ai::MarginInstance, v, p::PositionSide) = cash!(cash(ai, p), v)
 618  # Positive `fees_base` go `trade --> exchange`
 619  # Negative `fees_base` go `exchange --> trade`
 620  # When updating a position t.amount must be fee adjusted if there are (positive) fees in base currency.
 621  # We assume the amount field in a trade is always PRE fees. So
 622  # - If the trade amount is 1 and fees are 0.01, the cash to add (sub) to the asset will be ±0.99
 623  # - If the trade amount is 1 and fees are -0.01 (rebates), the cash to add (sub) to the asset will be ±1.01
 624  @doc "The amount of a trade include fees (either positive or negative)."
 625  amount_with_fees(amt, fb) =
 626      if fb > 0.0 # trade --> exchange (the amount spent is the trade amount plus the base fees)
 627          amt - fb
 628      else # exchange --> trade (rebates, the amount spent is the trade amount minus the base fees (which we get back))
 629          amt + fb
 630      end
 631  amount_with_fees(t::Trade) = amount_with_fees(t.amount, t.fees_base)
 632  function Instruments.cash!(ai::NoMarginInstance, t::BuyTrade)
 633      amt = amount_with_fees(t)
 634      add!(cash(ai), amt)
 635  end
 636  @doc """ Update the cash value for a `NoMarginInstance` after a `SellTrade`.
 637  
 638  $(TYPEDSIGNATURES)
 639  
 640  This function updates the cash value of a `NoMarginInstance` after a `SellTrade`. The cash value would typically increase after a sell trade, as assets are sold in exchange for cash.
 641  
 642  """
 643  function Instruments.cash!(ai::NoMarginInstance, t::SellTrade)
 644      amt = amount_with_fees(t)
 645      add!(cash(ai), amt)
 646      add!(committed(ai), amt)
 647  end
 648  @doc """ Update the cash value for a `MarginInstance` after an `IncreaseTrade`.
 649  
 650  $(TYPEDSIGNATURES)
 651  
 652  This function updates the cash value of a `MarginInstance` after an `IncreaseTrade`. The cash value would typically decrease after an increase trade, as assets are bought using cash.
 653  
 654  """
 655  function Instruments.cash!(ai::MarginInstance, t::IncreaseTrade)
 656      amt = amount_with_fees(t)
 657      add!(cash(ai, positionside(t)()), amt)
 658  end
 659  @doc """ Update the cash value for a `MarginInstance` after a `ReduceTrade`.
 660  
 661  $(TYPEDSIGNATURES)
 662  
 663  This function updates the cash value of a `MarginInstance` after a `ReduceTrade`. The cash value would typically increase after a reduce trade, as assets are sold in exchange for cash.
 664  
 665  """
 666  function Instruments.cash!(ai::MarginInstance, t::ReduceTrade)
 667      amt = amount_with_fees(t)
 668      add!(cash(ai, positionside(t)()), amt)
 669      add!(committed(ai, positionside(t)()), amt)
 670  end
 671  @doc """ Calculate the free cash for a `NoMarginInstance`.
 672  
 673  $(TYPEDSIGNATURES)
 674  
 675  This function calculates the free cash (cash that is not tied up in trades) of a `NoMarginInstance`. It takes into account the current cash, open orders, and any additional factors specified in `args`.
 676  
 677  """
 678  function freecash(ai::NoMarginInstance, args...)
 679      ca = cash(ai) - committed(ai)
 680      @deassert ca |> gtxzero (cash(ai), committed(ai))
 681      ca
 682  end
 683  @doc """ Calculate the free cash for a `MarginInstance` with long position.
 684  
 685  $(TYPEDSIGNATURES)
 686  
 687  This function calculates the free cash (cash that is not tied up in trades) of a `MarginInstance` that has a long position. It takes into account the current cash, open long positions, and the margin requirements for those positions.
 688  
 689  """
 690  function freecash(ai::MarginInstance, p::ByPos{Long})
 691      @deassert cash(ai, p) |> gtxzero
 692      @deassert committed(ai, p) |> gtxzero
 693      ca = max(0.0, cash(ai, p) - committed(ai, p))
 694      @deassert ca |> gtxzero (cash(ai, p), committed(ai, p))
 695      ca
 696  end
 697  @doc """ Calculate the free cash for a `MarginInstance` with short position.
 698  
 699  $(TYPEDSIGNATURES)
 700  
 701  This function calculates the free cash (cash that is not tied up in trades) of a `MarginInstance` that has a short position. It takes into account the current cash, open short positions, and the margin requirements for those positions.
 702  
 703  """
 704  function freecash(ai::MarginInstance, p::ByPos{Short})
 705      @deassert cash(ai, p) |> ltxzero
 706      @deassert committed(ai, p) |> ltxzero
 707      ca = min(0.0, cash(ai, p) - committed(ai, p))
 708      @deassert ca |> ltxzero (cash(ai, p), committed(ai, p))
 709      ca
 710  end
 711  _reset!(ai) = begin
 712      empty!(ai.history)
 713      ai.lastpos[] = nothing
 714  end
 715  @doc """ Resets asset cash and commitments for a `NoMarginInstance`.
 716  
 717  $(TYPEDSIGNATURES)
 718  
 719  This function resets the cash and commitments (open trades) of a `NoMarginInstance` to initial values. Any additional arguments in `args` are used to adjust the reset process, if necessary.
 720  
 721  """
 722  reset!(ai::NoMarginInstance, args...) = begin
 723      cash!(ai, 0.0)
 724      cash!(committed(ai), 0.0)
 725      _reset!(ai)
 726  end
 727  @doc """ Resets asset positions for a `MarginInstance`.
 728  
 729  $(TYPEDSIGNATURES)
 730  
 731  This function resets the positions (open trades) of a `MarginInstance` to initial values. Any additional arguments in `args` are used to adjust the reset process, if necessary.
 732  
 733  """
 734  reset!(ai::MarginInstance, args...) = begin
 735      reset!(position(ai, Short()), args...)
 736      reset!(position(ai, Long()), args...)
 737      _reset!(ai)
 738  end
 739  
 740  reset!(ai::MarginInstance, p::PositionSide) = begin
 741      reset!(position(ai, p))
 742      let sop = position(ai, opposite(p))
 743          if isopen(sop)
 744              ai.lastpos[] = sop
 745          else
 746              ai.lastpos[] = nothing
 747          end
 748      end
 749  end
 750  Data.DFUtils.firstdate(ai::AssetInstance) = begin
 751      df = ohlcv(ai)
 752      isempty(df) ? DateTime(0) : first(df.timestamp)
 753  end
 754  Data.DFUtils.lastdate(ai::AssetInstance) = begin
 755      df = ohlcv(ai)
 756      isempty(df) ? DateTime(0) : last(df.timestamp)
 757  end
 758  
 759  function Base.print(io::IO, ai::NoMarginInstance)
 760      write(io, raw(ai), "~[", compactnum(ai.cash.value), "]{", ai.exchange.name, "}")
 761  end
 762  function Base.print(io::IO, ai::MarginInstance)
 763      long = compactnum(cash(ai, Long()).value)
 764      short = compactnum(cash(ai, Short()).value)
 765      write(io, "[\"", raw(ai), "\"][L:", long, "/S:", short, "][", ai.exchange.name, "]")
 766  end
 767  Base.show(io::IO, ::MIME"text/plain", ai::AssetInstance) = print(io, ai)
 768  Base.show(io::IO, ai::AssetInstance) = print(io, "\"", raw(ai), "\"")
 769  
 770  @doc """ Stub data for an `AssetInstance` with a `DataFrame`.
 771  
 772  $(TYPEDSIGNATURES)
 773  
 774  This function stabs data of an `AssetInstance` with a given `DataFrame`. It's used for testing or simulating scenarios with pre-defined data.
 775  
 776  """
 777  stub!(ai::AssetInstance, df::DataFrame) = begin
 778      tf = timeframe!(df)
 779      ai.data[tf] = df
 780  end
 781  @doc """ Calculate the value of a `NoMarginInstance`.
 782  
 783  $(TYPEDSIGNATURES)
 784  
 785  This function calculates the value of a `NoMarginInstance`. It uses the current price (defaulting to the last historical price), the cash in the instance and the maximum fees. The value represents the amount of cash that could be obtained by liquidating the instance at the current price, taking into account the fees.
 786  
 787  """
 788  function value(
 789      ai::NoMarginInstance;
 790      current_price=lastprice(ai, Val(:history)),
 791      fees=current_price * cash(ai) * maxfees(ai),
 792  )
 793      cash(ai) * current_price - fees
 794  end
 795  @doc "Taker fees for the asset instance (usually higher than maker fees.)"
 796  takerfees(ai::AssetInstance) = ai.fees.taker
 797  @doc "Maker fees for the asset instance (usually lower than taker fees.)"
 798  makerfees(ai::AssetInstance) = ai.fees.maker
 799  @doc "The minimum fees for trading in the asset market (usually the highest vip level.)"
 800  minfees(ai::AssetInstance) = ai.fees.min
 801  @doc "The maximum fees for trading in the asset market (usually the lowest vip level.)"
 802  maxfees(ai::AssetInstance) = ai.fees.max
 803  @doc "ExchangeID for the asset instance."
 804  exchangeid(::AssetInstance{<:AbstractAsset,E}) where {E<:ExchangeID} = E
 805  @doc "The exchange of the asset instance."
 806  exchange(ai::AssetInstance) = getfield(ai, :exchange)
 807  @doc "Asset instance long position."
 808  position(ai::MarginInstance, ::ByPos{Long}) = getfield(ai, :longpos)
 809  @doc "Asset instance short position."
 810  position(ai::MarginInstance, ::ByPos{Short}) = getfield(ai, :shortpos)
 811  @doc "Asset position by order."
 812  position(ai::MarginInstance, ::ByPos{S}) where {S<:PositionSide} = position(ai, S)
 813  @doc "Returns the last open asset position or nothing."
 814  position(ai::MarginInstance) = getfield(ai, :lastpos)[]
 815  @doc "Get the trade history of an `AssetInstance`."
 816  trades(ai::AssetInstance) = getfield(ai, :history)
 817  _history_timestamp(ai) =
 818      let history = trades(ai)
 819          if isempty(history)
 820              DateTime(0)
 821          else
 822              last(history).date
 823          end
 824      end
 825  @doc "Get the timestamp of the last trade."
 826  timestamp(ai::NoMarginInstance, _=nothing) = _history_timestamp(ai)
 827  timestamp(::MarginInstance, ::Nothing) = DateTime(0)
 828  function timestamp(ai::MarginInstance, ::ByPos{P}=posside(ai)) where {P}
 829      pos = position(ai, P())
 830      if isnothing(pos)
 831          _history_timestamp(ai)
 832      else
 833          timestamp(pos)
 834      end
 835  end
 836  @doc "Check if an asset position is open."
 837  function isopen(ai::MarginInstance, ::Union{Type{S},S,Position{S}}) where {S<:PositionSide}
 838      isopen(position(ai, S))
 839  end
 840  @doc "Asset position notional value."
 841  function notional(ai::MarginInstance, ::ByPos{S}) where {S<:PositionSide}
 842      position(ai, S) |> notional
 843  end
 844  @doc "Asset entry price.
 845  
 846  $(TYPEDSIGNATURES)
 847  "
 848  function price(ai::MarginInstance, fromprice, ::ByPos{S}) where {S<:PositionSide}
 849      v = position(ai, S) |> price
 850      ifelse(iszero(v), fromprice, v)
 851  end
 852  @doc "Asset entry price."
 853  entryprice(ai::MarginInstance, fromprice, pos::ByPos) = price(ai, fromprice, pos)
 854  @doc "Asset entry price.
 855  
 856  $(TYPEDSIGNATURES)
 857  "
 858  price(::NoMarginInstance, fromprice, args...) = fromprice
 859  @doc "Asset position liquidation price."
 860  function liqprice(ai::MarginInstance, ::ByPos{S}) where {S<:PositionSide}
 861      position(ai, S) |> liqprice
 862  end
 863  @doc "Sets asset position liquidation price.
 864  
 865  $(TYPEDSIGNATURES)
 866  "
 867  function liqprice!(ai::MarginInstance, v, ::ByPos{S}) where {S<:PositionSide}
 868      liqprice!(position(ai, S), v)
 869  end
 870  @doc "Asset position leverage."
 871  function leverage(ai::MarginInstance, ::ByPos{S}=posside(ai)) where {S<:PositionSide}
 872      position(ai, S) |> leverage
 873  end
 874  leverage(::MarginInstance, ::Nothing) = 1.0
 875  leverage(::NoMarginInstance, args...) = 1.0
 876  @doc "Asset position status (open or closed)."
 877  function status(ai::MarginInstance, ::ByPos{S}) where {S<:PositionSide}
 878      position(ai, S) |> status
 879  end
 880  @doc "Asset position maintenance margin."
 881  function maintenance(ai::MarginInstance, ::ByPos{S}) where {S<:PositionSide}
 882      position(ai, S) |> maintenance
 883  end
 884  @doc "Asset position initial margin."
 885  function margin(ai::MarginInstance, ::ByPos{S}) where {S<:PositionSide}
 886      position(ai, S) |> margin
 887  end
 888  @doc "Asset position additional margin."
 889  function additional(ai::MarginInstance, ::ByPos{S}) where {S<:PositionSide}
 890      position(ai, S) |> additional
 891  end
 892  @doc """ Get the position tier for a `MarginInstance`.
 893  
 894  $(TYPEDSIGNATURES)
 895  
 896  This function returns the tier of the position for a `MarginInstance` for a given size and position side (`Long` or `Short`). The tier indicates the level of risk or capital requirement for the position.
 897  
 898  """
 899  function tier(ai::MarginInstance, size, ::ByPos{S}) where {S<:PositionSide}
 900      tier(position(ai, S), size)
 901  end
 902  @doc """ Get the maintenance margin rate for a `MarginInstance`.
 903  
 904  $(TYPEDSIGNATURES)
 905  
 906  This function returns the maintenance margin rate for a `MarginInstance` for a given size and position side (`Long` or `Short`). The maintenance margin rate is the minimum amount of equity that must be maintained in a margin account.
 907  
 908  """
 909  function mmr(ai::MarginInstance, size, s::ByPos)
 910      mmr(position(ai, s), size)
 911  end
 912  @doc """ Get the bankruptcy price for an asset position.
 913  
 914  $(TYPEDSIGNATURES)
 915  
 916  This function calculates the bankruptcy price, which is the price at which the asset position would be fully liquidated. It takes into account the current price of the asset and the position side (`Long` or `Short`).
 917  
 918  """
 919  function bankruptcy(ai, price, ps::ByPos{P}) where {P<:PositionSide}
 920      bankruptcy(position(ai, ps), price)
 921  end
 922  function bankruptcy(ai, o::Order{T,A,E,P}) where {T,A,E,P<:PositionSide}
 923      bankruptcy(ai, o.price, P())
 924  end
 925  
 926  @doc """ Update the leverage for an asset position.
 927  
 928  $(TYPEDSIGNATURES)
 929  
 930  This function updates the leverage for a position in an asset instance. Leverage is the use of various financial instruments or borrowed capital to increase the potential return of an investment. The function takes a leverage value `v` and a position side (`Long` or `Short`) as inputs.
 931  
 932  """
 933  function leverage!(ai, v, p::PositionSide)
 934      po = position(ai, p)
 935      leverage!(po, v)
 936      # ensure leverage tiers and limits agree
 937      @deassert leverage(po) <= ai.limits.leverage.max
 938  end
 939  
 940  @doc """ Set the leverage to maximum for a `CrossInstance`.
 941  
 942  $(TYPEDSIGNATURES)
 943  
 944  This function sets the leverage for a `CrossInstance` to the maximum value for the current tier. Some exchanges interpret a leverage value of 0 as max leverage in cross margin mode. This means that the maximum amount of borrowed capital will be used to increase the potential return of the investment.
 945  
 946  """
 947  function leverage!(ai::CrossInstance, p::PositionSide, ::Val{:max})
 948      po = position(ai, p)
 949      po.leverage[] = 0.0
 950  end
 951  
 952  @doc "The opposite position w.r.t. the asset instance and another `Position` or `PositionSide`."
 953  function opposite(ai::MarginInstance, ::Union{P,Position{P}}) where {P}
 954      position(ai, opposite(P))
 955  end
 956  
 957  function _lastpos!(ai::MarginInstance, p::PositionSide, ::PositionClose)
 958      sop = position(ai, opposite(p))
 959      isopen(sop) && (ai.lastpos[] = sop)
 960  end
 961  
 962  function _lastpos!(ai::MarginInstance, p::PositionSide, ::PositionOpen)
 963      ai.lastpos[] = position(ai, p)
 964  end
 965  
 966  @doc """ Update the status of a hedged position in a `HedgedInstance`.
 967  
 968  $(TYPEDSIGNATURES)
 969  
 970  This function opens or closes the status of a hedged position in a `HedgedInstance`. A hedged position is a position that is offset by a corresponding position in a related commodity or security. The `PositionSide` and `PositionStatus` are provided as inputs.
 971  
 972  """
 973  function status!(ai::HedgedInstance, p::PositionSide, pstat::PositionStatus)
 974      pos = position(ai, p)
 975      _status!(pos, pstat)
 976      _lastpos!(ai, p, pstat)
 977  end
 978  
 979  @doc """ Update the status of a non-hedged position in a `MarginInstance`.
 980  
 981  $(TYPEDSIGNATURES)
 982  
 983  This function opens or closes the status of a non-hedged position in a `MarginInstance`. A non-hedged position is a position that is not offset by a corresponding position in a related commodity or security. The `PositionSide` and `PositionStatus` are provided as inputs.
 984  
 985  """
 986  function status!(ai::MarginInstance, p::PositionSide, pstat::PositionStatus)
 987      pos = position(ai, p)
 988      opp = opposite(ai, p)
 989      # HACK: the `!iszero` check is needed because in SimMode the `NewTrade` call! in `_update_from_trade!` can trigger aditional trades
 990      if pstat == PositionOpen() && status(opp) == PositionOpen() && !iszero(cash(opp))
 991          @error "double position in non hedged mode" ai.longpos ai.shortpos
 992          error()
 993      end
 994      _status!(pos, pstat)
 995      _lastpos!(ai, p, pstat)
 996  end
 997  
 998  value(v::Real, args...; kwargs...) = v
 999  @doc """ Calculate the value of a `MarginInstance`.
1000  
1001  $(TYPEDSIGNATURES)
1002  
1003  This function calculates the value of a `MarginInstance`. It takes into account the current price (defaulting to the price of the position), the cash in the position and the maximum fees. The value represents the amount of cash that could be obtained by liquidating the position at the current price, taking into account the fees.
1004  
1005  """
1006  function value(
1007      ai::MarginInstance,
1008      ::ByPos{P}=posside(ai);
1009      current_price=price(position(ai, P)),
1010      fees=current_price * abs(cash(ai, P)) * maxfees(ai),
1011  ) where {P}
1012      pos = position(ai, P)
1013      @deassert margin(pos) > 0.0 || !isopen(pos)
1014      @deassert additional(pos) >= 0.0
1015      margin(pos) + additional(pos) + pnl(pos, current_price) - fees
1016  end
1017  
1018  @doc """ Calculate the profit and loss (PnL) of an asset position.
1019  
1020  $(TYPEDSIGNATURES)
1021  
1022  This function calculates the profit and loss (PnL) for an asset position. It takes into account the current price and the position. The PnL represents the gain or loss made on the position, based on the current price compared to the price at which the position was opened.
1023  
1024  """
1025  function pnl(ai, ::ByPos{P}, price) where {P}
1026      pos = position(ai, P)
1027      isnothing(pos) && return 0.0
1028      pnl(pos, price)
1029  end
1030  
1031  @doc """ Calculate the profit and loss percentage (PnL%) of an asset position.
1032  
1033  $(TYPEDSIGNATURES)
1034  
1035  This function calculates the profit and loss percentage (PnL%) for an asset position in a `MarginInstance`. It takes into account the current price and the position. The PnL% represents the gain or loss made on the position, as a percentage of the investment, based on the current price compared to the price at which the position was opened.
1036  
1037  """
1038  function pnlpct(ai::MarginInstance, ::ByPos{P}, price; pos=position(ai, P)) where {P}
1039      isnothing(pos) && return 0.0
1040      pnlpct(pos, price)
1041  end
1042  pnlpct(ai::MarginInstance, v::Number) = begin
1043      pos = position(ai)
1044      isnothing(pos) && return 0.0
1045      pnlpct(pos, v)
1046  end
1047  
1048  @doc """ Get the last price for an `AssetInstance`.
1049  
1050  $(TYPEDSIGNATURES)
1051  
1052  This function returns the last known price for an `AssetInstance`. Additional arguments and keyword arguments can be provided to adjust the way the last price is calculated, if necessary.
1053  
1054  """
1055  function lastprice(ai::AssetInstance, args...; hist=false, kwargs...)
1056      exc = ai.exchange
1057      tickers = @tickers! markettype(exc, marginmode(ai)) false TICKERS_CACHE10
1058      tick = get(tickers, raw(ai), nothing)
1059      this_args = if isnothing(tick)
1060          if hist
1061              (ai, Val(:history))
1062          else
1063              (raw(ai), exc)
1064          end
1065      else
1066          (exc, tick)
1067      end
1068      lastprice(this_args...)
1069  end
1070  @doc """ Get the last price from the history for an `AssetInstance`.
1071  
1072  $(TYPEDSIGNATURES)
1073  
1074  This function returns the last known price from the historical data for an `AssetInstance`. It's useful when you need to reference the most recent historical price for calculations or comparisons.
1075  
1076  """
1077  function lastprice(ai::AssetInstance, ::Val{:history})
1078      v = ai.history
1079      if length(v) > 0
1080          last(v).price
1081      else
1082          lastprice(ai; hist=true)
1083      end
1084  end
1085  
1086  function lastprice(ai::AssetInstance, date::DateTime)
1087      h = trades(ai)
1088      if length(h) > 0
1089          trade = last(h)
1090          if date >= trade.date
1091              return trade.price
1092          end
1093      end
1094      lastprice(ai)
1095  end
1096  
1097  @doc """ Get the timeframe for an `AssetInstance`.
1098  
1099  $(TYPEDSIGNATURES)
1100  
1101  This function returns the timeframe for an `AssetInstance`. The timeframe represents the interval at which the asset's price data is sampled or updated.
1102  
1103  """
1104  function timeframe(ai::AssetInstance)
1105      data = getfield(ai, :data)
1106      if length(data) > 0
1107          first(keys(data))
1108      else
1109          @warn "asset: can't infer timeframe since there is not data"
1110          tf"1m"
1111      end
1112  end
1113  
1114  include("constructors.jl")
1115  
1116  export AssetInstance, instance, load!, @rprice, @ramount
1117  export asset, raw, ohlcv, ohlcv_dict, bc, qc, default_asset_df
1118  export takerfees, makerfees, maxfees, minfees, ishedged, isdust, nondust
1119  export Long, Short, position, posside, cash, committed
1120  export liqprice, leverage, bankruptcy, entryprice, price
1121  export additional, margin, maintenance
1122  export leverage, mmr, status!