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.
- {simudyne_pulse-0.7.0/src/simudyne_pulse.egg-info → simudyne_pulse-0.7.0.dev2}/PKG-INFO +1 -1
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/pyproject.toml +1 -1
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/client.py +4 -0
- simudyne_pulse-0.7.0.dev2/src/simudyne/resources/fix.py +30 -0
- simudyne_pulse-0.7.0.dev2/src/simudyne/resources/fm.py +199 -0
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/resources/simulation.py +78 -0
- simudyne_pulse-0.7.0.dev2/src/simudyne/resources/validation.py +528 -0
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2/src/simudyne_pulse.egg-info}/PKG-INFO +1 -1
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne_pulse.egg-info/SOURCES.txt +5 -1
- simudyne_pulse-0.7.0.dev2/tests/test_new_resources.py +150 -0
- simudyne_pulse-0.7.0.dev2/tests/test_validation.py +158 -0
- simudyne_pulse-0.7.0/src/simudyne/resources/validation.py +0 -267
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/LICENSE +0 -0
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/README.md +0 -0
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/setup.cfg +0 -0
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/__init__.py +0 -0
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/exceptions.py +0 -0
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/resources/__init__.py +0 -0
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/resources/api_keys.py +0 -0
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/resources/data.py +0 -0
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/resources/historical.py +0 -0
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/resources/profile.py +0 -0
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne/resources/simulator_gym.py +0 -0
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne_pulse.egg-info/requires.txt +0 -0
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
- {simudyne_pulse-0.7.0 → simudyne_pulse-0.7.0.dev2}/tests/test_simulator_gym.py +0 -0
|
@@ -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.
|