simudyne-pulse 0.7.0.dev2__tar.gz → 0.7.0.dev4__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.
Files changed (26) hide show
  1. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/PKG-INFO +1 -1
  2. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/pyproject.toml +1 -1
  3. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/data.py +3 -0
  4. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/simulation.py +7 -3
  5. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/validation.py +128 -7
  6. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/src/simudyne_pulse.egg-info/PKG-INFO +1 -1
  7. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/tests/test_validation.py +110 -0
  8. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/LICENSE +0 -0
  9. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/README.md +0 -0
  10. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/setup.cfg +0 -0
  11. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/src/simudyne/__init__.py +0 -0
  12. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/src/simudyne/client.py +0 -0
  13. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/src/simudyne/exceptions.py +0 -0
  14. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/__init__.py +0 -0
  15. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/api_keys.py +0 -0
  16. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/fix.py +0 -0
  17. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/fm.py +0 -0
  18. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/historical.py +0 -0
  19. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/profile.py +0 -0
  20. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/simulator_gym.py +0 -0
  21. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/src/simudyne_pulse.egg-info/SOURCES.txt +0 -0
  22. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
  23. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/src/simudyne_pulse.egg-info/requires.txt +0 -0
  24. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
  25. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/tests/test_new_resources.py +0 -0
  26. {simudyne_pulse-0.7.0.dev2 → simudyne_pulse-0.7.0.dev4}/tests/test_simulator_gym.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simudyne-pulse
3
- Version: 0.7.0.dev2
3
+ Version: 0.7.0.dev4
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simudyne-pulse"
7
- version = "0.7.0-dev.2"
7
+ version = "0.7.0-dev.4"
8
8
  description = "Python SDK for the Simudyne Pulse synthetic market data API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -11,6 +11,7 @@ class DataResource:
11
11
  exchange: str | None = None,
12
12
  provider: str | None = None,
13
13
  date: str | None = None,
14
+ q: str | None = None,
14
15
  limit: int | None = None,
15
16
  offset: int | None = None,
16
17
  ):
@@ -26,6 +27,7 @@ class DataResource:
26
27
  date: Calibration date "YYYY-MM-DD". Keeps only instruments
27
28
  calibrated on that date, and narrows each instrument's
28
29
  available_dates to it.
30
+ q: Case-insensitive substring match over ticker and company name.
29
31
  limit: Max instruments to return.
30
32
  offset: Instruments to skip (for paging alongside limit).
31
33
 
@@ -38,6 +40,7 @@ class DataResource:
38
40
  "exchange": exchange,
39
41
  "provider": provider,
40
42
  "date": date,
43
+ "q": q,
41
44
  "limit": limit,
42
45
  "offset": offset,
43
46
  }.items() if v is not None
@@ -299,13 +299,16 @@ class SimulationResource:
299
299
  payload["batch_size"] = batch_size
300
300
  return self._pro_request("POST", CALIBRATE_PATH, json=payload)
301
301
 
302
- def get_jobs(self):
302
+ def get_jobs(self, limit: int = 100):
303
303
  """
304
- Get all simulation jobs submitted by the authenticated user.
304
+ Get simulation jobs submitted by the authenticated user, newest first.
305
305
 
306
306
  Returns a list of jobs with their associated simulation IDs. Use this
307
307
  to find job IDs for past runs or to see what simulations are pending.
308
308
 
309
+ Args:
310
+ limit: Max jobs to return (default 100, max 500)
311
+
309
312
  Returns:
310
313
  dict: Contains:
311
314
  - jobs (list): List of job objects, each with:
@@ -313,6 +316,7 @@ class SimulationResource:
313
316
  - sim_ids (list): List of simulation IDs in this job
314
317
  - created_at (str): Timestamp when job was submitted
315
318
  - total (int): Total number of jobs
319
+ - returned (int): Jobs in this page
316
320
 
317
321
  Example:
318
322
  >>> result = client.simulation.get_jobs()
@@ -322,7 +326,7 @@ class SimulationResource:
322
326
  ... print(f"Job {job['job_id']}: {len(job['sim_ids'])} simulations")
323
327
  ... print(f" Created: {job['created_at']}")
324
328
  """
325
- return self._pro_request("GET", JOBS_PATH)
329
+ return self._pro_request("GET", JOBS_PATH, params={"limit": limit})
326
330
 
327
331
  def get_job_status(self, job_id: str):
