/ SimMode / src / positions / utils.jl
utils.jl
  1  using .OrderTypes.ExchangeTypes: ExchangeID
  2  using .OrderTypes: PositionSide, PositionTrade, LiquidationType, ReduceOnlyOrder
  3  using .Strategies.Instruments.Derivatives: Derivative
  4  using Executors.Instances: leverage_tiers, tier, position
  5  import Executors.Instances: Position, MarginInstance
  6  using Executors: withtrade!, maintenance!, orders, isliquidatable, LIQUIDATION_FEES
  7  using .Strategies: IsolatedStrategy, MarginStrategy, exchangeid
  8  using .Instances: PositionOpen, PositionUpdate, PositionClose
  9  using .Instances: margin, maintenance, status, posside
 10  using .Misc: DFT
 11  import Executors: position!
 12  
 13  """
 14  Open a position in `s` with `ai` using `t`.
 15  
 16  $(TYPEDSIGNATURES)
 17  
 18  The function opens a position in the specified strategy using the given margin instance and position trade.
 19  """
 20  function open_position!(
 21      s::IsolatedStrategy, ai::MarginInstance, t::PositionTrade{P};
 22  ) where {P<:PositionSide}
 23      # NOTE: Order of calls is important
 24      po = position(ai, P)
 25      @deassert cash(ai, opposite(P())) == 0.0 (cash(ai, opposite(P()))),
 26      status(ai, opposite(P()))
 27      @deassert !isopen(po)
 28      @deassert notional(po) == 0.0
 29      # Cash should already be updated from trade construction
 30      @deassert abs(cash(po)) == abs(cash(ai, P())) >= abs(t.amount)
 31      withtrade!(po, t)
 32      # Notional should never be above the trade size
 33      # unless fees are negative
 34      @deassert notional(po) < abs(t.size) ||
 35          minfees(ai) < 0.0 ||
 36          abs(t.amount) < abs(cash(ai, P()))
 37      # finalize
 38      status!(ai, P(), PositionOpen())
 39      @deassert status(po) == PositionOpen()
 40      call!(s, ai, t, po, PositionOpen())
 41  end
 42  
 43  @doc """Force exit a position.
 44  
 45  $(TYPEDSIGNATURES)
 46  
 47  This function cancels all orders associated with the specified position and updates the position with a forced order. The function also handles cases where the position is already closed or has zero committed funds.
 48  
 49  """
 50  function force_exit_position(s::Strategy, ai, p, date::DateTime; kwargs...)
 51      @assert !hasorders(s, ai)
 52      @deassert isempty(collect(values(s, ai, p)))
 53      @deassert iszero(committed(ai, p)) committed(ai, p)
 54      ot = ReduceOnlyOrder(p)
 55      price = priceat(s, ot, ai, date)
 56      amount = abs(nondust(ai, ot, price))
 57      if amount > 0.0
 58          prevcash = s.cash.value
 59          t = call!(s, ai, ot; amount, date, price, kwargs...)
 60          @debug "force exit position: " amount price t.price s.cash.value - prevcash t.value
 61          @deassert let o = t.order
 62              (
 63                  t isa Trade &&
 64                  o.date == date &&
 65                  isapprox(o.amount, amount; atol=ai.precision.amount)
 66              ) || isnothing(t)
 67          end
 68          # marketorder!(s, o, ai, o.amount; o.price, date, slippage)
 69          @deassert isdust(ai, price, p)
 70      end
 71  end
 72  
 73  """
 74  Closes a leveraged position.
 75  
 76  $(TYPEDSIGNATURES)
 77  
 78  When a date is given, this function closes pending orders and sells the remaining cash.
 79  It then resets the position, deletes it from the holdings, and checks that the position is closed and no funds are committed.
 80  
 81  """
 82  function close_position!(s::IsolatedStrategy, ai, p::PositionSide, date=nothing; kwargs...)
 83      @deassert !hasorders(s, ai, p)
 84      # when a date is given we should close pending orders and sell remaining cash
 85      if !isnothing(date)
 86          force_exit_position(s, ai, p, date; kwargs...)
 87      end
 88      reset!(ai, p)
 89      delete!(s.holdings, ai)
 90      @deassert !isopen(position(ai, p)) && iszero(ai)
 91      true
 92  end
 93  
 94  # TODO: Implement updating margin of open positions
 95  # function update_margin!(pos::Position, qty::Real)
 96  #     p = posside(pos)
 97  #     price = entryprice(pos)
 98  #     lev = leverage(pos)
 99  #     size = notional(pos)
