initial: import DOLPHIN baseline 2026-04-21 from dolphinng5_predict working tree
Includes core prod + GREEN/BLUE subsystems: - prod/ (BLUE harness, configs, scripts, docs) - nautilus_dolphin/ (GREEN Nautilus-native impl + dvae/ preserved) - adaptive_exit/ (AEM engine + models/bucket_assignments.pkl) - Observability/ (EsoF advisor, TUI, dashboards) - external_factors/ (EsoF producer) - mc_forewarning_qlabs_fork/ (MC regime/envelope) Excludes runtime caches, logs, backups, and reproducible artifacts per .gitignore.
This commit is contained in:
392
prod/test_deribit_fix.py
Executable file
392
prod/test_deribit_fix.py
Executable file
@@ -0,0 +1,392 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user