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