simudyne-pulse 0.6.0.dev1__py3-none-any.whl

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.
@@ -0,0 +1,118 @@
1
+ import json
2
+
3
+ import websocket
4
+
5
+
6
+ class GymSession:
7
+ """A live connection to a Pulse simulator-gym environment session."""
8
+
9
+ def __init__(self, ws: websocket.WebSocket, session_id: str):
10
+ self._ws = ws
11
+ self.session_id = session_id
12
+
13
+ def reset(self, seed: int = None) -> dict:
14
+ """Reset the environment and return the initial observation.
15
+
16
+ Args:
17
+ seed: Optional random seed for reproducibility.
18
+
19
+ Returns:
20
+ dict with keys: type, obs (47 floats), reward, done, info
21
+ """
22
+ self._ws.send(json.dumps({"type": "reset", "seed": seed}))
23
+ msg = json.loads(self._ws.recv())
24
+ if msg["type"] == "error":
25
+ raise RuntimeError(f"Simulator error: {msg['message']}")
26
+ return msg
27
+
28
+ def step(self, action: int) -> dict:
29
+ """Take one step in the environment.
30
+
31
+ Args:
32
+ action: Integer action in range 0-13.
33
+
34
+ Returns:
35
+ dict with keys: type, obs (47 floats), reward, done, info
36
+ """
37
+ self._ws.send(json.dumps({"type": "step", "action": action}))
38
+ msg = json.loads(self._ws.recv())
39
+ if msg["type"] == "error":
40
+ raise RuntimeError(f"Simulator error: {msg['message']}")
41
+ return msg
42
+
43
+ def close(self):
44
+ """Close the session and the underlying WebSocket connection."""
45
+ try:
46
+ self._ws.send(json.dumps({"type": "close"}))
47
+ self._ws.recv()
48
+ finally:
49
+ self._ws.close()
50
+
51
+ def __enter__(self):
52
+ return self
53
+
54
+ def __exit__(self, *args):
55
+ self.close()
56
+
57
+
58
+ class SimulatorGymResource:
59
+ def __init__(self, client):
60
+ self._client = client
61
+
62
+ def _ws_base_url(self) -> str:
63
+ url = self._client.base_url
64
+ if url.startswith("https://"):
65
+ return "wss://" + url[len("https://"):]
66
+ if url.startswith("http://"):
67
+ return "ws://" + url[len("http://"):]
68
+ return url
69
+
70
+ def connect(self, symbol: str, cal_date: str, exchange: str) -> GymSession:
71
+ """Open a new simulator-gym session.
72
+
73
+ Args:
74
+ symbol: Trading symbol, e.g. "700.HK"
75
+ cal_date: Calibration date, e.g. "2025-09-02"
76
+ exchange: Exchange identifier, e.g. "HKEX.Securities"
77
+
78
+ Returns:
79
+ GymSession — use as a context manager:
80
+
81
+ with client.simulator_gym.connect("700.HK", "2025-09-02", "HKEX.Securities") as env:
82
+ obs = env.reset(seed=42)
83
+ result = env.step(0)
84
+ """
85
+ ws_url = f"{self._ws_base_url()}/ws/simulator-gym"
86
+ try:
87
+ ws = websocket.create_connection(
88
+ ws_url,
89
+ header={"X-API-Key": self._client.api_key},
90
+ )
91
+ except websocket.WebSocketBadStatusException as e:
92
+ if "403" in str(e):
93
+ raise RuntimeError(
94
+ "Authentication failed: invalid or expired API key. "
95
+ "Check your API key with client.api_keys.list()."
96
+ ) from e
97
+ raise RuntimeError(f"Failed to connect to simulator-gym: {e}") from e
98
+ except Exception as e:
99
+ raise RuntimeError(f"Failed to connect to simulator-gym: {e}") from e
100
+
101
+ ws.send(json.dumps({
102
+ "type": "create",
103
+ "config": {
104
+ "symbol": symbol,
105
+ "cal_date": cal_date,
106
+ "exchange": exchange,
107
+ }
108
+ }))
109
+
110
+ msg = json.loads(ws.recv())
111
+ if msg["type"] == "error":
112
+ ws.close()
113
+ raise RuntimeError(f"Failed to create session: {msg['message']}")
114
+ if msg["type"] != "created":
115
+ ws.close()
116
+ raise RuntimeError(f"Unexpected response type '{msg['type']}' during session creation")
117
+
118
+ return GymSession(ws, session_id=msg["session_id"])
@@ -0,0 +1,234 @@
1
+ """
2
+ Validation Resource for the Pulse SDK.
3
+
4
+ This module provides methods for validating simulation quality by comparing
5
+ simulated LOB data against historical data using distributional metrics,
6
+ impact response analysis, and FID scores.
7
+
8
+ Workflow:
9
+ 1. Submit a validation job with run() -> returns job_id
10
+ 2. Poll status with get_job(job_id) or use run_pipeline() for blocking
11
+ 3. View results including distances and plots
12
+ 4. List past jobs with list_jobs()
13
+ """
14
+
15
+ import time
16
+ import base64
17
+
18
+
19
+ RUN_PATH = "/validation/run"
20
+ JOBS_PATH = "/validation/jobs"
21
+
22
+
23
+ class ValidationResource:
24
+ def __init__(self, client):
25
+ self._client = client
26
+
27
+ def run(
28
+ self,
29
+ symbol: str,
30
+ date: str,
31
+ sim_ids: list[str],
32
+ ticksize: float = 1.0,
33
+ run_metrics: bool = True,
34
+ run_impact: bool = False,
35
+ run_fid: bool = False,
36
+ n_levels: int = 10,
37
+ rescale_volumes: bool = True,
38
+ lot_size: int = 1,
39
+ ) -> dict:
40
+ """Submit a validation job.
41
+
42
+ Compares simulation output against historical market data using
43
+ distributional distance metrics (L1, Wasserstein), impact response
44
+ curves, and FID scores.
45
+
46
+ Historical data is fetched automatically from GCS based on symbol and date.
47
+ Simulation data is fetched from each sim_id's sim_data.parquet in GCS.
48
+
49
+ Args:
50
+ symbol: Trading symbol (e.g. "700.HK")
51
+ date: Calibration date in YYYY-MM-DD format (e.g. "2025-09-01")
52
+ sim_ids: List of simulation IDs to validate (max 25)
53
+ ticksize: Tick size for the symbol
54
+ run_metrics: Compute L1/Wasserstein distributional distances
55
+ run_impact: Compute impact response curves
56
+ run_fid: Compute Frechet Inception Distance
57
+ n_levels: Number of L2 book levels to use
58
+ rescale_volumes: Multiply simulated L2 size columns by lot_size
59
+ lot_size: Lot size multiplier for volume rescaling
60
+
61
+ Returns:
62
+ dict with job_id, status, message
63
+ """
64
+ payload = {
65
+ "symbol": symbol,
66
+ "date": date,
67
+ "sim_ids": sim_ids,
68
+ "ticksize": ticksize,
69
+ "config": {
70
+ "run_metrics": run_metrics,
71
+ "run_impact": run_impact,
72
+ "run_fid": run_fid,
73
+ "n_levels": n_levels,
74
+ "rescale_volumes": rescale_volumes,
75
+ "lot_size": lot_size,
76
+ },
77
+ }
78
+ return self._client._request("POST", RUN_PATH, json=payload)
79
+
80
+ def get_job(self, job_id: str) -> dict:
81
+ """Get validation job status and results.
82
+
83
+ Args:
84
+ job_id: The job ID returned by run()
85
+
86
+ Returns:
87
+ dict with:
88
+ - status: "pending", "running", "completed", or "failed"
89
+ - distances: dict of {metric: {l1: [...], w: [...]}} (when completed)
90
+ - fid_scores: list of floats (when completed and run_fid=True)
91
+ - plots: list of {name, content_base64} (when completed)
92
+ - metadata: dict with run parameters
93
+ - error: error message (when failed)
94
+ """
95
+ return self._client._request("GET", f"{JOBS_PATH}/{job_id}")
96
+
97
+ def list_jobs(self, limit: int = 50) -> dict:
98
+ """List validation jobs for the current user.
99
+
100
+ Args:
101
+ limit: Max number of jobs to return (default 50, max 200)
102
+
103
+ Returns:
104
+ dict with jobs list and total count
105
+ """
106
+ return self._client._request("GET", JOBS_PATH, params={"limit": limit})
107
+
108
+ def run_pipeline(
109
+ self,
110
+ symbol: str,
111
+ date: str,
112
+ sim_ids: list[str],
113
+ ticksize: float = 1.0,
114
+ run_metrics: bool = True,
115
+ run_impact: bool = False,
116
+ run_fid: bool = False,
117
+ n_levels: int = 10,
118
+ rescale_volumes: bool = True,
119
+ lot_size: int = 1,
120
+ poll_interval: float = 3.0,
121
+ timeout: float = 600.0,
122
+ ) -> dict:
123
+ """Submit a validation job and block until it completes.
124
+
125
+ Combines run() + polling get_job() into a single call.
126
+ Prints progress to stderr.
127
+
128
+ Args:
129
+ symbol: Trading symbol (e.g. "700.HK")
130
+ date: Calibration date in YYYY-MM-DD format
131
+ sim_ids: List of simulation IDs to validate (max 25)
132
+ ticksize: Tick size for the symbol
133
+ run_metrics: Compute L1/Wasserstein distributional distances
134
+ run_impact: Compute impact response curves
135
+ run_fid: Compute Frechet Inception Distance
136
+ n_levels: Number of L2 book levels to use
137
+ rescale_volumes: Multiply simulated L2 size columns by lot_size
138
+ lot_size: Lot size multiplier for volume rescaling
139
+ poll_interval: Seconds between status checks (default 3)
140
+ timeout: Max seconds to wait (default 600)
141
+
142
+ Returns:
143
+ dict with full validation results (distances, plots, metadata)
144
+
145
+ Raises:
146
+ RuntimeError: If the validation job fails
147
+ TimeoutError: If the job doesn't complete within timeout
148
+ """
149
+ import sys
150
+
151
+ job = self.run(
152
+ symbol=symbol,
153
+ date=date,
154
+ sim_ids=sim_ids,
155
+ ticksize=ticksize,
156
+ run_metrics=run_metrics,
157
+ run_impact=run_impact,
158
+ run_fid=run_fid,
159
+ n_levels=n_levels,
160
+ rescale_volumes=rescale_volumes,
161
+ lot_size=lot_size,
162
+ )
163
+ job_id = job["job_id"]
164
+ print(f"Validation job submitted: {job_id}", file=sys.stderr)
165
+
166
+ start = time.time()
167
+ while True:
168
+ result = self.get_job(job_id)
169
+ status = result["status"]
170
+
171
+ if status == "completed":
172
+ elapsed = time.time() - start
173
+ print(f"Completed in {elapsed:.1f}s", file=sys.stderr)
174
+ return result
175
+ elif status == "failed":
176
+ raise RuntimeError(f"Validation failed: {result.get('error')}")
177
+
178
+ if time.time() - start > timeout:
179
+ raise TimeoutError(f"Validation job {job_id} timed out after {timeout}s")
180
+
181
+ time.sleep(poll_interval)
182
+
183
+ def display_plots(self, result: dict) -> "PlotDisplay":
184
+ """Return a PlotDisplay object for displaying validation plots.
185
+
186
+ Usage:
187
+ plots = client.validation.display_plots(result)
188
+ plots.distributions() # show distribution histograms
189
+ plots.distances() # show spider plots
190
+ plots.impact_response() # show impact response plots
191
+
192
+ Args:
193
+ result: The result dict from run_pipeline() or get_job()
194
+ """
195
+ return PlotDisplay(result)
196
+
197
+
198
+ class PlotDisplay:
199
+ """Displays categorized validation plots inline in Jupyter notebooks."""
200
+
201
+ def __init__(self, result: dict):
202
+ plots = result.get("plots") or {}
203
+ self._distributions = plots.get("distributions", [])
204
+ self._distances = plots.get("distances", [])
205
+ self._impact_response = plots.get("impact_response", [])
206
+
207
+ def _show(self, plot_list, title):
208
+ from IPython.display import display, Image
209
+
210
+ if not plot_list:
211
+ print(f"No {title} plots available")
212
+ return
213
+
214
+ for plot in plot_list:
215
+ print(f"\n--- {plot['name']} ---")
216
+ display(Image(data=base64.b64decode(plot["content_base64"])))
217
+
218
+ def distributions(self):
219
+ """Display distribution histogram plots."""
220
+ self._show(self._distributions, "distribution")
221
+
222
+ def distances(self):
223
+ """Display spider plots (L1 and Wasserstein distances)."""
224
+ self._show(self._distances, "distance")
225
+
226
+ def impact_response(self):
227
+ """Display impact response plots."""
228
+ self._show(self._impact_response, "impact response")
229
+
230
+ def all(self):
231
+ """Display all plots."""
232
+ self.distances()
233
+ self.distributions()
234
+ self.impact_response()
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: simudyne-pulse
3
+ Version: 0.6.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-api
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
35
+ ```
36
+
37
+ The distribution is named `simudyne-pulse`; the import name is `simudyne`:
38
+
39
+ ```python
40
+ from simudyne import PulseABM
41
+ ```
42
+
43
+ Requires Python 3.10+.
44
+
45
+ ### Development builds
46
+
47
+ The `dev` branch is a prerelease channel. Pushes to it publish prerelease
48
+ versions (e.g. `0.6.0.dev1`) that are separate from the stable versions cut on
49
+ `prod`. `pip install simudyne-pulse` always resolves to the latest **stable**
50
+ release and ignores prereleases, so dev builds can never affect a normal
51
+ install.
52
+
53
+ To install the latest dev build, opt in with `--pre`:
54
+
55
+ ```bash
56
+ pip install --pre simudyne-pulse
57
+ ```
58
+
59
+ Only use dev builds for testing unreleased changes; they are not guaranteed
60
+ stable. Merge `dev` into `main` to promote those changes to a stable release.
61
+
62
+ ## Quick start
63
+
64
+ ```python
65
+ from simudyne import PulseABM
66
+
67
+ client = PulseABM(api_key="pk_live_...")
68
+
69
+ # List available exchanges, symbols, and dates
70
+ symbols = client.data.get_symbols(year=2024)
71
+ print(symbols)
72
+
73
+ # Fetch L2 order book data
74
+ df = client.data.get_L2("HKEX", "HSIJ4", "2024-04-02T09:15:00", "2024-04-02T09:16:00")
75
+ print(df.head())
76
+ ```
77
+
78
+ ## API reference
79
+
80
+ ### `PulseABM(api_key, base_url=None)`
81
+
82
+ | Parameter | Env variable | Default |
83
+ |-----------|-------------|---------|
84
+ | `api_key` | `SIMUDYNE_API_KEY` | required |
85
+ | `base_url` | `SIMUDYNE_BASE_URL` | Pulse API |
86
+
87
+ ### `client.data`
88
+
89
+ All data methods return Polars DataFrames. Large result sets are automatically paginated.
90
+
91
+ ```python
92
+ # Available exchanges, symbols, and dates
93
+ client.data.get_symbols(year=2024)
94
+
95
+ # L1: top of book (best bid/ask)
96
+ client.data.get_L1("HKEX", "HSIJ4", "2024-04-02T09:15:00", "2024-04-02T10:00:00")
97
+
98
+ # L2: full order book (all levels)
99
+ client.data.get_L2("HKEX", "HSIJ4", "2024-04-02T09:15:00", "2024-04-02T09:16:00")
100
+
101
+ # Orders: individual order events
102
+ client.data.get_orders("HKEX", "HSIJ4", "2024-04-02T09:15:00", "2024-04-02T10:00:00")
103
+
104
+ # Trades: executed trades
105
+ client.data.get_trades("HKEX", "HSIJ4", "2024-04-02T09:15:00", "2024-04-02T10:00:00")
106
+ ```
107
+
108
+ **Parameters** (same for all data methods):
109
+
110
+ | Parameter | Type | Description |
111
+ |-----------|------|-------------|
112
+ | `exchange` | str | Exchange code (e.g. `HKEX`) |
113
+ | `sym` | str | Symbol name (e.g. `HSIJ4`) |
114
+ | `datetime_start` | str | Start time, ISO 8601 (e.g. `2024-04-02T09:15:00`) |
115
+ | `datetime_end` | str | End time, ISO 8601 |
116
+
117
+ ### `client.profile`
118
+
119
+ ```python
120
+ client.profile.get() # Account info
121
+ client.profile.usage() # API usage stats
122
+ ```
123
+
124
+ ### `client.api_keys`
125
+
126
+ ```python
127
+ client.api_keys.list() # List active keys
128
+ client.api_keys.create(name="research") # Create a new key
129
+ client.api_keys.revoke(key_id="key_...") # Revoke a key
130
+ ```
131
+
132
+ ## Configuration
133
+
134
+ You can set your API key as an environment variable instead of passing it directly:
135
+
136
+ ```bash
137
+ export SIMUDYNE_API_KEY=pk_live_...
138
+ ```
139
+
140
+ ```python
141
+ from simudyne import PulseABM
142
+ client = PulseABM() # picks up SIMUDYNE_API_KEY automatically
143
+ ```
144
+
145
+ ## License
146
+
147
+ MIT
@@ -0,0 +1,16 @@
1
+ simudyne/__init__.py,sha256=m4FqChvvnm2ESEG1imAqGj7u-ksFxY6gPgRlyHFlL1c,92
2
+ simudyne/client.py,sha256=7EHrhp1sdJVPn1EEmuqQnN-i82__gZ9CZ64cs5Cp_3U,4606
3
+ simudyne/exceptions.py,sha256=ZfMiN4TTIMvqQ4EVfJ_4aNe3OvtZlzpWSyvfkuE5neo,270
4
+ simudyne/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ simudyne/resources/api_keys.py,sha256=Pu2srG6ymsf0Ax1VbeA3X0Ofs0pq8j6b42iQ81T0iUE,393
6
+ simudyne/resources/data.py,sha256=_KH40tvLuMESJuH4t7ign7FKr2WcuObLRAF2bPpZbgg,241
7
+ simudyne/resources/historical.py,sha256=-PkcCd7KRZyO2uG3GMpWgZjwmnmZL42-_4I_mJSbrww,76
8
+ simudyne/resources/profile.py,sha256=XYMdRoF1s2NXvHdIBay0M3gkeuKf7P7YSK1XffoE1JM,1028
9
+ simudyne/resources/simulation.py,sha256=xRc9PBmrh4OE5qh8pzs9WebOw2Utfx8uaMYOJ-IeNIg,28000
10
+ simudyne/resources/simulator_gym.py,sha256=MWo7xv5fVFYpTAKU3sJVyHoDZvA8P0dDgSxgCindCMk,3873
11
+ simudyne/resources/validation.py,sha256=V8-IFEKa8s0mlqHzT1kQ1j_OKKLFcM9RGx53p7a65uQ,8025
12
+ simudyne_pulse-0.6.0.dev1.dist-info/licenses/LICENSE,sha256=H_hgosvz8TIG6NAgsaluNzCBOe3lHZddEWwyJM9PTWs,1069
13
+ simudyne_pulse-0.6.0.dev1.dist-info/METADATA,sha256=fCIMn2TD-pvUseI2f3kII3qBnJLRxtppUiGRSFTt344,4141
14
+ simudyne_pulse-0.6.0.dev1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
15
+ simudyne_pulse-0.6.0.dev1.dist-info/top_level.txt,sha256=PT1iWu7CFbICTaYJ7vZSKdup-YPFtHV5j2y5LPe6UP0,9
16
+ simudyne_pulse-0.6.0.dev1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Simudyne Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ simudyne