This commit is contained in:
Maximilian Schneider 2021-07-02 17:55:57 +02:00
parent c6c3240cbf
commit c0a3b9b805
2 changed files with 88 additions and 64 deletions

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}

View File

@ -1,38 +1,50 @@
import { Account, Connection, PublicKey } from "@solana/web3.js" import { Account, Connection, PublicKey } from '@solana/web3.js'
import { Market } from "@project-serum/serum" import { Market } from '@project-serum/serum'
import cors from "cors" import cors from 'cors'
import express from "express" import express from 'express'
import { Tedis, TedisPool } from "tedis" import { Tedis, TedisPool } from 'tedis'
import { URL } from "url" import { URL } from 'url'
import { decodeRecentEvents } from "./events" import { decodeRecentEvents } from './events'
import { MarketConfig, Trade, TradeSide } from "./interfaces" import { MarketConfig, Trade, TradeSide } from './interfaces'
import { RedisConfig, RedisStore, createRedisStore } from "./redis" import { RedisConfig, RedisStore, createRedisStore } from './redis'
import { resolutions, sleep } from "./time" import { resolutions, sleep } from './time'
async function collectEventQueue(m: MarketConfig, r: RedisConfig) { async function collectEventQueue(m: MarketConfig, r: RedisConfig) {
const store = await createRedisStore(r, m.marketName) const store = await createRedisStore(r, m.marketName)
const marketAddress = new PublicKey(m.marketPk) const marketAddress = new PublicKey(m.marketPk)
const programKey = new PublicKey(m.programId) const programKey = new PublicKey(m.programId)
const connection = new Connection(m.clusterUrl) const connection = new Connection(m.clusterUrl)
const market = await Market.load(connection, marketAddress, undefined, programKey) const market = await Market.load(
connection,
marketAddress,
undefined,
programKey
)
async function fetchTrades(lastSeqNum?: number): Promise<[Trade[], number]> { async function fetchTrades(lastSeqNum?: number): Promise<[Trade[], number]> {
const now = Date.now() const now = Date.now()
const accountInfo = await connection.getAccountInfo(market["_decoded"].eventQueue) const accountInfo = await connection.getAccountInfo(
market['_decoded'].eventQueue
)
if (accountInfo === null) { if (accountInfo === null) {
throw new Error(`Event queue account for market ${m.marketName} not found`) throw new Error(
`Event queue account for market ${m.marketName} not found`
)
} }
const { header, events } = decodeRecentEvents(accountInfo.data, lastSeqNum); const { header, events } = decodeRecentEvents(accountInfo.data, lastSeqNum)
const takerFills = events.filter((e) => e.eventFlags.fill && !e.eventFlags.maker) const takerFills = events.filter(
const trades = takerFills.map(e => market.parseFillEvent(e)).map((e) => { (e) => e.eventFlags.fill && !e.eventFlags.maker
return { )
price: e.price, const trades = takerFills
side: e.side === "buy" ? TradeSide.Buy : TradeSide.Sell, .map((e) => market.parseFillEvent(e))
size: e.size, .map((e) => {
ts: now, return {
} price: e.price,
}) side: e.side === 'buy' ? TradeSide.Buy : TradeSide.Sell,
size: e.size,
ts: now,
}
})
/* /*
if (trades.length > 0) if (trades.length > 0)
console.log({e: events.map(e => e.eventFlags), takerFills, trades}) console.log({e: events.map(e => e.eventFlags), takerFills, trades})
@ -51,42 +63,45 @@ async function collectEventQueue(m: MarketConfig, r: RedisConfig) {
while (true) { while (true) {
try { try {
const lastSeqNum = (await store.loadNumber('LASTSEQ')); const lastSeqNum = await store.loadNumber('LASTSEQ')
const [trades, currentSeqNum] = await fetchTrades(lastSeqNum); const [trades, currentSeqNum] = await fetchTrades(lastSeqNum)
storeTrades(trades) storeTrades(trades)
store.storeNumber('LASTSEQ', currentSeqNum) store.storeNumber('LASTSEQ', currentSeqNum)
} catch (err) { } catch (err) {
const error = err.toString().split("\n", 1)[0] const error = err.toString().split('\n', 1)[0]
console.error(m.marketName, { error }) console.error(m.marketName, { error })
} }
await sleep({Seconds: process.env.INTERVAL ? parseInt(process.env.INTERVAL) : 10}) await sleep({
Seconds: process.env.INTERVAL ? parseInt(process.env.INTERVAL) : 10,
})
} }
} }
const redisUrl = new URL(process.env.REDISCLOUD_URL || "redis://localhost:6379") const redisUrl = new URL(process.env.REDISCLOUD_URL || 'redis://localhost:6379')
const host = redisUrl.hostname const host = redisUrl.hostname
const port = parseInt(redisUrl.port) const port = parseInt(redisUrl.port)
let password: string | undefined let password: string | undefined
if (redisUrl.password !== "") { if (redisUrl.password !== '') {
password = redisUrl.password password = redisUrl.password
} }
const network = "mainnet-beta" const network = 'mainnet-beta'
const clusterUrl = process.env.RPC_ENDPOINT_URL || "https://solana-api.projectserum.com" const clusterUrl =
const programIdV3 = "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin" process.env.RPC_ENDPOINT_URL || 'https://solana-api.projectserum.com'
const programIdV3 = '9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin'
const nativeMarketsV3: Record<string, string> = { const nativeMarketsV3: Record<string, string> = {
"BTC/USDT": "C1EuT9VokAKLiW7i2ASnZUvxDoKuKkCpDDeNxAptuNe4", 'BTC/USDT': 'C1EuT9VokAKLiW7i2ASnZUvxDoKuKkCpDDeNxAptuNe4',
"ETH/USDT": "7dLVkUfBVfCGkFhSXDCq1ukM9usathSgS716t643iFGF", 'ETH/USDT': '7dLVkUfBVfCGkFhSXDCq1ukM9usathSgS716t643iFGF',
"SOL/USDT": "HWHvQhFmJB3NUcu1aihKmrKegfVxBEHzwVX6yZCKEsi1", 'SOL/USDT': 'HWHvQhFmJB3NUcu1aihKmrKegfVxBEHzwVX6yZCKEsi1',
"SRM/USDT": "AtNnsY1AyRERWJ8xCskfz38YdvruWVJQUVXgScC1iPb", 'SRM/USDT': 'AtNnsY1AyRERWJ8xCskfz38YdvruWVJQUVXgScC1iPb',
"RAY/USDT": "teE55QrL4a4QSfydR9dnHF97jgCfptpuigbb53Lo95g", 'RAY/USDT': 'teE55QrL4a4QSfydR9dnHF97jgCfptpuigbb53Lo95g',
"BTC/USDC": "A8YFbxQYFVqKZaoYJLLUVcQiWP7G2MeEgW5wsAQgMvFw", 'BTC/USDC': 'A8YFbxQYFVqKZaoYJLLUVcQiWP7G2MeEgW5wsAQgMvFw',
"ETH/USDC": "4tSvZvnbyzHXLMTiFonMyxZoHmFqau1XArcRCVHLZ5gX", 'ETH/USDC': '4tSvZvnbyzHXLMTiFonMyxZoHmFqau1XArcRCVHLZ5gX',
"SOL/USDC": "9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT", 'SOL/USDC': '9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT',
"SRM/USDC": "ByRys5tuUWDgL73G8JBAEfkdFf8JWBzPBDHsBVQ5vbQA", 'SRM/USDC': 'ByRys5tuUWDgL73G8JBAEfkdFf8JWBzPBDHsBVQ5vbQA',
"RAY/USDC": "2xiv8A5xrJ7RnGdxXB42uFEkYHJjszEhaJyKKt4WaLep", 'RAY/USDC': '2xiv8A5xrJ7RnGdxXB42uFEkYHJjszEhaJyKKt4WaLep',
"MCAPS/USDC": "GgzXqy6agt7nnfoPjAEAFpWqnUwLBK5r2acaAQqXiEM8", 'MCAPS/USDC': 'GgzXqy6agt7nnfoPjAEAFpWqnUwLBK5r2acaAQqXiEM8',
} }
const symbolsByPk = Object.assign( const symbolsByPk = Object.assign(
@ -97,21 +112,26 @@ const symbolsByPk = Object.assign(
function collectMarketData(programId: string, markets: Record<string, string>) { function collectMarketData(programId: string, markets: Record<string, string>) {
Object.entries(markets).forEach((e) => { Object.entries(markets).forEach((e) => {
const [marketName, marketPk] = e const [marketName, marketPk] = e
const marketConfig = { clusterUrl, programId, marketName, marketPk } as MarketConfig const marketConfig = {
collectEventQueue(marketConfig, { host, port, password, db: 0}); clusterUrl,
programId,
marketName,
marketPk,
} as MarketConfig
collectEventQueue(marketConfig, { host, port, password, db: 0 })
}) })
} }
collectMarketData(programIdV3, nativeMarketsV3) collectMarketData(programIdV3, nativeMarketsV3)
const max_conn = parseInt(process.env.REDIS_MAX_CONN || "") || 200; const max_conn = parseInt(process.env.REDIS_MAX_CONN || '') || 200
const redisConfig = { host, port, password, db: 0, max_conn } const redisConfig = { host, port, password, db: 0, max_conn }
const pool = new TedisPool(redisConfig) const pool = new TedisPool(redisConfig)
const app = express() const app = express()
app.use(cors()) app.use(cors())
app.get("/tv/config", async (req, res) => { app.get('/tv/config', async (req, res) => {
const response = { const response = {
supported_resolutions: Object.keys(resolutions), supported_resolutions: Object.keys(resolutions),
supports_group_request: false, supports_group_request: false,
@ -122,17 +142,17 @@ app.get("/tv/config", async (req, res) => {
res.send(response) res.send(response)
}) })
app.get("/tv/symbols", async (req, res) => { app.get('/tv/symbols', async (req, res) => {
const symbol = req.query.symbol as string const symbol = req.query.symbol as string
const response = { const response = {
name: symbol, name: symbol,
ticker: symbol, ticker: symbol,
description: symbol, description: symbol,
type: "Spot", type: 'Spot',
session: "24x7", session: '24x7',
exchange: "Mango", exchange: 'Mango',
listed_exchange: "Mango", listed_exchange: 'Mango',
timezone: "Etc/UTC", timezone: 'Etc/UTC',
has_intraday: true, has_intraday: true,
supported_resolutions: Object.keys(resolutions), supported_resolutions: Object.keys(resolutions),
minmov: 1, minmov: 1,
@ -141,7 +161,7 @@ app.get("/tv/symbols", async (req, res) => {
res.send(response) res.send(response)
}) })
app.get("/tv/history", async (req, res) => { app.get('/tv/history', async (req, res) => {
// parse // parse
const marketName = req.query.symbol as string const marketName = req.query.symbol as string
const marketPk = nativeMarketsV3[marketName] const marketPk = nativeMarketsV3[marketName]
@ -154,7 +174,7 @@ app.get("/tv/history", async (req, res) => {
const validResolution = resolution != undefined const validResolution = resolution != undefined
const validFrom = true || new Date(from).getFullYear() >= 2021 const validFrom = true || new Date(from).getFullYear() >= 2021
if (!(validSymbol && validResolution && validFrom)) { if (!(validSymbol && validResolution && validFrom)) {
const error = { s: "error", validSymbol, validResolution, validFrom } const error = { s: 'error', validSymbol, validResolution, validFrom }
console.error({ marketName, error }) console.error({ marketName, error })
res.status(404).send(error) res.status(404).send(error)
return return
@ -176,7 +196,7 @@ app.get("/tv/history", async (req, res) => {
} }
const candles = await store.loadCandles(resolution, from, to) const candles = await store.loadCandles(resolution, from, to)
const response = { const response = {
s: "ok", s: 'ok',
t: candles.map((c) => c.start / 1000), t: candles.map((c) => c.start / 1000),
c: candles.map((c) => c.close), c: candles.map((c) => c.close),
o: candles.map((c) => c.open), o: candles.map((c) => c.open),
@ -191,12 +211,12 @@ app.get("/tv/history", async (req, res) => {
} }
} catch (e) { } catch (e) {
console.error({ req, e }) console.error({ req, e })
const error = { s: "error" } const error = { s: 'error' }
res.status(500).send(error) res.status(500).send(error)
} }
}) })
app.get("/trades/address/:marketPk", async (req, res) => { app.get('/trades/address/:marketPk', async (req, res) => {
// parse // parse
const marketPk = req.params.marketPk as string const marketPk = req.params.marketPk as string
const marketName = symbolsByPk[marketPk] const marketName = symbolsByPk[marketPk]
@ -204,7 +224,7 @@ app.get("/trades/address/:marketPk", async (req, res) => {
// validate // validate
const validPk = marketName != undefined const validPk = marketName != undefined
if (!validPk) { if (!validPk) {
const error = { s: "error", validPk } const error = { s: 'error', validPk }
console.error({ marketPk, error }) console.error({ marketPk, error })
res.status(404).send(error) res.status(404).send(error)
return return
@ -224,9 +244,9 @@ app.get("/trades/address/:marketPk", async (req, res) => {
marketAddress: marketPk, marketAddress: marketPk,
price: t.price, price: t.price,
size: t.size, size: t.size,
side: t.side == TradeSide.Buy ? "buy" : "sell", side: t.side == TradeSide.Buy ? 'buy' : 'sell',
time: t.ts, time: t.ts,
orderId: "", orderId: '',
feeCost: 0, feeCost: 0,
} }
}), }),
@ -238,10 +258,10 @@ app.get("/trades/address/:marketPk", async (req, res) => {
} }
} catch (e) { } catch (e) {
console.error({ req, e }) console.error({ req, e })
const error = { s: "error" } const error = { s: 'error' }
res.status(500).send(error) res.status(500).send(error)
} }
}) })
const httpPort = parseInt(process.env.PORT || "5000") const httpPort = parseInt(process.env.PORT || '5000')
app.listen(httpPort) app.listen(httpPort)