detquantlib 3.14.0__tar.gz → 3.15.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.
Files changed (33) hide show
  1. {detquantlib-3.14.0 → detquantlib-3.15.0}/PKG-INFO +3 -3
  2. detquantlib-3.15.0/detquantlib/assets/__init__.py +4 -0
  3. detquantlib-3.15.0/detquantlib/assets/battery.py +174 -0
  4. detquantlib-3.15.0/detquantlib/assets/wind_turbine.py +92 -0
  5. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/data/databases/detdatabase.py +14 -14
  6. {detquantlib-3.14.0 → detquantlib-3.15.0}/pyproject.toml +8 -3
  7. {detquantlib-3.14.0 → detquantlib-3.15.0}/LICENSE.txt +0 -0
  8. {detquantlib-3.14.0 → detquantlib-3.15.0}/README.md +0 -0
  9. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/__init__.py +0 -0
  10. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/converters/__init__.py +0 -0
  11. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/converters/definitions.py +0 -0
  12. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/converters/energy.py +0 -0
  13. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/converters/helpers.py +0 -0
  14. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/converters/price.py +0 -0
  15. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/data/__init__.py +0 -0
  16. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/data/databases/helpers.py +0 -0
  17. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/data/entsoe/entsoe.py +0 -0
  18. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/data/sftp/sftp.py +0 -0
  19. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/dates/__init__.py +0 -0
  20. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/dates/dates.py +0 -0
  21. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/figures/__init__.py +0 -0
  22. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/figures/plotly_figures.py +0 -0
  23. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/forecasting/__init__.py +0 -0
  24. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/forecasting/forecasting.py +0 -0
  25. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/outputs/__init__.py +0 -0
  26. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/outputs/outputs_interface.py +0 -0
  27. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/stats/__init__.py +0 -0
  28. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/stats/data_analysis.py +0 -0
  29. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/tradable_products/__init__.py +0 -0
  30. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/tradable_products/tradable_products.py +0 -0
  31. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/utils/__init__.py +0 -0
  32. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/utils/logging.py +0 -0
  33. {detquantlib-3.14.0 → detquantlib-3.15.0}/detquantlib/utils/utils.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detquantlib
3
- Version: 3.14.0
3
+ Version: 3.15.0
4
4
  Summary: An internal library containing functions and classes that can be used across Quant models.
5
5
  License-File: LICENSE.txt
6
- Author: DET
6
+ Author: Dynamic Energy Trading
7
7
  Requires-Python: >=3.10,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.10
@@ -19,9 +19,9 @@ Requires-Dist: plotly (>=6.3.1,<7.0.0)
19
19
  Requires-Dist: pyarrow (>=21.0.0,<22.0.0)
20
20
  Requires-Dist: pyodbc (>=5.2.0,<6.0.0)
21
21
  Requires-Dist: python-dotenv (>=1.1.0,<2.0.0)
22
+ Requires-Dist: redis (>=7.4.0,<8.0.0)
22
23
  Requires-Dist: scipy (>=1.15.2,<2.0.0)
23
24
  Requires-Dist: sqlalchemy (>=2.0.43,<3.0.0)
24
- Project-URL: Repository, https://github.com/Dynamic-Energy-Trading/detquantlib
25
25
  Description-Content-Type: text/markdown
26
26
 
27
27
  # DET Quant Library
