implement tradingview endpoints
* snap candles to period boundaries * reverse order of trade history * fetch from serum v3 & v2 * integrate native & wrapped tether markets
This commit is contained in:
parent
3c7c56b371
commit
0eb97ecc52
20
package.json
20
package.json
|
@ -9,26 +9,34 @@
|
|||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/bn.js": "^5.1.0",
|
||||
"@types/cors": "^2.8.10",
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/jasmine": "^3.6.3",
|
||||
"@types/node": "^14.14.28",
|
||||
"jasmine": "^3.6.4",
|
||||
"jasmine-spec-reporter": "^6.0.0",
|
||||
"jasmine-ts": "^0.3.0",
|
||||
"ts-node": "^9.1.1",
|
||||
"ts-node": "8.10.2",
|
||||
"ts-node-dev": "^1.1.1",
|
||||
"typescript": "^4.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mango/client": "git+ssh://git@github.com:blockworks-foundation/mango-client-ts.git",
|
||||
"@project-serum/serum": "^0.13.20",
|
||||
"@solana/web3.js": "^0.91.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"tedis": "^0.1.12"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "ts-node src/index.ts",
|
||||
"watch": "tsc --watch",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "ts-node-dev src/index.ts",
|
||||
"clean": "rm -rf dist",
|
||||
"prepare": "run-s clean build",
|
||||
"prepare": "run-s clean postinstall",
|
||||
"postinstall": "tsc",
|
||||
"shell": "node -e \"$(< shell)\" -i --experimental-repl-await",
|
||||
"test": "jasmine-ts --config=jasmine.json"
|
||||
},
|
||||
"engines": {
|
||||
"node": "14.x"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { Candle, Trade, Coder } from './interfaces';
|
||||
import { Candle, Trade, TradeSide, Coder } from './interfaces';
|
||||
|
||||
export class Base64TradeCoder implements Coder<Trade> {
|
||||
constructor() {};
|
||||
|
||||
encode(t: Trade): string {
|
||||
const buf = Buffer.alloc(14);
|
||||
const buf = Buffer.alloc(15);
|
||||
buf.writeFloatLE(t.price, 0);
|
||||
buf.writeFloatLE(t.size, 4);
|
||||
buf.writeUIntLE(t.ts, 8, 6);
|
||||
buf.writeUInt8(t.side, 14);
|
||||
const base64 = buf.toString('base64');
|
||||
return base64;
|
||||
};
|
||||
|
@ -17,7 +18,8 @@ export class Base64TradeCoder implements Coder<Trade> {
|
|||
const trade = {
|
||||
price: buf.readFloatLE(0),
|
||||
size: buf.readFloatLE(4),
|
||||
ts: buf.readUIntLE(8, 6)
|
||||
ts: buf.readUIntLE(8, 6),
|
||||
side: buf.readUInt8(14) as TradeSide,
|
||||
};
|
||||
return trade;
|
||||
};
|
||||
|
|
269
src/index.ts
269
src/index.ts
|
@ -1,9 +1,13 @@
|
|||
import { Account, Connection, PublicKey } from '@solana/web3.js';
|
||||
import { Market } from '@project-serum/serum';
|
||||
import { IDS } from '@mango/client';
|
||||
import { Tedis } from 'tedis';
|
||||
import { Order, Trade } from './interfaces';
|
||||
import { RedisStore } from './redis';
|
||||
import { Market, decodeEventQueue } from '@project-serum/serum';
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
import { Tedis, TedisPool } from 'tedis';
|
||||
import { URL } from 'url';
|
||||
import { Order, Trade, TradeSide } from './interfaces';
|
||||
import { RedisConfig, RedisStore, createRedisStore } from './redis';
|
||||
import { encodeEvents } from './serum';
|
||||
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
@ -31,7 +35,11 @@ class OrderBuffer {
|
|||
const now = Date.now();
|
||||
const takerOrders = fills.filter(o => !o.eventFlags.maker);
|
||||
const allTrades = takerOrders.map( o => {
|
||||
return { id: o.orderId.toString(16), price: o.price, size: o.size, ts: now };
|
||||
return { id: o.orderId.toString(16),
|
||||
price: o.price,
|
||||
side: o.side === 'buy' ? TradeSide.Buy : TradeSide.Sell,
|
||||
size: o.size,
|
||||
ts: now };
|
||||
});
|
||||
const newTrades = allTrades.filter(t => !this.cache.has(t.id));
|
||||
|
||||
|
@ -58,53 +66,242 @@ class OrderBuffer {
|
|||
}
|
||||
}
|
||||
|
||||
// process data from cluster as it arrives
|
||||
async function observeMarket(clusterUrl: string, programId: string, marketName:string, marketPk: string, tradeCb: (trades: Trade[]) => void) {
|
||||
const marketAddress = new PublicKey(marketPk);
|
||||
const programKey = new PublicKey(programId);
|
||||
interface MarketConfig {
|
||||
clusterUrl: string;
|
||||
programId: string;
|
||||
marketName: string;
|
||||
marketPk: string;
|
||||
};
|
||||
|
||||
const connection = new Connection(clusterUrl);
|
||||
console.log({ marketName, connection });
|
||||
async function collectTrades(m: MarketConfig, r: RedisConfig) {
|
||||
|
||||
const store = await createRedisStore(r, m.marketName);
|
||||
const marketAddress = new PublicKey(m.marketPk);
|
||||
const programKey = new PublicKey(m.programId);
|
||||
const connection = new Connection(m.clusterUrl);
|
||||
const market = await Market.load(connection, marketAddress, undefined, programKey);
|
||||
console.log({ marketName, market });
|
||||
|
||||
async function storeTrades(ts: Trade[]) {
|
||||
if (ts.length > 0) {
|
||||
console.log(m.marketName, ts.length);
|
||||
for (let i = 0; i < ts.length; i += 1) {
|
||||
await store.storeTrade(ts[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const orderBuffer = new OrderBuffer();
|
||||
while (true) {
|
||||
try {
|
||||
let fills = await market.loadFills(connection);
|
||||
let trades = orderBuffer.filterNewTrades(fills);
|
||||
if (trades.length > 0) {
|
||||
tradeCb(trades);
|
||||
storeTrades(trades);
|
||||
} catch (err) {
|
||||
const error = err.toString().split('\n', 1)[0];
|
||||
console.error(m.marketName, {error});
|
||||
}
|
||||
|
||||
await sleep(10000);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
async function collectEventQueue(m: MarketConfig, r: RedisConfig) {
|
||||
const store = await createRedisStore(r, m.marketName);
|
||||
const marketAddress = new PublicKey(m.marketPk);
|
||||
const programKey = new PublicKey(m.programId);
|
||||
const connection = new Connection(m.clusterUrl);
|
||||
const market = await Market.load(connection, marketAddress, undefined, programKey);
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const accountInfo = await connection.getAccountInfo(market['_decoded'].eventQueue);
|
||||
if (accountInfo === null) {
|
||||
throw new Error(`Event queue account for market ${m.marketName} not found`);
|
||||
}
|
||||
const events = decodeEventQueue(accountInfo.data, 1000).filter(e => e.eventFlags.fill);
|
||||
if (events.length > 0) {
|
||||
const encoded = encodeEvents(events);
|
||||
store.storeBuffer(Date.now(), encoded);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err.toString().split('\n', 1)[0];
|
||||
console.error({ marketName, error });
|
||||
console.error(m.marketName, { error });
|
||||
}
|
||||
|
||||
await sleep(5000);
|
||||
await sleep(10000);
|
||||
}
|
||||
};
|
||||
|
||||
const redisUrl = new URL(process.env.REDISCLOUD_URL || 'redis://localhost:6379');
|
||||
const host = redisUrl.hostname;
|
||||
const port = parseInt(redisUrl.port);
|
||||
let password: string | undefined;
|
||||
if (redisUrl.password !== '') {
|
||||
password = redisUrl.password;
|
||||
}
|
||||
|
||||
const { log, error } = console;
|
||||
console.log = (...args: any[]) => log.bind(console)(new Date(), ...args);
|
||||
console.error = (...args: any[]) => log.bind(error)(new Date(), ...args);
|
||||
const network = "mainnet-beta"
|
||||
const clusterUrl = "https://solana-api.projectserum.com";
|
||||
const programIdV2 = "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o";
|
||||
const programIdV3 = "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin";
|
||||
const marketsV2: Record<string, string> = {
|
||||
"BTC/USDC": "CVfYa8RGXnuDBeGmniCcdkBwoLqVxh92xB1JqgRQx3F",
|
||||
"BTC/USDT": "EXnGBBSamqzd3uxEdRLUiYzjJkTwQyorAaFXdfteuGXe",
|
||||
"ETH/USDC": "H5uzEytiByuXt964KampmuNCurNDwkVVypkym75J2DQW",
|
||||
"ETH/USDT": "5abZGhrELnUnfM9ZUnvK6XJPoBU5eShZwfFPkdhAC7o",
|
||||
"RAY/USDC": "Bgz8EEMBjejAGSn6FdtKJkSGtvg4cuJUuRwaCBp28S3U",
|
||||
"RAY/USDT": "HZyhLoyAnfQ72irTdqPdWo2oFL9zzXaBmAqN72iF3sdX",
|
||||
"SOL/USDC": "7xMDbYTCqQEcK2aM9LbetGtNFJpzKdfXzLL5juaLh4GJ",
|
||||
"SOL/USDT": "7xLk17EQQ5KLDLDe44wCmupJKJjTGd8hs3eSVVhCx932",
|
||||
"SRM/USDC": "CDdR97S8y96v3To93aKvi3nCnjUrbuVSuumw8FLvbVeg",
|
||||
"SRM/USDT": "H3APNWA8bZW2gLMSq5sRL41JSMmEJ648AqoEdDgLcdvB",
|
||||
"USDT/USDC": "8EuuEwULFM7n7zthPjC7kA64LPRzYkpAyuLFiLuVg7D4",
|
||||
};
|
||||
|
||||
let network = "mainnet-beta"
|
||||
let clusterUrl = IDS['cluster_urls'][network];
|
||||
let programId = IDS[network]['dex_program_id'];
|
||||
Object.entries(IDS[network]['spot_markets']).forEach(e => {
|
||||
const [marketName, marketPk] = e;
|
||||
console.log('start processing', {network, clusterUrl, marketName, marketPk});
|
||||
const connection = new Tedis({
|
||||
port: 6379,
|
||||
host: "127.0.0.1"});
|
||||
const store = new RedisStore(connection, marketName);
|
||||
observeMarket(clusterUrl, programId, marketName as string, marketPk as string, async (trades) => {
|
||||
console.log({marketName, trades});
|
||||
for (let i = 0; i < trades.length; i += 1) {
|
||||
await store.store(trades[i]);
|
||||
}
|
||||
const marketsV3: Record<string, string> = {
|
||||
"BTC/USDT": "5r8FfnbNYcQbS1m4CYmoHYGjBtu6bxfo6UJHNRfzPiYH",
|
||||
"ETH/USDT": "71CtEComq2XdhGNbXBuYPmosAjMCPSedcgbNi5jDaGbR",
|
||||
};
|
||||
|
||||
const nativeMarketsV3: Record<string, string> = {
|
||||
"BTC/USDT": "C1EuT9VokAKLiW7i2ASnZUvxDoKuKkCpDDeNxAptuNe4",
|
||||
"ETH/USDT": "7dLVkUfBVfCGkFhSXDCq1ukM9usathSgS716t643iFGF",
|
||||
};
|
||||
|
||||
const symbolsByPk = Object.assign({}, ...Object.entries(marketsV2).map(([a,b]) => ({ [b]: a })),
|
||||
...Object.entries(marketsV3).map(([a,b]) => ({ [b]: a })),
|
||||
...Object.entries(nativeMarketsV3).map(([a,b]) => ({ [b]: a })));
|
||||
|
||||
function collectMarketData(programId: string, markets: Record<string, string>) {
|
||||
Object.entries(markets).forEach(e => {
|
||||
const [marketName, marketPk] = e;
|
||||
const marketConfig = { clusterUrl, programId, marketName, marketPk } as MarketConfig;
|
||||
collectTrades(marketConfig, { host, port, password, db: 0 });
|
||||
//collectEventQueue(marketConfig, { host, port, password, db: 1});
|
||||
});
|
||||
};
|
||||
|
||||
collectMarketData(programIdV2, marketsV2);
|
||||
collectMarketData(programIdV3, marketsV3);
|
||||
collectMarketData(programIdV3, nativeMarketsV3);
|
||||
|
||||
|
||||
interface TradingViewHistory {
|
||||
s: string;
|
||||
t: number[];
|
||||
c: number[];
|
||||
o: number[];
|
||||
h: number[];
|
||||
l: number[];
|
||||
v: number[];
|
||||
};
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
|
||||
const redisConfig = { host, port, password, db: 0, max_conn: 200 };
|
||||
const pool = new TedisPool(redisConfig);
|
||||
|
||||
const HOURS = 60 * MINUTES;
|
||||
const resolutions: {[id: string]: number | undefined} = {
|
||||
"1": 1 * MINUTES,
|
||||
"60": 1 * HOURS,
|
||||
"240": 4 * HOURS,
|
||||
"1D": 24 * HOURS
|
||||
};
|
||||
|
||||
app.get('/tv/config', async (req,res) => {
|
||||
const response = { supported_resolutions: Object.keys(resolutions), supports_group_request:false,
|
||||
supports_marks:false, supports_search:true, supports_timescale_marks:false };
|
||||
res.send(response);
|
||||
});
|
||||
|
||||
app.get('/tv/symbols', async (req, res) => {
|
||||
const symbol = req.query.symbol as string;
|
||||
const response = { name: symbol, ticker: symbol, description: symbol, type: "Spot", session: "24x7",
|
||||
exchange: "Mango", listed_exchange: "Mango", timezone: "Etc/UTC", has_intraday:true,
|
||||
supported_resolutions: Object.keys(resolutions), minmov: 1, pricescale:100 };
|
||||
res.send(response);
|
||||
});
|
||||
|
||||
app.get('/tv/history', async (req,res) => {
|
||||
// parse
|
||||
const marketName = req.query.symbol as string;
|
||||
const marketPk = nativeMarketsV3[marketName] || marketsV3[marketName] || marketsV2[marketName];
|
||||
const resolution = resolutions[req.query.resolution as string] as number;
|
||||
let from = parseInt(req.query.from as string) * 1000;
|
||||
let to = parseInt(req.query.to as string) * 1000;
|
||||
|
||||
// validate
|
||||
const validSymbol = marketPk != undefined;
|
||||
const validResolution = resolution != undefined;
|
||||
const validFrom = true || new Date(from).getFullYear() >= 2021;
|
||||
|
||||
// respond
|
||||
if (!(validSymbol && validResolution && validFrom)) {
|
||||
const error = { s: "error", validSymbol, validResolution, validFrom };
|
||||
console.error({req, error});
|
||||
res.status(500).send(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const conn = await pool.getTedis();
|
||||
try {
|
||||
const store = new RedisStore(conn, marketName);
|
||||
|
||||
// snap candle boundaries to exact hours
|
||||
from = Math.floor(from / resolution) * resolution;
|
||||
to = Math.ceil(to / resolution) * resolution;
|
||||
|
||||
// ensure the candle is at least one period in length
|
||||
if (from == to) {
|
||||
to += resolution;
|
||||
}
|
||||
const candles = await store.loadCandles(resolution, from, to);
|
||||
const response = { s: "ok",
|
||||
t: candles.map(c => c.start / 1000),
|
||||
c: candles.map(c => c.close),
|
||||
o: candles.map(c => c.open),
|
||||
h: candles.map(c => c.high),
|
||||
l: candles.map(c => c.low),
|
||||
v: candles.map(c => c.volume) };
|
||||
res.send(response);
|
||||
return;
|
||||
} finally {
|
||||
pool.putTedis(conn);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error({req, e});
|
||||
const error = { s: "error" };
|
||||
res.status(500).send(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/trades/address/:marketPk', async (req,res) => {
|
||||
try {
|
||||
const conn = await pool.getTedis();
|
||||
try {
|
||||
const marketPk = req.params.marketPk as string;
|
||||
const marketName = symbolsByPk[marketPk];
|
||||
const store = new RedisStore(conn, marketName);
|
||||
const trades = await store.loadRecentTrades();
|
||||
const response = { success: true, data: trades.map(t => { return { market: marketName, marketAddress: marketPk,
|
||||
price: t.price, size: t.size, side: t.side == TradeSide.Buy ? "buy" : "sell", time: t.ts, orderId: "", feeCost:0 };}) };
|
||||
res.send(response);
|
||||
return;
|
||||
} finally {
|
||||
pool.putTedis(conn);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error({req, e});
|
||||
const error = { s: "error" };
|
||||
res.status(500).send(error);
|
||||
}
|
||||
});
|
||||
|
||||
const httpPort = parseInt(process.env.PORT || '5000');
|
||||
app.listen(httpPort);
|
||||
|
||||
|
|
|
@ -3,13 +3,21 @@ import BN from 'bn.js';
|
|||
export interface Order {
|
||||
orderId: BN;
|
||||
price: number;
|
||||
side: 'buy' | 'sell';
|
||||
size: number;
|
||||
eventFlags: { maker: boolean };
|
||||
};
|
||||
|
||||
export enum TradeSide {
|
||||
None = 0,
|
||||
Buy = 1,
|
||||
Sell = 2
|
||||
}
|
||||
|
||||
export interface Trade {
|
||||
id?: string;
|
||||
price: number;
|
||||
side: TradeSide;
|
||||
size: number;
|
||||
ts: number;
|
||||
};
|
||||
|
@ -31,7 +39,12 @@ export interface Candle {
|
|||
};
|
||||
|
||||
export interface CandleStore {
|
||||
store: (t: Trade) => Promise<void>;
|
||||
load: (resolution: number, from: number, to:number) => Promise<Candle[]>;
|
||||
storeTrade: (t: Trade) => Promise<void>;
|
||||
loadCandles: (resolution: number, from: number, to:number) => Promise<Candle[]>;
|
||||
};
|
||||
|
||||
|
||||
export interface BufferStore {
|
||||
storeBuffer: (ts: number, b: Buffer) => Promise<void>;
|
||||
};
|
||||
|
||||
|
|
86
src/redis.ts
86
src/redis.ts
|
@ -2,10 +2,17 @@ import { Base64TradeCoder } from './base64';
|
|||
const coder = new Base64TradeCoder();
|
||||
|
||||
import { batch } from './candle';
|
||||
import { Candle, CandleStore, Trade } from './interfaces';
|
||||
import { BufferStore, Candle, CandleStore, Trade } from './interfaces';
|
||||
import { Tedis } from 'tedis';
|
||||
|
||||
export class RedisStore implements CandleStore {
|
||||
export interface RedisConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
db: number;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
export class RedisStore implements CandleStore, BufferStore {
|
||||
connection: Tedis;
|
||||
symbol: string;
|
||||
|
||||
|
@ -14,17 +21,52 @@ export class RedisStore implements CandleStore {
|
|||
this.symbol = symbol;
|
||||
};
|
||||
|
||||
async store(t: Trade): Promise<void> {
|
||||
keyForDay(ts: number): string {
|
||||
const d = new Date(ts);
|
||||
return `${this.symbol}-${d.getUTCFullYear()}-${d.getUTCMonth()}-${d.getUTCDate()}`;
|
||||
};
|
||||
|
||||
keyForTrade(t: Trade): string {
|
||||
return this.keyForDay(t.ts);
|
||||
};
|
||||
|
||||
keyMatchForCandle(resolution: number, from: number): string {
|
||||
const keys = [this.keyForBuffer(from), this.keyForBuffer(from+resolution)];
|
||||
for (let i = 0; i < Math.min(keys[0].length, keys[1].length); i += 1) {
|
||||
if (keys[0][i] != keys[1][i]) {
|
||||
return keys[0].substr(0, i) + "*";
|
||||
}
|
||||
}
|
||||
return keys[0];
|
||||
};
|
||||
|
||||
keysForCandles(resolution: number, from: number, to: number): string[] {
|
||||
const keys = new Set<string>();
|
||||
while (from < to) {
|
||||
keys.add(this.keyForDay(from));
|
||||
from += resolution
|
||||
};
|
||||
keys.add(this.keyForDay(to));
|
||||
return Array.from(keys);
|
||||
};
|
||||
|
||||
keyForBuffer(ts: number): string {
|
||||
return `${this.symbol}-${ts}`;
|
||||
};
|
||||
|
||||
// interface CandleStore
|
||||
|
||||
async storeTrade(t: Trade): Promise<void> {
|
||||
await this.connection.rpush(this.keyForTrade(t), coder.encode(t));
|
||||
};
|
||||
|
||||
async load(resolution: number, from: number, to: number): Promise<Candle[]> {
|
||||
async loadCandles(resolution: number, from: number, to: number): Promise<Candle[]> {
|
||||
const keys = this.keysForCandles(resolution, from, to);
|
||||
const tradeRequests = keys.map(k => this.connection.lrange(k, 0, -1));
|
||||
const tradeResponses = await Promise.all(tradeRequests);
|
||||
const trades = tradeResponses.flat().map(t => coder.decode(t));
|
||||
const candles: Candle[] = [];
|
||||
while (from + resolution < to) {
|
||||
while (from + resolution <= to) {
|
||||
let candle = batch(trades, from, from+resolution);
|
||||
if (candle) {
|
||||
candles.push(candle);
|
||||
|
@ -34,22 +76,28 @@ export class RedisStore implements CandleStore {
|
|||
return candles;
|
||||
};
|
||||
|
||||
keyForTime(ts: number): string {
|
||||
const d = new Date(ts);
|
||||
return `${this.symbol}-${d.getUTCFullYear()}-${d.getUTCMonth()}-${d.getUTCDate()}`;
|
||||
async loadRecentTrades(): Promise<Trade[]> {
|
||||
const today = Date.now();
|
||||
const yesterday = today - 24*60*60*1000;
|
||||
const keys = [this.keyForDay(yesterday), this.keyForDay(today)];
|
||||
const tradeRequests = keys.map(k => this.connection.lrange(k, 0, -1));
|
||||
const tradeResponses = await Promise.all(tradeRequests);
|
||||
const trades = tradeResponses.flat().slice(-50).reverse().map(t => coder.decode(t));
|
||||
return trades;
|
||||
};
|
||||
|
||||
keyForTrade(t: Trade): string {
|
||||
return this.keyForTime(t.ts);
|
||||
};
|
||||
// interface BufferStore
|
||||
|
||||
keysForCandles(resolution: number, from: number, to: number): string[] {
|
||||
const keys = new Set<string>();
|
||||
while (from < to) {
|
||||
keys.add(this.keyForTime(from));
|
||||
from += resolution
|
||||
};
|
||||
keys.add(this.keyForTime(to));
|
||||
return Array.from(keys);
|
||||
async storeBuffer(ts: number, b: Buffer): Promise<void> {
|
||||
const key = this.keyForBuffer(ts);
|
||||
await this.connection.set(key, b.toString('base64'));
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export async function createRedisStore(config: RedisConfig, symbol: string): Promise<RedisStore> {
|
||||
const conn = new Tedis({host: config.host, port: config.port, password: config.password});
|
||||
await conn.command("SELECT", config.db);
|
||||
return new RedisStore(conn, symbol);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
import { bits, blob, struct, u32, u8, Blob, Layout, UInt } from 'buffer-layout';
|
||||
import BN from 'bn.js';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
|
||||
// copied from serum-ts/layouts.ts
|
||||
|
||||
class Zeros extends Blob {
|
||||
decode(b, offset) {
|
||||
const slice = super.decode(b, offset);
|
||||
if (!slice.every((v) => v === 0)) {
|
||||
throw new Error('nonzero padding bytes');
|
||||
}
|
||||
return slice;
|
||||
}
|
||||
}
|
||||
|
||||
export function zeros(length) {
|
||||
return new Zeros(length);
|
||||
}
|
||||
|
||||
class PublicKeyLayout extends Blob {
|
||||
constructor(property) {
|
||||
super(32, property);
|
||||
}
|
||||
|
||||
decode(b, offset) {
|
||||
return new PublicKey(super.decode(b, offset));
|
||||
}
|
||||
|
||||
encode(src, b, offset) {
|
||||
return super.encode(src.toBuffer(), b, offset);
|
||||
}
|
||||
}
|
||||
|
||||
export function publicKeyLayout(property) {
|
||||
return new PublicKeyLayout(property);
|
||||
}
|
||||
|
||||
class BNLayout extends Blob {
|
||||
decode(b, offset) {
|
||||
return new BN(super.decode(b, offset), 10, 'le');
|
||||
}
|
||||
|
||||
encode(src, b, offset) {
|
||||
return super.encode(src.toArrayLike(Buffer, 'le', this.span), b, offset);
|
||||
}
|
||||
}
|
||||
|
||||
export function u64(property) {
|
||||
return new BNLayout(8, property);
|
||||
}
|
||||
|
||||
export function u128(property) {
|
||||
return new BNLayout(16, property);
|
||||
}
|
||||
|
||||
export class WideBits extends Layout {
|
||||
constructor(property) {
|
||||
super(8, property);
|
||||
this._lower = bits(u32(), false);
|
||||
this._upper = bits(u32(), false);
|
||||
}
|
||||
|
||||
addBoolean(property) {
|
||||
if (this._lower.fields.length < 32) {
|
||||
this._lower.addBoolean(property);
|
||||
} else {
|
||||
this._upper.addBoolean(property);
|
||||
}
|
||||
}
|
||||
|
||||
decode(b, offset = 0) {
|
||||
const lowerDecoded = this._lower.decode(b, offset);
|
||||
const upperDecoded = this._upper.decode(b, offset + this._lower.span);
|
||||
return { ...lowerDecoded, ...upperDecoded };
|
||||
}
|
||||
|
||||
encode(src, b, offset = 0) {
|
||||
return (
|
||||
this._lower.encode(src, b, offset) +
|
||||
this._upper.encode(src, b, offset + this._lower.span)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class VersionedLayout extends Layout {
|
||||
constructor(version, inner, property) {
|
||||
super(inner.span > 0 ? inner.span + 1 : inner.span, property);
|
||||
this.version = version;
|
||||
this.inner = inner;
|
||||
}
|
||||
|
||||
decode(b, offset = 0) {
|
||||
// if (b.readUInt8(offset) !== this._version) {
|
||||
// throw new Error('invalid version');
|
||||
// }
|
||||
return this.inner.decode(b, offset + 1);
|
||||
}
|
||||
|
||||
encode(src, b, offset = 0) {
|
||||
b.writeUInt8(this.version, offset);
|
||||
return 1 + this.inner.encode(src, b, offset + 1);
|
||||
}
|
||||
|
||||
getSpan(b, offset = 0) {
|
||||
return 1 + this.inner.getSpan(b, offset + 1);
|
||||
}
|
||||
}
|
||||
|
||||
class EnumLayout extends UInt {
|
||||
constructor(values, span, property) {
|
||||
super(span, property);
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
encode(src, b, offset) {
|
||||
if (this.values[src] !== undefined) {
|
||||
return super.encode(this.values[src], b, offset);
|
||||
}
|
||||
throw new Error('Invalid ' + this.property);
|
||||
}
|
||||
|
||||
decode(b, offset) {
|
||||
const decodedValue = super.decode(b, offset);
|
||||
const entry = Object.entries(this.values).find(
|
||||
([, value]) => value === decodedValue,
|
||||
);
|
||||
if (entry) {
|
||||
return entry[0];
|
||||
}
|
||||
throw new Error('Invalid ' + this.property);
|
||||
}
|
||||
}
|
||||
|
||||
export function sideLayout(property) {
|
||||
return new EnumLayout({ buy: 0, sell: 1 }, 4, property);
|
||||
}
|
||||
|
||||
export function orderTypeLayout(property) {
|
||||
return new EnumLayout({ limit: 0, ioc: 1, postOnly: 2 }, 4, property);
|
||||
}
|
||||
|
||||
export function selfTradeBehaviorLayout(property) {
|
||||
return new EnumLayout(
|
||||
{ decrementTake: 0, cancelProvide: 1, abortTransaction: 2 },
|
||||
4,
|
||||
property,
|
||||
);
|
||||
}
|
||||
|
||||
const ACCOUNT_FLAGS_LAYOUT = new WideBits();
|
||||
ACCOUNT_FLAGS_LAYOUT.addBoolean('initialized');
|
||||
ACCOUNT_FLAGS_LAYOUT.addBoolean('market');
|
||||
ACCOUNT_FLAGS_LAYOUT.addBoolean('openOrders');
|
||||
ACCOUNT_FLAGS_LAYOUT.addBoolean('requestQueue');
|
||||
ACCOUNT_FLAGS_LAYOUT.addBoolean('eventQueue');
|
||||
ACCOUNT_FLAGS_LAYOUT.addBoolean('bids');
|
||||
ACCOUNT_FLAGS_LAYOUT.addBoolean('asks');
|
||||
|
||||
export function accountFlagsLayout(property = 'accountFlags') {
|
||||
return ACCOUNT_FLAGS_LAYOUT.replicate(property);
|
||||
}
|
||||
|
||||
export function setLayoutDecoder(layout, decoder) {
|
||||
const originalDecode = layout.decode;
|
||||
layout.decode = function decode(b, offset = 0) {
|
||||
return decoder(originalDecode.call(this, b, offset));
|
||||
};
|
||||
}
|
||||
|
||||
export function setLayoutEncoder(layout, encoder) {
|
||||
const originalEncode = layout.encode;
|
||||
layout.encode = function encode(src, b, offset) {
|
||||
return originalEncode.call(this, encoder(src), b, offset);
|
||||
};
|
||||
return layout;
|
||||
}
|
||||
|
||||
// copied from serum-ts/queue.ts
|
||||
|
||||
const EVENT_FLAGS = bits(u8(), false, 'eventFlags');
|
||||
EVENT_FLAGS.addBoolean('fill');
|
||||
EVENT_FLAGS.addBoolean('out');
|
||||
EVENT_FLAGS.addBoolean('bid');
|
||||
EVENT_FLAGS.addBoolean('maker');
|
||||
|
||||
const EVENT = struct([
|
||||
EVENT_FLAGS,
|
||||
u8('openOrdersSlot'),
|
||||
u8('feeTier'),
|
||||
blob(5),
|
||||
u64('nativeQuantityReleased'), // Amount the user received
|
||||
u64('nativeQuantityPaid'), // Amount the user paid
|
||||
u64('nativeFeeOrRebate'),
|
||||
u128('orderId'),
|
||||
publicKeyLayout('openOrders'),
|
||||
u64('clientOrderId'),
|
||||
]);
|
||||
|
||||
// should be a PR
|
||||
|
||||
export function encodeEvents(events) {
|
||||
let buffer = new Buffer.alloc(events.length * EVENT.span);
|
||||
for (let i = 0; i < events.length; i += 1) {
|
||||
EVENT.encode(events[i], buffer, i*EVENT.span);
|
||||
}
|
||||
return buffer;
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { Base64TradeCoder, Base64CandleCoder } from '../base64';
|
||||
import { TradeSide } from '../interfaces';
|
||||
|
||||
const t = {id: '1234567890', price: 0.1234567, size: 1234567, ts: Date.now()};
|
||||
const t = {id: '1234567890', price: 0.1234567, side: TradeSide.Buy, size: 1234567, ts: Date.now()};
|
||||
const c = {open: 0.12345, close: 0.123456, high: 0.1234567, low: 0.12345678,
|
||||
volume: 1234567, vwap: 0.123456789, start: 1234567890, end: 1234567899};
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { RedisStore } from '../redis';
|
||||
import { TradeSide } from '../interfaces';
|
||||
import { Tedis } from "tedis";
|
||||
|
||||
|
||||
const SECONDS = 1000;
|
||||
const DAYS = 86400000;
|
||||
const YEARS = 365*DAYS;
|
||||
const ts = [{id: '0', price: 1.2, size: 3.4, ts: 1234567890000},
|
||||
{id: '1', price: 2.3, size: 4.5, ts: 1234567890000-0.8*DAYS},
|
||||
{id: '2', price: 3.4, size: 5.6, ts: 1234567890000+1*DAYS},
|
||||
{id: '3', price: 4.5, size: 6.7, ts: 1234567890000+3*DAYS}];
|
||||
const ts = [{id: '0', price: 1.2, side: TradeSide.Buy, size: 3.4, ts: 1234567890000},
|
||||
{id: '1', price: 2.3, side: TradeSide.Buy, size: 4.5, ts: 1234567890000-0.8*DAYS},
|
||||
{id: '2', price: 3.4, side: TradeSide.Buy, size: 5.6, ts: 1234567890000+1*DAYS},
|
||||
{id: '3', price: 4.5, side: TradeSide.Buy, size: 6.7, ts: 1234567890000+3*DAYS}];
|
||||
const s = new RedisStore({} as Tedis, 'ABC/DEF');
|
||||
|
||||
describe('RedisStore', () => {
|
||||
|
@ -21,7 +22,7 @@ describe('RedisStore', () => {
|
|||
expect(keys[2]).not.toBe(keys[3]);
|
||||
});
|
||||
|
||||
it('iterates buckets per day', () => {
|
||||
it('iterates trades for candles in buckets per day', () => {
|
||||
let from = ts[0].ts;
|
||||
let to = from + 1.2*DAYS;
|
||||
let keys = s.keysForCandles(DAYS, from, to);
|
||||
|
@ -30,11 +31,21 @@ describe('RedisStore', () => {
|
|||
'ABC/DEF-2009-1-15']);
|
||||
});
|
||||
|
||||
it('iterates preserving order', () => {
|
||||
it('iterates trades for candles preserving order', () => {
|
||||
let from = ts[0].ts;
|
||||
let to = from + 1*YEARS;
|
||||
let keys = s.keysForCandles(DAYS, from, to);
|
||||
expect(keys[0]).toEqual('ABC/DEF-2009-1-13');
|
||||
expect(keys[keys.length-1]).toEqual('ABC/DEF-2010-1-13');
|
||||
});
|
||||
|
||||
it('stores buffers in keys per millisecond', () => {
|
||||
expect(s.keyForBuffer(ts[0].ts)).toEqual('ABC/DEF-1234567890000');
|
||||
});
|
||||
|
||||
it('iterates events for a single candle', () => {
|
||||
let from = ts[0].ts;
|
||||
let keyMatch = s.keyMatchForCandle(DAYS, from);
|
||||
expect(keyMatch).toEqual('ABC/DEF-1234*');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||
"target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||
"lib": ["es2019"], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
"allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
|
|
Loading…
Reference in New Issue