simudyne-pulse 0.7.0.dev6__tar.gz → 0.7.0.dev8__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.dev6/src/simudyne_pulse.egg-info → simudyne_pulse-0.7.0.dev8}/PKG-INFO +50 -1
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/README.md +49 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/pyproject.toml +1 -1
- simudyne_pulse-0.7.0.dev8/src/simudyne/resources/validation.py +348 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8/src/simudyne_pulse.egg-info}/PKG-INFO +50 -1
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/tests/test_new_resources.py +11 -11
- simudyne_pulse-0.7.0.dev8/tests/test_validation.py +232 -0
- simudyne_pulse-0.7.0.dev6/src/simudyne/resources/validation.py +0 -822
- simudyne_pulse-0.7.0.dev6/tests/test_validation.py +0 -268
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/LICENSE +0 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/setup.cfg +0 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/src/simudyne/__init__.py +0 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/src/simudyne/client.py +0 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/src/simudyne/exceptions.py +0 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/src/simudyne/resources/__init__.py +0 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/src/simudyne/resources/api_keys.py +0 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/src/simudyne/resources/data.py +0 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/src/simudyne/resources/fix.py +0 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/src/simudyne/resources/fm.py +0 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/src/simudyne/resources/historical.py +0 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/src/simudyne/resources/profile.py +0 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/src/simudyne/resources/simulation.py +0 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/src/simudyne/resources/simulator_gym.py +0 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/src/simudyne_pulse.egg-info/SOURCES.txt +0 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/src/simudyne_pulse.egg-info/requires.txt +0 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
- {simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8}/tests/test_simulator_gym.py +0 -0
{simudyne_pulse-0.7.0.dev6/src/simudyne_pulse.egg-info → simudyne_pulse-0.7.0.dev8}/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.dev8
|
|
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,55 @@ 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
|
+
### Working from a checkout
|
|
63
|
+
|
|
64
|
+
To run the SDK from source — editing it, or using unreleased changes — install
|
|
65
|
+
it editable **into the interpreter you will actually import from**. A notebook
|
|
66
|
+
kernel is frequently not the python on your `$PATH`:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# in a notebook, find the right interpreter first
|
|
70
|
+
import sys; print(sys.executable)
|
|
71
|
+
|
|
72
|
+
# then, with that path
|
|
73
|
+
<that python> -m pip install -e /path/to/pulse-sdk
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Check which copy you loaded — this is worth doing whenever an attribute seems
|
|
77
|
+
to be missing:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import simudyne
|
|
81
|
+
from importlib.metadata import version
|
|
82
|
+
|
|
83
|
+
print(simudyne.__file__) # should be .../pulse-sdk/src/simudyne/...
|
|
84
|
+
print(version("simudyne-pulse"))
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### If an older install shadows it
|
|
88
|
+
|
|
89
|
+
The SDK was once published under the distribution name **`simudyne`**; it is
|
|
90
|
+
now **`simudyne-pulse`**. Both ship a module called `simudyne`, and the older
|
|
91
|
+
one installs a real directory while the editable install only adds a path
|
|
92
|
+
entry — so the old copy wins and you get errors like
|
|
93
|
+
`'PulseABM' object has no attribute 'validation'` from a version that predates
|
|
94
|
+
the feature. `pip list` shows both. Remove the obsolete one:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
<that python> -m pip uninstall simudyne # the old distribution
|
|
98
|
+
<that python> -m pip install -e /path/to/pulse-sdk
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Running the tests
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
uv run --with pytest --with requests --with pandas --with pyarrow \
|
|
105
|
+
python -m pytest tests/ -q
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
No network: the client's `_request` is replaced with a recorder, so the tests
|
|
109
|
+
assert the exact payload the API would receive.
|
|
110
|
+
|
|
62
111
|
## Quick start
|
|
63
112
|
|
|
64
113
|
```python
|
|
@@ -33,6 +33,55 @@ 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
|
+
### Working from a checkout
|
|
37
|
+
|
|
38
|
+
To run the SDK from source — editing it, or using unreleased changes — install
|
|
39
|
+
it editable **into the interpreter you will actually import from**. A notebook
|
|
40
|
+
kernel is frequently not the python on your `$PATH`:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# in a notebook, find the right interpreter first
|
|
44
|
+
import sys; print(sys.executable)
|
|
45
|
+
|
|
46
|
+
# then, with that path
|
|
47
|
+
<that python> -m pip install -e /path/to/pulse-sdk
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Check which copy you loaded — this is worth doing whenever an attribute seems
|
|
51
|
+
to be missing:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import simudyne
|
|
55
|
+
from importlib.metadata import version
|
|
56
|
+
|
|
57
|
+
print(simudyne.__file__) # should be .../pulse-sdk/src/simudyne/...
|
|
58
|
+
print(version("simudyne-pulse"))
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### If an older install shadows it
|
|
62
|
+
|
|
63
|
+
The SDK was once published under the distribution name **`simudyne`**; it is
|
|
64
|
+
now **`simudyne-pulse`**. Both ship a module called `simudyne`, and the older
|
|
65
|
+
one installs a real directory while the editable install only adds a path
|
|
66
|
+
entry — so the old copy wins and you get errors like
|
|
67
|
+
`'PulseABM' object has no attribute 'validation'` from a version that predates
|
|
68
|
+
the feature. `pip list` shows both. Remove the obsolete one:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
<that python> -m pip uninstall simudyne # the old distribution
|
|
72
|
+
<that python> -m pip install -e /path/to/pulse-sdk
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Running the tests
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
uv run --with pytest --with requests --with pandas --with pyarrow \
|
|
79
|
+
python -m pytest tests/ -q
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
No network: the client's `_request` is replaced with a recorder, so the tests
|
|
83
|
+
assert the exact payload the API would receive.
|
|
84
|
+
|
|
36
85
|
## Quick start
|
|
37
86
|
|
|
38
87
|
```python
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validation Resource for the Pulse SDK.
|
|
3
|
+
|
|
4
|
+
Compares simulated LOB data against a historical day using distributional
|
|
5
|
+
metrics, impact response, Cont's stylised facts, cross-level volume
|
|
6
|
+
correlation, and the MIND/FID inception distances on DeepLOB embeddings.
|
|
7
|
+
|
|
8
|
+
Submit, then poll::
|
|
9
|
+
|
|
10
|
+
job = client.validation.run(
|
|
11
|
+
symbol="BARC", date="2026-06-17", provider="bmll", exchange="lse",
|
|
12
|
+
sim_ids=[...], ticksize=0.05,
|
|
13
|
+
)
|
|
14
|
+
result = client.validation.get_job(job["job_id"])
|
|
15
|
+
result["distances"]["spread"]["l1"] # per simulation
|
|
16
|
+
result["stylised_fact_verdicts"]["heavy_tails"]
|
|
17
|
+
|
|
18
|
+
``run`` submits and returns; a validation of a real day takes minutes, so
|
|
19
|
+
``get_job`` is the status endpoint you call until it is done. ``list_jobs``
|
|
20
|
+
reaches past runs.
|
|
21
|
+
|
|
22
|
+
Selecting metrics
|
|
23
|
+
-----------------
|
|
24
|
+
One flag per metric — ``statistical``, ``stylised_facts``, ``impact``,
|
|
25
|
+
``volume_correlation``, ``fid``, ``mind``. Left as ``None`` each takes the
|
|
26
|
+
default for your tier, resolved server-side, so naming none behaves exactly as
|
|
27
|
+
your account is entitled to. Pass ``False`` to skip an expensive pass or
|
|
28
|
+
``True`` to force one on.
|
|
29
|
+
|
|
30
|
+
The historical gate
|
|
31
|
+
-------------------
|
|
32
|
+
Whether the historical half of the comparison comes back — the series in the
|
|
33
|
+
JSON *and* the historical trace on any rendered plot — is decided by your tier,
|
|
34
|
+
not by a parameter here. What is never gated: the L1 and Wasserstein
|
|
35
|
+
``distances``, the FID and MIND scores, and the true/false
|
|
36
|
+
``stylised_fact_verdicts`` for both the historical day and each simulation.
|
|
37
|
+
Those are derived scalars and booleans, not series, so every tier receives
|
|
38
|
+
them.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
import json
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
RUN_PATH = "/validation/run"
|
|
46
|
+
UPLOAD_PATH = "/validation/run/upload"
|
|
47
|
+
JOBS_PATH = "/validation/jobs"
|
|
48
|
+
|
|
49
|
+
#: The API rejects more per job; checked client-side so a 26-file submission
|
|
50
|
+
#: fails before any bytes are uploaded.
|
|
51
|
+
MAX_SIM_FILES = 25
|
|
52
|
+
|
|
53
|
+
#: One flag per area of checking. Left unset each takes the server's default.
|
|
54
|
+
_AREA_FLAGS = (
|
|
55
|
+
"statistical",
|
|
56
|
+
"stylised_facts",
|
|
57
|
+
"impact",
|
|
58
|
+
"volume_correlation",
|
|
59
|
+
"fid",
|
|
60
|
+
"mind",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
#: ``lob`` marks the frames as L2 snapshots, which switches off anything
|
|
64
|
+
#: needing the message stream; ``sample_period`` and ``match_generated_sample``
|
|
65
|
+
#: set the grid the book is resampled onto; ``plots`` is False, True, or a list
|
|
66
|
+
#: of plot ids.
|
|
67
|
+
_EXTRA_FIELDS = ("lob", "sample_period", "match_generated_sample", "plots")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _frame_to_parquet(entry, index: int):
|
|
71
|
+
"""Normalise one simulated run to ``(filename, parquet bytes)``.
|
|
72
|
+
|
|
73
|
+
Accepts a path, a ``(filename, bytes)`` pair, or a polars / pandas
|
|
74
|
+
DataFrame, which is written to parquet in memory. The frame must be pulse
|
|
75
|
+
format -- the same shape the engine writes to ``sim_data.parquet`` -- which
|
|
76
|
+
the server validates; sending something else fails there, not here.
|
|
77
|
+
"""
|
|
78
|
+
import io
|
|
79
|
+
|
|
80
|
+
if isinstance(entry, tuple):
|
|
81
|
+
return entry
|
|
82
|
+
|
|
83
|
+
# Duck-typed rather than imported: neither polars nor pandas is a hard
|
|
84
|
+
# dependency of the SDK, and importing one to test for the other would
|
|
85
|
+
# make it one.
|
|
86
|
+
writer = getattr(entry, "write_parquet", None) # polars
|
|
87
|
+
if writer is not None:
|
|
88
|
+
buf = io.BytesIO()
|
|
89
|
+
writer(buf)
|
|
90
|
+
return f"sim_{index}.parquet", buf.getvalue()
|
|
91
|
+
|
|
92
|
+
writer = getattr(entry, "to_parquet", None) # pandas
|
|
93
|
+
if writer is not None:
|
|
94
|
+
buf = io.BytesIO()
|
|
95
|
+
writer(buf, index=False)
|
|
96
|
+
return f"sim_{index}.parquet", buf.getvalue()
|
|
97
|
+
|
|
98
|
+
path = Path(entry)
|
|
99
|
+
return path.name, path.read_bytes()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _build_config(n_levels, **flags) -> dict:
|
|
103
|
+
"""The validation config as the API expects it.
|
|
104
|
+
|
|
105
|
+
Flags left as ``None`` are omitted rather than sent as null: the API reads
|
|
106
|
+
absence as "use my tier's default", and an explicit null would not do that.
|
|
107
|
+
"""
|
|
108
|
+
config = {"n_levels": n_levels}
|
|
109
|
+
for name in _AREA_FLAGS + _EXTRA_FIELDS:
|
|
110
|
+
value = flags.get(name)
|
|
111
|
+
if value is not None:
|
|
112
|
+
config[name] = value
|
|
113
|
+
|
|
114
|
+
unknown = set(flags) - set(_AREA_FLAGS) - set(_EXTRA_FIELDS)
|
|
115
|
+
if unknown:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"Unknown validation option(s): {sorted(unknown)}. "
|
|
118
|
+
f"Valid: {sorted(_AREA_FLAGS + _EXTRA_FIELDS)}"
|
|
119
|
+
)
|
|
120
|
+
return config
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _save_plots(result: dict, plot_dir=None) -> list:
|
|
124
|
+
"""Write the returned figures to disk and return the paths written.
|
|
125
|
+
|
|
126
|
+
The API sends rendered plots base64-encoded in the response. Left to
|
|
127
|
+
itself that is bytes you cannot look at, so when plots were asked for they
|
|
128
|
+
are written out: to ``plot_dir`` when given, otherwise the current
|
|
129
|
+
directory. The directory is created if it does not exist.
|
|
130
|
+
"""
|
|
131
|
+
import base64
|
|
132
|
+
|
|
133
|
+
target = Path(plot_dir) if plot_dir is not None else Path.cwd()
|
|
134
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
|
|
136
|
+
written = []
|
|
137
|
+
for category, entries in (result.get("plots") or {}).items():
|
|
138
|
+
for entry in entries or []:
|
|
139
|
+
content = entry.get("content_base64")
|
|
140
|
+
if not content:
|
|
141
|
+
continue
|
|
142
|
+
path = target / f"{entry.get('name') or category}.png"
|
|
143
|
+
path.write_bytes(base64.b64decode(content))
|
|
144
|
+
written.append(str(path))
|
|
145
|
+
return written
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ValidationResource:
|
|
149
|
+
"""Validate simulated market data against a historical day."""
|
|
150
|
+
|
|
151
|
+
def __init__(self, client):
|
|
152
|
+
self._client = client
|
|
153
|
+
|
|
154
|
+
def run(
|
|
155
|
+
self,
|
|
156
|
+
symbol: str,
|
|
157
|
+
date: str,
|
|
158
|
+
provider: str,
|
|
159
|
+
exchange: str,
|
|
160
|
+
*,
|
|
161
|
+
sim_ids=None,
|
|
162
|
+
sim_files=None,
|
|
163
|
+
ticksize: float = 1.0,
|
|
164
|
+
statistical=None,
|
|
165
|
+
stylised_facts=None,
|
|
166
|
+
impact=None,
|
|
167
|
+
volume_correlation=None,
|
|
168
|
+
fid=None,
|
|
169
|
+
mind=None,
|
|
170
|
+
lob=None,
|
|
171
|
+
sample_period=None,
|
|
172
|
+
match_generated_sample=None,
|
|
173
|
+
n_levels: int = 10,
|
|
174
|
+
plots=None,
|
|
175
|
+
) -> dict:
|
|
176
|
+
"""Submit a validation job.
|
|
177
|
+
|
|
178
|
+
Returns as soon as the job is accepted. Poll :meth:`get_job` for
|
|
179
|
+
status and, once it is complete, the result — a validation of a real
|
|
180
|
+
day takes minutes, which is far too long to hold a request open.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
symbol : str
|
|
185
|
+
Trading symbol, e.g. ``"BARC"`` or ``"700.HK"``.
|
|
186
|
+
date : str
|
|
187
|
+
Calibration date, ``"YYYY-MM-DD"``. The historical day compared
|
|
188
|
+
against.
|
|
189
|
+
provider : str
|
|
190
|
+
Data provider, ``"bmll"`` or ``"omd"``. Required: the same symbol
|
|
191
|
+
exists under both with different dates and tick sizes, so
|
|
192
|
+
``(symbol, provider, exchange)`` is the identity, not the symbol.
|
|
193
|
+
exchange : str
|
|
194
|
+
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
|
|
199
|
+
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.
|
|
202
|
+
ticksize : float, default 1.0
|
|
203
|
+
Minimum price increment. Should match the instrument.
|
|
204
|
+
statistical, stylised_facts, impact, volume_correlation, fid, mind : bool, optional
|
|
205
|
+
Whether to run each area. ``None`` uses your tier's default.
|
|
206
|
+
lob : bool, optional
|
|
207
|
+
The frames are L2 snapshots. Anything needing the message stream
|
|
208
|
+
is skipped, with the reason reported in
|
|
209
|
+
``metadata["areas_skipped"]``.
|
|
210
|
+
sample_period : str, optional
|
|
211
|
+
The grid the book is resampled onto in lob mode, e.g. ``"1s"``.
|
|
212
|
+
Omitted, the cadence is read off your generated frames; if those
|
|
213
|
+
are event-level there is no grid to match and nothing is
|
|
214
|
+
resampled.
|
|
215
|
+
match_generated_sample : bool, optional
|
|
216
|
+
Match the generated frames' grid even when ``sample_period`` is
|
|
217
|
+
given.
|
|
218
|
+
n_levels : int, default 10
|
|
219
|
+
Book levels to measure over.
|
|
220
|
+
plots : bool or list of str, optional
|
|
221
|
+
``None`` or ``False`` for none, ``True`` for every figure, or plot
|
|
222
|
+
ids such as ``["statistical.radar", "stylised_facts.overall"]``.
|
|
223
|
+
Rendered server-side; :meth:`get_job` writes them to disk.
|
|
224
|
+
|
|
225
|
+
Returns
|
|
226
|
+
-------
|
|
227
|
+
dict
|
|
228
|
+
``{"job_id": str, "status": str, "message": str}``. Pass the
|
|
229
|
+
job_id to :meth:`get_job`.
|
|
230
|
+
|
|
231
|
+
Raises
|
|
232
|
+
------
|
|
233
|
+
ValueError
|
|
234
|
+
If the parameters are invalid.
|
|
235
|
+
"""
|
|
236
|
+
if (sim_ids is None) == (sim_files is None):
|
|
237
|
+
raise ValueError("Pass exactly one of sim_ids or sim_files")
|
|
238
|
+
if sim_ids is not None and not sim_ids:
|
|
239
|
+
raise ValueError("sim_ids must not be empty")
|
|
240
|
+
if sim_files is not None and not sim_files:
|
|
241
|
+
raise ValueError("sim_files must not be empty")
|
|
242
|
+
count = len(sim_ids if sim_ids is not None else sim_files)
|
|
243
|
+
if count > MAX_SIM_FILES:
|
|
244
|
+
raise ValueError(f"Maximum {MAX_SIM_FILES} simulations per validation job")
|
|
245
|
+
if ticksize <= 0:
|
|
246
|
+
raise ValueError("ticksize must be positive")
|
|
247
|
+
if n_levels < 1:
|
|
248
|
+
raise ValueError("n_levels must be at least 1")
|
|
249
|
+
|
|
250
|
+
config = _build_config(
|
|
251
|
+
n_levels,
|
|
252
|
+
statistical=statistical,
|
|
253
|
+
stylised_facts=stylised_facts,
|
|
254
|
+
impact=impact,
|
|
255
|
+
volume_correlation=volume_correlation,
|
|
256
|
+
fid=fid,
|
|
257
|
+
mind=mind,
|
|
258
|
+
lob=lob,
|
|
259
|
+
sample_period=sample_period,
|
|
260
|
+
match_generated_sample=match_generated_sample,
|
|
261
|
+
plots=plots,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if sim_ids is not None:
|
|
265
|
+
submitted = self._client._request(
|
|
266
|
+
"POST",
|
|
267
|
+
RUN_PATH,
|
|
268
|
+
json={
|
|
269
|
+
"symbol": symbol,
|
|
270
|
+
"date": date,
|
|
271
|
+
"provider": provider,
|
|
272
|
+
"exchange": exchange,
|
|
273
|
+
"sim_ids": list(sim_ids),
|
|
274
|
+
"ticksize": ticksize,
|
|
275
|
+
"config": config,
|
|
276
|
+
},
|
|
277
|
+
)
|
|
278
|
+
else:
|
|
279
|
+
# Three-tuple parts: the content type matters to the server's
|
|
280
|
+
# multipart parser, so it is sent explicitly.
|
|
281
|
+
files = [
|
|
282
|
+
("sim_files", (*_frame_to_parquet(entry, i), "application/octet-stream"))
|
|
283
|
+
for i, entry in enumerate(sim_files)
|
|
284
|
+
]
|
|
285
|
+
submitted = self._client._request(
|
|
286
|
+
"POST",
|
|
287
|
+
UPLOAD_PATH,
|
|
288
|
+
files=files,
|
|
289
|
+
data={
|
|
290
|
+
"symbol": symbol,
|
|
291
|
+
"date": date,
|
|
292
|
+
"provider": provider,
|
|
293
|
+
"exchange": exchange,
|
|
294
|
+
"ticksize": str(ticksize),
|
|
295
|
+
"config": json.dumps(config),
|
|
296
|
+
},
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if not (submitted or {}).get("job_id"):
|
|
300
|
+
raise ValueError(f"Validation API returned no job_id: {submitted}")
|
|
301
|
+
return submitted
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def get_job(self, job_id: str, plot_dir=None) -> dict:
|
|
305
|
+
"""Fetch a validation job's status, and its result once it is done.
|
|
306
|
+
|
|
307
|
+
This is the status endpoint: call it until ``status`` is
|
|
308
|
+
``"completed"`` or ``"failed"``.
|
|
309
|
+
|
|
310
|
+
Parameters
|
|
311
|
+
----------
|
|
312
|
+
job_id : str
|
|
313
|
+
From :meth:`run`.
|
|
314
|
+
plot_dir : str or Path, optional
|
|
315
|
+
Where to write any figures the job rendered. Defaults to the
|
|
316
|
+
current directory; created if it does not exist. Only has an
|
|
317
|
+
effect once the job is complete and only if ``plots`` was set on
|
|
318
|
+
the run. The paths written come back in ``plot_paths``.
|
|
319
|
+
|
|
320
|
+
Returns
|
|
321
|
+
-------
|
|
322
|
+
dict
|
|
323
|
+
``job_id``, ``status``, ``symbol``, ``date`` and, once complete:
|
|
324
|
+
``metadata``, ``distances``, ``distributions``,
|
|
325
|
+
``impact_response``, ``stylised_facts``,
|
|
326
|
+
``stylised_fact_verdicts``, ``volume_correlation``,
|
|
327
|
+
``fid_scores``, ``mind_scores`` — enough to rebuild every figure —
|
|
328
|
+
plus ``plot_paths`` when figures were written.
|
|
329
|
+
"""
|
|
330
|
+
job = self._client._request("GET", f"{JOBS_PATH}/{job_id}")
|
|
331
|
+
if (job or {}).get("plots"):
|
|
332
|
+
job["plot_paths"] = _save_plots(job, plot_dir)
|
|
333
|
+
return job
|
|
334
|
+
|
|
335
|
+
def list_jobs(self, limit: int = 50) -> dict:
|
|
336
|
+
"""List your validation jobs, newest first.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
limit : int, default 50
|
|
341
|
+
Maximum number of jobs to return.
|
|
342
|
+
|
|
343
|
+
Returns
|
|
344
|
+
-------
|
|
345
|
+
dict
|
|
346
|
+
``{"jobs": list, "total": int}``.
|
|
347
|
+
"""
|
|
348
|
+
return self._client._request("GET", JOBS_PATH, params={"limit": limit})
|
{simudyne_pulse-0.7.0.dev6 → simudyne_pulse-0.7.0.dev8/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.dev8
|
|
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,55 @@ 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
|
+
### Working from a checkout
|
|
63
|
+
|
|
64
|
+
To run the SDK from source — editing it, or using unreleased changes — install
|
|
65
|
+
it editable **into the interpreter you will actually import from**. A notebook
|
|
66
|
+
kernel is frequently not the python on your `$PATH`:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# in a notebook, find the right interpreter first
|
|
70
|
+
import sys; print(sys.executable)
|
|
71
|
+
|
|
72
|
+
# then, with that path
|
|
73
|
+
<that python> -m pip install -e /path/to/pulse-sdk
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Check which copy you loaded — this is worth doing whenever an attribute seems
|
|
77
|
+
to be missing:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import simudyne
|
|
81
|
+
from importlib.metadata import version
|
|
82
|
+
|
|
83
|
+
print(simudyne.__file__) # should be .../pulse-sdk/src/simudyne/...
|
|
84
|
+
print(version("simudyne-pulse"))
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### If an older install shadows it
|
|
88
|
+
|
|
89
|
+
The SDK was once published under the distribution name **`simudyne`**; it is
|
|
90
|
+
now **`simudyne-pulse`**. Both ship a module called `simudyne`, and the older
|
|
91
|
+
one installs a real directory while the editable install only adds a path
|
|
92
|
+
entry — so the old copy wins and you get errors like
|
|
93
|
+
`'PulseABM' object has no attribute 'validation'` from a version that predates
|
|
94
|
+
the feature. `pip list` shows both. Remove the obsolete one:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
<that python> -m pip uninstall simudyne # the old distribution
|
|
98
|
+
<that python> -m pip install -e /path/to/pulse-sdk
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Running the tests
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
uv run --with pytest --with requests --with pandas --with pyarrow \
|
|
105
|
+
python -m pytest tests/ -q
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
No network: the client's `_request` is replaced with a recorder, so the tests
|
|
109
|
+
assert the exact payload the API would receive.
|
|
110
|
+
|
|
62
111
|
## Quick start
|
|
63
112
|
|
|
64
113
|
```python
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Payload tests for the 0.8.0 surface: validation.
|
|
1
|
+
"""Payload tests for the 0.8.0 surface: validation.run, the fm and fix
|
|
2
2
|
resources, and simulation.get_job_logs / run_lrm.
|
|
3
3
|
|
|
4
4
|
No network — same recorder pattern as test_validation.py. These pin the wire
|
|
@@ -30,7 +30,7 @@ class TestRunUpload:
|
|
|
30
30
|
def test_rejects_empty_before_any_request(self):
|
|
31
31
|
client = FakeClient()
|
|
32
32
|
with pytest.raises(ValueError, match="empty"):
|
|
33
|
-
ValidationResource(client).
|
|
33
|
+
ValidationResource(client).run(
|
|
34
34
|
"700.HK", "2025-09-01", "omd", "hkex_securities", sim_files=[]
|
|
35
35
|
)
|
|
36
36
|
assert client.calls == []
|
|
@@ -39,18 +39,18 @@ class TestRunUpload:
|
|
|
39
39
|
client = FakeClient()
|
|
40
40
|
files = [(f"sim_{i}.parquet", b"x") for i in range(26)]
|
|
41
41
|
with pytest.raises(ValueError, match="25"):
|
|
42
|
-
ValidationResource(client).
|
|
42
|
+
ValidationResource(client).run(
|
|
43
43
|
"700.HK", "2025-09-01", "omd", "hkex_securities", sim_files=files
|
|
44
44
|
)
|
|
45
45
|
assert client.calls == []
|
|
46
46
|
|
|
47
47
|
def test_multipart_payload_shape(self):
|
|
48
48
|
client = FakeClient([{"job_id": "v1", "status": "pending"}])
|
|
49
|
-
ValidationResource(client).
|
|
49
|
+
ValidationResource(client).run(
|
|
50
50
|
"700.HK", "2025-09-01", "omd", "hkex_securities",
|
|
51
51
|
sim_files=[("sim_0000.parquet", b"PARQ")],
|
|
52
52
|
ticksize=0.5,
|
|
53
|
-
|
|
53
|
+
statistical=False,
|
|
54
54
|
)
|
|
55
55
|
method, path, kwargs = client.calls[0]
|
|
56
56
|
assert (method, path) == ("POST", "/validation/run/upload")
|
|
@@ -58,11 +58,11 @@ class TestRunUpload:
|
|
|
58
58
|
assert data["provider"] == "omd" and data["exchange"] == "hkex_securities"
|
|
59
59
|
assert data["ticksize"] == "0.5"
|
|
60
60
|
config = json.loads(data["config"])
|
|
61
|
-
# explicit False sent; unset
|
|
62
|
-
assert config["
|
|
63
|
-
for flag in ("
|
|
61
|
+
# explicit False sent; unset area flags omitted (tier default)
|
|
62
|
+
assert config["statistical"] is False
|
|
63
|
+
for flag in ("impact", "stylised_facts", "fid", "mind",
|
|
64
|
+
"run_metrics", "plot_data"):
|
|
64
65
|
assert flag not in config
|
|
65
|
-
assert config["run_fid"] is True
|
|
66
66
|
[(field, (filename, content, mime))] = kwargs["files"]
|
|
67
67
|
assert field == "sim_files" and filename == "sim_0000.parquet"
|
|
68
68
|
assert content == b"PARQ" and mime == "application/octet-stream"
|
|
@@ -70,8 +70,8 @@ class TestRunUpload:
|
|
|
70
70
|
def test_reads_paths_from_disk(self, tmp_path):
|
|
71
71
|
p = tmp_path / "sim_0001.parquet"
|
|
72
72
|
p.write_bytes(b"BYTES")
|
|
73
|
-
client = FakeClient([{}])
|
|
74
|
-
ValidationResource(client).
|
|
73
|
+
client = FakeClient([{"job_id": "v1"}])
|
|
74
|
+
ValidationResource(client).run(
|
|
75
75
|
"700.HK", "2025-09-01", "omd", "hkex_securities", sim_files=[p]
|
|
76
76
|
)
|
|
77
77
|
[(_, (filename, content, _))] = client.calls[0][2]["files"]
|