simudyne-pulse 0.7.0.dev8__tar.gz → 0.7.0.dev10__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.dev8 → simudyne_pulse-0.7.0.dev10}/PKG-INFO +30 -1
  2. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/README.md +29 -0
  3. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/pyproject.toml +1 -1
  4. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/src/simudyne/resources/validation.py +44 -10
  5. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/src/simudyne_pulse.egg-info/PKG-INFO +30 -1
  6. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/tests/test_validation.py +53 -0
  7. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/LICENSE +0 -0
  8. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/setup.cfg +0 -0
  9. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/src/simudyne/__init__.py +0 -0
  10. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/src/simudyne/client.py +0 -0
  11. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/src/simudyne/exceptions.py +0 -0
  12. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/src/simudyne/resources/__init__.py +0 -0
  13. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/src/simudyne/resources/api_keys.py +0 -0
  14. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/src/simudyne/resources/data.py +0 -0
  15. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/src/simudyne/resources/fix.py +0 -0
  16. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/src/simudyne/resources/fm.py +0 -0
  17. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/src/simudyne/resources/historical.py +0 -0
  18. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/src/simudyne/resources/profile.py +0 -0
  19. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/src/simudyne/resources/simulation.py +0 -0
  20. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/src/simudyne/resources/simulator_gym.py +0 -0
  21. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/src/simudyne_pulse.egg-info/SOURCES.txt +0 -0
  22. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
  23. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/src/simudyne_pulse.egg-info/requires.txt +0 -0
  24. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
  25. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/tests/test_new_resources.py +0 -0
  26. {simudyne_pulse-0.7.0.dev8 → simudyne_pulse-0.7.0.dev10}/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.dev8
3
+ Version: 0.7.0.dev10
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
@@ -59,6 +59,35 @@ pip install --pre simudyne-pulse
59
59
  Only use dev builds for testing unreleased changes; they are not guaranteed
60
60
  stable. Merge `dev` into `prod` to promote those changes to a stable release.
61
61
 
62
+ ### Comparing named populations
63
+
64
+ Simulations are often not one population but several — a foundation model and
65
+ an ABM, or two versions of a model. Name them and the results compare them
66
+ instead of averaging them together:
67
+
68
+ ```python
69
+ job = client.validation.run(
70
+ symbol="TSCO", date="2026-06-23", provider="bmll", exchange="lse",
71
+ sim_ids={"fm": fm_ids, "abm": abm_ids},
72
+ plots=["statistical.radar", "stylised_facts.overall",
73
+ "volume_correlation.overall"],
74
+ )
75
+ result = client.validation.get_job(job["job_id"], plot_dir="figs")
76
+ result["distances"]["fm"]["spread"]["l1"]
77
+ result["stylised_fact_verdicts"]["heavy_tails"]["simulated"]["abm"]
78
+ ```
79
+
80
+ `distances`, `distributions`, the verdicts and the FID/MIND scores come back
81
+ keyed by name; the radar draws a polygon per population, the verdict table a
82
+ column each, and volume correlation puts historical first then a heatmap per
83
+ population. `sim_files` groups the same way.
84
+
85
+ A plain list is one unnamed population and keeps the flat shape exactly as
86
+ before. The historical day is measured once however many groups there are.
87
+
88
+ `plot_all=True` draws everything the enabled areas can draw, not just the
89
+ summaries.
90
+
62
91
  ### Working from a checkout
63
92
 
64
93
  To run the SDK from source — editing it, or using unreleased changes — install
@@ -33,6 +33,35 @@ pip install --pre simudyne-pulse
33
33
  Only use dev builds for testing unreleased changes; they are not guaranteed
34
34
  stable. Merge `dev` into `prod` to promote those changes to a stable release.
35
35
 
