positions.jl
1 using .Misc: MarginMode, WithMargin, Long, Short, PositionSide, ExecAction, HedgedMode 2 import .Misc: opposite, reset!, Misc 3 using .Instruments.Derivatives: Derivative 4 using Exchanges: LeverageTier, LeverageTiersDict, leverage_tiers, tier 5 import Exchanges: maxleverage, tier 6 using .Lang: @ifdebug 7 using Base: negate 8 import OrderTypes: isshort, islong, commit!, PositionUpdated, LeverageUpdated, MarginUpdated 9 import .Misc: marginmode 10 import .Instruments: cash! 11 12 @doc "A constant representing a vector of `DFT` type." 13 const OneVec = Vector{DFT} 14 15 @doc "A position has been opened." 16 struct PositionOpen <: ExecAction end 17 @doc "A position has been updated." 18 struct PositionUpdate <: ExecAction end 19 @doc "A position has been closed." 20 struct PositionClose <: ExecAction end 21 @doc "Position status is one of `PositionOpen`, `PositionClose`." 22 const PositionStatus = Union{PositionOpen,PositionClose} 23 @doc "Position change is one of `PositionOpen`, `PositionUpdate`, `PositionClose`." 24 const PositionChange = Union{PositionOpen,PositionUpdate,PositionClose} 25 opposite(::PositionOpen) = PositionClose() 26 opposite(::PositionClose) = PositionOpen() 27 28 @doc """ A position tracks the margin state of an asset instance. 29 30 $(FIELDS) 31 """ 32 @kwdef struct Position{P<:PositionSide,E<:ExchangeID,M<:MarginMode} 33 "Current status of the position" 34 status::Vector{PositionStatus} = [PositionClose()] 35 "Asset being tracked" 36 asset::Derivative 37 "Timestamp of the last update" 38 timestamp::Vector{DateTime} = [DateTime(0)] 39 "Asset liquidation price" 40 liquidation_price::OneVec = [0.0] 41 "Price at which the position was entered" 42 entryprice::OneVec = [0.0] 43 "Maintenance margin required for the position" 44 maintenance_margin::OneVec = [0.0] 45 "Initial margin required for the position" 46 initial_margin::OneVec = [0.0] 47 "Additional margin required for the position" 48 additional_margin::OneVec = [0.0] 49 "Notional value of the position" 50 notional::OneVec = [0.0] 51 "Cash value of the position" 52 cash::CCash{E}{S1} where {S1} 53 "Cash committed to the position" 54 cash_committed::CCash{E}{S2} where {S2} 55 "Leverage applied to the position" 56 leverage::OneVec = [1.0] 57 "Minimum size of the position" 58 min_size::T where {T<:Real} 59 "Whether the position is hedged or not" 60 hedged::Bool = false 61 "Leverage tiers applicable to the position" 62 tiers::Vector{LeverageTiersDict} 63 "Current tier applicable to the position" 64 this_tier::Vector{LeverageTier} 65 function Position{P,E,M}( 66 args...; kwargs... 67 ) where {P<:PositionSide,E<:ExchangeID,M<:MarginMode} 68 M == NoMargin && error("Trying to construct a position in `NoMargin` mode") 69 new{P,E,M}(args...; kwargs...) 70 end 71 end 72 73 74 exchangeid(::Position{<:PositionSide,E}) where {E<:ExchangeID} = E 75 76 @doc """ Resets position to initial state. 77 78 !!! warning "Also resets leverage" 79 When reopening a position, leverage should be set again. 80 81 """ 82 reset!(po::Position, ::Val{:full}) = begin 83 po.status[] = PositionClose() 84 po.timestamp[] = DateTime(0) 85 po.notional[] = 0.0 86 leverage!(po, 1.0) 87 tier!(po) 88 po.liquidation_price[] = 0.0 89 entryprice!(po, 0.0) 90 maintenance!(po, 0.0) 91 margin!(po) 92 additional!(po) 93 cash!(cash(po), 0.0) 94 cash!(committed(po), 0.0) 95 end 96 97 @doc """ Resets the bare fields to close a position. 98 99 $(TYPEDSIGNATURES) 100 101 """ 102 reset!(po::Position) = begin 103 po.status[] = PositionClose() 104 po.notional[] = 0.0 105 po.liquidation_price[] = 0.0 106 entryprice!(po, 0.0) 107 maintenance!(po, 0.0) 108 margin!(po) 109 additional!(po) 110 cash!(cash(po), 0.0) 111 cash!(committed(po), 0.0) 112 end 113 114 @doc "A constant representing a long position with margin in a specific exchange." 115 const LongPosition{E<:ExchangeID,M<:WithMargin} = Position{Long,E,M} 116 @doc "A constant representing a short position with margin in a specific exchange." 117 const ShortPosition{E<:ExchangeID,M<:WithMargin} = Position{Short,E,M} 118 119 @doc "The number of digits to keep for margin calculations." 120 const POSITION_PRECISION = 4 121 @doc "The number of digits allowed for leverage values." 122 const LEVERAGE_PRECISION = 2 123 @doc "A constant defining the rounding mode for positions as `RoundToZero`." 124 const POSITION_ROUNDING_MODE = RoundToZero 125 126 @doc """ Round function for values of position fields. 127 128 $(TYPEDSIGNATURES) 129 130 This function rounds the values of position fields to a specified precision. The default precision is `POSITION_PRECISION`. 131 132 """ 133 function _roundpos(v, digits=POSITION_PRECISION) 134 round(v, POSITION_ROUNDING_MODE; digits) 135 end 136 137 _roundlev(po, lev) = _roundpos(clamp(lev, 1.0, maxleverage(po)), LEVERAGE_PRECISION) 138 139 @doc "Updates position leverage." 140 function leverage!(po::Position, v) 141 @deassert maxleverage(po) == maxleverage(po, notional(po)) 142 po.leverage[] = _roundlev(po, v) 143 end 144 145 @doc """ Returns the maximum leverage for a given position and size. 146 147 $(TYPEDSIGNATURES) 148 149 The function retrieves the leverage tier applicable to the provided position and size, and returns the maximum leverage allowed within that tier. 150 151 """ 152 maxleverage(po::Position, size::Real) = tier(po, size)[2].max_leverage 153 maxleverage(po::Position) = po.this_tier[].max_leverage 154 155 _status!(po::Position, ::PositionClose) = begin 156 @assert po.status[] == PositionOpen() 157 po.status[] = PositionClose() 158 end 159 160 _status!(po::Position, ::PositionOpen) = begin 161 @assert po.status[] == PositionClose() 162 po.status[] = PositionOpen() 163 end 164 165 isopen(po::Position) = po.status[] == PositionOpen() 166 islong(::Position{Long}) = true 167 islong(::Position{Short}) = false 168 islong(::Union{Type{Long},Long}) = true 169 islong(::Union{Type{Short},Short}) = false 170 islong(::Missing) = false 171 isshort(::Position{Short}) = true 172 isshort(::Position{Long}) = false 173 isshort(::Union{Type{Short},Short}) = true 174 isshort(::Union{Type{Long},Long}) = false 175 isshort(::Missing) = false 176 Misc.marginmode(::Position{<:PositionSide,<:ExchangeID,M}) where {M<:MarginMode} = M() 177 function ishedged(::Position{<:PositionSide,<:ExchangeID,M}) where {M<:MarginMode} 178 ishedged(M()) 179 end 180 @doc """ Retrieves the leverage tier for a given position and size. 181 182 $(TYPEDSIGNATURES) 183 184 This function returns the tier that applies to a position of the provided size. 185 186 """ 187 function tier(po::Position, size) 188 # tier should work with abs values 189 @deassert tier(po.tiers[], po.cash.value) == tier(po.tiers[], negate(po.cash.value)) 190 tier(po.tiers[], size) 191 end 192 posside(::Position{P}) where {P<:PositionSide} = P() 193 posside(::ByPos{P}) where {P<:PositionSide} = P() 194 function posside(::Type{<:Order{<:O,<:A,<:E,P}}) where {O,A,E,P<:PositionSide} 195 P() 196 end 197 198 @doc "Position entryprice." 199 price(po::Position) = po.entryprice[] 200 entryprice(po::Position) = price(po) 201 @doc "Position liquidation price." 202 liqprice(pos::Position) = pos.liquidation_price[] 203 @doc "Position leverage." 204 leverage(pos::Position) = pos.leverage[] 205 @doc "Position status (open or closed)." 206 status(pos::Position) = pos.status[] 207 @doc "Position maintenance margin." 208 maintenance(pos::Position) = pos.maintenance_margin[] 209 @doc "Position initial margin (includes additional)." 210 margin(pos::Position) = pos.initial_margin[] 211 initial(args...; kwargs...) = margin(args...; kwargs...) 212 @doc "Position additional margin." 213 additional(pos::Position) = pos.additional_margin[] 214 @doc "Position maintenance margin rate." 215 mmr(pos::Position) = pos.this_tier[].mmr 216 @doc "Position notional value." 217 notional(pos::Position) = pos.notional[] 218 @doc "Held position." 219 cash(po::Position) = po.cash 220 @doc "Position locked in pending orders." 221 committed(po::Position) = po.cash_committed 222 @doc "Maximum value that can be lost by the position" 223 collateral(po::Position) = margin(po) + additional(po) 224 @doc "Last position update time" 225 timestamp(po::Position) = po.timestamp[] 226 227 @doc """ The price where the position is fully liquidated. 228 229 $(TYPEDSIGNATURES) 230 231 This function calculates and returns the price at which a position, given its leverage (`lev`), would be fully liquidated. 232 233 """ 234 function bankruptcy(price::Real, lev::Real, ::Long) 235 @deassert lev != 0.0 236 price * (lev - 1.0) / lev 237 end 238 function bankruptcy(price::Real, lev::Real, ::Short) 239 @deassert lev != 0.0 240 price * (lev + 1.0) / lev 241 end 242 function bankruptcy(pos::Position{P}, price) where P<:PositionSide 243 lev = leverage(pos) 244 bankruptcy(price, lev, P()) 245 end 246 function bankruptcy(pos::Position, o::Order{T,A,E,P}) where {T,A,E,P<:PositionSide} 247 bankruptcy(pos, o.price) 248 end 249 250 @doc """ Updates the timestamp of a position. 251 252 $(TYPEDSIGNATURES) 253 254 This function sets the timestamp of a given position (`po`) to the provided DateTime value (`d`). 255 256 """ 257 function timestamp!(po::Position, d::DateTime) 258 @deassert po.timestamp[] <= d "Position dates can only go forward.($(po.timestamp[]), $d)" 259 po.timestamp[] = d 260 end 261 262 @doc """ Updates position leverage tier according to size. 263 264 $(TYPEDSIGNATURES) 265 266 This function adjusts the leverage tier of a given position (`po`) based on the provided size. If no size is provided, the notional value of the position is used. 267 268 """ 269 function tier!(po::Position, size=notional(po)) 270 po.this_tier[] = tier(po, size)[2] 271 end 272 273 @doc "Update the entry price. 274 275 $(TYPEDSIGNATURES) 276 " 277 function entryprice!(po::Position, v=abs(notional(po) / cash(po))) 278 po.entryprice[] = v 279 end 280 281 @doc """ Update the notional value. 282 283 $(TYPEDSIGNATURES) 284 285 This function updates the notional value of a given position (`po`) to the provided value (`v`). 286 287 """ 288 function notional!(po::Position, v) 289 @ifdebug v < 0.0 && @warn "Notional value should never be negative ($v)" 290 po.notional[] = abs(v) 291 # update leverage tier after notional update 292 tier!(po) 293 v 294 end 295 296 @doc """ Sets initial margin given notional and leverage values. 297 298 $(TYPEDSIGNATURES) 299 300 This function sets the initial margin of a given position (`po`) based on the provided notional value (`ntl`) and leverage (`lev`). If no values are provided, the current notional value and leverage of the position are used. 301 302 """ 303 function margin!(po::Position; ntl=notional(po), lev=leverage(po)) 304 m = _roundpos(ntl / lev) 305 @deassert m <= notional(po) 306 po.initial_margin[] = m 307 end 308 309 @doc """ Sets initial margin (should always be positive). 310 311 $(TYPEDSIGNATURES) 312 313 This function sets the initial margin of a given position (`po`) to the provided value (`v`). If no value is provided, it defaults to 0.0. 314 315 """ 316 function initial!(po::Position, v=0.0) 317 po.initial_margin[] = abs(v) |> _roundpos 318 end 319 320 @doc """ Sets additional margin (should always be positive). 321 322 $(TYPEDSIGNATURES) 323 324 This function sets the additional margin of a given position (`po`) to the provided value (`v`). If no value is provided, it defaults to 0.0. 325 326 """ 327 function additional!(po::Position, v=0.0) 328 @deassert v + margin(po) <= notional(po) 329 po.additional_margin[] = abs(v) |> _roundpos 330 end 331 332 @doc """ Adds margin to a position. 333 334 $(TYPEDSIGNATURES) 335 336 This function adds a specified amount (`v`) to the margin of a given position (`po`). 337 338 """ 339 function addmargin!(po::Position, v) 340 @deassert margin(po) + v <= notional(po) 341 po.additional_margin[] = max(0.0, v + additional(po)) |> _roundpos 342 end 343 344 @doc """ Sets maintenance margin. 345 346 $(TYPEDSIGNATURES) 347 348 This function sets the maintenance margin of a given position (`po`) to the provided value (`v`). 349 350 """ 351 function maintenance!(po::Position, v) 352 po.maintenance_margin[] = _roundpos(v) 353 @deassert maintenance(po) <= margin(po) 354 v 355 end 356 357 @doc "Set position cash value." 358 Instruments.cash!(po::Position, v) = cash!(cash(po), v) 359 @doc "Set position committed cash value." 360 OrderTypes.commit!(po::Position, v) = cash!(committed(po), v) 361 362 @doc """ Calc PNL for long position given `current_price` as input. 363 364 $(TYPEDSIGNATURES) 365 366 This function calculates the Profit and Loss (PNL) for a long position (`po`), given the current price (`current_price`) and an optional amount (`amount`). If no amount is provided, the cash value of the position is used. 367 368 """ 369 function pnl(po::Position{Long}, current_price, amount=cash(po)) 370 @deassert notional(po) ~= abs(cash(po)) * price(po) 371 isopen(po) || return 0.0 372 pnl(price(po), current_price, amount, Long()) 373 end 374 375 @doc """ Calc PNL for short position given `current_price` as input. 376 377 $(TYPEDSIGNATURES) 378 379 This function calculates the Profit and Loss (PNL) for a short position (`po`), given the current price (`current_price`) and an optional amount (`amount`). If no amount is provided, the cash value of the position is used. 380 381 """ 382 function pnl(po::Position{Short}, current_price, amount=cash(po)) 383 isopen(po) || return 0.0 384 pnl(price(po), current_price, amount, Short()) 385 end 386 387 @doc """ Calc PNL percentage. 388 389 $(TYPEDSIGNATURES) 390 391 This function calculates the Profit and Loss (PNL) percentage for a given position (`po`) and value (`v`). 392 393 """ 394 pnlpct(po::Position, v) = begin 395 isopen(po) || return 0.0 396 pnl(po, v, 1.0) / price(po) 397 end 398 399 @doc """ Calculate PNL for a long position. 400 401 $(TYPEDSIGNATURES) 402 403 This function calculates the Profit and Loss (PNL) for a long position, given the entry price (`entryprice`), the current price (`current_price`), and the amount. 404 405 """ 406 function pnl(entryprice::T, current_price::T, amount, ::ByPos{Long}) where {T} 407 (current_price - entryprice) * abs(amount) 408 end 409 410 @doc """ Calculate PNL for a short position. 411 412 $(TYPEDSIGNATURES) 413 414 This function calculates the Profit and Loss (PNL) for a short position, given the entry price (`entryprice`), the current price (`current_price`), and the amount. 415 416 """ 417 function pnl(entryprice::T, current_price::T, amount, ::ByPos{Short}) where {T} 418 (entryprice - current_price) * abs(amount) 419 end 420 421 @doc """ Sets the liquidation price for a long position. 422 423 $(TYPEDSIGNATURES) 424 425 This function sets the liquidation price of a given long position (`po`) to the provided value (`v`). 426 427 """ 428 function liqprice!(po::Position{Long}, v) 429 @deassert v <= price(po) 430 po.liquidation_price[] = v 431 end 432 433 @doc """ Sets the liquidation price for a short position. 434 435 $(TYPEDSIGNATURES) 436 437 This function sets the liquidation price of a given short position (`po`) to the provided value (`v`). 438 439 """ 440 function liqprice!(po::Position{Short}, v) 441 @deassert v >= price(po) (v, price(po)) 442 po.liquidation_price[] = v 443 end 444 445 function Base.abs(po::Position) 446 abs(cash(po)) 447 end 448 449 function Base.print(io::IO, po::Position) 450 write(io, "Position($(posside(po)), $(po.asset))\n") 451 write(io, "entryprice: ") 452 write(io, string(price(po))) 453 write(io, "\namount: ") 454 write(io, string(cash(po))) 455 write(io, "\nnotional: ") 456 write(io, string(notional(po))) 457 write(io, "\ncollateral: ") 458 write(io, string(collateral(po))) 459 write(io, "\nleverage: ") 460 write(io, string(leverage(po))) 461 write(io, "\nmaintenance: ") 462 write(io, string(maintenance(po))) 463 write(io, "\nliquidation price: ") 464 write(io, string(liqprice(po))) 465 write(io, "\ndate: ") 466 write(io, string(po.timestamp[])) 467 end 468 469 Base.show(io::IO, ::MIME"text/plain", po::Position) = print(io, po) 470 Base.show(io::IO, po::Position) = print(io, po) 471 472 function PositionUpdated(tag, group, pos::Position) 473 PositionUpdated{exchangeid(pos)}( 474 Symbol(tag), 475 Symbol(group), 476 raw(pos.asset), 477 (posside(pos), isopen(pos)), 478 timestamp(pos), 479 liqprice(pos), 480 entryprice(pos), 481 maintenance(pos), 482 initial(pos), 483 leverage(pos), 484 notional(pos), 485 ) 486 end 487 488 # TODO: add `AddMargin` call! function 489 function MarginUpdated(tag, group, pos::Position; from_value::DFT=0.0) 490 MarginUpdated{exchangeid(pos)}( 491 Symbol(tag), 492 Symbol(group), 493 raw(pos.asset), 494 posside(pos), 495 timestamp(pos), 496 string(marginmode(pos)), 497 from_value, 498 margin(pos), 499 ) 500 end 501 502 function LeverageUpdated(tag, group, pos::Position; from_value::DFT=one(0.0)) 503 LeverageUpdated{exchangeid(pos)}( 504 Symbol(tag), 505 Symbol(group), 506 raw(pos.asset), 507 posside(pos), 508 timestamp(pos), 509 from_value, 510 leverage(pos), 511 ) 512 end 513 514 export notional, additional, price, bankruptcy, pnl, collateral 515 export status, timestamp, tier 516 export timestamp!, leverage!, tier! 517 export liqprice!, margin!, maintenance!, initial!, additional!, notional! 518 export PositionOpen, PositionClose, PositionUpdate, PositionStatus, PositionChange