328
332
  """
@@ -53,11 +53,56 @@ _TRI_STATE_FLAGS = (
53
53
  "plot_data",
54
54
  )
55
55
 
56
+ #: Areas of checking selectable per job, as of pulse-check 1.10.0. Left unset
57
+ #: they take the server's default, so a job that names none behaves as before.
58
+ _AREA_FLAGS = ("statistical", "stylised_facts", "impact", "volume_correlation",
59
+ "fid", "mind")
60
+
61
+ #: Everything else the 1.10.0 schema accepts. ``lob`` marks the frames as L2
62
+ #: snapshots, which switches off anything needing the message stream;
63
+ #: ``sample_period`` and ``match_generated_sample`` set the grid the book is
64
+ #: resampled onto; ``plots`` is False, True, or a list of plot ids;
65
+ #: ``historical_output`` is the demo-only gate that ``plot_data`` used to be.
66
+ _EXTRA_FIELDS = ("lob", "sample_period", "match_generated_sample", "plots",
67
+ "historical_output")
68
+
56
69
  #: SDK name -> API config field. The API kept ``run_fid`` for compatibility;
57
70
  #: the SDK spells out what it actually gates.
58
71
  _INCEPTION_WIRE_FIELD = "run_fid"
59
72
 
60
73
 
74
+ def _frame_to_parquet(entry, index: int):
75
+ """Normalise one simulated run to ``(filename, parquet bytes)``.
76
+
77
+ Accepts a path, a ``(filename, bytes)`` pair, or a polars / pandas
78
+ DataFrame, which is written to parquet in memory. The frame must be pulse
79
+ format -- the same shape the engine writes to ``sim_data.parquet`` -- which
80
+ the server validates; sending something else fails there, not here.
81
+ """
82
+ import io
83
+
84
+ if isinstance(entry, tuple):
85
+ return entry
86
+
87
+ # Duck-typed rather than imported: neither polars nor pandas is a hard
88
+ # dependency of the SDK, and importing one to test for the other would
89
+ # make it one.
90
+ writer = getattr(entry, "write_parquet", None) # polars
91
+ if writer is not None:
92
+ buf = io.BytesIO()
93
+ writer(buf)
94
+ return f"sim_{index}.parquet", buf.getvalue()
95
+
96
+ writer = getattr(entry, "to_parquet", None) # pandas
97
+ if writer is not None:
98
+ buf = io.BytesIO()
99
+ writer(buf, index=False)
100
+ return f"sim_{index}.parquet", buf.getvalue()
101
+
102
+ path = Path(entry)
103
+ return path.name, path.read_bytes()
104
+
105
+
61
106
  def _build_config(
62
107
  run_metrics,
63
108
  run_impact,
@@ -66,12 +111,14 @@ def _build_config(
66
111
  plot_data,
67
112
  n_levels,
68
113
  l2_only,
114
+ **extra,
69
115
  ) -> dict:
70
116
  """The validation config object as the API expects it.
71
117
 
72
118
  Tri-state flags left as None are omitted rather than sent as null: the API
73
119
  reads absence as "use my tier's default", and an explicit null would not
74
- do that.
120
+ do that. The same rule covers the 1.10.0 area flags and everything in
121
+ ``extra`` — naming nothing new leaves the job behaving exactly as before.
75
122
  """
76
123
  config = {
77
124
  "n_levels": n_levels,
@@ -84,6 +131,18 @@ def _build_config(
84
131
  ):
85
132
  if value is not None:
86
133
  config[flag] = value
134
+
135
+ for name in _AREA_FLAGS + _EXTRA_FIELDS:
136
+ value = extra.get(name)
137
+ if value is not None:
138
+ config[name] = value
139
+
140
+ unknown = set(extra) - set(_AREA_FLAGS) - set(_EXTRA_FIELDS)
141
+ if unknown:
142
+ raise ValueError(
143
+ f"Unknown validation option(s): {sorted(unknown)}. "
144
+ f"Valid: {sorted(_AREA_FLAGS + _EXTRA_FIELDS)}"
145
+ )
87
146
  return config
88
147
 
89
148
 
@@ -106,6 +165,17 @@ class ValidationResource:
106
165
  l2_only: bool = False,
107
166
  provider: str = None,
108
167
  exchange: str = None,
168
+ statistical: bool = None,
169
+ stylised_facts: bool = None,
170
+ impact: bool = None,
171
+ volume_correlation: bool = None,
172
+ fid: bool = None,
173
+ mind: bool = None,
174
+ lob: bool = None,
175
+ sample_period: str = None,
176
+ match_generated_sample: bool = None,
177
+ plots=None,
178
+ historical_output: bool = None,
109
179
  ) -> dict:
110
180
  """Submit a validation job.
