code.vegaprotocol.io/vega@v0.79.0/datanode/sqlstore/liquidity_provision.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 sqlstore
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  
    23  	"code.vegaprotocol.io/vega/datanode/entities"
    24  	"code.vegaprotocol.io/vega/datanode/metrics"
    25  	"code.vegaprotocol.io/vega/datanode/utils"
    26  	"code.vegaprotocol.io/vega/logging"
    27  	v2 "code.vegaprotocol.io/vega/protos/data-node/api/v2"
    28  	"code.vegaprotocol.io/vega/protos/vega"
    29  
    30  	"github.com/georgysavva/scany/pgxscan"
    31  	"github.com/jackc/pgx/v4"
    32  )
    33  
    34  var lpOrdering = TableOrdering{
    35  	ColumnOrdering{Name: "vega_time", Sorting: ASC},
    36  	ColumnOrdering{Name: "id", Sorting: ASC},
    37  }
    38  
    39  var providerOrdering = TableOrdering{
    40  	ColumnOrdering{Name: "market_id", Sorting: ASC},
    41  	ColumnOrdering{Name: "party_id", Sorting: ASC},
    42  	ColumnOrdering{Name: "ordinality", Sorting: ASC},
    43  }
    44  
    45  type LiquidityProvision struct {
    46  	*ConnectionSource
    47  	batcher  MapBatcher[entities.LiquidityProvisionKey, entities.LiquidityProvision]
    48  	observer utils.Observer[entities.LiquidityProvision]
    49  }
    50  
    51  type LiquidityProviderFeeShare struct {
    52  	Ordinality            int64
    53  	MarketID              entities.MarketID
    54  	PartyID               string
    55  	AverageLiquidityScore string `db:"average_score"`
    56  	EquityLikeShare       string
    57  	AverageEntryValuation string
    58  	VirtualStake          string
    59  }
    60  
    61  type LiquidityProviderSLA struct {
    62  	Ordinality                       int64
    63  	MarketID                         entities.MarketID
    64  	PartyID                          string
    65  	CurrentEpochFractionOfTimeOnBook string
    66  	LastEpochFractionOfTimeOnBook    string
    67  	LastEpochFeePenalty              string
    68  	LastEpochBondPenalty             string
    69  	HysteresisPeriodFeePenalties     []string
    70  	RequiredLiquidity                string
    71  	NotionalVolumeBuys               string
    72  	NotionalVolumeSells              string
    73  }
    74  
    75  const (
    76  	sqlOracleLiquidityProvisionColumns = `id, party_id, created_at, updated_at, market_id,
    77  		commitment_amount, fee, sells, buys, version, status, reference, tx_hash, vega_time`
    78  )
    79  
    80  func NewLiquidityProvision(connectionSource *ConnectionSource, log *logging.Logger) *LiquidityProvision {
    81  	return &LiquidityProvision{
    82  		ConnectionSource: connectionSource,
    83  		batcher: NewMapBatcher[entities.LiquidityProvisionKey, entities.LiquidityProvision](
    84  			"liquidity_provisions", entities.LiquidityProvisionColumns),
    85  		observer: utils.NewObserver[entities.LiquidityProvision]("liquidity_provisions", log, 10, 10),
    86  	}
    87  }
    88  
    89  func (lp *LiquidityProvision) Flush(ctx context.Context) error {
    90  	defer metrics.StartSQLQuery("LiquidityProvision", "Flush")()
    91  	flushed, err := lp.batcher.Flush(ctx, lp.ConnectionSource)
    92  	if err != nil {
    93  		return err
    94  	}
    95  
    96  	lp.observer.Notify(flushed)
    97  	return nil
    98  }
    99  
   100  func (lp *LiquidityProvision) ObserveLiquidityProvisions(ctx context.Context, retries int,
   101  	market *string, party *string,
   102  ) (<-chan []entities.LiquidityProvision, uint64) {
   103  	ch, ref := lp.observer.Observe(
   104  		ctx,
   105  		retries,
   106  		func(lp entities.LiquidityProvision) bool {
   107  			marketOk := market == nil || lp.MarketID.String() == *market
   108  			partyOk := party == nil || lp.PartyID.String() == *party
   109  			return marketOk && partyOk
   110  		})
   111  	return ch, ref
   112  }
   113  
   114  func (lp *LiquidityProvision) Upsert(ctx context.Context, liquidityProvision entities.LiquidityProvision) error {
   115  	lp.batcher.Add(liquidityProvision)
   116  	return nil
   117  }
   118  
   119  func (lp *LiquidityProvision) Get(ctx context.Context, partyID entities.PartyID, marketID entities.MarketID,
   120  	reference string,
   121  	live bool,
   122  	pagination entities.Pagination,
   123  ) ([]entities.CurrentAndPreviousLiquidityProvisions, entities.PageInfo, error) {
   124  	if len(partyID) == 0 && len(marketID) == 0 {
   125  		return nil, entities.PageInfo{}, errors.New("market or party filters are required")
   126  	}
   127  
   128  	switch p := pagination.(type) {
   129  	case entities.CursorPagination:
   130  		return lp.getWithCursorPagination(ctx, partyID, marketID, reference, live, p)
   131  	default:
   132  		panic("unsupported pagination")
   133  	}
   134  }
   135  
   136  func (lp *LiquidityProvision) GetByTxHash(ctx context.Context, txHash entities.TxHash) ([]entities.LiquidityProvision, error) {
   137  	defer metrics.StartSQLQuery("LiquidityProvision", "GetByTxHash")()
   138  
   139  	var liquidityProvisions []entities.LiquidityProvision
   140  	query := fmt.Sprintf(`SELECT %s FROM liquidity_provisions WHERE tx_hash = $1`, sqlOracleLiquidityProvisionColumns)
   141  
   142  	err := pgxscan.Select(ctx, lp.ConnectionSource, &liquidityProvisions, query, txHash)
   143  	if err != nil {
   144  		return nil, err
   145  	}
   146  
   147  	return liquidityProvisions, nil
   148  }
   149  
   150  func (lp *LiquidityProvision) ListProviders(ctx context.Context, partyID *entities.PartyID,
   151  	marketID *entities.MarketID, pagination entities.CursorPagination) (
   152  	[]entities.LiquidityProvider, entities.PageInfo, error,
   153  ) {
   154  	var pageInfo entities.PageInfo
   155  	var feeShares []LiquidityProviderFeeShare
   156  	var slas []LiquidityProviderSLA
   157  	var err error
   158  
   159  	if partyID == nil && marketID == nil {
   160  		return nil, pageInfo, errors.New("market, party or both filters are required")
   161  	}
   162  
   163  	// query providers fee shares
   164  	feeQuery, feeArgs := buildLiquidityProviderFeeShareQuery(partyID, marketID)
   165  	feeQuery, feeArgs, err = PaginateQuery[entities.LiquidityProviderCursor](feeQuery, feeArgs, providerOrdering, pagination)
   166  	if err != nil {
   167  		return nil, pageInfo, err
   168  	}
   169  
   170  	// query providers sla
   171  	slaQuery, slaArgs := buildLiquidityProviderSLA(partyID, marketID)
   172  	slaQuery, slaArgs, err = PaginateQuery[entities.LiquidityProviderCursor](slaQuery, slaArgs, providerOrdering, pagination)
   173  	if err != nil {
   174  		return nil, pageInfo, err
   175  	}
   176  
   177  	batch := &pgx.Batch{}
   178  
   179  	batch.Queue(feeQuery, feeArgs...)
   180  	batch.Queue(slaQuery, slaArgs...)
   181  
   182  	results := lp.SendBatch(ctx, batch)
   183  	defer results.Close()
   184  
   185  	feeRows, err := results.Query()
   186  	if err != nil {
   187  		return nil, pageInfo, err
   188  	}
   189  
   190  	if err := pgxscan.ScanAll(&feeShares, feeRows); err != nil {
   191  		return nil, pageInfo, fmt.Errorf("querying fee shares: %w", err)
   192  	}
   193  
   194  	slaRows, err := results.Query()
   195  	if err != nil {
   196  		return nil, pageInfo, err
   197  	}
   198  
   199  	if err := pgxscan.ScanAll(&slas, slaRows); err != nil {
   200  		return nil, pageInfo, fmt.Errorf("querying SLAs: %w", err)
   201  	}
   202  
   203  	slaPerParty := map[string]LiquidityProviderSLA{}
   204  	for _, sla := range slas {
   205  		slaPerParty[sla.PartyID] = sla
   206  	}
   207  
   208  	providers := []entities.LiquidityProvider{}
   209  	for _, feeShare := range feeShares {
   210  		provider := entities.LiquidityProvider{
   211  			Ordinality: feeShare.Ordinality,
   212  			PartyID:    entities.PartyID(feeShare.PartyID),
   213  			MarketID:   feeShare.MarketID,
   214  			FeeShare: &vega.LiquidityProviderFeeShare{
   215  				Party:                 feeShare.PartyID,
   216  				EquityLikeShare:       feeShare.EquityLikeShare,
   217  				AverageEntryValuation: feeShare.AverageEntryValuation,
   218  				AverageScore:          feeShare.AverageLiquidityScore,
   219  				VirtualStake:          feeShare.VirtualStake,
   220  			},
   221  		}
   222  
   223  		if sla, ok := slaPerParty[feeShare.PartyID]; ok {
   224  			provider.SLA = &vega.LiquidityProviderSLA{
   225  				Party:                            sla.PartyID,
   226  				CurrentEpochFractionOfTimeOnBook: sla.CurrentEpochFractionOfTimeOnBook,
   227  				LastEpochFractionOfTimeOnBook:    sla.LastEpochFractionOfTimeOnBook,
   228  				LastEpochFeePenalty:              sla.LastEpochFeePenalty,
   229  				LastEpochBondPenalty:             sla.LastEpochBondPenalty,
   230  				HysteresisPeriodFeePenalties:     sla.HysteresisPeriodFeePenalties,
   231  				RequiredLiquidity:                sla.RequiredLiquidity,
   232  				NotionalVolumeBuys:               sla.NotionalVolumeBuys,
   233  				NotionalVolumeSells:              sla.NotionalVolumeSells,
   234  			}
   235  		}
   236  
   237  		providers = append(providers, provider)
   238  	}
   239  
   240  	providers, pageInfo = entities.PageEntities[*v2.LiquidityProviderEdge](providers, pagination)
   241  
   242  	return providers, pageInfo, nil
   243  }
   244  
   245  func buildLiquidityProviderFeeShareQuery(partyID *entities.PartyID, marketID *entities.MarketID) (string, []interface{}) {
   246  	args := []interface{}{}
   247  
   248  	// The lp data is available in the current market data table
   249  	subQuery := `
   250  select
   251      ordinality,
   252  	cmd.market,
   253  	coalesce(lpfs.fee_share ->> 'party', '')                   as party,
   254  	coalesce(lpfs.fee_share ->> 'average_score', '')           as average_score,
   255  	coalesce(lpfs.fee_share ->> 'equity_like_share', '')       as equity_like_share,
   256  	coalesce(lpfs.fee_share ->> 'average_entry_valuation', '') as average_entry_valuation,
   257  	coalesce(lpfs.fee_share ->> 'virtual_stake', '') 		   as virtual_stake
   258  from current_market_data cmd,
   259  jsonb_array_elements(liquidity_provider_fee_shares) with ordinality lpfs(fee_share, ordinality)
   260  where liquidity_provider_fee_shares != 'null' and liquidity_provider_fee_shares is not null
   261  `
   262  
   263  	if partyID != nil {
   264  		subQuery = fmt.Sprintf("%s and decode(lpfs.fee_share ->>'party', 'hex') = %s", subQuery, nextBindVar(&args, partyID))
   265  	}
   266  
   267  	// if a specific market is requested, then filter by that market too
   268  	if marketID != nil {
   269  		subQuery = fmt.Sprintf("%s and cmd.market = %s", subQuery, nextBindVar(&args, *marketID))
   270  	}
   271  
   272  	// we join with the live liquidity providers table to make sure we are only returning data
   273  	// for liquidity providers that are currently active
   274  	query := fmt.Sprintf(`WITH liquidity_provider_fee_share(ordinality, market_id, party_id, average_score, equity_like_share, average_entry_valuation, virtual_stake) as (%s)
   275          SELECT fs.ordinality, fs.market_id, fs.party_id, fs.average_score, fs.equity_like_share, fs.average_entry_valuation, fs.virtual_stake
   276  	    FROM liquidity_provider_fee_share fs
   277          JOIN live_liquidity_provisions lps ON encode(lps.party_id, 'hex') = fs.party_id
   278          	AND lps.market_id = fs.market_id`, subQuery)
   279  
   280  	return query, args
   281  }
   282  
   283  func buildLiquidityProviderSLA(partyID *entities.PartyID, marketID *entities.MarketID) (string, []interface{}) {
   284  	args := []interface{}{}
   285  
   286  	// The lp data is available in the current market data table
   287  	subQuery := `
   288  select
   289      ordinality,
   290  	cmd.market,
   291  	lpsla.sla ->> 'party' as party,
   292  	coalesce(lpsla.sla ->> 'current_epoch_fraction_of_time_on_book', '') as current_epoch_fraction_of_time_on_book,
   293  	coalesce(lpsla.sla ->> 'last_epoch_fraction_of_time_on_book', '') 	 as last_epoch_fraction_of_time_on_book,
   294  	coalesce(lpsla.sla ->> 'last_epoch_fee_penalty', '')       			 as last_epoch_fee_penalty,
   295  	coalesce(lpsla.sla ->> 'last_epoch_bond_penalty', '') 				 as last_epoch_bond_penalty,
   296  	coalesce(lpsla.sla ->> 'required_liquidity', '') 					 as required_liquidity,
   297  	coalesce(lpsla.sla ->> 'notional_volume_buys', '') 					 as notional_volume_buys,
   298  	coalesce(lpsla.sla ->> 'notional_volume_sells', '') 				 as notional_volume_sells,
   299  	lpsla.sla -> 'hysteresis_period_fee_penalties' 		                 as hysteresis_period_fee_penalties
   300  from current_market_data cmd,
   301  jsonb_array_elements(liquidity_provider_sla) with ordinality lpsla(sla, ordinality)
   302  where liquidity_provider_sla != 'null' and liquidity_provider_sla is not null
   303  `
   304  
   305  	if partyID != nil {
   306  		subQuery = fmt.Sprintf("%s and decode(lpsla.sla ->>'party', 'hex') = %s", subQuery, nextBindVar(&args, partyID))
   307  	}
   308  
   309  	// if a specific market is requested, then filter by that market too
   310  	if marketID != nil {
   311  		subQuery = fmt.Sprintf("%s and cmd.market = %s", subQuery, nextBindVar(&args, *marketID))
   312  	}
   313  
   314  	// we join with the live liquidity providers table to make sure we are only returning data
   315  	// for liquidity providers that are currently active
   316  	query := fmt.Sprintf(`WITH liquidity_provider_sla(ordinality, market_id, party_id, current_epoch_fraction_of_time_on_book, last_epoch_fraction_of_time_on_book, last_epoch_fee_penalty, last_epoch_bond_penalty, required_liquidity, notional_volume_buys, notional_volume_sells, hysteresis_period_fee_penalties) as (%s)
   317          SELECT fs.ordinality, fs.market_id, fs.party_id, fs.current_epoch_fraction_of_time_on_book, fs.last_epoch_fraction_of_time_on_book, fs.last_epoch_fee_penalty, fs.last_epoch_bond_penalty, fs.required_liquidity, fs.notional_volume_buys, fs.notional_volume_sells, fs.hysteresis_period_fee_penalties
   318  	    FROM liquidity_provider_sla fs
   319          JOIN live_liquidity_provisions lps ON encode(lps.party_id, 'hex') = fs.party_id
   320          	AND lps.market_id = fs.market_id`, subQuery)
   321  	return query, args
   322  }
   323  
   324  func (lp *LiquidityProvision) getWithCursorPagination(ctx context.Context, partyID entities.PartyID, marketID entities.MarketID,
   325  	reference string, live bool, pagination entities.CursorPagination,
   326  ) ([]entities.CurrentAndPreviousLiquidityProvisions, entities.PageInfo, error) {
   327  	query, bindVars := lp.buildLiquidityProvisionsSelect(partyID, marketID, reference, live)
   328  
   329  	var err error
   330  	var pageInfo entities.PageInfo
   331  	query, bindVars, err = PaginateQuery[entities.LiquidityProvisionCursor](query, bindVars, lpOrdering, pagination)
   332  	if err != nil {
   333  		return nil, pageInfo, err
   334  	}
   335  
   336  	var liquidityProvisions []entities.CurrentAndPreviousLiquidityProvisions
   337  
   338  	if err = pgxscan.Select(ctx, lp.ConnectionSource, &liquidityProvisions, query, bindVars...); err != nil {
   339  		return nil, entities.PageInfo{}, err
   340  	}
   341  
   342  	pagedLiquidityProvisions, pageInfo := entities.PageEntities[*v2.LiquidityProvisionWithPendingEdge](liquidityProvisions, pagination)
   343  	return pagedLiquidityProvisions, pageInfo, nil
   344  }
   345  
   346  func (lp *LiquidityProvision) buildLiquidityProvisionsSelect(partyID entities.PartyID, marketID entities.MarketID,
   347  	reference string, live bool,
   348  ) (string, []interface{}) {
   349  	var bindVars []interface{}
   350  	sourceTable := "liquidity_provisions"
   351  	if live {
   352  		sourceTable = "live_liquidity_provisions"
   353  	}
   354  
   355  	selectSQL := fmt.Sprintf(`with last_active as (
   356  		select distinct on (id, version) *
   357  		FROM liquidity_provisions
   358  		WHERE status = 'STATUS_ACTIVE'
   359  		order by id, version, vega_time desc
   360  	), lps as (
   361  	select llp.id, llp.party_id, llp.created_at, llp.updated_at, llp.market_id,
   362  		   llp.commitment_amount, llp.fee, llp.sells, llp.buys, llp.version,
   363  		   llp.status, llp.reference, llp.tx_hash, llp.vega_time,
   364  		   lp.id previous_id, lp.party_id previous_party_id, lp.created_at previous_created_at,
   365  		   lp.updated_at previous_updated_at, lp.market_id previous_market_id,
   366  		   lp.commitment_amount previous_commitment_amount, lp.fee previous_fee,
   367  		   lp.sells previous_sells, lp.buys previous_buys, lp.version previous_version,
   368  		   lp.status previous_status, lp.reference previous_reference, lp.tx_hash previous_tx_hash,
   369  		   lp.vega_time previous_vega_time
   370  	from %s llp
   371  	left join last_active lp on llp.id = lp.id and llp.version - 1 = lp.version
   372  )
   373  	select *
   374  	from lps
   375  	`, sourceTable)
   376  
   377  	where := ""
   378  
   379  	if partyID != "" {
   380  		where = fmt.Sprintf("%s party_id=%s", where, nextBindVar(&bindVars, partyID))
   381  	}
   382  
   383  	if marketID != "" {
   384  		if len(where) > 0 {
   385  			where = where + " and "
   386  		}
   387  		where = fmt.Sprintf("%s market_id=%s", where, nextBindVar(&bindVars, marketID))
   388  	}
   389  
   390  	if reference != "" {
   391  		if len(where) > 0 {
   392  			where = where + " and "
   393  		}
   394  		where = fmt.Sprintf("%s reference=%s", where, nextBindVar(&bindVars, reference))
   395  	}
   396  
   397  	if len(where) > 0 {
   398  		where = fmt.Sprintf("where %s", where)
   399  	}
   400  
   401  	query := fmt.Sprintf(`%s %s`, selectSQL, where)
   402  	return query, bindVars
   403  }