simudyne-pulse 0.8.0.dev1__tar.gz → 0.8.0.dev3__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 → simudyne_pulse-0.8.0.dev3}/PKG-INFO +1 -3
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/README.md +0 -1
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/pyproject.toml +1 -2
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne/client.py +0 -4
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne/resources/fm.py +2 -2
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne/resources/simulation.py +4 -2
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne_pulse.egg-info/PKG-INFO +1 -3
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne_pulse.egg-info/SOURCES.txt +0 -2
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne_pulse.egg-info/requires.txt +0 -1
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/tests/test_api_contract.py +2 -2
- simudyne_pulse-0.8.0.dev1/src/simudyne/resources/simulator_gym.py +0 -304
- simudyne_pulse-0.8.0.dev1/tests/test_simulator_gym.py +0 -172
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/LICENSE +0 -0
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/setup.cfg +0 -0
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne/__init__.py +0 -0
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne/exceptions.py +0 -0
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne/resources/__init__.py +0 -0
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne/resources/api_keys.py +0 -0
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne/resources/data.py +0 -0
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne/resources/fix.py +0 -0
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne/resources/profile.py +0 -0
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne/resources/validation.py +0 -0
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/tests/test_new_resources.py +0 -0
- {simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/tests/test_validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simudyne-pulse
|
|
3
|
-
Version: 0.8.0.
|
|
3
|
+
Version: 0.8.0.dev3
|
|
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
|
|
@@ -19,7 +19,6 @@ License-File: LICENSE
|
|
|
19
19
|
Requires-Dist: requests>=2.28.0
|
|
20
20
|
Requires-Dist: polars>=0.20.0
|
|
21
21
|
Requires-Dist: tqdm>=4.60.0
|
|
22
|
-
Requires-Dist: websocket-client>=1.0.0
|
|
23
22
|
Provides-Extra: test
|
|
24
23
|
Requires-Dist: pytest>=8.0.0; extra == "test"
|
|
25
24
|
Dynamic: license-file
|
|
@@ -231,7 +230,6 @@ the user guide is at https://pulse.simudyne.com/docs.
|
|
|
231
230
|
| `client.validation` | `run`, `get_job_status`, `get_job`, `list_jobs` — results carry `errors` per failed area |
|
|
232
231
|
| `client.fm` | `models`, `live`; admin: `registry`, `register`, `activate`, `deactivate` |
|
|
233
232
|
| `client.fix` | FIX session usage |
|
|
234
|
-
| `client.simulator_gym` | the interactive simulator websocket |
|
|
235
233
|
|
|
236
234
|
Failures raise `PulseAPIError` with `status_code`, `detail` (exactly as the
|
|
237
235
|
API sent it) and `errors`; `str(exc)` renders validation errors as
|
|
@@ -205,7 +205,6 @@ the user guide is at https://pulse.simudyne.com/docs.
|
|
|
205
205
|
| `client.validation` | `run`, `get_job_status`, `get_job`, `list_jobs` — results carry `errors` per failed area |
|
|
206
206
|
| `client.fm` | `models`, `live`; admin: `registry`, `register`, `activate`, `deactivate` |
|
|
207
207
|
| `client.fix` | FIX session usage |
|
|
208
|
-
| `client.simulator_gym` | the interactive simulator websocket |
|
|
209
208
|
|
|
210
209
|
Failures raise `PulseAPIError` with `status_code`, `detail` (exactly as the
|
|
211
210
|
API sent it) and `errors`; `str(exc)` renders validation errors as
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "simudyne-pulse"
|
|
7
|
-
version = "0.8.0-dev.
|
|
7
|
+
version = "0.8.0-dev.3"
|
|
8
8
|
description = "Python SDK for the Simudyne Pulse synthetic market data API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -24,7 +24,6 @@ dependencies = [
|
|
|
24
24
|
"requests>=2.28.0",
|
|
25
25
|
"polars>=0.20.0",
|
|
26
26
|
"tqdm>=4.60.0",
|
|
27
|
-
"websocket-client>=1.0.0",
|
|
28
27
|
]
|
|
29
28
|
|
|
30
29
|
[project.optional-dependencies]
|
|
@@ -55,8 +55,6 @@ class PulseABM:
|
|
|
55
55
|
Foundation-model generation and its job lifecycle.
|
|
56
56
|
fix : FixResource
|
|
57
57
|
FIX gateway usage.
|
|
58
|
-
simulator_gym : SimulatorGymResource
|
|
59
|
-
Gym-style reinforcement-learning environment over a WebSocket.
|
|
60
58
|
|
|
61
59
|
Raises
|
|
62
60
|
------
|
|
@@ -109,7 +107,6 @@ class PulseABM:
|
|
|
109
107
|
from simudyne.resources.fix import FixResource
|
|
110
108
|
from simudyne.resources.fm import FmResource
|
|
111
109
|
from simudyne.resources.simulation import SimulationResource
|
|
112
|
-
from simudyne.resources.simulator_gym import SimulatorGymResource
|
|
113
110
|
from simudyne.resources.validation import ValidationResource
|
|
114
111
|
|
|
115
112
|
self.profile = ProfileResource(self)
|
|
@@ -118,7 +115,6 @@ class PulseABM:
|
|
|
118
115
|
self.fix = FixResource(self)
|
|
119
116
|
self.fm = FmResource(self)
|
|
120
117
|
self.simulation = SimulationResource(self)
|
|
121
|
-
self.simulator_gym = SimulatorGymResource(self)
|
|
122
118
|
self.validation = ValidationResource(self)
|
|
123
119
|
|
|
124
120
|
def _request_with_retries(self, method: str, url: str, **kwargs):
|
|
@@ -118,8 +118,8 @@ class FmResource:
|
|
|
118
118
|
-------
|
|
119
119
|
dict
|
|
120
120
|
``{env, models}``: the environment, and the latest row per model
|
|
121
|
-
with the fields :meth:`models` returns plus ``
|
|
122
|
-
|
|
121
|
+
with the fields :meth:`models` returns plus ``active`` and
|
|
122
|
+
``updated_by``. The container image is not returned.
|
|
123
123
|
|
|
124
124
|
Raises
|
|
125
125
|
------
|
{simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne/resources/simulation.py
RENAMED
|
@@ -211,8 +211,10 @@ class SimulationResource:
|
|
|
211
211
|
ms, s, min, m or h, e.g. "500ms", "5s", "1min"
|
|
212
212
|
- start_time (str): when to begin, "HH:MM" or "HH:MM:SS", e.g.
|
|
213
213
|
"10:30:00"
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
|
|
215
|
+
There is no side: each scenario sets its own direction
|
|
216
|
+
(flash_crash, gradual_selloff and trending_down sell; buy_panic
|
|
217
|
+
and trending_up buy).
|
|
216
218
|
exec_algos : list of dict, optional
|
|
217
219
|
Execution algorithms, same shape and meaning on both engines. Each
|
|
218
220
|
entry needs "type":
|
{simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/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.8.0.
|
|
3
|
+
Version: 0.8.0.dev3
|
|
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
|
|
@@ -19,7 +19,6 @@ License-File: LICENSE
|
|
|
19
19
|
Requires-Dist: requests>=2.28.0
|
|
20
20
|
Requires-Dist: polars>=0.20.0
|
|
21
21
|
Requires-Dist: tqdm>=4.60.0
|
|
22
|
-
Requires-Dist: websocket-client>=1.0.0
|
|
23
22
|
Provides-Extra: test
|
|
24
23
|
Requires-Dist: pytest>=8.0.0; extra == "test"
|
|
25
24
|
Dynamic: license-file
|
|
@@ -231,7 +230,6 @@ the user guide is at https://pulse.simudyne.com/docs.
|
|
|
231
230
|
| `client.validation` | `run`, `get_job_status`, `get_job`, `list_jobs` — results carry `errors` per failed area |
|
|
232
231
|
| `client.fm` | `models`, `live`; admin: `registry`, `register`, `activate`, `deactivate` |
|
|
233
232
|
| `client.fix` | FIX session usage |
|
|
234
|
-
| `client.simulator_gym` | the interactive simulator websocket |
|
|
235
233
|
|
|
236
234
|
Failures raise `PulseAPIError` with `status_code`, `detail` (exactly as the
|
|
237
235
|
API sent it) and `errors`; `str(exc)` renders validation errors as
|
{simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne_pulse.egg-info/SOURCES.txt
RENAMED
|
@@ -11,7 +11,6 @@ src/simudyne/resources/fix.py
|
|
|
11
11
|
src/simudyne/resources/fm.py
|
|
12
12
|
src/simudyne/resources/profile.py
|
|
13
13
|
src/simudyne/resources/simulation.py
|
|
14
|
-
src/simudyne/resources/simulator_gym.py
|
|
15
14
|
src/simudyne/resources/validation.py
|
|
16
15
|
src/simudyne_pulse.egg-info/PKG-INFO
|
|
17
16
|
src/simudyne_pulse.egg-info/SOURCES.txt
|
|
@@ -20,5 +19,4 @@ src/simudyne_pulse.egg-info/requires.txt
|
|
|
20
19
|
src/simudyne_pulse.egg-info/top_level.txt
|
|
21
20
|
tests/test_api_contract.py
|
|
22
21
|
tests/test_new_resources.py
|
|
23
|
-
tests/test_simulator_gym.py
|
|
24
22
|
tests/test_validation.py
|
|
@@ -61,11 +61,11 @@ class TestRunSendsWhatTheCallerSet:
|
|
|
61
61
|
def test_scenario_on_fm_run_is_sent_for_the_api_to_reject(self):
|
|
62
62
|
client = FakeClient()
|
|
63
63
|
SimulationResource(client).run(
|
|
64
|
-
engine="fm", model_id="m", scenario="flash_crash", scenario_params={"
|
|
64
|
+
engine="fm", model_id="m", scenario="flash_crash", scenario_params={"impact_multiplier": 2.0}
|
|
65
65
|
)
|
|
66
66
|
body = _sent(client)
|
|
67
67
|
assert body["scenario"] == "flash_crash"
|
|
68
|
-
assert body["scenario_params"] == {"
|
|
68
|
+
assert body["scenario_params"] == {"impact_multiplier": 2.0}
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
class TestLrm:
|
|
@@ -1,304 +0,0 @@
|
|
|
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
|
-
Returned by
|
|
10
|
-
:meth:`SimulatorGymResource.connect` — not constructed directly. Unlike
|
|
11
|
-
every other object in this SDK the session is stateful and long-lived: it
|
|
12
|
-
holds a WebSocket open and is stepped, in the style of a Gymnasium
|
|
13
|
-
environment.
|
|
14
|
-
|
|
15
|
-
Use it as a context manager so the session is closed even if stepping
|
|
16
|
-
raises; otherwise call :meth:`close` yourself.
|
|
17
|
-
|
|
18
|
-
Parameters
|
|
19
|
-
----------
|
|
20
|
-
ws : websocket.WebSocket
|
|
21
|
-
The open socket to the gym service.
|
|
22
|
-
session_id : str
|
|
23
|
-
Server-assigned id for this session.
|
|
24
|
-
|
|
25
|
-
Attributes
|
|
26
|
-
----------
|
|
27
|
-
session_id : str
|
|
28
|
-
Server-assigned id for this session.
|
|
29
|
-
|
|
30
|
-
Examples
|
|
31
|
-
--------
|
|
32
|
-
>>> with client.simulator_gym.connect(
|
|
33
|
-
... symbol="700.HK", cal_date="2025-09-02", exchange="HKEX.Securities"
|
|
34
|
-
... ) as env:
|
|
35
|
-
... obs = env.reset(seed=42)
|
|
36
|
-
... while True:
|
|
37
|
-
... result = env.step(0)
|
|
38
|
-
... if result["done"]:
|
|
39
|
-
... break
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
def __init__(self, ws: websocket.WebSocket, session_id: str):
|
|
43
|
-
self._ws = ws
|
|
44
|
-
self.session_id = session_id
|
|
45
|
-
|
|
46
|
-
def reset(self, seed: int = None) -> dict:
|
|
47
|
-
"""Reset the environment and return the initial observation.
|
|
48
|
-
|
|
49
|
-
Parameters
|
|
50
|
-
----------
|
|
51
|
-
seed : int, optional
|
|
52
|
-
Optional random seed for reproducibility.
|
|
53
|
-
|
|
54
|
-
Returns
|
|
55
|
-
-------
|
|
56
|
-
dict
|
|
57
|
-
Initial step result containing:
|
|
58
|
-
|
|
59
|
-
- type (str): the message type, "step"
|
|
60
|
-
- obs (list of float): the observation, 47 floats
|
|
61
|
-
- reward (float): always 0.0 for a reset
|
|
62
|
-
- done (bool): always False for a reset
|
|
63
|
-
- info (dict): per-step diagnostics from the simulator
|
|
64
|
-
|
|
65
|
-
Raises
|
|
66
|
-
------
|
|
67
|
-
RuntimeError
|
|
68
|
-
If the simulator replies with an error. Note this resource raises
|
|
69
|
-
``RuntimeError`` rather than ``PulseAPIError`` — it speaks
|
|
70
|
-
WebSocket, not HTTP.
|
|
71
|
-
|
|
72
|
-
See Also
|
|
73
|
-
--------
|
|
74
|
-
GymSession.step : Advance the episode.
|
|
75
|
-
GymSession.close : End the session.
|
|
76
|
-
|
|
77
|
-
Examples
|
|
78
|
-
--------
|
|
79
|
-
>>> obs = env.reset(seed=42)
|
|
80
|
-
>>> len(obs["obs"])
|
|
81
|
-
47
|
|
82
|
-
|
|
83
|
-
Reset without a seed for a fresh random episode:
|
|
84
|
-
|
|
85
|
-
>>> obs = env.reset()
|
|
86
|
-
"""
|
|
87
|
-
self._ws.send(json.dumps({"type": "reset", "seed": seed}))
|
|
88
|
-
msg = json.loads(self._ws.recv())
|
|
89
|
-
if msg["type"] == "error":
|
|
90
|
-
raise RuntimeError(f"Simulator error: {msg['message']}")
|
|
91
|
-
return msg
|
|
92
|
-
|
|
93
|
-
def step(self, action: int) -> dict:
|
|
94
|
-
"""Take one step in the environment.
|
|
95
|
-
|
|
96
|
-
Parameters
|
|
97
|
-
----------
|
|
98
|
-
action : int
|
|
99
|
-
Integer action in range 0-13.
|
|
100
|
-
|
|
101
|
-
Returns
|
|
102
|
-
-------
|
|
103
|
-
dict
|
|
104
|
-
Step result containing:
|
|
105
|
-
|
|
106
|
-
- type (str): the message type, "step"
|
|
107
|
-
- obs (list of float): the observation, 47 floats
|
|
108
|
-
- reward (float): reward for this step
|
|
109
|
-
- done (bool): True once the episode has ended
|
|
110
|
-
- info (dict): per-step diagnostics from the simulator
|
|
111
|
-
|
|
112
|
-
Raises
|
|
113
|
-
------
|
|
114
|
-
RuntimeError
|
|
115
|
-
If ``action`` is outside 0-13, or the simulator replies with an
|
|
116
|
-
error. Note this resource raises ``RuntimeError`` rather than
|
|
117
|
-
``PulseAPIError`` — it speaks WebSocket, not HTTP.
|
|
118
|
-
|
|
119
|
-
See Also
|
|
120
|
-
--------
|
|
121
|
-
GymSession.reset : Start a new episode.
|
|
122
|
-
GymSession.close : End the session.
|
|
123
|
-
|
|
124
|
-
Examples
|
|
125
|
-
--------
|
|
126
|
-
>>> result = env.step(0)
|
|
127
|
-
>>> result["reward"], result["done"]
|
|
128
|
-
(0.0012, False)
|
|
129
|
-
|
|
130
|
-
Run an episode to completion, accumulating reward:
|
|
131
|
-
|
|
132
|
-
>>> total = 0.0
|
|
133
|
-
>>> obs = env.reset(seed=42)
|
|
134
|
-
>>> while not obs["done"]:
|
|
135
|
-
... obs = env.step(0)
|
|
136
|
-
... total += obs["reward"]
|
|
137
|
-
"""
|
|
138
|
-
self._ws.send(json.dumps({"type": "step", "action": action}))
|
|
139
|
-
msg = json.loads(self._ws.recv())
|
|
140
|
-
if msg["type"] == "error":
|
|
141
|
-
raise RuntimeError(f"Simulator error: {msg['message']}")
|
|
142
|
-
return msg
|
|
143
|
-
|
|
144
|
-
def close(self):
|
|
145
|
-
"""Close the session and the underlying WebSocket connection.
|
|
146
|
-
|
|
147
|
-
Sends a ``close`` message, waits for the acknowledgement, then shuts
|
|
148
|
-
the socket down. The socket is closed even if the acknowledgement never
|
|
149
|
-
arrives, so this is safe to call on a session that has already failed.
|
|
150
|
-
Calling it twice is harmless.
|
|
151
|
-
|
|
152
|
-
Called automatically when the session is used as a context manager.
|
|
153
|
-
|
|
154
|
-
See Also
|
|
155
|
-
--------
|
|
156
|
-
SimulatorGymResource.connect : Open a new session.
|
|
157
|
-
|
|
158
|
-
Examples
|
|
159
|
-
--------
|
|
160
|
-
>>> env = client.simulator_gym.connect(
|
|
161
|
-
... symbol="700.HK", cal_date="2025-09-02", exchange="HKEX.Securities"
|
|
162
|
-
... )
|
|
163
|
-
>>> try:
|
|
164
|
-
... env.reset(seed=42)
|
|
165
|
-
... finally:
|
|
166
|
-
... env.close()
|
|
167
|
-
"""
|
|
168
|
-
try:
|
|
169
|
-
self._ws.send(json.dumps({"type": "close"}))
|
|
170
|
-
self._ws.recv()
|
|
171
|
-
finally:
|
|
172
|
-
self._ws.close()
|
|
173
|
-
|
|
174
|
-
def __enter__(self):
|
|
175
|
-
return self
|
|
176
|
-
|
|
177
|
-
def __exit__(self, *args):
|
|
178
|
-
self.close()
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
class SimulatorGymResource:
|
|
182
|
-
"""Open Gym-style reinforcement-learning environments over a WebSocket.
|
|
183
|
-
|
|
184
|
-
Reached as ``client.simulator_gym``. The API authenticates the socket with
|
|
185
|
-
your key and proxies it to the gym service, which drives the real engine.
|
|
186
|
-
|
|
187
|
-
This is the one resource that is stateful: :meth:`connect` returns a
|
|
188
|
-
:class:`GymSession` you hold open and step, rather than a dict.
|
|
189
|
-
|
|
190
|
-
Examples
|
|
191
|
-
--------
|
|
192
|
-
>>> with client.simulator_gym.connect(
|
|
193
|
-
... symbol="700.HK", cal_date="2025-09-02", exchange="HKEX.Securities"
|
|
194
|
-
... ) as env:
|
|
195
|
-
... obs = env.reset(seed=42)
|
|
196
|
-
... result = env.step(0)
|
|
197
|
-
"""
|
|
198
|
-
|
|
199
|
-
def __init__(self, client):
|
|
200
|
-
self._client = client
|
|
201
|
-
|
|
202
|
-
def _ws_base_url(self) -> str:
|
|
203
|
-
url = self._client.base_url
|
|
204
|
-
if url.startswith("https://"):
|
|
205
|
-
return "wss://" + url[len("https://"):]
|
|
206
|
-
if url.startswith("http://"):
|
|
207
|
-
return "ws://" + url[len("http://"):]
|
|
208
|
-
return url
|
|
209
|
-
|
|
210
|
-
def connect(self, symbol: str, cal_date: str, exchange: str) -> GymSession:
|
|
211
|
-
"""Open a new simulator-gym session.
|
|
212
|
-
|
|
213
|
-
Parameters
|
|
214
|
-
----------
|
|
215
|
-
symbol : str
|
|
216
|
-
Trading symbol, e.g. "700.HK"
|
|
217
|
-
cal_date : str
|
|
218
|
-
Calibration date, e.g. "2025-09-02"
|
|
219
|
-
exchange : str
|
|
220
|
-
Exchange identifier, e.g. "HKEX.Securities"
|
|
221
|
-
|
|
222
|
-
Returns
|
|
223
|
-
-------
|
|
224
|
-
GymSession
|
|
225
|
-
An open session, ready to :meth:`~GymSession.reset` and
|
|
226
|
-
:meth:`~GymSession.step`. Use it as a context manager so it is
|
|
227
|
-
closed on the way out.
|
|
228
|
-
|
|
229
|
-
Raises
|
|
230
|
-
------
|
|
231
|
-
RuntimeError
|
|
232
|
-
If the key is rejected, the gym service cannot be reached, or the
|
|
233
|
-
service refuses the session — an unknown symbol or date, most
|
|
234
|
-
often. This resource raises ``RuntimeError`` rather than
|
|
235
|
-
``PulseAPIError`` because it speaks WebSocket, not HTTP.
|
|
236
|
-
|
|
237
|
-
See Also
|
|
238
|
-
--------
|
|
239
|
-
GymSession.reset : Start an episode on the session.
|
|
240
|
-
GymSession.step : Advance it.
|
|
241
|
-
GymSession.close : End it.
|
|
242
|
-
|
|
243
|
-
Notes
|
|
244
|
-
-----
|
|
245
|
-
``exchange`` takes its display form here — ``"HKEX.Securities"`` — not
|
|
246
|
-
the protocol slug ``"hkex_securities"`` used by
|
|
247
|
-
:meth:`~simudyne.resources.simulation.SimulationResource.run` and the
|
|
248
|
-
data catalog.
|
|
249
|
-
|
|
250
|
-
Examples
|
|
251
|
-
--------
|
|
252
|
-
>>> with client.simulator_gym.connect(
|
|
253
|
-
... symbol="700.HK",
|
|
254
|
-
... cal_date="2025-09-02",
|
|
255
|
-
... exchange="HKEX.Securities",
|
|
256
|
-
... ) as env:
|
|
257
|
-
... obs = env.reset(seed=42)
|
|
258
|
-
... result = env.step(0)
|
|
259
|
-
... print(result["reward"], result["done"])
|
|
260
|
-
|
|
261
|
-
Without the context manager, close it yourself:
|
|
262
|
-
|
|
263
|
-
>>> env = client.simulator_gym.connect(
|
|
264
|
-
... symbol="700.HK", cal_date="2025-09-02", exchange="HKEX.Securities"
|
|
265
|
-
... )
|
|
266
|
-
>>> try:
|
|
267
|
-
... env.reset(seed=42)
|
|
268
|
-
... finally:
|
|
269
|
-
... env.close()
|
|
270
|
-
"""
|
|
271
|
-
ws_url = f"{self._ws_base_url()}/ws/simulator-gym"
|
|
272
|
-
try:
|
|
273
|
-
ws = websocket.create_connection(
|
|
274
|
-
ws_url,
|
|
275
|
-
header={"X-API-Key": self._client.api_key},
|
|
276
|
-
)
|
|
277
|
-
except websocket.WebSocketBadStatusException as e:
|
|
278
|
-
if "403" in str(e):
|
|
279
|
-
raise RuntimeError(
|
|
280
|
-
"Authentication failed: invalid or expired API key. "
|
|
281
|
-
"Check your API key with client.api_keys.list()."
|
|
282
|
-
) from e
|
|
283
|
-
raise RuntimeError(f"Failed to connect to simulator-gym: {e}") from e
|
|
284
|
-
except Exception as e:
|
|
285
|
-
raise RuntimeError(f"Failed to connect to simulator-gym: {e}") from e
|
|
286
|
-
|
|
287
|
-
ws.send(json.dumps({
|
|
288
|
-
"type": "create",
|
|
289
|
-
"config": {
|
|
290
|
-
"symbol": symbol,
|
|
291
|
-
"cal_date": cal_date,
|
|
292
|
-
"exchange": exchange,
|
|
293
|
-
}
|
|
294
|
-
}))
|
|
295
|
-
|
|
296
|
-
msg = json.loads(ws.recv())
|
|
297
|
-
if msg["type"] == "error":
|
|
298
|
-
ws.close()
|
|
299
|
-
raise RuntimeError(f"Failed to create session: {msg['message']}")
|
|
300
|
-
if msg["type"] != "created":
|
|
301
|
-
ws.close()
|
|
302
|
-
raise RuntimeError(f"Unexpected response type '{msg['type']}' during session creation")
|
|
303
|
-
|
|
304
|
-
return GymSession(ws, session_id=msg["session_id"])
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
from unittest.mock import MagicMock, patch, Mock
|
|
3
|
-
import requests
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class TestSimulatorGymAuth(unittest.TestCase):
|
|
7
|
-
"""Verify WebSocket authentication uses header instead of query param."""
|
|
8
|
-
|
|
9
|
-
@patch("simudyne.resources.simulator_gym.websocket.create_connection")
|
|
10
|
-
def test_connect_uses_header_auth(self, mock_create_connection):
|
|
11
|
-
mock_ws = MagicMock()
|
|
12
|
-
mock_ws.recv.return_value = '{"type": "created", "session_id": "test-123"}'
|
|
13
|
-
mock_create_connection.return_value = mock_ws
|
|
14
|
-
|
|
15
|
-
from simudyne import PulseABM
|
|
16
|
-
|
|
17
|
-
client = PulseABM(api_key="pk_test_secret123")
|
|
18
|
-
client.simulator_gym.connect("700.HK", "2025-09-02", "HKEX.Securities")
|
|
19
|
-
|
|
20
|
-
mock_create_connection.assert_called_once()
|
|
21
|
-
call_args = mock_create_connection.call_args
|
|
22
|
-
|
|
23
|
-
url = call_args[0][0]
|
|
24
|
-
self.assertNotIn("api_key=", url, "API key should not be in URL query string")
|
|
25
|
-
self.assertNotIn("pk_test_secret123", url, "API key should not appear in URL")
|
|
26
|
-
|
|
27
|
-
header = call_args[1].get("header", {})
|
|
28
|
-
self.assertEqual(header.get("X-API-Key"), "pk_test_secret123", "API key should be in X-API-Key header")
|
|
29
|
-
|
|
30
|
-
@patch("simudyne.resources.simulator_gym.websocket.create_connection")
|
|
31
|
-
def test_connect_url_format(self, mock_create_connection):
|
|
32
|
-
mock_ws = MagicMock()
|
|
33
|
-
mock_ws.recv.return_value = '{"type": "created", "session_id": "test-123"}'
|
|
34
|
-
mock_create_connection.return_value = mock_ws
|
|
35
|
-
|
|
36
|
-
from simudyne import PulseABM
|
|
37
|
-
|
|
38
|
-
client = PulseABM(api_key="pk_test_xyz", base_url="https://api.example.com")
|
|
39
|
-
client.simulator_gym.connect("700.HK", "2025-09-02", "HKEX.Securities")
|
|
40
|
-
|
|
41
|
-
url = mock_create_connection.call_args[0][0]
|
|
42
|
-
self.assertEqual(url, "wss://api.example.com/ws/simulator-gym")
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class TestClientTLSVerification(unittest.TestCase):
|
|
46
|
-
"""Verify TLS verification is explicitly enabled."""
|
|
47
|
-
|
|
48
|
-
def test_session_verify_enabled(self):
|
|
49
|
-
from simudyne import PulseABM
|
|
50
|
-
|
|
51
|
-
client = PulseABM(api_key="pk_test_123")
|
|
52
|
-
self.assertTrue(client.session.verify, "TLS verification should be explicitly True")
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
class TestTimeoutAndRetry(unittest.TestCase):
|
|
56
|
-
"""Verify timeout and exponential backoff behavior."""
|
|
57
|
-
|
|
58
|
-
def test_default_timeout(self):
|
|
59
|
-
from simudyne import PulseABM
|
|
60
|
-
|
|
61
|
-
client = PulseABM(api_key="pk_test_123")
|
|
62
|
-
self.assertEqual(client.timeout, 30)
|
|
63
|
-
|
|
64
|
-
def test_custom_timeout(self):
|
|
65
|
-
from simudyne import PulseABM
|
|
66
|
-
|
|
67
|
-
client = PulseABM(api_key="pk_test_123", timeout=60)
|
|
68
|
-
self.assertEqual(client.timeout, 60)
|
|
69
|
-
|
|
70
|
-
def test_custom_max_retries(self):
|
|
71
|
-
from simudyne import PulseABM
|
|
72
|
-
|
|
73
|
-
client = PulseABM(api_key="pk_test_123", max_retries=5)
|
|
74
|
-
self.assertEqual(client.max_retries, 5)
|
|
75
|
-
|
|
76
|
-
@patch("simudyne.client.time.sleep")
|
|
77
|
-
def test_retries_on_503(self, mock_sleep):
|
|
78
|
-
from simudyne import PulseABM
|
|
79
|
-
|
|
80
|
-
client = PulseABM(api_key="pk_test_123", max_retries=2)
|
|
81
|
-
|
|
82
|
-
mock_response_fail = Mock()
|
|
83
|
-
mock_response_fail.ok = False
|
|
84
|
-
mock_response_fail.status_code = 503
|
|
85
|
-
|
|
86
|
-
mock_response_ok = Mock()
|
|
87
|
-
mock_response_ok.ok = True
|
|
88
|
-
mock_response_ok.json.return_value = {"status": "ok"}
|
|
89
|
-
|
|
90
|
-
client.session.request = Mock(side_effect=[mock_response_fail, mock_response_ok])
|
|
91
|
-
|
|
92
|
-
result = client._request("GET", "/test")
|
|
93
|
-
|
|
94
|
-
self.assertEqual(result, {"status": "ok"})
|
|
95
|
-
self.assertEqual(client.session.request.call_count, 2)
|
|
96
|
-
mock_sleep.assert_called_once_with(1)
|
|
97
|
-
|
|
98
|
-
@patch("simudyne.client.time.sleep")
|
|
99
|
-
def test_raises_after_max_retries(self, mock_sleep):
|
|
100
|
-
from simudyne import PulseABM, PulseAPIError
|
|
101
|
-
|
|
102
|
-
client = PulseABM(api_key="pk_test_123", max_retries=2)
|
|
103
|
-
|
|
104
|
-
mock_response = Mock()
|
|
105
|
-
mock_response.ok = False
|
|
106
|
-
mock_response.status_code = 503
|
|
107
|
-
mock_response.text = "Service Unavailable"
|
|
108
|
-
mock_response.json.return_value = {"detail": "Service Unavailable"}
|
|
109
|
-
|
|
110
|
-
client.session.request = Mock(return_value=mock_response)
|
|
111
|
-
|
|
112
|
-
with self.assertRaises(PulseAPIError) as ctx:
|
|
113
|
-
client._request("GET", "/test")
|
|
114
|
-
|
|
115
|
-
self.assertEqual(ctx.exception.status_code, 503)
|
|
116
|
-
self.assertEqual(client.session.request.call_count, 3)
|
|
117
|
-
|
|
118
|
-
def test_no_retry_on_400(self):
|
|
119
|
-
from simudyne import PulseABM, PulseAPIError
|
|
120
|
-
|
|
121
|
-
client = PulseABM(api_key="pk_test_123", max_retries=3)
|
|
122
|
-
|
|
123
|
-
mock_response = Mock()
|
|
124
|
-
mock_response.ok = False
|
|
125
|
-
mock_response.status_code = 400
|
|
126
|
-
mock_response.text = "Bad Request"
|
|
127
|
-
mock_response.json.return_value = {"detail": "Bad Request"}
|
|
128
|
-
|
|
129
|
-
client.session.request = Mock(return_value=mock_response)
|
|
130
|
-
|
|
131
|
-
with self.assertRaises(PulseAPIError):
|
|
132
|
-
client._request("GET", "/test")
|
|
133
|
-
|
|
134
|
-
self.assertEqual(client.session.request.call_count, 1)
|
|
135
|
-
|
|
136
|
-
@patch("simudyne.client.time.sleep")
|
|
137
|
-
def test_retries_on_429_rate_limit(self, mock_sleep):
|
|
138
|
-
from simudyne import PulseABM
|
|
139
|
-
|
|
140
|
-
client = PulseABM(api_key="pk_test_123", max_retries=2)
|
|
141
|
-
|
|
142
|
-
mock_response_fail = Mock()
|
|
143
|
-
mock_response_fail.ok = False
|
|
144
|
-
mock_response_fail.status_code = 429
|
|
145
|
-
|
|
146
|
-
mock_response_ok = Mock()
|
|
147
|
-
mock_response_ok.ok = True
|
|
148
|
-
mock_response_ok.json.return_value = {"data": "success"}
|
|
149
|
-
|
|
150
|
-
client.session.request = Mock(side_effect=[mock_response_fail, mock_response_ok])
|
|
151
|
-
|
|
152
|
-
result = client._request("GET", "/test")
|
|
153
|
-
|
|
154
|
-
self.assertEqual(result, {"data": "success"})
|
|
155
|
-
self.assertEqual(client.session.request.call_count, 2)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
class TestPulseAPIError(unittest.TestCase):
|
|
159
|
-
"""Verify custom exception behavior."""
|
|
160
|
-
|
|
161
|
-
def test_exception_attributes(self):
|
|
162
|
-
from simudyne import PulseAPIError
|
|
163
|
-
|
|
164
|
-
err = PulseAPIError(404, "Not Found")
|
|
165
|
-
self.assertEqual(err.status_code, 404)
|
|
166
|
-
self.assertEqual(err.detail, "Not Found")
|
|
167
|
-
self.assertIn("404", str(err))
|
|
168
|
-
self.assertIn("Not Found", str(err))
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if __name__ == "__main__":
|
|
172
|
-
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne/resources/validation.py
RENAMED
|
File without changes
|
|
File without changes
|
{simudyne_pulse-0.8.0.dev1 → simudyne_pulse-0.8.0.dev3}/src/simudyne_pulse.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|