simudyne-pulse 0.6.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Simudyne Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: simudyne-pulse
3
+ Version: 0.6.0
4
+ Summary: Python SDK for the Simudyne Pulse synthetic market data API
5
+ Author-email: Simudyne <support@simudyne.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://pulse.simudyne.com
8
+ Project-URL: Documentation, https://pulse.simudyne.com/docs
9
+ Project-URL: Repository, https://github.com/simudyne/pulse-api
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: requests>=2.28.0
20
+ Requires-Dist: polars>=0.20.0
21
+ Requires-Dist: tqdm>=4.60.0
22
+ Requires-Dist: websocket-client>=1.0.0
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest>=8.0.0; extra == "test"
25
+ Dynamic: license-file
26
+
27
+ # Simudyne Pulse Python SDK
28
+
29
+ Python client for the [Pulse](https://pulse.simudyne.com) synthetic market data API. Returns data as [Polars](https://pola.rs/) DataFrames.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install simudyne-pulse
35
+ ```
36
+
37
+ The distribution is named `simudyne-pulse`; the import name is `simudyne`:
38
+
39
+ ```python
40
+ from simudyne import PulseABM
41
+ ```
42
+
43
+ Requires Python 3.10+.
44
+
45
+ ### Development builds
46
+
47
+ The `dev` branch is a prerelease channel. Pushes to it publish prerelease
48
+ versions (e.g. `0.6.0.dev1`) that are separate from the stable versions cut on
49
+ `prod`. `pip install simudyne-pulse` always resolves to the latest **stable**
50
+ release and ignores prereleases, so dev builds can never affect a normal
51
+ install.
52
+
53
+ To install the latest dev build, opt in with `--pre`:
54
+
55
+ ```bash
56
+ pip install --pre simudyne-pulse
57
+ ```
58
+
59
+ Only use dev builds for testing unreleased changes; they are not guaranteed
60
+ stable. Merge `dev` into `prod` to promote those changes to a stable release.
61
+
62
+ ## Quick start
63
+
64
+ ```python
65
+ from simudyne import PulseABM
66
+
67
+ client = PulseABM(api_key="pk_live_...")
68
+
69
+ # List available exchanges, symbols, and dates
70
+ symbols = client.data.get_symbols(year=2024)
71
+ print(symbols)
72
+
73
+ # Fetch L2 order book data
74
+ df = client.data.get_L2("HKEX", "HSIJ4", "2024-04-02T09:15:00", "2024-04-02T09:16:00")
75
+ print(df.head())
76
+ ```
77
+
78
+ ## API reference
79
+
80
+ ### `PulseABM(api_key, base_url=None)`
81
+
82
+ | Parameter | Env variable | Default |
83
+ |-----------|-------------|---------|
84
+ | `api_key` | `SIMUDYNE_API_KEY` | required |
85
+ | `base_url` | `SIMUDYNE_BASE_URL` | Pulse API |
86
+
87
+ ### `client.data`
88
+
89
+ All data methods return Polars DataFrames. Large result sets are automatically paginated.
90
+
91
+ ```python
92
+ # Available exchanges, symbols, and dates
93
+ client.data.get_symbols(year=2024)
94
+
95
+ # L1: top of book (best bid/ask)
96
+ client.data.get_L1("HKEX", "HSIJ4", "2024-04-02T09:15:00", "2024-04-02T10:00:00")
97
+
98
+ # L2: full order book (all levels)
99
+ client.data.get_L2("HKEX", "HSIJ4", "2024-04-02T09:15:00", "2024-04-02T09:16:00")
100
+
101
+ # Orders: individual order events
102
+ client.data.get_orders("HKEX", "HSIJ4", "2024-04-02T09:15:00", "2024-04-02T10:00:00")
103
+
104
+ # Trades: executed trades
105
+ client.data.get_trades("HKEX", "HSIJ4", "2024-04-02T09:15:00", "2024-04-02T10:00:00")
106
+ ```
107
+
108
+ **Parameters** (same for all data methods):
109
+
110
+ | Parameter | Type | Description |
111
+ |-----------|------|-------------|
112
+ | `exchange` | str | Exchange code (e.g. `HKEX`) |
113
+ | `sym` | str | Symbol name (e.g. `HSIJ4`) |
114
+ | `datetime_start` | str | Start time, ISO 8601 (e.g. `2024-04-02T09:15:00`) |
115
+ | `datetime_end` | str | End time, ISO 8601 |
116
+
117
+ ### `client.profile`
118
+
119
+ ```python
120
+ client.profile.get() # Account info
121
+ client.profile.usage() # API usage stats
122
+ ```
123
+
124
+ ### `client.api_keys`
125
+
126
+ ```python
127
+ client.api_keys.list() # List active keys
128
+ client.api_keys.create(name="research") # Create a new key
129
+ client.api_keys.revoke(key_id="key_...") # Revoke a key
130
+ ```
131
+
132
+ ## Configuration
133
+
134
+ You can set your API key as an environment variable instead of passing it directly:
135
+
136
+ ```bash
137
+ export SIMUDYNE_API_KEY=pk_live_...
138
+ ```
139
+
140
+ ```python
141
+ from simudyne import PulseABM
142
+ client = PulseABM() # picks up SIMUDYNE_API_KEY automatically
143
+ ```
144
+
145
+ ## License
146
+
147
+ MIT
@@ -0,0 +1,121 @@
1
+ # Simudyne Pulse Python SDK
2
+
3
+ Python client for the [Pulse](https://pulse.simudyne.com) synthetic market data API. Returns data as [Polars](https://pola.rs/) DataFrames.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install simudyne-pulse
9
+ ```
10
+
11
+ The distribution is named `simudyne-pulse`; the import name is `simudyne`:
12
+
13
+ ```python
14
+ from simudyne import PulseABM
15
+ ```
16
+
17
+ Requires Python 3.10+.
18
+
19
+ ### Development builds
20
+
21
+ The `dev` branch is a prerelease channel. Pushes to it publish prerelease
22
+ versions (e.g. `0.6.0.dev1`) that are separate from the stable versions cut on
23
+ `prod`. `pip install simudyne-pulse` always resolves to the latest **stable**
24
+ release and ignores prereleases, so dev builds can never affect a normal
25
+ install.
26
+
27
+ To install the latest dev build, opt in with `--pre`:
28
+
29
+ ```bash
30
+ pip install --pre simudyne-pulse
31
+ ```
32
+
33
+ Only use dev builds for testing unreleased changes; they are not guaranteed
34
+ stable. Merge `dev` into `prod` to promote those changes to a stable release.
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ from simudyne import PulseABM
40
+
41
+ client = PulseABM(api_key="pk_live_...")
42
+
43
+ # List available exchanges, symbols, and dates
44
+ symbols = client.data.get_symbols(year=2024)
45
+ print(symbols)
46
+
47
+ # Fetch L2 order book data
48
+ df = client.data.get_L2("HKEX", "HSIJ4", "2024-04-02T09:15:00", "2024-04-02T09:16:00")
49
+ print(df.head())
50
+ ```
51
+
52
+ ## API reference
53
+
54
+ ### `PulseABM(api_key, base_url=None)`
55
+
56
+ | Parameter | Env variable | Default |
57
+ |-----------|-------------|---------|
58
+ | `api_key` | `SIMUDYNE_API_KEY` | required |
59
+ | `base_url` | `SIMUDYNE_BASE_URL` | Pulse API |
60
+
61
+ ### `client.data`
62
+
63
+ All data methods return Polars DataFrames. Large result sets are automatically paginated.
64
+
65
+ ```python
66
+ # Available exchanges, symbols, and dates
67
+ client.data.get_symbols(year=2024)
68
+
69
+ # L1: top of book (best bid/ask)
70
+ client.data.get_L1("HKEX", "HSIJ4", "2024-04-02T09:15:00", "2024-04-02T10:00:00")
71
+
72
+ # L2: full order book (all levels)
73
+ client.data.get_L2("HKEX", "HSIJ4", "2024-04-02T09:15:00", "2024-04-02T09:16:00")
74
+
75
+ # Orders: individual order events
76
+ client.data.get_orders("HKEX", "HSIJ4", "2024-04-02T09:15:00", "2024-04-02T10:00:00")
77
+
78
+ # Trades: executed trades
79
+ client.data.get_trades("HKEX", "HSIJ4", "2024-04-02T09:15:00", "2024-04-02T10:00:00")
80
+ ```
81
+
82
+ **Parameters** (same for all data methods):
83
+
84
+ | Parameter | Type | Description |
85
+ |-----------|------|-------------|
86
+ | `exchange` | str | Exchange code (e.g. `HKEX`) |
87
+ | `sym` | str | Symbol name (e.g. `HSIJ4`) |
88
+ | `datetime_start` | str | Start time, ISO 8601 (e.g. `2024-04-02T09:15:00`) |
89
+ | `datetime_end` | str | End time, ISO 8601 |
90
+
91
+ ### `client.profile`
92
+
93
+ ```python
94
+ client.profile.get() # Account info
95
+ client.profile.usage() # API usage stats
96
+ ```
97
+
98
+ ### `client.api_keys`
99
+
100
+ ```python
101
+ client.api_keys.list() # List active keys
102
+ client.api_keys.create(name="research") # Create a new key
103
+ client.api_keys.revoke(key_id="key_...") # Revoke a key
104
+ ```
105
+
106
+ ## Configuration
107
+
108
+ You can set your API key as an environment variable instead of passing it directly:
109
+
110
+ ```bash
111
+ export SIMUDYNE_API_KEY=pk_live_...
112
+ ```
113
+
114
+ ```python
115
+ from simudyne import PulseABM
116
+ client = PulseABM() # picks up SIMUDYNE_API_KEY automatically
117
+ ```
118
+
119
+ ## License
120
+
121
+ MIT
@@ -0,0 +1,63 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "simudyne-pulse"
7
+ version = "0.6.0"
8
+ description = "Python SDK for the Simudyne Pulse synthetic market data API"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Simudyne", email = "support@simudyne.com" },
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Operating System :: OS Independent",
22
+ ]
23
+ dependencies = [
24
+ "requests>=2.28.0",
25
+ "polars>=0.20.0",
26
+ "tqdm>=4.60.0",
27
+ "websocket-client>=1.0.0",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ test = ["pytest>=8.0.0"]
32
+
33
+ [project.urls]
34
+ Homepage = "https://pulse.simudyne.com"
35
+ Documentation = "https://pulse.simudyne.com/docs"
36
+ Repository = "https://github.com/simudyne/pulse-api"
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+ [tool.setuptools.packages.find]
46
+ where = ["src"]
47
+
48
+ [tool.semantic_release]
49
+ version_toml = ["pyproject.toml:project.version"]
50
+ changelog_file = "CHANGELOG.md"
51
+ build_command = "pip install build && python -m build"
52
+
53
+ # Release channels. `prod` cuts stable versions (0.6.0); `dev` cuts
54
+ # prereleases (0.6.0-dev.1). Prerelease tags never collide with stable ones,
55
+ # and `pip install git+...@dev` is how you pull a dev build.
56
+ [tool.semantic_release.branches.prod]
57
+ match = "prod"
58
+ prerelease = false
59
+
60
+ [tool.semantic_release.branches.dev]
61
+ match = "dev"
62
+ prerelease = true
63
+ prerelease_token = "dev"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from simudyne.client import PulseABM, PulseAPIError
2
+
3
+ __all__ = ["PulseABM", "PulseAPIError"]
@@ -0,0 +1,121 @@
1
+ import os
2
+ import sys
3
+ import time
4
+ import requests
5
+ import io
6
+ import polars as pl
7
+ from tqdm import tqdm
8
+ import tempfile
9
+ import math
10
+
11
+ from simudyne.exceptions import PulseAPIError
12
+
13
+
14
+ class PulseABM:
15
+ DEFAULT_BASE_URL = "https://pulse-api.simudyne.com"
16
+ DEFAULT_TIMEOUT = 30
17
+ RETRYABLE_STATUS_CODES = {429, 502, 503, 504}
18
+
19
+ def __init__(
20
+ self,
21
+ api_key: str = None,
22
+ base_url: str = None,
23
+ timeout: int = None,
24
+ max_retries: int = 3,
25
+ ):
26
+ self.api_key = api_key or os.getenv("SIMUDYNE_API_KEY")
27
+ if not self.api_key:
28
+ raise ValueError("SIMUDYNE_API_KEY is not set -> pass api_key or set SIMUDYNE_API_KEY in environment")
29
+ self.base_url = base_url or os.getenv("SIMUDYNE_BASE_URL", self.DEFAULT_BASE_URL)
30
+ self.timeout = timeout or self.DEFAULT_TIMEOUT
31
+ self.max_retries = max_retries
32
+ self.session = requests.Session()
33
+ self.session.headers.update({"X-API-Key": self.api_key})
34
+ self.session.verify = True
35
+
36
+ from simudyne.resources.profile import ProfileResource
37
+ from simudyne.resources.api_keys import ApiKeysResource
38
+ from simudyne.resources.data import DataResource
39
+ from simudyne.resources.simulation import SimulationResource
40
+ from simudyne.resources.simulator_gym import SimulatorGymResource
41
+ from simudyne.resources.validation import ValidationResource
42
+
43
+ self.profile = ProfileResource(self)
44
+ self.api_keys = ApiKeysResource(self)
45
+ self.data = DataResource(self)
46
+ self.simulation = SimulationResource(self)
47
+ self.simulator_gym = SimulatorGymResource(self)
48
+ self.validation = ValidationResource(self)
49
+
50
+ def _request_with_retries(self, method: str, url: str, **kwargs):
51
+ """Execute request with timeout and exponential backoff for transient errors."""
52
+ kwargs.setdefault("timeout", self.timeout)
53
+ last_exception = None
54
+
55
+ for attempt in range(self.max_retries + 1):
56
+ try:
57
+ response = self.session.request(method, url, **kwargs)
58
+
59
+ if response.ok:
60
+ return response
61
+
62
+ if response.status_code in self.RETRYABLE_STATUS_CODES and attempt < self.max_retries:
63
+ delay = 2 ** attempt
64
+ time.sleep(delay)
65
+ continue
66
+
67
+ try:
68
+ detail = response.json().get("detail", response.text)
69
+ except ValueError:
70
+ detail = response.text
71
+ raise PulseAPIError(response.status_code, detail)
72
+
73
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
74
+ last_exception = e
75
+ if attempt < self.max_retries:
76
+ delay = 2 ** attempt
77
+ time.sleep(delay)
78
+ continue
79
+ raise
80
+
81
+ raise last_exception
82
+
83
+ def _request(self, method: str, endpoint: str, **kwargs):
84
+ url = f"{self.base_url}{endpoint}"
85
+ response = self._request_with_retries(method, url, **kwargs)
86
+ return response.json()
87
+
88
+ def _request_csv(self, method, endpoint, **kwargs):
89
+ PAGE_SIZE = 100000
90
+ params = kwargs.get("params", {})
91
+
92
+ params["limit"] = PAGE_SIZE
93
+ params["offset"] = 0
94
+ kwargs["params"] = params
95
+
96
+ url = f"{self.base_url}{endpoint}"
97
+ response = self._request_with_retries(method, url, **kwargs)
98
+
99
+ total_rows = int(response.headers.get("X-Total-Rows", 0))
100
+ total_pages = math.ceil(total_rows / PAGE_SIZE) if total_rows > 0 else 1
101
+
102
+ frames = []
103
+ df = pl.read_csv(response.text.encode(), infer_schema_length=10000)
104
+ frames.append(df)
105
+ schema = df.schema
106
+
107
+ if total_pages > 1:
108
+ with tqdm(total=total_pages, initial=1, unit="page", desc=f"Downloading ({total_rows:,} rows)") as pbar:
109
+ for page in range(1, total_pages):
110
+ params["offset"] = page * PAGE_SIZE
111
+ kwargs["params"] = params
112
+ response = self._request_with_retries(method, url, **kwargs)
113
+ df = pl.read_csv(response.text.encode(), schema_overrides=schema, infer_schema_length=10000)
114
+ if df.is_empty():
115
+ break
116
+ frames.append(df)
117
+ pbar.update(1)
118
+
119
+ result = pl.concat(frames) if len(frames) > 1 else frames[0]
120
+ print(f"Done: {result.shape[0]:,} rows, {result.shape[1]} columns", file=sys.stderr)
121
+ return result
@@ -0,0 +1,7 @@
1
+ class PulseAPIError(Exception):
2
+ """Exception raised for Pulse API errors."""
3
+
4
+ def __init__(self, status_code: int, detail: str):
5
+ self.status_code = status_code
6
+ self.detail = detail
7
+ super().__init__(f"API Error ({status_code}): {detail}")
@@ -0,0 +1,12 @@
1
+ class ApiKeysResource:
2
+ def __init__(self, client):
3
+ self._client = client
4
+
5
+ def create(self, name: str):
6
+ return self._client._request("POST", "/api-keys", json={"name": name})
7
+
8
+ def list(self):
9
+ return self._client._request("GET", "/api-keys")
10
+
11
+ def revoke(self, key_id: str):
12
+ return self._client._request("DELETE", f"/api-keys/{key_id}")
@@ -0,0 +1,45 @@
1
+ AVAILABLE_SYMBOLS_PATH = "/data/available-symbols"
2
+
3
+
4
+ class DataResource:
5
+ def __init__(self, client):
6
+ self._client = client
7
+
8
+ def get_available_symbols(
9
+ self,
10
+ symbol: str | None = None,
11
+ exchange: str | None = None,
12
+ provider: str | None = None,
13
+ date: str | None = None,
14
+ limit: int | None = None,
15
+ offset: int | None = None,
16
+ ):
17
+ """List calibrated instruments and the dates available for each.
18
+
19
+ All arguments are optional filters; with none passed the whole catalog
20
+ comes back, as before.
21
+
22
+ Args:
23
+ symbol: Exact symbol, e.g. "700.HK".
24
+ exchange: Exchange protocol, e.g. "hkex_securities".
25
+ provider: Data provider, e.g. "omd" or "bmll".
26
+ date: Calibration date "YYYY-MM-DD". Keeps only instruments
27
+ calibrated on that date, and narrows each instrument's
28
+ available_dates to it.
29
+ limit: Max instruments to return.
30
+ offset: Instruments to skip (for paging alongside limit).
31
+
32
+ Returns:
33
+ list of instrument dicts, each with available_dates.
34
+ """
35
+ params = {
36
+ k: v for k, v in {
37
+ "symbol": symbol,
38
+ "exchange": exchange,
39
+ "provider": provider,
40
+ "date": date,
41
+ "limit": limit,
42
+ "offset": offset,
43
+ }.items() if v is not None
44
+ }
45
+ return self._client._request("GET", AVAILABLE_SYMBOLS_PATH, params=params or None)
@@ -0,0 +1 @@
1
+ # Historical endpoints have been removed. Use data.get_available() instead.
@@ -0,0 +1,30 @@
1
+ class ProfileResource:
2
+ def __init__(self, client):
3
+ self._client = client
4
+
5
+ def get(self):
6
+ return self._client._request("GET", "/profile")
7
+
8
+ def usage(self):
9
+ return self._client._request("GET", "/profile/usage")
10
+
11
+ def downloads(self):
12
+ """Download quota usage for the current rolling 24-hour window.
13
+
14
+ Returns a dict with ``limit``, ``used``, ``remaining``,
15
+ ``window_hours`` and ``downloaded_groups``. ``limit`` and
16
+ ``remaining`` are ``None`` on the pro and demo tiers (unlimited); on
17
+ the free tier they reflect the account's daily download allowance.
18
+
19
+ A "download" is one simulation group (all Monte Carlo runs of one
20
+ scenario). ``used`` counts NEW groups added in the window;
21
+ ``downloaded_groups`` lists every group you have ever downloaded —
22
+ membership is permanent, so those are free to re-fetch forever in any
23
+ run or file format.
24
+
25
+ Example:
26
+ >>> quota = client.profile.downloads()
27
+ >>> print(f"{quota['used']}/{quota['limit']} used, {quota['remaining']} left")
28
+ >>> print(f"{len(quota['downloaded_groups'])} groups owned")
29
+ """
30
+ return self._client._request("GET", "/profile/downloads")