#!/usr/bin/env python3 """ DERIBIT API FIX - UNIT TESTS WITH CONTROL VALUES ================================================ Control Values are historical known-good data points for validation. === MARCH 2026 CONTROL VALUES (from 2026-03-19 12:00 UTC) === - DVOL BTC: 54.88 - DVOL ETH: 79.13 - Funding BTC: 2.7150135764992048e-06 - Funding ETH: -8.348741006833234e-07 === MID-FEBRUARY 2026 CONTROL VALUES (from 2026-02-15 12:00 UTC) === - Funding BTC: 0.0001770952404538274 (17.7 bps - high funding period) - Funding ETH: -4.402918249409541e-06 (-0.44 bps - negative funding) Note: DVOL historical data has limited retention. Tests use March 2026 as primary control, with range validation for current values. === CURRENT VALID RANGES === - DVOL BTC: 30.0 - 150.0 (typical BTC volatility range) - DVOL ETH: 40.0 - 200.0 (ETH typically higher vol than BTC) - Funding: -0.001 to +0.001 (-100 bps to +100 bps) These tests verify: 1. URL builder generates correct timestamps 2. Parsers extract correct values from API response format 3. Values match known control values within tolerance 4. Current values fall within expected ranges """ import unittest import json import time from datetime import datetime, timezone import sys from pathlib import Path # Add parent to path sys.path.insert(0, str(Path(__file__).parent)) sys.path.insert(0, str(Path(__file__).parent.parent / "external_factors")) from realtime_exf_service import RealTimeExFService, Parsers, INDICATORS # === CONTROL VALUES FROM KNOWN HISTORICAL DATA === # March 19, 2026 12:00 UTC (verified from API) CONTROL_VALUES_MAR_2026 = { 'timestamp': 1773931200, 'date': '2026-03-19T12:00:00+00:00', 'dvol_btc': 54.88, 'dvol_eth': 79.13, 'fund_dbt_btc': 2.7150135764992048e-06, 'fund_dbt_eth': -8.348741006833234e-07, } # February 15, 2026 12:00 UTC (verified from API) CONTROL_VALUES_FEB_2026 = { 'timestamp': 1739611200, 'date': '2026-02-15T12:00:00+00:00', 'fund_dbt_btc': 0.0001770952404538274, # 17.7 bps - elevated funding 'fund_dbt_eth': -4.402918249409541e-06, # -0.44 bps - negative funding } # Valid ranges for sanity checks VALID_RANGES = { 'dvol_btc': (30.0, 150.0), # BTC vol typically 30-150% 'dvol_eth': (40.0, 200.0), # ETH vol typically 40-200% 'fund_dbt_btc': (-0.001, 0.001), # -100 bps to +100 bps 'fund_dbt_eth': (-0.001, 0.001), # -100 bps to +100 bps } # Mock API responses MOCK_DVOL_RESPONSE_MAR_2026 = { "jsonrpc": "2.0", "result": { "data": [ [1773931140000, 54.87, 54.87, 54.86, 54.86], # 11:59 [1773931200000, 54.87, 54.88, 54.87, CONTROL_VALUES_MAR_2026['dvol_btc']], # 12:00 [1773931260000, 54.88, 54.88, 54.88, CONTROL_VALUES_MAR_2026['dvol_btc']], # 12:01 ], "status": "ok" }, "usIn": 1773931200000000, "usOut": 1773931200000100, "usDiff": 100 } MOCK_FUNDING_RESPONSE_MAR_2026 = { "jsonrpc": "2.0", "result": CONTROL_VALUES_MAR_2026['fund_dbt_btc'], "usIn": 1773931200000000, "usOut": 1773931200000100, "usDiff": 100 } MOCK_FUNDING_RESPONSE_FEB_2026 = { "jsonrpc": "2.0", "result": CONTROL_VALUES_FEB_2026['fund_dbt_btc'], "usIn": 1739611200000000, "usOut": 1739611200000100, "usDiff": 100 } class TestDeribitURLBuilder(unittest.TestCase): """Test the _build_deribit_url method.""" def setUp(self): self.svc = RealTimeExFService() def test_dvol_btc_url_structure(self): """DVOL BTC URL should contain required parameters.""" url = self.svc._build_deribit_url('dvol:BTC') self.assertIn('currency=BTC', url) self.assertIn('resolution=60', url) self.assertIn('start_timestamp=', url) self.assertIn('end_timestamp=', url) self.assertIn('get_volatility_index_data', url) def test_dvol_eth_url_structure(self): """DVOL ETH URL should contain ETH parameter.""" url = self.svc._build_deribit_url('dvol:ETH') self.assertIn('currency=ETH', url) self.assertIn('get_volatility_index_data', url) def test_funding_btc_url_structure(self): """Funding BTC URL should contain instrument name.""" url = self.svc._build_deribit_url('funding:BTC-PERPETUAL') self.assertIn('instrument_name=BTC-PERPETUAL', url) self.assertIn('get_funding_rate_value', url) self.assertIn('start_timestamp=', url) self.assertIn('end_timestamp=', url) def test_timestamp_range(self): """Timestamps should bracket a reasonable time window.""" url = self.svc._build_deribit_url('dvol:BTC') # Extract timestamps import re start_match = re.search(r'start_timestamp=(\d+)', url) end_match = re.search(r'end_timestamp=(\d+)', url) self.assertIsNotNone(start_match) self.assertIsNotNone(end_match) start_ts = int(start_match.group(1)) end_ts = int(end_match.group(1)) # End should be after start self.assertGreater(end_ts, start_ts) # Window should be approximately 2 hours for DVOL window_ms = end_ts - start_ts self.assertAlmostEqual(window_ms, 7200 * 1000, delta=60000) # ±1 minute def test_funding_window_24h(self): """Funding URL should use 24-hour window.""" url = self.svc._build_deribit_url('funding:BTC-PERPETUAL') import re start_match = re.search(r'start_timestamp=(\d+)', url) end_match = re.search(r'end_timestamp=(\d+)', url) start_ts = int(start_match.group(1)) end_ts = int(end_match.group(1)) # Window should be approximately 24 hours window_ms = end_ts - start_ts self.assertAlmostEqual(window_ms, 86400 * 1000, delta=60000) class TestDeribitParsersMarch2026(unittest.TestCase): """Test parsers against March 2026 control values.""" def test_parse_dvol_btc_march_control(self): """Parser should extract March 2026 control value 54.88.""" result = Parsers.parse_deribit_dvol(MOCK_DVOL_RESPONSE_MAR_2026) self.assertIsInstance(result, float) self.assertAlmostEqual( result, CONTROL_VALUES_MAR_2026['dvol_btc'], places=2, msg=f"Expected {CONTROL_VALUES_MAR_2026['dvol_btc']}, got {result}" ) def test_parse_dvol_eth_march_control(self): """Parser should extract March 2026 ETH control value 79.13.""" # Modify mock for ETH mock_eth = json.loads(json.dumps(MOCK_DVOL_RESPONSE_MAR_2026)) mock_eth['result']['data'] = [ [1773931200000, 79.10, 79.15, 79.10, CONTROL_VALUES_MAR_2026['dvol_eth']] ] result = Parsers.parse_deribit_dvol(mock_eth) self.assertIsInstance(result, float) self.assertAlmostEqual( result, CONTROL_VALUES_MAR_2026['dvol_eth'], places=1 ) def test_parse_funding_btc_march_control(self): """Parser should extract March 2026 funding control value.""" result = Parsers.parse_deribit_fund(MOCK_FUNDING_RESPONSE_MAR_2026) self.assertIsInstance(result, float) self.assertAlmostEqual( result, CONTROL_VALUES_MAR_2026['fund_dbt_btc'], places=15 ) def test_parse_funding_btc_feb_control(self): """Parser should extract February 2026 funding control value (17.7 bps).""" result = Parsers.parse_deribit_fund(MOCK_FUNDING_RESPONSE_FEB_2026) self.assertIsInstance(result, float) self.assertAlmostEqual( result, CONTROL_VALUES_FEB_2026['fund_dbt_btc'], places=15 ) # Verify it's in the elevated range (positive funding = shorts pay longs) self.assertGreater(result, 0.0001) # > 10 bps class TestDeribitParsersEdgeCases(unittest.TestCase): """Test parser edge cases and error handling.""" def test_parse_dvol_empty_response(self): """Parser should return 0.0 for empty response.""" result = Parsers.parse_deribit_dvol({}) self.assertEqual(result, 0.0) def test_parse_dvol_no_data(self): """Parser should return 0.0 when no data array.""" result = Parsers.parse_deribit_dvol({"result": {"data": []}}) self.assertEqual(result, 0.0) def test_parse_dvol_missing_result(self): """Parser should return 0.0 when result missing.""" result = Parsers.parse_deribit_dvol({"error": "some_error"}) self.assertEqual(result, 0.0) def test_parse_funding_scalar_result(self): """Parser should handle scalar (non-array) result.""" mock = {"result": 0.00001} result = Parsers.parse_deribit_fund(mock) self.assertEqual(result, 0.00001) def test_parse_funding_list_result(self): """Parser should handle list result with interest_8h field.""" mock = {"result": [{"interest_8h": 0.00002}]} result = Parsers.parse_deribit_fund(mock) self.assertEqual(result, 0.00002) def test_parse_funding_empty(self): """Parser should return 0.0 for empty funding response.""" result = Parsers.parse_deribit_fund({}) self.assertEqual(result, 0.0) class TestIndicatorConfig(unittest.TestCase): """Test that indicator metadata is correctly configured.""" def test_dvol_btc_config(self): """DVOL BTC should have correct metadata.""" meta = INDICATORS['dvol_btc'] self.assertEqual(meta.name, 'dvol_btc') self.assertEqual(meta.source, 'deribit') self.assertTrue(meta.url.startswith('dvol:')) self.assertEqual(meta.parser, 'parse_deribit_dvol') self.assertTrue(meta.acb_critical) self.assertEqual(meta.optimal_lag_days, 1) def test_dvol_eth_config(self): """DVOL ETH should have correct metadata.""" meta = INDICATORS['dvol_eth'] self.assertEqual(meta.name, 'dvol_eth') self.assertTrue(meta.url.startswith('dvol:')) self.assertTrue(meta.acb_critical) def test_funding_btc_config(self): """Funding BTC should have correct metadata.""" meta = INDICATORS['fund_dbt_btc'] self.assertEqual(meta.name, 'fund_dbt_btc') self.assertTrue(meta.url.startswith('funding:')) def test_all_deribit_indicators_exist(self): """All expected Deribit indicators should be configured.""" expected = ['dvol_btc', 'dvol_eth', 'fund_dbt_btc', 'fund_dbt_eth'] for ind in expected: self.assertIn(ind, INDICATORS) self.assertEqual(INDICATORS[ind].source, 'deribit') class TestLiveAPIRanges(unittest.TestCase): """Live API tests - validates current values are in expected ranges.""" def test_live_dvol_in_valid_range(self): """Live DVOL should be in valid range (30-150 for BTC).""" import os if os.environ.get('RUN_LIVE_TESTS') != '1': self.skipTest("Set RUN_LIVE_TESTS=1 to run live tests") import aiohttp import asyncio async def fetch(): svc = RealTimeExFService() url = svc._build_deribit_url('dvol:BTC') async with aiohttp.ClientSession() as session: async with session.get(url) as resp: self.assertEqual(resp.status, 200) data = await resp.json() value = Parsers.parse_deribit_dvol(data) # Validate range min_val, max_val = VALID_RANGES['dvol_btc'] self.assertGreaterEqual(value, min_val) self.assertLessEqual(value, max_val) return value value = asyncio.run(fetch()) print(f"\nLive DVOL BTC: {value} (valid range: {VALID_RANGES['dvol_btc']})") def test_live_funding_in_valid_range(self): """Live funding should be in valid range (-0.001 to +0.001).""" import os if os.environ.get('RUN_LIVE_TESTS') != '1': self.skipTest("Set RUN_LIVE_TESTS=1 to run live tests") import aiohttp import asyncio async def fetch(): svc = RealTimeExFService() url = svc._build_deribit_url('funding:BTC-PERPETUAL') async with aiohttp.ClientSession() as session: async with session.get(url) as resp: data = await resp.json() value = Parsers.parse_deribit_fund(data) min_val, max_val = VALID_RANGES['fund_dbt_btc'] self.assertGreaterEqual(value, min_val) self.assertLessEqual(value, max_val) return value value = asyncio.run(fetch()) print(f"\nLive Funding BTC: {value}") def run_tests(): """Run all tests.""" loader = unittest.TestLoader() suite = unittest.TestSuite() # Add all test classes suite.addTests(loader.loadTestsFromTestCase(TestDeribitURLBuilder)) suite.addTests(loader.loadTestsFromTestCase(TestDeribitParsersMarch2026)) suite.addTests(loader.loadTestsFromTestCase(TestDeribitParsersEdgeCases)) suite.addTests(loader.loadTestsFromTestCase(TestIndicatorConfig)) # Skip live tests by default # suite.addTests(loader.loadTestsFromTestCase(TestLiveAPIRanges)) runner = unittest.TextTestRunner(verbosity=2) result = runner.run(suite) return result.wasSuccessful() if __name__ == '__main__': success = run_tests() sys.exit(0 if success else 1)