simudyne-pulse 0.7.0.dev12__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.dev12 → simudyne_pulse-0.7.0.dev13}/PKG-INFO +1 -1
  2. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/pyproject.toml +1 -1
  3. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/validation.py +143 -4
  4. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/PKG-INFO +1 -1
  5. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/tests/test_validation.py +41 -0
  6. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/LICENSE +0 -0
  7. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/README.md +0 -0
  8. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/setup.cfg +0 -0
  9. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/__init__.py +0 -0
  10. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/client.py +0 -0
  11. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/exceptions.py +0 -0
  12. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/__init__.py +0 -0
  13. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/api_keys.py +0 -0
  14. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/data.py +0 -0
  15. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/fix.py +0 -0
  16. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/fm.py +0 -0
  17. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/historical.py +0 -0
  18. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/profile.py +0 -0
  19. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/simulation.py +0 -0
  20. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/simulator_gym.py +0 -0
  21. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/SOURCES.txt +0 -0
  22. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
  23. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/requires.txt +0 -0
  24. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
  25. {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/tests/test_new_resources.py +0 -0
  26. {simudyne_pulse-0.7.0.dev12 → 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.dev12
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
@@ -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.12"
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
@@ -249,7 +275,58 @@ class ValidationResource:
249
275
  Raises
250
276
  ------
251
277
  ValueError
252
- 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
+ ... )
253
330
  """
254
331
  if (sim_ids is None) == (sim_files is None):
255
332
  raise ValueError("Pass exactly one of sim_ids or sim_files")
@@ -258,8 +335,24 @@ class ValidationResource:
258
335
  if sim_files is not None and not sim_files:
259
336
  raise ValueError("sim_files must not be empty")
260
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
+ )
261
354
  count = (
262
- sum(len(v) for v in given.values())
355
+ sum(len(list(v)) for v in given.values())
263
356
  if isinstance(given, dict)
264
357
  else len(given)
265
358
  )
@@ -367,6 +460,34 @@ class ValidationResource:
367
460
  ``stylised_fact_verdicts``, ``volume_correlation``,
368
461
  ``fid_scores``, ``mind_scores`` — enough to rebuild every figure —
369
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}
370
491
  """
371
492
  job = self._client._request("GET", f"{JOBS_PATH}/{job_id}")
372
493
  if (job or {}).get("plots"):
@@ -384,6 +505,24 @@ class ValidationResource:
384
505
  Returns
385
506
  -------
386
507
  dict
387
- ``{"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"])
388
527
  """
389
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.dev12
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
@@ -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})