ivolatility-backtesting 2.148__tar.gz → 2.149__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ivolatility-backtesting might be problematic. Click here for more details.

Files changed (17) hide show
  1. {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/PKG-INFO +2 -2
  2. {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/ivolatility_backtesting/ivolatility_backtesting.py +32 -7
  3. {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/ivolatility_backtesting.egg-info/PKG-INFO +2 -2
  4. {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/ivolatility_backtesting.egg-info/SOURCES.txt +2 -1
  5. {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/ivolatility_backtesting.egg-info/requires.txt +1 -1
  6. {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/pyproject.toml +2 -2
  7. ivolatility_backtesting-2.149/tests/test_2149_client_errors.py +131 -0
  8. {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/README.md +0 -0
  9. {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/ivolatility_backtesting/__init__.py +0 -0
  10. {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/ivolatility_backtesting.egg-info/dependency_links.txt +0 -0
  11. {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/ivolatility_backtesting.egg-info/top_level.txt +0 -0
  12. {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/setup.cfg +0 -0
  13. {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/tests/test_2142_fixes.py +0 -0
  14. {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/tests/test_2144_duckdb_dedup.py +0 -0
  15. {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/tests/test_2146_vix_vro.py +0 -0
  16. {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/tests/test_2147_cents_multiplier.py +0 -0
  17. {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/tests/test_2148_loader_memory.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivolatility_backtesting
3
- Version: 2.148
3
+ Version: 2.149
4
4
  Summary: A universal backtesting framework for financial strategies using the IVolatility API.
5
5
  Author-email: IVolatility <support@ivolatility.com>
6
6
  Project-URL: Homepage, https://ivolatility.com
@@ -19,7 +19,7 @@ Requires-Dist: pandas<3.0,>=1.5.0
19
19
  Requires-Dist: numpy<3.0,>=1.21.0
20
20
  Requires-Dist: matplotlib>=3.5.0
21
21
  Requires-Dist: seaborn>=0.11.0
22
- Requires-Dist: ivolatility>=1.8.2
22
+ Requires-Dist: ivolatility>=1.9.6
23
23
  Requires-Dist: psutil>=7.1.0
24
24
 
25
25
  # IVolatility Backtesting Framework v1.32
@@ -40,13 +40,24 @@ import os
40
40
  # Distinctive User-Agent on ALL REST traffic: lets the gateway/Graylog tell
41
41
  # framework calls apart from hand-written scripts, and workspace runs from
42
42
  # local pip installs (CODER_AGENT_URL is set inside Coder workspaces only).
43
+ def _ivb_client():
44
+ if os.getenv('IVOL_CLIENT'):
45
+ return os.getenv('IVOL_CLIENT')
46
+ if os.getenv('CODER_AGENT_URL'):
47
+ return 'workspace'
48
+ # venv created by the IVolAI plugin (ivolai-dist setup_env.sh); pods use /home/coder/.venv
49
+ if os.path.normpath(__import__('sys').prefix).endswith(os.path.join('ivolatility', '.venv')):
50
+ return 'plugin'
51
+ return 'local'
52
+
53
+
43
54
  def _ivb_user_agent():
44
55
  try:
45
56
  from importlib.metadata import version as _pkg_version
46
57
  _ver = _pkg_version('ivolatility_backtesting')
47
58
  except Exception:
48
59
  _ver = 'dev'
49
- _env = 'workspace' if os.getenv('CODER_AGENT_URL') else 'local'
60
+ _env = _ivb_client()
50
61
  try:
51
62
  import requests as _rq
52
63
  _rq_ver = _rq.__version__
@@ -1793,8 +1804,10 @@ def _create_sdk_like_session():
1793
1804
  from urllib3.util.retry import Retry
1794
1805
  from requests.adapters import HTTPAdapter
1795
1806
  s = _req.Session()
1796
- retry = Retry(total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504],
1797
- allowed_methods=["GET", "POST", "DELETE"])
1807
+ # SDK >= 1.9.6 pauses before the first retry; reuse it so thread sessions behave the same
1808
+ retry = getattr(_ivol_inner, '_retry_strategy', None) or Retry(
1809
+ total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504],
1810
+ allowed_methods=["GET", "POST", "DELETE"])
1798
1811
  adapter = HTTPAdapter(pool_connections=10, pool_maxsize=10, max_retries=retry)
1799
1812
  s.mount("https://", adapter)
1800
1813
  s.mount("http://", adapter)
@@ -7822,8 +7835,9 @@ def _api_call_direct(endpoint: str, max_retries: int = 3, **kwargs) -> Optional[
7822
7835
  return None
7823
7836
  df = data if isinstance(data, pd.DataFrame) else pd.DataFrame(data)
7824
7837
  return df if not df.empty else None
7825
- except RETRYABLE:
7826
- if attempt == max_retries - 1:
7838
+ except RETRYABLE as e:
7839
+ if _is_client_error(e) or attempt == max_retries - 1:
7840
+ _safe_print(f" ⚠️ {endpoint}: {e}")
7827
7841
  return None
7828
7842
  continue
7829
7843
  except Exception:
@@ -7831,6 +7845,12 @@ def _api_call_direct(endpoint: str, max_retries: int = 3, **kwargs) -> Optional[
7831
7845
  return None
7832
7846
 
7833
7847
 
7848
+ def _is_client_error(exc):
7849
+ """4xx other than 429: the same request cannot succeed on retry."""
7850
+ status = getattr(getattr(exc, 'response', None), 'status_code', None)
7851
+ return status is not None and 400 <= status < 500 and status != 429
7852
+
7853
+
7834
7854
  def api_call(endpoint, cache_config=None, debug=False, max_retries=3, skip_parquet_cache=False,
7835
7855
  _chunk_info=None, **kwargs):
7836
7856
  """
@@ -7927,6 +7947,8 @@ def api_call(endpoint, cache_config=None, debug=False, max_retries=3, skip_parqu
7927
7947
 
7928
7948
  except RETRYABLE_ERRORS as e:
7929
7949
  last_exception = e
7950
+ if _is_client_error(e):
7951
+ raise
7930
7952
  if debug_level >= 1:
7931
7953
  print(f"[RETRY] Network error on attempt {attempt + 1}/{max_retries}: {type(e).__name__}: {e}")
7932
7954
 
@@ -21853,8 +21875,11 @@ def _load_futures_for_hedge(config, preloaded):
21853
21875
  if r.status_code == 200:
21854
21876
  data = r.json().get('data', [])
21855
21877
  all_rows.extend(data)
21856
- except Exception:
21857
- pass
21878
+ else:
21879
+ print(f" ⚠️ futures/eod/prices {fut_root} {current:%Y-%m-%d}..{chunk_end:%Y-%m-%d}: "
21880
+ f"HTTP {r.status_code} {r.text[:300]}")
21881
+ except Exception as e:
21882
+ print(f" ⚠️ futures/eod/prices {fut_root} {current:%Y-%m-%d}: {e}")
21858
21883
  current = chunk_end + _td(days=1)
21859
21884
 
21860
21885
  if all_rows:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivolatility_backtesting
3
- Version: 2.148
3
+ Version: 2.149
4
4
  Summary: A universal backtesting framework for financial strategies using the IVolatility API.
5
5
  Author-email: IVolatility <support@ivolatility.com>
6
6
  Project-URL: Homepage, https://ivolatility.com
@@ -19,7 +19,7 @@ Requires-Dist: pandas<3.0,>=1.5.0
19
19
  Requires-Dist: numpy<3.0,>=1.21.0
20
20
  Requires-Dist: matplotlib>=3.5.0
21
21
  Requires-Dist: seaborn>=0.11.0
22
- Requires-Dist: ivolatility>=1.8.2
22
+ Requires-Dist: ivolatility>=1.9.6
23
23
  Requires-Dist: psutil>=7.1.0
24
24
 
25
25
  # IVolatility Backtesting Framework v1.32
@@ -12,4 +12,5 @@ tests/test_2142_fixes.py
12
12
  tests/test_2144_duckdb_dedup.py
13
13
  tests/test_2146_vix_vro.py
14
14
  tests/test_2147_cents_multiplier.py
15
- tests/test_2148_loader_memory.py
15
+ tests/test_2148_loader_memory.py
16
+ tests/test_2149_client_errors.py
@@ -2,5 +2,5 @@ pandas<3.0,>=1.5.0
2
2
  numpy<3.0,>=1.21.0
3
3
  matplotlib>=3.5.0
4
4
  seaborn>=0.11.0
5
- ivolatility>=1.8.2
5
+ ivolatility>=1.9.6
6
6
  psutil>=7.1.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ivolatility_backtesting"
7
- version = "2.148"
7
+ version = "2.149"
8
8
  description = "A universal backtesting framework for financial strategies using the IVolatility API."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -17,7 +17,7 @@ dependencies = [
17
17
  "numpy>=1.21.0,<3.0",
18
18
  "matplotlib>=3.5.0",
19
19
  "seaborn>=0.11.0",
20
- "ivolatility>=1.8.2",
20
+ "ivolatility>=1.9.6",
21
21
  "psutil>=7.1.0",
22
22
  ]
23
23
  keywords = ["backtesting", "finance", "trading", "ivolatility"]
@@ -0,0 +1,131 @@
1
+ # No retry on 4xx, pause before retrying a 429, client tag in User-Agent.
2
+ # Run: python3 tests/test_2149_client_errors.py (or pytest). No network: a local server stands in for the API.
3
+ import json
4
+ import os
5
+ import sys
6
+ import threading
7
+ import time
8
+ import traceback
9
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
10
+
11
+ import matplotlib
12
+ matplotlib.use('Agg')
13
+
14
+ HITS = {}
15
+
16
+
17
+ class _Api(BaseHTTPRequestHandler):
18
+ def log_message(self, *args):
19
+ pass
20
+
21
+ def _reply(self, code, body):
22
+ data = json.dumps(body).encode()
23
+ self.send_response(code)
24
+ self.send_header('Content-Type', 'application/json')
25
+ self.send_header('Content-Length', str(len(data)))
26
+ self.end_headers()
27
+ self.wfile.write(data)
28
+
29
+ def do_GET(self):
30
+ path = self.path.split('?')[0]
31
+ HITS[path] = HITS.get(path, 0) + 1
32
+ if path == '/busy' and HITS[path] == 1:
33
+ self._reply(429, {'code': 429, 'message': 'per-second rate limit'})
34
+ elif path == '/equities/eod/stock-prices':
35
+ self._reply(403, {'code': 403, 'name': 'Forbidden',
36
+ 'message': 'Trial records limit exhausted. To continue, please upgrade to a paid plan'})
37
+ else:
38
+ self._reply(200, {'ok': True})
39
+
40
+
41
+ # CI runners set HTTP_PROXY; the local stand-in API must bypass it
42
+ for _var in ('NO_PROXY', 'no_proxy'):
43
+ os.environ[_var] = ','.join(filter(None, [os.environ.get(_var), '127.0.0.1', 'localhost']))
44
+
45
+ _SERVER = ThreadingHTTPServer(('127.0.0.1', 0), _Api)
46
+ threading.Thread(target=_SERVER.serve_forever, daemon=True).start()
47
+ _BASE = f'http://127.0.0.1:{_SERVER.server_address[1]}'
48
+ os.environ['IVOL_API_BASE_URL'] = _BASE
49
+ os.environ['API_KEY'] = 'TESTKEY000000000'
50
+
51
+ _REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
52
+ sys.path.insert(0, os.path.join(_REPO, 'ivolatility_backtesting'))
53
+ import ivolatility_backtesting as lib # noqa: E402
54
+ import requests # noqa: E402
55
+
56
+
57
+ def _sdk_version():
58
+ return tuple(int(x) for x in lib.ivol.__version__.split('.')[:3])
59
+
60
+
61
+ def test_client_error_not_retried():
62
+ HITS.pop('/equities/eod/stock-prices', None)
63
+ try:
64
+ lib.api_call('/equities/eod/stock-prices', symbol='AAPL', from_='2024-01-02', to='2024-01-03')
65
+ raise AssertionError('403 must raise')
66
+ except requests.HTTPError as e:
67
+ assert e.response.status_code == 403
68
+ if _sdk_version() >= (1, 9, 6):
69
+ assert 'Trial records limit' in str(e)
70
+ assert HITS['/equities/eod/stock-prices'] == 1
71
+
72
+
73
+ def test_direct_call_not_retried_on_client_error():
74
+ HITS.pop('/equities/eod/stock-prices', None)
75
+ assert lib._api_call_direct('/equities/eod/stock-prices', symbol='AAPL',
76
+ from_='2024-01-02', to='2024-01-03') is None
77
+ assert HITS['/equities/eod/stock-prices'] == 1
78
+
79
+
80
+ def test_thread_session_pauses_before_retrying_429():
81
+ if _sdk_version() < (1, 9, 6):
82
+ return
83
+ HITS.pop('/busy', None)
84
+ t0 = time.time()
85
+ r = lib._create_sdk_like_session().get(f'{_BASE}/busy', timeout=10)
86
+ assert r.status_code == 200 and HITS['/busy'] == 2
87
+ assert time.time() - t0 >= 1.0
88
+
89
+
90
+ def test_is_client_error():
91
+ class E(Exception):
92
+ def __init__(self, code):
93
+ self.response = type('R', (), {'status_code': code})()
94
+ assert lib._is_client_error(E(403)) and lib._is_client_error(E(400))
95
+ assert not lib._is_client_error(E(429)) and not lib._is_client_error(E(500))
96
+ assert not lib._is_client_error(ValueError())
97
+
98
+
99
+ def test_client_tag():
100
+ saved = {k: os.environ.pop(k, None) for k in ('IVOL_CLIENT', 'CODER_AGENT_URL')}
101
+ saved_prefix = sys.prefix
102
+ try:
103
+ sys.prefix = '/opt/venv'
104
+ assert lib._ivb_client() == 'local'
105
+ sys.prefix = os.path.join('/Users/x', 'ivolatility', '.venv')
106
+ assert lib._ivb_client() == 'plugin'
107
+ os.environ['CODER_AGENT_URL'] = 'http://agent'
108
+ assert lib._ivb_client() == 'workspace'
109
+ os.environ['IVOL_CLIENT'] = 'chat'
110
+ assert lib._ivb_client() == 'chat'
111
+ finally:
112
+ sys.prefix = saved_prefix
113
+ for k, v in saved.items():
114
+ os.environ.pop(k, None)
115
+ if v is not None:
116
+ os.environ[k] = v
117
+
118
+
119
+ if __name__ == '__main__':
120
+ failed = 0
121
+ for name, fn in list(globals().items()):
122
+ if name.startswith('test_') and callable(fn):
123
+ try:
124
+ fn()
125
+ print(f'PASS {name}')
126
+ except Exception:
127
+ failed += 1
128
+ print(f'FAIL {name}')
129
+ traceback.print_exc()
130
+ _SERVER.shutdown()
131
+ sys.exit(1 if failed else 0)