OasisDEX/oasis-react

View on GitHub
src/store/selectors/charts.js

Summary

Maintainability
D
2 days
Test Coverage
A
99%
import { createSelector } from 'reselect';
import reselect from '../../utils/reselect';
import moment from 'moment-timezone';
import BigNumber from 'bignumber.js';
import web3 from '../../bootstrap/web3';
import _ from 'lodash';
import {Map, List} from 'immutable';
import {removeOutliersFromArray} from '../../utils/functions';

const trades = state => state.get('trades').get('marketHistory');

const tokenTrades = createSelector(
  trades,
  reselect.getProps,
  (trades, props) => {
    const tokens = props ? [props.tradingPair.baseToken, props.tradingPair.quoteToken] : [];
    return (trades || []).filter(t =>
      t.buyWhichToken == tokens[0] && t.sellWhichToken == tokens[1] ||
      t.buyWhichToken == tokens[1] && t.sellWhichToken == tokens[0]
    )
  },
)


const priceChartTrades = createSelector(
  tokenTrades,
  (trades) => {
    const since = moment(Date.now()).startOf('day').subtract(6, 'days').unix()
    const res = (trades || []).filter(t =>
      t.timestamp >= since
    );
    return res.length == 0 ? [] : res.toJS();
  }
)

const priceChartLabels = createSelector(
  priceChartTrades,
  (priceChartTrades) => priceChartTrades.map((trade) => trade.timestamp)
)

const priceChartValues = createSelector(
  priceChartTrades,
  reselect.getProps,
  (priceChartTrades, props) => priceChartTrades.map((trade) => {
    let baseAmount;
    let quoteAmount;
    if (trade.buyWhichToken === props.tradingPair.quoteToken) {
      quoteAmount = new BigNumber(trade.buyHowMuch);
      baseAmount = new BigNumber(trade.sellHowMuch);
    } else {
      baseAmount = new BigNumber(trade.buyHowMuch);
      quoteAmount = new BigNumber(trade.sellHowMuch);
    }
    return quoteAmount.dividedBy(baseAmount).toFixed(4);
  })
)


const volumeChartTrades = createSelector(
  tokenTrades,
  (trades) => {
    const since = moment(Date.now()).startOf('day').subtract(6, 'days').unix()
    return (trades || []).filter(t =>
      t.timestamp >= since
    );
  }
)

const volumeChartPoints = createSelector(
  () => (
    [6, 5, 4, 3, 2, 1, 0].map(i =>
      moment(Date.now()).startOf('day').subtract(i, 'days')
    )
  )
)

const volumeChartLabels = createSelector(
  volumeChartPoints,
  (volumeChartPoints) => volumeChartPoints.map(d => d.unix())
)

const volumeChartData = createSelector(
  volumeChartTrades,
  volumeChartPoints,
  reselect.getProps,
  (volumeChartTrades, volumeChartPoints, props) => {
      let volumes = {base: {}, quote: {}}
      volumeChartPoints.forEach(day => {
        volumes.base[day.unix()] = new BigNumber(0);
        volumes.quote[day.unix()] = new BigNumber(0);
      })
      volumeChartTrades.forEach(trade => {
        const day = moment.unix(trade.timestamp).startOf('day').unix();
        if (trade.buyWhichToken === props.tradingPair.quoteToken) {
          volumes.quote[day] = volumes.quote[day].add(new BigNumber(trade.buyHowMuch));
          volumes.base[day] = volumes.base[day].add(new BigNumber(trade.sellHowMuch));
        } else {
          volumes.quote[day] = volumes.quote[day].add(new BigNumber(trade.sellHowMuch));
          volumes.base[day] = volumes.base[day].add(new BigNumber(trade.buyHowMuch));
        }
      })
      return volumes;
  }
)

const volumeChartValues = createSelector(
  volumeChartData,
  (volumeChartData) => Object.keys(volumeChartData.quote).map((key) =>
    web3.fromWei(volumeChartData.quote[key]).toFixed(2)
  )
)

const volumeChartTooltips = createSelector(
  volumeChartData,
  (volumeChartData) => ({
    base: Object.values(volumeChartData.base).map(v => web3.fromWei(v).toFormat(2)),
    quote: Object.values(volumeChartData.quote).map(v => web3.fromWei(v).toFormat(2)),
  })
)


const offers = state => state.get('offers').get('offers');

const offersBids = createSelector(
  offers,
  reselect.getProps,
  (offers, props) => (
    !props ? [] : removeOutliersFromArray(
      ((offers.get(Map({baseToken: props.tradingPair.baseToken, quoteToken: props.tradingPair.quoteToken})) || Map()).get('buyOffers') || List()).sortBy(b => b.bid_price_sort).toJS()
    , 'bid_price_sort', 3)
  ),
)

const offersAsks = createSelector(
  offers,
  reselect.getProps,
  (offers, props) => (
    !props ? [] : removeOutliersFromArray(
      ((offers.get(Map({baseToken: props.tradingPair.baseToken, quoteToken: props.tradingPair.quoteToken})) || Map()).get('sellOffers') || List()).sortBy(a => a.ask_price_sort).toJS()
    , 'ask_price_sort', 3)
  ),
)

function upSum(array) {
  return array.reduce(({sum, result}, bn) => (
    {sum: sum.add(bn), result: result.concat(bn.add(sum))}
  ), {sum: new BigNumber(0), result: []}).result;
}

