simudyne-pulse 0.7.2__tar.gz → 0.8.0.dev1__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.8.0.dev1/PKG-INFO +270 -0
- simudyne_pulse-0.8.0.dev1/README.md +244 -0
- {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/pyproject.toml +2 -2
- {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/src/simudyne/client.py +82 -6
- simudyne_pulse-0.8.0.dev1/src/simudyne/exceptions.py +96 -0
- {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/src/simudyne/resources/api_keys.py +57 -3
- simudyne_pulse-0.8.0.dev1/src/simudyne/resources/data.py +356 -0
- simudyne_pulse-0.8.0.dev1/src/simudyne/resources/fix.py +79 -0
- simudyne_pulse-0.8.0.dev1/src/simudyne/resources/fm.py +340 -0
- {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/src/simudyne/resources/profile.py +62 -0
- simudyne_pulse-0.8.0.dev1/src/simudyne/resources/simulation.py +1292 -0
- simudyne_pulse-0.8.0.dev1/src/simudyne/resources/simulator_gym.py +304 -0
- simudyne_pulse-0.8.0.dev1/src/simudyne/resources/validation.py +677 -0
- simudyne_pulse-0.8.0.dev1/src/simudyne_pulse.egg-info/PKG-INFO +270 -0
- {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/src/simudyne_pulse.egg-info/SOURCES.txt +6 -2
- simudyne_pulse-0.8.0.dev1/tests/test_api_contract.py +191 -0
- simudyne_pulse-0.8.0.dev1/tests/test_new_resources.py +214 -0
- simudyne_pulse-0.8.0.dev1/tests/test_validation.py +429 -0
- simudyne_pulse-0.7.2/PKG-INFO +0 -147
- simudyne_pulse-0.7.2/README.md +0 -121
- simudyne_pulse-0.7.2/src/simudyne/exceptions.py +0 -7
- simudyne_pulse-0.7.2/src/simudyne/resources/data.py +0 -53
- simudyne_pulse-0.7.2/src/simudyne/resources/historical.py +0 -1
- simudyne_pulse-0.7.2/src/simudyne/resources/simulation.py +0 -790
- simudyne_pulse-0.7.2/src/simudyne/resources/simulator_gym.py +0 -131
- simudyne_pulse-0.7.2/src/simudyne/resources/validation.py +0 -299
- simudyne_pulse-0.7.2/src/simudyne_pulse.egg-info/PKG-INFO +0 -147
- {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/LICENSE +0 -0
- {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/setup.cfg +0 -0
- {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/src/simudyne/__init__.py +0 -0
- {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/src/simudyne/resources/__init__.py +0 -0
- {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
- {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/src/simudyne_pulse.egg-info/requires.txt +0 -0
- {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
- {simudyne_pulse-0.7.2 → simudyne_pulse-0.8.0.dev1}/tests/test_simulator_gym.py +0 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simudyne-pulse
|
|
3
|
+
Version: 0.8.0.dev1
|
|
4
|
+
Summary: Python SDK for the Simudyne Pulse synthetic market data API
|
|
5
|
+
Author-email: Simudyne <support@simudyne.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://pulse.simudyne.com
|
|
8
|
+
Project-URL: Documentation, https://pulse.simudyne.com/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/simudyne/pulse-sdk
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: requests>=2.28.0
|
|
20
|
+
Requires-Dist: polars>=0.20.0
|
|
21
|
+
Requires-Dist: tqdm>=4.60.0
|
|
22
|
+
Requires-Dist: websocket-client>=1.0.0
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest>=8.0.0; extra == "test"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# Simudyne Pulse Python SDK
|
|
28
|
+
|
|
29
|
+
Python client for the [Pulse](https://pulse.simudyne.com) synthetic market data API. Returns data as [Polars](https://pola.rs/) DataFrames.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install "simudyne-pulse>=0.8.0"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
0.7.x predates the current API: its validation and data calls are rejected by
|
|
38
|
+
pulse-api-pod 1.77 and later.
|
|
39
|
+
|
|
40
|
+
The distribution is named `simudyne-pulse`; the import name is `simudyne`:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from simudyne import PulseABM
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Requires Python 3.10+.
|
|
47
|
+
|
|
48
|
+
### Development builds
|
|
49
|
+
|
|
50
|
+
The `dev` branch is a prerelease channel. Pushes to it publish prerelease
|
|
51
|
+
versions (e.g. `0.8.0.dev1`) that are separate from the stable versions cut on
|
|
52
|
+
`prod`. `pip install simudyne-pulse` always resolves to the latest **stable**
|
|
53
|
+
release and ignores prereleases, so dev builds can never affect a normal
|
|
54
|
+
install.
|
|
55
|
+
|
|
56
|
+
To install the latest dev build, opt in with `--pre`:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install --pre simudyne-pulse
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Only use dev builds for testing unreleased changes; they are not guaranteed
|
|
63
|
+
stable. Merge `dev` into `prod` to promote those changes to a stable release.
|
|
64
|
+
|
|
65
|
+
### Comparing named populations
|
|
66
|
+
|
|
67
|
+
Simulations are often not one population but several — a foundation model and
|
|
68
|
+
an ABM, or two versions of a model. Name them and the results compare them
|
|
69
|
+
instead of averaging them together:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
job = client.validation.run(
|
|
73
|
+
symbol="TSCO", date="2026-06-23", provider="bmll", exchange="lse",
|
|
74
|
+
sim_ids={"fm": fm_ids, "abm": abm_ids},
|
|
75
|
+
plots=["statistical.radar", "stylised_facts.overall",
|
|
76
|
+
"volume_correlation.overall"],
|
|
77
|
+
)
|
|
78
|
+
result = client.validation.get_job(job["job_id"], plot_dir="figs")
|
|
79
|
+
result["distances"]["fm"]["spread"]["l1"]
|
|
80
|
+
result["stylised_fact_verdicts"]["heavy_tails"]["simulated"]["abm"]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`distances`, `distributions`, the verdicts and the FID/MIND scores come back
|
|
84
|
+
keyed by name; the radar draws a polygon per population, the verdict table a
|
|
85
|
+
column each, and volume correlation puts historical first then a heatmap per
|
|
86
|
+
population. `sim_files` groups the same way.
|
|
87
|
+
|
|
88
|
+
A plain list is one unnamed population and keeps the flat shape exactly as
|
|
89
|
+
before. The historical day is measured once however many groups there are.
|
|
90
|
+
|
|
91
|
+
### Your own runs beside platform runs
|
|
92
|
+
|
|
93
|
+
`sim_ids` and `sim_files` can be given together — your own model against
|
|
94
|
+
platform simulations, on one set of figures, 25 runs in total across the two:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
job = client.validation.run(
|
|
98
|
+
symbol="TSCO", date="2026-06-23", provider="bmll", exchange="lse",
|
|
99
|
+
sim_files={"fm": ["my_run.parquet"]},
|
|
100
|
+
sim_ids={"abm": abm_ids},
|
|
101
|
+
plots=["statistical.radar", "stylised_facts.overall"],
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
With both present each source is a population of its own, so a bare list gets
|
|
106
|
+
a name rather than merging into whatever else is there:
|
|
107
|
+
|
|
108
|
+
| passed | populations |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| files mapping + ids list | its names, plus `platform` |
|
|
111
|
+
| files list + ids mapping | `uploaded`, plus its names |
|
|
112
|
+
| both lists | `uploaded` and `platform` |
|
|
113
|
+
| both mappings | their own names |
|
|
114
|
+
| either one alone | exactly as before |
|
|
115
|
+
|
|
116
|
+
The uploads are measured first and the platform runs follow.
|
|
117
|
+
|
|
118
|
+
`plots` is the only plot switch: unset draws nothing, `True` draws every
|
|
119
|
+
figure the enabled areas can draw, and a list draws just those ids. An area
|
|
120
|
+
switched off draws nothing either way.
|
|
121
|
+
|
|
122
|
+
| id | figure |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `statistical.radar` | distance spider, one polygon per population |
|
|
125
|
+
| `statistical.distribution` | every metric's KDE |
|
|
126
|
+
| `statistical.distribution.{metric}` | one metric, e.g. `.spread` |
|
|
127
|
+
| `stylised_facts.overall` | the verdict table |
|
|
128
|
+
| `stylised_facts.fact` | every fact |
|
|
129
|
+
| `stylised_facts.fact.{name}` | one fact, e.g. `.heavy_tails` |
|
|
130
|
+
| `impact.response` | impact response by event type |
|
|
131
|
+
| `impact.event` | every event type |
|
|
132
|
+
| `impact.event.{type}` | one event type |
|
|
133
|
+
| `volume_correlation.levels` | level correlation heatmap |
|
|
134
|
+
| `volume_correlation.changes` | change correlation heatmap |
|
|
135
|
+
| `volume_correlation.overall` | both of the above |
|
|
136
|
+
|
|
137
|
+
Each returned figure carries the `id` it was asked for — match on that, not on
|
|
138
|
+
the filename.
|
|
139
|
+
|
|
140
|
+
### Working from a checkout
|
|
141
|
+
|
|
142
|
+
To run the SDK from source — editing it, or using unreleased changes — install
|
|
143
|
+
it editable **into the interpreter you will actually import from**. A notebook
|
|
144
|
+
kernel is frequently not the python on your `$PATH`:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# in a notebook, find the right interpreter first
|
|
148
|
+
import sys; print(sys.executable)
|
|
149
|
+
|
|
150
|
+
# then, with that path
|
|
151
|
+
<that python> -m pip install -e /path/to/pulse-sdk
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Check which copy you loaded — this is worth doing whenever an attribute seems
|
|
155
|
+
to be missing:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
import simudyne
|
|
159
|
+
from importlib.metadata import version
|
|
160
|
+
|
|
161
|
+
print(simudyne.__file__) # should be .../pulse-sdk/src/simudyne/...
|
|
162
|
+
print(version("simudyne-pulse"))
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### If an older install shadows it
|
|
166
|
+
|
|
167
|
+
The SDK was once published under the distribution name **`simudyne`**; it is
|
|
168
|
+
now **`simudyne-pulse`**. Both ship a module called `simudyne`, and the older
|
|
169
|
+
one installs a real directory while the editable install only adds a path
|
|
170
|
+
entry — so the old copy wins and you get errors like
|
|
171
|
+
`'PulseABM' object has no attribute 'validation'` from a version that predates
|
|
172
|
+
the feature. `pip list` shows both. Remove the obsolete one:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
<that python> -m pip uninstall simudyne # the old distribution
|
|
176
|
+
<that python> -m pip install -e /path/to/pulse-sdk
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Running the tests
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
uv run --with pytest --with requests --with pandas --with pyarrow \
|
|
183
|
+
python -m pytest tests/ -q
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
No network: the client's `_request` is replaced with a recorder, so the tests
|
|
187
|
+
assert the exact payload the API would receive.
|
|
188
|
+
|
|
189
|
+
## Quick start
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from simudyne import PulseABM
|
|
193
|
+
|
|
194
|
+
client = PulseABM(api_key="pk_live_...") # or set SIMUDYNE_API_KEY
|
|
195
|
+
|
|
196
|
+
# Days the agent-based model can simulate
|
|
197
|
+
cal = client.data.calibrated_data(symbol="700.HK")
|
|
198
|
+
|
|
199
|
+
# Run it, poll, read the generated book
|
|
200
|
+
job = client.simulation.run(
|
|
201
|
+
symbol="700.HK", cal_date="2025-09-02",
|
|
202
|
+
provider="omd", exchange="hkex_securities",
|
|
203
|
+
)
|
|
204
|
+
import time
|
|
205
|
+
while not client.simulation.get_job_status(job["job_id"])["is_complete"]:
|
|
206
|
+
time.sleep(20)
|
|
207
|
+
book = client.simulation.get_sim_data(job["sim_ids"][0])
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
A foundation model runs through the same call with `engine="fm"` and a
|
|
211
|
+
`model_id`, which may be the model's production name (e.g.
|
|
212
|
+
`"flow-hkex-1-100M"`; see `client.fm.models()`). Fields that belong to the
|
|
213
|
+
other engine are sent and rejected by the API with a 422 rather than dropped.
|
|
214
|
+
|
|
215
|
+
Every method has a full numpy-style docstring (`help(client.simulation.run)`);
|
|
216
|
+
the user guide is at https://pulse.simudyne.com/docs.
|
|
217
|
+
|
|
218
|
+
## API reference
|
|
219
|
+
|
|
220
|
+
### `PulseABM(api_key, base_url=None)`
|
|
221
|
+
|
|
222
|
+
| Parameter | Env variable | Default |
|
|
223
|
+
|-----------|-------------|---------|
|
|
224
|
+
| `api_key` | `SIMUDYNE_API_KEY` | required |
|
|
225
|
+
| `base_url` | `SIMUDYNE_BASE_URL` | Pulse API |
|
|
226
|
+
|
|
227
|
+
| Resource | What it covers |
|
|
228
|
+
|----------|----------------|
|
|
229
|
+
| `client.data` | `available_data` (every symbol-day that exists), `calibrated_data` (what the ABM can run), `calibrate` |
|
|
230
|
+
| `client.simulation` | `run` (both engines), `run_lrm`, `get_jobs`, `get_job_status`, `get_job_results`, `get_job_logs`, sim files and downloads |
|
|
231
|
+
| `client.validation` | `run`, `get_job_status`, `get_job`, `list_jobs` — results carry `errors` per failed area |
|
|
232
|
+
| `client.fm` | `models`, `live`; admin: `registry`, `register`, `activate`, `deactivate` |
|
|
233
|
+
| `client.fix` | FIX session usage |
|
|
234
|
+
| `client.simulator_gym` | the interactive simulator websocket |
|
|
235
|
+
|
|
236
|
+
Failures raise `PulseAPIError` with `status_code`, `detail` (exactly as the
|
|
237
|
+
API sent it) and `errors`; `str(exc)` renders validation errors as
|
|
238
|
+
`field: message` lines.
|
|
239
|
+
|
|
240
|
+
### `client.profile`
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
client.profile.get() # Account info
|
|
244
|
+
client.profile.usage() # API usage stats
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### `client.api_keys`
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
client.api_keys.list() # List active keys
|
|
251
|
+
client.api_keys.create(name="research") # Create a new key
|
|
252
|
+
client.api_keys.revoke(key_id="key_...") # Revoke a key
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Configuration
|
|
256
|
+
|
|
257
|
+
You can set your API key as an environment variable instead of passing it directly:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
export SIMUDYNE_API_KEY=pk_live_...
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
from simudyne import PulseABM
|
|
265
|
+
client = PulseABM() # picks up SIMUDYNE_API_KEY automatically
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
MIT
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Simudyne Pulse Python SDK
|
|
2
|
+
|
|
3
|
+
Python client for the [Pulse](https://pulse.simudyne.com) synthetic market data API. Returns data as [Polars](https://pola.rs/) DataFrames.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install "simudyne-pulse>=0.8.0"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
0.7.x predates the current API: its validation and data calls are rejected by
|
|
12
|
+
pulse-api-pod 1.77 and later.
|
|
13
|
+
|
|
14
|
+
The distribution is named `simudyne-pulse`; the import name is `simudyne`:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from simudyne import PulseABM
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires Python 3.10+.
|
|
21
|
+
|
|
22
|
+
### Development builds
|
|
23
|
+
|
|
24
|
+
The `dev` branch is a prerelease channel. Pushes to it publish prerelease
|
|
25
|
+
versions (e.g. `0.8.0.dev1`) that are separate from the stable versions cut on
|
|
26
|
+
`prod`. `pip install simudyne-pulse` always resolves to the latest **stable**
|
|
27
|
+
release and ignores prereleases, so dev builds can never affect a normal
|
|
28
|
+
install.
|
|
29
|
+
|
|
30
|
+
To install the latest dev build, opt in with `--pre`:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install --pre simudyne-pulse
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Only use dev builds for testing unreleased changes; they are not guaranteed
|
|
37
|
+
stable. Merge `dev` into `prod` to promote those changes to a stable release.
|
|
38
|
+
|
|
39
|
+
### Comparing named populations
|
|
40
|
+
|
|
41
|
+
Simulations are often not one population but several — a foundation model and
|
|
42
|
+
an ABM, or two versions of a model. Name them and the results compare them
|
|
43
|
+
instead of averaging them together:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
job = client.validation.run(
|
|
47
|
+
symbol="TSCO", date="2026-06-23", provider="bmll", exchange="lse",
|
|
48
|
+
sim_ids={"fm": fm_ids, "abm": abm_ids},
|
|
49
|
+
plots=["statistical.radar", "stylised_facts.overall",
|
|
50
|
+
"volume_correlation.overall"],
|
|
51
|
+
)
|
|
52
|
+
result = client.validation.get_job(job["job_id"], plot_dir="figs")
|
|
53
|
+
result["distances"]["fm"]["spread"]["l1"]
|
|
54
|
+
result["stylised_fact_verdicts"]["heavy_tails"]["simulated"]["abm"]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`distances`, `distributions`, the verdicts and the FID/MIND scores come back
|
|
58
|
+
keyed by name; the radar draws a polygon per population, the verdict table a
|
|
59
|
+
column each, and volume correlation puts historical first then a heatmap per
|
|
60
|
+
population. `sim_files` groups the same way.
|
|
61
|
+
|
|
62
|
+
A plain list is one unnamed population and keeps the flat shape exactly as
|
|
63
|
+
before. The historical day is measured once however many groups there are.
|
|
64
|
+
|
|
65
|
+
### Your own runs beside platform runs
|
|
66
|
+
|
|
67
|
+
`sim_ids` and `sim_files` can be given together — your own model against
|
|
68
|
+
platform simulations, on one set of figures, 25 runs in total across the two:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
job = client.validation.run(
|
|
72
|
+
symbol="TSCO", date="2026-06-23", provider="bmll", exchange="lse",
|
|
73
|
+
sim_files={"fm": ["my_run.parquet"]},
|
|
74
|
+
sim_ids={"abm": abm_ids},
|
|
75
|
+
plots=["statistical.radar", "stylised_facts.overall"],
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
With both present each source is a population of its own, so a bare list gets
|
|
80
|
+
a name rather than merging into whatever else is there:
|
|
81
|
+
|
|
82
|
+
| passed | populations |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| files mapping + ids list | its names, plus `platform` |
|
|
85
|
+
| files list + ids mapping | `uploaded`, plus its names |
|
|
86
|
+
| both lists | `uploaded` and `platform` |
|
|
87
|
+
| both mappings | their own names |
|
|
88
|
+
| either one alone | exactly as before |
|
|
89
|
+
|
|
90
|
+
The uploads are measured first and the platform runs follow.
|
|
91
|
+
|
|
92
|
+
`plots` is the only plot switch: unset draws nothing, `True` draws every
|
|
93
|
+
figure the enabled areas can draw, and a list draws just those ids. An area
|
|
94
|
+
switched off draws nothing either way.
|
|
95
|
+
|
|
96
|
+
| id | figure |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `statistical.radar` | distance spider, one polygon per population |
|
|
99
|
+
| `statistical.distribution` | every metric's KDE |
|
|
100
|
+
| `statistical.distribution.{metric}` | one metric, e.g. `.spread` |
|
|
101
|
+
| `stylised_facts.overall` | the verdict table |
|
|
102
|
+
| `stylised_facts.fact` | every fact |
|
|
103
|
+
| `stylised_facts.fact.{name}` | one fact, e.g. `.heavy_tails` |
|
|
104
|
+
| `impact.response` | impact response by event type |
|
|
105
|
+
| `impact.event` | every event type |
|
|
106
|
+
| `impact.event.{type}` | one event type |
|
|
107
|
+
| `volume_correlation.levels` | level correlation heatmap |
|
|
108
|
+
| `volume_correlation.changes` | change correlation heatmap |
|
|
109
|
+
| `volume_correlation.overall` | both of the above |
|
|
110
|
+
|
|
111
|
+
Each returned figure carries the `id` it was asked for — match on that, not on
|
|
112
|
+
the filename.
|
|
113
|
+
|
|
114
|
+
### Working from a checkout
|
|
115
|
+
|
|
116
|
+
To run the SDK from source — editing it, or using unreleased changes — install
|
|
117
|
+
it editable **into the interpreter you will actually import from**. A notebook
|
|
118
|
+
kernel is frequently not the python on your `$PATH`:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# in a notebook, find the right interpreter first
|
|
122
|
+
import sys; print(sys.executable)
|
|
123
|
+
|
|
124
|
+
# then, with that path
|
|
125
|
+
<that python> -m pip install -e /path/to/pulse-sdk
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Check which copy you loaded — this is worth doing whenever an attribute seems
|
|
129
|
+
to be missing:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
import simudyne
|
|
133
|
+
from importlib.metadata import version
|
|
134
|
+
|
|
135
|
+
print(simudyne.__file__) # should be .../pulse-sdk/src/simudyne/...
|
|
136
|
+
print(version("simudyne-pulse"))
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### If an older install shadows it
|
|
140
|
+
|
|
141
|
+
The SDK was once published under the distribution name **`simudyne`**; it is
|
|
142
|
+
now **`simudyne-pulse`**. Both ship a module called `simudyne`, and the older
|
|
143
|
+
one installs a real directory while the editable install only adds a path
|
|
144
|
+
entry — so the old copy wins and you get errors like
|
|
145
|
+
`'PulseABM' object has no attribute 'validation'` from a version that predates
|
|
146
|
+
the feature. `pip list` shows both. Remove the obsolete one:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
<that python> -m pip uninstall simudyne # the old distribution
|
|
150
|
+
<that python> -m pip install -e /path/to/pulse-sdk
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Running the tests
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
uv run --with pytest --with requests --with pandas --with pyarrow \
|
|
157
|
+
python -m pytest tests/ -q
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
No network: the client's `_request` is replaced with a recorder, so the tests
|
|
161
|
+
assert the exact payload the API would receive.
|
|
162
|
+
|
|
163
|
+
## Quick start
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from simudyne import PulseABM
|
|
167
|
+
|
|
168
|
+
client = PulseABM(api_key="pk_live_...") # or set SIMUDYNE_API_KEY
|
|
169
|
+
|
|
170
|
+
# Days the agent-based model can simulate
|
|
171
|
+
cal = client.data.calibrated_data(symbol="700.HK")
|
|
172
|
+
|
|
173
|
+
# Run it, poll, read the generated book
|
|
174
|
+
job = client.simulation.run(
|
|
175
|
+
symbol="700.HK", cal_date="2025-09-02",
|
|
176
|
+
provider="omd", exchange="hkex_securities",
|
|
177
|
+
)
|
|
178
|
+
import time
|
|
179
|
+
while not client.simulation.get_job_status(job["job_id"])["is_complete"]:
|
|
180
|
+
time.sleep(20)
|
|
181
|
+
book = client.simulation.get_sim_data(job["sim_ids"][0])
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
A foundation model runs through the same call with `engine="fm"` and a
|
|
185
|
+
`model_id`, which may be the model's production name (e.g.
|
|
186
|
+
`"flow-hkex-1-100M"`; see `client.fm.models()`). Fields that belong to the
|
|
187
|
+
other engine are sent and rejected by the API with a 422 rather than dropped.
|
|
188
|
+
|
|
189
|
+
Every method has a full numpy-style docstring (`help(client.simulation.run)`);
|
|
190
|
+
the user guide is at https://pulse.simudyne.com/docs.
|
|
191
|
+
|
|
192
|
+
## API reference
|
|
193
|
+
|
|
194
|
+
### `PulseABM(api_key, base_url=None)`
|
|
195
|
+
|
|
196
|
+
| Parameter | Env variable | Default |
|
|
197
|
+
|-----------|-------------|---------|
|
|
198
|
+
| `api_key` | `SIMUDYNE_API_KEY` | required |
|
|
199
|
+
| `base_url` | `SIMUDYNE_BASE_URL` | Pulse API |
|
|
200
|
+
|
|
201
|
+
| Resource | What it covers |
|
|
202
|
+
|----------|----------------|
|
|
203
|
+
| `client.data` | `available_data` (every symbol-day that exists), `calibrated_data` (what the ABM can run), `calibrate` |
|
|
204
|
+
| `client.simulation` | `run` (both engines), `run_lrm`, `get_jobs`, `get_job_status`, `get_job_results`, `get_job_logs`, sim files and downloads |
|
|
205
|
+
| `client.validation` | `run`, `get_job_status`, `get_job`, `list_jobs` — results carry `errors` per failed area |
|
|
206
|
+
| `client.fm` | `models`, `live`; admin: `registry`, `register`, `activate`, `deactivate` |
|
|
207
|
+
| `client.fix` | FIX session usage |
|
|
208
|
+
| `client.simulator_gym` | the interactive simulator websocket |
|
|
209
|
+
|
|
210
|
+
Failures raise `PulseAPIError` with `status_code`, `detail` (exactly as the
|
|
211
|
+
API sent it) and `errors`; `str(exc)` renders validation errors as
|
|
212
|
+
`field: message` lines.
|
|
213
|
+
|
|
214
|
+
### `client.profile`
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
client.profile.get() # Account info
|
|
218
|
+
client.profile.usage() # API usage stats
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### `client.api_keys`
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
client.api_keys.list() # List active keys
|
|
225
|
+
client.api_keys.create(name="research") # Create a new key
|
|
226
|
+
client.api_keys.revoke(key_id="key_...") # Revoke a key
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Configuration
|
|
230
|
+
|
|
231
|
+
You can set your API key as an environment variable instead of passing it directly:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
export SIMUDYNE_API_KEY=pk_live_...
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
from simudyne import PulseABM
|
|
239
|
+
client = PulseABM() # picks up SIMUDYNE_API_KEY automatically
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "simudyne-pulse"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.0-dev.1"
|
|
8
8
|
description = "Python SDK for the Simudyne Pulse synthetic market data API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -33,7 +33,7 @@ test = ["pytest>=8.0.0"]
|
|
|
33
33
|
[project.urls]
|
|
34
34
|
Homepage = "https://pulse.simudyne.com"
|
|
35
35
|
Documentation = "https://pulse.simudyne.com/docs"
|
|
36
|
-
Repository = "https://github.com/simudyne/pulse-
|
|
36
|
+
Repository = "https://github.com/simudyne/pulse-sdk"
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
|
|
@@ -8,10 +8,80 @@ from tqdm import tqdm
|
|
|
8
8
|
import tempfile
|
|
9
9
|
import math
|
|
10
10
|
|
|
11
|
-
from simudyne.exceptions import PulseAPIError
|
|
11
|
+
from simudyne.exceptions import PulseAPIError, error_from_response # noqa: F401
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class PulseABM:
|
|
15
|
+
"""Client for the Simudyne Pulse synthetic market data API.
|
|
16
|
+
|
|
17
|
+
The client holds the API key and HTTP session, and exposes every endpoint
|
|
18
|
+
through a resource attribute (``client.simulation``, ``client.data``, ...).
|
|
19
|
+
Construct it once and reuse it; the underlying :class:`requests.Session`
|
|
20
|
+
keeps connections alive across calls.
|
|
21
|
+
|
|
22
|
+
Transient failures (HTTP 429, 502, 503, 504) and network timeouts are
|
|
23
|
+
retried automatically with exponential backoff. Any other non-2xx response
|
|
24
|
+
raises :class:`~simudyne.exceptions.PulseAPIError`.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
api_key : str, optional
|
|
29
|
+
Pulse API key (``pk_live_...``). Falls back to the ``SIMUDYNE_API_KEY``
|
|
30
|
+
environment variable when omitted.
|
|
31
|
+
base_url : str, optional
|
|
32
|
+
API root, with no trailing slash. Falls back to the
|
|
33
|
+
``SIMUDYNE_BASE_URL`` environment variable, then to
|
|
34
|
+
``https://pulse-api.simudyne.com``.
|
|
35
|
+
timeout : int, optional
|
|
36
|
+
Per-request timeout in seconds. Defaults to 30.
|
|
37
|
+
max_retries : int, default 3
|
|
38
|
+
Retry attempts for transient errors. Backoff is ``2 ** attempt``
|
|
39
|
+
seconds, so the default waits 1s, 2s then 4s before giving up.
|
|
40
|
+
|
|
41
|
+
Attributes
|
|
42
|
+
----------
|
|
43
|
+
profile : ProfileResource
|
|
44
|
+
Account details, usage and download history.
|
|
45
|
+
api_keys : ApiKeysResource
|
|
46
|
+
Create, list and revoke API keys.
|
|
47
|
+
data : DataResource
|
|
48
|
+
The market data catalogue: what exists, what is calibrated,
|
|
49
|
+
and calibrating a day.
|
|
50
|
+
simulation : SimulationResource
|
|
51
|
+
Submit agent-based simulations and retrieve their results.
|
|
52
|
+
validation : ValidationResource
|
|
53
|
+
Score synthetic data against real data.
|
|
54
|
+
fm : FmResource
|
|
55
|
+
Foundation-model generation and its job lifecycle.
|
|
56
|
+
fix : FixResource
|
|
57
|
+
FIX gateway usage.
|
|
58
|
+
simulator_gym : SimulatorGymResource
|
|
59
|
+
Gym-style reinforcement-learning environment over a WebSocket.
|
|
60
|
+
|
|
61
|
+
Raises
|
|
62
|
+
------
|
|
63
|
+
ValueError
|
|
64
|
+
If no API key is given and ``SIMUDYNE_API_KEY`` is unset.
|
|
65
|
+
|
|
66
|
+
Examples
|
|
67
|
+
--------
|
|
68
|
+
Key taken from the environment:
|
|
69
|
+
|
|
70
|
+
>>> import os
|
|
71
|
+
>>> os.environ["SIMUDYNE_API_KEY"] = "pk_live_..."
|
|
72
|
+
>>> client = PulseABM()
|
|
73
|
+
|
|
74
|
+
Key passed explicitly, against a non-default deployment:
|
|
75
|
+
|
|
76
|
+
>>> client = PulseABM(
|
|
77
|
+
... api_key="pk_live_...",
|
|
78
|
+
... base_url="https://pulse-api-dev.simudyne.com",
|
|
79
|
+
... timeout=60,
|
|
80
|
+
... )
|
|
81
|
+
>>> client.profile.get()["email"]
|
|
82
|
+
'you@example.com'
|
|
83
|
+
"""
|
|
84
|
+
|
|
15
85
|
DEFAULT_BASE_URL = "https://pulse-api.simudyne.com"
|
|
16
86
|
DEFAULT_TIMEOUT = 30
|
|
17
87
|
RETRYABLE_STATUS_CODES = {429, 502, 503, 504}
|
|
@@ -36,6 +106,8 @@ class PulseABM:
|
|
|
36
106
|
from simudyne.resources.profile import ProfileResource
|
|
37
107
|
from simudyne.resources.api_keys import ApiKeysResource
|
|
38
108
|
from simudyne.resources.data import DataResource
|
|
109
|
+
from simudyne.resources.fix import FixResource
|
|
110
|
+
from simudyne.resources.fm import FmResource
|
|
39
111
|
from simudyne.resources.simulation import SimulationResource
|
|
40
112
|
from simudyne.resources.simulator_gym import SimulatorGymResource
|
|
41
113
|
from simudyne.resources.validation import ValidationResource
|
|
@@ -43,6 +115,8 @@ class PulseABM:
|
|
|
43
115
|
self.profile = ProfileResource(self)
|
|
44
116
|
self.api_keys = ApiKeysResource(self)
|
|
45
117
|
self.data = DataResource(self)
|
|
118
|
+
self.fix = FixResource(self)
|
|
119
|
+
self.fm = FmResource(self)
|
|
46
120
|
self.simulation = SimulationResource(self)
|
|
47
121
|
self.simulator_gym = SimulatorGymResource(self)
|
|
48
122
|
self.validation = ValidationResource(self)
|
|
@@ -64,11 +138,7 @@ class PulseABM:
|
|
|
64
138
|
time.sleep(delay)
|
|
65
139
|
continue
|
|
66
140
|
|
|
67
|
-
|
|
68
|
-
detail = response.json().get("detail", response.text)
|
|
69
|
-
except ValueError:
|
|
70
|
-
detail = response.text
|
|
71
|
-
raise PulseAPIError(response.status_code, detail)
|
|
141
|
+
raise error_from_response(response)
|
|
72
142
|
|
|
73
143
|
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
|
|
74
144
|
last_exception = e
|
|
@@ -85,6 +155,12 @@ class PulseABM:
|
|
|
85
155
|
response = self._request_with_retries(method, url, **kwargs)
|
|
86
156
|
return response.json()
|
|
87
157
|
|
|
158
|
+
def _request_text(self, method: str, endpoint: str, **kwargs):
|
|
159
|
+
"""Like _request, for endpoints that answer text/plain not JSON."""
|
|
160
|
+
url = f"{self.base_url}{endpoint}"
|
|
161
|
+
response = self._request_with_retries(method, url, **kwargs)
|
|
162
|
+
return response.text
|
|
163
|
+
|
|
88
164
|
def _request_csv(self, method, endpoint, **kwargs):
|
|
89
165
|
PAGE_SIZE = 100000
|
|
90
166
|
params = kwargs.get("params", {})
|