simudyne-pulse 0.7.0.dev6__tar.gz → 0.7.2__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.
Files changed (27) hide show
  1. {simudyne_pulse-0.7.0.dev6/src/simudyne_pulse.egg-info → simudyne_pulse-0.7.2}/PKG-INFO +1 -1
  2. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/pyproject.toml +1 -1
  3. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/src/simudyne/client.py +0 -4
  4. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/src/simudyne/resources/data.py +0 -4
  5. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/src/simudyne/resources/simulation.py +3 -106
  6. simudyne_pulse-0.7.2/src/simudyne/resources/validation.py +299 -0
  7. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2/src/simudyne_pulse.egg-info}/PKG-INFO +1 -1
  8. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/src/simudyne_pulse.egg-info/SOURCES.txt +1 -5
  9. simudyne_pulse-0.7.0.dev6/src/simudyne/resources/fix.py +0 -32
  10. simudyne_pulse-0.7.0.dev6/src/simudyne/resources/fm.py +0 -227
  11. simudyne_pulse-0.7.0.dev6/src/simudyne/resources/validation.py +0 -822
  12. simudyne_pulse-0.7.0.dev6/tests/test_new_resources.py +0 -150
  13. simudyne_pulse-0.7.0.dev6/tests/test_validation.py +0 -268
  14. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/LICENSE +0 -0
  15. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/README.md +0 -0
  16. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/setup.cfg +0 -0
  17. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/src/simudyne/__init__.py +0 -0
  18. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/src/simudyne/exceptions.py +0 -0
  19. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/src/simudyne/resources/__init__.py +0 -0
  20. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/src/simudyne/resources/api_keys.py +0 -0
  21. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/src/simudyne/resources/historical.py +0 -0
  22. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/src/simudyne/resources/profile.py +0 -0
  23. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/src/simudyne/resources/simulator_gym.py +0 -0
  24. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
  25. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/src/simudyne_pulse.egg-info/requires.txt +0 -0
  26. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
  27. {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.2}/tests/test_simulator_gym.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simudyne-pulse
3
- Version: 0.7.0.dev6
3
+ Version: 0.7.2
4
4
  Summary: Python SDK for the Simudyne Pulse synthetic market data API
5
5
  Author-email: Simudyne <support@simudyne.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simudyne-pulse"
7
- version = "0.7.0-dev.6"
7
+ version = "0.7.2"
8
8
  description = "Python SDK for the Simudyne Pulse synthetic market data API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -36,8 +36,6 @@ class PulseABM:
36
36
  from simudyne.resources.profile import ProfileResource
37
37
  from simudyne.resources.api_keys import ApiKeysResource
38
38
  from simudyne.resources.data import DataResource
39
- from simudyne.resources.fix import FixResource
40
- from simudyne.resources.fm import FmResource
41
39
  from simudyne.resources.simulation import SimulationResource
42
40
  from simudyne.resources.simulator_gym import SimulatorGymResource
43
41
  from simudyne.resources.validation import ValidationResource
@@ -45,8 +43,6 @@ class PulseABM:
45
43
  self.profile = ProfileResource(self)
46
44
  self.api_keys = ApiKeysResource(self)
47
45
  self.data = DataResource(self)
48
- self.fix = FixResource(self)
49
- self.fm = FmResource(self)
50
46
  self.simulation = SimulationResource(self)
51
47
  self.simulator_gym = SimulatorGymResource(self)
52
48
  self.validation = ValidationResource(self)
@@ -11,7 +11,6 @@ class DataResource:
11
11
  exchange: str | None = None,
12
12
  provider: str | None = None,
13
13
  date: str | None = None,
14
- q: str | None = None,
15
14
  limit: int | None = None,
16
15
  offset: int | None = None,
17
16
  ):
@@ -32,8 +31,6 @@ class DataResource:
32
31
  Calibration date "YYYY-MM-DD". Keeps only instruments
33
32
  calibrated on that date, and narrows each instrument's