100  #     prev_additional = margin(pos) - size / lev
101  #     @deassert prev_additional >= 0.0 && qty >= 0.0
102  #     additional = prev_additional + qty
103  #     liqp = liqprice(p, price, lev, mmr(pos); additional, size)
104  #     liqprice!(pos, liqp)
105  #     # margin!(pos, )
106  # end
107  
108  @doc """ Liquidates a position at a particular date.
109  
110  $(TYPEDSIGNATURES)
111  
112  `fees`: the fees for liquidating a position (usually higher than trading fees.)
113  `actual_price/amount`: the price/amount to execute the liquidation market order with (for paper mode).
114  """
115  function liquidate!(
116      s::MarginStrategy, ai::MarginInstance, p::PositionSide, date, fees=LIQUIDATION_FEES;
117  )
118      pos = position(ai, p)
119      ords = collect(values(s, ai, p))
120      for o in ords
121          @deassert o isa Order
122          cancel!(s, o, ai; err=LiquidationOverride(o, liqprice(pos), date, p))
123      end
124      amount = abs(cash(pos).value)
125      price = liqprice(pos)
126      t = call!(s, ai, LiquidationOrder{liqside(p),typeof(p)}; amount, date, price, fees)
127      isnothing(t) || begin
128          @deassert t.order.date == date && 0.0 < abs(t.amount) <= abs(t.order.amount)
129      end
130      @deassert isdust(ai, price, p) (notional(ai, p), cash(ai, p), cash(ai, p) * price, p)
131      close_position!(s, ai, p)
132  end
133  
134  """
135  Checks asset positions for liquidations and executes them (Non hedged mode, so only the currently open position).
136  
137  $(TYPEDSIGNATURES)
138  
139  If a position is open and liquidatable, it is liquidated using the `liquidate!` function.
140  The liquidation is performed on the asset positions in `ai` on the specified `date`.
141  
142  """
143  function maybe_liquidate!(s::IsolatedStrategy, ai::MarginInstance, date::DateTime)
144      pos = position(ai)
145      isnothing(pos) && return nothing
146      @deassert !isopen(opposite(ai, pos))
147      p = posside(pos)
148      isliquidatable(s, ai, p, date) && liquidate!(s, ai, p, date)
149  end
150  
151  @doc """Updates the position by applying a position trade.
152  
153  $(TYPEDSIGNATURES)
154  
155  Applies the position trade `t` to the isolated strategy `s` and the margin instance `ai`.
156  The order of calls is important.
157  Checks if the position has a notional value not equal to zero.
158  Updates the cash of the position using the trade construction.
159  """
160  function update_position!(
161      s::IsolatedStrategy, ai, t::PositionTrade{P}
162  ) where {P<:PositionSide}
163      # NOTE: Order of calls is important
164      po = position(ai, P)
165      @deassert notional(po) != 0.0
166      # Cash should already be updated from trade construction
167      withtrade!(po, t)
168      # position is still open
169      call!(s, ai, t, po, PositionUpdate())
170  end
171  
172  @doc """ Updates or opens a position based on a given trade.
173  
174  $(TYPEDSIGNATURES)
175  
176  This function checks if a position is open. If it is, it either updates the position with the given trade or closes it if the position is dust.
177  If the position is not open, it opens a new position with the given trade.
178  After updating or opening the position, it checks if the position needs to be liquidated.
179  
180  """
181  function position!(
182      s::IsolatedStrategy, ai::MarginInstance, t::PositionTrade{P}; check_liq=true
183  ) where {P<:PositionSide}
184      @deassert exchangeid(s) == exchangeid(t)
185      @deassert t.order.asset == ai.asset
186      pos = position(ai, P)
187      if isopen(pos)
188          if isdust(ai, t.price, P())
189              close_position!(s, ai, P())
190          else
191              @deassert !iszero(cash(pos)) || t isa ReduceTrade
192              @debug "position update" pos.entryprice[] t.value t.price
193              update_position!(s, ai, t)
194          end
195      elseif t isa IncreaseTrade
196          @debug "position open" cash(ai, t) t
197          open_position!(s, ai, t)
198      end
199      if check_liq
200          maybe_liquidate!(s, ai, t.date)
201      end
202  end
203  
204  @doc """ Updates an isolated position in `Sim` mode from a new candle.
205  
206  $(TYPEDSIGNATURES)
207  
208  This function checks if a position is open and updates the timestamp.
209  If the position is liquidatable, it is liquidated.
210  Otherwise, the position remains open and a `PositionUpdate` is pinged.
211  
212  """
213  function position!(s::IsolatedStrategy{Sim}, ai, date::DateTime, pos::Position=position(ai))
214      # NOTE: Order of calls is important
215      @deassert isopen(pos)
216      p = posside(pos)
217      @deassert notional(pos) != 0.0
218      timestamp!(pos, date)
219      if isliquidatable(s, ai, p, date)
220          liquidate!(s, ai, p, date)
221      else
222          # position is still open
223          call!(s, ai, date, pos, PositionUpdate())
224      end
225  end
226  
227  _checkorders(s) = begin
228      for (_, ords) in s.buyorders
229          for (_, o) in ords
230              @assert abs(committed(o)) > 0.0
231          end
232      end
233      for (_, ords) in s.sellorders
234          for (_, o) in ords
235              @assert abs(committed(o)) > 0.0
236          end
237      end
238  end
239  
240  """ Updates all open positions in an isolated (non hedged) strategy for a specific date.
241  
242  $(TYPEDSIGNATURES)
243  
244  This function is used to update the state of all active asset holdings within the provided instance of `IsolatedStrategy` for a specified date.
245  Execution updates include the maintenance of position and order records and accounting for any change of asset state to reflect liquidations or trade updates.
246  """
247  function positions!(s::IsolatedStrategy{<:Union{Paper,Sim}}, date::DateTime)
248      @ifdebug _checkorders(s)
249      for ai in s.holdings
250          @deassert isopen(ai) || hasorders(s, ai) ai
251          if isopen(ai)
252              position!(s, ai, date)
253          end
254      end
255      @ifdebug _checkorders(s)
256      @ifdebug for ai in universe(s)
257          @assert !(isopen(ai, Short()) && isopen(ai, Long()))
258          po = position(ai)
259          @assert if !isnothing(po)
260              ai ∈ s.holdings && !iszero(cash(po)) && isopen(po)
261          else
262              iszero(cash(ai, Long())) &&
263                  iszero(cash(ai, Short())) &&
264                  !isopen(ai, Long()) &&
265                  !isopen(ai, Short())
266          end
267      end
268  end
269  
270  positions!(args...; kwargs...) = nothing