Compare commits

..
Author SHA1 Message Date
google-labs-jules[bot]andmaghdam f9be43b827 Add unit tests for walk_forward_splits
Created tests/test_model_training.py and added unit tests for the walk_forward_splits function in models/model_training.py. Tests cover basic chronological splitting logic, varying number of splits, dataset sizes that don't divide perfectly, and end bounds capping. Verified that the tests accurately catch regressions.

Co-authored-by: maghdam <63883156+maghdam@users.noreply.github.com>
2026-03-11 18:39:00 +00:00
10 changed files with 91 additions and 64 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
-64
View File
@@ -1,64 +0,0 @@
import pandas as pd
import numpy as np
from features.labeling_schemes import create_labels_multi_bar
def test_create_labels_multi_bar():
# Toy dataframe with close prices
df = pd.DataFrame({
"close": [100.0, 102.0, 99.0, 99.0, 105.0]
})
# horizon = 1, threshold = 0.01
# row 0: close = 100, future = 102, return = 0.02 >= 0.01 -> label = 1
# row 1: close = 102, future = 99, return = -3/102 = -0.0294 <= -0.01 -> label = -1
# row 2: close = 99, future = 99, return = 0.00 -> label = 0
# row 3: close = 99, future = 105, return = 6/99 = 0.0606 >= 0.01 -> label = 1
# row 4: close = 105, future = NaN
res = create_labels_multi_bar(df, horizon=1, threshold=0.01)
# Should have 4 rows because the last row is dropped due to NaN future return
assert len(res) == 4
expected_labels = [1, -1, 0, 1]
np.testing.assert_array_equal(res["multi_bar_label"].values, expected_labels)
# Check returns
expected_returns = [0.02, -3/102, 0.0, 6/99]
np.testing.assert_array_almost_equal(res["future_return_h"].values, expected_returns)
def test_create_labels_multi_bar_custom_horizon():
# Test with horizon=2, threshold=0.05
df = pd.DataFrame({
"close": [100.0, 101.0, 105.0, 90.0, 95.0, 100.0]
})
# horizon = 2
# row 0: close 100, future 105 (idx 2), return 0.05 >= 0.05 -> 1
# row 1: close 101, future 90 (idx 3), return -11/101 = -0.1089 <= -0.05 -> -1
# row 2: close 105, future 95 (idx 4), return -10/105 = -0.0952 <= -0.05 -> -1
# row 3: close 90, future 100 (idx 5), return 10/90 = 0.1111 >= 0.05 -> 1
# row 4: NaN
# row 5: NaN
res = create_labels_multi_bar(df, horizon=2, threshold=0.05)
assert len(res) == 4
expected_labels = [1, -1, -1, 1]
np.testing.assert_array_equal(res["multi_bar_label"].values, expected_labels)
def test_create_labels_multi_bar_exact_threshold():
# Check boundary condition where return is exactly the threshold
df = pd.DataFrame({
"close": [100.0, 105.0, 95.0]
})
# threshold = 0.05, horizon = 1
# row 0: return 0.05 -> 1
# row 1: return -10/105 = -0.0952 -> -1
res = create_labels_multi_bar(df, horizon=1, threshold=0.05)
assert res.iloc[0]["multi_bar_label"] == 1
res = create_labels_multi_bar(df, horizon=1, threshold=0.1)
# return is 0.05, which is < 0.1 and > -0.1
assert res.iloc[0]["multi_bar_label"] == 0
+91
View File
@@ -0,0 +1,91 @@
import unittest
import pandas as pd
import numpy as np
from models.model_training import walk_forward_splits
class TestModelTraining(unittest.TestCase):
def setUp(self):
# Create a simple dummy sequence (e.g., numbers 0 to 9)
self.X = pd.DataFrame({'feature': np.arange(10)})
self.y = pd.Series(np.arange(10))
def test_walk_forward_splits_basic(self):
"""Test basic chronological splitting with default n_splits=3 on n=10."""
# For n=10, n_splits=3, fold_size = 10 // (3 + 1) = 2
# Fold 1: Train [0:2], Test [2:4]
# Fold 2: Train [0:4], Test [4:6]
# Fold 3: Train [0:6], Test [6:8]
folds = walk_forward_splits(self.X, self.y, n_splits=3)
self.assertEqual(len(folds), 3)
# Fold 1
X_train_1, y_train_1, X_test_1, y_test_1 = folds[0]
self.assertEqual(len(X_train_1), 2)
self.assertEqual(len(X_test_1), 2)
self.assertTrue(np.array_equal(X_train_1['feature'].values, [0, 1]))
self.assertTrue(np.array_equal(X_test_1['feature'].values, [2, 3]))
# Fold 2
X_train_2, y_train_2, X_test_2, y_test_2 = folds[1]
self.assertEqual(len(X_train_2), 4)
self.assertEqual(len(X_test_2), 2)
self.assertTrue(np.array_equal(X_train_2['feature'].values, [0, 1, 2, 3]))
self.assertTrue(np.array_equal(X_test_2['feature'].values, [4, 5]))
# Fold 3
X_train_3, y_train_3, X_test_3, y_test_3 = folds[2]
self.assertEqual(len(X_train_3), 6)
self.assertEqual(len(X_test_3), 2)
self.assertTrue(np.array_equal(X_train_3['feature'].values, [0, 1, 2, 3, 4, 5]))
self.assertTrue(np.array_equal(X_test_3['feature'].values, [6, 7]))
def test_walk_forward_splits_varying_n_splits(self):
"""Test varying number of splits."""
# For n=10, n_splits=4, fold_size = 10 // (4 + 1) = 2
folds = walk_forward_splits(self.X, self.y, n_splits=4)
self.assertEqual(len(folds), 4)
# Last fold: Train [0:8], Test [8:10]
X_train_last, _, X_test_last, _ = folds[-1]
self.assertEqual(len(X_train_last), 8)
self.assertEqual(len(X_test_last), 2)
self.assertEqual(X_test_last['feature'].iloc[-1], 9)
def test_walk_forward_splits_remainder(self):
"""Test dataset size that doesn't divide perfectly."""
# n=11, n_splits=3, fold_size = 11 // (3 + 1) = 2
# Fold 1: Train [0:2], Test [2:4]
# Fold 2: Train [0:4], Test [4:6]
# Fold 3: Train [0:6], Test [6:8]
X_11 = pd.DataFrame({'feature': np.arange(11)})
y_11 = pd.Series(np.arange(11))
folds = walk_forward_splits(X_11, y_11, n_splits=3)
self.assertEqual(len(folds), 3)
# Check last fold bounds
X_train_last, _, X_test_last, _ = folds[-1]
self.assertEqual(len(X_train_last), 6)
self.assertEqual(len(X_test_last), 2)
self.assertTrue(np.array_equal(X_test_last['feature'].values, [6, 7]))
def test_walk_forward_splits_end_bounds(self):
"""Test case where end_test is capped at n."""
# n=5, n_splits=2, fold_size = 5 // 3 = 1
# Fold 1: Train [0:1], Test [1:2]
# Fold 2: Train [0:2], Test [2:3]
X_5 = pd.DataFrame({'feature': np.arange(5)})
y_5 = pd.Series(np.arange(5))
folds = walk_forward_splits(X_5, y_5, n_splits=2)
self.assertEqual(len(folds), 2)
X_train_last, _, X_test_last, _ = folds[-1]
self.assertEqual(len(X_train_last), 2)
self.assertEqual(len(X_test_last), 1)
self.assertTrue(np.array_equal(X_test_last['feature'].values, [2]))
if __name__ == '__main__':
unittest.main()