111
181
 
@@ -155,6 +225,17 @@ class ValidationResource:
155
225
  config = _build_config(
156
226
  run_metrics, run_impact, run_inception_distances,
157
227
  run_stylised_facts, plot_data, n_levels, l2_only,
228
+ statistical=statistical,
229
+ stylised_facts=stylised_facts,
230
+ impact=impact,
231
+ volume_correlation=volume_correlation,
232
+ fid=fid,
233
+ mind=mind,
234
+ lob=lob,
235
+ sample_period=sample_period,
236
+ match_generated_sample=match_generated_sample,
237
+ plots=plots,
238
+ historical_output=historical_output,
158
239
  )
159
240
 
160
241
  payload = {
@@ -186,6 +267,17 @@ class ValidationResource:
186
267
  plot_data: bool = None,
187
268
  n_levels: int = 10,
188
269
  l2_only: bool = False,
270
+ statistical: bool = None,
271
+ stylised_facts: bool = None,
272
+ impact: bool = None,
273
+ volume_correlation: bool = None,
274
+ fid: bool = None,
275
+ mind: bool = None,
276
+ lob: bool = None,
277
+ sample_period: str = None,
278
+ match_generated_sample: bool = None,
279
+ plots=None,
280
+ historical_output: bool = None,
189
281
  ) -> dict:
190
282
  """Submit a validation job from simulation files you hold yourself.
191
283
 
@@ -227,15 +319,22 @@ class ValidationResource:
227
319
  config = _build_config(
228
320
  run_metrics, run_impact, run_inception_distances,
229
321
  run_stylised_facts, plot_data, n_levels, l2_only,
322
+ statistical=statistical,
323
+ stylised_facts=stylised_facts,
324
+ impact=impact,
325
+ volume_correlation=volume_correlation,
326
+ fid=fid,
327
+ mind=mind,
328
+ lob=lob,
329
+ sample_period=sample_period,
330
+ match_generated_sample=match_generated_sample,
331
+ plots=plots,
332
+ historical_output=historical_output,
230
333
  )
231
334
 
232
335
  files = []
233
- for entry in sim_files:
234
- if isinstance(entry, tuple):
235
- filename, content = entry
236
- else:
237
- path = Path(entry)
238
- filename, content = path.name, path.read_bytes()
336
+ for index, entry in enumerate(sim_files):
337
+ filename, content = _frame_to_parquet(entry, index)
239
338
  files.append(
240
339
  ("sim_files", (filename, content, "application/octet-stream"))
241
340
  )
@@ -316,6 +415,17 @@ class ValidationResource:
316
415
  exchange: str = None,
317
416
  poll_interval: float = 3.0,
318
417
  timeout: float = 600.0,
418
+ statistical: bool = None,
419
+ stylised_facts: bool = None,
420
+ impact: bool = None,
421
+ volume_correlation: bool = None,
422
+ fid: bool = None,
423
+ mind: bool = None,
424
+ lob: bool = None,
425
+ sample_period: str = None,
426
+ match_generated_sample: bool = None,
427
+ plots=None,
428
+ historical_output: bool = None,
319
429
  ) -> dict:
320
430
  """Submit a validation job and block until it completes.
321
431
 
@@ -364,6 +474,17 @@ class ValidationResource:
364
474
  l2_only=l2_only,
365
475
  provider=provider,
366
476
  exchange=exchange,
477
+ statistical=statistical,
478
+ stylised_facts=stylised_facts,
479
+ impact=impact,
480
+ volume_correlation=volume_correlation,
481
+ fid=fid,
482
+ mind=mind,
483
+ lob=lob,
484
+ sample_period=sample_period,
485
+ match_generated_sample=match_generated_sample,
486
+ plots=plots,
487
+ historical_output=historical_output,
367
488
  )
368
489
  job_id = job["job_id"]
369
490
  print(f"Validation job submitted: {job_id}", file=sys.stderr)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simudyne-pulse
3
- Version: 0.7.0.dev2
3
+ Version: 0.7.0.dev4
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
@@ -156,3 +156,113 @@ class TestRunPipeline:
156
156
  )
