"""Test deterministici dello SHADOW MODE di TP01 (src/live/deribit.py). Coprono la logica a rischio zero che NON tocca la rete: quantizzazione notional->contratti, sizing target, costruzione ordine di ribilancio (buy/sell/exit/None), e PARITA' col backtest (il target live = ultimo target della serie causale). Il fill reale (slippage/fee) NON e' qui: si valida solo col micro-test mainnet. """ import sys from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(PROJECT_ROOT)) from src.live.deribit import (build_rebalance_order, notional_to_amount, target_notional_usd) from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d def test_notional_to_amount_step_and_min(): # BTC step/min $10 assert notional_to_amount("BTC-PERPETUAL", 1000) == 1000 assert notional_to_amount("BTC-PERPETUAL", 1006) == 1010 # round allo step assert notional_to_amount("BTC-PERPETUAL", 7) == 10 # clamp al minimo (>= mezzo step) assert notional_to_amount("BTC-PERPETUAL", 3) == 0.0 # < mezzo step -> niente ordine # ETH step/min $1 assert notional_to_amount("ETH-PERPETUAL", 33.7) == 34 assert notional_to_amount("ETH-PERPETUAL", 0.4) == 0.0 # usa il valore assoluto (il segno lo decide il delta a monte) assert notional_to_amount("BTC-PERPETUAL", -1000) == 1000 def test_no_float_artifacts(): # _quantize_step usa Decimal: nessun 0.07200000000000001 & co. v = notional_to_amount("ETH-PERPETUAL", 72.0) assert v == 72 and float(v).is_integer() def test_target_notional_5050_weight(): # 50/50 book: notional asset = 0.5 * frazione * equity assert target_notional_usd(1.0, 0.5, 2000) == 1000 assert target_notional_usd(2.0, 0.5, 2000) == 2000 # leva-cap 2x -> piena equity sull'asset assert target_notional_usd(0.0, 0.5, 2000) == 0.0 def test_build_order_entry(): o = build_rebalance_order("BTC-PERPETUAL", target_fraction=1.0, weight=0.5, equity_usd=2000, current_pos_usd=0.0) assert o["side"] == "buy" and o["amount"] == 1000 and o["reduce_only"] is False assert o["target_notional"] == 1000 and o["delta_notional"] == 1000 def test_build_order_exit_is_reduce_only(): o = build_rebalance_order("ETH-PERPETUAL", target_fraction=0.0, weight=0.5, equity_usd=2000, current_pos_usd=1000.0) assert o["side"] == "sell" and o["reduce_only"] is True and o["amount"] == 1000 def test_build_order_already_at_target_is_none(): o = build_rebalance_order("BTC-PERPETUAL", 1.0, 0.5, 2000, current_pos_usd=1000.0) assert o is None # delta 0 -> nessun ordine def test_build_order_subthreshold_is_none(): # delta $3 su BTC (< mezzo step $5) -> niente ordine o = build_rebalance_order("BTC-PERPETUAL", 0.5015, 0.5, 2000, current_pos_usd=498.5) assert o is None def test_partial_rebalance_direction(): # target $1000, ho $600 -> compro $400; ho $1400 -> vendo $400 up = build_rebalance_order("BTC-PERPETUAL", 1.0, 0.5, 2000, 600.0) dn = build_rebalance_order("BTC-PERPETUAL", 1.0, 0.5, 2000, 1400.0) assert up["side"] == "buy" and up["amount"] == 400 assert dn["side"] == "sell" and dn["amount"] == 400 and dn["reduce_only"] is False def test_parity_live_target_equals_backtest(): # il target live (current_target) DEVE essere l'ultimo della serie causale del backtest from src.backtest.harness import load tp = TrendPortfolio(**CANONICAL) df = resample_1d(load("BTC", "1h")) assert tp.current_target(df) == tp.target_series(df)[-1]