34
33
  available_dates to it.
35
- q : str, optional
36
- Case-insensitive substring match over ticker and company name.
37
34
  limit : int, optional
38
35
  Max instruments to return.
39
36
  offset : int, optional
@@ -49,7 +46,6 @@ class DataResource:
49
46
  "exchange": exchange,
50
47
  "provider": provider,
51
48
  "date": date,
52
- "q": q,
53
49
  "limit": limit,
54
50
  "offset": offset,
55
51
  }.items() if v is not None
@@ -21,7 +21,6 @@ RESULTS_PATH = "/simulation/results"
21
21
  CACHED_PATH = "/simulation/cached"
22
22
  SAMPLE_PATH = "/simulation/sample"
23
23
  CALIBRATE_PATH = "/calibrate"
24
- LRM_PATH = "/simulation/lrm/run"
25
24
 
26
25
  # Available market scenarios
27
26
  SCENARIOS = {
@@ -319,17 +318,12 @@ class SimulationResource:
319
318
  payload["batch_size"] = batch_size
320
319
  return self._pro_request("POST", CALIBRATE_PATH, json=payload)
321
320
 
322
- def get_jobs(self, limit: int = 100):
323
- """Get simulation jobs submitted by the authenticated user, newest first.
321
+ def get_jobs(self):
322
+ """Get all simulation jobs submitted by the authenticated user.
324
323
 
325
324
  Returns a list of jobs with their associated simulation IDs. Use this
326
325
  to find job IDs for past runs or to see what simulations are pending.
327
326
 
328
- Parameters
329
- ----------
330
- limit : int, default 100
331
- Max jobs to return (default 100, max 500)
332
-
333
327
  Returns
334
328
  -------
335
329
  dict
@@ -339,7 +333,6 @@ class SimulationResource:
339
333
  - sim_ids (list): List of simulation IDs in this job
340
334
  - created_at (str): Timestamp when job was submitted
341
335
  - total (int): Total number of jobs
342
- - returned (int): Jobs in this page
343
336
 
344
337
  Examples
345
338
  --------
@@ -350,7 +343,7 @@ class SimulationResource:
350
343
  ... print(f"Job {job['job_id']}: {len(job['sim_ids'])} simulations")
351
344
  ... print(f" Created: {job['created_at']}")
352
345
  """
353
- return self._pro_request("GET", JOBS_PATH, params={"limit": limit})
346
+ return self._pro_request("GET", JOBS_PATH)
354
347
 
355
348
  def get_job_status(self, job_id: str):
356
349
  """Get the status of all simulations in a job.
@@ -479,102 +472,6 @@ class SimulationResource:
479
472
  """
480
473
  return self._pro_request("GET", f"{JOBS_PATH}/{job_id}/results")
481
474
 
482
- def get_job_logs(self, job_id: str) -> str:
483
- """Fetch the engine run log for one of your jobs, as plain text.
484
-
485
- The worker writes a diagnostic log per job — this is the thing to read
486
- when a run fails or finishes with nothing plottable, and the thing to
487
- attach when sending a problem to support@simudyne.com.
488
-
489
- Parameters
490
- ----------
491
- job_id : str
492
- The job ID from run() or get_jobs()
493
-
494
- Returns
495
- -------
496
- str
497
- The log text.
498
-
499
- Raises
500
- ------
501
- PulseAPIError
502
- 404 when the job does not exist, is not yours, or
503
- wrote no log.
504
-
505
- Examples
506
- --------
507
- >>> status = client.simulation.get_job_status(job_id)
508
- >>> if status["has_errors"]:
509
- ... print(client.simulation.get_job_logs(job_id)[:2000])
510
- """
511
- # Plain text, not JSON — go through the retrying transport directly.
512
- url = f"{self._client.base_url}{JOBS_PATH}/{job_id}/logs"
513
- response = self._client._request_with_retries("GET", url)
514
- return response.text
515
-
516
- def run_lrm(
517
- self,
518
- symbol: str,
519
- cal_date: str,
520
- provider: str,
521
- exchange: str,
522
- order_sizes: list,
523
- n_runs: int = 50,
524
- seed: int = 42,
525
- horizon_mins: int = 60,
526
- strategy: str = "vwap",
527
- side: str = None,
528
- ):
529
- """Run a liquidity-risk grid: market impact across a ladder of order sizes.
530
-
531
- One execution algo is built per entry in order_sizes, and all of them
532
- share a single baseline, so the cost is ``n_runs * (1 + len(order_sizes))``
533
- simulations rather than one baseline per size. Poll the returned job_id
534
- through the usual job endpoints.
535
-
536
- Parameters
537
- ----------
538
- symbol : str
539
- Trading symbol (e.g. "700")
540
- cal_date : str
541
- Calibration date in YYYY-MM-DD format
542
- provider : str
543
- Data provider (e.g. "omd", "bmll")
544
- exchange : str
545
- Exchange protocol (e.g. "hkex_securities")
546
- order_sizes : list
547
- Order sizes in LOTS — one algo per entry
548
- n_runs : int, default 50
549
- Monte Carlo runs per arm (default 50)
550
- seed : int, default 42
551
- Random seed (default 42)
552
- horizon_mins : int, default 60
553
- Execution horizon in minutes (default 60)
554
- strategy : str, default 'vwap'
555
- "vwap" or "twap" (default "vwap")
556
- side : str, optional
557
- "buy" or "sell"; defaults to the sign of each order size
558
-
559
- Returns
560
- -------
561
- dict with job_id and the queued sim_ids
562
- """
563
- payload = {
564
- "symbol": symbol,
565
- "cal_date": cal_date,
566
- "provider": provider,
567
- "exchange": exchange,
568
- "order_sizes": order_sizes,
569
- "n_runs": n_runs,
570
- "seed": seed,
571
- "horizon_mins": horizon_mins,
572
- "strategy": strategy,
573
- }
574
- if side is not None:
575
- payload["side"] = side
576
- return self._pro_request("POST", LRM_PATH, json=payload)
577
-
578
475
  def list_sim_files(self, sim_id: str):
579
476
  """List available files for a specific simulation.
580
477
 
@@ -0,0 +1,299 @@
1
+ """
2
+ Validation Resource for the Pulse SDK.
3
+
4
+ This module provides methods for validating simulation quality by comparing
5
+ simulated LOB data against historical data using distributional metrics,
6
+ impact response analysis, and FID scores.
7
+
8
+ Workflow:
9
+ 1. Submit a validation job with run() -> returns job_id
10
+ 2. Poll status with get_job(job_id) or use run_pipeline() for blocking
11
+ 3. View results including distances and plots
12
+ 4. List past jobs with list_jobs()
13
+ """
14
+
15
+ import time
16
+ import base64
17
+
18
+
19
+ RUN_PATH = "/validation/run"
20
+ JOBS_PATH = "/validation/jobs"
21
+
22
+
23
+ class ValidationResource:
24
+ def __init__(self, client):
25
+ self._client = client
26
+
27
+ def run(
28
+ self,
29
+ symbol: str,
30
+ date: str,
31
+ sim_ids: list[str],
32
+ ticksize: float = 1.0,
33
+ run_metrics: bool = True,
34
+ run_impact: bool = False,
35
+ run_fid: bool = False,
36
+ n_levels: int = 10,
37
+ rescale_volumes: bool = True,
38
+ lot_size: int = 1,
39
+ run_stylised_facts: bool | None = None,
40
+ ) -> dict:
41
+ """Submit a validation job.
42
+
43
+ Compares simulation output against historical market data using
44
+ distributional distance metrics (L1, Wasserstein), impact response
45
+ curves, and FID scores.
46
+
47
+ Historical data is fetched automatically from GCS based on symbol and date.
48
+ Simulation data is fetched from each sim_id's sim_data.parquet in GCS.
49
+
50
+ Parameters
51
+ ----------
52
+ symbol : str
53
+ Trading symbol (e.g. "700.HK")
54
+ date : str
55
+ Calibration date in YYYY-MM-DD format (e.g. "2025-09-01")
56
+ sim_ids : list[str]
57
+ List of simulation IDs to validate (max 25)
58
+ ticksize : float, default 1.0
59
+ Tick size for the symbol
60
+ run_metrics : bool, default True
61
+ Compute L1/Wasserstein distributional distances
62
+ run_impact : bool, default False
63
+ Compute impact response curves
64
+ run_fid : bool, default False
65
+ Compute Frechet Inception Distance
66
+ run_stylised_facts : bool, optional
67
+ Compute stylised facts (autocorrelation, heavy
68
+ tails, volatility clustering). Left unset it is omitted from the
69
+ request, so the API applies your tier's default — demo accounts
70
+ get them, pro accounts do not.
71
+ n_levels : int, default 10
72
+ Number of L2 book levels to use
73
+ rescale_volumes : bool, default True
74
+ Multiply simulated L2 size columns by lot_size
75
+ lot_size : int, default 1
76
+ Lot size multiplier for volume rescaling
77
+
78
+ Returns
79
+ -------
80
+ dict with job_id, status, message
81
+ """
82
+ # run_metrics/run_impact/run_fid keep sending their long-standing values
83
+ # so existing callers see no change. run_stylised_facts is omitted when
84
+ # unset, letting the API apply the tier default; sending False would opt
85
+ # demo accounts out of results that tier is meant to return.
86
+ config = {
87
+ "run_metrics": run_metrics,
88
+ "run_impact": run_impact,
89
+ "run_fid": run_fid,
90
+ "n_levels": n_levels,
91
+ "rescale_volumes": rescale_volumes,
92
+ "lot_size": lot_size,
93
+ }
94
+ if run_stylised_facts is not None:
95
+ config["run_stylised_facts"] = run_stylised_facts
96
+
97
+ payload = {
98
+ "symbol": symbol,
99
+ "date": date,
100
+ "sim_ids": sim_ids,
101
+ "ticksize": ticksize,
102
+ "config": config,
103
+ }
104
+ return self._client._request("POST", RUN_PATH, json=payload)
105
+
106
+ def get_job(self, job_id: str) -> dict:
107
+ """Get validation job status and results.
108
+
109
+ Parameters
110
+ ----------
111
+ job_id : str
112
+ The job ID returned by run()
113
+
114
+ Returns
115
+ -------
116
+ dict with
117
+ - status: "pending", "running", "completed", or "failed"
118
+ - distances: dict of {metric: {l1: [...], w: [...]}} (when completed)
119
+ - metadata: dict with run parameters, including which passes ran
120
+ - error: error message (when failed)
121
+
122
+ Demo-tier accounts additionally receive the numbers derived from the
123
+ historical data, which are withheld at the pro tier:
124
+
125
+ - distributions: per-metric historical vs simulated histograms
126
+ - impact_response: impact response curves as numbers, historical and
127
+ one block per sim run
128
+ - stylised_facts: historical and one block per sim run
129
+ - fid_scores: one score per sim run (None where not computable)
130
+ """
131
+ return self._client._request("GET", f"{JOBS_PATH}/{job_id}")
132
+
133
+ def list_jobs(self, limit: int = 50) -> dict:
134
+ """List validation jobs for the current user.
135
+
136
+ Parameters
137
+ ----------
138
+ limit : int, default 50
139
+ Max number of jobs to return (default 50, max 200)
140
+
141
+ Returns
142
+ -------
143
+ dict with jobs list and total count
144
+ """
145
+ return self._client._request("GET", JOBS_PATH, params={"limit": limit})
146
+
147
+ def run_pipeline(
148
+ self,
149
+ symbol: str,
150
+ date: str,
151
+ sim_ids: list[str],
152
+ ticksize: float = 1.0,
153
+ run_metrics: bool = True,
154
+ run_impact: bool = False,
155
+ run_fid: bool = False,
156
+ n_levels: int = 10,
157
+ rescale_volumes: bool = True,
158
+ lot_size: int = 1,
159
+ poll_interval: float = 3.0,
160
+ timeout: float = 600.0,
161
+ run_stylised_facts: bool | None = None,
162
+ ) -> dict:
163
+ """Submit a validation job and block until it completes.
164
+
165
+ Combines run() + polling get_job() into a single call.
166
+ Prints progress to stderr.
167
+
168
+ Parameters
169
+ ----------
170
+ symbol : str
171
+ Trading symbol (e.g. "700.HK")
172
+ date : str
173
+ Calibration date in YYYY-MM-DD format
174
+ sim_ids : list[str]
175
+ List of simulation IDs to validate (max 25)
176
+ ticksize : float, default 1.0
177
+ Tick size for the symbol
178
+ run_metrics : bool, default True
179
+ Compute L1/Wasserstein distributional distances
180
+ run_impact : bool, default False
181
+ Compute impact response curves
182
+ run_fid : bool, default False
183
+ Compute Frechet Inception Distance
184
+ run_stylised_facts : bool, optional
185
+ Compute stylised facts (autocorrelation, heavy
186
+ tails, volatility clustering). Left unset it is omitted from the
187
+ request, so the API applies your tier's default — demo accounts
188
+ get them, pro accounts do not.
189
+ n_levels : int, default 10
190
+ Number of L2 book levels to use
191
+ rescale_volumes : bool, default True
192
+ Multiply simulated L2 size columns by lot_size
193
+ lot_size : int, default 1
194
+ Lot size multiplier for volume rescaling
195
+ poll_interval : float, default 3.0
196
+ Seconds between status checks (default 3)
197
+ timeout : float, default 600.0
198
+ Max seconds to wait (default 600)
199
+
200
+ Returns
201
+ -------
202
+ dict with full validation results (distances, plots, metadata)
203
+
204
+ Raises
205
+ ------
206
+ RuntimeError
207
+ If the validation job fails
208
+ TimeoutError
209
+ If the job doesn't complete within timeout
210
+ """
211
+ import sys
212
+
213
+ job = self.run(
214
+ symbol=symbol,
215
+ date=date,
216
+ sim_ids=sim_ids,
217
+ ticksize=ticksize,
218
+ run_metrics=run_metrics,
219
+ run_impact=run_impact,
220
+ run_fid=run_fid,
221
+ run_stylised_facts=run_stylised_facts,
222
+ n_levels=n_levels,
223
+ rescale_volumes=rescale_volumes,
224
+ lot_size=lot_size,
225
+ )
226
+ job_id = job["job_id"]
227
+ print(f"Validation job submitted: {job_id}", file=sys.stderr)
228
+
229
+ start = time.time()
230
+ while True:
231
+ result = self.get_job(job_id)
232
+ status = result["status"]
233
+
234
+ if status == "completed":
235
+ elapsed = time.time() - start
236
+ print(f"Completed in {elapsed:.1f}s", file=sys.stderr)
237
+ return result
238
+ elif status == "failed":
239
+ raise RuntimeError(f"Validation failed: {result.get('error')}")
240
+
241
+ if time.time() - start > timeout:
242
+ raise TimeoutError(f"Validation job {job_id} timed out after {timeout}s")
243
+
244
+ time.sleep(poll_interval)
245
+
246
+ def display_plots(self, result: dict) -> "PlotDisplay":
247
+ """Return a PlotDisplay object for displaying validation plots.
248
+
249
+ Usage:
250
+ plots = client.validation.display_plots(result)
251
+ plots.distributions() # show distribution histograms
252
+ plots.distances() # show spider plots
253
+ plots.impact_response() # show impact response plots
254
+
255
+ Parameters
256
+ ----------
257
+ result : dict
258
+ The result dict from run_pipeline() or get_job()
259
+ """
260
+ return PlotDisplay(result)
261
+
262
+
263
+ class PlotDisplay:
264
+ """Displays categorized validation plots inline in Jupyter notebooks."""
265
+
266
+ def __init__(self, result: dict):
267
+ plots = result.get("plots") or {}
268
+ self._distributions = plots.get("distributions", [])
269
+ self._distances = plots.get("distances", [])
270
+ self._impact_response = plots.get("impact_response", [])
271
+
272
+ def _show(self, plot_list, title):
273
+ from IPython.display import display, Image
274
+
275
+ if not plot_list:
276
+ print(f"No {title} plots available")
277
+ return
278
+
279
+ for plot in plot_list:
280
+ print(f"\n--- {plot['name']} ---")
281
+ display(Image(data=base64.b64decode(plot["content_base64"])))
282
+
283
+ def distributions(self):
284
+ """Display distribution histogram plots."""
285
+ self._show(self._distributions, "distribution")
286
+
287
+ def distances(self):
288
+ """Display spider plots (L1 and Wasserstein distances)."""
289
+ self._show(self._distances, "distance")
290
+
291
+ def impact_response(self):
292
+ """Display impact response plots."""
293
+ self._show(self._impact_response, "impact response")
294
+
295
+ def all(self):
296
+ """Display all plots."""
297
+ self.distances()
298
+ self.distributions()
299
+ self.impact_response()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simudyne-pulse
3
- Version: 0.7.0.dev6
3
+ Version: 0.7.2
4
4
  Summary: Python SDK for the Simudyne Pulse synthetic market data API
5
5
  Author-email: Simudyne <support@simudyne.com>
6
6
  License-Expression: MIT
@@ -7,8 +7,6 @@ src/simudyne/exceptions.py
7
7
  src/simudyne/resources/__init__.py
8
8
  src/simudyne/resources/api_keys.py
9
9
  src/simudyne/resources/data.py
10
- src/simudyne/resources/fix.py
11
- src/simudyne/resources/fm.py
12
10
  src/simudyne/resources/historical.py
13
11
  src/simudyne/resources/profile.py
14
12
  src/simudyne/resources/simulation.py
@@ -19,6 +17,4 @@ src/simudyne_pulse.egg-info/SOURCES.txt
19
17
  src/simudyne_pulse.egg-info/dependency_links.txt
20
18
  src/simudyne_pulse.egg-info/requires.txt
21
19
  src/simudyne_pulse.egg-info/top_level.txt
22
- tests/test_new_resources.py
23
- tests/test_simulator_gym.py
24
- tests/test_validation.py
20
+ tests/test_simulator_gym.py
@@ -1,32 +0,0 @@
1
- """
2
- FIX Resource for the Pulse SDK.
3
-
4
- Pulse simulations can be consumed over the FIX protocol by a market-data
5
- session instead of the REST download endpoints. The FIX connection itself is
6
- provisioned per organisation (see the FIX docs page); this resource covers
7
- what the SDK can usefully do about it — reporting what your account has run
8
- over FIX.
9
- """
10
-
11
- USAGE_PATH = "/fix/usage"
12
-
13
-
14
- class FixResource:
15
- def __init__(self, client):
16
- self._client = client
17
-
18
- def usage(self) -> dict:
19
- """FIX simulation statistics: what was run over FIX, and how often.
20
-
21
- Returns
22
- -------
23
- dict with ``total_runs``, ``simulated_seconds``, and ``runs`` — one
24
- entry per distinct configuration, each with its ``symbol``,
25
- ``cal_date``, ``scenario`` and ``runs`` count.
26
-
27
- Examples
28
- --------
29
- >>> usage = client.fix.usage()
30
- >>> print(f"{usage['total_runs']} FIX runs")
31
- """
32
- return self._client._request("GET", USAGE_PATH)