36
+ ### Comparing named populations
37
+
38
+ Simulations are often not one population but several — a foundation model and
39
+ an ABM, or two versions of a model. Name them and the results compare them
40
+ instead of averaging them together:
41
+
42
+ ```python
43
+ job = client.validation.run(
44
+ symbol="TSCO", date="2026-06-23", provider="bmll", exchange="lse",
45
+ sim_ids={"fm": fm_ids, "abm": abm_ids},
46
+ plots=["statistical.radar", "stylised_facts.overall",
47
+ "volume_correlation.overall"],
48
+ )
49
+ result = client.validation.get_job(job["job_id"], plot_dir="figs")
50
+ result["distances"]["fm"]["spread"]["l1"]
51
+ result["stylised_fact_verdicts"]["heavy_tails"]["simulated"]["abm"]
52
+ ```
53
+
54
+ `distances`, `distributions`, the verdicts and the FID/MIND scores come back
55
+ keyed by name; the radar draws a polygon per population, the verdict table a
56
+ column each, and volume correlation puts historical first then a heatmap per
57
+ population. `sim_files` groups the same way.
58
+
59
+ A plain list is one unnamed population and keeps the flat shape exactly as
60
+ before. The historical day is measured once however many groups there are.
61
+
62
+ `plot_all=True` draws everything the enabled areas can draw, not just the
63
+ summaries.
64
+
36
65
  ### Working from a checkout
37
66
 
38
67
  To run the SDK from source — editing it, or using unreleased changes — install
@@ -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.8"
7
+ version = "0.7.0-dev.10"
8
8
  description = "Python SDK for the Simudyne Pulse synthetic market data API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -64,7 +64,8 @@ _AREA_FLAGS = (
64
64
  #: needing the message stream; ``sample_period`` and ``match_generated_sample``
65
65
  #: set the grid the book is resampled onto; ``plots`` is False, True, or a list
66
66
  #: of plot ids.
67
- _EXTRA_FIELDS = ("lob", "sample_period", "match_generated_sample", "plots")
67
+ _EXTRA_FIELDS = ("lob", "sample_period", "match_generated_sample", "plots",
68
+ "plot_all")
68
69
 
69
70
 
70
71
  def _frame_to_parquet(entry, index: int):
@@ -172,6 +173,7 @@ class ValidationResource:
172
173
  match_generated_sample=None,
173
174
  n_levels: int = 10,
174
175
  plots=None,
176
+ plot_all: bool = False,
175
177
  ) -> dict:
176
178
  """Submit a validation job.
177
179
 
@@ -192,13 +194,17 @@ class ValidationResource:
192
194
  ``(symbol, provider, exchange)`` is the identity, not the symbol.
193
195
  exchange : str
194
196
  Exchange, e.g. ``"lse"`` or ``"hkex_securities"``.
195
- sim_ids : list of str, optional
196
- Platform simulation IDs, 1 to 25. Exactly one of ``sim_ids`` or
197
- ``sim_files`` is required.
198
- sim_files : list, optional
197
+ sim_ids : list of str or dict, optional
198
+ Platform simulation IDs, 1 to 25. A flat list is one unnamed
199
+ population. A mapping — ``{"fm": [...], "abm": [...]}`` — compares
200
+ the named populations: distances, distributions, verdicts and
201
+ FID/MIND scores come back keyed by name, and the summary figures
202
+ draw one series per name instead of averaging them together.
203
+ Exactly one of ``sim_ids`` or ``sim_files`` is required.
204
+ sim_files : list or dict, optional
199
205
  Your own simulated runs, up to 25: paths, ``(filename, bytes)``
200
- pairs, or polars / pandas DataFrames in pulse format. The
201
- historical side is fetched for you.
206
+ pairs, or polars / pandas DataFrames in pulse format. Grouped the
207
+ same way as ``sim_ids``. The historical side is fetched for you.
202
208
  ticksize : float, default 1.0
203
209
  Minimum price increment. Should match the instrument.
204
210
  statistical, stylised_facts, impact, volume_correlation, fid, mind : bool, optional
@@ -221,6 +227,10 @@ class ValidationResource:
221
227
  ``None`` or ``False`` for none, ``True`` for every figure, or plot
222
228
  ids such as ``["statistical.radar", "stylised_facts.overall"]``.
223
229
  Rendered server-side; :meth:`get_job` writes them to disk.
230
+ plot_all : bool, default False
231
+ Draw everything the enabled areas can draw — every distribution and
232
+ every stylised fact, not just the summaries. An area switched off
233
+ still draws nothing, so this means "all of what ran".
224
234
 
225
235
  Returns
226
236
  -------
@@ -239,7 +249,12 @@ class ValidationResource:
239
249
  raise ValueError("sim_ids must not be empty")
240
250
  if sim_files is not None and not sim_files:
241
251
  raise ValueError("sim_files must not be empty")
242
- count = len(sim_ids if sim_ids is not None else sim_files)
252
+ given = sim_ids if sim_ids is not None else sim_files
253
+ count = (
254
+ sum(len(v) for v in given.values())
255
+ if isinstance(given, dict)
256
+ else len(given)
257
+ )
243
258
  if count > MAX_SIM_FILES:
244
259
  raise ValueError(f"Maximum {MAX_SIM_FILES} simulations per validation job")
245
260
  if ticksize <= 0:
@@ -259,6 +274,7 @@ class ValidationResource:
259
274
  sample_period=sample_period,
260
275
  match_generated_sample=match_generated_sample,
261
276
  plots=plots,
277
+ plot_all=plot_all or None,
262
278
  )
263
279
 
264
280
  if sim_ids is not None:
@@ -270,7 +286,11 @@ class ValidationResource:
270
286
  "date": date,
271
287
  "provider": provider,
272
288
  "exchange": exchange,
273
- "sim_ids": list(sim_ids),
289
+ "sim_ids": (
290
+ {k: list(v) for k, v in sim_ids.items()}
291
+ if isinstance(sim_ids, dict)
292
+ else list(sim_ids)
293
+ ),
274
294
  "ticksize": ticksize,
275
295
  "config": config,
276
296
  },
@@ -278,10 +298,24 @@ class ValidationResource:
278
298
  else:
279
299
  # Three-tuple parts: the content type matters to the server's
280
300
  # multipart parser, so it is sent explicitly.
301
+ # Grouped uploads travel flat with the grouping beside them, the
302
+ # same way sim_ids do — multipart has no nesting.
303
+ if isinstance(sim_files, dict):
304
+ flat, groups, offset = [], {}, 0
305
+ for name, runs in sim_files.items():
306
+ runs = list(runs)
307
+ groups[name] = list(range(offset, offset + len(runs)))
308
+ offset += len(runs)
309
+ flat.extend(runs)
310
+ else:
311
+ flat, groups = list(sim_files), None
312
+
281
313
  files = [
282
314
  ("sim_files", (*_frame_to_parquet(entry, i), "application/octet-stream"))
283
- for i, entry in enumerate(sim_files)
315
+ for i, entry in enumerate(flat)
284
316
  ]
317
+ if groups:
318
+ config = {**config, "sim_groups": groups}
285
319
  submitted = self._client._request(
286
320
  "POST",
287
321
  UPLOAD_PATH,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simudyne-pulse
3
- Version: 0.7.0.dev8
3
+ Version: 0.7.0.dev10
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
@@ -59,6 +59,35 @@ pip install --pre simudyne-pulse
59
59
  Only use dev builds for testing unreleased changes; they are not guaranteed
60
60
  stable. Merge `dev` into `prod` to promote those changes to a stable release.
61
61
 
62
+ ### Comparing named populations
63
+
64
+ Simulations are often not one population but several — a foundation model and
65
+ an ABM, or two versions of a model. Name them and the results compare them
66
+ instead of averaging them together:
67
+
68
+ ```python
69
+ job = client.validation.run(
70
+ symbol="TSCO", date="2026-06-23", provider="bmll", exchange="lse",
71
+ sim_ids={"fm": fm_ids, "abm": abm_ids},
72
+ plots=["statistical.radar", "stylised_facts.overall",
73
+ "volume_correlation.overall"],
74
+ )
75
+ result = client.validation.get_job(job["job_id"], plot_dir="figs")
76
+ result["distances"]["fm"]["spread"]["l1"]
77
+ result["stylised_fact_verdicts"]["heavy_tails"]["simulated"]["abm"]
78
+ ```
79
+
80
+ `distances`, `distributions`, the verdicts and the FID/MIND scores come back
81
+ keyed by name; the radar draws a polygon per population, the verdict table a
82
+ column each, and volume correlation puts historical first then a heatmap per
83
+ population. `sim_files` groups the same way.
84
+
85
+ A plain list is one unnamed population and keeps the flat shape exactly as
86
+ before. The historical day is measured once however many groups there are.
87
+
88
+ `plot_all=True` draws everything the enabled areas can draw, not just the
89
+ summaries.
90
+
62
91
  ### Working from a checkout
63
92
 
64
93
  To run the SDK from source — editing it, or using unreleased changes — install
@@ -230,3 +230,56 @@ class TestSimulatedFrameShapes:
230
230
  frames = [pd.DataFrame({"a": [1]}), pd.DataFrame({"a": [2]})]
231
231
  names = [f[1][0] for f in self._files_sent(frames)]
232
232
  assert names == ["sim_0.parquet", "sim_1.parquet"]
233
+
234
+
235
+ class TestNamedPopulations:
236
+ """Runs can be named so the results compare populations, not an average."""
237
+
238
+ def test_grouped_sim_ids_go_as_a_mapping(self):
239
+ client = FakeClient([{"job_id": "v1"}])
240
+ ValidationResource(client).run(
241
+ sim_ids={"fm": ["a"], "abm": ["b", "c"]}, **IDENTITY
242
+ )
243
+ assert client.calls[0][2]["json"]["sim_ids"] == {
244
+ "fm": ["a"],
245
+ "abm": ["b", "c"],
246
+ }
247
+
248
+ def test_the_total_is_what_is_capped(self):
249
+ client = FakeClient()
250
+ with pytest.raises(ValueError, match="Maximum 25"):
251
+ ValidationResource(client).run(
252
+ sim_ids={"a": ["x"] * 13, "b": ["y"] * 13}, **IDENTITY
253
+ )
254
+ assert client.calls == []
255
+
256
+ def test_grouped_uploads_travel_flat_with_the_grouping_beside_them(self):
257
+ """Multipart has no nesting, so the groups go in the config."""
258
+ client = FakeClient([{"job_id": "v1"}])
259
+ ValidationResource(client).run(
260
+ sim_files={"fm": [("a.parquet", b"x")],
261
+ "abm": [("b.parquet", b"y"), ("c.parquet", b"z")]},
262
+ **IDENTITY,
263
+ )
264
+ kwargs = client.calls[0][2]
265
+ assert len(kwargs["files"]) == 3
266
+ assert json.loads(kwargs["data"]["config"])["sim_groups"] == {
267
+ "fm": [0], "abm": [1, 2]
268
+ }
269
+
270
+ def test_an_ungrouped_upload_carries_no_grouping(self):
271
+ client = FakeClient([{"job_id": "v1"}])
272
+ ValidationResource(client).run(
273
+ sim_files=[("a.parquet", b"x")], **IDENTITY
274
+ )
275
+ assert "sim_groups" not in json.loads(client.calls[0][2]["data"]["config"])
276
+
277
+
278
+ class TestPlotAll:
279
+ def test_it_is_forwarded(self):
280
+ client, _ = _run([{"job_id": "v1"}], plot_all=True)
281
+ assert client.calls[0][2]["json"]["config"]["plot_all"] is True
282
+
283
+ def test_it_is_omitted_when_not_asked_for(self):
284
+ client, _ = _run([{"job_id": "v1"}])
285
+ assert "plot_all" not in client.calls[0][2]["json"]["config"]