@@ -0,0 +1,4 @@
1
+ from .battery import Battery
2
+ from .wind_turbine import WindTurbine
3
+
4
+ __all__ = ["Battery", "WindTurbine"]
@@ -0,0 +1,174 @@
1
+ # Python built-in packages
2
+ import json
3
+ import os
4
+
5
+ # Third-party packages
6
+ import pandas as pd
7
+ import redis
8
+
9
+ # Internal modules
10
+ from detquantlib.utils import list_to_str
11
+
12
+
13
+ class Battery:
14
+ """A class containing the characteristics of a battery."""
15
+
16
+ def __init__(
17
+ self,
18
+ capacity_mwh: float = None,
19
+ power_mw: float = None,
20
+ state_of_charge_mwh: float = None,
21
+ roundtrip_eff: float = None,
22
+ group_id: int = None,
23
+ ):
24
+ """
25
+ Constructor method.
26
+
27
+ Args:
28
+ capacity_mwh: Battery capacity in MWh
29
+ power_mw: Battery power in MW
30
+ state_of_charge_mwh: Current state of charge in MWh
31
+ roundtrip_eff: Roundtrip efficiency
32
+ group_id: Group ID (used to differentiate batteries with different characteristics
33
+ """
34
+ # Assign inputs
35
+ self.capacity_mwh = capacity_mwh
36
+ self.power_mw = power_mw
37
+ self.state_of_charge_mwh = state_of_charge_mwh
38
+ self.roundtrip_eff = roundtrip_eff
39
+ self.group_id = group_id
40
+
41
+ # Calculate power-to-capacity ratio
42
+ if self.capacity_mwh and self.power_mw:
43
+ self.capacity_power_ratio = round(self.capacity_mwh / self.power_mw, 0)
44
+
45
+ @staticmethod
46
+ def build_from_redis() -> (list["Battery"], str):
47
+ """
48
+ Builds Battery objects from the individual battery units found in the redis cache.
49
+
50
+ Returns:
51
+ Tuple:
52
+ - List of Battery objects
53
+ - Message listing all the batteries whose information could not be retrieved
54
+ from the redis cache
55
+ """
56
+ # Load individual battery units
57
+ battery_units, missing_units = Battery.load_battery_units_from_redis()
58
+
59
+ # Print missing battery units
60
+ msg_missing_units = ""
61
+ nr_found, nr_miss = len(battery_units), len(missing_units)
62
+ if nr_miss > 0:
63
+ msg_missing_units = (
64
+ f"Failed to retrieve the battery storage data of {nr_miss}/{nr_found + nr_miss} "
65
+ f"batteries. Details:"
66
+ )
67
+ for mb in missing_units:
68
+ msg_missing_units += (
69
+ f"\n- Site node ID: {mb['site_node_id']}. Error message: '{mb['error_msg']}'."
70
+ )
71
+
72
+ if nr_found == 0:
73
+ return list(), msg_missing_units
74
+
75
+ # Set battery groups based on capacity:power ratio
76
+ battery_units = pd.DataFrame(battery_units)
77
+ battery_units.sort_values("cp_ratio", ignore_index=True, inplace=True)
78
+ battery_units["cp_group"] = battery_units["cp_ratio"].round(0)
79
+
80
+ # Calculate aggregated virtual batteries
81
+ agg_units = (
82
+ battery_units[["capacity_mwh", "power_mw", "state_of_charge_mwh", "cp_group"]]
83
+ .groupby("cp_group", as_index=False)
84
+ .agg({"capacity_mwh": "sum", "power_mw": "sum", "state_of_charge_mwh": "sum"})
85
+ .reset_index(drop=True)
86
+ )
87
+ agg_units["group_id"] = agg_units["cp_group"].rank(method="dense")
88
+ agg_units.drop(columns="cp_group", inplace=True)
89
+
90
+ # Round to avoid machine precision issues caused by 'group by'
91
+ agg_units = agg_units.round(9)
92
+
93
+ # Create battery objects
94
+ batteries = [Battery(**agg_units.loc[i, :].to_dict()) for i in agg_units.index]
95
+
96
+ return batteries, msg_missing_units
97
+
98
+ @staticmethod
99
+ def load_battery_units_from_redis() -> (list[dict], list[dict]):
100
+ """
101
+ Loads individual battery units from the redis cache.
102
+
103
+ Returns:
104
+ Tuple:
105
+ - Individual battery units whose information could be found
106
+ - Individual battery units whose information could not be found
107
+
108
+ Raises:
109
+ ConnectionError: Raises an error if environment variables are not defined.
110
+ """
111
+ # Check mandatory environment variables to connect to Redis cache
112
+ mandatory_vars = ["DET_REDIS_HOST", "DET_REDIS_PORT", "DET_REDIS_PASSWORD", "DET_REDIS_DB"]
113
+ for mv in mandatory_vars:
114
+ if mv not in os.environ:
115
+ raise ConnectionError(
116
+ f"Environment variable '{mv}' not found. Connection to Redis cache requires "
117
+ f"the following environment variables: {list_to_str(mandatory_vars)}."
118
+ )
119
+
120
+ # Connect to redis cache
121
+ r = redis.Redis(
122
+ host=os.environ["DET_REDIS_HOST"],
123
+ port=int(os.environ["DET_REDIS_PORT"]),
124
+ password=os.environ["DET_REDIS_PASSWORD"],
125
+ ssl=True,
126
+ db=int(os.environ["DET_REDIS_DB"]),
127
+ )
128
+
129
+ # Loop over cache data
130
+ battery_units = []
131
+ missing_units = []
132
+ for key in r.scan_iter("*"):
133
+ # Parse data
134
+ raw = r.get(key)
135
+ data = json.loads(raw)
136
+
137
+ try:
138
+ storage = data["data"]["state"]["storage"]
139
+ except KeyError:
140
+ # Skip battery if info cannot be found
141
+ missing_units.append(
142
+ dict(site_node_id=data["siteNodeId"], error_msg=data["data"].get("errors", ""))
143
+ )
144
+ continue
145
+
146
+ # Convert settings
147
+ capacity_mwh = storage["energy_capacity_Wh"] / 1e6
148
+ state_of_charge_mwh = storage["energy_stored_Wh"] / 1e6
149
+ charge_rate_mw = storage["max_charge_power_W"] / 1e6
150
+ discharge_rate_mw = storage["max_discharge_power_W"] / 1e6
151
+
152
+ # Set (dis)charge rate
153
+ power_mw = max(charge_rate_mw, -discharge_rate_mw)
154
+
155
+ # Calculate capacity:power ratio
156
+ cp_ratio = capacity_mwh / power_mw
157
+
158
+ # Get BESS group data
159
+ timestamp = data["time"]
160
+ site_node_id = data["siteNodeId"]
161
+
162
+ # Store characteristics
163
+ battery_units.append(
164
+ dict(
165
+ capacity_mwh=capacity_mwh,
166
+ power_mw=power_mw,
167
+ state_of_charge_mwh=state_of_charge_mwh,
168
+ cp_ratio=cp_ratio,
169
+ timestamp=timestamp,
170
+ site_node_id=site_node_id,
171
+ )
172
+ )
173
+
174
+ return battery_units, missing_units
@@ -0,0 +1,92 @@
1
+ # Third-party packages
2
+ import numpy as np
3
+ import pandas as pd
4
+ from scipy.interpolate import CubicSpline
5
+
6
+
7
+ class WindTurbine:
8
+ """A class to store wind turbine information and perform wind turbine-related calculations."""
9
+
10
+ def __init__(
11
+ self, rated_power: float, cut_in: float, cut_out: float, power_curve: CubicSpline = None
12
+ ):
13
+ """
14
+ Constructor method.
15
+
16
+ Args:
17
+ rated_power: Rated power
18
+ cut_in: Cut-in wind speed
19
+ cut_out: Cut-out wind speed
20
+ power_curve: Power curve, stored as a cubic spline object. If not provided, can be
21
+ set via the 'fit_power_curve()' method below.
22
+ """
23
+ self.rated_power = rated_power
24
+ self.cut_in = cut_in
25
+ self.cut_out = cut_out
26
+ self.power_curve = power_curve
27
+
28
+ def fit_power_curve(
29
+ self, wind_speed: list | np.ndarray | pd.Series, power: list | np.ndarray | pd.Series
30
+ ):
31
+ """
32
+ Fits the wind turbine's power curve with a cubic spline, and stores the resulting spline
33
+ object in attribute self.power_curve.
34
+
35
+ Args:
36
+ wind_speed: Wind speeds
37
+ power: Corresponding power
38
+ """
39
+ # Make sure input data is stored as numpy array
40
+ wind_speed, power = np.array(wind_speed), np.array(power)
41
+
42
+ # Trim data to cut-in - cut-out interval
43
+ idx = (wind_speed >= self.cut_in) & (wind_speed <= self.cut_out)
44
+ wind_speed_trim, power_trim = wind_speed[idx], power[idx]
45
+
46
+ # Add data point with power=0.0 before cut-in to ensure smooth behaviour of spline
47
+ # interpolation
48
+ w0 = wind_speed_trim[0] - np.diff(wind_speed_trim[:2])
49
+ p0 = [0]
50
+ wind_speed_trim, power_trim = np.concat([w0, wind_speed_trim]), np.concat([p0, power_trim])
51
+
52
+ # Fit cubic spline
53
+ cs = CubicSpline(wind_speed_trim, power_trim)
54
+
55
+ # Store spline object
56
+ self.power_curve = cs
57
+
58
+ def wind_to_power(self, wind_speed: list | np.ndarray | pd.Series) -> np.ndarray:
59
+ """
60
+ Converts wind speeds to wind turbine power.
61
+
62
+ Args:
63
+ wind_speed: Wind speeds
64
+
65
+ Returns:
66
+ Corresponding power
67
+
68
+ Raises:
69
+ ValueError: Raises an error if self.power_curve is None.
70
+ """
71
+ # Check that power curve attribute is defined
72
+ if self.power_curve is None:
73
+ raise ValueError(
74
+ "Cannot execute method self.wind_to_power() if attribute self.power_curve is "
75
+ "None. Use method self.fit_power_curve() to set self.power_curve based on power "
76
+ "curve data."
77
+ )
78
+
79
+ # Make sure input data is stored as numpy array
80
+ wind_speed = np.array(wind_speed)
81
+
82
+ # Cubic spline interpolation
83
+ power = self.power_curve(wind_speed)
84
+
85
+ # Cap to rated power
86
+ power = np.minimum(power, self.rated_power)
87
+
88
+ # Adjust values outside of cut-in - cut-out bounds
89
+ idx = (wind_speed < self.cut_in) | (wind_speed > self.cut_out)
90
+ power[idx] = 0.0
91
+
92
+ return power
@@ -51,7 +51,7 @@ class DetDatabase:
51
51
  Checks if environment variables needed by the class are defined.
