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.
- {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/PKG-INFO +2 -2
- {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/ivolatility_backtesting/ivolatility_backtesting.py +32 -7
- {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/ivolatility_backtesting.egg-info/PKG-INFO +2 -2
- {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/ivolatility_backtesting.egg-info/SOURCES.txt +2 -1
- {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/ivolatility_backtesting.egg-info/requires.txt +1 -1
- {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/pyproject.toml +2 -2
- ivolatility_backtesting-2.149/tests/test_2149_client_errors.py +131 -0
- {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/README.md +0 -0
- {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/ivolatility_backtesting/__init__.py +0 -0
- {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/ivolatility_backtesting.egg-info/dependency_links.txt +0 -0
- {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/ivolatility_backtesting.egg-info/top_level.txt +0 -0
- {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/setup.cfg +0 -0
- {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/tests/test_2142_fixes.py +0 -0
- {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/tests/test_2144_duckdb_dedup.py +0 -0
- {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/tests/test_2146_vix_vro.py +0 -0
- {ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/tests/test_2147_cents_multiplier.py +0 -0
- {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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
1797
|
-
|
|
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
|
-
|
|
21857
|
-
|
|
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.
|
|
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.
|
|
22
|
+
Requires-Dist: ivolatility>=1.9.6
|
|
23
23
|
Requires-Dist: psutil>=7.1.0
|
|
24
24
|
|
|
25
25
|
# IVolatility Backtesting Framework v1.32
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ivolatility_backtesting"
|
|
7
|
-
version = "2.
|
|
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.
|
|
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)
|
|
File without changes
|
{ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/ivolatility_backtesting/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/tests/test_2144_duckdb_dedup.py
RENAMED
|
File without changes
|
|
File without changes
|
{ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/tests/test_2147_cents_multiplier.py
RENAMED
|
File without changes
|
{ivolatility_backtesting-2.148 → ivolatility_backtesting-2.149}/tests/test_2148_loader_memory.py
RENAMED
|
File without changes
|