Spaces:
Running
Running
add tests
Browse files- .gitignore +0 -1
- tests/manual_test.py +29 -0
- tests/test_metrics.py +40 -0
- tests/test_resolver.py +76 -0
- tests/test_retrain_trigger.py +27 -0
.gitignore
CHANGED
|
@@ -29,7 +29,6 @@ Thumbs.db
|
|
| 29 |
./webscrapper.db-wal
|
| 30 |
.env
|
| 31 |
.env.example
|
| 32 |
-
/tests
|
| 33 |
._requirements.txt
|
| 34 |
._dockerfile
|
| 35 |
/logs
|
|
|
|
| 29 |
./webscrapper.db-wal
|
| 30 |
.env
|
| 31 |
.env.example
|
|
|
|
| 32 |
._requirements.txt
|
| 33 |
._dockerfile
|
| 34 |
/logs
|
tests/manual_test.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def run_manual_test(prediction_date):
|
| 2 |
+
|
| 3 |
+
# 1. Generate today's feature rows
|
| 4 |
+
features = build_live_features(
|
| 5 |
+
as_of_date=prediction_date
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
# 2. Validate against model metadata
|
| 9 |
+
validate_features(features)
|
| 10 |
+
|
| 11 |
+
# 3. Run XGBoost
|
| 12 |
+
predictions = predict(features)
|
| 13 |
+
|
| 14 |
+
# 4. Rank
|
| 15 |
+
predictions = rank_predictions(predictions)
|
| 16 |
+
|
| 17 |
+
# 5. Store
|
| 18 |
+
save_predictions(predictions)
|
| 19 |
+
|
| 20 |
+
# 6. Resolve using historical future data
|
| 21 |
+
resolve_predictions(
|
| 22 |
+
as_of_date=prediction_date
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
# 7. Calculate metrics
|
| 26 |
+
calculate_metrics()
|
| 27 |
+
|
| 28 |
+
# 8. Check retraining condition
|
| 29 |
+
check_retrain()
|
tests/test_metrics.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import date
|
| 2 |
+
import pytest
|
| 3 |
+
from evaluation.metrics import (
|
| 4 |
+
precision_at_threshold, hit_rate, positive_rate,
|
| 5 |
+
average_return, binary_pr_auc, rank_deciles, calculate_metrics
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
def test_precision():
|
| 9 |
+
assert precision_at_threshold([.9,.8,.4,.2], [1,0,1,0], .5) == .5
|
| 10 |
+
|
| 11 |
+
def test_rates():
|
| 12 |
+
assert hit_rate([1,0,1,0,0]) == .4
|
| 13 |
+
assert positive_rate([1,0,1,0,0]) == .4
|
| 14 |
+
|
| 15 |
+
def test_average_return():
|
| 16 |
+
assert average_return([.03,.05,-.01]) == pytest.approx(.0233333333)
|
| 17 |
+
|
| 18 |
+
def test_pr_auc():
|
| 19 |
+
assert binary_pr_auc([.9,.8,.2,.1], [1,1,0,0]) == pytest.approx(1.0)
|
| 20 |
+
|
| 21 |
+
def test_deciles():
|
| 22 |
+
probs = list(reversed([i/100 for i in range(1,101)]))
|
| 23 |
+
deciles = rank_deciles(probs, [1]*100, [.03]*100)
|
| 24 |
+
assert len(deciles) == 10
|
| 25 |
+
assert all(d["count"] == 10 for d in deciles)
|
| 26 |
+
|
| 27 |
+
def test_complete_metrics():
|
| 28 |
+
rows = [
|
| 29 |
+
{"prediction_date":date(2026,7,1),"symbol":"AAA","predicted_probability":.9,"actual_label":1,"actual_return":.05},
|
| 30 |
+
{"prediction_date":date(2026,7,1),"symbol":"BBB","predicted_probability":.4,"actual_label":0,"actual_return":-.02},
|
| 31 |
+
{"prediction_date":date(2026,7,2),"symbol":"AAA","predicted_probability":.8,"actual_label":1,"actual_return":.04},
|
| 32 |
+
{"prediction_date":date(2026,7,2),"symbol":"BBB","predicted_probability":.3,"actual_label":0,"actual_return":-.01},
|
| 33 |
+
]
|
| 34 |
+
result = calculate_metrics(rows, threshold=.5)
|
| 35 |
+
assert result.prediction_days == 2
|
| 36 |
+
assert result.resolved_predictions == 4
|
| 37 |
+
assert result.precision == 1.0
|
| 38 |
+
assert result.hit_rate == .5
|
| 39 |
+
assert result.average_return == pytest.approx(.015)
|
| 40 |
+
assert len(result.deciles) == 10
|
tests/test_resolver.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import date
|
| 2 |
+
|
| 3 |
+
# from evaluation.resolver import resolve_prediction_rows
|
| 4 |
+
|
| 5 |
+
from jobs import resolver
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def test_positive_prediction():
|
| 9 |
+
result = resolver.resolve_prediction_rows(
|
| 10 |
+
prediction_date=date(2026, 8, 3),
|
| 11 |
+
symbol="ABC",
|
| 12 |
+
future_rows=[
|
| 13 |
+
{"date": date(2026, 8, 4), "open": 100.0, "close": 101.0},
|
| 14 |
+
{"date": date(2026, 8, 5), "open": 101.0, "close": 102.0},
|
| 15 |
+
{"date": date(2026, 8, 6), "open": 102.0, "close": 103.0},
|
| 16 |
+
{"date": date(2026, 8, 7), "open": 103.0, "close": 104.0},
|
| 17 |
+
{"date": date(2026, 8, 10), "open": 104.0, "close": 104.0},
|
| 18 |
+
],
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
assert result is not None
|
| 22 |
+
assert result.entry_date == date(2026, 8, 4)
|
| 23 |
+
assert result.entry_open == 100.0
|
| 24 |
+
assert result.max_close == 104.0
|
| 25 |
+
assert result.actual_return == 0.04
|
| 26 |
+
assert result.actual_label == 1
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def test_exact_three_percent_is_positive():
|
| 30 |
+
result = resolver.resolve_prediction_rows(
|
| 31 |
+
prediction_date=date(2026, 8, 3),
|
| 32 |
+
symbol="ABC",
|
| 33 |
+
future_rows=[
|
| 34 |
+
{"date": date(2026, 8, 4), "open": 100.0, "close": 103.0},
|
| 35 |
+
{"date": date(2026, 8, 5), "open": 103.0, "close": 101.0},
|
| 36 |
+
{"date": date(2026, 8, 6), "open": 101.0, "close": 100.0},
|
| 37 |
+
{"date": date(2026, 8, 7), "open": 100.0, "close": 99.0},
|
| 38 |
+
{"date": date(2026, 8, 10), "open": 99.0, "close": 98.0},
|
| 39 |
+
],
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
assert result is not None
|
| 43 |
+
assert result.actual_return == 0.03
|
| 44 |
+
assert result.actual_label == 1
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def test_below_three_percent_is_negative():
|
| 48 |
+
result = resolver.resolve_prediction_rows(
|
| 49 |
+
prediction_date=date(2026, 8, 3),
|
| 50 |
+
symbol="ABC",
|
| 51 |
+
future_rows=[
|
| 52 |
+
{"date": date(2026, 8, 4), "open": 100.0, "close": 102.99},
|
| 53 |
+
{"date": date(2026, 8, 5), "open": 102.0, "close": 102.5},
|
| 54 |
+
{"date": date(2026, 8, 6), "open": 102.0, "close": 101.0},
|
| 55 |
+
{"date": date(2026, 8, 7), "open": 101.0, "close": 100.0},
|
| 56 |
+
{"date": date(2026, 8, 10), "open": 100.0, "close": 99.0},
|
| 57 |
+
],
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
assert result is not None
|
| 61 |
+
assert result.actual_return < 0.03
|
| 62 |
+
assert result.actual_label == 0
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def test_incomplete_window_stays_unresolved():
|
| 66 |
+
result = resolver.resolve_prediction_rows(
|
| 67 |
+
prediction_date=date(2026, 8, 3),
|
| 68 |
+
symbol="ABC",
|
| 69 |
+
future_rows=[
|
| 70 |
+
{"date": date(2026, 8, 4), "open": 100.0, "close": 105.0},
|
| 71 |
+
{"date": date(2026, 8, 5), "open": 105.0, "close": 105.0},
|
| 72 |
+
{"date": date(2026, 8, 6), "open": 105.0, "close": 105.0},
|
| 73 |
+
],
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
assert result is None
|
tests/test_retrain_trigger.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from evaluation.retrain_trigger import (
|
| 3 |
+
calculate_deterioration, evaluate_retrain_trigger
|
| 4 |
+
)
|
| 5 |
+
|
| 6 |
+
def test_15_percent_deterioration_triggers():
|
| 7 |
+
result = evaluate_retrain_trigger(0.60, 0.51)
|
| 8 |
+
assert result.should_flag is True
|
| 9 |
+
assert result.deterioration == pytest.approx(0.15)
|
| 10 |
+
|
| 11 |
+
def test_less_than_15_percent_deterioration_does_not_trigger():
|
| 12 |
+
result = evaluate_retrain_trigger(0.60, 0.52)
|
| 13 |
+
assert result.should_flag is False
|
| 14 |
+
assert result.deterioration == pytest.approx(0.1333333333)
|
| 15 |
+
|
| 16 |
+
def test_improvement_does_not_trigger():
|
| 17 |
+
result = evaluate_retrain_trigger(0.60, 0.65)
|
| 18 |
+
assert result.should_flag is False
|
| 19 |
+
assert result.deterioration < 0
|
| 20 |
+
|
| 21 |
+
def test_no_metric_does_not_trigger():
|
| 22 |
+
result = evaluate_retrain_trigger(0.60, None)
|
| 23 |
+
assert result.should_flag is False
|
| 24 |
+
assert result.deterioration is None
|
| 25 |
+
|
| 26 |
+
def test_deterioration_formula():
|
| 27 |
+
assert calculate_deterioration(0.50, 0.425) == pytest.approx(0.15)
|