mirror of
https://github.com/bisq-network/bisq.git
synced 2025-01-19 14:44:10 +01:00
Merge pull request #3890 from dmos62/dao-facts-and-figures-outlier-resistance
Improve readability of the daily burnt BSQ chart
This commit is contained in:
commit
a29d4903a6
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.common.util;
|
||||
|
||||
import java.util.DoubleSummaryStatistics;
|
||||
|
||||
/* Adds logic to DoubleSummaryStatistics for keeping track of sum of squares
|
||||
* and computing population variance and population standard deviation.
|
||||
* Kahan summation algorithm (for `getSumOfSquares`) sourced from the DoubleSummaryStatistics class.
|
||||
* Incremental variance algorithm sourced from https://math.stackexchange.com/a/1379804/316756
|
||||
*/
|
||||
public class DoubleSummaryStatisticsWithStdDev extends DoubleSummaryStatistics {
|
||||
private double sumOfSquares;
|
||||
private double sumOfSquaresCompensation; // Low order bits of sum of squares
|
||||
private double simpleSumOfSquares; // Used to compute right sum of squares for non-finite inputs
|
||||
|
||||
@Override
|
||||
public void accept(double value) {
|
||||
super.accept(value);
|
||||
double valueSquared = value * value;
|
||||
simpleSumOfSquares += valueSquared;
|
||||
sumOfSquaresWithCompensation(valueSquared);
|
||||
}
|
||||
|
||||
public void combine(DoubleSummaryStatisticsWithStdDev other) {
|
||||
super.combine(other);
|
||||
simpleSumOfSquares += other.simpleSumOfSquares;
|
||||
sumOfSquaresWithCompensation(other.sumOfSquares);
|
||||
sumOfSquaresWithCompensation(other.sumOfSquaresCompensation);
|
||||
}
|
||||
|
||||
/* Incorporate a new squared double value using Kahan summation /
|
||||
* compensated summation.
|
||||
*/
|
||||
private void sumOfSquaresWithCompensation(double valueSquared) {
|
||||
double tmp = valueSquared - sumOfSquaresCompensation;
|
||||
double velvel = sumOfSquares + tmp; // Little wolf of rounding error
|
||||
sumOfSquaresCompensation = (velvel - sumOfSquares) - tmp;
|
||||
sumOfSquares = velvel;
|
||||
}
|
||||
|
||||
private double getSumOfSquares() {
|
||||
// Better error bounds to add both terms as the final sum of squares
|
||||
double tmp = sumOfSquares + sumOfSquaresCompensation;
|
||||
if (Double.isNaN(tmp) && Double.isInfinite(simpleSumOfSquares))
|
||||
// If the compensated sum of squares is spuriously NaN from
|
||||
// accumulating one or more same-signed infinite values,
|
||||
// return the correctly-signed infinity stored in
|
||||
// simpleSumOfSquares.
|
||||
return simpleSumOfSquares;
|
||||
else
|
||||
return tmp;
|
||||
}
|
||||
|
||||
private double getVariance() {
|
||||
double sumOfSquares = getSumOfSquares();
|
||||
long count = getCount();
|
||||
double mean = getAverage();
|
||||
return (sumOfSquares / count) - (mean * mean);
|
||||
}
|
||||
|
||||
public final double getStandardDeviation() {
|
||||
double variance = getVariance();
|
||||
return Math.sqrt(variance);
|
||||
}
|
||||
|
||||
}
|
@ -2252,6 +2252,8 @@ dao.factsAndFigures.supply.compRequestIssueAmount=BSQ issued for compensation re
|
||||
dao.factsAndFigures.supply.reimbursementAmount=BSQ issued for reimbursement requests
|
||||
|
||||
dao.factsAndFigures.supply.burnt=BSQ burnt
|
||||
dao.factsAndFigures.supply.burntMovingAverage=15 days moving average
|
||||
dao.factsAndFigures.supply.burntZoomToInliers=Zoom to inliers
|
||||
|
||||
dao.factsAndFigures.supply.locked=Global state of locked BSQ
|
||||
dao.factsAndFigures.supply.totalLockedUpAmount=Locked up in bonds
|
||||
|
@ -1621,6 +1621,17 @@ textfield */
|
||||
-fx-stroke: -bs-buy;
|
||||
}
|
||||
|
||||
/* The .chart-line-symbol rules change the color of the legend symbol */
|
||||
#charts-dao .default-color0.chart-series-line { -fx-stroke: -bs-chart-dao-line1; }
|
||||
#charts-dao .default-color0.chart-line-symbol { -fx-background-color: -bs-chart-dao-line1, -bs-background-color; }
|
||||
|
||||
#charts-dao .default-color1.chart-series-line { -fx-stroke: -bs-chart-dao-line2; }
|
||||
#charts-dao .default-color1.chart-line-symbol { -fx-background-color: -bs-chart-dao-line2, -bs-background-color; }
|
||||
|
||||
#charts-dao .chart-series-line {
|
||||
-fx-stroke-width: 1px;
|
||||
}
|
||||
|
||||
#charts .default-color0.chart-series-area-fill {
|
||||
-fx-fill: -bs-sell-transparent;
|
||||
}
|
||||
|
@ -20,7 +20,9 @@ package bisq.desktop.main.dao.economy.supply;
|
||||
import bisq.desktop.common.view.ActivatableView;
|
||||
import bisq.desktop.common.view.FxmlView;
|
||||
import bisq.desktop.components.TitledGroupBg;
|
||||
import bisq.desktop.util.AxisInlierUtils;
|
||||
import bisq.desktop.util.Layout;
|
||||
import bisq.desktop.util.MovingAverageUtils;
|
||||
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.dao.state.DaoStateListener;
|
||||
@ -39,18 +41,24 @@ import org.bitcoinj.core.Coin;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.chart.AreaChart;
|
||||
import javafx.scene.chart.LineChart;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import javafx.scene.chart.XYChart;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Side;
|
||||
|
||||
import javafx.collections.ListChangeListener;
|
||||
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.time.LocalDate;
|
||||
@ -68,9 +76,15 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Spliterators.AbstractSpliterator;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import static bisq.desktop.util.FormBuilder.addSlideToggleButton;
|
||||
import static bisq.desktop.util.FormBuilder.addTitledGroupBg;
|
||||
import static bisq.desktop.util.FormBuilder.addTopLabelReadOnlyTextField;
|
||||
|
||||
@ -92,7 +106,17 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
|
||||
private TextField genesisIssueAmountTextField, compRequestIssueAmountTextField, reimbursementAmountTextField,
|
||||
totalBurntFeeAmountTextField, totalLockedUpAmountTextField, totalUnlockingAmountTextField,
|
||||
totalUnlockedAmountTextField, totalConfiscatedAmountTextField, totalAmountOfInvalidatedBsqTextField;
|
||||
private XYChart.Series<Number, Number> seriesBSQIssued, seriesBSQBurnt;
|
||||
private XYChart.Series<Number, Number> seriesBSQIssued, seriesBSQBurnt, seriesBSQBurntMA;
|
||||
private ListChangeListener<XYChart.Data<Number, Number>> changeListenerBSQBurnt;
|
||||
private NumberAxis yAxisBSQBurnt;
|
||||
|
||||
private ToggleButton zoomToInliersSlide;
|
||||
private boolean isZoomingToInliers = false;
|
||||
|
||||
// Parameters for zooming to inliers; explanations in AxisInlierUtils.
|
||||
private int chartMaxNumberOfTicks = 10;
|
||||
private double chartPercentToTrim = 5;
|
||||
private double chartHowManyStdDevsConstituteOutlier = 10;
|
||||
|
||||
private static final Map<String, TemporalAdjuster> ADJUSTERS = new HashMap<>();
|
||||
|
||||
@ -112,12 +136,12 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
|
||||
ADJUSTERS.put(MONTH, TemporalAdjusters.firstDayOfMonth());
|
||||
ADJUSTERS.put(DAY, TemporalAdjusters.ofDateAdjuster(d -> d));
|
||||
|
||||
createSupplyIncreasedInformation();
|
||||
createSupplyReducedInformation();
|
||||
|
||||
createSupplyLockedInformation();
|
||||
}
|
||||
|
||||
@ -125,12 +149,22 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
|
||||
protected void activate() {
|
||||
daoFacade.addBsqStateListener(this);
|
||||
|
||||
if (isZoomingToInliers) {
|
||||
activateZoomingToInliers();
|
||||
}
|
||||
|
||||
updateWithBsqBlockChainData();
|
||||
|
||||
activateButtons();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deactivate() {
|
||||
daoFacade.removeBsqStateListener(this);
|
||||
|
||||
deactivateZoomingToInliers();
|
||||
|
||||
deactivateButtons();
|
||||
}
|
||||
|
||||
|
||||
@ -163,7 +197,11 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
|
||||
|
||||
|
||||
seriesBSQIssued = new XYChart.Series<>();
|
||||
createChart(seriesBSQIssued, Res.get("dao.factsAndFigures.supply.issued"), "MMM uu");
|
||||
|
||||
var chart = createBSQIssuedChart(seriesBSQIssued);
|
||||
|
||||
var chartPane = wrapInChartPane(chart);
|
||||
root.getChildren().add(chartPane);
|
||||
}
|
||||
|
||||
private void createSupplyReducedInformation() {
|
||||
@ -174,8 +212,16 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
|
||||
totalAmountOfInvalidatedBsqTextField = addTopLabelReadOnlyTextField(root, gridRow, 1,
|
||||
Res.get("dao.factsAndFigures.supply.invalidTxs"), Layout.FIRST_ROW_AND_GROUP_DISTANCE).second;
|
||||
|
||||
var buttonTitle = Res.get("dao.factsAndFigures.supply.burntZoomToInliers");
|
||||
zoomToInliersSlide = addSlideToggleButton(root, ++gridRow, buttonTitle);
|
||||
|
||||
seriesBSQBurnt = new XYChart.Series<>();
|
||||
createChart(seriesBSQBurnt, Res.get("dao.factsAndFigures.supply.burnt"), "d MMM");
|
||||
seriesBSQBurntMA = new XYChart.Series<>();
|
||||
|
||||
var chart = createBSQBurntChart(seriesBSQBurnt, seriesBSQBurntMA);
|
||||
|
||||
var chartPane = wrapInChartPane(chart);
|
||||
root.getChildren().add(chartPane);
|
||||
}
|
||||
|
||||
private void createSupplyLockedInformation() {
|
||||
@ -195,14 +241,88 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
|
||||
Res.get("dao.factsAndFigures.supply.totalConfiscatedAmount")).second;
|
||||
}
|
||||
|
||||
private void createChart(XYChart.Series<Number, Number> series, String seriesLabel, String datePattern) {
|
||||
private Node createBSQIssuedChart(XYChart.Series<Number, Number> series) {
|
||||
NumberAxis xAxis = new NumberAxis();
|
||||
xAxis.setForceZeroInRange(false);
|
||||
xAxis.setAutoRanging(true);
|
||||
xAxis.setTickLabelGap(6);
|
||||
xAxis.setTickMarkVisible(false);
|
||||
xAxis.setMinorTickVisible(false);
|
||||
xAxis.setTickLabelFormatter(new StringConverter<>() {
|
||||
configureAxis(xAxis);
|
||||
xAxis.setTickLabelFormatter(getTimestampTickLabelFormatter("MMM uu"));
|
||||
|
||||
NumberAxis yAxis = new NumberAxis();
|
||||
configureYAxis(yAxis);
|
||||
yAxis.setTickLabelFormatter(BSQPriceTickLabelFormatter);
|
||||
|
||||
AreaChart<Number, Number> chart = new AreaChart<>(xAxis, yAxis);
|
||||
configureChart(chart);
|
||||
|
||||
series.setName(Res.get("dao.factsAndFigures.supply.issued"));
|
||||
chart.getData().add(series);
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Node createBSQBurntChart(
|
||||
XYChart.Series<Number, Number> seriesBSQBurnt,
|
||||
XYChart.Series<Number, Number> seriesBSQBurntMA
|
||||
) {
|
||||
Supplier<NumberAxis> makeXAxis = () -> {
|
||||
NumberAxis xAxis = new NumberAxis();
|
||||
configureAxis(xAxis);
|
||||
xAxis.setTickLabelFormatter(getTimestampTickLabelFormatter("d MMM"));
|
||||
return xAxis;
|
||||
};
|
||||
|
||||
Supplier<NumberAxis> makeYAxis = () -> {
|
||||
NumberAxis yAxis = new NumberAxis();
|
||||
configureYAxis(yAxis);
|
||||
yAxis.setTickLabelFormatter(BSQPriceTickLabelFormatter);
|
||||
return yAxis;
|
||||
};
|
||||
|
||||
seriesBSQBurnt.setName(Res.get("dao.factsAndFigures.supply.burnt"));
|
||||
|
||||
var burntMALabel = Res.get("dao.factsAndFigures.supply.burntMovingAverage");
|
||||
seriesBSQBurntMA.setName(burntMALabel);
|
||||
|
||||
var yAxis = makeYAxis.get();
|
||||
initializeChangeListener(yAxis);
|
||||
|
||||
var chart = new LineChart<>(makeXAxis.get(), yAxis);
|
||||
|
||||
chart.getData().addAll(seriesBSQBurnt, seriesBSQBurntMA);
|
||||
|
||||
configureChart(chart);
|
||||
chart.setCreateSymbols(false);
|
||||
chart.setLegendVisible(true);
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
private void initializeChangeListener(NumberAxis axis) {
|
||||
// Keep a class-scope reference. Needed for switching between inliers-only and full chart.
|
||||
yAxisBSQBurnt = axis;
|
||||
|
||||
changeListenerBSQBurnt = AxisInlierUtils.getListenerThatZoomsToInliers(
|
||||
yAxisBSQBurnt, chartMaxNumberOfTicks, chartPercentToTrim, chartHowManyStdDevsConstituteOutlier);
|
||||
}
|
||||
|
||||
private void configureYAxis(NumberAxis axis) {
|
||||
configureAxis(axis);
|
||||
|
||||
axis.setForceZeroInRange(true);
|
||||
axis.setTickLabelGap(5);
|
||||
axis.setSide(Side.RIGHT);
|
||||
}
|
||||
|
||||
private void configureAxis(NumberAxis axis) {
|
||||
axis.setForceZeroInRange(false);
|
||||
axis.setAutoRanging(true);
|
||||
axis.setTickMarkVisible(false);
|
||||
axis.setMinorTickVisible(false);
|
||||
axis.setTickLabelGap(6);
|
||||
}
|
||||
|
||||
private StringConverter<Number> getTimestampTickLabelFormatter(String datePattern) {
|
||||
return new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Number timestamp) {
|
||||
LocalDateTime localDateTime = LocalDateTime.ofEpochSecond(timestamp.longValue(),
|
||||
@ -214,54 +334,48 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
|
||||
public Number fromString(String string) {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
NumberAxis yAxis = new NumberAxis();
|
||||
yAxis.setForceZeroInRange(false);
|
||||
yAxis.setSide(Side.RIGHT);
|
||||
yAxis.setAutoRanging(true);
|
||||
yAxis.setTickMarkVisible(false);
|
||||
yAxis.setMinorTickVisible(false);
|
||||
yAxis.setTickLabelGap(5);
|
||||
yAxis.setTickLabelFormatter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Number marketPrice) {
|
||||
return bsqFormatter.formatBSQSatoshisWithCode(marketPrice.longValue());
|
||||
}
|
||||
private StringConverter<Number> BSQPriceTickLabelFormatter =
|
||||
new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Number marketPrice) {
|
||||
return bsqFormatter.formatBSQSatoshisWithCode(marketPrice.longValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
series.setName(seriesLabel);
|
||||
|
||||
AreaChart<Number, Number> chart = new AreaChart<>(xAxis, yAxis);
|
||||
private <X, Y> void configureChart(XYChart<X, Y> chart) {
|
||||
chart.setLegendVisible(false);
|
||||
chart.setAnimated(false);
|
||||
chart.setId("charts-dao");
|
||||
chart.setMinHeight(250);
|
||||
chart.setPrefHeight(250);
|
||||
chart.setCreateSymbols(true);
|
||||
chart.setPadding(new Insets(0));
|
||||
chart.getData().add(series);
|
||||
|
||||
chart.setMinHeight(300);
|
||||
chart.setPrefHeight(300);
|
||||
chart.setPadding(new Insets(0));
|
||||
}
|
||||
|
||||
private Pane wrapInChartPane(Node child) {
|
||||
AnchorPane chartPane = new AnchorPane();
|
||||
chartPane.getStyleClass().add("chart-pane");
|
||||
|
||||
AnchorPane.setTopAnchor(chart, 15d);
|
||||
AnchorPane.setBottomAnchor(chart, 10d);
|
||||
AnchorPane.setLeftAnchor(chart, 25d);
|
||||
AnchorPane.setRightAnchor(chart, 10d);
|
||||
AnchorPane.setTopAnchor(child, 15d);
|
||||
AnchorPane.setBottomAnchor(child, 10d);
|
||||
AnchorPane.setLeftAnchor(child, 25d);
|
||||
AnchorPane.setRightAnchor(child, 10d);
|
||||
|
||||
chartPane.getChildren().add(chart);
|
||||
chartPane.getChildren().add(child);
|
||||
|
||||
GridPane.setColumnSpan(chartPane, 2);
|
||||
GridPane.setRowIndex(chartPane, ++gridRow);
|
||||
GridPane.setMargin(chartPane, new Insets(10, 0, 0, 0));
|
||||
|
||||
root.getChildren().add(chartPane);
|
||||
return chartPane;
|
||||
}
|
||||
|
||||
private void updateWithBsqBlockChainData() {
|
||||
@ -288,11 +402,16 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
|
||||
String minusSign = totalAmountOfInvalidatedBsq.isPositive() ? "-" : "";
|
||||
totalAmountOfInvalidatedBsqTextField.setText(minusSign + bsqFormatter.formatAmountWithGroupSeparatorAndCode(totalAmountOfInvalidatedBsq));
|
||||
|
||||
updateCharts();
|
||||
updateChartSeries();
|
||||
}
|
||||
|
||||
private void updateCharts() {
|
||||
seriesBSQIssued.getData().clear();
|
||||
private void updateChartSeries() {
|
||||
var updatedBurntBsq = updateBSQBurnt();
|
||||
updateBSQBurntMA(updatedBurntBsq);
|
||||
updateBSQIssued();
|
||||
}
|
||||
|
||||
private List<XYChart.Data<Number, Number>> updateBSQBurnt() {
|
||||
seriesBSQBurnt.getData().clear();
|
||||
|
||||
Set<Tx> burntTxs = new HashSet<>(daoStateService.getBurntFeeTxs());
|
||||
@ -306,16 +425,61 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
|
||||
List<XYChart.Data<Number, Number>> updatedBurntBsq = burntBsqByDay.keySet().stream()
|
||||
.map(date -> {
|
||||
ZonedDateTime zonedDateTime = date.atStartOfDay(ZoneId.systemDefault());
|
||||
return new XYChart.Data<Number, Number>(zonedDateTime.toInstant().getEpochSecond(), burntBsqByDay.get(date)
|
||||
.stream()
|
||||
.mapToDouble(Tx::getBurntBsq)
|
||||
.sum()
|
||||
return new XYChart.Data<Number, Number>(
|
||||
zonedDateTime.toInstant().getEpochSecond(),
|
||||
burntBsqByDay.get(date)
|
||||
.stream()
|
||||
.mapToDouble(Tx::getBurntBsq)
|
||||
.sum()
|
||||
);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
seriesBSQBurnt.getData().setAll(updatedBurntBsq);
|
||||
|
||||
return updatedBurntBsq;
|
||||
}
|
||||
|
||||
private void updateBSQBurntMA(List<XYChart.Data<Number, Number>> updatedBurntBsq) {
|
||||
seriesBSQBurntMA.getData().clear();
|
||||
|
||||
Comparator<Number> compareXChronology =
|
||||
Comparator.comparingInt(Number::intValue);
|
||||
|
||||
Comparator<XYChart.Data<Number, Number>> compareXyDataChronology =
|
||||
(xyData1, xyData2) ->
|
||||
compareXChronology.compare(
|
||||
xyData1.getXValue(),
|
||||
xyData2.getXValue());
|
||||
|
||||
var sortedUpdatedBurntBsq = updatedBurntBsq
|
||||
.stream()
|
||||
.sorted(compareXyDataChronology)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
var burntBsqXValues = sortedUpdatedBurntBsq.stream().map(XYChart.Data::getXValue);
|
||||
var burntBsqYValues = sortedUpdatedBurntBsq.stream().map(XYChart.Data::getYValue);
|
||||
|
||||
var maPeriod = 15;
|
||||
var burntBsqMAYValues =
|
||||
MovingAverageUtils.simpleMovingAverage(
|
||||
burntBsqYValues,
|
||||
maPeriod);
|
||||
|
||||
BiFunction<Number, Double, XYChart.Data<Number, Number>> xyToXyData =
|
||||
XYChart.Data::new;
|
||||
|
||||
List<XYChart.Data<Number, Number>> burntBsqMA =
|
||||
zip(burntBsqXValues, burntBsqMAYValues, xyToXyData)
|
||||
.filter(xyData -> Double.isFinite(xyData.getYValue().doubleValue()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
seriesBSQBurntMA.getData().setAll(burntBsqMA);
|
||||
}
|
||||
|
||||
private void updateBSQIssued() {
|
||||
seriesBSQIssued.getData().clear();
|
||||
|
||||
Stream<Issuance> bsqByCompensation = daoStateService.getIssuanceSet(IssuanceType.COMPENSATION).stream()
|
||||
.sorted(Comparator.comparing(Issuance::getChainHeight));
|
||||
|
||||
@ -338,5 +502,77 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
|
||||
|
||||
seriesBSQIssued.getData().setAll(updatedAddedBSQ);
|
||||
}
|
||||
}
|
||||
|
||||
private void activateButtons() {
|
||||
zoomToInliersSlide.setSelected(isZoomingToInliers);
|
||||
zoomToInliersSlide.setOnAction(e -> handleZoomToInliersSlide(!isZoomingToInliers));
|
||||
}
|
||||
|
||||
private void deactivateButtons() {
|
||||
zoomToInliersSlide.setOnAction(null);
|
||||
}
|
||||
|
||||
private void handleZoomToInliersSlide(boolean shouldActivate) {
|
||||
isZoomingToInliers = !isZoomingToInliers;
|
||||
if (shouldActivate) {
|
||||
activateZoomingToInliers();
|
||||
} else {
|
||||
deactivateZoomingToInliers();
|
||||
}
|
||||
}
|
||||
|
||||
private void activateZoomingToInliers() {
|
||||
seriesBSQBurnt.getData().addListener(changeListenerBSQBurnt);
|
||||
|
||||
// Initial zoom has to be triggered manually; otherwise, it
|
||||
// would be triggered only on a change event in the series
|
||||
triggerZoomToInliers();
|
||||
}
|
||||
|
||||
private void deactivateZoomingToInliers() {
|
||||
seriesBSQBurnt.getData().removeListener(changeListenerBSQBurnt);
|
||||
|
||||
// Reactivate automatic ranging
|
||||
yAxisBSQBurnt.autoRangingProperty().set(true);
|
||||
}
|
||||
|
||||
private void triggerZoomToInliers() {
|
||||
var xyValues = seriesBSQBurnt.getData();
|
||||
AxisInlierUtils.zoomToInliers(
|
||||
yAxisBSQBurnt,
|
||||
xyValues,
|
||||
chartMaxNumberOfTicks,
|
||||
chartPercentToTrim,
|
||||
chartHowManyStdDevsConstituteOutlier
|
||||
);
|
||||
}
|
||||
|
||||
// When Guava version is bumped to at least 21.0,
|
||||
// can be replaced with com.google.common.collect.Streams.zip
|
||||
public static <L, R, T> Stream<T> zip(
|
||||
Stream<L> leftStream,
|
||||
Stream<R> rightStream,
|
||||
BiFunction<L, R, T> combiner
|
||||
) {
|
||||
var lefts = leftStream.spliterator();
|
||||
var rights = rightStream.spliterator();
|
||||
var spliterator =
|
||||
new AbstractSpliterator<T>(
|
||||
Long.min(
|
||||
lefts.estimateSize(),
|
||||
rights.estimateSize()
|
||||
),
|
||||
lefts.characteristics() & rights.characteristics()
|
||||
) {
|
||||
@Override
|
||||
public boolean tryAdvance(Consumer<? super T> action) {
|
||||
return lefts.tryAdvance(
|
||||
left -> rights.tryAdvance(
|
||||
right -> action.accept(combiner.apply(left, right))
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
return StreamSupport.stream(spliterator, false);
|
||||
}
|
||||
}
|
||||
|
@ -128,6 +128,10 @@
|
||||
-bs-chart-tick: rgba(255, 255, 255, 0.7);
|
||||
-bs-chart-lines: rgba(0, 0, 0, 0.3);
|
||||
-bs-white: white;
|
||||
|
||||
/* dao chart colors */
|
||||
-bs-chart-dao-line1: -bs-color-green-5;
|
||||
-bs-chart-dao-line2: -bs-color-blue-2;
|
||||
}
|
||||
|
||||
/* list view */
|
||||
|
@ -101,4 +101,8 @@
|
||||
-fx-default-button: derive(-fx-accent, 95%);
|
||||
-bs-progress-bar-track: #e0e0e0;
|
||||
-bs-white: white;
|
||||
|
||||
/* dao chart colors */
|
||||
-bs-chart-dao-line1: -bs-color-green-3;
|
||||
-bs-chart-dao-line2: -bs-color-blue-5;
|
||||
}
|
||||
|
310
desktop/src/main/java/bisq/desktop/util/AxisInlierUtils.java
Normal file
310
desktop/src/main/java/bisq/desktop/util/AxisInlierUtils.java
Normal file
@ -0,0 +1,310 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.desktop.util;
|
||||
|
||||
import bisq.common.util.DoubleSummaryStatisticsWithStdDev;
|
||||
import bisq.common.util.Tuple2;
|
||||
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import javafx.scene.chart.XYChart;
|
||||
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.DoubleSummaryStatistics;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class AxisInlierUtils {
|
||||
|
||||
/* Returns a ListChangeListener that is meant to be attached to an
|
||||
* ObservableList. On event, it triggers a recalculation of a provided
|
||||
* axis' range so as to zoom in on inliers.
|
||||
*/
|
||||
public static ListChangeListener<XYChart.Data<Number, Number>> getListenerThatZoomsToInliers(
|
||||
NumberAxis axis,
|
||||
int maxNumberOfTicks,
|
||||
double percentToTrim,
|
||||
double howManyStdDevsConstituteOutlier
|
||||
) {
|
||||
return change -> {
|
||||
boolean axisHasBeenInitialized = axis != null;
|
||||
if (axisHasBeenInitialized) {
|
||||
zoomToInliers(
|
||||
axis,
|
||||
change.getList(),
|
||||
maxNumberOfTicks,
|
||||
percentToTrim,
|
||||
howManyStdDevsConstituteOutlier
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* Applies the inlier range to the axis bounds and sets an appropriate tick-unit.
|
||||
* The methods describing the arguments passed here are `computeReferenceTickUnit`,
|
||||
* `trim`, and `computeInlierThreshold`.
|
||||
*/
|
||||
public static void zoomToInliers(
|
||||
NumberAxis yAxis,
|
||||
ObservableList<? extends XYChart.Data<Number, Number>> xyValues,
|
||||
int maxNumberOfTicks,
|
||||
double percentToTrim,
|
||||
double howManyStdDevsConstituteOutlier
|
||||
) {
|
||||
List<Double> yValues = extractYValues(xyValues);
|
||||
|
||||
if (yValues.size() < 3) {
|
||||
// with less than 3 elements, there is no meaningful inlier analysis
|
||||
return;
|
||||
}
|
||||
|
||||
Tuple2<Double, Double> inlierRange =
|
||||
findInlierRange(yValues, percentToTrim, howManyStdDevsConstituteOutlier);
|
||||
|
||||
applyRange(yAxis, maxNumberOfTicks, inlierRange);
|
||||
}
|
||||
|
||||
private static List<Double> extractYValues(ObservableList<? extends XYChart.Data<Number, Number>> xyValues) {
|
||||
return xyValues
|
||||
.stream()
|
||||
.map(xyData -> (double) xyData.getYValue())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/* Finds the minimum and maximum inlier values. The returned values may be NaN.
|
||||
* See `computeInlierThreshold` for the definition of inlier.
|
||||
*/
|
||||
private static Tuple2<Double, Double> findInlierRange(
|
||||
List<Double> yValues,
|
||||
double percentToTrim,
|
||||
double howManyStdDevsConstituteOutlier
|
||||
) {
|
||||
Tuple2<Double, Double> inlierThreshold =
|
||||
computeInlierThreshold(yValues, percentToTrim, howManyStdDevsConstituteOutlier);
|
||||
|
||||
DoubleSummaryStatistics inlierStatistics =
|
||||
yValues
|
||||
.stream()
|
||||
.filter(y -> withinBounds(inlierThreshold, y))
|
||||
.mapToDouble(Double::doubleValue)
|
||||
.summaryStatistics();
|
||||
|
||||
var inlierMin = inlierStatistics.getMin();
|
||||
var inlierMax = inlierStatistics.getMax();
|
||||
|
||||
return new Tuple2<>(inlierMin, inlierMax);
|
||||
}
|
||||
|
||||
private static boolean withinBounds(Tuple2<Double, Double> bounds, double number) {
|
||||
var lowerBound = bounds.first;
|
||||
var upperBound = bounds.second;
|
||||
return (lowerBound <= number) && (number <= upperBound);
|
||||
}
|
||||
|
||||
/* Computes the lower and upper inlier thresholds. A point lying outside
|
||||
* these thresholds is considered an outlier, and a point lying within
|
||||
* is considered an inlier.
|
||||
* The thresholds are found by trimming the dataset (see method `trim`),
|
||||
* then adding or subtracting a multiple of its (trimmed) standard
|
||||
* deviation from its (trimmed) mean.
|
||||
*/
|
||||
private static Tuple2<Double, Double> computeInlierThreshold(
|
||||
List<Double> numbers, double percentToTrim, double howManyStdDevsConstituteOutlier
|
||||
) {
|
||||
if (howManyStdDevsConstituteOutlier <= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"howManyStdDevsConstituteOutlier should be a positive number");
|
||||
}
|
||||
|
||||
List<Double> trimmed = trim(percentToTrim, numbers);
|
||||
|
||||
DoubleSummaryStatisticsWithStdDev summaryStatistics =
|
||||
trimmed.stream()
|
||||
.collect(
|
||||
DoubleSummaryStatisticsWithStdDev::new,
|
||||
DoubleSummaryStatisticsWithStdDev::accept,
|
||||
DoubleSummaryStatisticsWithStdDev::combine);
|
||||
|
||||
double mean = summaryStatistics.getAverage();
|
||||
double stdDev = summaryStatistics.getStandardDeviation();
|
||||
|
||||
var inlierLowerThreshold = mean - (stdDev * howManyStdDevsConstituteOutlier);
|
||||
var inlierUpperThreshold = mean + (stdDev * howManyStdDevsConstituteOutlier);
|
||||
|
||||
return new Tuple2<>(inlierLowerThreshold, inlierUpperThreshold);
|
||||
}
|
||||
|
||||
/* Sorts the data and discards given percentage from the left and right sides each.
|
||||
* E.g. 5% trim implies a total of 10% (2x 5%) of elements discarded.
|
||||
* Used in calculating trimmed mean (and in turn trimmed standard deviation),
|
||||
* which is more robust to outliers than a simple mean.
|
||||
*/
|
||||
private static List<Double> trim(double percentToTrim, List<Double> numbers) {
|
||||
var minPercentToTrim = 0;
|
||||
var maxPercentToTrim = 50;
|
||||
if (minPercentToTrim > percentToTrim || percentToTrim > maxPercentToTrim) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format(
|
||||
"The percentage of data points to trim must be in the range [%d,%d].",
|
||||
minPercentToTrim, maxPercentToTrim));
|
||||
}
|
||||
|
||||
var totalPercentTrim = percentToTrim * 2;
|
||||
if (totalPercentTrim == 0) {
|
||||
return numbers;
|
||||
}
|
||||
if (totalPercentTrim == 100) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if (numbers.isEmpty()) {
|
||||
return numbers;
|
||||
}
|
||||
|
||||
var count = numbers.size();
|
||||
int countToDropFromEachSide = (int) Math.round((count / 100d) * percentToTrim); // visada >= 0?
|
||||
if (countToDropFromEachSide == 0) {
|
||||
return numbers;
|
||||
}
|
||||
|
||||
var sorted = numbers.stream().sorted();
|
||||
|
||||
var oneSideTrimmed = sorted.skip(countToDropFromEachSide);
|
||||
|
||||
// Here, having already trimmed the left-side, we are implicitly trimming
|
||||
// the right-side by specifying a limit to the stream's length.
|
||||
// An explicit right-side drop/trim/skip is not supported by the Stream API.
|
||||
var countAfterTrim = count - (countToDropFromEachSide * 2); // visada > 0? ir <= count?
|
||||
var bothSidesTrimmed = oneSideTrimmed.limit(countAfterTrim);
|
||||
|
||||
return bothSidesTrimmed.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/* On the given axis, sets the provided lower and upper bounds, and
|
||||
* computes an appropriate major tick unit (distance between major ticks in data-space).
|
||||
* External computation of tick unit is necessary, because JavaFX doesn't support automatic
|
||||
* tick unit computation when axis bounds are set manually.
|
||||
*/
|
||||
private static void applyRange(NumberAxis axis, int maxNumberOfTicks, Tuple2<Double, Double> bounds) {
|
||||
var boundsWidth = getBoundsWidth(bounds);
|
||||
if (boundsWidth < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"The lower bound must be a smaller number than the upper bound");
|
||||
}
|
||||
if (boundsWidth == 0 || Double.isNaN(boundsWidth)) {
|
||||
// less than 2 unique data-points: recalculating axis range doesn't make sense
|
||||
return;
|
||||
}
|
||||
|
||||
axis.setAutoRanging(false);
|
||||
|
||||
var lowerBound = bounds.first;
|
||||
var upperBound = bounds.second;
|
||||
|
||||
// If one of the ends of the range weren't zero,
|
||||
// additional logic would be needed to make ticks "round".
|
||||
// Of course, many, if not most, charts benefit from having 0 on the axis.
|
||||
if (lowerBound > 0) {
|
||||
lowerBound = 0d;
|
||||
} else if (upperBound < 0) {
|
||||
upperBound = 0d;
|
||||
}
|
||||
|
||||
axis.setLowerBound(lowerBound);
|
||||
axis.setUpperBound(upperBound);
|
||||
|
||||
var referenceTickUnit = computeReferenceTickUnit(maxNumberOfTicks, bounds);
|
||||
|
||||
var tickUnit = computeTickUnit(referenceTickUnit);
|
||||
|
||||
axis.setTickUnit(tickUnit);
|
||||
}
|
||||
|
||||
/* Uses bounds and maximum number of major ticks to find a reference tick unit
|
||||
* for the `computeTickUnit` method. The reference tick unit is later used as a
|
||||
* starting point for tick unit's search.
|
||||
* The rationale behind dividing the range/domain/width of an axis by maximum number
|
||||
* of ticks is that it yields a good number of ticks, but they are not "well rounded",
|
||||
* hence the next step of computing the actual tick unit.
|
||||
* `maxNumberOfTicks` specifies how many subdivisions (major tick units) an axis
|
||||
* should have at most. The final number of subdivisions, after `computeTickUnit`,
|
||||
* usually will be lower, but never higher.
|
||||
*/
|
||||
private static double computeReferenceTickUnit(int maxNumberOfTicks, Tuple2<Double, Double> bounds) {
|
||||
if (maxNumberOfTicks <= 0) {
|
||||
throw new IllegalArgumentException("maxNumberOfTicks must be a positive number");
|
||||
}
|
||||
var width = getBoundsWidth(bounds);
|
||||
return width / maxNumberOfTicks;
|
||||
}
|
||||
|
||||
/* Extracted from cern.extjfx.chart.DefaultTickUnitSupplier (licensed Apache 2.0).
|
||||
* Original description below; note that the `multipliers` vector is hardcoded in the method to the default value
|
||||
* used in the source class:
|
||||
*
|
||||
* Computes tick unit using the following formula: tickUnit = M*10^E, where M is one of the multipliers specified in
|
||||
* the constructor and E is an exponent of 10. Both M and E are selected so that the calculated unit is the smallest
|
||||
* (closest to the zero) value that is greater than or equal to the reference tick unit.
|
||||
*
|
||||
* For example with multipliers [1, 2, 5], the method will give the following results:
|
||||
*
|
||||
* computeTickUnit(0.01) returns 0.01
|
||||
* computeTickUnit(0.42) returns 0.5
|
||||
* computeTickUnit(1.73) returns 2
|
||||
* computeTickUnit(5) returns 5
|
||||
* computeTickUnit(27) returns 50
|
||||
*
|
||||
* @param referenceTickUnit the reference tick unit, must be a positive number
|
||||
*/
|
||||
private static double computeTickUnit(double referenceTickUnit) {
|
||||
if (referenceTickUnit <= 0) {
|
||||
throw new IllegalArgumentException("The reference tick unit must be a positive number");
|
||||
}
|
||||
|
||||
// Default multipliers vector extracted from the source class.
|
||||
double[] multipliers = {1d, 2.5, 5d};
|
||||
|
||||
int BASE = 10;
|
||||
int exp = (int) Math.floor(Math.log10(referenceTickUnit));
|
||||
double factor = referenceTickUnit / Math.pow(BASE, exp);
|
||||
|
||||
double multiplier = 0;
|
||||
int lastIndex = multipliers.length - 1;
|
||||
if (factor > multipliers[lastIndex]) {
|
||||
exp++;
|
||||
multiplier = multipliers[0];
|
||||
} else {
|
||||
for (int i = lastIndex; i >= 0; i--) {
|
||||
if (factor <= multipliers[i]) {
|
||||
multiplier = multipliers[i];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return multiplier * Math.pow(BASE, exp);
|
||||
}
|
||||
|
||||
private static double getBoundsWidth(Tuple2<Double, Double> bounds) {
|
||||
var lowerBound = bounds.first;
|
||||
var upperBound = bounds.second;
|
||||
return Math.abs(upperBound - lowerBound);
|
||||
}
|
||||
}
|
128
desktop/src/main/java/bisq/desktop/util/MovingAverageUtils.java
Normal file
128
desktop/src/main/java/bisq/desktop/util/MovingAverageUtils.java
Normal file
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.desktop.util;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.Objects;
|
||||
import java.util.Queue;
|
||||
import java.util.Spliterator;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public class MovingAverageUtils {
|
||||
|
||||
/* With period 2, on an input of [1,2,3,4], should return [Double.NaN, 1.5, 2.5, 3.5].
|
||||
*
|
||||
* In case of the source stream having too few elements to compute a moving average
|
||||
* (as a function of the provided period), the returned stream will (only) contain
|
||||
* a sequence of (period - 1) NaNs. Otherwise, the resulting stream is prepadded with
|
||||
* these NaNs. See `prependLagCompensation` for details.
|
||||
*/
|
||||
public static Stream<Double> simpleMovingAverage(Stream<Number> source, int period) {
|
||||
if (period < 1) {
|
||||
throw new IllegalArgumentException("Simple moving average period must be a positive number.");
|
||||
}
|
||||
|
||||
var windows = SlidingWindowSpliterator.windowed(source, period);
|
||||
Stream<Double> averages =
|
||||
windows.map(window ->
|
||||
window
|
||||
.mapToDouble(Number::doubleValue)
|
||||
.summaryStatistics()
|
||||
.getAverage()
|
||||
);
|
||||
|
||||
return prependLagCompensation(averages, period);
|
||||
}
|
||||
|
||||
/* Given a period of for example 3, prepends a sequence of 2 NaNs.
|
||||
* In this way the returned stream has the same length as the input stream,
|
||||
* and the index of a given average matches the index of the last element
|
||||
* of a sequence of data points from which the average was computed,
|
||||
* Provided there were enough data points in the input stream to compute
|
||||
* the moving average (see next paragraph).
|
||||
*
|
||||
* Unfortunately, if there are too little data points to calculate the
|
||||
* moving average, this will return a stream with more elements, that are
|
||||
* all NaNs, than the input stream contained. This is due to the inherent
|
||||
* laziness of streams: we cannot check the relevant streams' sizes
|
||||
* without destroying them, so we cannot make the prepadding adaptive.
|
||||
* The exact number of NaNs returned in this case is `period - 1`.
|
||||
*/
|
||||
private static Stream<Double> prependLagCompensation(Stream<Double> averages, int period) {
|
||||
var lag = period - 1;
|
||||
var lagCompensation = Collections.nCopies(lag, Double.NaN).stream();
|
||||
return Stream.concat(lagCompensation, averages);
|
||||
}
|
||||
|
||||
static class SlidingWindowSpliterator<T> implements Spliterator<Stream<T>> {
|
||||
|
||||
static <T> Stream<Stream<T>> windowed(Stream<T> source, int windowSize) {
|
||||
return StreamSupport.stream(new SlidingWindowSpliterator<>(source, windowSize), false);
|
||||
}
|
||||
|
||||
private final Queue<T> buffer;
|
||||
private final Iterator<T> sourceIterator;
|
||||
private final int windowSize;
|
||||
|
||||
private SlidingWindowSpliterator(Stream<T> source, int windowSize) {
|
||||
this.buffer = new ArrayDeque<>(windowSize);
|
||||
this.sourceIterator = Objects.requireNonNull(source).iterator();
|
||||
this.windowSize = windowSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryAdvance(Consumer<? super Stream<T>> action) {
|
||||
if (windowSize < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (sourceIterator.hasNext()) {
|
||||
buffer.add(sourceIterator.next());
|
||||
|
||||
if (buffer.size() == windowSize) {
|
||||
action.accept(Arrays.stream((T[]) buffer.toArray(new Object[0])));
|
||||
buffer.poll();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Spliterator<Stream<T>> trySplit() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long estimateSize() {
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int characteristics() {
|
||||
return ORDERED | NONNULL;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.desktop.util;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class MovingAverageUtilsTest {
|
||||
|
||||
private static final int NAN = -99;
|
||||
|
||||
private static int[] calcMA(int period, int[] input) {
|
||||
System.out.println("Input:");
|
||||
System.out.println(Arrays.toString(input));
|
||||
|
||||
Stream<Number> streamInput =
|
||||
Arrays
|
||||
.stream(input)
|
||||
.boxed()
|
||||
.map(x -> x == NAN ? Double.NaN : x);
|
||||
|
||||
int[] output = MovingAverageUtils
|
||||
.simpleMovingAverage(streamInput, period)
|
||||
.mapToInt(x -> Double.isFinite(x) ? (int) Math.round(x) : NAN)
|
||||
.toArray();
|
||||
|
||||
System.out.println("Output:");
|
||||
System.out.println(Arrays.toString(output));
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static void testMA(int period, int[] input, int[] expected) {
|
||||
var output = calcMA(period, input);
|
||||
Assert.assertArrayEquals(output, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void normalConditions() {
|
||||
testMA(
|
||||
2,
|
||||
new int[]{10, 20, 30, 40},
|
||||
new int[]{NAN, 15, 25, 35}
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inputContainsNaNs0() {
|
||||
testMA(
|
||||
2,
|
||||
new int[]{NAN, 20, 30, 40},
|
||||
new int[]{NAN, NAN, 25, 35}
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inputContainsNaNs1() {
|
||||
testMA(
|
||||
2,
|
||||
new int[]{10, NAN, 30, 40},
|
||||
new int[]{NAN, NAN, NAN, 35}
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inputContainsNaNs2() {
|
||||
testMA(
|
||||
2,
|
||||
new int[]{10, NAN, NAN, 40},
|
||||
new int[]{NAN, NAN, NAN, NAN}
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inputContainsNaNs3() {
|
||||
testMA(
|
||||
2,
|
||||
new int[]{10, NAN, 30, NAN, 40},
|
||||
new int[]{NAN, NAN, NAN, NAN, NAN}
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonsensicalPeriod0() {
|
||||
testMA(
|
||||
1,
|
||||
new int[]{10, 20},
|
||||
new int[]{10, 20}
|
||||
);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void nonsensicalPeriod1() {
|
||||
var impossible = new int[]{};
|
||||
testMA(
|
||||
0,
|
||||
new int[]{10, 20},
|
||||
impossible
|
||||
);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void nonsensicalPeriod2() {
|
||||
var impossible = new int[]{};
|
||||
testMA(
|
||||
-1,
|
||||
new int[]{10, 20},
|
||||
impossible
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tooLittleData0() {
|
||||
testMA(
|
||||
3,
|
||||
new int[]{},
|
||||
new int[]{NAN, NAN}
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tooLittleData1() {
|
||||
testMA(
|
||||
3,
|
||||
new int[]{10},
|
||||
new int[]{NAN, NAN}
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tooLittleData2() {
|
||||
testMA(
|
||||
3,
|
||||
new int[]{10, 20},
|
||||
new int[]{NAN, NAN}
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user