oracle

package
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 16, 2025 License: Apache-2.0 Imports: 11 Imported by: 0

README

Index Price Aggregator

Overview

The price aggregator defined in this package is used to convert a set of price feeds into a common set of prices. The prices used in aggregation contain:

  • the most recent prices fetched by each provider.
  • index prices which are the previous aggregated prices.
  • scaled prices which are effectively the index price but with a common decimal precision (as determined by the market map).

Each of the prices provided are safe to use as they are already filtered by the oracle - specifically each price is within the MaxPriceAge window.

Configuration

MarketMap

The MarketMap contains:

  • Tickers - the list of tickers that we care about.
  • Providers - each ticker maps to a list of providers that fetch prices for that ticker.

Say that the oracle is configured to fetch prices for the following feeds:

  • BITCOIN/USD (8 decimal precision)
  • ETHEREUM/USD (18 decimal precision)
  • USDT/USD (6 decimal precision)

Using the example above, a sample market map might look like the following (assume we have correctly configured the providers):

	marketmap = mmtypes.MarketMap{
		Markets: map[string]mmtypes.Market{
			BTC_USD.String(): {
                Ticker: mmtypes.Ticker{
                    CurrencyPair:     constants.BITCOIN_USD,
                    Decimals:         8,
                    MinProviderCount: 3,
                },
				ProviderConfigs: []mmtypes.ProviderConfig{
					{
						Name:           coinbase.Name,
						OffChainTicker: "BTC-USD",
					},
					{
						Name:           coinbase.Name,
						OffChainTicker: "BTC-USDT",
						NormalizeByPair: &pkgtypes.CurrencyPair{
							Base:  "USDT",
							Quote: "USD",
						},
					},
					{
						Name:           binance.Name,
						OffChainTicker: "BTCUSDT",
						NormalizeByPair: &pkgtypes.CurrencyPair{
							Base:  "USDT",
							Quote: "USD",
						},
					},
				},
			},
			ETH_USD.String(): {
                Ticker: mmtypes.Ticker{
                    CurrencyPair:     constants.ETHEREUM_USD,
                    Decimals:         11,
                    MinProviderCount: 3,
                },
				ProviderConfigs: []mmtypes.ProviderConfig{
					{
						Name:           coinbase.Name,
						OffChainTicker: "ETH-USD",
					},
					{
						Name:           coinbase.Name,
						OffChainTicker: "ETH-USDT",
						NormalizeByPair: &pkgtypes.CurrencyPair{
							Base:  "USDT",
							Quote: "USD",
						},
					},
					{
						Name:           binance.Name,
						OffChainTicker: "ETHUSDT",
						NormalizeByPair: &pkgtypes.CurrencyPair{
							Base:  "USDT",
							Quote: "USD",
						},
					},
				},
			},
			USDT_USD.String(): {
                Ticker: mmtypes.Ticker{
                    CurrencyPair:     constants.USDT_USD,
                    Decimals:         6,
                    MinProviderCount: 2,
                },
				ProviderConfigs: []mmtypes.ProviderConfig{
					{
						Name:           coinbase.Name,
						OffChainTicker: "USDT-USD",
					},
					{
						Name:           coinbase.Name,
						OffChainTicker: "USDC-USDT",
						Invert:         true,
					},
					{
						Name:           binance.Name,
						OffChainTicker: "USDTUSD",
					},
					{
						Name:           kucoin.Name,
						OffChainTicker: "BTC-USDT",
						Invert:         true,
						NormalizeByPair: &pkgtypes.CurrencyPair{
							Base:  "BTC",
							Quote: "USD",
						},
					},
				},
			},
		},
	}

From the above market map, we can see that the following paths are used to calculate the price of each ticker:

  • BTC/USD
    • COINBASE BTC/USD
    • COINBASE BTC/USDT * INDEX USDT/USD
    • BINANCE BTC/USDT * INDEX USDT/USD
  • ETH/USD
    • COINBASE ETH/USD
    • COINBASE ETH/USDT * INDEX USDT/USD
    • BINANCE ETH/USDT * INDEX USDT/USD
  • USDT/USD
    • COINBASE USDT/USD
    • COINBASE USDC/USDT ^ -1
    • BINANCE USDT/USD
    • KUCOIN BTC/USDT ^-1 * INDEX BTC/USD

A few important considerations:

  1. Each ticker (BTC/USD, ETH/USD, USDT/USD) can have a configured MinimumProviderCount which is the minimum number of providers that are required to calculate the price of the ticker.
  2. Each path that is not a direct conversion (e.g. BTC/USD) must configure the second operation to utilize the index price i.e. of a primary ticker i.e. market.

Aggregation

Precision

Precision is retained as much as possible in the aggregator. Each price included by each provider is converted to the maximum amount of precision that is possible for the price (and what big.Float is capable of handling). The index prices are always big.Floats with minimal precision lost between conversions, scaling, and aggregation.

Example Aggregation

Given the market map above, let's assume that we have the following prices fetched by the providers:

  • COINBASE BTC/USD: 71_000
  • COINBASE BTC/USDT: 70_000
  • BINANCE BTC/USDT: 70_500
  • INDEX USDT/USD: 1.05

The aggregator will calculate the price of BTC/USD using the following paths:

  1. COINBASE BTC/USD
  2. COINBASE BTC/USDT * INDEX USDT/USD
  3. BINANCE BTC/USDT * INDEX USDT/USD