52
52
 
53
53
  Raises:
54
- EnvironmentError: Raises an error if environment variables are not defined
54
+ ConnectionError: Raises an error if environment variables are not defined.
55
55
  """
56
56
  required_env_vars = [
57
57
  dict(name="DET_DB_NAME", value=None, description="Database name"),
@@ -64,7 +64,7 @@ class DetDatabase:
64
64
  if d["name"] not in available_env_vars:
65
65
  required_env_vars_names = [x["name"] for x in required_env_vars]
66
66
  required_env_vars_str = list_to_str(required_env_vars_names)
67
- raise EnvironmentError(
67
+ raise ConnectionError(
68
68
  f"The DetDatabase class requires the following environment variables: "
69
69
  f"{required_env_vars_str}. Environment variable '{d['name']}' "
70
70
  f"(description: '{d['description']}') not found."
@@ -104,11 +104,11 @@ class DetDatabase:
104
104
  Dataframe containing the queried data
105
105
 
106
106
  Raises:
107
- Exception: Raises an error if the SQL query fails
107
+ Exception: Raises an error if the SQL query fails.
108
108
  """
109
109
  try:
110
110
  df = pd.read_sql_query(query, con=self.engine)
111
- except Exception as e:
111
+ except Exception:
112
112
  # If query fails, close connection before raising the error
