225 lines
9.7 KiB
Python
225 lines
9.7 KiB
Python
import contextlib
|
|
import io
|
|
import json
|
|
import os
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from sqlalchemy.exc import OperationalError
|
|
|
|
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
|
|
|
|
from scripts import run_historical_compatibility_repair
|
|
|
|
|
|
class _MappingsResult:
|
|
def __init__(self, rows):
|
|
self._rows = rows
|
|
|
|
def mappings(self):
|
|
return self
|
|
|
|
def all(self):
|
|
return self._rows
|
|
|
|
|
|
class _ConnectionStub:
|
|
def __init__(self):
|
|
self.updates = []
|
|
self.reference_migration_mode = False
|
|
|
|
def execute(self, sql, params=None):
|
|
sql_text = str(sql).lower()
|
|
if "from trade_signals" in sql_text:
|
|
return _MappingsResult([{"id": 1, "invalidations": ["mss invalidated"]}])
|
|
if "from structure_events" in sql_text:
|
|
return _MappingsResult([{"id": 2, "accepted": True, "meta": {}}])
|
|
if "from fvg_zones" in sql_text and "select" in sql_text:
|
|
return _MappingsResult([{"id": 3, "status": "open", "meta": {}}])
|
|
if "select" in sql_text and "from liquidity_pools" in sql_text:
|
|
if self.reference_migration_mode:
|
|
return _MappingsResult(
|
|
[
|
|
{
|
|
"instrument_id": 1,
|
|
"timeframe": "1m",
|
|
"side": "sell_side",
|
|
"price_low": 100.0,
|
|
"price_high": 100.0,
|
|
"id": 21,
|
|
"pool_type": "prior_low",
|
|
"reference_ids": [7],
|
|
"created_ts": "2026-04-23T05:24:00+00:00",
|
|
"meta": {},
|
|
"sweep_events": [
|
|
{
|
|
"id": 200,
|
|
"sweep_ts": "2026-04-23T05:24:00+00:00",
|
|
"side_swept": "sell_side",
|
|
"sweep_high": "101.0",
|
|
"sweep_low": "99.0",
|
|
"close_back_inside": False,
|
|
"validation_level": "weak",
|
|
"status": "active",
|
|
"meta": {"pool_type": "prior_low"},
|
|
}
|
|
],
|
|
"sweep_ts_list": ["2026-04-23T05:24:00+00:00"],
|
|
"sweep_count": 1,
|
|
},
|
|
{
|
|
"instrument_id": 1,
|
|
"timeframe": "1m",
|
|
"side": "sell_side",
|
|
"price_low": 100.0,
|
|
"price_high": 100.0,
|
|
"id": 22,
|
|
"pool_type": "prior_low",
|
|
"reference_ids": [7],
|
|
"created_ts": "2026-04-23T05:24:00+00:00",
|
|
"meta": {},
|
|
"sweep_events": [
|
|
{
|
|
"id": 201,
|
|
"sweep_ts": "2026-04-23T05:26:00+00:00",
|
|
"side_swept": "sell_side",
|
|
"sweep_high": "102.0",
|
|
"sweep_low": "98.0",
|
|
"close_back_inside": False,
|
|
"validation_level": "weak",
|
|
"status": "active",
|
|
"meta": {"pool_type": "prior_low"},
|
|
}
|
|
],
|
|
"sweep_ts_list": ["2026-04-23T05:26:00+00:00"],
|
|
"sweep_count": 1,
|
|
},
|
|
]
|
|
)
|
|
return _MappingsResult(
|
|
[
|
|
{
|
|
"instrument_id": 1,
|
|
"timeframe": "1m",
|
|
"side": "buy_side",
|
|
"price_low": 100.0,
|
|
"price_high": 100.0,
|
|
"id": 9,
|
|
"pool_type": "equal_highs",
|
|
"reference_ids": [1, 2],
|
|
"created_ts": "2026-04-29T10:36:00+00:00",
|
|
"meta": {"merged_reference_count": 2},
|
|
"sweep_events": [],
|
|
"sweep_ts_list": [],
|
|
"sweep_count": 0,
|
|
},
|
|
{
|
|
"instrument_id": 1,
|
|
"timeframe": "1m",
|
|
"side": "buy_side",
|
|
"price_low": 100.0,
|
|
"price_high": 100.0,
|
|
"id": 10,
|
|
"pool_type": "equal_highs",
|
|
"reference_ids": [1, 2, 3],
|
|
"created_ts": "2026-04-29T10:36:00+00:00",
|
|
"meta": {"merged_reference_count": 3},
|
|
"sweep_events": [],
|
|
"sweep_ts_list": [],
|
|
"sweep_count": 0,
|
|
}
|
|
]
|
|
)
|
|
self.updates.append({"sql": sql_text, "params": params})
|
|
return _MappingsResult([])
|
|
|
|
|
|
class _BeginStub:
|
|
def __init__(self, connection):
|
|
self.connection = connection
|
|
|
|
def __enter__(self):
|
|
return self.connection
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
|
|
class HistoricalCompatibilityRepairScriptTests(unittest.TestCase):
|
|
def test_dry_run_reports_candidate_repairs_without_updates(self) -> None:
|
|
connection = _ConnectionStub()
|
|
|
|
with patch("scripts.run_historical_compatibility_repair.engine.begin", return_value=_BeginStub(connection)):
|
|
summary = run_historical_compatibility_repair.run_historical_compatibility_repair()
|
|
|
|
self.assertTrue(summary["passed"])
|
|
self.assertTrue(summary["dry_run"])
|
|
self.assertEqual(summary["candidate_repairs"]["signal_invalidations"], 1)
|
|
self.assertEqual(summary["candidate_repairs"]["mss_meta"], 1)
|
|
self.assertEqual(summary["candidate_repairs"]["fvg_meta"], 1)
|
|
self.assertEqual(summary["candidate_repairs"]["liquidity_duplicate_groups"], 1)
|
|
self.assertEqual(summary["candidate_repairs"]["liquidity_safe_auto_merge_groups"], 1)
|
|
self.assertEqual(summary["candidate_repairs"]["liquidity_safe_duplicate_sweep_merge_groups"], 0)
|
|
self.assertEqual(summary["candidate_repairs"]["liquidity_reference_migration_executable_groups"], 0)
|
|
self.assertEqual(connection.updates, [])
|
|
|
|
def test_apply_safe_repairs_updates_supported_categories(self) -> None:
|
|
connection = _ConnectionStub()
|
|
|
|
with patch("scripts.run_historical_compatibility_repair.engine.begin", return_value=_BeginStub(connection)):
|
|
summary = run_historical_compatibility_repair.run_historical_compatibility_repair(
|
|
apply_signal_invalidations=True,
|
|
apply_mss_meta=True,
|
|
apply_fvg_meta=True,
|
|
apply_liquidity_safe_merges=True,
|
|
)
|
|
|
|
self.assertFalse(summary["dry_run"])
|
|
self.assertEqual(summary["applied_repairs"]["signal_invalidations"], 1)
|
|
self.assertEqual(summary["applied_repairs"]["mss_meta"], 1)
|
|
self.assertEqual(summary["applied_repairs"]["fvg_meta"], 1)
|
|
self.assertEqual(summary["applied_repairs"]["liquidity_safe_merges"], 1)
|
|
self.assertEqual(len(connection.updates), 4)
|
|
|
|
def test_apply_liquidity_safe_merges_can_run_independently(self) -> None:
|
|
connection = _ConnectionStub()
|
|
|
|
with patch("scripts.run_historical_compatibility_repair.engine.begin", return_value=_BeginStub(connection)):
|
|
summary = run_historical_compatibility_repair.run_historical_compatibility_repair(
|
|
apply_liquidity_safe_merges=True,
|
|
)
|
|
|
|
self.assertFalse(summary["dry_run"])
|
|
self.assertEqual(summary["applied_repairs"]["signal_invalidations"], 0)
|
|
self.assertEqual(summary["applied_repairs"]["liquidity_safe_merges"], 1)
|
|
self.assertEqual(len(connection.updates), 1)
|
|
|
|
def test_apply_liquidity_reference_migrations_can_run_independently(self) -> None:
|
|
connection = _ConnectionStub()
|
|
connection.reference_migration_mode = True
|
|
|
|
with patch("scripts.run_historical_compatibility_repair.engine.begin", return_value=_BeginStub(connection)):
|
|
summary = run_historical_compatibility_repair.run_historical_compatibility_repair(
|
|
apply_liquidity_reference_migrations=True,
|
|
)
|
|
|
|
self.assertFalse(summary["dry_run"])
|
|
self.assertEqual(summary["candidate_repairs"]["liquidity_reference_migration_executable_groups"], 1)
|
|
self.assertEqual(summary["applied_repairs"]["liquidity_reference_migrations"], 1)
|
|
self.assertEqual(len(connection.updates), 2)
|
|
|
|
def test_main_reports_database_unavailable_json(self) -> None:
|
|
output = io.StringIO()
|
|
|
|
with patch("scripts.run_historical_compatibility_repair.engine.begin", side_effect=OperationalError("stmt", {}, Exception())), contextlib.redirect_stdout(output):
|
|
exit_code = run_historical_compatibility_repair.main(argv=[], env={"HISTORICAL_REPAIR_OUTPUT_JSON": "1"})
|
|
|
|
payload = json.loads(output.getvalue())
|
|
self.assertEqual(exit_code, 2)
|
|
self.assertFalse(payload["passed"])
|
|
self.assertEqual(payload["error_type"], "OperationalError")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|