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.
- simudyne_pulse-0.6.0/LICENSE +21 -0
- simudyne_pulse-0.6.0/PKG-INFO +147 -0
- simudyne_pulse-0.6.0/README.md +121 -0
- simudyne_pulse-0.6.0/pyproject.toml +63 -0
- simudyne_pulse-0.6.0/setup.cfg +4 -0
- simudyne_pulse-0.6.0/src/simudyne/__init__.py +3 -0
- simudyne_pulse-0.6.0/src/simudyne/client.py +121 -0
- simudyne_pulse-0.6.0/src/simudyne/exceptions.py +7 -0
- simudyne_pulse-0.6.0/src/simudyne/resources/__init__.py +0 -0
- simudyne_pulse-0.6.0/src/simudyne/resources/api_keys.py +12 -0
- simudyne_pulse-0.6.0/src/simudyne/resources/data.py +45 -0
- simudyne_pulse-0.6.0/src/simudyne/resources/historical.py +1 -0
- simudyne_pulse-0.6.0/src/simudyne/resources/profile.py +30 -0
- simudyne_pulse-0.6.0/src/simudyne/resources/simulation.py +717 -0
- simudyne_pulse-0.6.0/src/simudyne/resources/simulator_gym.py +118 -0
- simudyne_pulse-0.6.0/src/simudyne/resources/validation.py +234 -0
- simudyne_pulse-0.6.0/src/simudyne_pulse.egg-info/PKG-INFO +147 -0
- simudyne_pulse-0.6.0/src/simudyne_pulse.egg-info/SOURCES.txt +20 -0
- simudyne_pulse-0.6.0/src/simudyne_pulse.egg-info/dependency_links.txt +1 -0
- simudyne_pulse-0.6.0/src/simudyne_pulse.egg-info/requires.txt +7 -0
- simudyne_pulse-0.6.0/src/simudyne_pulse.egg-info/top_level.txt +1 -0
- simudyne_pulse-0.6.0/tests/test_simulator_gym.py +172 -0
|
@@ -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,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
|
|
File without changes
|
|
@@ -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")
|