113
113
  self.terminate_engine()
114
114
  raise
@@ -171,10 +171,10 @@ class DetDatabase:
171
171
 
172
172
  Raises:
173
173
  ValueError: Raises an error if input arguments 'columns' and 'process_data' are not
174
- compatible
174
+ compatible.
175
175
  ValueError: Raises an error if the combination of trading dates and delivery dates
176
176
  is not valid.
177
- ValueError: Raises an error if no price data is found for user inputs
177
+ ValueError: Raises an error if no price data is found for user inputs.
178
178
  """
179
179
  # Input validation
180
180
  if process_data and columns is not None:
@@ -385,10 +385,10 @@ class DetDatabase:
385
385
 
386
386
  Raises:
387
387
  ValueError: Raises an error if input arguments 'columns' and 'process_data' are not
388
- compatible
388
+ compatible.
389
389
  ValueError: Raises an error if the combination of trading dates and delivery dates
390
390
  is not valid.
391
- ValueError: Raises an error if no price data is found for user inputs
391
+ ValueError: Raises an error if no price data is found for user inputs.
392
392
  """
393
393
  # Input validation
394
394
  if process_data and columns is not None:
@@ -569,7 +569,7 @@ class DetDatabase:
569
569
  Dataframe containing futures end-of-day settlement prices