This gives a set of prices:

  • COINBASE BTC/USD: 71_000
  • COINBASE BTC/USDT * INDEX USDT/USD: 73_500
  • BINANCE BTC/USDT * INDEX USDT/USD: 73_575

The final price of BTC/USD is the median of the above prices, which is 73_500. In the case of an even number of prices, the median is the average of the two middle numbers.

Other Considerations

Cycle Detection

It is possible to have cycles in the market map. If the price of a ticker is dependent on a different ticker, which in turn is dependent on the first ticker, then we have a cycle. This can affect price liveness and can cause the oracle to be stuck in a loop. To prevent this, we recommend that markets that are dependent on each other have a sufficient amount of providers, have considerable MinProviderCount, and have sufficient amounts of direct conversions (i.e. not dependent on other tickers).

If a cycle does exist, it will likely be resolved after a few iterations of the oracle.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type IndexPriceAggregator

type IndexPriceAggregator struct {
	// contains filtered or unexported fields
}

IndexPriceAggregator is an aggregator that calculates the median price for each ticker, resolved from a predefined set of conversion markets. A conversion market is a set of markets that can be used to convert the prices of a set of tickers to a common ticker. These are defined in the market map configuration.

func NewIndexPriceAggregator

func NewIndexPriceAggregator(
	logger *zap.Logger,
	cfg mmtypes.MarketMap,
	metrics oraclemetrics.Metrics,
) (*IndexPriceAggregator, error)

NewIndexPriceAggregator returns a new Index Price Aggregator.

func (*IndexPriceAggregator) AggregatePrices

func (m *IndexPriceAggregator) AggregatePrices()

AggregatePrices implements the aggregate function for the median price calculation. Specifically, this aggregation function aggregates the prices seen by each provider by first converting each price to a common ticker and then calculating the median of the converted prices. Prices are converted either

  1. Directly from the base ticker to the target ticker. i.e. I have BTC/USD and I want BTC/USD.
  2. Using the index price of an asset. i.e. I have BTC/USDT and I want BTC/USD. I can convert BTC/USDT to BTC/USD using the index price of USDT/USD.

The index price cache contains the previously calculated median prices.

func (*IndexPriceAggregator) CalculateAdjustedPrice

func (m *IndexPriceAggregator) CalculateAdjustedPrice(
	cfg mmtypes.ProviderConfig,
) (*big.Float, error)

CalculateAdjustedPrice calculates an adjusted price for a given set of operations (if applicable). In particular, this assumes that every operation is either:

  1. A direct conversion from the base ticker to the target ticker i.e. we want BTC/USD and we have BTC/USD from a provider (e.g. Coinbase).
  2. We need to convert the price of a given asset against the index price of an asset.

In the first case, we can simply return the price of the provider. In the second case, we need to adjust the price by the index price of the asset. If the index price is not available, we return an error.

func (*IndexPriceAggregator) CalculateConvertedPrices

func (m *IndexPriceAggregator) CalculateConvertedPrices(
	market mmtypes.Market,
) []*big.Float

CalculateConvertedPrices calculates the converted prices for a given set of paths and target ticker. The prices utilized are the prices most recently seen by the providers. Each price is within a MaxPriceAge window so is safe to use.

func (*IndexPriceAggregator) GetIndexPrice

func (m *IndexPriceAggregator) GetIndexPrice(
	cp pkgtypes.CurrencyPair,
) (*big.Float, error)

GetIndexPrice returns the relevant index price. Note that the aggregator's index price cache stores prices in the form of ticker -> price.

func (*IndexPriceAggregator) GetIndexPrices

func (m *IndexPriceAggregator) GetIndexPrices() types.Prices

GetIndexPrices returns the index prices the aggregator has.

func (*IndexPriceAggregator) GetMarketMap

func (m *IndexPriceAggregator) GetMarketMap() *mmtypes.MarketMap

GetMarketMap returns the market map for the oracle.

func (*IndexPriceAggregator) GetPrices

func (m *IndexPriceAggregator) GetPrices() types.Prices

GetPrices returns the aggregated data the aggregator has. Specifically, the prices returned are the scaled prices - where each price is scaled by the respective ticker's decimals.

func (*IndexPriceAggregator) GetProviderPrice

func (m *IndexPriceAggregator) GetProviderPrice(
	cfg mmtypes.ProviderConfig,
) (*big.Float, error)

GetProviderPrice returns the relevant provider price. Note that the aggregator's provider data cache stores prices in the form of providerName -> offChainTicker -> price.

func (*IndexPriceAggregator) Reset

func (m *IndexPriceAggregator) Reset()

Reset resets the data aggregator for all providers.

func (*IndexPriceAggregator) SetIndexPrices

func (m *IndexPriceAggregator) SetIndexPrices(
	prices types.Prices,
)

SetIndexPrice sets the index price for the given currency pair.

func (*IndexPriceAggregator) SetProviderPrices

func (m *IndexPriceAggregator) SetProviderPrices(provider string, data types.Prices)

SetProviderPrices updates the data aggregator with the given provider and data.

func (*IndexPriceAggregator) UpdateMarketMap

func (m *IndexPriceAggregator) UpdateMarketMap(marketMap mmtypes.MarketMap)

UpdateMarketMap updates the market map for the oracle.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL