simudyne-pulse 0.7.0.dev11__tar.gz → 0.7.0.dev13__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 (26) hide show
  1. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/PKG-INFO +2 -1
  2. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/README.md +1 -0
  3. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/pyproject.toml +1 -1
  4. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/validation.py +146 -6
  5. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/PKG-INFO +2 -1
  6. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/tests/test_validation.py +41 -0
  7. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/LICENSE +0 -0
  8. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/setup.cfg +0 -0
  9. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/__init__.py +0 -0
  10. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/client.py +0 -0
  11. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/exceptions.py +0 -0
  12. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/__init__.py +0 -0
  13. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/api_keys.py +0 -0
  14. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/data.py +0 -0
  15. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/fix.py +0 -0
  16. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/fm.py +0 -0
  17. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/historical.py +0 -0
  18. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/profile.py +0 -0
  19. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/simulation.py +0 -0
  20. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/simulator_gym.py +0 -0
  21. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/SOURCES.txt +0 -0
  22. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
  23. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/requires.txt +0 -0
  24. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
  25. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/tests/test_new_resources.py +0 -0
  26. {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/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.dev11
3
+ Version: 0.7.0.dev13
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
@@ -98,6 +98,7 @@ switched off draws nothing either way.
98
98
  | `stylised_facts.fact` | every fact |
99
99
  | `stylised_facts.fact.{name}` | one fact, e.g. `.heavy_tails` |
100
100
  | `impact.response` | impact response by event type |
101
+ | `impact.event` | every event type |
101
102
  | `impact.event.{type}` | one event type |
102
103
  | `volume_correlation.levels` | level correlation heatmap |
103
104
  | `volume_correlation.changes` | change correlation heatmap |
@@ -72,6 +72,7 @@ switched off draws nothing either way.
72
72
  | `stylised_facts.fact` | every fact |
73
73
  | `stylised_facts.fact.{name}` | one fact, e.g. `.heavy_tails` |
74
74
  | `impact.response` | impact response by event type |
75
+ | `impact.event` | every event type |
75
76
  | `impact.event.{type}` | one event type |
76
77
  | `volume_correlation.levels` | level correlation heatmap |
77
78
  | `volume_correlation.changes` | change correlation heatmap |
@@ -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.11"
7
+ version = "0.7.0-dev.13"
8
8
  description = "Python SDK for the Simudyne Pulse synthetic market data API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -39,6 +39,7 @@ them.
39
39
  """
40
40
 
41
41
  import json
42
+ from collections.abc import Iterable
42
43
  from pathlib import Path
43
44
 
44
45
 
@@ -146,7 +147,32 @@ def _save_plots(result: dict, plot_dir=None) -> list:
146
147
 
147
148
 
148
149
  class ValidationResource:
149
- """Validate simulated market data against a historical day."""
150
+ """Score simulated market data against a real historical day.
151
+
152
+ Reached as ``client.validation``. A job compares one or more simulated runs
153
+ — either platform ``sim_ids`` or your own uploaded frames — against the
154
+ real day for the same instrument, and reports distances, distribution
155
+ overlaps, stylised-fact verdicts and FID/MIND scores.
156
+
157
+ Jobs are asynchronous: :meth:`run` returns a ``job_id`` and :meth:`get_job`
158
+ is polled until it is terminal. A validation of a real day takes minutes.
159
+
160
+ Examples
161
+ --------
162
+ >>> import time
163
+ >>> job = client.validation.run(
164
+ ... symbol="BARC", date="2025-09-02",
165
+ ... provider="bmll", exchange="lse",
166
+ ... sim_ids=["sim_abc", "sim_def"],
167
+ ... plots=True,
168
+ ... )
169
+ >>> while True:
170
+ ... result = client.validation.get_job(job["job_id"], plot_dir="figures/")
171
+ ... if result["status"] in ("completed", "failed"):
172
+ ... break
173
+ ... time.sleep(30)
174
+ >>> result["distances"]
175
+ """
150
176
 
151
177
  def __init__(self, client):
152
178
  self._client = client
@@ -234,8 +260,9 @@ class ValidationResource:
234
260
  - ``stylised_facts.overall`` — the verdict table
235
261
  - ``stylised_facts.fact`` — every fact;
236
262
  ``stylised_facts.fact.{name}`` for one, e.g. ``.heavy_tails``
237
- - ``impact.response`` — impact response by event type;
238
- ``impact.event.{type}`` for one
263
+ - ``impact.response`` — impact response by event type
264
+ - ``impact.event`` — every event type; ``impact.event.{type}``
265
+ for one
239
266
  - ``volume_correlation.levels``, ``volume_correlation.changes``,
240
267
  or ``volume_correlation.overall`` for both
241
268
 
@@ -248,7 +275,58 @@ class ValidationResource:
248
275
  Raises
249
276
  ------
250
277
  ValueError
251
- If the parameters are invalid.
278
+ If neither or both of ``sim_ids`` and ``sim_files`` are given,
279
+ either is empty, more than 25 runs are passed, ``ticksize`` is not
280
+ positive, or ``n_levels`` is below 1. Also raised if the API
281
+ accepts the request but returns no ``job_id``.
282
+ PulseAPIError
283
+ If the instrument has no historical data for ``date``, a
284
+ ``sim_id`` is unknown, or the tier does not allow a requested
285
+ area.
286
+
287
+ Examples
288
+ --------
289
+ Platform simulations against the real day:
290
+
291
+ >>> job = client.validation.run(
292
+ ... symbol="BARC",
293
+ ... date="2025-09-02",
294
+ ... provider="bmll",
295
+ ... exchange="lse",
296
+ ... sim_ids=["sim_abc", "sim_def"],
297
+ ... )
298
+ >>> job["job_id"]
299
+ 'val_01H...'
300
+
301
+ Two named populations, compared against each other and the real day:
302
+
303
+ >>> job = client.validation.run(
304
+ ... symbol="BARC", date="2025-09-02",
305
+ ... provider="bmll", exchange="lse",
306
+ ... sim_ids={"fm": ["sim_abc"], "abm": ["sim_def", "sim_ghi"]},
307
+ ... )
308
+
309
+ Your own frames, with only the areas you want and selected figures:
310
+
311
+ >>> job = client.validation.run(
312
+ ... symbol="BARC", date="2025-09-02",
313
+ ... provider="bmll", exchange="lse",
314
+ ... sim_files=[my_dataframe],
315
+ ... statistical=True,
316
+ ... stylised_facts=True,
317
+ ... impact=False,
318
+ ... plots=["statistical.radar", "stylised_facts.overall"],
319
+ ... )
320
+
321
+ L2 snapshots resampled onto a one-second grid:
322
+
323
+ >>> job = client.validation.run(
324
+ ... symbol="BARC", date="2025-09-02",
325
+ ... provider="bmll", exchange="lse",
326
+ ... sim_files=["book_snapshots.parquet"],
327
+ ... lob=True,
328
+ ... sample_period="1s",
329
+ ... )
252
330
  """
253
331
  if (sim_ids is None) == (sim_files is None):
254
332
  raise ValueError("Pass exactly one of sim_ids or sim_files")
@@ -257,8 +335,24 @@ class ValidationResource:
257
335
  if sim_files is not None and not sim_files:
258
336
  raise ValueError("sim_files must not be empty")
259
337
  given = sim_ids if sim_ids is not None else sim_files
338
+ name = "sim_ids" if sim_ids is not None else "sim_files"
339
+ if isinstance(given, dict):
340
+ # A bare string is iterable, so {"fm": "one.parquet"} used to count
341
+ # its characters as runs and fail with "Maximum 25 simulations" —
342
+ # a number the caller never wrote. Say what is actually wrong.
343
+ for group, runs in given.items():
344
+ if isinstance(runs, (str, bytes)):
345
+ raise ValueError(
346
+ f"{name}[{group!r}] must be a list of runs, not a "
347
+ f"single {type(runs).__name__} — write [{runs!r}]"
348
+ )
349
+ if not isinstance(runs, Iterable):
350
+ raise ValueError(
351
+ f"{name}[{group!r}] must be a list of runs, not "
352
+ f"{type(runs).__name__}"
353
+ )
260
354
  count = (
261
- sum(len(v) for v in given.values())
355
+ sum(len(list(v)) for v in given.values())
262
356
  if isinstance(given, dict)
263
357
  else len(given)
264
358
  )
@@ -366,6 +460,34 @@ class ValidationResource:
366
460
  ``stylised_fact_verdicts``, ``volume_correlation``,
367
461
  ``fid_scores``, ``mind_scores`` — enough to rebuild every figure —
368
462
  plus ``plot_paths`` when figures were written.
463
+
464
+ Raises
465
+ ------
466
+ PulseAPIError
467
+ If ``job_id`` is unknown or belongs to another account — status
468
+ 404.
469
+
470
+ Examples
471
+ --------
472
+ >>> result = client.validation.get_job(job_id)
473
+ >>> result["status"]
474
+ 'running'
475
+
476
+ Poll to completion, writing any figures into a directory:
477
+
478
+ >>> import time
479
+ >>> while True:
480
+ ... result = client.validation.get_job(job_id, plot_dir="figures/")
481
+ ... if result["status"] in ("completed", "failed"):
482
+ ... break
483
+ ... time.sleep(30)
484
+ >>> print(result["plot_paths"])
485
+ ['figures/statistical.radar.png', 'figures/stylised_facts.overall.png']
486
+
487
+ With named populations, the scores come back keyed by name:
488
+
489
+ >>> result["fid_scores"]
490
+ {'fm': 0.142, 'abm': 0.318}
369
491
  """
370
492
  job = self._client._request("GET", f"{JOBS_PATH}/{job_id}")
371
493
  if (job or {}).get("plots"):
@@ -383,6 +505,24 @@ class ValidationResource:
383
505
  Returns
384
506
  -------
385
507
  dict
386
- ``{"jobs": list, "total": int}``.
508
+ Contains ``jobs``, a list of job summaries newest first, and
509
+ ``total``, the count before ``limit`` was applied.
510
+
511
+ Raises
512
+ ------
513
+ PulseAPIError
514
+ If the key is invalid, revoked or expired — status 401.
515
+
516
+ Examples
517
+ --------
518
+ >>> listing = client.validation.list_jobs(limit=10)
519
+ >>> for job in listing["jobs"]:
520
+ ... print(job["job_id"], job["status"], job["symbol"], job["date"])
521
+
522
+ Find the most recent completed job and read it back:
523
+
524
+ >>> done = [j for j in listing["jobs"] if j["status"] == "completed"]
525
+ >>> newest = done[0]
526
+ >>> result = client.validation.get_job(newest["job_id"])
387
527
  """
388
528
  return self._client._request("GET", JOBS_PATH, params={"limit": limit})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simudyne-pulse
3
- Version: 0.7.0.dev11
3
+ Version: 0.7.0.dev13
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
@@ -98,6 +98,7 @@ switched off draws nothing either way.
98
98
  | `stylised_facts.fact` | every fact |
99
99
  | `stylised_facts.fact.{name}` | one fact, e.g. `.heavy_tails` |
100
100
  | `impact.response` | impact response by event type |
101
+ | `impact.event` | every event type |
101
102
  | `impact.event.{type}` | one event type |
102
103
  | `volume_correlation.levels` | level correlation heatmap |
103
104
  | `volume_correlation.changes` | change correlation heatmap |
@@ -289,3 +289,44 @@ class TestPlotEverything:
289
289
  def test_plot_all_is_gone(self):
290
290
  with pytest.raises(TypeError):
291
291
  _run([{"job_id": "v1"}], plot_all=True)
292
+
293
+
294
+ class TestGroupShape:
295
+ """A group's value is a list of runs. A bare string is not one.
296
+
297
+ ``{"fm": "one.parquet"}`` is the natural thing to write for a group of
298
+ one, and it used to count the path's characters as runs and fail with
299
+ "Maximum 25 simulations per validation job" — a number the caller never
300
+ wrote anywhere.
301
+ """
302
+
303
+ @staticmethod
304
+ def _submit(**kwargs):
305
+ return ValidationResource(FakeClient([{"job_id": "v1"}])).run(
306
+ **{**IDENTITY, **kwargs}
307
+ )
308
+
309
+ def test_a_string_names_itself_not_the_file_count(self):
310
+ with pytest.raises(ValueError, match=r"sim_files\['fm'\] must be a list"):
311
+ self._submit(sim_files={"fm": "one.parquet"})
312
+
313
+ def test_the_message_shows_the_fix(self):
314
+ with pytest.raises(ValueError, match=r"write \['one.parquet'\]"):
315
+ self._submit(sim_files={"fm": "one.parquet"})
316
+
317
+ def test_sim_ids_groups_are_checked_the_same_way(self):
318
+ with pytest.raises(ValueError, match=r"sim_ids\['abm'\] must be a list"):
319
+ self._submit(sim_ids={"abm": "sim_abc"})
320
+
321
+ def test_a_non_iterable_group_is_named_plainly(self):
322
+ with pytest.raises(ValueError, match=r"not NoneType"):
323
+ self._submit(sim_ids={"abm": None})
324
+
325
+ def test_a_real_list_still_passes(self):
326
+ client = FakeClient([{"job_id": "v1"}])
327
+ ValidationResource(client).run(sim_ids={"abm": ["a", "b"]}, **IDENTITY)
328
+ assert client.calls[0][2]["json"]["sim_ids"] == {"abm": ["a", "b"]}
329
+
330
+ def test_the_count_still_catches_too_many(self):
331
+ with pytest.raises(ValueError, match="Maximum 25"):
332
+ self._submit(sim_ids={"a": ["x"] * 13, "b": ["y"] * 13})