function downSum(array) {
  return array.reduceRight(({sum, result}, bn) => (
    {sum: sum.add(bn), result: [bn.add(sum)].concat(result)}
  ), {sum: new BigNumber(0), result: []}).result;
}

function bigSum(array) {
  return array.reduce((sum, bn) =>
    sum ? sum.add(new BigNumber(bn)) : new BigNumber(bn)
  , null);
}

function groupBy(array, by = _.identity) {
  let result = [];
  array.forEach(e => {
    if ((result[result.length-1] || [])[0] && by((result[result.length-1] || [])[0]) == by(e))
      result[result.length-1].push(e);
    else
      result.push([e]);
  });
  return result;
}

function mapWithPreviousResult(array, mapper = _.identity) {
  return array.reduce(({prevRes, result}, e) => {
    const res = mapper(e, prevRes);
    return {prevRes: res, result: result.concat([res])};
  }, {prevRes: null, result: []}).result;
}

const depthChartData = createSelector(
  offersBids,
  offersAsks,
  (bids, asks) => {
    const askPrices = _.sortedUniq(asks.map(ask => ask.ask_price_sort));
    const askGroups = groupBy(asks, ask => ask.ask_price_sort);
    const askAmounts = {
      quote: upSum(askGroups.map(group => bigSum(group.map(ask => ask.buyHowMuch.toString())))),
      base: upSum(askGroups.map(group => bigSum(group.map(ask => ask.sellHowMuch.toString())))),
    };

    const bidPrices = _.sortedUniq(bids.map(bid => bid.bid_price_sort));
    const bidGroups = groupBy(bids, bid => bid.bid_price_sort);
    const bidAmounts = {
      quote: downSum(bidGroups.map(group => bigSum(group.map(bid => bid.sellHowMuch.toString())))),
      base: downSum(bidGroups.map(group => bigSum(group.map(bid => bid.buyHowMuch.toString())))),
    };

    const vals = _.uniq(bidPrices.concat(askPrices).sort((a, b) => new BigNumber(a.toString()).lt(new BigNumber(b.toString())) ? -1 : 1));

    const askAmountsData = mapWithPreviousResult(vals, (val, prevResult) => {
      const index = askPrices.indexOf(val);
      if (index !== -1) {
        // If there is a specific value for the price in asks, we add it
        return {
          graph: {x: val, y: web3.fromWei(askAmounts.quote[index]).toFixed(3)},
          tooltip: {quote: askAmounts.quote[index], base: askAmounts.base[index]},
        };
      } else if (askPrices.length === 0 ||
        (new BigNumber(val.toString())).lt((new BigNumber(askPrices[0].toString()))) ||
        (new BigNumber(val.toString())).gt((new BigNumber(askPrices[askPrices.length - 1].toString())))) {
        // If the price is lower or higher than the asks range there is not value to print in the graph
        return {
          graph: {x: val, y: null},
          tooltip: {quote: null, base: null},
        };
      } else {
        // If there is not an ask amount for this price, we need to add the previous amount
        return {
          graph: {x: val, y: prevResult.graph.y},
          tooltip: prevResult.tooltip,
        };
      }
    });

    const bidAmountsData = mapWithPreviousResult(vals.slice().reverse(), (val, prevResult) => {
      const index = bidPrices.indexOf(val);
      if (index !== -1) {
        // If there is a specific value for the price in bids, we add it
        return {
          graph: {x: val, y: web3.fromWei(bidAmounts.quote[index]).toFixed(3)},
          tooltip: {quote: bidAmounts.quote[index], base: bidAmounts.base[index]},
        };
      } else if (bidPrices.length === 0 ||
        (new BigNumber(val.toString())).lt((new BigNumber(bidPrices[0].toString()))) ||
        (new BigNumber(val.toString())).gt((new BigNumber(bidPrices[bidPrices.length - 1].toString())))) {
        // If the price is lower or higher than the bids range there is not value to print in the graph
        return {
          graph: {x: val, y: null},
          tooltip: {quote: null, base: null},
        };
      } else {
        // If there is not a bid amount for this price, we need to add the next available amount
        return {
          graph: {x: val, y: prevResult.graph.y},
          tooltip: prevResult.tooltip,
        };
      }
    });

    return {vals, askAmountsData, bidAmountsData: bidAmountsData.reverse()}
  }
)

const depthChartLabels = createSelector(
  depthChartData,
  (depthChartData) => depthChartData.vals
)

const depthChartValues = createSelector(
  depthChartData,
  (depthChartData) => ({
    buy: depthChartData.bidAmountsData.map(d => d.graph),
    sell: depthChartData.askAmountsData.map(d => d.graph),
  })
)

const depthChartTooltips = createSelector(
  depthChartData,
  (depthChartData) => ({
    buy: depthChartData.bidAmountsData.map(v => ({
      base: new BigNumber(web3.fromWei(v.tooltip.base)).toFormat(2),
      quote: new BigNumber(web3.fromWei(v.tooltip.quote)).toFormat(2),
    })),
    sell: depthChartData.askAmountsData.map(v => ({
      base: new BigNumber(web3.fromWei(v.tooltip.base)).toFormat(2),
      quote: new BigNumber(web3.fromWei(v.tooltip.quote)).toFormat(2),
    })),
  })
)


export default {
  priceChartLabels,
  priceChartValues,
  volumeChartLabels,
  volumeChartValues,
  volumeChartTooltips,
  depthChartLabels,
  depthChartValues,
  depthChartTooltips,
}