2021-03-16 15:15:11 -07:00
|
|
|
import WebSocket from 'ws'
|
|
|
|
import ReconnectingWebSocket from 'reconnecting-websocket'
|
2021-02-17 05:39:03 -08:00
|
|
|
import EventEmitter from "events"
|
2021-05-26 05:46:14 -07:00
|
|
|
import pako from 'pako'
|
2021-04-05 12:20:54 -07:00
|
|
|
import { eventsIter, median, notify } from "./utils"
|
2021-02-17 05:39:03 -08:00
|
|
|
|
2021-02-20 02:00:17 -08:00
|
|
|
import { log } from "./log"
|
2021-03-16 01:04:38 -07:00
|
|
|
import winston from "winston"
|
2021-05-05 16:02:45 -07:00
|
|
|
import fs from "fs";
|
2021-03-16 17:14:30 -07:00
|
|
|
|
|
|
|
const SECONDS = 1000
|
2021-02-17 05:39:03 -08:00
|
|
|
export const UPDATE = "UPDATE"
|
|
|
|
|
|
|
|
export interface IPrice {
|
2021-03-16 01:04:38 -07:00
|
|
|
source: string
|
|
|
|
pair: string
|
2021-02-17 05:39:03 -08:00
|
|
|
decimals: number
|
|
|
|
value: number
|
2021-03-16 17:14:30 -07:00
|
|
|
timestamp?: number
|
2021-02-17 05:39:03 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface IPriceFeed {
|
|
|
|
[Symbol.asyncIterator]: () => AsyncIterator<IPrice>
|
|
|
|
}
|
|
|
|
|
2021-03-16 01:04:38 -07:00
|
|
|
export abstract class PriceFeed {
|
|
|
|
public emitter = new EventEmitter()
|
|
|
|
|
2021-03-16 15:15:11 -07:00
|
|
|
protected conn!: ReconnectingWebSocket
|
2021-03-16 01:04:38 -07:00
|
|
|
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) => {
|
2021-03-16 15:15:11 -07:00
|
|
|
const conn = new ReconnectingWebSocket(this.baseurl, [], { WebSocket })
|
|
|
|
conn.addEventListener("open", () => {
|
2021-04-05 12:20:54 -07:00
|
|
|
notify(`socket ${this.baseurl}: open`)
|
2021-03-16 01:04:38 -07:00
|
|
|
|
|
|
|
this.conn = conn
|
|
|
|
|
|
|
|
for (let pair of this.pairs) {
|
|
|
|
this.handleSubscribe(pair)
|
|
|
|
}
|
|
|
|
|
|
|
|
resolve()
|
|
|
|
})
|
|
|
|
|
2021-03-16 15:15:11 -07:00
|
|
|
conn.addEventListener("close", () => {
|
2021-04-05 12:20:54 -07:00
|
|
|
notify(`socket ${this.baseurl}: closed`)
|
2021-03-16 01:04:38 -07:00
|
|
|
})
|
|
|
|
|
2021-03-16 15:15:11 -07:00
|
|
|
conn.addEventListener("error", (e) => {
|
2021-04-05 12:20:54 -07:00
|
|
|
notify(`socket ${this.baseurl}: error=${e}`)
|
2021-03-16 15:15:11 -07:00
|
|
|
})
|
2021-03-16 01:04:38 -07:00
|
|
|
|
2021-03-16 15:15:11 -07:00
|
|
|
conn.addEventListener("message", (msg) => {
|
|
|
|
const price = this.parseMessage(msg.data)
|
2021-03-16 01:04:38 -07:00
|
|
|
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>
|
|
|
|
}
|
|
|
|
|
2021-06-03 04:52:35 -07:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
const pair = payload.product_id as string
|
|
|
|
|
|
|
|
const price: IPrice = {
|
|
|
|
source: CoinBase.name,
|
|
|
|
pair,
|
|
|
|
decimals: 2,
|
|
|
|
value: Math.floor(payload.price * 100),
|
|
|
|
}
|
|
|
|
|
|
|
|
return price
|
|
|
|
}
|
|
|
|
|
|
|
|
async handleSubscribe(pair: string) {
|
|
|
|
this.conn.send(
|
|
|
|
JSON.stringify({
|
|
|
|
type: "subscribe",
|
|
|
|
product_ids: [pair],
|
|
|
|
channels: ["ticker"],
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-16 01:04:38 -07:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-06-03 04:52:35 -07:00
|
|
|
const pair = payload.channel as string
|
2021-03-16 01:04:38 -07:00
|
|
|
|
|
|
|
const price: IPrice = {
|
|
|
|
source: BitStamp.name,
|
|
|
|
pair,
|
|
|
|
decimals: 2,
|
|
|
|
value: Math.floor(payload.data.price * 100),
|
|
|
|
}
|
|
|
|
|
|
|
|
return price
|
|
|
|
}
|
|
|
|
|
|
|
|
async handleSubscribe(pair: string) {
|
|
|
|
this.conn.send(
|
|
|
|
JSON.stringify({
|
|
|
|
event: "bts:subscribe",
|
|
|
|
data: {
|
2021-06-03 04:52:35 -07:00
|
|
|
channel: pair,
|
2021-03-16 01:04:38 -07:00
|
|
|
},
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-06-03 04:52:35 -07:00
|
|
|
const pair = payload.market as string
|
2021-03-16 01:04:38 -07:00
|
|
|
|
|
|
|
const price: IPrice = {
|
|
|
|
source: FTX.name,
|
|
|
|
pair,
|
|
|
|
decimals: 2,
|
|
|
|
value: Math.floor(payload.data.last * 100),
|
|
|
|
}
|
|
|
|
|
|
|
|
return price
|
|
|
|
}
|
|
|
|
|
|
|
|
async handleSubscribe(pair: string) {
|
|
|
|
this.conn.send(
|
|
|
|
JSON.stringify({
|
|
|
|
op: "subscribe",
|
|
|
|
channel: "ticker",
|
2021-06-03 04:52:35 -07:00
|
|
|
market: pair,
|
2021-03-16 01:04:38 -07:00
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-26 05:07:20 -07:00
|
|
|
export class Binance extends PriceFeed {
|
|
|
|
protected log = log.child({ class: Binance.name })
|
|
|
|
protected baseurl = "wss://stream.binance.com/ws"
|
|
|
|
|
|
|
|
parseMessage(data) {
|
|
|
|
const payload = JSON.parse(data)
|
|
|
|
|
|
|
|
// {
|
|
|
|
// "e": "trade", // Event type
|
|
|
|
// "E": 123456789, // Event time
|
|
|
|
// "s": "BNBBTC", // Symbol
|
|
|
|
// "t": 12345, // Trade ID
|
|
|
|
// "p": "0.001", // Price
|
|
|
|
// "q": "100", // Quantity
|
|
|
|
// "b": 88, // Buyer order ID
|
|
|
|
// "a": 50, // Seller order ID
|
|
|
|
// "T": 123456785, // Trade time
|
|
|
|
// "m": true, // Is the buyer the market maker?
|
|
|
|
// "M": true // Ignore
|
|
|
|
// }
|
|
|
|
|
|
|
|
if (payload.e != "trade") {
|
|
|
|
return
|
|
|
|
}
|
2021-06-03 04:52:35 -07:00
|
|
|
|
|
|
|
const pair = payload.s;
|
2021-04-26 05:07:20 -07:00
|
|
|
|
|
|
|
|
|
|
|
const price: IPrice = {
|
|
|
|
source: Binance.name,
|
|
|
|
pair,
|
|
|
|
decimals: 2,
|
|
|
|
value: Math.floor(payload.p * 100),
|
|
|
|
}
|
|
|
|
|
|
|
|
return price
|
|
|
|
}
|
|
|
|
|
|
|
|
async handleSubscribe(pair: string) {
|
2021-06-03 04:52:35 -07:00
|
|
|
const targetPair = `${pair}@trade`.toLowerCase()
|
2021-04-26 05:07:20 -07:00
|
|
|
this.conn.send(
|
|
|
|
JSON.stringify({
|
|
|
|
method: "SUBSCRIBE",
|
|
|
|
params: [
|
|
|
|
targetPair,
|
|
|
|
],
|
|
|
|
id: 1
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-26 05:46:14 -07:00
|
|
|
export class OKEx extends PriceFeed {
|
|
|
|
protected log = log.child({ class: OKEx.name })
|
|
|
|
protected baseurl = "wss://real.okex.com:8443/ws/v3"
|
|
|
|
|
|
|
|
parseMessage(data) {
|
|
|
|
const message = pako.inflate(data, { raw: true, to: 'string' });
|
|
|
|
const payload = JSON.parse(message);
|
|
|
|
|
|
|
|
// {
|
|
|
|
// "table":"spot/ticker",
|
|
|
|
// "data": [
|
|
|
|
// {
|
|
|
|
// "last":"2819.04",
|
|
|
|
// "open_24h":"2447.02",
|
|
|
|
// "best_bid":"2818.82",
|
|
|
|
// "high_24h":"2909.68",
|
|
|
|
// "low_24h":"2380.95",
|
|
|
|
// "open_utc0":"2704.92",
|
|
|
|
// "open_utc8":"2610.12",
|
|
|
|
// "base_volume_24h":"215048.740665",
|
|
|
|
// "quote_volume_24h":"578231392.9501",
|
|
|
|
// "best_ask":"2818.83",
|
|
|
|
// "instrument_id":"ETH-USDT",
|
|
|
|
// "timestamp":"2021-05-26T11:46:11.826Z",
|
|
|
|
// "best_bid_size":"0.104506",
|
|
|
|
// "best_ask_size":"21.524559",
|
|
|
|
// "last_qty":"0.210619"
|
|
|
|
// }
|
|
|
|
// ]
|
|
|
|
// }
|
|
|
|
|
|
|
|
if (payload.table != "spot/ticker") {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-06-03 04:52:35 -07:00
|
|
|
const pair = payload.data[0].instrument_id as string;
|
|
|
|
|
2021-05-26 05:46:14 -07:00
|
|
|
const price: IPrice = {
|
|
|
|
source: OKEx.name,
|
|
|
|
pair,
|
|
|
|
decimals: 2,
|
|
|
|
value: Math.floor(payload.data[0].last * 100),
|
|
|
|
}
|
|
|
|
|
|
|
|
return price
|
|
|
|
}
|
|
|
|
|
|
|
|
async handleSubscribe(pair: string) {
|
2021-06-03 04:52:35 -07:00
|
|
|
const targetPair = `spot/ticker:${pair}`
|
2021-05-26 05:46:14 -07:00
|
|
|
this.conn.send(
|
|
|
|
JSON.stringify({
|
|
|
|
"op": "subscribe",
|
|
|
|
"args": [
|
|
|
|
targetPair,
|
|
|
|
]
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-16 01:04:38 -07:00
|
|
|
export class AggregatedFeed {
|
|
|
|
public emitter = new EventEmitter()
|
|
|
|
public prices: IPrice[] = []
|
|
|
|
|
|
|
|
// assume that the feeds are already connected
|
2021-06-04 01:16:27 -07:00
|
|
|
constructor(public feeds: PriceFeed[], public pairMappings: string[], public decimals: number, public pair: string) {
|
2021-03-16 01:04:38 -07:00
|
|
|
this.subscribe()
|
|
|
|
}
|
|
|
|
|
|
|
|
private subscribe() {
|
|
|
|
const pair = this.pair
|
2021-06-03 04:52:35 -07:00
|
|
|
const pairMappings = this.pairMappings;
|
2021-06-04 01:16:27 -07:00
|
|
|
const decimals = this.decimals;
|
2021-06-03 04:52:35 -07:00
|
|
|
|
|
|
|
let j = 0
|
|
|
|
|
|
|
|
for (let i = 0; i < this.feeds.length; i++) {
|
|
|
|
const feed = this.feeds[i];
|
|
|
|
feed.subscribe(pairMappings[i]);
|
|
|
|
const index = j;
|
|
|
|
j++;
|
|
|
|
// store the price updates in the ith position of `this.prices`
|
|
|
|
feed.emitter.on(UPDATE, (price: IPrice) => {
|
|
|
|
if (price.pair != pairMappings[i]) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
price.timestamp = Date.now()
|
2021-06-04 01:16:27 -07:00
|
|
|
price.decimals = decimals;
|
2021-06-03 04:52:35 -07:00
|
|
|
this.prices[index] = price
|
|
|
|
this.onPriceUpdate(price)
|
|
|
|
})
|
2021-03-16 01:04:38 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private onPriceUpdate(price: IPrice) {
|
|
|
|
// log.debug("aggregated price update", {
|
|
|
|
// prices: this.prices,
|
|
|
|
// median: this.median,
|
|
|
|
// })
|
|
|
|
this.emitter.emit(UPDATE, this)
|
|
|
|
}
|
|
|
|
|
2021-03-16 01:16:56 -07:00
|
|
|
async *medians() {
|
2021-03-16 01:04:38 -07:00
|
|
|
for await (let _ of this.updates()) {
|
2021-03-16 01:16:56 -07:00
|
|
|
const price = this.median
|
|
|
|
if (price) {
|
|
|
|
yield price
|
|
|
|
}
|
2021-03-16 01:04:38 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-16 01:16:56 -07:00
|
|
|
async *updates() {
|
2021-03-16 01:04:38 -07:00
|
|
|
for await (let _ of eventsIter<AggregatedFeed>(this.emitter, "UPDATE")) {
|
|
|
|
yield this
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-16 17:14:30 -07:00
|
|
|
// filter out prices that are older than 10 seconds
|
|
|
|
recentPrices() : IPrice[] {
|
|
|
|
return this.prices.filter((p) => p &&
|
|
|
|
p.timestamp &&
|
|
|
|
(p.timestamp - Date.now()) < 10*SECONDS)
|
|
|
|
}
|
2021-03-16 01:04:38 -07:00
|
|
|
|
2021-03-16 17:14:30 -07:00
|
|
|
get median(): IPrice | undefined {
|
|
|
|
const prices = this.recentPrices()
|
2021-03-16 01:04:38 -07:00
|
|
|
if (prices.length == 0) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const values = prices.map((price) => price.value)
|
|
|
|
|
2021-03-16 17:14:30 -07:00
|
|
|
const result = {
|
2021-03-16 01:04:38 -07:00
|
|
|
source: "median",
|
|
|
|
pair: prices[0].pair,
|
|
|
|
decimals: prices[0].decimals,
|
|
|
|
value: median(values),
|
|
|
|
}
|
2021-03-16 17:14:30 -07:00
|
|
|
|
|
|
|
// console.log({...result, values})
|
|
|
|
|
|
|
|
return result;
|
2021-03-16 01:04:38 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO remove
|
2021-02-17 05:39:03 -08:00
|
|
|
export function coinbase(pair: string): IPriceFeed {
|
2021-02-19 05:13:56 -08:00
|
|
|
// TODO: can subscribe to many pairs with one connection
|
2021-02-17 05:39:03 -08:00
|
|
|
const emitter = new EventEmitter()
|
|
|
|
|
|
|
|
const ws = new WebSocket("wss://ws-feed.pro.coinbase.com")
|
|
|
|
|
2021-02-19 05:13:56 -08:00
|
|
|
// "btc:usd" => "BTC-USD"
|
|
|
|
pair = pair.replace(":", "-").toUpperCase()
|
2021-02-17 05:39:03 -08:00
|
|
|
ws.on("open", () => {
|
2021-02-20 02:00:17 -08:00
|
|
|
log.debug(`price feed connected`, { pair })
|
2021-02-17 05:39:03 -08:00
|
|
|
ws.send(
|
|
|
|
JSON.stringify({
|
|
|
|
type: "subscribe",
|
2021-02-19 05:13:56 -08:00
|
|
|
product_ids: [pair],
|
2021-02-17 05:39:03 -08:00
|
|
|
channels: ["ticker"],
|
|
|
|
})
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
ws.on("message", async (data) => {
|
|
|
|
const json = JSON.parse(data)
|
2021-02-20 02:00:17 -08:00
|
|
|
log.debug("price update", json)
|
|
|
|
|
2021-02-17 05:39:03 -08:00
|
|
|
if (!json || !json.price) {
|
2021-02-20 02:00:17 -08:00
|
|
|
return
|
2021-02-17 05:39:03 -08:00
|
|
|
}
|
|
|
|
const price: IPrice = {
|
2021-03-16 01:04:38 -07:00
|
|
|
source: "coinbase",
|
|
|
|
pair,
|
2021-02-17 05:39:03 -08:00
|
|
|
decimals: 2,
|
|
|
|
value: Math.floor(json.price * 100),
|
|
|
|
}
|
|
|
|
emitter.emit(UPDATE, price)
|
|
|
|
// console.log("current price:", json.price)
|
|
|
|
})
|
|
|
|
|
|
|
|
ws.on("close", (err) => {
|
|
|
|
// TODO: automatic reconnect
|
2021-02-20 02:00:17 -08:00
|
|
|
log.debug(`price feed closed`, { pair, err: err.toString() })
|
2021-02-17 05:39:03 -08:00
|
|
|
process.exit(1)
|
|
|
|
})
|
|
|
|
|
2021-02-20 01:05:05 -08:00
|
|
|
return eventsIter(emitter, UPDATE)
|
2021-02-17 05:39:03 -08:00
|
|
|
}
|
2021-05-05 16:02:45 -07:00
|
|
|
|
|
|
|
export function file(pair: string, filepath: string): IPriceFeed {
|
|
|
|
const emitter = new EventEmitter()
|
2021-06-04 01:16:27 -07:00
|
|
|
|
2021-05-05 16:02:45 -07:00
|
|
|
try {
|
|
|
|
fs.accessSync(filepath);
|
|
|
|
} catch {
|
|
|
|
fs.writeFileSync(filepath, '');
|
|
|
|
console.log('feed file created at ' + filepath)
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('started file feed for ' + pair)
|
|
|
|
|
|
|
|
fs.watch(filepath, (event) => {
|
|
|
|
if (event === 'change') {
|
|
|
|
const data = fs.readFileSync(filepath, 'utf8')
|
|
|
|
const price: IPrice = {
|
|
|
|
source: "file",
|
|
|
|
pair,
|
|
|
|
decimals: 2,
|
|
|
|
value: parseFloat(data)
|
|
|
|
}
|
|
|
|
console.log('price update: ', price)
|
|
|
|
emitter.emit(UPDATE, price)
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return eventsIter(emitter, UPDATE)
|
2021-06-04 01:16:27 -07:00
|
|
|
}
|