code.vegaprotocol.io/vega@v0.79.0/datanode/sqlstore/candles.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  	"encoding/hex"
    21  	"fmt"
    22  	"strings"
    23  	"time"
    24  
    25  	"code.vegaprotocol.io/vega/datanode/candlesv2"
    26  	"code.vegaprotocol.io/vega/datanode/entities"
    27  	"code.vegaprotocol.io/vega/datanode/metrics"
    28  	"code.vegaprotocol.io/vega/libs/crypto"
    29  	v2 "code.vegaprotocol.io/vega/protos/data-node/api/v2"
    30  
    31  	"github.com/georgysavva/scany/pgxscan"
    32  	"github.com/shopspring/decimal"
    33  )
    34  
    35  const (
    36  	sourceDataTableName    = "trades"
    37  	candlesViewNamePrePend = sourceDataTableName + "_candle_"
    38  )
    39  
    40  var candleOrdering = TableOrdering{
    41  	ColumnOrdering{Name: "period_start", Sorting: ASC},
    42  }
    43  
    44  type Candles struct {
    45  	*ConnectionSource
    46  
    47  	config candlesv2.CandleStoreConfig
    48  	ctx    context.Context
    49  }
    50  
    51  type ErrInvalidCandleID struct {
    52  	err error
    53  }
    54  
    55  func (e ErrInvalidCandleID) Error() string {
    56  	return e.err.Error()
    57  }
    58  
    59  func newErrInvalidCandleID(err error) ErrInvalidCandleID {
    60  	return ErrInvalidCandleID{err: err}
    61  }
    62  
    63  func NewCandles(ctx context.Context, connectionSource *ConnectionSource, config candlesv2.CandleStoreConfig) *Candles {
    64  	return &Candles{
    65  		ConnectionSource: connectionSource,
    66  		ctx:              ctx,
    67  		config:           config,
    68  	}
    69  }
    70  
    71  func (cs *Candles) getCandlesSubquery(ctx context.Context, descriptor candleDescriptor, from, to *time.Time, args []interface{}) (string, []interface{}, error) {
    72  	// We have to use the time_bucket_gapfill function to fill in the gaps in the data that is generated by the continuous aggregations
    73  	// https://docs.timescale.com/api/latest/hyperfunctions/gapfilling/time_bucket_gapfill/
    74  	// Unfortunately this function cannot be used when creating the continuous aggregations as it is not supported, so instead we have to
    75  	// ensure we pass the interval corresponding to the view we are querying.
    76  	// We could use locf function to carry forward the value from the previous candle, but this would be wrong, it would be better to zero the
    77  	// values out and have the user carry the last close value forward for open, high, low and close when there is a gap.
    78  	// We only carry forward the last_update_in_period value to indicate when we last received a trade that would have updated the candle.
    79  	// It doesn't matter what aggregation function we use as long as we're using the view corresponding to the interval, there should
    80  	// only ever be 1 row to aggregate.
    81  
    82  	interval := descriptor.interval
    83  	if interval == "block" {
    84  		interval = "1 minute"
    85  	}
    86  
    87  	groupBy := true
    88  	query := fmt.Sprintf(`SELECT
    89  		time_bucket_gapfill('%s', period_start) as period_start,
    90  		first(open) as open,
    91  		first(close) as close,
    92  		first(high) as high,
    93  		first(low) as low,
    94  		first(volume) as volume,
    95  		first(notional) as notional,
    96  		locf(first(last_update_in_period)) as last_update_in_period
    97  	FROM %s WHERE market_id = $1`,
    98  		interval, descriptor.view)
    99  
   100  	// As a result of having to use the time_bucket_gapfill function we have to provide and start and finish date to the query.
   101  	// The documentation suggests using the where clause for better performance as the query planner can use it to optimise performance
   102  	candlesDateRange := struct {
   103  		StartDate *time.Time
   104  		EndDate   *time.Time
   105  	}{}
   106  
   107  	if from == nil || to == nil {
   108  		datesQuery := fmt.Sprintf("select min(period_start) as start_date, max(period_start) as end_date from %s where market_id = $1", descriptor.view)
   109  		marketID := entities.MarketID(descriptor.market)
   110  		err := pgxscan.Get(ctx, cs.ConnectionSource, &candlesDateRange, datesQuery, marketID)
   111  		if err != nil {
   112  			return "", args, fmt.Errorf("querying candles date range: %w", err)
   113  		}
   114  	}
   115  
   116  	// These can only be nil if there is no candle data so there's no gaps to fill and just return an empty row set
   117  	if candlesDateRange.StartDate == nil && candlesDateRange.EndDate == nil {
   118  		query = fmt.Sprintf("select period_start, open, close, high, low, volume, notional, last_update_in_period from %s where market_id = $1",
   119  			descriptor.view)
   120  		groupBy = false
   121  	}
   122  
   123  	if from != nil {
   124  		query = fmt.Sprintf("%s AND period_start >= %s", query, nextBindVar(&args, from))
   125  	} else if candlesDateRange.StartDate != nil {
   126  		query = fmt.Sprintf("%s AND period_start >= %s", query, nextBindVar(&args, *candlesDateRange.StartDate))
   127  	}
   128  
   129  	if to != nil {
   130  		query = fmt.Sprintf("%s AND period_start < %s", query, nextBindVar(&args, to))
   131  	} else if candlesDateRange.EndDate != nil {
   132  		// as no end time has been specified, we want the end time to be inclusive rather than exclusive
   133  		// otherwise we will always miss the last candle when the user does not specify an end time.
   134  		query = fmt.Sprintf("%s AND period_start <= %s", query, nextBindVar(&args, candlesDateRange.EndDate))
   135  	}
   136  
   137  	if groupBy {
   138  		query = fmt.Sprintf("%s GROUP BY time_bucket_gapfill('%s', period_start)", query, interval)
   139  	}
   140  
   141  	return query, args, nil
   142  }
   143  
   144  // GetCandleDataForTimeSpan gets the candles for a given interval, from and to are optional.
   145  func (cs *Candles) GetCandleDataForTimeSpan(ctx context.Context, candleID string, from *time.Time, to *time.Time,
   146  	p entities.CursorPagination) ([]entities.Candle, entities.PageInfo, error,
   147  ) {
   148  	pageInfo := entities.PageInfo{}
   149  
   150  	descriptor, err := candleDescriptorFromCandleID(candleID)
   151  	if err != nil {
   152  		return nil, pageInfo, newErrInvalidCandleID(fmt.Errorf("getting candle data for time span: %w", err))
   153  	}
   154  
   155  	exists, err := cs.CandleExists(ctx, descriptor.id)
   156  	if err != nil {
   157  		return nil, pageInfo, fmt.Errorf("getting candles for time span:%w", err)
   158  	}
   159  
   160  	if !exists {
   161  		return nil, pageInfo, fmt.Errorf("no candle exists for candle id:%s", candleID)
   162  	}
   163  
   164  	var candles []entities.Candle
   165  
   166  	marketAsBytes, err := hex.DecodeString(descriptor.market)
   167  	if err != nil {
   168  		return nil, pageInfo, fmt.Errorf("invalid market:%w", err)
   169  	}
   170  
   171  	args := []interface{}{marketAsBytes}
   172  
   173  	subQuery, args, err := cs.getCandlesSubquery(ctx, descriptor, from, to, args)
   174  	if err != nil {
   175  		return nil, pageInfo, fmt.Errorf("gap fill query failure: %w", err)
   176  	}
   177  
   178  	// We don't want to add the subquery yet because the where clause in it will confuse the pagination query builder
   179  	query := `select period_start,
   180  		coalesce(open, 0) as open,
   181  		coalesce(close, 0) as close,
   182  		coalesce(high, 0) as high,
   183  		coalesce(low, 0) as low,
   184  		coalesce(volume, 0) as volume,
   185  		coalesce(notional, 0) as notional,
   186  		coalesce(last_update_in_period, period_start) as last_update_in_period
   187  	from gap_filled_candles`
   188  
   189  	query, args, err = PaginateQuery[entities.CandleCursor](query, args, candleOrdering, p)
   190  	if err != nil {
   191  		return nil, pageInfo, err
   192  	}
   193  
   194  	// now that we have the paged query, we can add in the subquery
   195  	query = fmt.Sprintf("with gap_filled_candles as (%s) %s", subQuery, query)
   196  
   197  	defer metrics.StartSQLQuery("Candles", "GetCandleDataForTimeSpan")()
   198  	err = pgxscan.Select(ctx, cs.ConnectionSource, &candles, query, args...)
   199  	if err != nil {
   200  		return nil, pageInfo, fmt.Errorf("querying candles: %w", err)
   201  	}
   202  
   203  	var pagedCandles []entities.Candle
   204  
   205  	pagedCandles, pageInfo = entities.PageEntities[*v2.CandleEdge](candles, p)
   206  
   207  	return pagedCandles, pageInfo, nil
   208  }
   209  
   210  // GetCandlesForMarket returns a map of existing intervals to candle ids for the given market.
   211  func (cs *Candles) GetCandlesForMarket(ctx context.Context, market string) (map[string]string, error) {
   212  	intervalToView, err := cs.getIntervalToView(ctx)
   213  	if err != nil {
   214  		return nil, fmt.Errorf("getting existing candles:%w", err)
   215  	}
   216  
   217  	candles := map[string]string{}
   218  	for interval := range intervalToView {
   219  		candles[interval] = candleDescriptorFromIntervalAndMarket(interval, market).id
   220  	}
   221  	return candles, nil
   222  }
   223  
   224  func (cs *Candles) GetCandleIDForIntervalAndMarket(ctx context.Context, interval string, market string) (bool, string, error) {
   225  	interval, err := cs.normaliseInterval(ctx, interval)
   226  	if err != nil {
   227  		return false, "", fmt.Errorf("invalid interval: %w", err)
   228  	}
   229  
   230  	viewAlreadyExists, existingInterval, err := cs.viewExistsForInterval(ctx, interval)
   231  	if err != nil {
   232  		return false, "", fmt.Errorf("checking for existing view: %w", err)
   233  	}
   234  
   235  	if viewAlreadyExists {
   236  		descriptor := candleDescriptorFromIntervalAndMarket(existingInterval, market)
   237  		return true, descriptor.id, nil
   238  	}
   239  
   240  	return false, "", nil
   241  }
   242  
   243  func (cs *Candles) getIntervalToView(ctx context.Context) (map[string]string, error) {
   244  	query := fmt.Sprintf("SELECT table_name AS view_name FROM INFORMATION_SCHEMA.views WHERE table_name LIKE '%s%%'",
   245  		candlesViewNamePrePend)
   246  	defer metrics.StartSQLQuery("Candles", "GetIntervalToView")()
   247  	rows, err := cs.Query(ctx, query)
   248  	if err != nil {
   249  		return nil, fmt.Errorf("fetching existing views for interval: %w", err)
   250  	}
   251  
   252  	var viewNames []string
   253  	for rows.Next() {
   254  		var viewName string
   255  		err := rows.Scan(&viewName)
   256  		if err != nil {
   257  			return nil, fmt.Errorf("fetching existing views for interval: %w", err)
   258  		}
   259  		viewNames = append(viewNames, viewName)
   260  	}
   261  
   262  	result := map[string]string{}
   263  	for _, viewName := range viewNames {
   264  		interval, err := getIntervalFromViewName(viewName)
   265  		if err != nil {
   266  			return nil, fmt.Errorf("fetching existing views for interval: %w", err)
   267  		}
   268  
   269  		result[interval] = viewName
   270  	}
   271  	return result, nil
   272  }
   273  
   274  func (cs *Candles) CandleExists(ctx context.Context, candleID string) (bool, error) {
   275  	descriptor, err := candleDescriptorFromCandleID(candleID)
   276  	if err != nil {
   277  		return false, fmt.Errorf("candle exists:%w", err)
   278  	}
   279  
   280  	exists, _, err := cs.viewExistsForInterval(ctx, descriptor.interval)
   281  	if err != nil {
   282  		return false, fmt.Errorf("candle exists:%w", err)
   283  	}
   284  
   285  	return exists, nil
   286  }
   287  
   288  func (cs *Candles) viewExistsForInterval(ctx context.Context, interval string) (bool, string, error) {
   289  	intervalToView, err := cs.getIntervalToView(ctx)
   290  	if err != nil {
   291  		return false, "", fmt.Errorf("checking if view exist for interval:%w", err)
   292  	}
   293  
   294  	if _, ok := intervalToView[interval]; ok {
   295  		return true, interval, nil
   296  	}
   297  
   298  	// Also check for existing Intervals that are specified differently but amount to the same thing  (i.e 7 days = 1 week)
   299  	existingIntervals := map[int64]string{}
   300  	for existingInterval := range intervalToView {
   301  		if existingInterval == "block" {
   302  			continue
   303  		}
   304  		seconds, err := cs.getIntervalSeconds(ctx, existingInterval)
   305  		if err != nil {
   306  			return false, "", fmt.Errorf("checking if view exists for interval:%w", err)
   307  		}
   308  		existingIntervals[seconds] = existingInterval
   309  	}
   310  
   311  	seconds, err := cs.getIntervalSeconds(ctx, interval)
   312  	if err != nil {
   313  		return false, "", fmt.Errorf("checking if view exists for interval:%w", err)
   314  	}
   315  
   316  	if existingInterval, ok := existingIntervals[seconds]; ok {
   317  		return true, existingInterval, nil
   318  	}
   319  
   320  	return false, "", nil
   321  }
   322  
   323  func (cs *Candles) normaliseInterval(ctx context.Context, interval string) (string, error) {
   324  	var normalizedInterval string
   325  
   326  	defer metrics.StartSQLQuery("Candles", "normaliseInterval")()
   327  	_, err := cs.Exec(ctx, "SET intervalstyle = 'postgres_verbose' ")
   328  	if err != nil {
   329  		return "", fmt.Errorf("normalising interval, failed to set interval style:%w", err)
   330  	}
   331  
   332  	query := fmt.Sprintf("select cast( INTERVAL '%s' as text)", interval)
   333  	row := cs.QueryRow(ctx, query)
   334  
   335  	err = row.Scan(&normalizedInterval)
   336  	if err != nil {
   337  		return "", fmt.Errorf("normalising interval:%s :%w", interval, err)
   338  	}
   339  
   340  	normalizedInterval = strings.ReplaceAll(normalizedInterval, "@ ", "")
   341  
   342  	return normalizedInterval, nil
   343  }
   344  
   345  func (cs *Candles) getIntervalSeconds(ctx context.Context, interval string) (int64, error) {
   346  	var seconds decimal.Decimal
   347  
   348  	defer metrics.StartSQLQuery("Candles", "getIntervalSeconds")()
   349  	query := fmt.Sprintf("SELECT EXTRACT(epoch FROM INTERVAL '%s')", interval)
   350  	row := cs.QueryRow(ctx, query)
   351  
   352  	err := row.Scan(&seconds)
   353  	if err != nil {
   354  		return 0, err
   355  	}
   356  
   357  	return seconds.IntPart(), nil
   358  }
   359  
   360  func getIntervalFromViewName(viewName string) (string, error) {
   361  	split := strings.Split(viewName, candlesViewNamePrePend)
   362  	if len(split) != 2 {
   363  		return "", fmt.Errorf("view name has unexpected format:%s", viewName)
   364  	}
   365  	return strings.ReplaceAll(split[1], "_", " "), nil
   366  }
   367  
   368  func getViewNameForInterval(interval string) string {
   369  	return candlesViewNamePrePend + strings.ReplaceAll(interval, " ", "_")
   370  }
   371  
   372  type candleDescriptor struct {
   373  	id       string
   374  	view     string
   375  	interval string
   376  	market   string
   377  }
   378  
   379  func candleDescriptorFromCandleID(id string) (candleDescriptor, error) {
   380  	idx := strings.LastIndex(id, "_")
   381  
   382  	if idx == -1 {
   383  		return candleDescriptor{}, fmt.Errorf("invalid candle id:%s", id)
   384  	}
   385  
   386  	market := id[idx+1:]
   387  	view := id[:idx]
   388  
   389  	split := strings.Split(view, candlesViewNamePrePend)
   390  	if len(split) != 2 {
   391  		return candleDescriptor{}, fmt.Errorf("parsing candle id, view name has unexpected format:%s", id)
   392  	}
   393  
   394  	interval, err := getIntervalFromViewName(view)
   395  	if err != nil {
   396  		return candleDescriptor{}, fmt.Errorf("parsing candleDescriptor id, failed to get interval from view name:%w", err)
   397  	}
   398  
   399  	if !crypto.IsValidVegaID(market) {
   400  		return candleDescriptor{}, fmt.Errorf("not a valid market id: %v", market)
   401  	}
   402  
   403  	return candleDescriptor{
   404  		id:       id,
   405  		view:     view,
   406  		interval: interval,
   407  		market:   market,
   408  	}, nil
   409  }
   410  
   411  func candleDescriptorFromIntervalAndMarket(interval string, market string) candleDescriptor {
   412  	view := getViewNameForInterval(interval)
   413  	id := view + "_" + market
   414  
   415  	return candleDescriptor{
   416  		id:       id,
   417  		view:     view,
   418  		interval: interval,
   419  		market:   market,
   420  	}
   421  }