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