simudyne-pulse 0.7.0.dev5__tar.gz → 0.7.0.dev7__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.dev5/src/simudyne_pulse.egg-info → simudyne_pulse-0.7.0.dev7}/PKG-INFO +1 -1
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/pyproject.toml +1 -1
- simudyne_pulse-0.7.0.dev7/src/simudyne/resources/api_keys.py +83 -0
- simudyne_pulse-0.7.0.dev7/src/simudyne/resources/profile.py +103 -0
- simudyne_pulse-0.7.0.dev7/src/simudyne/resources/validation.py +346 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7/src/simudyne_pulse.egg-info}/PKG-INFO +1 -1
- simudyne_pulse-0.7.0.dev7/tests/test_validation.py +230 -0
- simudyne_pulse-0.7.0.dev5/src/simudyne/resources/api_keys.py +0 -12
- simudyne_pulse-0.7.0.dev5/src/simudyne/resources/profile.py +0 -30
- simudyne_pulse-0.7.0.dev5/src/simudyne/resources/validation.py +0 -712
- simudyne_pulse-0.7.0.dev5/tests/test_validation.py +0 -268
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/LICENSE +0 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/README.md +0 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/setup.cfg +0 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/src/simudyne/__init__.py +0 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/src/simudyne/client.py +0 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/src/simudyne/exceptions.py +0 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/src/simudyne/resources/__init__.py +0 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/src/simudyne/resources/data.py +0 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/src/simudyne/resources/fix.py +0 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/src/simudyne/resources/fm.py +0 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/src/simudyne/resources/historical.py +0 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/src/simudyne/resources/simulation.py +0 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/src/simudyne/resources/simulator_gym.py +0 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/src/simudyne_pulse.egg-info/SOURCES.txt +0 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/src/simudyne_pulse.egg-info/dependency_links.txt +0 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/src/simudyne_pulse.egg-info/requires.txt +0 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/src/simudyne_pulse.egg-info/top_level.txt +0 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/tests/test_new_resources.py +0 -0
- {simudyne_pulse-0.7.0.dev5 → simudyne_pulse-0.7.0.dev7}/tests/test_simulator_gym.py +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
class ApiKeysResource:
|
|
2
|
+
def __init__(self, client):
|
|
3
|
+
self._client = client
|
|
4
|
+
|
|
5
|
+
def create(self, name: str):
|
|
6
|
+
"""Mint a new API key.
|
|
7
|
+
|
|
8
|
+
The only call that ever returns the key material. It is shown once —
|
|
9
|
+
store it before the call returns, or mint another.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
name : str
|
|
14
|
+
A label for the key. Name it by purpose ("notebook",
|
|
15
|
+
"production", "CI pipeline") so you know which to revoke if one
|
|
16
|
+
is compromised.
|
|
17
|
+
|
|
18
|
+
Returns
|
|
19
|
+
-------
|
|
20
|
+
dict
|
|
21
|
+
api_key : str
|
|
22
|
+
The key material, shown exactly once.
|
|
23
|
+
api_key_id : str
|
|
24
|
+
The handle to pass to :meth:`revoke`.
|
|
25
|
+
warning : str
|
|
26
|
+
A reminder that the key will not be shown again.
|
|
27
|
+
|
|
28
|
+
Examples
|
|
29
|
+
--------
|
|
30
|
+
>>> new_key = client.api_keys.create(name="Training script")
|
|
31
|
+
>>> print(new_key["api_key"]) # save this, it won't be shown again
|
|
32
|
+
"""
|
|
33
|
+
return self._client._request("POST", "/api-keys", json={"name": name})
|
|
34
|
+
|
|
35
|
+
def list(self):
|
|
36
|
+
"""List your active API keys and their labels.
|
|
37
|
+
|
|
38
|
+
The full key is never returned after creation; only ``key_prefix``,
|
|
39
|
+
the first 12 characters, is stored. That is enough to tell keys apart
|
|
40
|
+
and not enough to reconstruct one.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
list of dict
|
|
45
|
+
One entry per key, each with:
|
|
46
|
+
|
|
47
|
+
- api_key_id (str): the handle to pass to :meth:`revoke`
|
|
48
|
+
- key_prefix (str): first 12 characters, e.g. "pk_live_ab12"
|
|
49
|
+
- name (str): the label you gave it
|
|
50
|
+
- is_active (bool): False once revoked
|
|
51
|
+
- created_at (str): when it was created
|
|
52
|
+
- last_used_at (str or None): None until the key is first used
|
|
53
|
+
- expires_at (str or None): None for keys that do not expire
|
|
54
|
+
|
|
55
|
+
Examples
|
|
56
|
+
--------
|
|
57
|
+
>>> for key in client.api_keys.list():
|
|
58
|
+
... print(key["name"], key["key_prefix"], key["created_at"])
|
|
59
|
+
"""
|
|
60
|
+
return self._client._request("GET", "/api-keys")
|
|
61
|
+
|
|
62
|
+
def revoke(self, key_id: str):
|
|
63
|
+
"""Invalidate a key immediately.
|
|
64
|
+
|
|
65
|
+
Any request still using the key returns HTTP 401 from this point on.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
key_id : str
|
|
70
|
+
The ``api_key_id`` from :meth:`list` or :meth:`create` — not the
|
|
71
|
+
key material itself.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
dict
|
|
76
|
+
The revocation result.
|
|
77
|
+
|
|
78
|
+
Examples
|
|
79
|
+
--------
|
|
80
|
+
>>> new_key = client.api_keys.create(name="Training script")
|
|
81
|
+
>>> client.api_keys.revoke(key_id=new_key["api_key_id"])
|
|
82
|
+
"""
|
|
83
|
+
return self._client._request("DELETE", f"/api-keys/{key_id}")
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
class ProfileResource:
|
|
2
|
+
def __init__(self, client):
|
|
3
|
+
self._client = client
|
|
4
|
+
|
|
5
|
+
def get(self):
|
|
6
|
+
"""Return your account record.
|
|
7
|
+
|
|
8
|
+
The cheapest way to confirm a key works and to find out which tier it
|
|
9
|
+
is on — worth calling first when a 403 surprises you.
|
|
10
|
+
|
|
11
|
+
Returns
|
|
12
|
+
-------
|
|
13
|
+
dict
|
|
14
|
+
user_id : str
|
|
15
|
+
Your account id. Also your FIX CompID, where FIX is enabled.
|
|
16
|
+
email : str
|
|
17
|
+
The address the account was created with.
|
|
18
|
+
name : str
|
|
19
|
+
Display name.
|
|
20
|
+
organization : str
|
|
21
|
+
Organisation, empty string if never set.
|
|
22
|
+
is_admin : bool
|
|
23
|
+
Administrative flag governing user administration, not
|
|
24
|
+
simulator access. It is not a tier bypass: an admin flag on a
|
|
25
|
+
free-tier key still cannot run a simulation. When a call
|
|
26
|
+
returns 403, read ``tier``, not this.
|
|
27
|
+
tier : {"free", "pro", "demo"}
|
|
28
|
+
What gates every endpoint.
|
|
29
|
+
download_limit : int
|
|
30
|
+
New simulation groups per rolling 24 hours. Only enforced on
|
|
31
|
+
the free tier.
|
|
32
|
+
|
|
33
|
+
Examples
|
|
34
|
+
--------
|
|
35
|
+
>>> me = client.profile.get()
|
|
36
|
+
>>> print(f"{me['email']} — tier {me['tier']}")
|
|
37
|
+
"""
|
|
38
|
+
return self._client._request("GET", "/profile")
|
|
39
|
+
|
|
40
|
+
def usage(self):
|
|
41
|
+
"""Return your request count for the current calendar month.
|
|
42
|
+
|
|
43
|
+
Resets on the 1st, and counts *requests* rather than simulations —
|
|
44
|
+
one ``run()`` of 100 Monte Carlo runs is a single request here.
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
dict
|
|
49
|
+
user_id : str
|
|
50
|
+
Your account id.
|
|
51
|
+
month : str
|
|
52
|
+
The calendar month being reported, "YYYY-MM".
|
|
53
|
+
total_requests : int
|
|
54
|
+
Requests so far this month.
|
|
55
|
+
by_endpoint : dict
|
|
56
|
+
Request count keyed by endpoint path. The keys are the
|
|
57
|
+
concrete paths that were called, so a job-scoped endpoint
|
|
58
|
+
appears once per job id rather than as a template — aggregate
|
|
59
|
+
by prefix if you want per-endpoint totals.
|
|
60
|
+
|
|
61
|
+
Examples
|
|
62
|
+
--------
|
|
63
|
+
>>> usage = client.profile.usage()
|
|
64
|
+
>>> print(f"{usage['month']}: {usage['total_requests']} requests")
|
|
65
|
+
"""
|
|
66
|
+
return self._client._request("GET", "/profile/usage")
|
|
67
|
+
|
|
68
|
+
def downloads(self):
|
|
69
|
+
"""Return the free-tier download allowance for the current window.
|
|
70
|
+
|
|
71
|
+
The unit is a simulation group — every Monte Carlo run of one
|
|
72
|
+
symbol/date/scenario — not a call, a run, or a file. Membership is
|
|
73
|
+
permanent: a group you have downloaded before is free to re-fetch
|
|
74
|
+
forever, in any run or file format.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
dict
|
|
79
|
+
limit : int or None
|
|
80
|
+
Groups per window. None on the pro and demo tiers, meaning
|
|
81
|
+
unlimited.
|
|
82
|
+
used : int
|
|
83
|
+
New groups added in the current window.
|
|
84
|
+
remaining : int or None
|
|
85
|
+
``limit`` minus ``used``, floored at 0. None when unlimited.
|
|
86
|
+
window_hours : int
|
|
87
|
+
Length of the rolling window, 24.
|
|
88
|
+
downloaded_groups : list of str or None
|
|
89
|
+
Every group you have ever downloaded. None when unlimited.
|
|
90
|
+
downloaded_group_details : list of dict or None
|
|
91
|
+
The same groups, each with a ``downloaded_at`` timestamp.
|
|
92
|
+
|
|
93
|
+
Notes
|
|
94
|
+
-----
|
|
95
|
+
A request that would exceed the allowance returns HTTP 429 without
|
|
96
|
+
charging anything, so a partial download never silently eats quota.
|
|
97
|
+
|
|
98
|
+
Examples
|
|
99
|
+
--------
|
|
100
|
+
>>> quota = client.profile.downloads()
|
|
101
|
+
>>> print(f"{quota['used']}/{quota['limit']} used, {quota['remaining']} left")
|
|
102
|
+
"""
|
|
103
|
+
return self._client._request("GET", "/profile/downloads")
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validation Resource for the Pulse SDK.
|
|
3
|
+
|
|
4
|
+
Compares simulated LOB data against a historical day using distributional
|
|
5
|
+
metrics, impact response, Cont's stylised facts, cross-level volume
|
|
6
|
+
correlation, and the MIND/FID inception distances on DeepLOB embeddings.
|
|
7
|
+
|
|
8
|
+
Submit, then poll::
|
|
9
|
+
|
|
10
|
+
job = client.validation.run(
|
|
11
|
+
symbol="BARC", date="2026-06-17", provider="bmll", exchange="lse",
|
|
12
|
+
sim_ids=[...], ticksize=0.05,
|
|
13
|
+
)
|
|
14
|
+
result = client.validation.get_job(job["job_id"])
|
|
15
|
+
result["distances"]["spread"]["l1"] # per simulation
|
|
16
|
+
result["stylised_fact_verdicts"]["heavy_tails"]
|
|
17
|
+
|
|
18
|
+
``run`` submits and returns; a validation of a real day takes minutes, so
|
|
19
|
+
``get_job`` is the status endpoint you call until it is done. ``list_jobs``
|
|
20
|
+
reaches past runs.
|
|
21
|
+
|
|
22
|
+
Selecting metrics
|
|
23
|
+
-----------------
|
|
24
|
+
One flag per metric — ``statistical``, ``stylised_facts``, ``impact``,
|
|
25
|
+
``volume_correlation``, ``fid``, ``mind``. Left as ``None`` each takes the
|
|
26
|
+
default for your tier, resolved server-side, so naming none behaves exactly as
|
|
27
|
+
your account is entitled to. Pass ``False`` to skip an expensive pass or
|
|
28
|
+
``True`` to force one on.
|
|
29
|
+
|
|
30
|
+
The historical gate
|
|
31
|
+
-------------------
|
|
32
|
+
Whether the historical half of the comparison comes back — the series in the
|
|
33
|
+
JSON *and* the historical trace on any rendered plot — is decided by your tier,
|
|
34
|
+
not by a parameter here. What is never gated: the L1 and Wasserstein
|
|
35
|
+
``distances``, the FID and MIND scores, and the true/false
|
|
36
|
+
``stylised_fact_verdicts`` for both the historical day and each simulation.
|
|
37
|
+
Those are derived scalars and booleans, not series, so every tier receives
|
|
38
|
+
them.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
import json
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
RUN_PATH = "/validation/run"
|
|
46
|
+
UPLOAD_PATH = "/validation/run/upload"
|
|
47
|
+
JOBS_PATH = "/validation/jobs"
|
|
48
|
+
|
|
49
|
+
#: The API rejects more per job; checked client-side so a 26-file submission
|
|
50
|
+
#: fails before any bytes are uploaded.
|
|
51
|
+
MAX_SIM_FILES = 25
|
|
52
|
+
|
|
53
|
+
#: One flag per area of checking. Left unset each takes the server's default.
|
|
54
|
+
_AREA_FLAGS = (
|
|
55
|
+
"statistical",
|
|
56
|
+
"stylised_facts",
|
|
57
|
+
"impact",
|
|
58
|
+
"volume_correlation",
|
|
59
|
+
"fid",
|
|
60
|
+
"mind",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
#: ``lob`` marks the frames as L2 snapshots, which switches off anything
|
|
64
|
+
#: needing the message stream; ``sample_period`` and ``match_generated_sample``
|
|
65
|
+
#: set the grid the book is resampled onto; ``plots`` is False, True, or a list
|
|
66
|
+
#: of plot ids.
|
|
67
|
+
_EXTRA_FIELDS = ("lob", "sample_period", "match_generated_sample", "plots")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _frame_to_parquet(entry, index: int):
|
|
71
|
+
"""Normalise one simulated run to ``(filename, parquet bytes)``.
|
|
72
|
+
|
|
73
|
+
Accepts a path, a ``(filename, bytes)`` pair, or a polars / pandas
|
|
74
|
+
DataFrame, which is written to parquet in memory. The frame must be pulse
|
|
75
|
+
format -- the same shape the engine writes to ``sim_data.parquet`` -- which
|
|
76
|
+
the server validates; sending something else fails there, not here.
|
|
77
|
+
"""
|
|
78
|
+
import io
|
|
79
|
+
|
|
80
|
+
if isinstance(entry, tuple):
|
|
81
|
+
return entry
|
|
82
|
+
|
|
83
|
+
# Duck-typed rather than imported: neither polars nor pandas is a hard
|
|
84
|
+
# dependency of the SDK, and importing one to test for the other would
|
|
85
|
+
# make it one.
|
|
86
|
+
writer = getattr(entry, "write_parquet", None) # polars
|
|
87
|
+
if writer is not None:
|
|
88
|
+
buf = io.BytesIO()
|
|
89
|
+
writer(buf)
|
|
90
|
+
return f"sim_{index}.parquet", buf.getvalue()
|
|
91
|
+
|
|
92
|
+
writer = getattr(entry, "to_parquet", None) # pandas
|
|
93
|
+
if writer is not None:
|
|
94
|
+
buf = io.BytesIO()
|
|
95
|
+
writer(buf, index=False)
|
|
96
|
+
return f"sim_{index}.parquet", buf.getvalue()
|
|
97
|
+
|
|
98
|
+
path = Path(entry)
|
|
99
|
+
return path.name, path.read_bytes()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _build_config(n_levels, **flags) -> dict:
|
|
103
|
+
"""The validation config as the API expects it.
|
|
104
|
+
|
|
105
|
+
Flags left as ``None`` are omitted rather than sent as null: the API reads
|
|
106
|
+
absence as "use my tier's default", and an explicit null would not do that.
|
|
107
|
+
"""
|
|
108
|
+
config = {"n_levels": n_levels}
|
|
109
|
+
for name in _AREA_FLAGS + _EXTRA_FIELDS:
|
|
110
|
+
value = flags.get(name)
|
|
111
|
+
if value is not None:
|
|
112
|
+
config[name] = value
|
|
113
|
+
|
|
114
|
+
unknown = set(flags) - set(_AREA_FLAGS) - set(_EXTRA_FIELDS)
|
|
115
|
+
if unknown:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"Unknown validation option(s): {sorted(unknown)}. "
|
|
118
|
+
f"Valid: {sorted(_AREA_FLAGS + _EXTRA_FIELDS)}"
|
|
119
|
+
)
|
|
120
|
+
return config
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _save_plots(result: dict, plot_dir=None) -> list:
|
|
124
|
+
"""Write the returned figures to disk and return the paths written.
|
|
125
|
+
|
|
126
|
+
The API sends rendered plots base64-encoded in the response. Left to
|
|
127
|
+
itself that is bytes you cannot look at, so when plots were asked for they
|
|
128
|
+
are written out: to ``plot_dir`` when given, otherwise the current
|
|
129
|
+
directory. The directory is created if it does not exist.
|
|
130
|
+
"""
|
|
131
|
+
import base64
|
|
132
|
+
|
|
133
|
+
target = Path(plot_dir) if plot_dir is not None else Path.cwd()
|
|
134
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
|
|
136
|
+
written = []
|
|
137
|
+
for category, entries in (result.get("plots") or {}).items():
|
|
138
|
+
for entry in entries or []:
|
|
139
|
+
content = entry.get("content_base64")
|
|
140
|
+
if not content:
|
|
141
|
+
continue
|
|
142
|
+
path = target / f"{entry.get('name') or category}.png"
|
|
143
|
+
path.write_bytes(base64.b64decode(content))
|
|
144
|
+
written.append(str(path))
|
|
145
|
+
return written
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ValidationResource:
|
|
149
|
+
"""Validate simulated market data against a historical day."""
|
|
150
|
+
|
|
151
|
+
def __init__(self, client):
|
|
152
|
+
self._client = client
|
|
153
|
+
|
|
154
|
+
def run(
|
|
155
|
+
self,
|
|
156
|
+
symbol: str,
|
|
157
|
+
date: str,
|
|
158
|
+
provider: str,
|
|
159
|
+
exchange: str,
|
|
160
|
+
*,
|
|
161
|
+
sim_ids=None,
|
|
162
|
+
sim_files=None,
|
|
163
|
+
ticksize: float = 1.0,
|
|
164
|
+
statistical=None,
|
|
165
|
+
stylised_facts=None,
|
|
166
|
+
impact=None,
|
|
167
|
+
volume_correlation=None,
|
|
168
|
+
fid=None,
|
|
169
|
+
mind=None,
|
|
170
|
+
lob=None,
|
|
171
|
+
sample_period=None,
|
|
172
|
+
match_generated_sample=None,
|
|
173
|
+
n_levels: int = 10,
|
|
174
|
+
plots=None,
|
|
175
|
+
) -> dict:
|
|
176
|
+
"""Submit a validation job.
|
|
177
|
+
|
|
178
|
+
Returns as soon as the job is accepted. Poll :meth:`get_job` for
|
|
179
|
+
status and, once it is complete, the result — a validation of a real
|
|
180
|
+
day takes minutes, which is far too long to hold a request open.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
symbol : str
|
|
185
|
+
Trading symbol, e.g. ``"BARC"`` or ``"700.HK"``.
|
|
186
|
+
date : str
|
|
187
|
+
Calibration date, ``"YYYY-MM-DD"``. The historical day compared
|
|
188
|
+
against.
|
|
189
|
+
provider : str
|
|
190
|
+
Data provider, ``"bmll"`` or ``"omd"``. Required: the same symbol
|
|
191
|
+
exists under both with different dates and tick sizes, so
|
|
192
|
+
``(symbol, provider, exchange)`` is the identity, not the symbol.
|
|
193
|
+
exchange : str
|
|
194
|
+
Exchange, e.g. ``"lse"`` or ``"hkex_securities"``.
|
|
195
|
+
sim_ids : list of str, optional
|
|
196
|
+
Platform simulation IDs, 1 to 25. Exactly one of ``sim_ids`` or
|
|
197
|
+
``sim_files`` is required.
|
|
198
|
+
sim_files : list, optional
|
|
199
|
+
Your own simulated runs, up to 25: paths, ``(filename, bytes)``
|
|
200
|
+
pairs, or polars / pandas DataFrames in pulse format. The
|
|
201
|
+
historical side is fetched for you.
|
|
202
|
+
ticksize : float, default 1.0
|
|
203
|
+
Minimum price increment. Should match the instrument.
|
|
204
|
+
statistical, stylised_facts, impact, volume_correlation, fid, mind : bool, optional
|
|
205
|
+
Whether to run each area. ``None`` uses your tier's default.
|
|
206
|
+
lob : bool, optional
|
|
207
|
+
The frames are L2 snapshots. Anything needing the message stream
|
|
208
|
+
is skipped, with the reason reported in
|
|
209
|
+
``metadata["areas_skipped"]``.
|
|
210
|
+
sample_period : str, optional
|
|
211
|
+
The grid the book is resampled onto in lob mode, e.g. ``"1s"``.
|
|
212
|
+
Omitted, the cadence is read off your generated frames; if those
|
|
213
|
+
are event-level there is no grid to match and nothing is
|
|
214
|
+
resampled.
|
|
215
|
+
match_generated_sample : bool, optional
|
|
216
|
+
Match the generated frames' grid even when ``sample_period`` is
|
|
217
|
+
given.
|
|
218
|
+
n_levels : int, default 10
|
|
219
|
+
Book levels to measure over.
|
|
220
|
+
plots : bool or list of str, optional
|
|
221
|
+
``None`` or ``False`` for none, ``True`` for every figure, or plot
|
|
222
|
+
ids such as ``["statistical.radar", "stylised_facts.overall"]``.
|
|
223
|
+
Rendered server-side; :meth:`get_job` writes them to disk.
|
|
224
|
+
|
|
225
|
+
Returns
|
|
226
|
+
-------
|
|
227
|
+
dict
|
|
228
|
+
``{"job_id": str, "status": str, "message": str}``. Pass the
|
|
229
|
+
job_id to :meth:`get_job`.
|
|
230
|
+
|
|
231
|
+
Raises
|
|
232
|
+
------
|
|
233
|
+
ValueError
|
|
234
|
+
If the parameters are invalid.
|
|
235
|
+
"""
|
|
236
|
+
if (sim_ids is None) == (sim_files is None):
|
|
237
|
+
raise ValueError("Pass exactly one of sim_ids or sim_files")
|
|
238
|
+
if sim_ids is not None and not sim_ids:
|
|
239
|
+
raise ValueError("sim_ids must not be empty")
|
|
240
|
+
if sim_files is not None and not sim_files:
|
|
241
|
+
raise ValueError("sim_files must not be empty")
|
|
242
|
+
count = len(sim_ids if sim_ids is not None else sim_files)
|
|
243
|
+
if count > MAX_SIM_FILES:
|
|
244
|
+
raise ValueError(f"Maximum {MAX_SIM_FILES} simulations per validation job")
|
|
245
|
+
if ticksize <= 0:
|
|
246
|
+
raise ValueError("ticksize must be positive")
|
|
247
|
+
if n_levels < 1:
|
|
248
|
+
raise ValueError("n_levels must be at least 1")
|
|
249
|
+
|
|
250
|
+
config = _build_config(
|
|
251
|
+
n_levels,
|
|
252
|
+
statistical=statistical,
|
|
253
|
+
stylised_facts=stylised_facts,
|
|
254
|
+
impact=impact,
|
|
255
|
+
volume_correlation=volume_correlation,
|
|
256
|
+
fid=fid,
|
|
257
|
+
mind=mind,
|
|
258
|
+
lob=lob,
|
|
259
|
+
sample_period=sample_period,
|
|
260
|
+
match_generated_sample=match_generated_sample,
|
|
261
|
+
plots=plots,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if sim_ids is not None:
|
|
265
|
+
submitted = self._client._request(
|
|
266
|
+
"POST",
|
|
267
|
+
RUN_PATH,
|
|
268
|
+
json={
|
|
269
|
+
"symbol": symbol,
|
|
270
|
+
"date": date,
|
|
271
|
+
"provider": provider,
|
|
272
|
+
"exchange": exchange,
|
|
273
|
+
"sim_ids": list(sim_ids),
|
|
274
|
+
"ticksize": ticksize,
|
|
275
|
+
"config": config,
|
|
276
|
+
},
|
|
277
|
+
)
|
|
278
|
+
else:
|
|
279
|
+
files = [
|
|
280
|
+
("sim_files", _frame_to_parquet(entry, i))
|
|
281
|
+
for i, entry in enumerate(sim_files)
|
|
282
|
+
]
|
|
283
|
+
submitted = self._client._request(
|
|
284
|
+
"POST",
|
|
285
|
+
UPLOAD_PATH,
|
|
286
|
+
files=files,
|
|
287
|
+
data={
|
|
288
|
+
"symbol": symbol,
|
|
289
|
+
"date": date,
|
|
290
|
+
"provider": provider,
|
|
291
|
+
"exchange": exchange,
|
|
292
|
+
"ticksize": str(ticksize),
|
|
293
|
+
"config": json.dumps(config),
|
|
294
|
+
},
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if not (submitted or {}).get("job_id"):
|
|
298
|
+
raise ValueError(f"Validation API returned no job_id: {submitted}")
|
|
299
|
+
return submitted
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def get_job(self, job_id: str, plot_dir=None) -> dict:
|
|
303
|
+
"""Fetch a validation job's status, and its result once it is done.
|
|
304
|
+
|
|
305
|
+
This is the status endpoint: call it until ``status`` is
|
|
306
|
+
``"completed"`` or ``"failed"``.
|
|
307
|
+
|
|
308
|
+
Parameters
|
|
309
|
+
----------
|
|
310
|
+
job_id : str
|
|
311
|
+
From :meth:`run`.
|
|
312
|
+
plot_dir : str or Path, optional
|
|
313
|
+
Where to write any figures the job rendered. Defaults to the
|
|
314
|
+
current directory; created if it does not exist. Only has an
|
|
315
|
+
effect once the job is complete and only if ``plots`` was set on
|
|
316
|
+
the run. The paths written come back in ``plot_paths``.
|
|
317
|
+
|
|
318
|
+
Returns
|
|
319
|
+
-------
|
|
320
|
+
dict
|
|
321
|
+
``job_id``, ``status``, ``symbol``, ``date`` and, once complete:
|
|
322
|
+
``metadata``, ``distances``, ``distributions``,
|
|
323
|
+
``impact_response``, ``stylised_facts``,
|
|
324
|
+
``stylised_fact_verdicts``, ``volume_correlation``,
|
|
325
|
+
``fid_scores``, ``mind_scores`` — enough to rebuild every figure —
|
|
326
|
+
plus ``plot_paths`` when figures were written.
|
|
327
|
+
"""
|
|
328
|
+
job = self._client._request("GET", f"{JOBS_PATH}/{job_id}")
|
|
329
|
+
if (job or {}).get("plots"):
|
|
330
|
+
job["plot_paths"] = _save_plots(job, plot_dir)
|
|
331
|
+
return job
|
|
332
|
+
|
|
333
|
+
def list_jobs(self, limit: int = 50) -> dict:
|
|
334
|
+
"""List your validation jobs, newest first.
|
|
335
|
+
|
|
336
|
+
Parameters
|
|
337
|
+
----------
|
|
338
|
+
limit : int, default 50
|
|
339
|
+
Maximum number of jobs to return.
|
|
340
|
+
|
|
341
|
+
Returns
|
|
342
|
+
-------
|
|
343
|
+
dict
|
|
344
|
+
``{"jobs": list, "total": int}``.
|
|
345
|
+
"""
|
|
346
|
+
return self._client._request("GET", JOBS_PATH, params={"limit": limit})
|