Built by OA agent per VIOLET_SUB_SPEC__L3_EXCHANGE_LEVERAGE.md; reviewed for compliance/flaws. VERIFIED: wraps real prod/bingx/leverage.py (untouched), constants imported from it, NO arbitrary upper caps, exact == bit-identity (1e6 gate 0 mismatches), ROUND_HALF_EVEN explicitly tested (1.5->2 AND 2.5->2), clamping+non-default caps+frozen model. 38 tests pass on independent rerun. REVIEW FIX: to_exchange clamped negative internal_conviction to 0 in the trace field (reused ConvictionLeverage ge=0); changed trace field to plain float (poison guard only) so it records the ACTUAL input faithfully; dropped the clamp + unused import. Relocated 8 off-spec leverage-spike scratch files off repo root -> prod/VIOLET_dev/l3_spike/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
186 lines
6.4 KiB
Python
186 lines
6.4 KiB
Python
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
|