Skip to content

Commit

Permalink
Move getUpdateChartResult, getCandleData, getTimeFromTickIndex to Cha…
Browse files Browse the repository at this point in the history
…rtCalculations

Make maxTicks static and rename to MAX_TICKS
  • Loading branch information
chimp1984 committed Nov 4, 2021
1 parent d9e99c7 commit fb0f91a
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 151 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,50 @@

package bisq.desktop.main.market.trades;

import bisq.desktop.main.market.trades.charts.CandleData;
import bisq.desktop.util.DisplayUtils;

import bisq.core.locale.CurrencyUtil;
import bisq.core.monetary.Altcoin;
import bisq.core.trade.statistics.TradeStatistics3;

import bisq.common.util.MathUtils;

import org.bitcoinj.core.Coin;

import com.google.common.annotations.VisibleForTesting;

import javafx.scene.chart.XYChart;

import javafx.util.Pair;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;

import lombok.Getter;

public class ChartCalculations {
static final ZoneId ZONE_ID = ZoneId.systemDefault();


///////////////////////////////////////////////////////////////////////////////////////////
// Async
///////////////////////////////////////////////////////////////////////////////////////////

static CompletableFuture<Map<TradesChartsViewModel.TickUnit, Map<Long, Long>>> getUsdAveragePriceMapsPerTickUnit(Set<TradeStatistics3> tradeStatisticsSet) {
return CompletableFuture.supplyAsync(() -> {
Map<TradesChartsViewModel.TickUnit, Map<Long, Long>> usdAveragePriceMapsPerTickUnit = new HashMap<>();
Expand Down Expand Up @@ -77,18 +99,96 @@ static CompletableFuture<List<TradeStatistics3>> getTradeStatisticsForCurrency(S
});
}

static long getAveragePrice(List<TradeStatistics3> tradeStatisticsList) {
long accumulatedAmount = 0;
long accumulatedVolume = 0;
for (TradeStatistics3 tradeStatistics : tradeStatisticsList) {
accumulatedAmount += tradeStatistics.getAmount();
accumulatedVolume += tradeStatistics.getTradeVolume().getValue();
static UpdateChartResult getUpdateChartResult(List<TradeStatistics3> tradeStatisticsByCurrency,
TradesChartsViewModel.TickUnit tickUnit,
Map<TradesChartsViewModel.TickUnit, Map<Long, Long>> usdAveragePriceMapsPerTickUnit,
String currencyCode) {
// Generate date range and create sets for all ticks
Map<Long, Pair<Date, Set<TradeStatistics3>>> itemsPerInterval = getItemsPerInterval(tradeStatisticsByCurrency, tickUnit);

Map<Long, Long> usdAveragePriceMap = usdAveragePriceMapsPerTickUnit.get(tickUnit);
AtomicLong averageUsdPrice = new AtomicLong(0);

// create CandleData for defined time interval
List<CandleData> candleDataList = itemsPerInterval.entrySet().stream()
.filter(entry -> entry.getKey() >= 0 && !entry.getValue().getValue().isEmpty())
.map(entry -> {
long tickStartDate = entry.getValue().getKey().getTime();
// If we don't have a price we take the previous one
if (usdAveragePriceMap.containsKey(tickStartDate)) {
averageUsdPrice.set(usdAveragePriceMap.get(tickStartDate));
}
return getCandleData(entry.getKey(), entry.getValue().getValue(), averageUsdPrice.get(), tickUnit, currencyCode, itemsPerInterval);
})
.sorted(Comparator.comparingLong(o -> o.tick))
.collect(Collectors.toList());

List<XYChart.Data<Number, Number>> priceItems = candleDataList.stream()
.map(e -> new XYChart.Data<Number, Number>(e.tick, e.open, e))
.collect(Collectors.toList());

List<XYChart.Data<Number, Number>> volumeItems = candleDataList.stream()
.map(candleData -> new XYChart.Data<Number, Number>(candleData.tick, candleData.accumulatedAmount, candleData))
.collect(Collectors.toList());

List<XYChart.Data<Number, Number>> volumeInUsdItems = candleDataList.stream()
.map(candleData -> new XYChart.Data<Number, Number>(candleData.tick, candleData.volumeInUsd, candleData))
.collect(Collectors.toList());

return new UpdateChartResult(itemsPerInterval, priceItems, volumeItems, volumeInUsdItems);
}

@Getter
static class UpdateChartResult {
private final Map<Long, Pair<Date, Set<TradeStatistics3>>> itemsPerInterval;
private final List<XYChart.Data<Number, Number>> priceItems;
private final List<XYChart.Data<Number, Number>> volumeItems;
private final List<XYChart.Data<Number, Number>> volumeInUsdItems;

public UpdateChartResult(Map<Long, Pair<Date, Set<TradeStatistics3>>> itemsPerInterval,
List<XYChart.Data<Number, Number>> priceItems,
List<XYChart.Data<Number, Number>> volumeItems,
List<XYChart.Data<Number, Number>> volumeInUsdItems) {

this.itemsPerInterval = itemsPerInterval;
this.priceItems = priceItems;
this.volumeItems = volumeItems;
this.volumeInUsdItems = volumeInUsdItems;
}
}

double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, Coin.SMALLEST_UNIT_EXPONENT);
return MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount);

///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////

static Map<Long, Pair<Date, Set<TradeStatistics3>>> getItemsPerInterval(List<TradeStatistics3> tradeStatisticsByCurrency,
TradesChartsViewModel.TickUnit tickUnit) {
// Generate date range and create sets for all ticks
Map<Long, Pair<Date, Set<TradeStatistics3>>> itemsPerInterval = new HashMap<>();
Date time = new Date();
for (long i = TradesChartsViewModel.MAX_TICKS + 1; i >= 0; --i) {
Pair<Date, Set<TradeStatistics3>> pair = new Pair<>((Date) time.clone(), new HashSet<>());
itemsPerInterval.put(i, pair);
// We adjust the time for the next iteration
time.setTime(time.getTime() - 1);
time = roundToTick(time, tickUnit);
}

// Get all entries for the defined time interval
tradeStatisticsByCurrency.forEach(tradeStatistics -> {
for (long i = TradesChartsViewModel.MAX_TICKS; i > 0; --i) {
Pair<Date, Set<TradeStatistics3>> pair = itemsPerInterval.get(i);
if (tradeStatistics.getDate().after(pair.getKey())) {
pair.getValue().add(tradeStatistics);
break;
}
}
});
return itemsPerInterval;
}


static Date roundToTick(LocalDateTime localDate, TradesChartsViewModel.TickUnit tickUnit) {
switch (tickUnit) {
case YEAR:
Expand All @@ -109,4 +209,91 @@ static Date roundToTick(LocalDateTime localDate, TradesChartsViewModel.TickUnit
return Date.from(localDate.atZone(ZONE_ID).toInstant());
}
}

static Date roundToTick(Date time, TradesChartsViewModel.TickUnit tickUnit) {
return roundToTick(time.toInstant().atZone(ChartCalculations.ZONE_ID).toLocalDateTime(), tickUnit);
}

private static long getAveragePrice(List<TradeStatistics3> tradeStatisticsList) {
long accumulatedAmount = 0;
long accumulatedVolume = 0;
for (TradeStatistics3 tradeStatistics : tradeStatisticsList) {
accumulatedAmount += tradeStatistics.getAmount();
accumulatedVolume += tradeStatistics.getTradeVolume().getValue();
}

double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, Coin.SMALLEST_UNIT_EXPONENT);
return MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount);
}

@VisibleForTesting
static CandleData getCandleData(long tick, Set<TradeStatistics3> set,
long averageUsdPrice,
TradesChartsViewModel.TickUnit tickUnit,
String currencyCode,
Map<Long, Pair<Date, Set<TradeStatistics3>>> itemsPerInterval) {
long open = 0;
long close = 0;
long high = 0;
long low = 0;
long accumulatedVolume = 0;
long accumulatedAmount = 0;
long numTrades = set.size();
List<Long> tradePrices = new ArrayList<>();
for (TradeStatistics3 item : set) {
long tradePriceAsLong = item.getTradePrice().getValue();
// Previously a check was done which inverted the low and high for cryptocurrencies.
low = (low != 0) ? Math.min(low, tradePriceAsLong) : tradePriceAsLong;
high = (high != 0) ? Math.max(high, tradePriceAsLong) : tradePriceAsLong;

accumulatedVolume += item.getTradeVolume().getValue();
accumulatedAmount += item.getTradeAmount().getValue();
tradePrices.add(tradePriceAsLong);
}
Collections.sort(tradePrices);

List<TradeStatistics3> list = new ArrayList<>(set);
list.sort(Comparator.comparingLong(TradeStatistics3::getDateAsLong));
if (list.size() > 0) {
open = list.get(0).getTradePrice().getValue();
close = list.get(list.size() - 1).getTradePrice().getValue();
}

long averagePrice;
Long[] prices = new Long[tradePrices.size()];
tradePrices.toArray(prices);
long medianPrice = MathUtils.getMedian(prices);
boolean isBullish;
if (CurrencyUtil.isCryptoCurrency(currencyCode)) {
isBullish = close < open;
double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, Altcoin.SMALLEST_UNIT_EXPONENT);
averagePrice = MathUtils.roundDoubleToLong(accumulatedAmountAsDouble / (double) accumulatedVolume);
} else {
isBullish = close > open;
double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, Coin.SMALLEST_UNIT_EXPONENT);
averagePrice = MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount);
}

