439 lines
18 KiB
Python
439 lines
18 KiB
Python
import os
|
|
import unittest
|
|
|
|
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
|
|
|
|
from src.services.maintenance import (
|
|
build_historical_compatibility_summary,
|
|
classify_liquidity_duplicate_groups,
|
|
derive_historical_fvg_meta,
|
|
derive_historical_mss_meta,
|
|
plan_liquidity_reference_migrations,
|
|
plan_fvg_meta_repairs,
|
|
plan_mss_meta_repairs,
|
|
plan_trade_signal_invalidation_repairs,
|
|
summarize_liquidity_duplicate_groups,
|
|
)
|
|
|
|
|
|
class HistoricalCompatibilityServiceTests(unittest.TestCase):
|
|
def test_plans_trade_signal_invalidation_repairs(self) -> None:
|
|
repairs = plan_trade_signal_invalidation_repairs(
|
|
[
|
|
{"id": 1, "invalidations": ["mss invalidated", "fvg fully mitigated"]},
|
|
{"id": 2, "invalidations": ["mss_invalidated"]},
|
|
]
|
|
)
|
|
|
|
self.assertEqual(repairs, [{"id": 1, "invalidations": ["mss_invalidated", "fvg_mitigated"]}])
|
|
|
|
def test_derives_historical_mss_meta(self) -> None:
|
|
repaired = derive_historical_mss_meta({"accepted": False, "meta": {"reclaimed_old_control": True}})
|
|
|
|
self.assertEqual(repaired["state"], "invalidated")
|
|
self.assertEqual(repaired["status_reason"], "old_control_reclaimed")
|
|
|
|
def test_plans_historical_fvg_meta_repairs(self) -> None:
|
|
repairs = plan_fvg_meta_repairs(
|
|
[
|
|
{"id": 3, "status": "mitigated", "meta": {}},
|
|
{"id": 4, "status": "open", "meta": {"status_reason": "no_touch", "invalidated": False, "has_data_gap": False, "gap_count": 0, "data_quality_warnings": []}},
|
|
]
|
|
)
|
|
|
|
self.assertEqual(len(repairs), 1)
|
|
self.assertEqual(repairs[0]["id"], 3)
|
|
self.assertEqual(repairs[0]["meta"]["status_reason"], "mitigation_threshold_reached")
|
|
self.assertFalse(repairs[0]["meta"]["invalidated"])
|
|
|
|
def test_summarizes_liquidity_duplicate_groups(self) -> None:
|
|
summary = summarize_liquidity_duplicate_groups(
|
|
[
|
|
{
|
|
"instrument_id": 1,
|
|
"timeframe": "1m",
|
|
"side": "buy_side",
|
|
"price_low": 100.0,
|
|
"price_high": 100.0,
|
|
"duplicate_count": 2,
|
|
"ids": [10, 9],
|
|
}
|
|
]
|
|
)
|
|
|
|
self.assertEqual(summary["group_count"], 1)
|
|
self.assertEqual(summary["row_count"], 2)
|
|
self.assertEqual(summary["groups"][0]["ids"], [10, 9])
|
|
|
|
def test_classifies_unreferenced_superset_pair_as_safe_auto_merge(self) -> None:
|
|
classified = classify_liquidity_duplicate_groups(
|
|
[
|
|
{
|
|
"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_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_ts_list": [],
|
|
"sweep_count": 0,
|
|
},
|
|
]
|
|
)
|
|
|
|
self.assertEqual(classified["classification_counts"]["safe_auto_merge_unreferenced_superset"], 1)
|
|
self.assertEqual(classified["groups"][0]["classification"], "safe_auto_merge_unreferenced_superset")
|
|
self.assertEqual(classified["groups"][0]["canonical_pool_id"], 10)
|
|
self.assertEqual(classified["groups"][0]["remove_pool_ids"], [9])
|
|
|
|
def test_classifies_exact_duplicate_referenced_pair_as_reference_migration(self) -> None:
|
|
classified = classify_liquidity_duplicate_groups(
|
|
[
|
|
{
|
|
"instrument_id": 1,
|
|
"timeframe": "1m",
|
|
"side": "sell_side",
|
|
"price_low": 100.0,
|
|
"price_high": 100.0,
|
|
"id": 11,
|
|
"pool_type": "prior_low",
|
|
"reference_ids": [7],
|
|
"created_ts": "2026-04-23T05:24:00+00:00",
|
|
"meta": {},
|
|
"sweep_events": [
|
|
{
|
|
"id": 100,
|
|
"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": 12,
|
|
"pool_type": "prior_low",
|
|
"reference_ids": [7],
|
|
"created_ts": "2026-04-23T05:24:00+00:00",
|
|
"meta": {},
|
|
"sweep_events": [
|
|
{
|
|
"id": 101,
|
|
"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,
|
|
},
|
|
]
|
|
)
|
|
|
|
self.assertEqual(classified["classification_counts"]["safe_exact_duplicate_with_duplicate_sweeps"], 1)
|
|
self.assertEqual(classified["groups"][0]["classification"], "safe_exact_duplicate_with_duplicate_sweeps")
|
|
|
|
def test_classifies_nonidentical_referenced_pair_as_reference_migration(self) -> None:
|
|
classified = classify_liquidity_duplicate_groups(
|
|
[
|
|
{
|
|
"instrument_id": 1,
|
|
"timeframe": "1m",
|
|
"side": "sell_side",
|
|
"price_low": 100.0,
|
|
"price_high": 100.0,
|
|
"id": 21,
|
|
"pool_type": "prior_low",
|
|
"reference_ids": [9],
|
|
"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": [9],
|
|
"created_ts": "2026-04-23T05:24:00+00:00",
|
|
"meta": {},
|
|
"sweep_events": [
|
|
{
|
|
"id": 201,
|
|
"sweep_ts": "2026-04-23T05:24: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:24:00+00:00"],
|
|
"sweep_count": 1,
|
|
},
|
|
]
|
|
)
|
|
|
|
self.assertEqual(classified["classification_counts"]["needs_sweep_reference_migration"], 1)
|
|
self.assertEqual(classified["groups"][0]["classification"], "needs_sweep_reference_migration")
|
|
|
|
def test_plans_executable_liquidity_reference_migration_when_sweep_timestamps_do_not_conflict(self) -> None:
|
|
duplicates = classify_liquidity_duplicate_groups(
|
|
[
|
|
{
|
|
"instrument_id": 1,
|
|
"timeframe": "1m",
|
|
"side": "sell_side",
|
|
"price_low": 100.0,
|
|
"price_high": 100.0,
|
|
"id": 21,
|
|
"pool_type": "prior_low",
|
|
"reference_ids": [9],
|
|
"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": [9],
|
|
"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,
|
|
},
|
|
]
|
|
)
|
|
|
|
plan = plan_liquidity_reference_migrations(duplicates)
|
|
|
|
self.assertEqual(plan["group_count"], 1)
|
|
self.assertEqual(plan["executable_group_count"], 1)
|
|
self.assertEqual(plan["blocked_group_count"], 0)
|
|
self.assertTrue(plan["groups"][0]["is_executable"])
|
|
self.assertEqual(plan["groups"][0]["actions"][0]["action"], "rebind_sweep_event")
|
|
self.assertEqual(plan["groups"][0]["remove_pool_ids"], [21])
|
|
|
|
def test_blocks_liquidity_reference_migration_when_sweep_timestamp_conflicts(self) -> None:
|
|
duplicates = classify_liquidity_duplicate_groups(
|
|
[
|
|
{
|
|
"instrument_id": 1,
|
|
"timeframe": "1m",
|
|
"side": "sell_side",
|
|
"price_low": 100.0,
|
|
"price_high": 100.0,
|
|
"id": 21,
|
|
"pool_type": "prior_low",
|
|
"reference_ids": [9],
|
|
"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": [9],
|
|
"created_ts": "2026-04-23T05:24:00+00:00",
|
|
"meta": {},
|
|
"sweep_events": [
|
|
{
|
|
"id": 201,
|
|
"sweep_ts": "2026-04-23T05:24: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:24:00+00:00"],
|
|
"sweep_count": 1,
|
|
},
|
|
]
|
|
)
|
|
|
|
plan = plan_liquidity_reference_migrations(duplicates)
|
|
|
|
self.assertEqual(plan["group_count"], 1)
|
|
self.assertEqual(plan["executable_group_count"], 0)
|
|
self.assertEqual(plan["blocked_group_count"], 1)
|
|
self.assertFalse(plan["groups"][0]["is_executable"])
|
|
self.assertEqual(plan["groups"][0]["blocked_reason"], "sweep_ts_conflict")
|
|
self.assertEqual(plan["groups"][0]["conflicts"][0]["type"], "duplicate_sweep_ts_conflict")
|
|
|
|
def test_classifies_mixed_version_group_as_keep_history(self) -> None:
|
|
classified = classify_liquidity_duplicate_groups(
|
|
[
|
|
{
|
|
"instrument_id": 1,
|
|
"timeframe": "1m",
|
|
"side": "buy_side",
|
|
"price_low": 100.0,
|
|
"price_high": 100.0,
|
|
"id": 13,
|
|
"pool_type": "prior_high",
|
|
"reference_ids": [1],
|
|
"created_ts": "2026-04-23T05:24:00+00:00",
|
|
"meta": {},
|
|
"sweep_ts_list": ["2026-04-23T05:24:00+00:00"],
|
|
"sweep_count": 1,
|
|
},
|
|
{
|
|
"instrument_id": 1,
|
|
"timeframe": "1m",
|
|
"side": "buy_side",
|
|
"price_low": 100.0,
|
|
"price_high": 100.0,
|
|
"id": 14,
|
|
"pool_type": "equal_highs",
|
|
"reference_ids": [1, 2],
|
|
"created_ts": "2026-04-23T05:24:00+00:00",
|
|
"meta": {"merged_reference_count": 2},
|
|
"sweep_ts_list": ["2026-04-23T05:24:00+00:00"],
|
|
"sweep_count": 1,
|
|
},
|
|
]
|
|
)
|
|
|
|
self.assertEqual(classified["classification_counts"]["keep_version_history"], 1)
|
|
self.assertEqual(classified["groups"][0]["classification"], "keep_version_history")
|
|
|
|
def test_builds_historical_compatibility_summary(self) -> None:
|
|
summary = build_historical_compatibility_summary(
|
|
signal_repairs=[{"id": 1}],
|
|
mss_repairs=[{"id": 2}],
|
|
fvg_repairs=[],
|
|
liquidity_duplicates={
|
|
"group_count": 1,
|
|
"row_count": 3,
|
|
"classification_counts": {
|
|
"safe_auto_merge_unreferenced_superset": 1,
|
|
"needs_sweep_reference_migration": 0,
|
|
"keep_version_history": 0,
|
|
},
|
|
"groups": [{"ids": [9, 8, 7]}],
|
|
},
|
|
liquidity_reference_migrations={"group_count": 0, "executable_group_count": 0, "blocked_group_count": 0, "groups": []},
|
|
applied_repairs={"signal_invalidations": 1, "mss_meta": 1, "fvg_meta": 0, "liquidity_reference_migrations": 0},
|
|
dry_run=False,
|
|
)
|
|
|
|
self.assertTrue(summary["passed"])
|
|
self.assertFalse(summary["dry_run"])
|
|
self.assertEqual(summary["candidate_repairs"]["liquidity_duplicate_rows"], 3)
|
|
self.assertEqual(summary["candidate_repairs"]["liquidity_safe_auto_merge_groups"], 1)
|
|
self.assertEqual(summary["candidate_repairs"]["liquidity_reference_migration_executable_groups"], 0)
|
|
self.assertEqual(summary["applied_repairs"]["signal_invalidations"], 1)
|
|
self.assertEqual(summary["manual_review"]["liquidity_duplicates"][0]["ids"], [9, 8, 7])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|