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.
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/PKG-INFO +1 -1
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/pyproject.toml +1 -1
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/validation.py +143 -4
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/PKG-INFO +1 -1
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/tests/test_validation.py +41 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/LICENSE +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/README.md +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/setup.cfg +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/__init__.py +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/client.py +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/exceptions.py +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/__init__.py +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/api_keys.py +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/data.py +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/fix.py +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/fm.py +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/historical.py +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/profile.py +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/simulation.py +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/simulator_gym.py +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/SOURCES.txt +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/requires.txt +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/tests/test_new_resources.py +0 -0
- {simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/tests/test_simulator_gym.py +0 -0
{simudyne_pulse-0.7.0.dev12 → 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
|
|
@@ -249,7 +275,58 @@ class ValidationResource:
|
|
|
249
275
|
Raises
|
|
250
276
|
------
|
|
251
277
|
ValueError
|
|
252
|
-
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
|
+
... )
|
|
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
|
-
``
|
|
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})
|
|
@@ -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
|
|
File without changes
|
{simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/__init__.py
RENAMED
|
File without changes
|
{simudyne_pulse-0.7.0.dev12 → 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.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/historical.py
RENAMED
|
File without changes
|
|
File without changes
|
{simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/simulation.py
RENAMED
|
File without changes
|
{simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne/resources/simulator_gym.py
RENAMED
|
File without changes
|
{simudyne_pulse-0.7.0.dev12 → 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.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/requires.txt
RENAMED
|
File without changes
|
{simudyne_pulse-0.7.0.dev12 → simudyne_pulse-0.7.0.dev13}/src/simudyne_pulse.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|