volt-client 1.0.0__py3-none-any.whl
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.
volt_client/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Volt Client - Python client for Volt Power Analytics API.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from volt_client import VoltClient
|
|
6
|
+
|
|
7
|
+
client = VoltClient(api_key="volt_your_api_key")
|
|
8
|
+
df = client.get_actual("price spot no1 nordpool eur mwh min60 actual", "2024-01-01", "2024-12-31")
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from volt_client.client import (
|
|
12
|
+
DataFrame,
|
|
13
|
+
VoltAccessError,
|
|
14
|
+
VoltClient,
|
|
15
|
+
VoltClientError,
|
|
16
|
+
VoltCurveNotFoundError,
|
|
17
|
+
VoltEmptyDataError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__version__ = "1.0.0"
|
|
21
|
+
__all__ = [
|
|
22
|
+
"VoltClient",
|
|
23
|
+
"VoltClientError",
|
|
24
|
+
"VoltAccessError",
|
|
25
|
+
"VoltCurveNotFoundError",
|
|
26
|
+
"VoltEmptyDataError",
|
|
27
|
+
"DataFrame",
|
|
28
|
+
]
|
volt_client/client.py
ADDED
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Volt Client for accessing Volt Power Analytics time series data API.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from volt_client import VoltClient
|
|
6
|
+
|
|
7
|
+
# Recommended: Use API key (generated from dashboard)
|
|
8
|
+
client = VoltClient(api_key="volt_abc123...")
|
|
9
|
+
df = client.get_actual("price power no1 nordpool eur min60 actual", "2024-01-01", "2024-12-31")
|
|
10
|
+
|
|
11
|
+
# For better performance with large datasets, use polars:
|
|
12
|
+
client = VoltClient(api_key="volt_abc123...", use_polars=True)
|
|
13
|
+
df = client.get_actual(...) # Returns polars DataFrame
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
import orjson
|
|
22
|
+
import polars as pl
|
|
23
|
+
import requests
|
|
24
|
+
from requests.adapters import HTTPAdapter
|
|
25
|
+
from urllib3.util.retry import Retry
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
import pandas as pd
|
|
29
|
+
|
|
30
|
+
# Type alias for DataFrame return type
|
|
31
|
+
DataFrame = pl.DataFrame
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# Default API URL
|
|
36
|
+
DEFAULT_API_URL = "https://volt-api-gateway.azure-api.net/volt"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class VoltClientError(Exception):
|
|
40
|
+
"""Base exception for VoltClient errors."""
|
|
41
|
+
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class VoltAccessError(VoltClientError):
|
|
46
|
+
"""Raised when access is denied."""
|
|
47
|
+
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class VoltCurveNotFoundError(VoltClientError):
|
|
52
|
+
"""Raised when a curve is not found."""
|
|
53
|
+
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class VoltEmptyDataError(VoltClientError):
|
|
58
|
+
"""Raised when no data is returned."""
|
|
59
|
+
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class VoltClient:
|
|
64
|
+
"""
|
|
65
|
+
Client for accessing Volt Power Analytics time series data API.
|
|
66
|
+
|
|
67
|
+
Internally uses polars for all data processing (10x faster than pandas).
|
|
68
|
+
By default returns pandas DataFrames for backwards compatibility.
|
|
69
|
+
|
|
70
|
+
Parameters:
|
|
71
|
+
-----------
|
|
72
|
+
api_key : str
|
|
73
|
+
API key for authentication (required, generated from dashboard).
|
|
74
|
+
Example: "volt_abc123..."
|
|
75
|
+
base_url : str, optional
|
|
76
|
+
Base URL for the API (default: https://volt-api-gateway.azure-api.net/volt)
|
|
77
|
+
use_polars : bool, optional
|
|
78
|
+
If True, return polars DataFrames instead of pandas.
|
|
79
|
+
Default: False (returns pandas DataFrames for backwards compatibility)
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
--------
|
|
83
|
+
>>> from volt_client import VoltClient
|
|
84
|
+
>>> client = VoltClient(api_key="volt_abc123...")
|
|
85
|
+
>>> df = client.get_actual("price power no1 nordpool eur min60 actual", "2024-01-01", "2024-12-31")
|
|
86
|
+
|
|
87
|
+
>>> # For better performance with large datasets:
|
|
88
|
+
>>> client = VoltClient(api_key="volt_abc123...", use_polars=True)
|
|
89
|
+
>>> df = client.get_actual(...) # Returns polars DataFrame
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
api_key: str,
|
|
95
|
+
base_url: str = DEFAULT_API_URL,
|
|
96
|
+
use_polars: bool = False,
|
|
97
|
+
):
|
|
98
|
+
if not api_key:
|
|
99
|
+
raise ValueError("API key is required. Get one from the Volt Analytics dashboard.")
|
|
100
|
+
|
|
101
|
+
self.api_key = api_key
|
|
102
|
+
self.base_url = base_url.rstrip("/")
|
|
103
|
+
self.use_polars = use_polars
|
|
104
|
+
self._session = None
|
|
105
|
+
self._curves_cache: pl.DataFrame | None = None
|
|
106
|
+
|
|
107
|
+
def _get_session(self) -> requests.Session:
|
|
108
|
+
"""Get or create a requests session with retry logic."""
|
|
109
|
+
if self._session is None:
|
|
110
|
+
self._session = requests.Session()
|
|
111
|
+
retry = Retry(
|
|
112
|
+
total=3,
|
|
113
|
+
backoff_factor=0.3,
|
|
114
|
+
status_forcelist=(500, 502, 503, 504),
|
|
115
|
+
)
|
|
116
|
+
adapter = HTTPAdapter(max_retries=retry)
|
|
117
|
+
self._session.mount("http://", adapter)
|
|
118
|
+
self._session.mount("https://", adapter)
|
|
119
|
+
return self._session
|
|
120
|
+
|
|
121
|
+
def _get_headers(self) -> dict:
|
|
122
|
+
"""Get request headers with authentication credentials."""
|
|
123
|
+
return {
|
|
124
|
+
"Content-Type": "application/json",
|
|
125
|
+
"X-API-Key": self.api_key,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
def _request(self, method: str, endpoint: str, params: dict = None) -> dict:
|
|
129
|
+
"""Make an API request."""
|
|
130
|
+
session = self._get_session()
|
|
131
|
+
url = f"{self.base_url}{endpoint}"
|
|
132
|
+
|
|
133
|
+
response = session.request(
|
|
134
|
+
method=method,
|
|
135
|
+
url=url,
|
|
136
|
+
headers=self._get_headers(),
|
|
137
|
+
params=params,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if response.status_code == 401:
|
|
141
|
+
raise VoltAccessError("Invalid API key or access denied")
|
|
142
|
+
elif response.status_code == 403:
|
|
143
|
+
raise VoltAccessError(f"Access denied to {endpoint}")
|
|
144
|
+
elif response.status_code == 404:
|
|
145
|
+
# Include params in error for debugging
|
|
146
|
+
param_info = f" (params: {params})" if params else ""
|
|
147
|
+
raise VoltCurveNotFoundError(f"Not found: {endpoint}{param_info}")
|
|
148
|
+
elif response.status_code != 200:
|
|
149
|
+
raise VoltClientError(f"API error: {response.status_code} - {response.text}")
|
|
150
|
+
|
|
151
|
+
# Use orjson for faster JSON parsing (3-10x faster than stdlib json)
|
|
152
|
+
return orjson.loads(response.content)
|
|
153
|
+
|
|
154
|
+
def _to_output(self, df: pl.DataFrame) -> DataFrame | pd.DataFrame:
|
|
155
|
+
"""Convert polars DataFrame to output format based on use_polars setting."""
|
|
156
|
+
if self.use_polars:
|
|
157
|
+
return df
|
|
158
|
+
|
|
159
|
+
pdf = df.to_pandas()
|
|
160
|
+
if "ts" in pdf.columns:
|
|
161
|
+
pdf = pdf.set_index("ts")
|
|
162
|
+
return pdf
|
|
163
|
+
|
|
164
|
+
def search(self, area: str = None, groups: str = None) -> DataFrame:
|
|
165
|
+
"""
|
|
166
|
+
Search for available curves.
|
|
167
|
+
|
|
168
|
+
Parameters:
|
|
169
|
+
-----------
|
|
170
|
+
area : str, optional
|
|
171
|
+
Filter by area (e.g., "NO1", "SE1")
|
|
172
|
+
groups : str, optional
|
|
173
|
+
Filter by group (e.g., "historical", "mid-term")
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
--------
|
|
177
|
+
DataFrame with curve metadata (curve_id, curve_name, area, groups, etc.)
|
|
178
|
+
Returns polars DataFrame if use_polars=True, otherwise pandas DataFrame.
|
|
179
|
+
"""
|
|
180
|
+
params = {}
|
|
181
|
+
if area:
|
|
182
|
+
params["area"] = area
|
|
183
|
+
if groups:
|
|
184
|
+
params["groups"] = groups
|
|
185
|
+
|
|
186
|
+
data = self._request("GET", "/curves", params=params)
|
|
187
|
+
return pl.DataFrame(data) if self.use_polars else pl.DataFrame(data).to_pandas()
|
|
188
|
+
|
|
189
|
+
def get_curve_id(self, curve_name: str) -> int:
|
|
190
|
+
"""
|
|
191
|
+
Get the curve ID for a given curve name.
|
|
192
|
+
|
|
193
|
+
Parameters:
|
|
194
|
+
-----------
|
|
195
|
+
curve_name : str
|
|
196
|
+
The unique curve name
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
--------
|
|
200
|
+
int: The curve ID
|
|
201
|
+
"""
|
|
202
|
+
# Always use polars internally for cache
|
|
203
|
+
if self._curves_cache is None:
|
|
204
|
+
data = self._request("GET", "/curves", params={})
|
|
205
|
+
self._curves_cache = pl.DataFrame(data)
|
|
206
|
+
|
|
207
|
+
curve_name_lower = curve_name.lower()
|
|
208
|
+
match = self._curves_cache.filter(
|
|
209
|
+
pl.col("curve_name").str.to_lowercase() == curve_name_lower
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if match.is_empty():
|
|
213
|
+
raise VoltCurveNotFoundError(
|
|
214
|
+
f"Curve '{curve_name}' not found or you do not have access to it"
|
|
215
|
+
)
|
|
216
|
+
return int(match["curve_id"][0])
|
|
217
|
+
|
|
218
|
+
def name_search(self, search_query: str) -> DataFrame:
|
|
219
|
+
"""
|
|
220
|
+
Search for curves by keywords in the name.
|
|
221
|
+
|
|
222
|
+
Parameters:
|
|
223
|
+
-----------
|
|
224
|
+
search_query : str
|
|
225
|
+
Space-separated keywords to search for (e.g., "price no1 actual")
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
--------
|
|
229
|
+
DataFrame with matching curves.
|
|
230
|
+
Returns polars DataFrame if use_polars=True, otherwise pandas DataFrame.
|
|
231
|
+
"""
|
|
232
|
+
# Always use polars internally for cache
|
|
233
|
+
if self._curves_cache is None:
|
|
234
|
+
data = self._request("GET", "/curves", params={})
|
|
235
|
+
self._curves_cache = pl.DataFrame(data)
|
|
236
|
+
|
|
237
|
+
search_terms = search_query.lower().split()
|
|
238
|
+
|
|
239
|
+
# Build polars filter expression
|
|
240
|
+
filter_expr = pl.lit(True)
|
|
241
|
+
for term in search_terms:
|
|
242
|
+
filter_expr = filter_expr & pl.col("curve_name").str.to_lowercase().str.contains(term)
|
|
243
|
+
matches = self._curves_cache.filter(filter_expr)
|
|
244
|
+
|
|
245
|
+
if matches.is_empty():
|
|
246
|
+
logger.warning(f"No curves found matching '{search_query}'")
|
|
247
|
+
|
|
248
|
+
return matches if self.use_polars else matches.to_pandas()
|
|
249
|
+
|
|
250
|
+
def get_actual(
|
|
251
|
+
self,
|
|
252
|
+
curve_name,
|
|
253
|
+
start_date: str,
|
|
254
|
+
end_date: str,
|
|
255
|
+
tz: str = None,
|
|
256
|
+
agg: str = None,
|
|
257
|
+
agg_func: str = None,
|
|
258
|
+
) -> DataFrame:
|
|
259
|
+
"""
|
|
260
|
+
Get actual (historical) time series data for a curve or multiple curves.
|
|
261
|
+
|
|
262
|
+
Supports both single curve and bulk queries. When a list is provided,
|
|
263
|
+
automatically uses a single optimized bulk request (much faster than
|
|
264
|
+
calling get_actual() in a loop).
|
|
265
|
+
|
|
266
|
+
Parameters:
|
|
267
|
+
-----------
|
|
268
|
+
curve_name : str or list of str
|
|
269
|
+
Single curve name, or list of curve names for bulk query.
|
|
270
|
+
Curve names must end with 'actual' or 'synthetic'.
|
|
271
|
+
start_date : str
|
|
272
|
+
Start date in format 'YYYY-MM-DD'
|
|
273
|
+
end_date : str
|
|
274
|
+
End date in format 'YYYY-MM-DD'
|
|
275
|
+
tz : str, optional
|
|
276
|
+
Output timezone (e.g., 'Europe/Oslo', 'CET'). Default: UTC.
|
|
277
|
+
agg : str, optional
|
|
278
|
+
Aggregation period: 'hourly', 'daily', 'weekly', 'monthly', 'quarterly', 'yearly'.
|
|
279
|
+
If specified, data is aggregated server-side.
|
|
280
|
+
agg_func : str, optional
|
|
281
|
+
Aggregation function: 'avg', 'sum', 'min', 'max'. Default: 'avg'.
|
|
282
|
+
Only used when agg is specified.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
--------
|
|
286
|
+
DataFrame with DatetimeIndex and value column(s) named after the curve(s).
|
|
287
|
+
Returns polars DataFrame if use_polars=True, otherwise pandas DataFrame.
|
|
288
|
+
|
|
289
|
+
Examples:
|
|
290
|
+
---------
|
|
291
|
+
# Single curve
|
|
292
|
+
df = client.get_actual(
|
|
293
|
+
"price power no1 nordpool eur min60 actual",
|
|
294
|
+
"2024-01-01", "2024-12-31"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Multiple curves (bulk query - much faster)
|
|
298
|
+
df = client.get_actual(
|
|
299
|
+
["price power no1 nordpool eur min60 actual",
|
|
300
|
+
"price power no2 nordpool eur min60 actual",
|
|
301
|
+
"price power se1 nordpool eur min60 actual"],
|
|
302
|
+
"2024-01-01", "2024-12-31",
|
|
303
|
+
tz="Europe/Oslo",
|
|
304
|
+
agg="daily"
|
|
305
|
+
)
|
|
306
|
+
# Returns DataFrame with one column per curve
|
|
307
|
+
"""
|
|
308
|
+
# If list provided, use bulk query for efficiency
|
|
309
|
+
if isinstance(curve_name, list | tuple):
|
|
310
|
+
return self.get_bulk(
|
|
311
|
+
curve_names=list(curve_name),
|
|
312
|
+
start_date=start_date,
|
|
313
|
+
end_date=end_date,
|
|
314
|
+
tz=tz,
|
|
315
|
+
agg=agg,
|
|
316
|
+
agg_func=agg_func,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
curve_name_lower = curve_name.lower()
|
|
320
|
+
curve_type = curve_name_lower.split()[-1]
|
|
321
|
+
|
|
322
|
+
if curve_type not in ["actual", "synthetic"]:
|
|
323
|
+
raise VoltClientError(
|
|
324
|
+
f"Curve '{curve_name}' is not of type 'actual' or 'synthetic' but '{curve_type}'. "
|
|
325
|
+
"Use get_actual only for actual/synthetic curves."
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
curve_id = self.get_curve_id(curve_name_lower)
|
|
329
|
+
|
|
330
|
+
params = {
|
|
331
|
+
"curve_id": curve_id,
|
|
332
|
+
"start_date": start_date,
|
|
333
|
+
"end_date": end_date,
|
|
334
|
+
}
|
|
335
|
+
if tz:
|
|
336
|
+
params["tz"] = tz
|
|
337
|
+
if agg:
|
|
338
|
+
params["agg"] = agg
|
|
339
|
+
if agg_func:
|
|
340
|
+
params["agg_func"] = agg_func
|
|
341
|
+
|
|
342
|
+
data = self._request("GET", "/data/actual", params=params)
|
|
343
|
+
|
|
344
|
+
if not data.get("data"):
|
|
345
|
+
raise VoltEmptyDataError(
|
|
346
|
+
f"No data for '{curve_name}' between {start_date} and {end_date}"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Always use polars internally for processing
|
|
350
|
+
df = pl.DataFrame(data["data"])
|
|
351
|
+
target_tz = tz or "UTC"
|
|
352
|
+
df = df.with_columns(
|
|
353
|
+
pl.col("ts")
|
|
354
|
+
.str.to_datetime(time_unit="us", time_zone="UTC")
|
|
355
|
+
.dt.convert_time_zone(target_tz)
|
|
356
|
+
)
|
|
357
|
+
df = df.sort("ts")
|
|
358
|
+
df = df.unique(subset=["ts"], keep="last")
|
|
359
|
+
df = df.rename({"value": curve_name_lower})
|
|
360
|
+
df = df.select(["ts", curve_name_lower])
|
|
361
|
+
|
|
362
|
+
logger.info(f"Extracted data for: {curve_name} from {df['ts'][0]} to {df['ts'][-1]}")
|
|
363
|
+
|
|
364
|
+
return self._to_output(df)
|
|
365
|
+
|
|
366
|
+
def get_fcast(
|
|
367
|
+
self,
|
|
368
|
+
curve_name,
|
|
369
|
+
start_date: str,
|
|
370
|
+
end_date: str,
|
|
371
|
+
as_of: str = None,
|
|
372
|
+
tz: str = None,
|
|
373
|
+
freq: str = None,
|
|
374
|
+
tags: str = None,
|
|
375
|
+
) -> DataFrame:
|
|
376
|
+
"""
|
|
377
|
+
Get forecast time series data for a curve or multiple curves.
|
|
378
|
+
|
|
379
|
+
Supports both single curve and bulk queries. When a list is provided,
|
|
380
|
+
automatically uses the bulk endpoint (much faster than calling get_fcast()
|
|
381
|
+
in a loop).
|
|
382
|
+
|
|
383
|
+
Parameters:
|
|
384
|
+
-----------
|
|
385
|
+
curve_name : str or list of str
|
|
386
|
+
Single curve name, or list of curve names for bulk query.
|
|
387
|
+
Curve names must end with 'fcast'.
|
|
388
|
+
start_date : str
|
|
389
|
+
Start date in format 'YYYY-MM-DD'
|
|
390
|
+
end_date : str
|
|
391
|
+
End date in format 'YYYY-MM-DD'
|
|
392
|
+
as_of : str, optional
|
|
393
|
+
As-of date for forecast (YYYY-MM-DD). If not specified, uses latest.
|
|
394
|
+
Note: For bulk queries, as_of is not supported.
|
|
395
|
+
tz : str, optional
|
|
396
|
+
Output timezone (e.g., 'Europe/Oslo', 'CET'). Default: UTC.
|
|
397
|
+
Note: For mid-term percentile data (when freq is specified), only
|
|
398
|
+
CET-compatible timezones are allowed.
|
|
399
|
+
freq : str, optional
|
|
400
|
+
Frequency for mid-term percentile/average data (e.g., 'weekly', 'monthly').
|
|
401
|
+
Only applicable for mid-term curves. Not supported in bulk queries.
|
|
402
|
+
tags : str, optional
|
|
403
|
+
Comma-separated weather year tags for mid-term hourly data.
|
|
404
|
+
Not supported in bulk queries.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
--------
|
|
408
|
+
DataFrame with timestamp and value column(s).
|
|
409
|
+
Returns polars DataFrame if use_polars=True, otherwise pandas DataFrame.
|
|
410
|
+
|
|
411
|
+
Examples:
|
|
412
|
+
---------
|
|
413
|
+
# Single curve
|
|
414
|
+
df = client.get_fcast("prod wind no1 mid-term fcast", "2024-01-01", "2024-12-31")
|
|
415
|
+
|
|
416
|
+
# Multiple curves (bulk query - much faster)
|
|
417
|
+
df = client.get_fcast([
|
|
418
|
+
"prod wind no1 mid-term fcast",
|
|
419
|
+
"prod wind no2 mid-term fcast",
|
|
420
|
+
"prod solar de mid-term fcast"
|
|
421
|
+
], "2024-01-01", "2024-12-31")
|
|
422
|
+
"""
|
|
423
|
+
# If list provided, use bulk query for efficiency
|
|
424
|
+
if isinstance(curve_name, list | tuple):
|
|
425
|
+
if as_of or freq or tags:
|
|
426
|
+
logger.warning(
|
|
427
|
+
"Bulk forecast queries do not support as_of, freq, or tags parameters. "
|
|
428
|
+
"These will be ignored."
|
|
429
|
+
)
|
|
430
|
+
return self.get_bulk(
|
|
431
|
+
curve_names=list(curve_name),
|
|
432
|
+
start_date=start_date,
|
|
433
|
+
end_date=end_date,
|
|
434
|
+
tz=tz,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
curve_name_lower = curve_name.lower()
|
|
438
|
+
curve_type = curve_name_lower.split()[-1]
|
|
439
|
+
|
|
440
|
+
if curve_type != "fcast":
|
|
441
|
+
raise VoltClientError(
|
|
442
|
+
f"Curve '{curve_name}' is not of type 'fcast' but '{curve_type}'. "
|
|
443
|
+
"Use get_fcast only for forecast curves."
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
curve_id = self.get_curve_id(curve_name_lower)
|
|
447
|
+
|
|
448
|
+
params = {
|
|
449
|
+
"curve_id": curve_id,
|
|
450
|
+
"start_date": start_date,
|
|
451
|
+
"end_date": end_date,
|
|
452
|
+
}
|
|
453
|
+
if as_of:
|
|
454
|
+
params["as_of"] = as_of
|
|
455
|
+
if tz:
|
|
456
|
+
params["tz"] = tz
|
|
457
|
+
if freq:
|
|
458
|
+
params["freq"] = freq
|
|
459
|
+
if tags:
|
|
460
|
+
params["tags"] = tags
|
|
461
|
+
|
|
462
|
+
data = self._request("GET", "/data/fcast", params=params)
|
|
463
|
+
|
|
464
|
+
if not data.get("data"):
|
|
465
|
+
raise VoltEmptyDataError(
|
|
466
|
+
f"No data for '{curve_name}' between {start_date} and {end_date}"
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Always use polars internally
|
|
470
|
+
df = pl.DataFrame(data["data"])
|
|
471
|
+
target_tz = tz or "UTC"
|
|
472
|
+
df = df.with_columns(
|
|
473
|
+
pl.col("ts")
|
|
474
|
+
.str.to_datetime(time_unit="us", time_zone="UTC")
|
|
475
|
+
.dt.convert_time_zone(target_tz)
|
|
476
|
+
)
|
|
477
|
+
df = df.sort("ts")
|
|
478
|
+
df = df.unique(subset=["ts"], keep="last")
|
|
479
|
+
if "value" in df.columns:
|
|
480
|
+
df = df.rename({"value": curve_name_lower})
|
|
481
|
+
|
|
482
|
+
logger.info(
|
|
483
|
+
f"Extracted forecast data for: {curve_name} from {df['ts'][0]} to {df['ts'][-1]}"
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
return self._to_output(df)
|
|
487
|
+
|
|
488
|
+
def get_closing(
|
|
489
|
+
self,
|
|
490
|
+
curve_name,
|
|
491
|
+
as_of: str = None,
|
|
492
|
+
tz: str = None,
|
|
493
|
+
) -> DataFrame:
|
|
494
|
+
"""
|
|
495
|
+
Get latest closing/settlement prices for a forward curve or multiple curves.
|
|
496
|
+
|
|
497
|
+
Supports both single curve and bulk queries. When a list is provided,
|
|
498
|
+
automatically uses a single optimized bulk request (much faster than
|
|
499
|
+
calling get_closing() in a loop).
|
|
500
|
+
|
|
501
|
+
Parameters:
|
|
502
|
+
-----------
|
|
503
|
+
curve_name : str or list of str
|
|
504
|
+
Single curve name, or list of curve names for bulk query.
|
|
505
|
+
Curve names must end with 'close' or 'settlement'.
|
|
506
|
+
as_of : str, optional
|
|
507
|
+
As-of date (YYYY-MM-DD). If not specified, uses latest for each curve.
|
|
508
|
+
tz : str, optional
|
|
509
|
+
Output timezone (e.g., 'Europe/Oslo', 'CET'). Default: UTC.
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
--------
|
|
513
|
+
For single curve: DataFrame with forward contract data (delivery periods and prices).
|
|
514
|
+
For bulk query: DataFrame with curve_name, ts, value, and as_of columns.
|
|
515
|
+
Returns polars DataFrame if use_polars=True, otherwise pandas DataFrame.
|
|
516
|
+
|
|
517
|
+
Examples:
|
|
518
|
+
---------
|
|
519
|
+
# Single curve
|
|
520
|
+
df = client.get_closing("price future no1 nasdaq close")
|
|
521
|
+
|
|
522
|
+
# Multiple curves (bulk query - much faster)
|
|
523
|
+
df = client.get_closing([
|
|
524
|
+
"price future no1 nasdaq close",
|
|
525
|
+
"price future no2 nasdaq close",
|
|
526
|
+
"price future se1 nasdaq close"
|
|
527
|
+
])
|
|
528
|
+
"""
|
|
529
|
+
# If list provided, use bulk query for efficiency
|
|
530
|
+
if isinstance(curve_name, list | tuple):
|
|
531
|
+
return self.get_bulk_closing(
|
|
532
|
+
curve_names=list(curve_name),
|
|
533
|
+
as_of=as_of,
|
|
534
|
+
tz=tz,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
curve_name_lower = curve_name.lower()
|
|
538
|
+
curve_type = curve_name_lower.split()[-1]
|
|
539
|
+
|
|
540
|
+
if curve_type not in ["close", "settlement"]:
|
|
541
|
+
raise VoltClientError(
|
|
542
|
+
f"Curve '{curve_name}' is not of type 'close' or 'settlement' but '{curve_type}'. "
|
|
543
|
+
"Use get_closing only for closing/settlement curves."
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
curve_id = self.get_curve_id(curve_name_lower)
|
|
547
|
+
|
|
548
|
+
params = {"curve_id": curve_id}
|
|
549
|
+
if as_of:
|
|
550
|
+
params["as_of"] = as_of
|
|
551
|
+
if tz:
|
|
552
|
+
params["tz"] = tz
|
|
553
|
+
|
|
554
|
+
data = self._request("GET", "/data/closing", params=params)
|
|
555
|
+
|
|
556
|
+
if not data.get("data"):
|
|
557
|
+
raise VoltEmptyDataError(f"No closing data for '{curve_name}'")
|
|
558
|
+
|
|
559
|
+
# Always use polars internally
|
|
560
|
+
df = pl.DataFrame(data["data"])
|
|
561
|
+
target_tz = tz or "UTC"
|
|
562
|
+
for col in ["ts", "delivery_end"]:
|
|
563
|
+
if col in df.columns:
|
|
564
|
+
df = df.with_columns(
|
|
565
|
+
pl.col(col)
|
|
566
|
+
.str.to_datetime(time_unit="us", time_zone="UTC")
|
|
567
|
+
.dt.convert_time_zone(target_tz)
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
logger.info(
|
|
571
|
+
f"Extracted closing data for: {curve_name}, as_of: {data.get('as_of')}, count: {len(df)}"
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
return df if self.use_polars else df.to_pandas()
|
|
575
|
+
|
|
576
|
+
def get_bulk_closing(
|
|
577
|
+
self,
|
|
578
|
+
curve_names: list[str],
|
|
579
|
+
as_of: str = None,
|
|
580
|
+
as_of_start: str = None,
|
|
581
|
+
as_of_end: str = None,
|
|
582
|
+
tz: str = None,
|
|
583
|
+
) -> DataFrame:
|
|
584
|
+
"""
|
|
585
|
+
Get closing/settlement prices for multiple forward curves in a single request.
|
|
586
|
+
|
|
587
|
+
This is much more efficient than calling get_closing() multiple times
|
|
588
|
+
as it makes a single HTTP request and ClickHouse query.
|
|
589
|
+
|
|
590
|
+
Parameters:
|
|
591
|
+
-----------
|
|
592
|
+
curve_names : list of str
|
|
593
|
+
List of curve names to fetch (must end with 'close' or 'settlement')
|
|
594
|
+
as_of : str, optional
|
|
595
|
+
Single as-of date (YYYY-MM-DD). If not specified, uses latest for each curve.
|
|
596
|
+
as_of_start : str, optional
|
|
597
|
+
Start of as-of range (YYYY-MM-DD). Use with as_of_end for development over time.
|
|
598
|
+
as_of_end : str, optional
|
|
599
|
+
End of as-of range (YYYY-MM-DD). Use with as_of_start for development over time.
|
|
600
|
+
tz : str, optional
|
|
601
|
+
Output timezone (e.g., 'Europe/Oslo', 'CET'). Default: UTC.
|
|
602
|
+
|
|
603
|
+
Returns:
|
|
604
|
+
--------
|
|
605
|
+
DataFrame with columns: curve_name, ts, value, as_of.
|
|
606
|
+
Returns polars DataFrame if use_polars=True, otherwise pandas DataFrame.
|
|
607
|
+
|
|
608
|
+
Examples:
|
|
609
|
+
---------
|
|
610
|
+
# Snapshot mode - latest for each curve
|
|
611
|
+
df = client.get_bulk_closing(["curve1 close", "curve2 close"])
|
|
612
|
+
|
|
613
|
+
# Snapshot mode - specific date
|
|
614
|
+
df = client.get_bulk_closing(["curve1 close", "curve2 close"], as_of="2025-01-15")
|
|
615
|
+
|
|
616
|
+
# Range mode - development over time
|
|
617
|
+
df = client.get_bulk_closing(
|
|
618
|
+
["curve1 close", "curve2 close"],
|
|
619
|
+
as_of_start="2025-01-01",
|
|
620
|
+
as_of_end="2025-01-15"
|
|
621
|
+
)
|
|
622
|
+
"""
|
|
623
|
+
if not curve_names:
|
|
624
|
+
raise VoltClientError("curve_names cannot be empty")
|
|
625
|
+
|
|
626
|
+
if as_of and (as_of_start or as_of_end):
|
|
627
|
+
raise VoltClientError(
|
|
628
|
+
"Cannot use 'as_of' together with 'as_of_start'/'as_of_end'. "
|
|
629
|
+
"Use either snapshot mode (as_of) or range mode (as_of_start + as_of_end)."
|
|
630
|
+
)
|
|
631
|
+
if (as_of_start and not as_of_end) or (as_of_end and not as_of_start):
|
|
632
|
+
raise VoltClientError(
|
|
633
|
+
"Both 'as_of_start' and 'as_of_end' must be specified for range mode."
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
# Get curve IDs for all names
|
|
637
|
+
curve_ids = []
|
|
638
|
+
id_to_name = {}
|
|
639
|
+
for name in curve_names:
|
|
640
|
+
name_lower = name.lower()
|
|
641
|
+
curve_id = self.get_curve_id(name_lower)
|
|
642
|
+
curve_ids.append(curve_id)
|
|
643
|
+
id_to_name[curve_id] = name_lower
|
|
644
|
+
|
|
645
|
+
params = {
|
|
646
|
+
"curve_ids": ",".join(str(cid) for cid in curve_ids),
|
|
647
|
+
}
|
|
648
|
+
if as_of:
|
|
649
|
+
params["as_of"] = as_of
|
|
650
|
+
if as_of_start:
|
|
651
|
+
params["as_of_start"] = as_of_start
|
|
652
|
+
if as_of_end:
|
|
653
|
+
params["as_of_end"] = as_of_end
|
|
654
|
+
if tz:
|
|
655
|
+
params["tz"] = tz
|
|
656
|
+
|
|
657
|
+
data = self._request("GET", "/data/closing/bulk", params=params)
|
|
658
|
+
|
|
659
|
+
curves_data = data.get("curves", [])
|
|
660
|
+
if not curves_data:
|
|
661
|
+
raise VoltEmptyDataError(
|
|
662
|
+
f"No closing data found for any of the {len(curve_names)} requested curves. "
|
|
663
|
+
"Check that curves have data in curves_forwards table."
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
# Log which curves had data vs which didn't
|
|
667
|
+
curves_with_data = {c["curve_id"] for c in curves_data}
|
|
668
|
+
curves_without_data = [id_to_name[cid] for cid in curve_ids if cid not in curves_with_data]
|
|
669
|
+
if curves_without_data:
|
|
670
|
+
logger.warning(
|
|
671
|
+
f"No closing data for {len(curves_without_data)} curves: {curves_without_data[:5]}..."
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
# Build combined DataFrame using pl.concat (avoids Python row-by-row loop)
|
|
675
|
+
dfs = []
|
|
676
|
+
for curve in curves_data:
|
|
677
|
+
curve_id = curve["curve_id"]
|
|
678
|
+
curve_name = id_to_name.get(curve_id, curve.get("curve_name", f"curve_{curve_id}"))
|
|
679
|
+
curve_as_of = curve.get("as_of")
|
|
680
|
+
|
|
681
|
+
if not curve.get("data"):
|
|
682
|
+
continue
|
|
683
|
+
|
|
684
|
+
cdf = pl.DataFrame(curve["data"])
|
|
685
|
+
cdf = cdf.with_columns(pl.lit(curve_name).alias("curve_name"))
|
|
686
|
+
# In range mode, as_of is on each point; in snapshot mode, add from curve
|
|
687
|
+
if "as_of" not in cdf.columns:
|
|
688
|
+
cdf = cdf.with_columns(pl.lit(curve_as_of).alias("as_of"))
|
|
689
|
+
dfs.append(cdf)
|
|
690
|
+
|
|
691
|
+
if not dfs:
|
|
692
|
+
raise VoltEmptyDataError("No closing data for curves")
|
|
693
|
+
|
|
694
|
+
df = pl.concat(dfs)
|
|
695
|
+
target_tz = tz or "UTC"
|
|
696
|
+
df = df.with_columns(
|
|
697
|
+
pl.col("ts")
|
|
698
|
+
.str.to_datetime(time_unit="us", time_zone="UTC")
|
|
699
|
+
.dt.convert_time_zone(target_tz)
|
|
700
|
+
)
|
|
701
|
+
df = df.sort(["curve_name", "as_of", "ts"])
|
|
702
|
+
|
|
703
|
+
logger.info(
|
|
704
|
+
f"Extracted bulk closing data for {len(curve_names)} curves: {len(df)} rows total"
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
return df if self.use_polars else df.to_pandas()
|
|
708
|
+
|
|
709
|
+
def get_bulk(
|
|
710
|
+
self,
|
|
711
|
+
curve_names: list[str],
|
|
712
|
+
start_date: str,
|
|
713
|
+
end_date: str,
|
|
714
|
+
tz: str = None,
|
|
715
|
+
agg: str = None,
|
|
716
|
+
agg_func: str = None,
|
|
717
|
+
) -> DataFrame:
|
|
718
|
+
"""
|
|
719
|
+
Get time series data for multiple curves in a single request.
|
|
720
|
+
|
|
721
|
+
This is much more efficient than calling get_actual() multiple times
|
|
722
|
+
as it makes a single HTTP request and ClickHouse query.
|
|
723
|
+
|
|
724
|
+
Parameters:
|
|
725
|
+
-----------
|
|
726
|
+
curve_names : list of str
|
|
727
|
+
List of curve names to fetch
|
|
728
|
+
start_date : str
|
|
729
|
+
Start date in format 'YYYY-MM-DD'
|
|
730
|
+
end_date : str
|
|
731
|
+
End date in format 'YYYY-MM-DD'
|
|
732
|
+
tz : str, optional
|
|
733
|
+
Output timezone (e.g., 'Europe/Oslo', 'CET'). Default: UTC.
|
|
734
|
+
agg : str, optional
|
|
735
|
+
Aggregation period: 'hourly', 'daily', 'weekly', 'monthly', 'quarterly', 'yearly'.
|
|
736
|
+
agg_func : str, optional
|
|
737
|
+
Aggregation function: 'avg', 'sum', 'min', 'max'. Default: 'avg'.
|
|
738
|
+
|
|
739
|
+
Returns:
|
|
740
|
+
--------
|
|
741
|
+
DataFrame with timestamp column/index and one column per curve.
|
|
742
|
+
Returns polars DataFrame if use_polars=True, otherwise pandas DataFrame.
|
|
743
|
+
"""
|
|
744
|
+
if not curve_names:
|
|
745
|
+
raise VoltClientError("curve_names cannot be empty")
|
|
746
|
+
|
|
747
|
+
# Get curve IDs for all names
|
|
748
|
+
curve_ids = []
|
|
749
|
+
name_to_id = {}
|
|
750
|
+
for name in curve_names:
|
|
751
|
+
name_lower = name.lower()
|
|
752
|
+
curve_id = self.get_curve_id(name_lower)
|
|
753
|
+
curve_ids.append(curve_id)
|
|
754
|
+
name_to_id[curve_id] = name_lower
|
|
755
|
+
|
|
756
|
+
params = {
|
|
757
|
+
"curve_ids": ",".join(str(cid) for cid in curve_ids),
|
|
758
|
+
"start_date": start_date,
|
|
759
|
+
"end_date": end_date,
|
|
760
|
+
}
|
|
761
|
+
if tz:
|
|
762
|
+
params["tz"] = tz
|
|
763
|
+
if agg:
|
|
764
|
+
params["agg"] = agg
|
|
765
|
+
if agg_func:
|
|
766
|
+
params["agg_func"] = agg_func
|
|
767
|
+
|
|
768
|
+
data = self._request("GET", "/data", params=params)
|
|
769
|
+
|
|
770
|
+
curves_data = data.get("curves", [])
|
|
771
|
+
if not curves_data:
|
|
772
|
+
raise VoltEmptyDataError(f"No data for curves between {start_date} and {end_date}")
|
|
773
|
+
|
|
774
|
+
# Always use polars internally for processing
|
|
775
|
+
dfs = []
|
|
776
|
+
for curve in curves_data:
|
|
777
|
+
curve_id = curve["curve_id"]
|
|
778
|
+
curve_name = name_to_id.get(curve_id, f"curve_{curve_id}")
|
|
779
|
+
|
|
780
|
+
if curve.get("data"):
|
|
781
|
+
df = pl.DataFrame(curve["data"])
|
|
782
|
+
target_tz = tz or "UTC"
|
|
783
|
+
df = df.with_columns(
|
|
784
|
+
pl.col("ts")
|
|
785
|
+
.str.to_datetime(time_unit="us", time_zone="UTC")
|
|
786
|
+
.dt.convert_time_zone(target_tz)
|
|
787
|
+
)
|
|
788
|
+
df = df.rename({"value": curve_name})
|
|
789
|
+
df = df.select(["ts", curve_name])
|
|
790
|
+
dfs.append(df)
|
|
791
|
+
|
|
792
|
+
if not dfs:
|
|
793
|
+
raise VoltEmptyDataError(f"No data for curves between {start_date} and {end_date}")
|
|
794
|
+
|
|
795
|
+
# Join all DataFrames on timestamp using polars (much faster than pandas)
|
|
796
|
+
result = dfs[0]
|
|
797
|
+
for df in dfs[1:]:
|
|
798
|
+
result = result.join(df, on="ts", how="full", coalesce=True)
|
|
799
|
+
|
|
800
|
+
result = result.sort("ts")
|
|
801
|
+
result = result.unique(subset=["ts"], keep="last")
|
|
802
|
+
|
|
803
|
+
logger.info(
|
|
804
|
+
f"Extracted bulk data for {len(curve_names)} curves: "
|
|
805
|
+
f"{result['ts'][0]} to {result['ts'][-1]}, {len(result)} rows"
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
return self._to_output(result)
|
|
809
|
+
|
|
810
|
+
def get_areas(self) -> list[str]:
|
|
811
|
+
"""Get list of areas the user has access to."""
|
|
812
|
+
data = self._request("GET", "/areas")
|
|
813
|
+
return data.get("areas", [])
|
|
814
|
+
|
|
815
|
+
def get_groups(self) -> list[str]:
|
|
816
|
+
"""Get list of groups the user has access to."""
|
|
817
|
+
data = self._request("GET", "/groups")
|
|
818
|
+
return data.get("groups", [])
|
|
819
|
+
|
|
820
|
+
def me(self) -> dict:
|
|
821
|
+
"""Get current user information."""
|
|
822
|
+
return self._request("GET", "/me")
|
|
823
|
+
|
|
824
|
+
def my_rules(self) -> dict:
|
|
825
|
+
"""Get current user's permission rules."""
|
|
826
|
+
return self._request("GET", "/me/rules")
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: volt-client
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python client for Volt Power Analytics API - Access energy market time series data
|
|
5
|
+
Author-email: Volt Power Analytics <support@voltanalytics.no>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://voltpoweranalytics.com
|
|
8
|
+
Project-URL: Documentation, https://github.com/Volt-Power-Analytics/volt-api/tree/main/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/Volt-Power-Analytics/volt-api
|
|
10
|
+
Keywords: energy,api,time-series,power,market,data
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: polars>=0.20.0
|
|
21
|
+
Requires-Dist: requests>=2.28.0
|
|
22
|
+
Requires-Dist: orjson>=3.9.0
|
|
23
|
+
Provides-Extra: pandas
|
|
24
|
+
Requires-Dist: pandas>=2.0.0; extra == "pandas"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
28
|
+
|
|
29
|
+
# Volt Client
|
|
30
|
+
|
|
31
|
+
Python client for the Volt Power Analytics API - Access energy market time series data.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install volt-client
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import os
|
|
43
|
+
from volt_client import VoltClient
|
|
44
|
+
|
|
45
|
+
# Initialize with your API key
|
|
46
|
+
client = VoltClient(api_key=os.environ["VOLT_API_KEY"])
|
|
47
|
+
|
|
48
|
+
# Get spot prices
|
|
49
|
+
df = client.get_actual(
|
|
50
|
+
"price spot no1 nordpool eur mwh min60 actual",
|
|
51
|
+
start_date="2024-01-01",
|
|
52
|
+
end_date="2024-12-31"
|
|
53
|
+
)
|
|
54
|
+
print(df.head())
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- Simple Python interface for Volt Power Analytics API
|
|
60
|
+
- Support for historical data, forecasts, and forward curves
|
|
61
|
+
- Bulk queries for efficient data retrieval
|
|
62
|
+
- Timezone conversion and server-side aggregation
|
|
63
|
+
- Returns pandas DataFrames (or polars for better performance)
|
|
64
|
+
|
|
65
|
+
## Usage Examples
|
|
66
|
+
|
|
67
|
+
### Historical Data
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# Single curve
|
|
71
|
+
df = client.get_actual(
|
|
72
|
+
"price spot no1 nordpool eur mwh min60 actual",
|
|
73
|
+
start_date="2024-01-01",
|
|
74
|
+
end_date="2024-12-31",
|
|
75
|
+
tz="Europe/Oslo",
|
|
76
|
+
agg="daily",
|
|
77
|
+
agg_func="avg"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Multiple curves (bulk query - much faster)
|
|
81
|
+
df = client.get_actual(
|
|
82
|
+
["price spot no1...", "price spot no2...", "price spot no3..."],
|
|
83
|
+
start_date="2024-01-01",
|
|
84
|
+
end_date="2024-12-31"
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Forecast Data
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
df = client.get_fcast(
|
|
92
|
+
"price no1 volt emps mid-term fcast",
|
|
93
|
+
start_date="2025-01-01",
|
|
94
|
+
end_date="2025-12-31",
|
|
95
|
+
freq="weekly" # Returns percentiles: avg, p10, p25, p50, p75, p90
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Forward Curves
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
df = client.get_closing("price future no1 nasdaq eur mwh close")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Performance Tips
|
|
106
|
+
|
|
107
|
+
For large datasets, use polars mode (10x faster):
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
client = VoltClient(api_key="...", use_polars=True)
|
|
111
|
+
df = client.get_actual(...) # Returns polars DataFrame
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## API Reference
|
|
115
|
+
|
|
116
|
+
| Method | Description |
|
|
117
|
+
|--------|-------------|
|
|
118
|
+
| `get_actual(curve, start, end)` | Get historical/actual data |
|
|
119
|
+
| `get_fcast(curve, start, end)` | Get forecast data |
|
|
120
|
+
| `get_closing(curve)` | Get forward curve prices |
|
|
121
|
+
| `search(area=None)` | Search available curves |
|
|
122
|
+
| `get_areas()` | List accessible areas |
|
|
123
|
+
| `get_groups()` | List accessible groups |
|
|
124
|
+
|
|
125
|
+
## Documentation
|
|
126
|
+
|
|
127
|
+
Full documentation: https://github.com/Volt-Power-Analytics/volt-api/tree/main/docs
|
|
128
|
+
|
|
129
|
+
## Support
|
|
130
|
+
|
|
131
|
+
Contact: support@voltanalytics.no
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
volt_client/__init__.py,sha256=Q8uuwfiSqVyDsYOELc0nts1A_StcmdZVcp1tv7KsfSY,605
|
|
2
|
+
volt_client/client.py,sha256=4he1uSL1NsCqZjWGJqV1Tme0YVCKRafn9hSJ2f33REs,28408
|
|
3
|
+
volt_client-1.0.0.dist-info/METADATA,sha256=rMwKnKyyYmx1ShO1lc3AxiHis8ZJJPdJB8bKiepXqXM,3470
|
|
4
|
+
volt_client-1.0.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
5
|
+
volt_client-1.0.0.dist-info/top_level.txt,sha256=BWXHOWfVwSZW7GgLwFjWUEgeFmPWCs3XK8B1DjNNtYs,12
|
|
6
|
+
volt_client-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
volt_client
|