Date dateFrom = new Date(getTimeFromTickIndex(tick, itemsPerInterval));
Date dateTo = new Date(getTimeFromTickIndex(tick + 1, itemsPerInterval));
String dateString = tickUnit.ordinal() > TradesChartsViewModel.TickUnit.DAY.ordinal() ?
DisplayUtils.formatDateTimeSpan(dateFrom, dateTo) :
DisplayUtils.formatDate(dateFrom) + " - " + DisplayUtils.formatDate(dateTo);

// We do not need precision, so we scale down before multiplication otherwise we could get an overflow.
averageUsdPrice = (long) MathUtils.scaleDownByPowerOf10((double) averageUsdPrice, 4);
long volumeInUsd = averageUsdPrice * (long) MathUtils.scaleDownByPowerOf10((double) accumulatedAmount, 4);
// We store USD value without decimals as its only total volume, no precision is needed.
volumeInUsd = (long) MathUtils.scaleDownByPowerOf10((double) volumeInUsd, 4);
return new CandleData(tick, open, close, high, low, averagePrice, medianPrice, accumulatedAmount, accumulatedVolume,
numTrades, isBullish, dateString, volumeInUsd);
}

static long getTimeFromTickIndex(long tick, Map<Long, Pair<Date, Set<TradeStatistics3>>> itemsPerInterval) {
if (tick > TradesChartsViewModel.MAX_TICKS + 1 ||
itemsPerInterval.get(tick) == null) {
return 0;
}
return itemsPerInterval.get(tick).getKey().getTime();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ private void exportToCsv() {
private void createCharts() {
priceSeries = new XYChart.Series<>();

priceAxisX = new NumberAxis(0, model.maxTicks + 1, 1);
priceAxisX = new NumberAxis(0, TradesChartsViewModel.MAX_TICKS + 1, 1);
priceAxisX.setTickUnit(4);
priceAxisX.setMinorTickCount(4);
priceAxisX.setMinorTickVisible(true);
Expand Down Expand Up @@ -539,11 +539,11 @@ public Number fromString(String string) {

priceChartPane.getChildren().add(priceChart);

volumeAxisX = new NumberAxis(0, model.maxTicks + 1, 1);
volumeAxisX = new NumberAxis(0, TradesChartsViewModel.MAX_TICKS + 1, 1);
volumeAxisY = new NumberAxis();
volumeChart = getVolumeChart(volumeAxisX, volumeAxisY, volumeSeries, "BTC");

volumeInUsdAxisX = new NumberAxis(0, model.maxTicks + 1, 1);
volumeInUsdAxisX = new NumberAxis(0, TradesChartsViewModel.MAX_TICKS + 1, 1);
NumberAxis volumeInUsdAxisY = new NumberAxis();
volumeInUsdChart = getVolumeChart(volumeInUsdAxisX, volumeInUsdAxisY, volumeInUsdSeries, "USD");
volumeInUsdChart.setVisible(false);
Expand Down Expand Up @@ -650,7 +650,7 @@ public String toString(Number object) {
long index = MathUtils.doubleToLong((double) object);
// The last tick is on the chart edge, it is not well spaced with
// the previous tick and interferes with its label.
if (model.maxTicks + 1 == index) return "";
if (TradesChartsViewModel.MAX_TICKS + 1 == index) return "";

long time = model.getTimeFromTickIndex(index);
String fmt = "";
Expand Down
Loading

0 comments on commit fb0f91a

Please sign in to comment.