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  }