simudyne-pulse 0.7.0.dev14__tar.gz → 0.7.0.dev15__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.
- {simudyne_pulse-0.7.0.dev14/src/simudyne_pulse.egg-info → simudyne_pulse-0.7.0.dev15}/PKG-INFO +1 -1
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/pyproject.toml +1 -1
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/src/simudyne/client.py +79 -0
- simudyne_pulse-0.7.0.dev15/src/simudyne/exceptions.py +44 -0
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/src/simudyne/resources/api_keys.py +35 -0
- simudyne_pulse-0.7.0.dev15/src/simudyne/resources/data.py +109 -0
- simudyne_pulse-0.7.0.dev15/src/simudyne/resources/fix.py +75 -0
- simudyne_pulse-0.7.0.dev15/src/simudyne/resources/fm.py +477 -0
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/src/simudyne/resources/profile.py +45 -0
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/src/simudyne/resources/simulation.py +572 -220
- simudyne_pulse-0.7.0.dev15/src/simudyne/resources/simulator_gym.py +284 -0
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/src/simudyne/resources/validation.py +5 -3
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15/src/simudyne_pulse.egg-info}/PKG-INFO +1 -1
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/src/simudyne_pulse.egg-info/SOURCES.txt +0 -1
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/tests/test_new_resources.py +1 -1
- simudyne_pulse-0.7.0.dev14/src/simudyne/exceptions.py +0 -7
- simudyne_pulse-0.7.0.dev14/src/simudyne/resources/data.py +0 -57
- simudyne_pulse-0.7.0.dev14/src/simudyne/resources/fix.py +0 -32
- simudyne_pulse-0.7.0.dev14/src/simudyne/resources/fm.py +0 -227
- simudyne_pulse-0.7.0.dev14/src/simudyne/resources/historical.py +0 -1
- simudyne_pulse-0.7.0.dev14/src/simudyne/resources/simulator_gym.py +0 -131
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/LICENSE +0 -0
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/README.md +0 -0
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/setup.cfg +0 -0
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/src/simudyne/__init__.py +0 -0
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/src/simudyne/resources/__init__.py +0 -0
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/src/simudyne_pulse.egg-info/requires.txt +0 -0
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/tests/test_simulator_gym.py +0 -0
- {simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/tests/test_validation.py +0 -0
|
@@ -12,6 +12,75 @@ from simudyne.exceptions import PulseAPIError
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class PulseABM:
|
|
15
|
+
"""Client for the Simudyne Pulse synthetic market data API.
|
|
16
|
+
|
|
17
|
+
The client holds the API key and HTTP session, and exposes every endpoint
|
|
18
|
+
through a resource attribute (``client.simulation``, ``client.data``, ...).
|
|
19
|
+
Construct it once and reuse it; the underlying :class:`requests.Session`
|
|
20
|
+
keeps connections alive across calls.
|
|
21
|
+
|
|
22
|
+
Transient failures (HTTP 429, 502, 503, 504) and network timeouts are
|
|
23
|
+
retried automatically with exponential backoff. Any other non-2xx response
|
|
24
|
+
raises :class:`~simudyne.exceptions.PulseAPIError`.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
api_key : str, optional
|
|
29
|
+
Pulse API key (``pk_live_...``). Falls back to the ``SIMUDYNE_API_KEY``
|
|
30
|
+
environment variable when omitted.
|
|
31
|
+
base_url : str, optional
|
|
32
|
+
API root, with no trailing slash. Falls back to the
|
|
33
|
+
``SIMUDYNE_BASE_URL`` environment variable, then to
|
|
34
|
+
``https://pulse-api.simudyne.com``.
|
|
35
|
+
timeout : int, optional
|
|
36
|
+
Per-request timeout in seconds. Defaults to 30.
|
|
37
|
+
max_retries : int, default 3
|
|
38
|
+
Retry attempts for transient errors. Backoff is ``2 ** attempt``
|
|
39
|
+
seconds, so the default waits 1s, 2s then 4s before giving up.
|
|
40
|
+
|
|
41
|
+
Attributes
|
|
42
|
+
----------
|
|
43
|
+
profile : ProfileResource
|
|
44
|
+
Account details, usage and download history.
|
|
45
|
+
api_keys : ApiKeysResource
|
|
46
|
+
Create, list and revoke API keys.
|
|
47
|
+
data : DataResource
|
|
48
|
+
The catalog of calibrated symbols available to simulate.
|
|
49
|
+
simulation : SimulationResource
|
|
50
|
+
Submit agent-based simulations and retrieve their results.
|
|
51
|
+
validation : ValidationResource
|
|
52
|
+
Score synthetic data against real data.
|
|
53
|
+
fm : FmResource
|
|
54
|
+
Foundation-model generation and its job lifecycle.
|
|
55
|
+
fix : FixResource
|
|
56
|
+
FIX gateway usage.
|
|
57
|
+
simulator_gym : SimulatorGymResource
|
|
58
|
+
Gym-style reinforcement-learning environment over a WebSocket.
|
|
59
|
+
|
|
60
|
+
Raises
|
|
61
|
+
------
|
|
62
|
+
ValueError
|
|
63
|
+
If no API key is given and ``SIMUDYNE_API_KEY`` is unset.
|
|
64
|
+
|
|
65
|
+
Examples
|
|
66
|
+
--------
|
|
67
|
+
Key taken from the environment:
|
|
68
|
+
|
|
69
|
+
>>> import os
|
|
70
|
+
>>> os.environ["SIMUDYNE_API_KEY"] = "pk_live_..."
|
|
71
|
+
>>> client = PulseABM()
|
|
72
|
+
|
|
73
|
+
Key passed explicitly, against a non-default deployment:
|
|
74
|
+
|
|
75
|
+
>>> client = PulseABM(
|
|
76
|
+
... api_key="pk_live_...",
|
|
77
|
+
... base_url="https://pulse-api-dev.simudyne.com",
|
|
78
|
+
... timeout=60,
|
|
79
|
+
... )
|
|
80
|
+
>>> client.profile.get()["email"]
|
|
81
|
+
'you@example.com'
|
|
82
|
+
"""
|
|
83
|
+
|
|
15
84
|
DEFAULT_BASE_URL = "https://pulse-api.simudyne.com"
|
|
16
85
|
DEFAULT_TIMEOUT = 30
|
|
17
86
|
RETRYABLE_STATUS_CODES = {429, 502, 503, 504}
|
|
@@ -41,6 +110,8 @@ class PulseABM:
|
|
|
41
110
|
from simudyne.resources.simulation import SimulationResource
|
|
42
111
|
from simudyne.resources.simulator_gym import SimulatorGymResource
|
|
43
112
|
from simudyne.resources.validation import ValidationResource
|
|
113
|
+
from simudyne.resources.fm import FmResource
|
|
114
|
+
from simudyne.resources.fix import FixResource
|
|
44
115
|
|
|
45
116
|
self.profile = ProfileResource(self)
|
|
46
117
|
self.api_keys = ApiKeysResource(self)
|
|
@@ -50,6 +121,8 @@ class PulseABM:
|
|
|
50
121
|
self.simulation = SimulationResource(self)
|
|
51
122
|
self.simulator_gym = SimulatorGymResource(self)
|
|
52
123
|
self.validation = ValidationResource(self)
|
|
124
|
+
self.fm = FmResource(self)
|
|
125
|
+
self.fix = FixResource(self)
|
|
53
126
|
|
|
54
127
|
def _request_with_retries(self, method: str, url: str, **kwargs):
|
|
55
128
|
"""Execute request with timeout and exponential backoff for transient errors."""
|
|
@@ -89,6 +162,12 @@ class PulseABM:
|
|
|
89
162
|
response = self._request_with_retries(method, url, **kwargs)
|
|
90
163
|
return response.json()
|
|
91
164
|
|
|
165
|
+
def _request_text(self, method: str, endpoint: str, **kwargs):
|
|
166
|
+
"""Like _request, for endpoints that answer text/plain not JSON."""
|
|
167
|
+
url = f"{self.base_url}{endpoint}"
|
|
168
|
+
response = self._request_with_retries(method, url, **kwargs)
|
|
169
|
+
return response.text
|
|
170
|
+
|
|
92
171
|
def _request_csv(self, method, endpoint, **kwargs):
|
|
93
172
|
PAGE_SIZE = 100000
|
|
94
173
|
params = kwargs.get("params", {})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
class PulseAPIError(Exception):
|
|
2
|
+
"""Raised when the Pulse API returns a non-2xx response.
|
|
3
|
+
|
|
4
|
+
Every resource method raises this on API-level failure. Transient statuses
|
|
5
|
+
(429, 502, 503, 504) are retried by the client first, so receiving this
|
|
6
|
+
means the request failed after all retries were exhausted, or failed with a
|
|
7
|
+
status that is not worth retrying.
|
|
8
|
+
|
|
9
|
+
Parameters
|
|
10
|
+
----------
|
|
11
|
+
status_code : int
|
|
12
|
+
HTTP status code returned by the API.
|
|
13
|
+
detail : str
|
|
14
|
+
Message from the response body's ``detail`` field, or the raw response
|
|
15
|
+
text when the body is not JSON.
|
|
16
|
+
|
|
17
|
+
Attributes
|
|
18
|
+
----------
|
|
19
|
+
status_code : int
|
|
20
|
+
HTTP status code returned by the API.
|
|
21
|
+
detail : str
|
|
22
|
+
Human-readable explanation of the failure.
|
|
23
|
+
|
|
24
|
+
Examples
|
|
25
|
+
--------
|
|
26
|
+
>>> try:
|
|
27
|
+
... client.simulation.get_job_status("does-not-exist")
|
|
28
|
+
... except PulseAPIError as exc:
|
|
29
|
+
... print(exc.status_code, exc.detail)
|
|
30
|
+
404 Job not found
|
|
31
|
+
|
|
32
|
+
Retry only on rate limiting, and let anything else surface:
|
|
33
|
+
|
|
34
|
+
>>> try:
|
|
35
|
+
... client.simulation.run(...)
|
|
36
|
+
... except PulseAPIError as exc:
|
|
37
|
+
... if exc.status_code != 429:
|
|
38
|
+
... raise
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, status_code: int, detail: str):
|
|
42
|
+
self.status_code = status_code
|
|
43
|
+
self.detail = detail
|
|
44
|
+
super().__init__(f"API Error ({status_code}): {detail}")
|
{simudyne_pulse-0.7.0.dev14 → simudyne_pulse-0.7.0.dev15}/src/simudyne/resources/api_keys.py
RENAMED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
class ApiKeysResource:
|
|
2
|
+
"""Create, list and revoke the API keys on your account.
|
|
3
|
+
|
|
4
|
+
Reached as ``client.api_keys``. Keys are the only credential the SDK
|
|
5
|
+
accepts, and key material is returned exactly once, by :meth:`create`.
|
|
6
|
+
|
|
7
|
+
Examples
|
|
8
|
+
--------
|
|
9
|
+
Mint a key for a job, use it, then revoke it:
|
|
10
|
+
|
|
11
|
+
>>> key = client.api_keys.create(name="Nightly backtest")
|
|
12
|
+
>>> run_client = PulseABM(api_key=key["api_key"])
|
|
13
|
+
>>> client.api_keys.revoke(key_id=key["api_key_id"])
|
|
14
|
+
"""
|
|
15
|
+
|
|
2
16
|
def __init__(self, client):
|
|
3
17
|
self._client = client
|
|
4
18
|
|
|
@@ -25,6 +39,11 @@ class ApiKeysResource:
|
|
|
25
39
|
warning : str
|
|
26
40
|
A reminder that the key will not be shown again.
|
|
27
41
|
|
|
42
|
+
Raises
|
|
43
|
+
------
|
|
44
|
+
PulseAPIError
|
|
45
|
+
If the account has reached its key limit, or ``name`` is rejected.
|
|
46
|
+
|
|
28
47
|
Examples
|
|
29
48
|
--------
|
|
30
49
|
>>> new_key = client.api_keys.create(name="Training script")
|
|
@@ -52,6 +71,11 @@ class ApiKeysResource:
|
|
|
52
71
|
- last_used_at (str or None): None until the key is first used
|
|
53
72
|
- expires_at (str or None): None for keys that do not expire
|
|
54
73
|
|
|
74
|
+
Raises
|
|
75
|
+
------
|
|
76
|
+
PulseAPIError
|
|
77
|
+
If the calling key is invalid or has been revoked.
|
|
78
|
+
|
|
55
79
|
Examples
|
|
56
80
|
--------
|
|
57
81
|
>>> for key in client.api_keys.list():
|
|
@@ -75,9 +99,20 @@ class ApiKeysResource:
|
|
|
75
99
|
dict
|
|
76
100
|
The revocation result.
|
|
77
101
|
|
|
102
|
+
Raises
|
|
103
|
+
------
|
|
104
|
+
PulseAPIError
|
|
105
|
+
If ``key_id`` does not exist or does not belong to your account.
|
|
106
|
+
|
|
78
107
|
Examples
|
|
79
108
|
--------
|
|
80
109
|
>>> new_key = client.api_keys.create(name="Training script")
|
|
81
110
|
>>> client.api_keys.revoke(key_id=new_key["api_key_id"])
|
|
111
|
+
|
|
112
|
+
Revoking the key you are currently authenticating with locks you out
|
|
113
|
+
of the API, so mint the replacement first:
|
|
114
|
+
|
|
115
|
+
>>> replacement = client.api_keys.create(name="Rotated")
|
|
116
|
+
>>> client.api_keys.revoke(key_id=old_key_id)
|
|
82
117
|
"""
|
|
83
118
|
return self._client._request("DELETE", f"/api-keys/{key_id}")
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
AVAILABLE_SYMBOLS_PATH = "/data/available-symbols"
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DataResource:
|
|
5
|
+
"""Browse the catalog of calibrated instruments you can simulate.
|
|
6
|
+
|
|
7
|
+
Reached as ``client.data``. Every simulation is pinned to a symbol that has
|
|
8
|
+
already been calibrated, so this is the resource that tells you which
|
|
9
|
+
``provider`` / ``exchange`` / ``symbol`` / ``cal_date`` combinations exist.
|
|
10
|
+
|
|
11
|
+
Examples
|
|
12
|
+
--------
|
|
13
|
+
>>> catalog = client.data.get_available_symbols(exchange="hkex_securities")
|
|
14
|
+
>>> for instrument in catalog[:3]:
|
|
15
|
+
... dates = instrument["available_dates"]
|
|
16
|
+
... print(instrument["symbol"], dates[:2])
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, client):
|
|
20
|
+
self._client = client
|
|
21
|
+
|
|
22
|
+
def get_available_symbols(
|
|
23
|
+
self,
|
|
24
|
+
symbol: str | None = None,
|
|
25
|
+
exchange: str | None = None,
|
|
26
|
+
provider: str | None = None,
|
|
27
|
+
date: str | None = None,
|
|
28
|
+
q: str | None = None,
|
|
29
|
+
limit: int | None = None,
|
|
30
|
+
offset: int | None = None,
|
|
31
|
+
):
|
|
32
|
+
"""List calibrated instruments and the dates available for each.
|
|
33
|
+
|
|
34
|
+
All arguments are optional filters; with none passed the whole catalog
|
|
35
|
+
comes back, as before.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
symbol : str, optional
|
|
40
|
+
Exact symbol, e.g. "700.HK".
|
|
41
|
+
exchange : str, optional
|
|
42
|
+
Exchange protocol, e.g. "hkex_securities".
|
|
43
|
+
provider : str, optional
|
|
44
|
+
Data provider, e.g. "omd" or "bmll".
|
|
45
|
+
date : str, optional
|
|
46
|
+
Calibration date "YYYY-MM-DD". Keeps only instruments
|
|
47
|
+
calibrated on that date, and narrows each instrument's
|
|
48
|
+
available_dates to it.
|
|
49
|
+
q : str, optional
|
|
50
|
+
Case-insensitive substring match over ticker and company name.
|
|
51
|
+
limit : int, optional
|
|
52
|
+
Max instruments to return.
|
|
53
|
+
offset : int, optional
|
|
54
|
+
Instruments to skip (for paging alongside limit).
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
list of dict
|
|
59
|
+
One entry per instrument, each with:
|
|
60
|
+
|
|
61
|
+
- symbol (str): ticker, e.g. "700.HK"
|
|
62
|
+
- exchange (str): exchange protocol, e.g. "hkex_securities"
|
|
63
|
+
- provider (str): data provider, e.g. "omd"
|
|
64
|
+
- available_dates (list of str): calibration dates, "YYYY-MM-DD",
|
|
65
|
+
any of which is a valid ``cal_date`` for
|
|
66
|
+
:meth:`~simudyne.resources.simulation.SimulationResource.run`
|
|
67
|
+
|
|
68
|
+
Raises
|
|
69
|
+
------
|
|
70
|
+
PulseAPIError
|
|
71
|
+
If a filter is malformed — most often ``date`` not in
|
|
72
|
+
``YYYY-MM-DD`` form.
|
|
73
|
+
|
|
74
|
+
Examples
|
|
75
|
+
--------
|
|
76
|
+
The whole catalog:
|
|
77
|
+
|
|
78
|
+
>>> everything = client.data.get_available_symbols()
|
|
79
|
+
>>> len(everything)
|
|
80
|
+
412
|
|
81
|
+
|
|
82
|
+
One instrument, to see which dates it can be run for:
|
|
83
|
+
|
|
84
|
+
>>> hits = client.data.get_available_symbols(symbol="700.HK")
|
|
85
|
+
>>> first = hits[0]
|
|
86
|
+
>>> print(first["available_dates"])
|
|
87
|
+
['2025-09-01', '2025-09-02', '2025-09-03']
|
|
88
|
+
|
|
89
|
+
Free-text search over ticker and company name, paged:
|
|
90
|
+
|
|
91
|
+
>>> page = client.data.get_available_symbols(q="tencent", limit=10)
|
|
92
|
+
|
|
93
|
+
Everything calibrated on one date, which is the usual way to pick a
|
|
94
|
+
date that is valid across several instruments:
|
|
95
|
+
|
|
96
|
+
>>> on_date = client.data.get_available_symbols(date="2025-09-01")
|
|
97
|
+
"""
|
|
98
|
+
params = {
|
|
99
|
+
k: v for k, v in {
|
|
100
|
+
"symbol": symbol,
|
|
101
|
+
"exchange": exchange,
|
|
102
|
+
"provider": provider,
|
|
103
|
+
"date": date,
|
|
104
|
+
"q": q,
|
|
105
|
+
"limit": limit,
|
|
106
|
+
"offset": offset,
|
|
107
|
+
}.items() if v is not None
|
|
108
|
+
}
|
|
109
|
+
return self._client._request("GET", AVAILABLE_SYMBOLS_PATH, params=params or None)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""FIX venue usage: what you have run over a FIX session.
|
|
2
|
+
|
|
3
|
+
The FIX venue itself is a raw-TCP FIX 4.4 acceptor, not an HTTP API — you reach
|
|
4
|
+
it with your own FIX engine, not with this SDK. What lives here is the one
|
|
5
|
+
HTTP endpoint a FIX client wants: a record of what it has run.
|
|
6
|
+
|
|
7
|
+
Runs are written by the venue rather than by the client, because the venue is
|
|
8
|
+
what knows the session, the account and the start parameters. Only the shape of
|
|
9
|
+
a run is kept -- symbol, date, scenario, speed, horizon. No orders, no
|
|
10
|
+
schedules, no fills: that is your strategy, and it is not ours to store.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
USAGE_PATH = "/fix/usage"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FixResource:
|
|
17
|
+
"""Usage records for simulations started over the FIX venue.
|
|
18
|
+
|
|
19
|
+
Reached as ``client.fix``. The venue itself is raw-TCP FIX 4.4 and is not
|
|
20
|
+
driven from this SDK — you connect to it with your own FIX engine. This
|
|
21
|
+
resource only reads back what the venue recorded.
|
|
22
|
+
|
|
23
|
+
Examples
|
|
24
|
+
--------
|
|
25
|
+
>>> usage = client.fix.usage()
|
|
26
|
+
>>> print(usage["total_runs"], "runs,", usage["simulated_seconds"], "s")
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, client):
|
|
30
|
+
self._client = client
|
|
31
|
+
|
|
32
|
+
def usage(self, user_id: str = None):
|
|
33
|
+
"""Simulations you have started over FIX, and how many times.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
user_id : str, optional
|
|
38
|
+
Another account to read, which requires an admin key.
|
|
39
|
+
Omit it for your own usage.
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
dict
|
|
44
|
+
Usage summary containing:
|
|
45
|
+
|
|
46
|
+
- total_runs (int): FIX sessions recorded
|
|
47
|
+
- simulated_seconds (float): total horizon across those runs
|
|
48
|
+
- by_symbol / by_scenario / by_cal_date (dict): run counts
|
|
49
|
+
- runs (list of dict): one ``{symbol, cal_date, scenario, runs}``
|
|
50
|
+
per distinct combination, most-run first
|
|
51
|
+
- last_run_at (str or None): when the most recent one started,
|
|
52
|
+
None if nothing has been run
|
|
53
|
+
- truncated (bool): True if the history hit the server's cap
|
|
54
|
+
|
|
55
|
+
Raises
|
|
56
|
+
------
|
|
57
|
+
PulseAPIError
|
|
58
|
+
If ``user_id`` is given without an admin key, or names an account
|
|
59
|
+
that does not exist.
|
|
60
|
+
|
|
61
|
+
Examples
|
|
62
|
+
--------
|
|
63
|
+
>>> usage = client.fix.usage()
|
|
64
|
+
>>> print(f"{usage['total_runs']} FIX runs")
|
|
65
|
+
>>> runs = usage["runs"]
|
|
66
|
+
>>> for run in runs[:5]:
|
|
67
|
+
... print(f" {run['symbol']} {run['cal_date']} "
|
|
68
|
+
... f"{run['scenario']}: {run['runs']}x")
|
|
69
|
+
|
|
70
|
+
Read another account, which needs an admin key:
|
|
71
|
+
|
|
72
|
+
>>> client.fix.usage(user_id="usr_01H...")
|
|
73
|
+
"""
|
|
74
|
+
params = {"user_id": user_id} if user_id else None
|
|
75
|
+
return self._client._request("GET", USAGE_PATH, params=params)
|