Merge remote-tracking branch 'octopus/master'
This commit is contained in:
commit
28d35d037c
|
@ -155,10 +155,10 @@ impl<'a> RemoveOracleContext<'a> {
|
||||||
|
|
||||||
struct SubmitContext<'a> {
|
struct SubmitContext<'a> {
|
||||||
clock: Clock,
|
clock: Clock,
|
||||||
aggregator: &'a AccountInfo<'a>,
|
aggregator: &'a AccountInfo<'a>, // write
|
||||||
round_submissions: &'a AccountInfo<'a>,
|
round_submissions: &'a AccountInfo<'a>, // write
|
||||||
answer_submissions: &'a AccountInfo<'a>,
|
answer_submissions: &'a AccountInfo<'a>, // write
|
||||||
oracle: &'a AccountInfo<'a>,
|
oracle: &'a AccountInfo<'a>, // write
|
||||||
oracle_owner: &'a AccountInfo<'a>, // signed
|
oracle_owner: &'a AccountInfo<'a>, // signed
|
||||||
|
|
||||||
// NOTE: 5.84942*10^11 years even if 1 sec per round. don't bother with handling wrapparound.
|
// NOTE: 5.84942*10^11 years even if 1 sec per round. don't bother with handling wrapparound.
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { AggregatedFeed, BitStamp, CoinBase, FTX } from "../src/feeds"
|
||||||
|
|
||||||
|
// print the median of the current prices of three CEXes
|
||||||
|
async function main() {
|
||||||
|
const feeds = [new CoinBase(), new BitStamp(), new FTX()]
|
||||||
|
for (let feed of feeds) {
|
||||||
|
feed.connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// const aggfeed = new AggregatedFeed(feeds, "btc:usd")
|
||||||
|
// const aggfeed2 = new AggregatedFeed(feeds, "eth:usd")
|
||||||
|
|
||||||
|
for (let pair of ["btc:usd", "eth:usd"]) {
|
||||||
|
const aggfeed = new AggregatedFeed(feeds, pair)
|
||||||
|
|
||||||
|
setImmediate(async () => {
|
||||||
|
for await (let _ of aggfeed.updates()) {
|
||||||
|
console.log(aggfeed.median)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => console.log(err))
|
|
@ -2,7 +2,14 @@ import fs from "fs"
|
||||||
import { Wallet } from "solray"
|
import { Wallet } from "solray"
|
||||||
import { AggregatorDeployFile } from "./Deployer"
|
import { AggregatorDeployFile } from "./Deployer"
|
||||||
import { loadJSONFile } from "./json"
|
import { loadJSONFile } from "./json"
|
||||||
import { coinbase } from "./feeds"
|
import {
|
||||||
|
AggregatedFeed,
|
||||||
|
BitStamp,
|
||||||
|
CoinBase,
|
||||||
|
coinbase,
|
||||||
|
FTX,
|
||||||
|
PriceFeed,
|
||||||
|
} from "./feeds"
|
||||||
import { Submitter, SubmitterConfig } from "./Submitter"
|
import { Submitter, SubmitterConfig } from "./Submitter"
|
||||||
import { log } from "./log"
|
import { log } from "./log"
|
||||||
import { conn } from "./context"
|
import { conn } from "./context"
|
||||||
|
@ -10,12 +17,21 @@ import { conn } from "./context"
|
||||||
// Look at all the available aggregators and submit to those that the wallet can
|
// Look at all the available aggregators and submit to those that the wallet can
|
||||||
// act as an oracle.
|
// act as an oracle.
|
||||||
export class PriceFeeder {
|
export class PriceFeeder {
|
||||||
|
private feeds: PriceFeed[]
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private deployInfo: AggregatorDeployFile,
|
private deployInfo: AggregatorDeployFile,
|
||||||
private wallet: Wallet
|
private wallet: Wallet
|
||||||
) {}
|
) {
|
||||||
|
this.feeds = [new CoinBase(), new BitStamp(), new FTX()]
|
||||||
|
}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
|
// connect to the price feeds
|
||||||
|
for (const feed of this.feeds) {
|
||||||
|
feed.connect()
|
||||||
|
}
|
||||||
|
|
||||||
// find aggregators that this wallet can act as oracle
|
// find aggregators that this wallet can act as oracle
|
||||||
this.startAccessibleAggregators()
|
this.startAccessibleAggregators()
|
||||||
}
|
}
|
||||||
|
@ -40,13 +56,13 @@ export class PriceFeeder {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const priceFeed = coinbase(name)
|
const feed = new AggregatedFeed(this.feeds, name)
|
||||||
|
const priceFeed = feed.medians()
|
||||||
|
|
||||||
const pair = name.replace(":", "-").toUpperCase()
|
|
||||||
let minValueChangeForNewRound = 100
|
let minValueChangeForNewRound = 100
|
||||||
if (pair === "BTC-USD") {
|
if (name === "btc:usd") {
|
||||||
minValueChangeForNewRound = 5000
|
minValueChangeForNewRound = 5000
|
||||||
} else if (pair === "ETH-USD") {
|
} else if (name === "eth:usd") {
|
||||||
minValueChangeForNewRound = 150
|
minValueChangeForNewRound = 150
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
319
src/feeds.ts
319
src/feeds.ts
|
@ -1,12 +1,15 @@
|
||||||
import WebSocket from "ws"
|
import WebSocket from "ws"
|
||||||
import EventEmitter from "events"
|
import EventEmitter from "events"
|
||||||
import { eventsIter } from "./utils"
|
import { eventsIter, median } from "./utils"
|
||||||
|
|
||||||
import { log } from "./log"
|
import { log } from "./log"
|
||||||
|
import winston from "winston"
|
||||||
|
|
||||||
export const UPDATE = "UPDATE"
|
export const UPDATE = "UPDATE"
|
||||||
|
|
||||||
export interface IPrice {
|
export interface IPrice {
|
||||||
|
source: string
|
||||||
|
pair: string
|
||||||
decimals: number
|
decimals: number
|
||||||
value: number
|
value: number
|
||||||
}
|
}
|
||||||
|
@ -15,6 +18,318 @@ export interface IPriceFeed {
|
||||||
[Symbol.asyncIterator]: () => AsyncIterator<IPrice>
|
[Symbol.asyncIterator]: () => AsyncIterator<IPrice>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export abstract class PriceFeed {
|
||||||
|
public emitter = new EventEmitter()
|
||||||
|
|
||||||
|
protected conn!: WebSocket
|
||||||
|
protected connected!: Promise<void>
|
||||||
|
|
||||||
|
protected abstract get log(): winston.Logger
|
||||||
|
protected abstract get baseurl(): string
|
||||||
|
|
||||||
|
// subscribed pairs. should re-subscribe on reconnect
|
||||||
|
public pairs: string[] = []
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
this.log.debug("connecting", { baseurl: this.baseurl })
|
||||||
|
|
||||||
|
this.connected = new Promise<void>((resolve) => {
|
||||||
|
const conn = new WebSocket(this.baseurl)
|
||||||
|
conn.on("open", () => {
|
||||||
|
this.log.debug("connected")
|
||||||
|
|
||||||
|
this.conn = conn
|
||||||
|
|
||||||
|
for (let pair of this.pairs) {
|
||||||
|
this.handleSubscribe(pair)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
|
||||||
|
conn.on("close", () => {
|
||||||
|
// TODO: auto-reconnect & re-subscribe
|
||||||
|
})
|
||||||
|
|
||||||
|
conn.on("message", async (data) => {
|
||||||
|
// this.log.debug("raw price update", { data })
|
||||||
|
|
||||||
|
const price = this.parseMessage(data)
|
||||||
|
|
||||||
|
if (price) {
|
||||||
|
this.onMessage(price)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.connected
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(pair: string) {
|
||||||
|
if (this.pairs.includes(pair)) {
|
||||||
|
// already subscribed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pairs.push(pair)
|
||||||
|
|
||||||
|
if (this.conn) {
|
||||||
|
// if already connected immediately subscribe
|
||||||
|
this.handleSubscribe(pair)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(price: IPrice) {
|
||||||
|
this.log.debug("emit price update", { price })
|
||||||
|
|
||||||
|
this.emitter.emit(UPDATE, price)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract parseMessage(data: any): IPrice | undefined
|
||||||
|
// abstract parseMessage(pair: string)
|
||||||
|
// abstract parseMessage(pair: string)
|
||||||
|
abstract handleSubscribe(pair: string): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BitStamp extends PriceFeed {
|
||||||
|
protected log = log.child({ class: BitStamp.name })
|
||||||
|
protected baseurl = "wss://ws.bitstamp.net"
|
||||||
|
|
||||||
|
parseMessage(data) {
|
||||||
|
const payload = JSON.parse(data)
|
||||||
|
|
||||||
|
// {
|
||||||
|
// "channel": "live_trades_btcusd",
|
||||||
|
// "data": {
|
||||||
|
// "amount": 0.02,
|
||||||
|
// "amount_str": "0.02000000",
|
||||||
|
// "buy_order_id": 1339567984607234,
|
||||||
|
// "id": 157699738,
|
||||||
|
// "microtimestamp": "1615877939649000",
|
||||||
|
// "price": 55008.3,
|
||||||
|
// "price_str": "55008.30",
|
||||||
|
// "sell_order_id": 1339567982141443,
|
||||||
|
// "timestamp": "1615877939",
|
||||||
|
// "type": 0
|
||||||
|
// },
|
||||||
|
// "event": "trade"
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (payload.event != "trade") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = (payload.channel as string).replace("live_trades_", "")
|
||||||
|
|
||||||
|
// assume that the symbols for the pair are 3 letters
|
||||||
|
const pair = channel.slice(0, 3) + ":" + channel.slice(3)
|
||||||
|
|
||||||
|
const price: IPrice = {
|
||||||
|
source: BitStamp.name,
|
||||||
|
pair,
|
||||||
|
decimals: 2,
|
||||||
|
value: Math.floor(payload.data.price * 100),
|
||||||
|
}
|
||||||
|
|
||||||
|
return price
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSubscribe(pair: string) {
|
||||||
|
// "btc:usd" => "BTCUSD"
|
||||||
|
const targetPair = pair.replace(":", "").toUpperCase()
|
||||||
|
|
||||||
|
this.conn.send(
|
||||||
|
JSON.stringify({
|
||||||
|
event: "bts:subscribe",
|
||||||
|
data: {
|
||||||
|
channel: `live_trades_${targetPair.replace("/", "").toLowerCase()}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FTX extends PriceFeed {
|
||||||
|
protected log = log.child({ class: FTX.name })
|
||||||
|
protected baseurl = "wss://ftx.com/ws/"
|
||||||
|
|
||||||
|
parseMessage(data) {
|
||||||
|
const payload = JSON.parse(data)
|
||||||
|
|
||||||
|
// {
|
||||||
|
// "channel": "ticker",
|
||||||
|
// "market": "BTC/USD",
|
||||||
|
// "type": "update",
|
||||||
|
// "data": {
|
||||||
|
// "bid": 54567,
|
||||||
|
// "ask": 54577,
|
||||||
|
// "bidSize": 0.0583,
|
||||||
|
// "askSize": 0.2051,
|
||||||
|
// "last": 54582,
|
||||||
|
// "time": 1615877027.551234
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (payload.type != "update" || payload.channel != "ticker") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pair = (payload.market as string).replace("/", ":").toLowerCase()
|
||||||
|
|
||||||
|
const price: IPrice = {
|
||||||
|
source: FTX.name,
|
||||||
|
pair,
|
||||||
|
decimals: 2,
|
||||||
|
value: Math.floor(payload.data.last * 100),
|
||||||
|
}
|
||||||
|
|
||||||
|
return price
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSubscribe(pair: string) {
|
||||||
|
// "btc:usd" => "BTC-USD"
|
||||||
|
const targetPair = pair.replace(":", "/").toUpperCase()
|
||||||
|
|
||||||
|
this.conn.send(
|
||||||
|
JSON.stringify({
|
||||||
|
op: "subscribe",
|
||||||
|
channel: "ticker",
|
||||||
|
market: targetPair,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoinBase extends PriceFeed {
|
||||||
|
protected log = log.child({ class: CoinBase.name })
|
||||||
|
protected baseurl = "wss://ws-feed.pro.coinbase.com"
|
||||||
|
|
||||||
|
parseMessage(data) {
|
||||||
|
const payload = JSON.parse(data)
|
||||||
|
|
||||||
|
// {
|
||||||
|
// "type": "ticker",
|
||||||
|
// "sequence": 22772426228,
|
||||||
|
// "product_id": "BTC-USD",
|
||||||
|
// "price": "53784.59",
|
||||||
|
// "open_24h": "58795.78",
|
||||||
|
// "volume_24h": "35749.39437842",
|
||||||
|
// "low_24h": "53221",
|
||||||
|
// "high_24h": "58799.66",
|
||||||
|
// "volume_30d": "733685.27275521",
|
||||||
|
// "best_bid": "53784.58",
|
||||||
|
// "best_ask": "53784.59",
|
||||||
|
// "side": "buy",
|
||||||
|
// "time": "2021-03-16T06:26:06.791440Z",
|
||||||
|
// "trade_id": 145698988,
|
||||||
|
// "last_size": "0.00474597"
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (payload.type != "ticker") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// "BTC-USD" => "btc:usd"
|
||||||
|
const pair = (payload.product_id as string).replace("-", ":").toLowerCase()
|
||||||
|
|
||||||
|
const price: IPrice = {
|
||||||
|
source: CoinBase.name,
|
||||||
|
pair,
|
||||||
|
decimals: 2,
|
||||||
|
value: Math.floor(payload.price * 100),
|
||||||
|
}
|
||||||
|
|
||||||
|
return price
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSubscribe(pair: string) {
|
||||||
|
// "btc:usd" => "BTC-USD"
|
||||||
|
const targetPair = pair.replace(":", "-").toUpperCase()
|
||||||
|
|
||||||
|
this.conn.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "subscribe",
|
||||||
|
product_ids: [targetPair],
|
||||||
|
channels: ["ticker"],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AggregatedFeed {
|
||||||
|
public emitter = new EventEmitter()
|
||||||
|
public prices: IPrice[] = []
|
||||||
|
|
||||||
|
// assume that the feeds are already connected
|
||||||
|
constructor(public feeds: PriceFeed[], public pair: string) {
|
||||||
|
this.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
private subscribe() {
|
||||||
|
const pair = this.pair
|
||||||
|
|
||||||
|
let i = 0
|
||||||
|
for (let feed of this.feeds) {
|
||||||
|
feed.subscribe(pair)
|
||||||
|
|
||||||
|
const index = i
|
||||||
|
i++
|
||||||
|
|
||||||
|
// store the price updates in the ith position of `this.prices`
|
||||||
|
feed.emitter.on(UPDATE, (price: IPrice) => {
|
||||||
|
if (price.pair != pair) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.prices[index] = price
|
||||||
|
|
||||||
|
this.onPriceUpdate(price)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onPriceUpdate(price: IPrice) {
|
||||||
|
// log.debug("aggregated price update", {
|
||||||
|
// prices: this.prices,
|
||||||
|
// median: this.median,
|
||||||
|
// })
|
||||||
|
this.emitter.emit(UPDATE, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
async *medians() {
|
||||||
|
for await (let _ of this.updates()) {
|
||||||
|
const price = this.median
|
||||||
|
if (price) {
|
||||||
|
yield price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async *updates() {
|
||||||
|
for await (let _ of eventsIter<AggregatedFeed>(this.emitter, "UPDATE")) {
|
||||||
|
yield this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get median(): IPrice | undefined {
|
||||||
|
const prices = this.prices.filter((price) => price != undefined)
|
||||||
|
|
||||||
|
if (prices.length == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = prices.map((price) => price.value)
|
||||||
|
|
||||||
|
return {
|
||||||
|
source: "median",
|
||||||
|
pair: prices[0].pair,
|
||||||
|
decimals: prices[0].decimals,
|
||||||
|
value: median(values),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO remove
|
||||||
export function coinbase(pair: string): IPriceFeed {
|
export function coinbase(pair: string): IPriceFeed {
|
||||||
// TODO: can subscribe to many pairs with one connection
|
// TODO: can subscribe to many pairs with one connection
|
||||||
const emitter = new EventEmitter()
|
const emitter = new EventEmitter()
|
||||||
|
@ -42,6 +357,8 @@ export function coinbase(pair: string): IPriceFeed {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const price: IPrice = {
|
const price: IPrice = {
|
||||||
|
source: "coinbase",
|
||||||
|
pair,
|
||||||
decimals: 2,
|
decimals: 2,
|
||||||
value: Math.floor(json.price * 100),
|
value: Math.floor(json.price * 100),
|
||||||
}
|
}
|
||||||
|
|
14
src/utils.ts
14
src/utils.ts
|
@ -3,6 +3,20 @@ import EventEmitter from "events"
|
||||||
|
|
||||||
import { solana, Wallet } from "solray"
|
import { solana, Wallet } from "solray"
|
||||||
|
|
||||||
|
export function median(values: number[]): number {
|
||||||
|
if (values.length === 0) return 0
|
||||||
|
|
||||||
|
values.sort(function (a, b) {
|
||||||
|
return a - b
|
||||||
|
})
|
||||||
|
|
||||||
|
var half = Math.floor(values.length / 2)
|
||||||
|
|
||||||
|
if (values.length % 2) return values[half]
|
||||||
|
|
||||||
|
return Math.floor((values[half - 1] + values[half]) / 2.0)
|
||||||
|
}
|
||||||
|
|
||||||
export function getMedian(submissions: number[]): number {
|
export function getMedian(submissions: number[]): number {
|
||||||
const values = submissions
|
const values = submissions
|
||||||
.filter((s: any) => s.value != 0)
|
.filter((s: any) => s.value != 0)
|
||||||
|
|
Loading…
Reference in New Issue