157
157
  assert result["status"] == "completed"
158
158
  assert "spread" in result["distances"]
159
+
160
+
161
+ class TestNewValidationOptions:
162
+ """pulse-check 1.10.0 areas, sampling and plot selection.
163
+
164
+ Everything here is opt-in: a job naming none of it must send exactly the
165
+ config it sent before, so existing callers are untouched.
166
+ """
167
+
168
+ BASE = dict(
169
+ run_metrics=None,
170
+ run_impact=None,
171
+ run_inception_distances=True,
172
+ run_stylised_facts=None,
173
+ plot_data=None,
174
+ n_levels=10,
175
+ l2_only=False,
176
+ )
177
+
178
+ def test_naming_nothing_new_is_unchanged(self):
179
+ from simudyne.resources.validation import _build_config
180
+
181
+ assert _build_config(**self.BASE) == {
182
+ "n_levels": 10,
183
+ "l2_only": False,
184
+ "run_fid": True,
185
+ }
186
+
187
+ @pytest.mark.parametrize(
188
+ "name,value",
189
+ [
190
+ ("statistical", False),
191
+ ("stylised_facts", True),
192
+ ("impact", False),
193
+ ("volume_correlation", True),
194
+ ("fid", False),
195
+ ("mind", True),
196
+ ("lob", True),
197
+ ("sample_period", "100ms"),
198
+ ("match_generated_sample", True),
199
+ ("historical_output", True),
200
+ ("plots", ["volume_correlation.levels"]),
201
+ ],
202
+ )
203
+ def test_option_is_forwarded(self, name, value):
204
+ from simudyne.resources.validation import _build_config
205
+
206
+ assert _build_config(**self.BASE, **{name: value})[name] == value
207
+
208
+ def test_unknown_option_is_rejected_by_name(self):
209
+ from simudyne.resources.validation import _build_config
210
+
211
+ with pytest.raises(ValueError, match="Unknown validation option"):
212
+ _build_config(**self.BASE, volume_corelation=True)
213
+
214
+ def test_none_is_omitted_not_sent_as_null(self):
215
+ """Absence means 'use my tier's default'; null would not."""
216
+ from simudyne.resources.validation import _build_config
217
+
218
+ assert "volume_correlation" not in _build_config(
219
+ **self.BASE, volume_correlation=None
220
+ )
221
+
222
+
223
+ class TestSimulatedFrameShapes:
224
+ """run_upload takes a path, a (name, bytes) pair, or a DataFrame."""
225
+
226
+ def test_tuple_passes_through(self):
227
+ from simudyne.resources.validation import _frame_to_parquet
228
+
229
+ assert _frame_to_parquet(("a.parquet", b"raw"), 0) == ("a.parquet", b"raw")
230
+
231
+ def test_path(self, tmp_path):
232
+ import pandas as pd
233
+
234
+ from simudyne.resources.validation import _frame_to_parquet
235
+
236
+ p = tmp_path / "run.parquet"
237
+ pd.DataFrame({"a": [1, 2]}).to_parquet(p)
238
+ name, content = _frame_to_parquet(str(p), 0)
239
+ assert name == "run.parquet"
240
+ assert content[:4] == b"PAR1"
241
+
242
+ def test_pandas_frame(self):
243
+ import pandas as pd
244
+
245
+ from simudyne.resources.validation import _frame_to_parquet
246
+
247
+ name, content = _frame_to_parquet(pd.DataFrame({"a": [1, 2]}), 3)
248
+ assert name == "sim_3.parquet"
249
+ assert content[:4] == b"PAR1"
250
+
251
+ def test_polars_frame(self):
252
+ pl = pytest.importorskip("polars")
253
+
254
+ from simudyne.resources.validation import _frame_to_parquet
255
+
256
+ name, content = _frame_to_parquet(pl.DataFrame({"a": [1, 2]}), 1)
257
+ assert name == "sim_1.parquet"
258
+ assert content[:4] == b"PAR1"
259
+
260
+ def test_each_frame_gets_its_own_name(self):
261
+ """Distinct names, or the multipart upload collapses the runs."""
262
+ import pandas as pd
263
+
264
+ from simudyne.resources.validation import _frame_to_parquet
265
+
266
+ frames = [pd.DataFrame({"a": [i]}) for i in range(3)]
267
+ names = [_frame_to_parquet(f, i)[0] for i, f in enumerate(frames)]
268
+ assert len(set(names)) == 3