164 lines
8.6 KiB
Python
"""Stdlib-only tests for safe wrapper argv and observed-outcome audits.
| |||
| |||
Run with a Python installation: python -m unittest discover -s ML -p test_legacy_entrypoints.py
| |||
These tests never train, connect to a broker, or install packages.
| |||
"""
| |||
import contextlib
| |||
import csv
| |||
import io
| |||
import json
| |||
from pathlib import Path
| |||
import tempfile
| |||
import unittest
| |||
from unittest.mock import patch
| |||
| |||
import snapshot_dataset
| |||
import snapshot_export_onnx
| |||
import snapshot_train
| |||
| |||
| |||
class LegacyEntrypointTests(unittest.TestCase):
| |||
def test_training_command_is_argument_vector_with_symbols(self):
| |||
command = snapshot_train.build_command("USDCHF, EURUSD", "artifacts/universal")
| |||
self.assertEqual(command[0], "powershell.exe")
| |||
self.assertIn("Build-MarketModel.ps1", command[5])
| |||
self.assertEqual(command[-2:], ["-Symbols", "USDCHF,EURUSD"])
| |||
| |||
def test_empty_duplicate_and_control_character_symbols_rejected(self):
| |||
for value in ("", "USDCHF,,EURUSD", "USDCHF,usdchf", "USDCHF,\nEURUSD"):
| |||
with self.subTest(value=value), self.assertRaises(ValueError):
| |||
snapshot_train.build_command(value, "output")
| |||
| |||
def test_dry_run_does_not_launch_training(self):
| |||
output = io.StringIO()
| |||
with patch.object(snapshot_train.subprocess, "run") as run, contextlib.redirect_stdout(output):
| |||
self.assertEqual(snapshot_train.main(["--dry-run", "--symbols", "USDCHF"]), 0)
| |||
run.assert_not_called()
| |||
self.assertIn("-Symbols", json.loads(output.getvalue()))
| |||
| |||
def test_status_training_argument_is_not_supported(self):
| |||
with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error:
| |||
snapshot_train.main(["--label_mode", "status"])
| |||
self.assertEqual(error.exception.code, 2)
| |||
| |||
def test_old_label_and_feature_apis_fail_explicitly(self):
| |||
for function in (snapshot_dataset.label_from_status,
| |||
snapshot_dataset.labels_from_future_prices,
| |||
snapshot_dataset.load_snapshot_dataframe,
| |||
snapshot_dataset.build_feature_matrix):
| |||
with self.subTest(function=function.__name__), self.assertRaisesRegex(RuntimeError, "retired"):
| |||
function(None)
| |||
| |||
def test_export_uses_single_native_exporter(self):
| |||
command = snapshot_export_onnx.build_command("input directory", "output directory")
| |||
self.assertIn("Export-MarketModel.ps1", command[5])
| |||
self.assertEqual(command[6], "-InputDirectory")
| |||
self.assertEqual(command[8], "-OutputDirectory")
| |||
| |||
def test_legacy_pickle_override_is_rejected(self):
| |||
with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error:
| |||
snapshot_export_onnx.main(["--artifacts", "old", "--scaler", "scaler.pkl"])
| |||
self.assertEqual(error.exception.code, 2)
| |||
| |||
def test_unicode_snapshots_are_inspected_without_labels(self):
| |||
with tempfile.TemporaryDirectory() as directory:
| |||
path = Path(directory) / "feature_batch_test.txt"
| |||
path.write_text("timestamp:2025.01.01 00:00:00\nsymbol:USDCHF\nstatus:executed\n", encoding="utf-16")
| |||
report = snapshot_dataset.inspect_snapshots(directory)
| |||
self.assertEqual(report["records"], 1)
| |||
self.assertEqual(report["symbols"], ["USDCHF"])
| |||
self.assertFalse(report["training_supported"])
| |||
self.assertNotIn("label", snapshot_dataset.parse_snapshot_file(path))
| |||
| |||
def test_duplicate_snapshot_fields_are_rejected(self):
| |||
with tempfile.TemporaryDirectory() as directory:
| |||
path = Path(directory) / "feature_batch_bad.txt"
| |||
path.write_text("symbol:USDCHF\nsymbol:EURUSD\n", encoding="utf-8")
| |||
with self.assertRaisesRegex(ValueError, "duplicate"):
| |||
snapshot_dataset.parse_snapshot_file(path)
| |||
| |||
def _write_market_fixture(self, path, mutation=None):
| |||
features = list(snapshot_dataset.MARKET_BASE_FEATURES + snapshot_dataset.ACTIVE_FEATURES)
| |||
fields = ["decision_time", "target_end", "split", "symbol", "direction", "label",
| |||
"net_log_return", "entry_spread_points", "exit_spread_points", *features]
| |||
rows = []
| |||
for split, month in (("train", "01"), ("validation", "07"), ("test", "10")):
| |||
for direction in (0, 1):
| |||
rows.append(dict(decision_time=f"2025.{month}.02 00:00", target_end=f"2025.{month}.02 04:00",
| |||
split=split, symbol="USDCHF", direction=direction, label=direction,
| |||
net_log_return=0.001 if direction else -0.001,
| |||
entry_spread_points=7, exit_spread_points=8,
| |||
**{feature: 0.0 for feature in features}))
| |||
for row in rows:
| |||
row["mm_direction"] = 1.0 if row["direction"] == 0 else -1.0
| |||
row["active_symbol_flag"] = 1.0
| |||
if mutation:
| |||
mutation(rows)
| |||
with path.open("w", newline="", encoding="utf-8") as handle:
| |||
writer = csv.DictWriter(handle, fieldnames=fields)
| |||
writer.writeheader()
| |||
writer.writerows(rows)
| |||
| |||
def test_market_audit_accepts_observed_paired_purged_rows(self):
| |||
with tempfile.TemporaryDirectory() as directory:
| |||
path = Path(directory) / "dataset.csv"
| |||
self._write_market_fixture(path)
| |||
report = snapshot_dataset.audit_market_dataset(path)
| |||
self.assertTrue(report["audit_passed"])
| |||
self.assertEqual(report["rows"], 6)
| |||
| |||
def test_unknown_labels_are_rejected_never_filled(self):
| |||
with tempfile.TemporaryDirectory() as directory:
| |||
path = Path(directory) / "dataset.csv"
| |||
self._write_market_fixture(path, lambda rows: rows[0].update(label=""))
| |||
with self.assertRaisesRegex(ValueError, "Unknown/malformed"):
| |||
snapshot_dataset.audit_market_dataset(path)
| |||
| |||
def test_split_boundaries_inferred_from_sibling_model_metadata(self):
| |||
with tempfile.TemporaryDirectory() as directory:
| |||
path = Path(directory) / "dataset.csv"
| |||
self._write_market_fixture(path)
| |||
content = path.read_text(encoding="utf-8")
| |||
content = content.replace("2025.01.02", "2026.01.02").replace("2025.07.02", "2026.07.02").replace("2025.10.02", "2026.09.02")
| |||
path.write_text(content, encoding="utf-8")
| |||
metadata = {"training_end": "2026.07.01", "validation_end": "2026.09.01", "test_end": "2026.10.01"}
| |||
(Path(directory) / "model_data.json").write_text(json.dumps(metadata), encoding="utf-8")
| |||
report = snapshot_dataset.audit_market_dataset(path)
| |||
self.assertTrue(report["audit_passed"])
| |||
self.assertEqual(report["split_boundaries"], metadata)
| |||
| |||
def test_explicit_boundaries_override_supplied_metadata_file(self):
| |||
with tempfile.TemporaryDirectory() as directory:
| |||
path = Path(directory) / "dataset.csv"
| |||
self._write_market_fixture(path)
| |||
metadata = Path(directory) / "alternate-model.json"
| |||
metadata.write_text(json.dumps({"training_end": "2026.07.01", "validation_end": "2026.09.01", "test_end": "2026.10.01"}), encoding="utf-8")
| |||
report = snapshot_dataset.audit_market_dataset(path, "2025.07.01", "2025.10.01", "2026.01.01", model_data=metadata)
| |||
self.assertTrue(report["audit_passed"])
| |||
self.assertEqual(report["split_boundaries"]["training_end"], "2025.07.01")
| |||
| |||
def test_partial_metadata_does_not_silently_use_legacy_dates(self):
| |||
with tempfile.TemporaryDirectory() as directory:
| |||
path = Path(directory) / "dataset.csv"
| |||
self._write_market_fixture(path)
| |||
(Path(directory) / "model_data.json").write_text('{"training_end":"2026.07.01"}', encoding="utf-8")
| |||
with self.assertRaisesRegex(ValueError, "validation_end"):
| |||
snapshot_dataset.audit_market_dataset(path)
| |||
| |||
def test_horizon_crossing_split_boundary_is_rejected(self):
| |||
with tempfile.TemporaryDirectory() as directory:
| |||
path = Path(directory) / "dataset.csv"
| |||
self._write_market_fixture(path, lambda rows: rows[0].update(target_end="2025.07.01 00:00"))
| |||
with self.assertRaisesRegex(ValueError, "split boundary"):
| |||
snapshot_dataset.audit_market_dataset(path)
| |||
| |||
def test_duplicate_symbol_time_direction_is_rejected(self):
| |||
with tempfile.TemporaryDirectory() as directory:
| |||
path = Path(directory) / "dataset.csv"
| |||
self._write_market_fixture(path, lambda rows: rows[1].update(rows[0]))
| |||
with self.assertRaisesRegex(ValueError, "Duplicate"):
| |||
snapshot_dataset.audit_market_dataset(path)
| |||
| |||
| |||
if __name__ == "__main__":
| |||
unittest.main()
|