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 }