from __future__ import annotations import json from datetime import datetime, timezone from pathlib import Path import numpy as np import pytest from hypothesis import given, settings, strategies as st from pydantic import ValidationError from prod.clean_arch.violet.exchange_leverage import ( CONVICTION_MAX, CONVICTION_MIN, EXCHANGE_LEV_MAX, EXCHANGE_LEV_MIN, ExchangeLeverageDecision, VioletExchangeLeverage, ) from prod.bingx import leverage as bingx_leverage REPORTS_DIR = Path("/mnt/dolphinng5_predict/prod/VIOLET_dev/reports") def _write_gate_report(name: str, **fields): REPORTS_DIR.mkdir(parents=True, exist_ok=True) ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") payload = { "generated_utc": datetime.now(timezone.utc).isoformat(), "layer": f"violet_v3_{name}", **fields, } path = REPORTS_DIR / f"violet_v3_{name}_{ts}.json" path.write_text(json.dumps(payload, indent=2, default=str)) return path def _wrapper(exchange_min: int = EXCHANGE_LEV_MIN, exchange_max: int = EXCHANGE_LEV_MAX) -> VioletExchangeLeverage: return VioletExchangeLeverage(exchange_min=exchange_min, exchange_max=exchange_max) def test_defaults_and_constants_match_blue_module(): w = _wrapper() assert w.exchange_min == 1 assert w.exchange_max == 3 assert CONVICTION_MIN == bingx_leverage.CONVICTION_MIN assert CONVICTION_MAX == bingx_leverage.CONVICTION_MAX assert EXCHANGE_LEV_MIN == bingx_leverage.EXCHANGE_LEV_MIN assert EXCHANGE_LEV_MAX == bingx_leverage.EXCHANGE_LEV_MAX def test_endpoints_map_cleanly(): w = _wrapper() assert w.map_target(0.5) == 1.0 assert w.map_target(9.0) == 3.0 assert w.normalize(1.0) == 1 assert w.normalize(3.0) == 3 def test_round_half_even_boundary_cases(): w = _wrapper() low = 0.5 + ((1.5 - 1.0) / (3.0 - 1.0)) * (9.0 - 0.5) high = 0.5 + ((2.5 - 1.0) / (3.0 - 1.0)) * (9.0 - 0.5) assert w.map_target(low) == pytest.approx(1.5) assert w.map_target(high) == pytest.approx(2.5) assert w.normalize(w.map_target(low)) == 2 assert w.normalize(w.map_target(high)) == 2 assert w.to_exchange(low).exchange_leverage == 2 assert w.to_exchange(high).exchange_leverage == 2 @pytest.mark.parametrize("conviction", [-10.0, -1.0, 0.0, 0.49, 9.1, 64.0]) @pytest.mark.parametrize("exchange_max", [1, 2, 3, 5, 9]) def test_out_of_range_clamps_like_blue(conviction, exchange_max): w = _wrapper(exchange_max=exchange_max) assert w.map_target(conviction) == bingx_leverage.map_internal_conviction_to_exchange_leverage_target( conviction, exchange_min=1, exchange_max=exchange_max, ) assert w.normalize(conviction) == bingx_leverage.normalize_bingx_leverage_value( conviction, exchange_min=1, exchange_max=exchange_max, ) assert w.to_exchange(conviction).exchange_leverage == bingx_leverage.map_internal_conviction_to_exchange_leverage( conviction, exchange_min=1, exchange_max=exchange_max, ) @pytest.mark.parametrize("exchange_max", [5, 9]) def test_non_default_exchange_max_flows_through(exchange_max): w = _wrapper(exchange_max=exchange_max) conviction = 6.25 decision = w.to_exchange(conviction) assert decision.exchange_min == 1 assert decision.exchange_max == exchange_max assert decision.target_exchange_leverage == bingx_leverage.map_internal_conviction_to_exchange_leverage_target( conviction, exchange_min=1, exchange_max=exchange_max ) assert decision.exchange_leverage == bingx_leverage.map_internal_conviction_to_exchange_leverage( conviction, exchange_min=1, exchange_max=exchange_max ) def test_exchange_leverage_decision_is_frozen(): d = ExchangeLeverageDecision( internal_conviction=1.5, target_exchange_leverage=1.25, exchange_leverage=1, exchange_min=1, exchange_max=3, ) with pytest.raises(ValidationError): d.exchange_leverage = 2 @given( conviction=st.floats(min_value=-5.0, max_value=64.0, allow_nan=False, allow_infinity=False), exchange_max=st.sampled_from([1, 2, 3, 5, 9]), ) @settings(max_examples=200, deadline=None) def test_property_bit_identity(conviction, exchange_max): w = _wrapper(exchange_max=exchange_max) target = w.map_target(conviction) blue_target = bingx_leverage.map_internal_conviction_to_exchange_leverage_target( conviction, exchange_min=1, exchange_max=exchange_max, ) assert target == blue_target final = w.to_exchange(conviction) blue_final = bingx_leverage.map_internal_conviction_to_exchange_leverage( conviction, exchange_min=1, exchange_max=exchange_max, ) assert final.target_exchange_leverage == blue_target assert final.exchange_leverage == blue_final assert isinstance(final.exchange_leverage, int) assert 1 <= final.exchange_leverage <= exchange_max @pytest.mark.gate def test_gate_exchange_leverage_bit_identity(): rng = np.random.default_rng(0) n = 1_000_000 conviction = rng.uniform(-1.0, 64.0, n) exchange_max = rng.choice(np.array([1, 2, 3, 5, 9], dtype=np.int64), n) violet = np.empty(n, dtype=np.int64) blue = np.empty(n, dtype=np.int64) violet_target = np.empty(n, dtype=np.float64) blue_target = np.empty(n, dtype=np.float64) w_cache: dict[int, VioletExchangeLeverage] = {} for i in range(n): ex_max = int(exchange_max[i]) w = w_cache.get(ex_max) if w is None: w = w_cache[ex_max] = _wrapper(exchange_max=ex_max) conv = float(conviction[i]) violet_target[i] = w.map_target(conv) violet[i] = w.to_exchange(conv).exchange_leverage blue_target[i] = bingx_leverage.map_internal_conviction_to_exchange_leverage_target( conv, exchange_min=1, exchange_max=ex_max ) blue[i] = bingx_leverage.map_internal_conviction_to_exchange_leverage( conv, exchange_min=1, exchange_max=ex_max ) target_mismatches = int(np.count_nonzero(violet_target != blue_target)) final_mismatches = int(np.count_nonzero(violet != blue)) total_mismatches = target_mismatches + final_mismatches _write_gate_report( "exchange_leverage", N=n, target_mismatches=target_mismatches, final_mismatches=final_mismatches, mismatches=total_mismatches, exchange_max_values=[1, 2, 3, 5, 9], ) assert total_mismatches == 0