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 }