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.
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/PKG-INFO +2 -1
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/README.md +1 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/pyproject.toml +1 -1
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/validation.py +146 -6
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/PKG-INFO +2 -1
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/tests/test_validation.py +41 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/LICENSE +0 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/setup.cfg +0 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/__init__.py +0 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/client.py +0 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/exceptions.py +0 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/__init__.py +0 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/api_keys.py +0 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/data.py +0 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/fix.py +0 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/fm.py +0 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/historical.py +0 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/profile.py +0 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/simulation.py +0 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/simulator_gym.py +0 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/SOURCES.txt +0 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/requires.txt +0 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
- {simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/tests/test_new_resources.py +0 -0
- {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.
|
|
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 |
|
{simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/validation.py
RENAMED
|
@@ -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
|
-
"""
|
|
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
|
-
|
|
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
|
|
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
|
-
``
|
|
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})
|
{simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simudyne-pulse
|
|
3
|
-
Version: 0.7.0.
|
|
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})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/__init__.py
RENAMED
|
File without changes
|
{simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/api_keys.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/historical.py
RENAMED
|
File without changes
|
|
File without changes
|
{simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/simulation.py
RENAMED
|
File without changes
|
{simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/simulator_gym.py
RENAMED
|
File without changes
|
{simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/requires.txt
RENAMED
|
File without changes
|
{simudyne_pulse-0.7.0.dev11 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|