code.vegaprotocol.io/vega@v0.79.0/datanode/entities/stop_orders.go (about) 1 // Copyright (C) 2023 Gobalsky Labs Limited 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 package entities 17 18 import ( 19 "encoding/json" 20 "errors" 21 "fmt" 22 "time" 23 24 "code.vegaprotocol.io/vega/libs/num" 25 "code.vegaprotocol.io/vega/libs/ptr" 26 v2 "code.vegaprotocol.io/vega/protos/data-node/api/v2" 27 "code.vegaprotocol.io/vega/protos/vega" 28 commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1" 29 pbevents "code.vegaprotocol.io/vega/protos/vega/events/v1" 30 ) 31 32 type ( 33 _StopOrder struct{} 34 StopOrderID = ID[_StopOrder] 35 StopOrder struct { 36 ID StopOrderID 37 OCOLinkID StopOrderID 38 ExpiresAt *time.Time 39 ExpiryStrategy StopOrderExpiryStrategy 40 TriggerDirection StopOrderTriggerDirection 41 Status StopOrderStatus 42 CreatedAt time.Time 43 UpdatedAt *time.Time 44 OrderID OrderID 45 TriggerPrice *string 46 TriggerPercentOffset *string 47 PartyID PartyID 48 MarketID MarketID 49 VegaTime time.Time 50 SeqNum uint64 51 TxHash TxHash 52 Submission *commandspb.OrderSubmission 53 RejectionReason StopOrderRejectionReason 54 SizeOverrideSetting int32 55 SizeOverrideValue *string 56 } 57 ) 58 59 type StopOrderKey struct { 60 ID StopOrderID 61 UpdatedAt time.Time 62 VegaTime time.Time 63 } 64 65 var StopOrderColumns = []string{ 66 "id", 67 "oco_link_id", 68 "expires_at", 69 "expiry_strategy", 70 "trigger_direction", 71 "status", 72 "created_at", 73 "updated_at", 74 "order_id", 75 "trigger_price", 76 "trigger_percent_offset", 77 "party_id", 78 "market_id", 79 "vega_time", 80 "seq_num", 81 "tx_hash", 82 "submission", 83 "rejection_reason", 84 "size_override_setting", 85 "size_override_value", 86 } 87 88 func (o StopOrder) ToProto() *pbevents.StopOrderEvent { 89 var ocoLinkID *string 90 var expiresAt, updatedAt *int64 91 var expiryStrategy *vega.StopOrder_ExpiryStrategy 92 var triggerPrice *vega.StopOrder_Price 93 var triggerPercentOffset *vega.StopOrder_TrailingPercentOffset 94 95 if o.OCOLinkID != "" { 96 ocoLinkID = ptr.From(o.OCOLinkID.String()) 97 } 98 99 if o.ExpiresAt != nil { 100 expiresAt = ptr.From(o.ExpiresAt.UnixNano()) 101 } 102 103 if o.ExpiryStrategy != StopOrderExpiryStrategyUnspecified { 104 expiryStrategy = ptr.From(vega.StopOrder_ExpiryStrategy(o.ExpiryStrategy)) 105 } 106 107 if o.TriggerPrice != nil { 108 triggerPrice = &vega.StopOrder_Price{ 109 Price: *o.TriggerPrice, 110 } 111 } 112 113 if o.TriggerPercentOffset != nil { 114 triggerPercentOffset = &vega.StopOrder_TrailingPercentOffset{ 115 TrailingPercentOffset: *o.TriggerPercentOffset, 116 } 117 } 118 119 // We cannot copy a nil value to a enum field in the database when using copy, so we only set the 120 // rejection reason on the proto if the stop order is rejected. Otherwise, we will leave the proto field 121 // as nil 122 var rejectionReason *vega.StopOrder_RejectionReason 123 if o.Status == StopOrderStatusRejected { 124 rejectionReason = ptr.From(vega.StopOrder_RejectionReason(o.RejectionReason)) 125 } 126 127 var sizeOVerrideValue *vega.StopOrder_SizeOverrideValue 128 129 if o.SizeOverrideValue != nil { 130 sizeOVerrideValue = &vega.StopOrder_SizeOverrideValue{ 131 Percentage: *o.SizeOverrideValue, 132 } 133 } 134 135 stopOrder := &vega.StopOrder{ 136 Id: o.ID.String(), 137 OcoLinkId: ocoLinkID, 138 ExpiresAt: expiresAt, 139 ExpiryStrategy: expiryStrategy, 140 TriggerDirection: vega.StopOrder_TriggerDirection(o.TriggerDirection), 141 Status: vega.StopOrder_Status(o.Status), 142 CreatedAt: o.CreatedAt.UnixNano(), 143 UpdatedAt: updatedAt, 144 OrderId: o.OrderID.String(), 145 PartyId: o.PartyID.String(), 146 MarketId: o.MarketID.String(), 147 RejectionReason: rejectionReason, 148 SizeOverrideSetting: vega.StopOrder_SizeOverrideSetting(o.SizeOverrideSetting), 149 SizeOverrideValue: sizeOVerrideValue, 150 } 151 152 if triggerPrice != nil { 153 stopOrder.Trigger = triggerPrice 154 } 155 156 if triggerPercentOffset != nil { 157 stopOrder.Trigger = triggerPercentOffset 158 } 159 160 event := &pbevents.StopOrderEvent{ 161 Submission: o.Submission, 162 StopOrder: stopOrder, 163 } 164 165 return event 166 } 167 168 func (s StopOrder) Key() StopOrderKey { 169 updatedAt := s.CreatedAt 170 if s.UpdatedAt != nil { 171 updatedAt = *s.UpdatedAt 172 } 173 174 return StopOrderKey{ 175 ID: s.ID, 176 UpdatedAt: updatedAt, 177 VegaTime: s.VegaTime, 178 } 179 } 180 181 func (s StopOrder) Cursor() *Cursor { 182 cursor := StopOrderCursor{ 183 CreatedAt: s.CreatedAt, 184 ID: s.ID, 185 VegaTime: s.VegaTime, 186 } 187 188 return NewCursor(cursor.String()) 189 } 190 191 func (s StopOrder) ToProtoEdge(_ ...any) (*v2.StopOrderEdge, error) { 192 return &v2.StopOrderEdge{ 193 Node: s.ToProto(), 194 Cursor: s.Cursor().Encode(), 195 }, nil 196 } 197 198 func StopOrderFromProto(so *pbevents.StopOrderEvent, vegaTime time.Time, seqNum uint64, txHash TxHash) (StopOrder, error) { 199 var ( 200 ocoLinkID StopOrderID 201 expiresAt, updatedAt *time.Time 202 expiryStrategy = StopOrderExpiryStrategyUnspecified 203 triggerPrice, triggerPercentOffset *string 204 ) 205 206 if so.StopOrder.OcoLinkId != nil { 207 ocoLinkID = StopOrderID(*so.StopOrder.OcoLinkId) 208 } 209 210 if so.StopOrder.ExpiresAt != nil { 211 expiresAt = ptr.From(NanosToPostgresTimestamp(*so.StopOrder.ExpiresAt)) 212 } 213 214 if so.StopOrder.ExpiryStrategy != nil { 215 expiryStrategy = StopOrderExpiryStrategy(*so.StopOrder.ExpiryStrategy) 216 } 217 218 if so.StopOrder.UpdatedAt != nil { 219 updatedAt = ptr.From(NanosToPostgresTimestamp(*so.StopOrder.UpdatedAt)) 220 if updatedAt.After(vegaTime) { 221 return StopOrder{}, fmt.Errorf("stop order updated time is in the future") 222 } 223 } 224 225 switch so.StopOrder.Trigger.(type) { 226 case *vega.StopOrder_Price: 227 price := so.StopOrder.GetPrice() 228 _, err := num.DecimalFromString(price) 229 if err != nil { 230 return StopOrder{}, fmt.Errorf("invalid stop order trigger price: %w", err) 231 } 232 233 triggerPrice = ptr.From(price) 234 case *vega.StopOrder_TrailingPercentOffset: 235 offset := so.StopOrder.GetTrailingPercentOffset() 236 percentage, err := num.DecimalFromString(offset) 237 if err != nil { 238 return StopOrder{}, fmt.Errorf("invalid stop order trigger percent offset: %w", err) 239 } 240 if percentage.LessThan(num.DecimalZero()) || percentage.GreaterThan(num.DecimalOne()) { 241 return StopOrder{}, errors.New("invalid stop order trigger percent offset, must be decimal value between 0 and 1") 242 } 243 244 triggerPercentOffset = ptr.From(offset) 245 } 246 247 // We will default to unspecified as we need to have a value in the enum field for the pgx copy command to work 248 // as it calls EncodeText on the enum fields and this will fail if the value is nil 249 // We will only use the rejection reason when we convert back to proto if the status of the order is rejected. 250 rejectionReason := StopOrderRejectionReasonUnspecified 251 if so.StopOrder.RejectionReason != nil { 252 rejectionReason = StopOrderRejectionReason(*so.StopOrder.RejectionReason) 253 } 254 255 var sizeOverrideValue *string 256 257 if so.StopOrder.SizeOverrideValue != nil && so.StopOrder.SizeOverrideValue.Percentage != "" { 258 sizeOverrideValue = ptr.From(so.StopOrder.SizeOverrideValue.Percentage) 259 } 260 261 stopOrder := StopOrder{ 262 ID: StopOrderID(so.StopOrder.Id), 263 OCOLinkID: ocoLinkID, 264 ExpiresAt: expiresAt, 265 ExpiryStrategy: expiryStrategy, 266 TriggerDirection: StopOrderTriggerDirection(so.StopOrder.TriggerDirection), 267 Status: StopOrderStatus(so.StopOrder.Status), 268 CreatedAt: NanosToPostgresTimestamp(so.StopOrder.CreatedAt), 269 UpdatedAt: updatedAt, 270 OrderID: OrderID(so.StopOrder.OrderId), 271 TriggerPrice: triggerPrice, 272 TriggerPercentOffset: triggerPercentOffset, 273 PartyID: PartyID(so.StopOrder.PartyId), 274 MarketID: MarketID(so.StopOrder.MarketId), 275 VegaTime: vegaTime, 276 SeqNum: seqNum, 277 TxHash: txHash, 278 Submission: so.Submission, 279 RejectionReason: rejectionReason, 280 SizeOverrideSetting: int32(so.StopOrder.SizeOverrideSetting), 281 SizeOverrideValue: sizeOverrideValue, 282 } 283 284 return stopOrder, nil 285 } 286 287 func (so StopOrder) ToRow() []interface{} { 288 return []interface{}{ 289 so.ID, 290 so.OCOLinkID, 291 so.ExpiresAt, 292 so.ExpiryStrategy, 293 so.TriggerDirection, 294 so.Status, 295 so.CreatedAt, 296 so.UpdatedAt, 297 so.OrderID, 298 so.TriggerPrice, 299 so.TriggerPercentOffset, 300 so.PartyID, 301 so.MarketID, 302 so.VegaTime, 303 so.SeqNum, 304 so.TxHash, 305 so.Submission, 306 so.RejectionReason, 307 so.SizeOverrideSetting, 308 so.SizeOverrideValue, 309 } 310 } 311 312 type StopOrderCursor struct { 313 CreatedAt time.Time `json:"createdAt"` 314 ID StopOrderID `json:"id"` 315 VegaTime time.Time `json:"vegaTime"` 316 } 317 318 func (c *StopOrderCursor) Parse(cursorString string) error { 319 if cursorString == "" { 320 return nil 321 } 322 return json.Unmarshal([]byte(cursorString), c) 323 } 324 325 func (c *StopOrderCursor) String() string { 326 bs, err := json.Marshal(c) 327 if err != nil { 328 // This should never happen 329 panic(fmt.Errorf("failed to marshal order stop cursor: %w", err)) 330 } 331 return string(bs) 332 }