mirror of
https://github.com/bisq-network/bisq.git
synced 2025-02-23 15:00:30 +01:00
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:
parent
11ff27b892
commit
3630abdeb8
3 changed files with 114 additions and 27 deletions
|
@ -22,6 +22,10 @@ import com.google.common.math.DoubleMath;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -109,27 +113,58 @@ public class MathUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class MovingAverage {
|
public static class MovingAverage {
|
||||||
private long[] window;
|
Deque<Long> window;
|
||||||
private int n, insert;
|
private int size;
|
||||||
private long sum;
|
private long sum;
|
||||||
|
private double outlier;
|
||||||
|
|
||||||
public MovingAverage(int size) {
|
// Outlier as ratio
|
||||||
window = new long[size];
|
public MovingAverage(int size, double outlier) {
|
||||||
insert = 0;
|
this.size = size;
|
||||||
|
window = new ArrayDeque<>(size);
|
||||||
|
this.outlier = outlier;
|
||||||
sum = 0;
|
sum = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public double next(long val) {
|
public Optional<Double> next(long val) {
|
||||||
if (n < window.length) n++;
|
var fullAtStart = isFull();
|
||||||
sum -= window[insert];
|
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;
|
sum += val;
|
||||||
window[insert] = val;
|
if (!fullAtStart && isFull() && outlier != 0) {
|
||||||
insert = (insert + 1) % window.length;
|
removeInitialOutlier();
|
||||||
return (double) sum / n;
|
}
|
||||||
|
// When discarding outliers, the first n non discarded elements return Optional.empty()
|
||||||
|
return outlier > 0 && !isFull() ? Optional.empty() : current();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean fullWindow() {
|
boolean isFull() {
|
||||||
return n == window.length;
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
57
common/src/test/java/bisq/common/util/MathUtilsTest.java
Normal file
57
common/src/test/java/bisq/common/util/MathUtilsTest.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -81,7 +81,6 @@ import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.collections.SetChangeListener;
|
import javafx.collections.SetChangeListener;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
@ -347,21 +346,17 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
|
||||||
.filter(e -> e.getTradeDate().compareTo(startDate) >= 0)
|
.filter(e -> e.getTradeDate().compareTo(startDate) >= 0)
|
||||||
.sorted(Comparator.comparing(TradeStatistics2::getTradeDate))
|
.sorted(Comparator.comparing(TradeStatistics2::getTradeDate))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
var movingAverage = new MathUtils.MovingAverage(10);
|
var movingAverage = new MathUtils.MovingAverage(10, 0.2);
|
||||||
var rangedMovingAverage = new ArrayList<Double>();
|
double[] extremes = {Double.MAX_VALUE, Double.MIN_VALUE};
|
||||||
sortedRangeData.forEach(e -> {
|
sortedRangeData.forEach(e -> {
|
||||||
var nextVal = movingAverage.next(e.getTradePrice().getValue());
|
var price = e.getTradePrice().getValue();
|
||||||
if (movingAverage.fullWindow()) {
|
movingAverage.next(price).ifPresent(val -> {
|
||||||
rangedMovingAverage.add(nextVal);
|
if (val < extremes[0]) extremes[0] = val;
|
||||||
}
|
if (val > extremes[1]) extremes[1] = val;
|
||||||
});
|
});
|
||||||
|
});
|
||||||
var min = rangedMovingAverage.stream()
|
var min = extremes[0];
|
||||||
.min(Double::compareTo)
|
var max = extremes[1];
|
||||||
.orElse(0d);
|
|
||||||
var max = rangedMovingAverage.stream()
|
|
||||||
.max(Double::compareTo)
|
|
||||||
.orElse(0d);
|
|
||||||
if (min == 0d || max == 0d) {
|
if (min == 0d || max == 0d) {
|
||||||
setBuyerSecurityDeposit(minSecurityDeposit, false);
|
setBuyerSecurityDeposit(minSecurityDeposit, false);
|
||||||
return;
|
return;
|
||||||
|
|
Loading…
Add table
Reference in a new issue