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:
Maximilian Schneider 2021-04-07 00:10:20 +02:00
parent 3c7c56b371
commit 0eb97ecc52
10 changed files with 1344 additions and 137 deletions

View File

@ -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"
}
}

View File

@ -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;
};

View File

@ -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);

View File

@ -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>;
};

View File

@ -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);
};

208
src/serum.js Normal file
View File

@ -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;
}

View File

@ -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};

View File

@ -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*');
});
});

View File

@ -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. */

841
yarn.lock

File diff suppressed because it is too large Load Diff