Skip to content

Commit 3b619a5

Browse files
authored
Add Weighted Moving Average (#6)
1 parent 8bbf8ed commit 3b619a5

File tree

3 files changed

+371
-0
lines changed

3 files changed

+371
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.gitattributes
22

3+
.vscode/
34
.gradle/
45
.idea/
56

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package tslib.movingaverage;
2+
3+
import java.util.ArrayDeque;
4+
import java.util.ArrayList;
5+
import java.util.Deque;
6+
import java.util.List;
7+
8+
/**
9+
* Weighted Moving Average (WMA) calculator.
10+
* Computes the weighted average of a sliding window over a numeric series.
11+
* More recent values in the window receive higher weights.
12+
*
13+
* Example usage:
14+
* WeightedMovingAverage wma = new WeightedMovingAverage(5);
15+
* List<Double> result = wma.compute(data);
16+
*
17+
* Returns `null` for the first (period - 1) elements, until the window is full.
18+
* Weights are assigned linearly: most recent value gets weight 'period',
19+
* second most recent gets weight 'period-1', etc.
20+
*
21+
* Author: navdeep
22+
*/
23+
public class WeightedMovingAverage implements MovingAverage {
24+
25+
private final Deque<Double> window;
26+
private final int period;
27+
private final double weightSum;
28+
29+
public WeightedMovingAverage(int period) {
30+
if (period <= 0) {
31+
throw new IllegalArgumentException("Period must be a positive integer!");
32+
}
33+
this.period = period;
34+
this.window = new ArrayDeque<>(period);
35+
// Calculate sum of weights: 1 + 2 + 3 + ... + period = period * (period + 1) / 2
36+
this.weightSum = period * (period + 1) / 2.0;
37+
}
38+
39+
/**
40+
* Computes the weighted moving average (WMA) over the input data.
41+
* Returns null for values where the moving average window is not yet full.
42+
*
43+
* @param data input time series
44+
* @return list of WMA values (same length as input)
45+
*/
46+
@Override
47+
public List<Double> compute(List<Double> data) {
48+
if (data == null || data.isEmpty()) return List.of();
49+
50+
List<Double> maData = new ArrayList<>(data.size());
51+
reset();
52+
53+
for (double value : data) {
54+
add(value);
55+
if (window.size() < period) {
56+
maData.add(null); // Not enough data yet
57+
} else {
58+
maData.add(getWeightedAverage());
59+
}
60+
}
61+
return maData;
62+
}
63+
64+
/**
65+
* Adds a new number to the window.
66+
*/
67+
public void add(double value) {
68+
window.addLast(value);
69+
if (window.size() > period) {
70+
window.removeFirst();
71+
}
72+
}
73+
74+
/**
75+
* Returns the current weighted average of the window.
76+
* Most recent value gets weight 'period', second most recent gets weight 'period-1', etc.
77+
*/
78+
private double getWeightedAverage() {
79+
double weightedSum = 0.0;
80+
int weight = 1;
81+
82+
for (double value : window) {
83+
weightedSum += value * weight;
84+
weight++;
85+
}
86+
87+
return weightedSum / weightSum;
88+
}
89+
90+
/**
91+
* Resets the moving average state.
92+
*/
93+
@Override
94+
public void reset() {
95+
window.clear();
96+
}
97+
}
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
package tslib.movingaverage;
2+
3+
import org.junit.Test;
4+
import org.junit.Before;
5+
import static org.junit.Assert.*;
6+
7+
import java.util.Arrays;
8+
import java.util.List;
9+
import java.util.ArrayList;
10+
11+
/**
12+
* Comprehensive unit tests for WeightedMovingAverage.
13+
* Tests edge cases, mathematical correctness, and API behavior.
14+
*
15+
* Author: navdeep
16+
*/
17+
public class WeightedMovingAverageTest {
18+
19+
private WeightedMovingAverage wma;
20+
21+
@Before
22+
public void setUp() {
23+
wma = new WeightedMovingAverage(5);
24+
}
25+
26+
@Test
27+
public void testConstructorWithValidPeriod() {
28+
WeightedMovingAverage wma3 = new WeightedMovingAverage(3);
29+
WeightedMovingAverage wma10 = new WeightedMovingAverage(10);
30+
assertNotNull(wma3);
31+
assertNotNull(wma10);
32+
}
33+
34+
@Test(expected = IllegalArgumentException.class)
35+
public void testConstructorWithInvalidPeriodZero() {
36+
new WeightedMovingAverage(0);
37+
}
38+
39+
@Test(expected = IllegalArgumentException.class)
40+
public void testConstructorWithInvalidPeriodNegative() {
41+
new WeightedMovingAverage(-1);
42+
}
43+
44+
@Test(expected = IllegalArgumentException.class)
45+
public void testConstructorWithInvalidPeriodNegativeFive() {
46+
new WeightedMovingAverage(-5);
47+
}
48+
49+
@Test
50+
public void testEmptyInput() {
51+
List<Double> result = wma.compute(new ArrayList<>());
52+
assertTrue(result.isEmpty());
53+
}
54+
55+
@Test
56+
public void testNullInput() {
57+
List<Double> result = wma.compute(null);
58+
assertTrue(result.isEmpty());
59+
}
60+
61+
@Test
62+
public void testSingleValue() {
63+
List<Double> data = Arrays.asList(10.0);
64+
List<Double> result = wma.compute(data);
65+
66+
assertEquals(1, result.size());
67+
assertNull(result.get(0)); // Not enough data for period 5
68+
}
69+
70+
@Test
71+
public void testInsufficientData() {
72+
List<Double> data = Arrays.asList(1.0, 2.0, 3.0, 4.0);
73+
List<Double> result = wma.compute(data);
74+
75+
assertEquals(4, result.size());
76+
assertNull(result.get(0));
77+
assertNull(result.get(1));
78+
assertNull(result.get(2));
79+
assertNull(result.get(3));
80+
}
81+
82+
@Test
83+
public void testExactPeriodData() {
84+
// Test with exactly 5 values: [1, 2, 3, 4, 5]
85+
// Weights: [1, 2, 3, 4, 5]
86+
// Weighted sum: 1*1 + 2*2 + 3*3 + 4*4 + 5*5 = 1 + 4 + 9 + 16 + 25 = 55
87+
// Weight sum: 1 + 2 + 3 + 4 + 5 = 15
88+
// Expected: 55/15 = 3.666...
89+
List<Double> data = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0);
90+
List<Double> result = wma.compute(data);
91+
92+
assertEquals(5, result.size());
93+
assertNull(result.get(0));
94+
assertNull(result.get(1));
95+
assertNull(result.get(2));
96+
assertNull(result.get(3));
97+
assertEquals(55.0/15.0, result.get(4), 1e-10);
98+
}
99+
100+
@Test
101+
public void testPeriod3Calculation() {
102+
WeightedMovingAverage wma3 = new WeightedMovingAverage(3);
103+
// Test with [1, 2, 3]
104+
// Weights: [1, 2, 3]
105+
// Weighted sum: 1*1 + 2*2 + 3*3 = 1 + 4 + 9 = 14
106+
// Weight sum: 1 + 2 + 3 = 6
107+
// Expected: 14/6 = 2.333...
108+
List<Double> data = Arrays.asList(1.0, 2.0, 3.0);
109+
List<Double> result = wma3.compute(data);
110+
111+
assertEquals(3, result.size());
112+
assertNull(result.get(0));
113+
assertNull(result.get(1));
114+
assertEquals(14.0/6.0, result.get(2), 1e-10);
115+
}
116+
117+
@Test
118+
public void testSlidingWindow() {
119+
// Test sliding window behavior
120+
// Data: [1, 2, 3, 4, 5, 6, 7]
121+
// Window 1: [1, 2, 3, 4, 5] -> (1*1 + 2*2 + 3*3 + 4*4 + 5*5) / 15 = 55/15
122+
// Window 2: [2, 3, 4, 5, 6] -> (2*1 + 3*2 + 4*3 + 5*4 + 6*5) / 15 = 70/15
123+
// Window 3: [3, 4, 5, 6, 7] -> (3*1 + 4*2 + 5*3 + 6*4 + 7*5) / 15 = 85/15
124+
List<Double> data = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0);
125+
List<Double> result = wma.compute(data);
126+
127+
assertEquals(7, result.size());
128+
assertNull(result.get(0));
129+
assertNull(result.get(1));
130+
assertNull(result.get(2));
131+
assertNull(result.get(3));
132+
assertEquals(55.0/15.0, result.get(4), 1e-10);
133+
assertEquals(70.0/15.0, result.get(5), 1e-10);
134+
assertEquals(85.0/15.0, result.get(6), 1e-10);
135+
}
136+
137+
@Test
138+
public void testConstantValues() {
139+
// Test with constant values - WMA should equal the constant
140+
List<Double> data = Arrays.asList(5.0, 5.0, 5.0, 5.0, 5.0);
141+
List<Double> result = wma.compute(data);
142+
143+
assertEquals(5, result.size());
144+
assertNull(result.get(0));
145+
assertNull(result.get(1));
146+
assertNull(result.get(2));
147+
assertNull(result.get(3));
148+
assertEquals(5.0, result.get(4), 1e-10);
149+
}
150+
151+
@Test
152+
public void testIncreasingValues() {
153+
// Test with strictly increasing values
154+
// Data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
155+
List<Double> data = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0);
156+
List<Double> result = wma.compute(data);
157+
158+
assertEquals(10, result.size());
159+
// Check that WMA values are increasing (trend following)
160+
assertTrue(result.get(4) < result.get(5));
161+
assertTrue(result.get(5) < result.get(6));
162+
assertTrue(result.get(6) < result.get(7));
163+
assertTrue(result.get(7) < result.get(8));
164+
assertTrue(result.get(8) < result.get(9));
165+
}
166+
167+
@Test
168+
public void testDecreasingValues() {
169+
// Test with strictly decreasing values
170+
// Data: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
171+
List<Double> data = Arrays.asList(10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0);
172+
List<Double> result = wma.compute(data);
173+
174+
assertEquals(10, result.size());
175+
// Check that WMA values are decreasing (trend following)
176+
assertTrue(result.get(4) > result.get(5));
177+
assertTrue(result.get(5) > result.get(6));
178+
assertTrue(result.get(6) > result.get(7));
179+
assertTrue(result.get(7) > result.get(8));
180+
assertTrue(result.get(8) > result.get(9));
181+
}
182+
183+
@Test
184+
public void testResetFunctionality() {
185+
// Test that reset works correctly
186+
List<Double> data1 = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0);
187+
List<Double> result1 = wma.compute(data1);
188+
189+
// Reset and compute with different data
190+
wma.reset();
191+
List<Double> data2 = Arrays.asList(10.0, 20.0, 30.0, 40.0, 50.0);
192+
List<Double> result2 = wma.compute(data2);
193+
194+
// Results should be different
195+
assertNotEquals(result1.get(4), result2.get(4));
196+
}
197+
198+
@Test
199+
public void testAddMethod() {
200+
// Test the add method directly
201+
wma.add(1.0);
202+
wma.add(2.0);
203+
wma.add(3.0);
204+
wma.add(4.0);
205+
wma.add(5.0);
206+
207+
// Should have 5 values now
208+
assertEquals(5, wma.compute(Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0)).size());
209+
}
210+
211+
@Test
212+
public void testLargePeriod() {
213+
// Test with a larger period
214+
WeightedMovingAverage wma10 = new WeightedMovingAverage(10);
215+
List<Double> data = new ArrayList<>();
216+
for (int i = 1; i <= 15; i++) {
217+
data.add((double) i);
218+
}
219+
220+
List<Double> result = wma10.compute(data);
221+
assertEquals(15, result.size());
222+
223+
// First 9 should be null
224+
for (int i = 0; i < 9; i++) {
225+
assertNull(result.get(i));
226+
}
227+
228+
// 10th should have a value
229+
assertNotNull(result.get(9));
230+
}
231+
232+
@Test
233+
public void testPrecision() {
234+
// Test precision with decimal values
235+
List<Double> data = Arrays.asList(1.1, 2.2, 3.3, 4.4, 5.5);
236+
List<Double> result = wma.compute(data);
237+
238+
// Manual calculation:
239+
// Weights: [1, 2, 3, 4, 5]
240+
// Weighted sum: 1.1*1 + 2.2*2 + 3.3*3 + 4.4*4 + 5.5*5 = 1.1 + 4.4 + 9.9 + 17.6 + 27.5 = 60.5
241+
// Weight sum: 15
242+
// Expected: 60.5/15 = 4.033...
243+
assertEquals(60.5/15.0, result.get(4), 1e-10);
244+
}
245+
246+
@Test
247+
public void testNegativeValues() {
248+
// Test with negative values
249+
List<Double> data = Arrays.asList(-1.0, -2.0, -3.0, -4.0, -5.0);
250+
List<Double> result = wma.compute(data);
251+
252+
// Manual calculation:
253+
// Weights: [1, 2, 3, 4, 5]
254+
// Weighted sum: -1*1 + -2*2 + -3*3 + -4*4 + -5*5 = -1 - 4 - 9 - 16 - 25 = -55
255+
// Weight sum: 15
256+
// Expected: -55/15 = -3.666...
257+
assertEquals(-55.0/15.0, result.get(4), 1e-10);
258+
}
259+
260+
@Test
261+
public void testMixedPositiveNegative() {
262+
// Test with mixed positive and negative values
263+
List<Double> data = Arrays.asList(-1.0, 2.0, -3.0, 4.0, -5.0);
264+
List<Double> result = wma.compute(data);
265+
266+
// Manual calculation:
267+
// Weights: [1, 2, 3, 4, 5]
268+
// Weighted sum: -1*1 + 2*2 + -3*3 + 4*4 + -5*5 = -1 + 4 - 9 + 16 - 25 = -15
269+
// Weight sum: 15
270+
// Expected: -15/15 = -1.0
271+
assertEquals(-1.0, result.get(4), 1e-10);
272+
}
273+
}

0 commit comments

Comments
 (0)