570
570
 
571
571
  Raises:
572
- ValueError: Raises an error if no price data is found for user inputs
572
+ ValueError: Raises an error if no price data is found for user inputs.
573
573
  """
574
574
  # Set default column values
575
575
  if columns is None:
@@ -669,8 +669,8 @@ class DetDatabase:
669
669
  A dictionary containing the requested information
670
670
 
671
671
  Raises:
672
- ValueError: Raises an error if match with input filter value is not unique
673
- ValueError: Raises an error if the input filter value is not found
672
+ ValueError: Raises an error if match with input filter value is not unique.
673
+ ValueError: Raises an error if the input filter value is not found.
674
674
  """
675
675
  # Get commodity information for user-defined filtering criteria
676
676
  condition = f"WHERE {filter_column}='{filter_value}'"
@@ -1000,7 +1000,7 @@ class DetDatabase:
1000
1000
  Dataframe containing customer day-ahead auction bids
1001
1001
 
1002
1002
  Raises:
1003
- ValueError: Raises an error if no data is found for user inputs
1003
+ ValueError: Raises an error if no data is found for user inputs.
1004
1004
  """
1005
1005
  # Convert start delivery date from local timezone to UTC and string
1006
1006
  start_delivery_date = start_delivery_date.replace(tzinfo=ZoneInfo(local_timezone))
@@ -1104,8 +1104,8 @@ class DetDatabase:
1104
1104
  A dictionary containing the requested information
1105
1105
 
1106
1106
  Raises:
1107
- ValueError: Raises an error if match with input filter value is not unique
1108
- ValueError: Raises an error if the input filter value is not found
1107
+ ValueError: Raises an error if match with input filter value is not unique.
1108
+ ValueError: Raises an error if the input filter value is not found.
1109
1109
  """
1110
1110
  # Get client information for user-defined filtering criteria
1111
1111
  condition = f"WHERE {filter_column}='{filter_value}'"
@@ -1,10 +1,9 @@
1
1
  [tool.poetry]
2
2
  name = "detquantlib"
3
- version = "3.14.0"
3
+ version = "3.15.0"
4
4
  description = "An internal library containing functions and classes that can be used across Quant models."
5
- authors = ["DET"]
5
+ authors = ["Dynamic Energy Trading"]
6
6
  readme = "README.md"
7
- repository = "https://github.com/Dynamic-Energy-Trading/detquantlib"
8
7
  packages = [
9
8
  { include = "detquantlib" }, # Specifies the location of the package
10
9
  ]
@@ -21,6 +20,7 @@ pandas = "^2.3.3"
21
20
  pyarrow = "^21.0.0"
22
21
  scipy = "^1.15.2"
23
22
  colorlog = "^6.10.1"
23
+ redis = "^7.4.0"
24
24
 
25
25
  [tool.poetry.group.dev.dependencies]
26
26
  toml = "^0.10.2"
@@ -30,6 +30,7 @@ pytest-cov = "^7.0.0"
30
30
  black = "^25.9.0"
31
31
  darglint = "^1.8.1"
32
32
  isort = "^6.1.0"
33
+ ruff = "^0.15.4"
33
34
  colorama = "^0.4.6"
34
35
  pymarkdownlnt = "^0.9.32"
35
36
  md-toc = "^9.0.0"
@@ -53,6 +54,10 @@ known_third_party = ["invoke"]
53
54
  [tool.black]
54
55
  line-length = 99
55
56
 
57
+ [tool.ruff.lint]
58
+ select = ["B", "F"] # "B"=flake8-bugbear, "F"=Pyflakes
59
+ ignore = ["F403", "F405"]
60
+
56
61
  [tool.pymarkdown]
57
62
  plugins.md013.enabled = false # Disable line length requirements
58
63
  plugins.md040.enabled = false # Disable fenced code blocks language requirements
File without changes
File without changes