Improve MovingAverage

Don't include outliers (20% deviation from moving average) in moving
average calculation. It's quite likely that low liquidity markets or
markets with large spreads can't calculate deposit suggestion and will
then suggest deposit from preferences.

Added test for moving average class
This commit is contained in:
sqrrm 2020-07-02 23:03:26 +02:00
parent 11ff27b892
commit 3630abdeb8
No known key found for this signature in database
GPG key ID: 45235F9EF87089EC
3 changed files with 114 additions and 27 deletions

View file

@ -22,6 +22,10 @@ import com.google.common.math.DoubleMath;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -109,27 +113,58 @@ public class MathUtils {
}
public static class MovingAverage {
private long[] window;
private int n, insert;
Deque<Long> window;
private int size;
private long sum;
private double outlier;
public MovingAverage(int size) {
window = new long[size];
insert = 0;
// Outlier as ratio
public MovingAverage(int size, double outlier) {
this.size = size;
window = new ArrayDeque<>(size);
this.outlier = outlier;
sum = 0;
}
public double next(long val) {
if (n < window.length) n++;
sum -= window[insert];
public Optional<Double> next(long val) {
var fullAtStart = isFull();
if (fullAtStart) {
if (outlier > 0) {
// Return early if it's an outlier
var avg = (double) sum / size;
if (Math.abs(avg - val) / avg > outlier) {
return Optional.empty();
}
}
sum -= window.remove();
}
window.add(val);
sum += val;
window[insert] = val;
insert = (insert + 1) % window.length;
return (double) sum / n;
if (!fullAtStart && isFull() && outlier != 0) {
removeInitialOutlier();
}
// When discarding outliers, the first n non discarded elements return Optional.empty()
return outlier > 0 && !isFull() ? Optional.empty() : current();
}
public boolean fullWindow() {
return n == window.length;
boolean isFull() {
return window.size() == size;
}
private void removeInitialOutlier() {
var element = window.iterator();
while (element.hasNext()) {
var val = element.next();
var avgExVal = (double) (sum - val) / (size - 1);
if (Math.abs(avgExVal - val) / avgExVal > outlier) {
element.remove();
break;
}
}
}
public Optional<Double> current() {
return window.size() == 0 ? Optional.empty() : Optional.of((double) sum / window.size());
}
}
}

View file

@ -0,0 +1,57 @@
/*
* 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 org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
public class MathUtilsTest {
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Test
public void testMovingAverageWithoutOutlierExclusion() {
var values = new int[]{4, 5, 3, 1, 2, 4};
// Moving average = 4, 4.5, 4, 3, 2, 7/3
var movingAverage = new MathUtils.MovingAverage(3, 0);
int i = 0;
assertEquals(4, movingAverage.next(values[i++]).get(),0.001);
assertEquals(4.5, movingAverage.next(values[i++]).get(),0.001);
assertEquals(4, movingAverage.next(values[i++]).get(),0.001);
assertEquals(3, movingAverage.next(values[i++]).get(),0.001);
assertEquals(2, movingAverage.next(values[i++]).get(),0.001);
assertEquals((double) 7 / 3, movingAverage.next(values[i]).get(),0.001);
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Test
public void testMovingAverageWithOutlierExclusion() {
var values = new int[]{100, 102, 95, 101, 120, 115};
// Moving average = N/A, N/A, 99, 99.333..., N/A, 103.666...
var movingAverage = new MathUtils.MovingAverage(3, 0.2);
int i = 0;
assertFalse(movingAverage.next(values[i++]).isPresent());
assertFalse(movingAverage.next(values[i++]).isPresent());
assertEquals(99, movingAverage.next(values[i++]).get(),0.001);
assertEquals(99.333, movingAverage.next(values[i++]).get(),0.001);
assertFalse(movingAverage.next(values[i++]).isPresent());
assertEquals(103.666, movingAverage.next(values[i]).get(),0.001);
}
}

View file

@ -81,7 +81,6 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.SetChangeListener;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
@ -347,21 +346,17 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
.filter(e -> e.getTradeDate().compareTo(startDate) >= 0)
.sorted(Comparator.comparing(TradeStatistics2::getTradeDate))
.collect(Collectors.toList());
var movingAverage = new MathUtils.MovingAverage(10);
var rangedMovingAverage = new ArrayList<Double>();
var movingAverage = new MathUtils.MovingAverage(10, 0.2);
double[] extremes = {Double.MAX_VALUE, Double.MIN_VALUE};
sortedRangeData.forEach(e -> {
var nextVal = movingAverage.next(e.getTradePrice().getValue());
if (movingAverage.fullWindow()) {
rangedMovingAverage.add(nextVal);
}
var price = e.getTradePrice().getValue();
movingAverage.next(price).ifPresent(val -> {
if (val < extremes[0]) extremes[0] = val;
if (val > extremes[1]) extremes[1] = val;
});
});
var min = rangedMovingAverage.stream()
.min(Double::compareTo)
.orElse(0d);
var max = rangedMovingAverage.stream()
.max(Double::compareTo)
.orElse(0d);
var min = extremes[0];
var max = extremes[1];
if (min == 0d || max == 0d) {
setBuyerSecurityDeposit(minSecurityDeposit, false);
return;