simudyne-pulse 0.7.0__tar.gz → 0.7.0.dev2__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 (27) hide show
  1. {simudyne_pulse-0.7.0/src/simudyne_pulse.egg-info → simudyne_pulse-0.7.0.dev2}/PKG-INFO +1 -1
  2. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/pyproject.toml +1 -1
  3. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/client.py +4 -0
  4. simudyne_pulse-0.7.0.dev2/src/simudyne/resources/fix.py +30 -0
  5. simudyne_pulse-0.7.0.dev2/src/simudyne/resources/fm.py +199 -0
  6. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/resources/simulation.py +78 -0
  7. simudyne_pulse-0.7.0.dev2/src/simudyne/resources/validation.py +528 -0
  8. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2/src/simudyne_pulse.egg-info}/PKG-INFO +1 -1
  9. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne_pulse.egg-info/SOURCES.txt +5 -1
  10. simudyne_pulse-0.7.0.dev2/tests/test_new_resources.py +150 -0
  11. simudyne_pulse-0.7.0.dev2/tests/test_validation.py +158 -0
  12. simudyne_pulse-0.7.0/src/simudyne/resources/validation.py +0 -267
  13. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/LICENSE +0 -0
  14. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/README.md +0 -0
  15. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/setup.cfg +0 -0
  16. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/__init__.py +0 -0
  17. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/exceptions.py +0 -0
  18. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/resources/__init__.py +0 -0
  19. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/resources/api_keys.py +0 -0
  20. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/resources/data.py +0 -0
  21. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/resources/historical.py +0 -0
  22. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/resources/profile.py +0 -0
  23. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/resources/simulator_gym.py +0 -0
  24. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
  25. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne_pulse.egg-info/requires.txt +0 -0
  26. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
  27. {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/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
3
+ Version: 0.7.0.dev2
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"
7
+ version = "0.7.0-dev.2"
8
8
  description = "Python SDK for the Simudyne Pulse synthetic market data API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -36,6 +36,8 @@ class PulseABM:
36
36
  from simudyne.resources.profile import ProfileResource
37
37
  from simudyne.resources.api_keys import ApiKeysResource
38
38
  from simudyne.resources.data import DataResource
39
+ from simudyne.resources.fix import FixResource
40
+ from simudyne.resources.fm import FmResource
39
41
  from simudyne.resources.simulation import SimulationResource
40
42
  from simudyne.resources.simulator_gym import SimulatorGymResource
41
43
  from simudyne.resources.validation import ValidationResource
@@ -43,6 +45,8 @@ class PulseABM:
43
45
  self.profile = ProfileResource(self)
44
46
  self.api_keys = ApiKeysResource(self)
45
47
  self.data = DataResource(self)
48
+ self.fix = FixResource(self)
49
+ self.fm = FmResource(self)
46
50
  self.simulation = SimulationResource(self)
47
51
  self.simulator_gym = SimulatorGymResource(self)
48
52
  self.validation = ValidationResource(self)
@@ -0,0 +1,30 @@
1
+ """
2
+ FIX Resource for the Pulse SDK.
3
+
4
+ Pulse simulations can be consumed over the FIX protocol by a market-data
5
+ session instead of the REST download endpoints. The FIX connection itself is
6
+ provisioned per organisation (see the FIX docs page); this resource covers
7
+ what the SDK can usefully do about it — reporting what your account has run
8
+ over FIX.
9
+ """
10
+
11
+ USAGE_PATH = "/fix/usage"
12
+
13
+
14
+ class FixResource:
15
+ def __init__(self, client):
16
+ self._client = client
17
+
18
+ def usage(self) -> dict:
19
+ """FIX simulation statistics: what was run over FIX, and how often.
20
+
21
+ Returns:
22
+ dict with ``total_runs``, ``simulated_seconds``, and ``runs`` — one
23
+ entry per distinct configuration, each with its ``symbol``,
24
+ ``cal_date``, ``scenario`` and ``runs`` count.
25
+
26
+ Example:
27
+ >>> usage = client.fix.usage()
28
+ >>> print(f"{usage['total_runs']} FIX runs")
29
+ """
30
+ return self._client._request("GET", USAGE_PATH)
@@ -0,0 +1,199 @@
1
+ """
2
+ Foundation Models Resource for the Pulse SDK.
3
+
4
+ A foundation-model (FM) run generates limit order book activity by continuing
5
+ from real market data rather than simulating agents. The platform slices the
6
+ model's required context out of the chosen market's data, runs inference, and
7
+ writes the output using the same conventions as an agent-based run — so the
8
+ ordinary simulation results and download endpoints work unchanged.
9
+
10
+ Workflow:
11
+ 1. Discover models with models() — each entry lists the markets it
12
+ supports and the model_args it accepts
13
+ 2. Find a promptable symbol/date with available_data(model_id=...)
14
+ 3. Submit inference with run() -> returns job_id and sim_ids
15
+ 4. Poll job_status(job_id) until it reaches a TERMINAL_STATUSES value;
16
+ read job_logs(job_id) when it failed
17
+ 5. Download with client.simulation.get_sim_data(sim_id) — an FM sim_id is
18
+ an ordinary sim_id
19
+
20
+ The output file contains the model's prompt context as well as its generated
21
+ frames: the leading rows of sim_data.parquet are real market data. The split
22
+ is recorded in the parquet file-level metadata (``pulse_fm.n_historical``,
23
+ ``pulse_fm.n_generated``, ``pulse_fm.segments``) — drop the context rows
24
+ before computing statistics on the generated activity.
25
+ """
26
+
27
+ MODELS_PATH = "/fm/models"
28
+ AVAILABLE_DATA_PATH = "/fm/available-data"
29
+ RUN_PATH = "/fm/run"
30
+ LIVE_PATH = "/fm/live"
31
+ JOBS_PATH = "/fm/jobs"
32
+
33
+ #: The only statuses a job never leaves. Everything else (queued,
34
+ #: provisioning, starting, running) means "keep polling".
35
+ TERMINAL_STATUSES = {"complete", "failed"}
36
+
37
+
38
+ class FmResource:
39
+ def __init__(self, client):
40
+ self._client = client
41
+
42
+ def models(self) -> dict:
43
+ """Active foundation models in this environment.
44
+
45
+ The pre-run discovery call: each entry carries the model's id and
46
+ version, the markets its ``supported_data`` declares (empty = any),
47
+ ``min_prompt_rows``, and the ``model_args`` descriptors for the knobs
48
+ run() will accept. Pro tier.
49
+
50
+ Returns:
51
+ dict with a ``models`` list.
52
+ """
53
+ return self._client._request("GET", MODELS_PATH)
54
+
55
+ def available_data(
56
+ self,
57
+ model_id: str = None,
58
+ symbol: str = None,
59
+ q: str = None,
60
+ provider: str = None,
61
+ exchange: str = None,
62
+ date: str = None,
63
+ limit: int = 50,
64
+ offset: int = 0,
65
+ ) -> dict:
66
+ """What a foundation model can be prompted with: the raw-data registry.
67
+
68
+ An FM prompt is the first rows of a REAL trading day, not a
69
+ calibration, so this searches the raw-data catalog rather than the
70
+ calibrated list at data.get_available_symbols(). Passing ``model_id``
71
+ restricts the result to the (provider, exchange) markets that model's
72
+ ``supported_data`` declares — exactly what run() will accept.
73
+
74
+ Args:
75
+ model_id: Registered model (see models()). Unknown id -> 404.
76
+ symbol: Exact symbol, e.g. "TSCO" or "700".
77
+ q: Case-insensitive substring match over the symbol.
78
+ provider: Data provider, e.g. "bmll".
79
+ exchange: Protocol, e.g. "lse" or "hkex_securities".
80
+ date: Trading date YYYY-MM-DD; keeps only symbols with data on it,
81
+ and only that date.
82
+ limit: Max symbols to return (default 50, max 200).
83
+ offset: Symbols to skip, for paging.
84
+
85
+ Returns:
86
+ dict with ``symbols`` (each with its provider, exchange and dates)
87
+ and ``total``, paged by symbol identity.
88
+ """
89
+ params = {"limit": limit, "offset": offset}
90
+ for key, value in (
91
+ ("model_id", model_id), ("symbol", symbol), ("q", q),
92
+ ("provider", provider), ("exchange", exchange), ("date", date),
93
+ ):
94
+ if value is not None:
95
+ params[key] = value
96
+ return self._client._request("GET", AVAILABLE_DATA_PATH, params=params)
97
+
98
+ def run(
99
+ self,
100
+ model_id: str,
101
+ symbol: str,
102
+ cal_date: str,
103
+ provider: str,
104
+ exchange: str,
105
+ duration_minutes: float = None,
106
+ horizon: int = None,
107
+ n_runs: int = 1,
108
+ seed: int = 42,
109
+ model_args: dict = None,
110
+ device: str = None,
111
+ exec_algos: list = None,
112
+ ) -> dict:
113
+ """Submit a foundation-model inference job. Pro tier.
114
+
115
+ Args:
116
+ model_id: A model from models().
117
+ symbol: Symbol whose real data becomes the prompt context.
118
+ cal_date: Trading date of the prompt, YYYY-MM-DD.
119
+ provider: Data provider, e.g. "bmll".
120
+ exchange: Exchange protocol, e.g. "hkex_securities".
121
+ duration_minutes: Sim horizon as wall-clock minutes measured from
122
+ the first row of the context. The resulting frame count is
123
+ reported in the run's output rather than requested up front.
124
+ horizon: Raw frame count — the older form; applies only when
125
+ duration_minutes is unset.
126
+ n_runs: Monte Carlo runs, 1-8 (per-run seeds derived from ``seed``
127
+ exactly as the ABM derives them).
128
+ seed: Random seed (default 42).
129
+ model_args: Overrides for the model's declared knobs (see
130
+ models()); unknown or out-of-range keys are rejected with 400.
131
+ device: "cpu" or "gpu"; None uses the model's default.
132
+ exec_algos: Execution algorithms, the ABM's config shape. At most
133
+ one per job; market runs only.
134
+
135
+ Returns:
136
+ dict with job_id, the model version, and the queued sim_ids.
137
+ """
138
+ payload = {
139
+ "model_id": model_id,
140
+ "symbol": symbol,
141
+ "cal_date": cal_date,
142
+ "provider": provider,
143
+ "exchange": exchange,
144
+ "n_runs": n_runs,
145
+ "seed": seed,
146
+ }
147
+ if duration_minutes is not None:
148
+ payload["duration_minutes"] = duration_minutes
149
+ if horizon is not None:
150
+ payload["horizon"] = horizon
151
+ if model_args is not None:
152
+ payload["model_args"] = model_args
153
+ if device is not None:
154
+ payload["device"] = device
155
+ if exec_algos is not None:
156
+ payload["exec_algos"] = exec_algos
157
+ return self._client._request("POST", RUN_PATH, json=payload)
158
+
159
+ def live(
160
+ self,
161
+ model_id: str,
162
+ horizon: int = None,
163
+ seed: int = 42,
164
+ model_args: dict = None,
165
+ ) -> dict:
166
+ """Start a live streaming session instead of a batch job. Pro tier.
167
+
168
+ Returns:
169
+ dict with ``token`` (used to connect to the live chart),
170
+ ``job_id`` and ``model_id``.
171
+ """
172
+ payload = {"model_id": model_id, "seed": seed}
173
+ if horizon is not None:
174
+ payload["horizon"] = horizon
175
+ if model_args is not None:
176
+ payload["model_args"] = model_args
177
+ return self._client._request("POST", LIVE_PATH, json=payload)
178
+
179
+ def job_status(self, job_id: str) -> dict:
180
+ """Foundation-model job status.
181
+
182
+ Returns:
183
+ dict with job_id, ``status``, ``message``, ``detail``,
184
+ ``started_at`` and ``completed_at``. ``status`` is one of queued,
185
+ provisioning, starting, running, complete or failed — only the
186
+ last two are terminal (see TERMINAL_STATUSES), so poll until one
187
+ of those appears. ``message`` is the human label for the state and
188
+ ``detail`` says what the run is waiting on (a GPU stockout reads
189
+ as the scheduler's own reason).
190
+ """
191
+ return self._client._request("GET", f"{JOBS_PATH}/{job_id}/status")
192
+
193
+ def job_logs(self, job_id: str) -> dict:
194
+ """Foundation-model job pod logs.
195
+
196
+ Returns:
197
+ dict with ``logs`` — the thing to read when a job failed.
198
+ """
199
+ return self._client._request("GET", f"{JOBS_PATH}/{job_id}/logs")
@@ -21,6 +21,7 @@ RESULTS_PATH = "/simulation/results"
21
21
  CACHED_PATH = "/simulation/cached"
22
22
  SAMPLE_PATH = "/simulation/sample"
23
23
  CALIBRATE_PATH = "/calibrate"
24
+ LRM_PATH = "/simulation/lrm/run"
24
25
 
25
26
  # Available market scenarios
26
27
  SCENARIOS = {
@@ -437,6 +438,83 @@ class SimulationResource:
437
438
  """
438
439
  return self._pro_request("GET", f"{JOBS_PATH}/{job_id}/results")
439
440
 
441
+ def get_job_logs(self, job_id: str) -> str:
442
+ """Fetch the engine run log for one of your jobs, as plain text.
443
+
444
+ The worker writes a diagnostic log per job — this is the thing to read
445
+ when a run fails or finishes with nothing plottable, and the thing to
446
+ attach when sending a problem to support@simudyne.com.
447
+
448
+ Args:
449
+ job_id: The job ID from run() or get_jobs()
450
+
451
+ Returns:
452
+ str: The log text.
453
+
454
+ Raises:
455
+ PulseAPIError: 404 when the job does not exist, is not yours, or
456
+ wrote no log.
457
+
458
+ Example:
459
+ >>> status = client.simulation.get_job_status(job_id)
460
+ >>> if status["has_errors"]:
461
+ ... print(client.simulation.get_job_logs(job_id)[:2000])
462
+ """
463
+ # Plain text, not JSON — go through the retrying transport directly.
464
+ url = f"{self._client.base_url}{JOBS_PATH}/{job_id}/logs"
465
+ response = self._client._request_with_retries("GET", url)
466
+ return response.text
467
+
468
+ def run_lrm(
469
+ self,
470
+ symbol: str,
471
+ cal_date: str,
472
+ provider: str,
473
+ exchange: str,
474
+ order_sizes: list,
475
+ n_runs: int = 50,
476
+ seed: int = 42,
477
+ horizon_mins: int = 60,
478
+ strategy: str = "vwap",
479
+ side: str = None,
480
+ ):
481
+ """Run a liquidity-risk grid: market impact across a ladder of order sizes.
482
+
483
+ One execution algo is built per entry in order_sizes, and all of them
484
+ share a single baseline, so the cost is ``n_runs * (1 + len(order_sizes))``
485
+ simulations rather than one baseline per size. Poll the returned job_id
486
+ through the usual job endpoints.
487
+
488
+ Args:
489
+ symbol: Trading symbol (e.g. "700")
490
+ cal_date: Calibration date in YYYY-MM-DD format
491
+ provider: Data provider (e.g. "omd", "bmll")
492
+ exchange: Exchange protocol (e.g. "hkex_securities")
493
+ order_sizes: Order sizes in LOTS — one algo per entry
494
+ n_runs: Monte Carlo runs per arm (default 50)
495
+ seed: Random seed (default 42)
496
+ horizon_mins: Execution horizon in minutes (default 60)
497
+ strategy: "vwap" or "twap" (default "vwap")
498
+ side: "buy" or "sell"; defaults to the sign of each order size
499
+
500
+ Returns:
501
+ dict with job_id and the queued sim_ids
502
+ """
503
+ payload = {
504
+ "symbol": symbol,
505
+ "cal_date": cal_date,
506
+ "provider": provider,
507
+ "exchange": exchange,
508
+ "order_sizes": order_sizes,
509
+ "n_runs": n_runs,
510
+ "seed": seed,
511
+ "horizon_mins": horizon_mins,
512
+ "strategy": strategy,
513
+ }
514
+ if side is not None:
515
+ payload["side"] = side
516
+ return self._pro_request("POST", LRM_PATH, json=payload)
517
+
440
518
  def list_sim_files(self, sim_id: str):
441
519
  """
442
520
  List available files for a specific simulation.