mempool/backend/src/api/bisq/markets-api.ts

613 lines
20 KiB
TypeScript
Raw Normal View History

2020-09-10 09:46:23 +02:00
import { Currencies, OffsersData, TradesData, Depth, Currency, Interval, HighLowOpenClose,
2020-09-14 22:11:52 +02:00
Markets, Offers, Offer, BisqTrade, MarketVolume, Tickers, Ticker, SummarizedIntervals, SummarizedInterval } from './interfaces';
2020-09-10 09:46:23 +02:00
2020-09-13 12:51:53 +02:00
import * as datetime from 'locutus/php/datetime';
2020-09-10 09:46:23 +02:00
class BisqMarketsApi {
private cryptoCurrencyData: Currency[] = [];
private fiatCurrencyData: Currency[] = [];
private offersData: OffsersData[] = [];
private tradesData: TradesData[] = [];
2020-09-14 22:11:52 +02:00
private fiatCurrenciesIndexed: { [code: string]: true } = {};
private tradeDataByMarket: { [market: string]: TradesData[] } = {};
2020-09-10 09:46:23 +02:00
constructor() { }
public setData(cryptoCurrency: Currency[], fiatCurrency: Currency[], offers: OffsersData[], trades: TradesData[]) {
this.cryptoCurrencyData = cryptoCurrency,
this.fiatCurrencyData = fiatCurrency;
this.offersData = offers;
this.tradesData = trades;
2020-09-14 22:11:52 +02:00
// Handle data for smarter memory caching
this.fiatCurrencyData.forEach((currency) => {
currency._type = 'fiat';
this.fiatCurrenciesIndexed[currency.code] = true;
});
this.cryptoCurrencyData.forEach((currency) => currency._type = 'crypto');
2020-09-10 09:46:23 +02:00
this.tradesData.forEach((trade) => {
trade._market = trade.currencyPair.toLowerCase().replace('/', '_');
2020-09-14 22:11:52 +02:00
if (!this.tradeDataByMarket[trade._market]) {
this.tradeDataByMarket[trade._market] = [];
}
this.tradeDataByMarket[trade._market].push(trade);
2020-09-10 09:46:23 +02:00
});
}
getCurrencies(
type: 'crypto' | 'fiat' | 'all' = 'all',
): Currencies {
let currencies: Currency[];
switch (type) {
case 'fiat':
currencies = this.fiatCurrencyData;
break;
case 'crypto':
currencies = this.cryptoCurrencyData;
break;
case 'all':
default:
currencies = this.cryptoCurrencyData.concat(this.fiatCurrencyData);
}
const result = {};
currencies.forEach((currency) => {
result[currency.code] = currency;
});
return result;
}
getDepth(
market: string,
): Depth {
const currencyPair = market.replace('_', '/').toUpperCase();
const buys = this.offersData
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'BUY')
2020-09-10 09:46:23 +02:00
.map((offer) => offer.price)
.sort((a, b) => b - a)
.map((price) => this.intToBtc(price));
const sells = this.offersData
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'SELL')
2020-09-10 09:46:23 +02:00
.map((offer) => offer.price)
.sort((a, b) => a - b)
.map((price) => this.intToBtc(price));
const result = {};
result[market] = {
'buys': buys,
'sells': sells,
};
return result;
}
getOffers(
market: string,
direction?: 'buy' | 'sell',
2020-09-10 09:46:23 +02:00
): Offers {
const currencyPair = market.replace('_', '/').toUpperCase();
let buys: Offer[] | null = null;
let sells: Offer[] | null = null;
if (!direction || direction === 'buy') {
2020-09-10 09:46:23 +02:00
buys = this.offersData
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'BUY')
.sort((a, b) => b.price - a.price)
2020-09-10 09:46:23 +02:00
.map((offer) => this.offerDataToOffer(offer));
}
if (!direction || direction === 'sell') {
2020-09-10 09:46:23 +02:00
sells = this.offersData
.filter((offer) => offer.currencyPair === currencyPair && offer.primaryMarketDirection === 'SELL')
.sort((a, b) => a.price - b.price)
2020-09-10 09:46:23 +02:00
.map((offer) => this.offerDataToOffer(offer));
}
const result: Offers = {};
result[market] = {
'buys': buys,
'sells': sells,
};
return result;
}
getMarkets(): Markets {
const allCurrencies = this.getCurrencies();
const markets = {};
for (const currency of Object.keys(allCurrencies)) {
if (allCurrencies[currency].code === 'BTC') {
continue;
}
const isFiat = allCurrencies[currency]._type === 'fiat';
2020-09-10 09:46:23 +02:00
const pmarketname = allCurrencies['BTC']['name'];
const lsymbol = isFiat ? 'BTC' : currency;
const rsymbol = isFiat ? currency : 'BTC';
const lname = isFiat ? pmarketname : allCurrencies[currency].name;
const rname = isFiat ? allCurrencies[currency].name : pmarketname;
const ltype = isFiat ? 'crypto' : allCurrencies[currency]._type;
2020-09-10 09:46:23 +02:00
const rtype = isFiat ? 'fiat' : 'crypto';
const lprecision = 8;
const rprecision = isFiat ? 2 : 8;
const pair = lsymbol.toLowerCase() + '_' + rsymbol.toLowerCase();
markets[pair] = {
'pair': pair,
'lname': lname,
'rname': rname,
'lsymbol': lsymbol,
'rsymbol': rsymbol,
'lprecision': lprecision,
'rprecision': rprecision,
'ltype': ltype,
'rtype': rtype,
'name': lname + '/' + rname,
};
}
return markets;
}
getTrades(
market: string,
timestamp_from?: number,
timestamp_to?: number,
trade_id_from?: string,
trade_id_to?: string,
direction?: 'buy' | 'sell',
limit: number = 100,
sort: 'asc' | 'desc' = 'desc',
2020-09-13 12:51:53 +02:00
): BisqTrade[] {
2020-09-10 09:46:23 +02:00
limit = Math.min(limit, 2000);
2020-09-14 22:11:52 +02:00
const _market = market === 'all' ? undefined : market;
2020-09-10 09:46:23 +02:00
if (!timestamp_from) {
2020-09-13 12:51:53 +02:00
timestamp_from = new Date('2016-01-01').getTime() / 1000;
2020-09-10 09:46:23 +02:00
}
if (!timestamp_to) {
2020-09-13 12:51:53 +02:00
timestamp_to = new Date().getTime() / 1000;
2020-09-10 09:46:23 +02:00
}
const matches = this.getTradesByCriteria(_market, timestamp_to, timestamp_from,
trade_id_to, trade_id_from, direction, sort, limit, false);
2020-09-10 09:46:23 +02:00
if (sort === 'asc') {
matches.sort((a, b) => a.tradeDate - b.tradeDate);
} else {
matches.sort((a, b) => b.tradeDate - a.tradeDate);
}
return matches.map((trade) => {
const bsqTrade: BisqTrade = {
direction: trade.primaryMarketDirection,
2020-09-13 12:51:53 +02:00
price: trade._tradePriceStr,
amount: trade._tradeAmountStr,
volume: trade._tradeVolumeStr,
payment_method: trade.paymentMethod,
trade_id: trade.offerId,
trade_date: trade.tradeDate,
};
if (market === 'all') {
bsqTrade.market = trade._market;
}
return bsqTrade;
});
2020-09-10 09:46:23 +02:00
}
getVolumes(
market?: string,
2020-09-14 22:11:52 +02:00
timestamp_from?: number,
timestamp_to?: number,
interval: Interval = 'auto',
milliseconds?: boolean,
): MarketVolume[] {
if (milliseconds) {
timestamp_from = timestamp_from ? timestamp_from / 1000 : timestamp_from;
timestamp_to = timestamp_to ? timestamp_to / 1000 : timestamp_to;
}
if (!timestamp_from) {
timestamp_from = new Date('2016-01-01').getTime() / 1000;
}
if (!timestamp_to) {
timestamp_to = new Date().getTime() / 1000;
}
const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from,
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
if (interval === 'auto') {
const range = timestamp_to - timestamp_from;
interval = this.getIntervalFromRange(range);
}
const intervals: any = {};
const marketVolumes: MarketVolume[] = [];
for (const trade of trades) {
const traded_at = trade['tradeDate'] / 1000;
const interval_start = this.intervalStart(traded_at, interval);
if (!intervals[interval_start]) {
intervals[interval_start] = {
'volume': 0,
'num_trades': 0,
};
}
const period = intervals[interval_start];
period['period_start'] = interval_start;
period['volume'] += this.fiatCurrenciesIndexed[trade.currency] ? trade._tradeAmount : trade._tradeVolume;
period['num_trades']++;
}
for (const p in intervals) {
if (intervals.hasOwnProperty(p)) {
const period = intervals[p];
marketVolumes.push({
period_start: period['period_start'],
num_trades: period['num_trades'],
volume: this.intToBtc(period['volume']),
});
}
}
return intervals;
2020-09-10 09:46:23 +02:00
}
getTicker(
market?: string,
2020-09-14 22:11:52 +02:00
): Tickers | Ticker | null {
if (market) {
return this.getTickerFromMarket(market);
}
const allMarkets = this.getMarkets();
const tickers = {};
for (const m in allMarkets) {
if (allMarkets.hasOwnProperty(m)) {
tickers[allMarkets[m].pair] = this.getTickerFromMarket(allMarkets[m].pair);
}
}
return tickers;
}
getTickerFromMarket(market: string): Ticker | null {
let ticker: Ticker;
const timestamp_from = datetime.strtotime('-24 hour');
const timestamp_to = new Date().getTime() / 1000;
const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from,
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
const periods: SummarizedInterval[] = Object.values(this.getTradesSummarized(trades, timestamp_from));
const allCurrencies = this.getCurrencies();
const currencyRight = allCurrencies[market.split('_')[1].toUpperCase()];
if (periods[0]) {
ticker = {
'last': this.intToBtc(periods[0].close),
'high': this.intToBtc(periods[0].high),
'low': this.intToBtc(periods[0].low),
'volume_left': this.intToBtc(periods[0].volume_left),
'volume_right': this.intToBtc(periods[0].volume_right),
'buy': null,
'sell': null,
};
} else {
const lastTrade = this.tradeDataByMarket[market];
if (!lastTrade) {
return null;
}
const tradePrice = lastTrade[0].primaryMarketTradePrice * Math.pow(10, 8 - currencyRight.precision);
const lastTradePrice = this.intToBtc(tradePrice);
ticker = {
'last': lastTradePrice,
'high': lastTradePrice,
'low': lastTradePrice,
'volume_left': '0',
'volume_right': '0',
'buy': null,
'sell': null,
};
}
const timestampFromMilli = timestamp_from * 1000;
const timestampToMilli = timestamp_to * 1000;
const currencyPair = market.replace('_', '/').toUpperCase();
const offersData = this.offersData.slice().sort((a, b) => a.price - b.price);
const buy = offersData.find((offer) => offer.currencyPair === currencyPair
&& offer.primaryMarketDirection === 'BUY'
&& offer.date >= timestampFromMilli
&& offer.date <= timestampToMilli
);
const sell = offersData.find((offer) => offer.currencyPair === currencyPair
&& offer.primaryMarketDirection === 'SELL'
&& offer.date >= timestampFromMilli
&& offer.date <= timestampToMilli
);
if (buy) {
ticker.buy = this.intToBtc(buy.primaryMarketPrice * Math.pow(10, 8 - currencyRight.precision));
}
if (sell) {
ticker.sell = this.intToBtc(sell.primaryMarketPrice * Math.pow(10, 8 - currencyRight.precision));
}
return ticker;
2020-09-10 09:46:23 +02:00
}
getHloc(
market: string,
interval: Interval = 'auto',
timestamp_from?: number,
timestamp_to?: number,
2020-09-13 12:51:53 +02:00
milliseconds?: boolean,
2020-09-10 09:46:23 +02:00
): HighLowOpenClose[] {
2020-09-13 12:51:53 +02:00
if (milliseconds) {
timestamp_from = timestamp_from ? timestamp_from / 1000 : timestamp_from;
timestamp_to = timestamp_to ? timestamp_to / 1000 : timestamp_to;
}
2020-09-10 09:46:23 +02:00
if (!timestamp_from) {
2020-09-13 12:51:53 +02:00
timestamp_from = new Date('2016-01-01').getTime() / 1000;
2020-09-10 09:46:23 +02:00
}
if (!timestamp_to) {
2020-09-13 12:51:53 +02:00
timestamp_to = new Date().getTime() / 1000;
2020-09-10 09:46:23 +02:00
}
2020-09-13 12:51:53 +02:00
const trades = this.getTradesByCriteria(market, timestamp_to, timestamp_from,
undefined, undefined, undefined, 'asc', Number.MAX_SAFE_INTEGER);
if (interval === 'auto') {
2020-09-14 22:11:52 +02:00
const range = timestamp_to - timestamp_from;
interval = this.getIntervalFromRange(range);
}
const intervals = this.getTradesSummarized(trades, timestamp_from, interval);
const hloc: HighLowOpenClose[] = [];
for (const p in intervals) {
if (intervals.hasOwnProperty(p)) {
const period = intervals[p];
hloc.push({
period_start: period['period_start'],
open: this.intToBtc(period['open']),
close: this.intToBtc(period['close']),
high: this.intToBtc(period['high']),
low: this.intToBtc(period['low']),
avg: this.intToBtc(period['avg']),
volume_right: this.intToBtc(period['volume_right']),
volume_left: this.intToBtc(period['volume_left']),
});
}
2020-09-13 12:51:53 +02:00
}
2020-09-14 22:11:52 +02:00
return hloc;
}
2020-09-13 12:51:53 +02:00
2020-09-14 22:11:52 +02:00
private getIntervalFromRange(range: number): Interval {
// two days range loads minute data
if (range <= 3600) {
// up to one hour range loads minutely data
return 'minute';
} else if (range <= 1 * 24 * 3600) {
// up to one day range loads half-hourly data
return 'half_hour';
} else if (range <= 3 * 24 * 3600) {
// up to 3 day range loads hourly data
return 'hour';
} else if (range <= 7 * 24 * 3600) {
// up to 7 day range loads half-daily data
return 'half_day';
} else if (range <= 60 * 24 * 3600) {
// up to 2 month range loads daily data
return 'day';
} else if (range <= 12 * 31 * 24 * 3600) {
// up to one year range loads weekly data
return 'week';
} else if (range <= 12 * 31 * 24 * 3600) {
// up to 5 year range loads monthly data
return 'month';
} else {
// greater range loads yearly data
return 'year';
}
2020-09-10 09:46:23 +02:00
}
2020-09-14 22:11:52 +02:00
private getTradesSummarized(trades: TradesData[], timestamp_from: number, interval?: string): SummarizedIntervals {
2020-09-13 12:51:53 +02:00
const intervals: any = {};
const intervals_prices: any = {};
for (const trade of trades) {
const traded_at = trade.tradeDate / 1000;
2020-09-14 22:11:52 +02:00
const interval_start = !interval ? timestamp_from : this.intervalStart(traded_at, interval);
2020-09-13 12:51:53 +02:00
if (!intervals[interval_start]) {
intervals[interval_start] = {
'open': 0,
'close': 0,
'high': 0,
'low': 0,
'avg': 0,
'volume_right': 0,
'volume_left': 0,
};
intervals_prices[interval_start] = [];
}
const period = intervals[interval_start];
const price = trade._tradePrice;
if (!intervals_prices[interval_start]['leftvol']) {
intervals_prices[interval_start]['leftvol'] = [];
}
if (!intervals_prices[interval_start]['rightvol']) {
intervals_prices[interval_start]['rightvol'] = [];
}
intervals_prices[interval_start]['leftvol'].push(trade._tradeAmount);
intervals_prices[interval_start]['rightvol'].push(trade._tradeVolume);
if (price) {
const plow = period['low'];
period['period_start'] = interval_start;
period['open'] = period['open'] || price;
period['close'] = price;
period['high'] = price > period['high'] ? price : period['high'];
period['low'] = (plow && price > plow) ? period['low'] : price;
period['avg'] = intervals_prices[interval_start]['rightvol'].reduce((p: number, c: number) => c + p, 0)
/ intervals_prices[interval_start]['leftvol'].reduce((c: number, p: number) => c + p, 0) * 100000000;
period['volume_left'] += trade._tradeAmount;
period['volume_right'] += trade._tradeVolume;
}
}
2020-09-14 22:11:52 +02:00
return intervals;
2020-09-13 12:51:53 +02:00
}
private getTradesByCriteria(
2020-09-14 22:11:52 +02:00
market: string | undefined,
2020-09-13 12:51:53 +02:00
timestamp_to: number,
timestamp_from: number,
trade_id_to: string | undefined,
trade_id_from: string | undefined,
direction: 'buy' | 'sell' | undefined,
sort: string, limit: number,
integerAmounts: boolean = true,
): TradesData[] {
let trade_id_from_ts: number | null = null;
let trade_id_to_ts: number | null = null;
const allCurrencies = this.getCurrencies();
const timestampFromMilli = timestamp_from * 1000;
const timestampToMilli = timestamp_to * 1000;
// note: the offer_id_from/to depends on iterating over trades in
// descending chronological order.
const tradesDataSorted = this.tradesData.slice();
if (sort === 'asc') {
tradesDataSorted.reverse();
}
let matches: TradesData[] = [];
for (const trade of tradesDataSorted) {
if (trade_id_from === trade.offerId) {
trade_id_from_ts = trade.tradeDate;
}
if (trade_id_to === trade.offerId) {
trade_id_to_ts = trade.tradeDate;
}
if (trade_id_to && trade_id_to_ts === null) {
continue;
}
if (trade_id_from && trade_id_from_ts != null && trade_id_from_ts !== trade.tradeDate) {
continue;
}
if (market && market !== trade._market) {
continue;
}
if (timestampFromMilli && timestampFromMilli > trade.tradeDate) {
continue;
}
if (timestampToMilli && timestampToMilli < trade.tradeDate) {
continue;
}
if (direction && direction !== trade.direction.toLowerCase()) {
continue;
}
// Filter out bogus trades with BTC/BTC or XXX/XXX market.
// See github issue: https://github.com/bitsquare/bitsquare/issues/883
const currencyPairs = trade.currencyPair.split('/');
if (currencyPairs[0] === currencyPairs[1]) {
continue;
}
const currencyLeft = allCurrencies[currencyPairs[0]];
const currencyRight = allCurrencies[currencyPairs[1]];
if (!currencyLeft || !currencyRight) {
continue;
}
const tradePrice = trade.primaryMarketTradePrice * Math.pow(10, 8 - currencyRight.precision);
const tradeAmount = trade.primaryMarketTradeAmount * Math.pow(10, 8 - currencyLeft.precision);
const tradeVolume = trade.primaryMarketTradeVolume * Math.pow(10, 8 - currencyRight.precision);
if (integerAmounts) {
trade._tradePrice = tradePrice;
trade._tradeAmount = tradeAmount;
trade._tradeVolume = tradeVolume;
trade._offerAmount = trade.offerAmount;
} else {
trade._tradePriceStr = this.intToBtc(tradePrice);
trade._tradeAmountStr = this.intToBtc(tradeAmount);
trade._tradeVolumeStr = this.intToBtc(tradeVolume);
trade._offerAmountStr = this.intToBtc(trade.offerAmount);
}
matches.push(trade);
if (matches.length >= limit) {
break;
}
}
if ((trade_id_from && !trade_id_from_ts) || (trade_id_to && !trade_id_to_ts)) {
matches = [];
}
return matches;
}
2020-09-14 22:11:52 +02:00
private intervalStart(ts: number, interval: string): number {
2020-09-13 12:51:53 +02:00
switch (interval) {
case 'minute':
return (ts - (ts % 60));
case '10_minute':
return (ts - (ts % 600));
case 'half_hour':
return (ts - (ts % 1800));
case 'hour':
return (ts - (ts % 3600));
case 'half_day':
return (ts - (ts % (3600 * 12)));
case 'day':
return datetime.strtotime('midnight today', ts);
case 'week':
return datetime.strtotime('midnight sunday last week', ts);
case 'month':
return datetime.strtotime('midnight first day of this month', ts);
case 'year':
return datetime.strtotime('midnight first day of january', ts);
default:
throw new Error('Unsupported interval: ' + interval);
}
}
2020-09-10 09:46:23 +02:00
private offerDataToOffer(offer: OffsersData): Offer {
return {
offer_id: offer.id,
offer_date: offer.date,
direction: offer.direction,
min_amount: this.intToBtc(offer.minAmount),
amount: this.intToBtc(offer.amount),
price: this.intToBtc(offer.price),
volume: this.intToBtc(offer.primaryMarketVolume),
payment_method: offer.paymentMethod,
offer_fee_txid: null,
};
}
private intToBtc(val: number): string {
return (val / 100000000).toFixed(8);
}
}
export default new BisqMarketsApi();