/ SimMode / src / orders / limit.jl
limit.jl
  1  using .Lang: @deassert, @posassert, Lang, @ifdebug
  2  using .OrderTypes
  3  using Executors.Checks: cost, withfees
  4  using Executors: AnyFOKOrder, AnyIOCOrder, AnyGTCOrder, AnyPostOnlyOrder
  5  import Executors: priceat, unfilled, isqueued
  6  import .OrderTypes: order!, FOKOrderType, IOCOrderType
  7  using Simulations: Simulations as sml
  8  using .Strategies: Strategies as st
  9  
 10  @doc """ Creates a simulated limit order.
 11  
 12  $(TYPEDSIGNATURES)
 13  
 14  This function creates a limit order in a simulated environment. It takes a strategy `s`, an order type `t`, and an asset `ai` as inputs, along with an `amount` and an optional `skipcommit` flag. If the order is valid, it is queued for execution.
 15  """
 16  function create_sim_limit_order(s, t, ai; amount, skipcommit=false, kwargs...)
 17      o = limitorder(s, ai, amount; type=t, skipcommit, kwargs...)
 18      isnothing(o) && return nothing
 19      queue!(s, o, ai; skipcommit) || return nothing
 20      @deassert skipcommit || abs(committed(o)) > 0.0
 21      return o
 22  end
 23  
 24  @doc """ The price at a particular date for an order.
 25  
 26  $(TYPEDSIGNATURES)
 27  
 28  This function returns the price at a particular date for an order. It takes a strategy `s`, an order type, an asset `ai`, and a date as inputs.
 29  """
 30  function priceat(s::Strategy{Sim}, ::Type{<:Order}, ai, date)
 31      st.openat(ai, date)
 32  end
 33  priceat(s::Strategy{Sim}, ::T, args...) where {T<:Order} = priceat(s, T, args...)
 34  function priceat(s::MarginStrategy{Sim}, ::T, args...) where {T<:Order}
 35      priceat(s, T, args...)
 36  end
 37  
 38  @doc """ Determines if a buy limit order is triggered.
 39  
 40  $(TYPEDSIGNATURES)
 41  
 42  This function checks if a buy limit order `o` is triggered at a given `date` for an asset `ai`. It returns a boolean indicating whether the order is triggered.
 43  """
 44  _istriggered(o::AnyLimitOrder{Buy}, date, ai) = begin
 45      pbs = _pricebyside(o, date, ai)
 46      pbs, (pbs <= o.price)
 47  end
 48  @doc """ Determines if a sell limit order is triggered.
 49  
 50  $(TYPEDSIGNATURES)
 51  
 52  This function checks if a sell limit order `o` is triggered at a given `date` for an asset `ai`. It returns a boolean indicating whether the order is triggered.
 53  """
 54  _istriggered(o::AnyLimitOrder{Sell}, date, ai) = begin
 55      pbs = _pricebyside(o, date, ai)
 56      pbs, pbs >= o.price
 57  end
 58  
 59  @doc "Progresses a simulated limit order."
 60  function order!(
 61      s::NoMarginStrategy{Sim}, o::Order{<:LimitOrderType}, date::DateTime, ai; kwargs...
 62  )
 63      @deassert abs(committed(o)) > 0.0 o
 64      limitorder_ifprice!(s, o, date, ai; kwargs...)
 65  end
 66  
 67  @doc "Progresses a simulated limit order for an isolated margin strategy."
 68  function order!(
 69      s::IsolatedStrategy{Sim}, o::Order{<:LimitOrderType}, date::DateTime, ai; kwargs...
 70  )
 71      @deassert abs(committed(o)) > 0.0 (pricetime(o), o)
 72      t = limitorder_ifprice!(s, o, date, ai; kwargs...)
 73      @deassert gtxzero(s.cash_committed, atol=2s.cash_committed.precision) s.cash_committed.value
 74      t
 75  end
 76  
 77  @doc """ Executes a limit order at a particular time only if price is lower(buy) or higher(sell) than order price.
 78  
 79  $(TYPEDSIGNATURES)
 80  
 81  This function executes a limit order `o` at a given `date` for an asset `ai` only if the price is lower (for buy orders) or higher (for sell orders) than the order price.
 82  """
 83  function limitorder_ifprice!(s::Strategy{Sim}, o::AnyLimitOrder, date, ai; kwargs...)
 84      @ifdebug PRICE_CHECKS[] += 1
 85      pbs, triggered = _istriggered(o, date, ai)
 86      if triggered
 87          # Order might trigger on high/low, but execution uses the *close* price.
 88          limitorder_ifvol!(s, o, date, ai; kwargs...)
 89      elseif o isa Union{AnyFOKOrder,AnyIOCOrder}
 90          if cancel!(s, o, ai; err=NotMatched(o.price, pbs, 0.0, 0.0))
 91              nothing
 92          end
 93      else
 94          missing
 95      end
 96  end
 97  
 98  @doc """ Determines if a trade should succeed based on the volume of the candle compared to the order amount.
 99  
100  $(TYPEDSIGNATURES)
101  
102  This function calculates the ratio of the volume of the candle (`cdl_vol`) to the order amount.
103  Depending on the ratio, it determines if the trade should succeed and returns a boolean indicating the result along with the actual amount that can be filled.
104  """
105  function _fill_happened(
106      amount, cdl_vol, depth=1; initial_amount=amount, max_depth=4, max_reduction=0.1
107  )
108      # The higher the volume of the candle compared to the order amount
109      # the more likely the trade will succeed
110      Lang.@posassert amount cdl_vol depth initial_amount max_depth max_reduction
111      ratio = cdl_vol / amount
112      if ratio > 100.0
113          true, amount
114      elseif ratio > 10.0
115          rand() < log10(ratio), amount
116      elseif depth < max_depth # Only try a small number of times with reduced amount
117          reduced_amount = amount / 2.0
118          if reduced_amount > initial_amount * max_reduction
119              _fill_happened(reduced_amount, cdl_vol, depth + 1; initial_amount)
120          else
121              false, 0.0
122          end
123      else
124          false, 0.0
125      end
126  end
127  
128  @doc """ Executes a limit order at a particular time according to volume.
129  
130  $(TYPEDSIGNATURES)
131  
132  This function executes a limit order `o` at a given `date` for an asset `ai` based on the volume of the candle compared to the order amount. It checks if the trade should succeed and performs the trade if conditions are met.
133  """
134  function limitorder_ifvol!(s::Strategy{Sim}, o::AnyLimitOrder, date, ai; kwargs...)
135      @ifdebug VOL_CHECKS[] += 1
136      ans::Union{Missing,Nothing,Trade} = missing
137      cdl_vol = st.volumeat(ai, date)
138      amount = unfilled(o)
139      @deassert amount > 0.0
140      if o isa AnyFOKOrder # check for full fill
141          # FOK can only be filled with max amount, so use max_depth=1
142          triggered, actual_amount = _fill_happened(amount, cdl_vol; max_depth=1)
143          if triggered
144              @deassert amount == actual_amount
145              ans = trade!(s, o, ai; price=o.price, date, actual_amount, kwargs...)
146          else
147              if cancel!(
148                  s, o, ai; err=NotMatched(o.price, priceat(s, o, ai, date), amount, cdl_vol)
149              )
150                  ans = nothing
151              end
152          end
153          @deassert !isqueued(o, s, ai)
154      else
155          # GTC and IOC can be partially filled so allow for amount reduction (max_depth=4)
156          triggered, actual_amount = _fill_happened(
157              amount, cdl_vol; max_depth=4, max_reduction=0.1
158          )
159          if triggered
160              @deassert actual_amount > amount * 0.1
161              ans = if o isa AnyPostOnlyOrder && o.date == date
162                  cancel!(s, o, ai; err=OrderCanceled(o))
163                  nothing
164              else
165                  trade!(s, o, ai; price=o.price, date, actual_amount, kwargs...)
166              end
167          else
168              # Cancel IOC orders if partially filled
169              if o isa AnyIOCOrder &&
170                  !isfilled(ai, o) &&
171                  cancel!(s, o, ai; err=NotFilled(amount, cdl_vol))
172                  ans = nothing
173              end
174          end
175          @deassert o isa AnyGTCOrder || !isqueued(o, s, ai)
176      end
177      ans
178  end