forked from celo-org/celo-oracle
-
Notifications
You must be signed in to change notification settings - Fork 0
/
exchange_price_source.ts
168 lines (148 loc) · 5.66 KB
/
exchange_price_source.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import { ExchangeAdapter, Ticker } from './exchange_adapters/base'
import BigNumber from 'bignumber.js'
import { PriceSource, WeightedPrice } from './price_source'
import { Exchange, OracleCurrencyPair } from './utils'
import { MetricCollector } from './metric_collector'
import { individualTickerChecks } from './aggregator_functions'
export interface OrientedExchangePair {
exchange: Exchange
symbol: OracleCurrencyPair
toInvert: boolean
}
export interface ExchangePriceSourceConfig {
pairs: OrientedExchangePair[]
}
export type OrientedAdapter = {
adapter: ExchangeAdapter
toInvert: boolean
}
export type PairData = {
bid: BigNumber
ask: BigNumber
baseVolume: BigNumber
quoteVolume: BigNumber
}
function invertPair(pair: PairData): PairData {
return {
bid: pair.ask.exponentiatedBy(-1),
ask: pair.bid.exponentiatedBy(-1),
baseVolume: pair.quoteVolume,
quoteVolume: pair.baseVolume,
}
}
function tickerToPairData(ticker: Ticker): PairData {
return {
bid: ticker.bid,
ask: ticker.ask,
baseVolume: ticker.baseVolume,
// Using lastPrice to convert from baseVolume to quoteVolume, whereas
// ideally one would use the baseVolume execution's VWAP.
quoteVolume: ticker.baseVolume.multipliedBy(ticker.lastPrice),
}
}
async function fetchPairData(
orientedAdapter: OrientedAdapter,
maxPercentageBidAskSpread: BigNumber,
metricCollector?: MetricCollector
): Promise<PairData> {
const { adapter, toInvert } = orientedAdapter
const ticker = await adapter.fetchTicker()
// Validate fetched ticker -- will throw if invalid.
individualTickerChecks(ticker, maxPercentageBidAskSpread)
if (metricCollector) {
metricCollector.ticker(ticker)
}
const pair = tickerToPairData(ticker)
return toInvert ? invertPair(pair) : pair
}
function cumulativeProduct(array: BigNumber[]): BigNumber[] {
if (array.length === 0) {
return []
}
const prod: BigNumber[] = [array[0]]
for (const x of array.slice(1)) {
prod.push(prod[prod.length - 1].multipliedBy(x))
}
return prod
}
/**
* Given a sequence of pairs data, calculate the data for the pair implied by
* such sequence.
*
* Each instance of PairData contains a buy and a sell quote (bid and ask,
* respectively), as well as the base and quote notional traded over a period
* of time. The implied buy quote is the product of all buy quotes on the input
* pairs data and, similarly, the implied sell quote is the product of all sell
* quotes. The implied base notional is the smallest base notional of the input
* pairs, once the notionals are converted to the base currency. Similarly the
* implied quote notional is the smallest quote notional of the input pairs,
* once they are converted to the quote currency.
*/
export function impliedPair(pairs: PairData[]): PairData {
const bids = pairs.map((p) => p.bid)
const asks = pairs.map((p) => p.ask)
// Uses VWAP rates (derived from quote/base volumes) to convert the base and
// quote volumes to the implied pair base and quote currencies, respectively.
const invertedRates = pairs.map((p) => p.baseVolume.dividedBy(p.quoteVolume))
const directRates = pairs.map((p) => p.quoteVolume.dividedBy(p.baseVolume))
// The last pair rate is not needed to bring base volumes to the same
// currency as the implied pair base.
const baseConversionRates = [new BigNumber(1), ...invertedRates.slice(0, -1)]
const baseConversionFactors = cumulativeProduct(baseConversionRates)
const baseVolumes = pairs.map((p, i) => p.baseVolume.multipliedBy(baseConversionFactors[i]))
// Similarly, the first pair rate is not needed to bring quote volumes to the
// same currency as the implied pair quote.
const quoteConversionRates = [...directRates.slice(1), new BigNumber(1)]
const quoteConversionFactors = cumulativeProduct(quoteConversionRates.reverse()).reverse()
const quoteVolumes = pairs.map((p, i) => p.quoteVolume.multipliedBy(quoteConversionFactors[i]))
const bid = bids.reduce((a, b) => a.multipliedBy(b), new BigNumber(1))
const ask = asks.reduce((a, b) => a.multipliedBy(b), new BigNumber(1))
const baseVolume = BigNumber.min(...baseVolumes)
const quoteVolume = BigNumber.min(...quoteVolumes)
return { bid, ask, baseVolume, quoteVolume }
}
/**
* ExchangePriceSource implements a PriceSource capable of fetching Tickers
* from different exchanges and pairs and combining them into a single
* WeightedPrice.
*/
export class ExchangePriceSource implements PriceSource {
private adapters: OrientedAdapter[]
private maxPercentageBidAskSpread: BigNumber
private metricCollector?: MetricCollector
constructor(
adapters: OrientedAdapter[],
maxPercentageBidAskSpread: BigNumber,
metricCollector?: MetricCollector
) {
this.adapters = adapters
this.maxPercentageBidAskSpread = maxPercentageBidAskSpread
this.metricCollector = metricCollector
}
/**
* Returns a unique string representation of a source's adapters.
*
* Example: an ExchangePriceSource using two Binance adapters for the CELOBTC
* and BTCEUR pairs would have "BINANCE:CELOBTC:false|BINANCE:BTCEUR:false"
* as a name.
*/
name(): string {
return this.adapters
.map(({ adapter, toInvert }) => {
return `${adapter.exchangeName}:${adapter.pairSymbol}:${toInvert}`
})
.join('|')
}
async fetchWeightedPrice(): Promise<WeightedPrice> {
const fetcher = (adapter: OrientedAdapter) => {
return fetchPairData(adapter, this.maxPercentageBidAskSpread, this.metricCollector)
}
const pairs = await Promise.all(this.adapters.map(fetcher))
const pair = impliedPair(pairs)
const mid = pair.bid.plus(pair.ask).dividedBy(2)
return {
price: mid,
weight: pair.baseVolume,
}
}
}