simudyne-pulse 0.7.0.dev3__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.dev3 → simudyne_pulse-0.7.0.dev4}/PKG-INFO +1 -1
  2. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/pyproject.toml +1 -1
  3. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/validation.py +128 -7
  4. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/src/simudyne_pulse.egg-info/PKG-INFO +1 -1
  5. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/tests/test_validation.py +110 -0
  6. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/LICENSE +0 -0
  7. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/README.md +0 -0
  8. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/setup.cfg +0 -0
  9. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/src/simudyne/__init__.py +0 -0
  10. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/src/simudyne/client.py +0 -0
  11. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/src/simudyne/exceptions.py +0 -0
  12. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/__init__.py +0 -0
  13. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/api_keys.py +0 -0
  14. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/data.py +0 -0
  15. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/fix.py +0 -0
  16. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/fm.py +0 -0
  17. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/historical.py +0 -0
  18. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/profile.py +0 -0
  19. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/simulation.py +0 -0
  20. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/src/simudyne/resources/simulator_gym.py +0 -0
  21. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/src/simudyne_pulse.egg-info/SOURCES.txt +0 -0
  22. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
  23. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/src/simudyne_pulse.egg-info/requires.txt +0 -0
  24. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
  25. {simudyne_pulse-0.7.0.dev3 → simudyne_pulse-0.7.0.dev4}/tests/test_new_resources.py +0 -0
  26. {simudyne_pulse-0.7.0.dev3 → 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.dev3
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.3"
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"
@@ -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.dev3
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