simudyne-pulse 0.7.3__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.3 → simudyne_pulse-0.8.0.dev1}/pyproject.toml +2 -2
- {simudyne_pulse-0.7.3 → simudyne_pulse-0.8.0.dev1}/src/simudyne/client.py +21 -10
- simudyne_pulse-0.8.0.dev1/src/simudyne/exceptions.py +96 -0
- {simudyne_pulse-0.7.3 → simudyne_pulse-0.8.0.dev1}/src/simudyne/resources/api_keys.py +25 -10
- 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.3 → simudyne_pulse-0.8.0.dev1}/src/simudyne/resources/profile.py +17 -0
- {simudyne_pulse-0.7.3 → simudyne_pulse-0.8.0.dev1}/src/simudyne/resources/simulation.py +460 -271
- {simudyne_pulse-0.7.3 → simudyne_pulse-0.8.0.dev1}/src/simudyne/resources/simulator_gym.py +20 -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.3 → 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.3/PKG-INFO +0 -147
- simudyne_pulse-0.7.3/README.md +0 -121
- simudyne_pulse-0.7.3/src/simudyne/exceptions.py +0 -44
- simudyne_pulse-0.7.3/src/simudyne/resources/data.py +0 -106
- simudyne_pulse-0.7.3/src/simudyne/resources/historical.py +0 -1
- simudyne_pulse-0.7.3/src/simudyne/resources/validation.py +0 -299
- simudyne_pulse-0.7.3/src/simudyne_pulse.egg-info/PKG-INFO +0 -147
- {simudyne_pulse-0.7.3 → simudyne_pulse-0.8.0.dev1}/LICENSE +0 -0
- {simudyne_pulse-0.7.3 → simudyne_pulse-0.8.0.dev1}/setup.cfg +0 -0
- {simudyne_pulse-0.7.3 → simudyne_pulse-0.8.0.dev1}/src/simudyne/__init__.py +0 -0
- {simudyne_pulse-0.7.3 → simudyne_pulse-0.8.0.dev1}/src/simudyne/resources/__init__.py +0 -0
- {simudyne_pulse-0.7.3 → simudyne_pulse-0.8.0.dev1}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
- {simudyne_pulse-0.7.3 → simudyne_pulse-0.8.0.dev1}/src/simudyne_pulse.egg-info/requires.txt +0 -0
- {simudyne_pulse-0.7.3 → simudyne_pulse-0.8.0.dev1}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
- {simudyne_pulse-0.7.3 → 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,7 +8,7 @@ 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:
|
|
@@ -45,13 +45,18 @@ class PulseABM:
|
|
|
45
45
|
api_keys : ApiKeysResource
|
|
46
46
|
Create, list and revoke API keys.
|
|
47
47
|
data : DataResource
|
|
48
|
-
The
|
|
48
|
+
The market data catalogue: what exists, what is calibrated,
|
|
49
|
+
and calibrating a day.
|
|
49
50
|
simulation : SimulationResource
|
|
50
51
|
Submit agent-based simulations and retrieve their results.
|
|
51
|
-
simulator_gym : SimulatorGymResource
|
|
52
|
-
Gym-style reinforcement-learning environment over a WebSocket.
|
|
53
52
|
validation : ValidationResource
|
|
54
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.
|
|
55
60
|
|
|
56
61
|
Raises
|
|
57
62
|
------
|
|
@@ -73,7 +78,7 @@ class PulseABM:
|
|
|
73
78
|
... base_url="https://pulse-api-dev.simudyne.com",
|
|
74
79
|
... timeout=60,
|
|
75
80
|
... )
|
|
76
|
-
>>>
|
|
81
|
+
>>> client.profile.get()["email"]
|
|
77
82
|
'you@example.com'
|
|
78
83
|
"""
|
|
79
84
|
|
|
@@ -101,6 +106,8 @@ class PulseABM:
|
|
|
101
106
|
from simudyne.resources.profile import ProfileResource
|
|
102
107
|
from simudyne.resources.api_keys import ApiKeysResource
|
|
103
108
|
from simudyne.resources.data import DataResource
|
|
109
|
+
from simudyne.resources.fix import FixResource
|
|
110
|
+
from simudyne.resources.fm import FmResource
|
|
104
111
|
from simudyne.resources.simulation import SimulationResource
|
|
105
112
|
from simudyne.resources.simulator_gym import SimulatorGymResource
|
|
106
113
|
from simudyne.resources.validation import ValidationResource
|
|
@@ -108,6 +115,8 @@ class PulseABM:
|
|
|
108
115
|
self.profile = ProfileResource(self)
|
|
109
116
|
self.api_keys = ApiKeysResource(self)
|
|
110
117
|
self.data = DataResource(self)
|
|
118
|
+
self.fix = FixResource(self)
|
|
119
|
+
self.fm = FmResource(self)
|
|
111
120
|
self.simulation = SimulationResource(self)
|
|
112
121
|
self.simulator_gym = SimulatorGymResource(self)
|
|
113
122
|
self.validation = ValidationResource(self)
|
|
@@ -129,11 +138,7 @@ class PulseABM:
|
|
|
129
138
|
time.sleep(delay)
|
|
130
139
|
continue
|
|
131
140
|
|
|
132
|
-
|
|
133
|
-
detail = response.json().get("detail", response.text)
|
|
134
|
-
except ValueError:
|
|
135
|
-
detail = response.text
|
|
136
|
-
raise PulseAPIError(response.status_code, detail)
|
|
141
|
+
raise error_from_response(response)
|
|
137
142
|
|
|
138
143
|
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
|
|
139
144
|
last_exception = e
|
|
@@ -150,6 +155,12 @@ class PulseABM:
|
|
|
150
155
|
response = self._request_with_retries(method, url, **kwargs)
|
|
151
156
|
return response.json()
|
|
152
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
|
+
|
|
153
164
|
def _request_csv(self, method, endpoint, **kwargs):
|
|
154
165
|
PAGE_SIZE = 100000
|
|
155
166
|
params = kwargs.get("params", {})
|