detquantlib 2.1.1__tar.gz → 3.0.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: detquantlib
3
- Version: 2.1.1
3
+ Version: 3.0.0
4
4
  Summary: An internal library containing functions and classes that can be used across Quant models.
5
5
  Author: DET
6
6
  Requires-Python: >=3.10,<4.0
@@ -1,4 +1,5 @@
1
1
  # Python built-in packages
2
+ import os
2
3
  import warnings
3
4
  from datetime import datetime
4
5
  from zoneinfo import ZoneInfo
@@ -16,42 +17,55 @@ class DetDatabase:
16
17
 
17
18
  def __init__(
18
19
  self,
19
- username: str,
20
- password: str,
21
- server: str,
22
- database: str,
23
20
  connection: pyodbc.Connection = None,
21
+ driver: str = "{ODBC Driver 18 for SQL Server}",
24
22
  ):
25
23
  """
26
24
  Constructor method.
27
25
 
28
26
  Args:
29
- username: Database username
30
- password: Database password
31
- server: Database server name
32
- database: Database name
33
27
  connection: Database connection object. This argument does not have to be passed
34
28
  when creating the object. It can be set after the object has been created, using
35
29
  the open_connection() method.
30
+ driver: ODBC driver
31
+
32
+ Raises:
33
+ EnvironmentError: Raises an error if environment variables are not defined
36
34
  """
37
- self.username = username
38
- self.password = password
39
- self.server = server
40
- self.database = database
41
35
  self.connection = connection
42
-
43
- # Define the ODBC driver
44
- self.driver = "{ODBC Driver 18 for SQL Server}"
36
+ self.driver = driver
37
+
38
+ # Check if environment variables needed by the class are defined
39
+ required_env_vars = [
40
+ dict(name="DET_DB_NAME", value=None, description="DET database name"),
41
+ dict(name="DET_DB_SERVER", value=None, description="DET database server name"),
42
+ dict(
43
+ name="DET_DB_USERNAME", value=None, description="Username to connect to database"
44
+ ),
45
+ dict(
46
+ name="DET_DB_PASSWORD", value=None, description="Password to connect to database"
47
+ ),
48
+ ]
49
+ available_env_vars = os.environ
50
+ for d in required_env_vars:
51
+ if d["name"] not in available_env_vars:
52
+ required_env_vars_names = [x["name"] for x in required_env_vars]
53
+ required_env_vars_str = ", ".join(f"'{x}'" for x in required_env_vars_names)
54
+ raise EnvironmentError(
55
+ f"The DetDatabase class requires the following environment variables: "
56
+ f"{required_env_vars_str}. Environment variable '{d['name']}' "
57
+ f"(description: '{d['description']}') not found."
58
+ )
45
59
 
46
60
  def open_connection(self):
47
61
  """Opens a connection to the database."""
48
62
  # Create the connection string
49
63
  connection_str = (
50
64
  f"DRIVER={self.driver};"
51
- f"SERVER={self.server};"
52
- f"DATABASE={self.database};"
53
- f"UID={self.username};"
54
- f"PWD={self.password}"
65
+ f"SERVER={os.getenv('DET_DB_SERVER')};"
66
+ f"DATABASE={os.getenv('DET_DB_NAME')};"
67
+ f"UID={os.getenv('DET_DB_USERNAME')};"
68
+ f"PWD={os.getenv('DET_DB_PASSWORD')}"
55
69
  )
56
70
  self.connection = pyodbc.connect(connection_str)
57
71
 
@@ -88,8 +102,7 @@ class DetDatabase:
88
102
 
89
103
  def load_entsoe_day_ahead_spot_prices(
90
104
  self,
91
- map_code: str,
92
- timezone: str,
105
+ commodity_name: str,
93
106
  start_trading_date: datetime = None,
94
107
  end_trading_date: datetime = None,
95
108
  start_delivery_date: datetime = None,
@@ -101,7 +114,7 @@ class DetDatabase:
101
114
  Loads entsoe day-ahead spot prices from the database.
102
115
 
103
116
  Args:
104
- map_code: Map code of the power country/region
117
+ commodity_name: Commodity name (as defined in the [META].[Commodity] database table)
105
118
  start_trading_date: Start trading date
106
119
  end_trading_date: End trading date
107
120
  Note: The user should provide either 'start_trading_date' and 'end_trading_date',
@@ -112,9 +125,6 @@ class DetDatabase:
112
125
  (i.e. delivery dates < end_date).
113
126
  Note: The user should provide either 'start_trading_date' and 'end_trading_date',
114
127
  or 'start_delivery_date' and 'end_delivery_date'.
115
- timezone: Timezone of the power country/region. This argument is important because
116
- ENTSOE provides all prices in the UTC timezone. We first convert the dates from
117
- UTC to the local timezone, and then filter for the requested delivery period.
118
128
  columns: Requested database table columns. Set columns=["*"] (i.e. as list) to get
119
129
  all columns.
120
130
  process_data: Indicates if data should be processed convert to standardized format
@@ -123,10 +133,13 @@ class DetDatabase:
123
133
  Dataframe containing day-ahead spot prices
124
134
 
125
135
  Raises:
126
- ValueError: Raises an error when input arguments 'columns' and 'process_data' are
127
- not compatible
128
- ValueError: Raises an error when the combination of trading dates and delivery dates
136
+ ValueError: Raises an error if input arguments 'columns' and 'process_data' are not
137
+ compatible
138
+ ValueError: Raises an error if the combination of trading dates and delivery dates
129
139
  is not valid.
140
+ ValueError: Raises an error if match with input commodity name is not unique
141
+ ValueError: Raises an error if input commodity is not supported
142
+ ValueError: Raises an error if no price data is found for user inputs
130
143
  """
131
144
  # Input validation
132
145
  if process_data and columns is not None:
@@ -164,6 +177,26 @@ class DetDatabase:
164
177
  else:
165
178
  columns_str = f"[{'], ['.join(columns)}]"
166
179
 
180
+ # Get commodity information (map code and local timezone)
181
+ # Note: The local timezone is important because ENTSOE provides all prices in the UTC
182
+ # timezone. We first convert the dates from UTC to the local timezone, and then filter
183
+ # for the requested delivery period.
184
+ commodity_info = self.load_commodities(
185
+ columns=["Timezone", "EntsoeMapCode"], conditions=f"WHERE Name='{commodity_name}'"
186
+ )
187
+ if commodity_info.shape[0] > 1:
188
+ raise ValueError(f"More than one match found with commodity '{commodity_name}'.")
189
+ elif commodity_info.shape[0] == 0:
190
+ supported_commodities = self.load_commodities(columns=["Name"])
191
+ supported_commodities_str = ", ".join(f"'{x}'" for x in supported_commodities["Name"])
192
+ raise ValueError(
193
+ f"Commodity '{commodity_name}' is not supported. Supported commodities: "
194
+ f"{supported_commodities_str}."
195
+ )
196
+ else:
197
+ map_code = commodity_info.loc[0, "EntsoeMapCode"]
198
+ timezone = commodity_info.loc[0, "Timezone"]
199
+
167
200
  # Convert start trading date to start delivery date
168
201
  if start_trading_date is not None:
169
202
  start_trading_date = pd.Timestamp(start_trading_date).floor("D")
@@ -198,6 +231,9 @@ class DetDatabase:
198
231
  df = self.query_db(query)
199
232
  self.close_connection()
200
233
 
234
+ if df.empty:
235
+ raise ValueError("No price data found for user-defined inputs.")
236
+
201
237
  # Sort data by delivery date
202
238
  df.sort_values(
203
239
  by=["DateTime(UTC)"], axis=0, ascending=True, inplace=True, ignore_index=True
@@ -211,40 +247,30 @@ class DetDatabase:
211
247
 
212
248
  # Process raw data and convert it to standardized format
213
249
  if process_data:
214
- df = DetDatabase.process_day_ahead_spot_prices(df, timezone)
250
+ df = DetDatabase.process_day_ahead_spot_prices(df, commodity_name, timezone)
215
251
 
216
252
  return df
217
253
 
218
254
  @staticmethod
219
- def process_day_ahead_spot_prices(df_in: pd.DataFrame, timezone: str) -> pd.DataFrame:
255
+ def process_day_ahead_spot_prices(
256
+ df_in: pd.DataFrame, commodity_name: str, timezone: str
257
+ ) -> pd.DataFrame:
220
258
  """
221
259
  Processes day-ahead spot prices and converts from ENTSOE format to standardized format.
222
260
 
223
261
  Args:
224
262
  df_in: Dataframe containing day-ahead spot prices
263
+ commodity_name: Commodity name (as defined in the [META].[Commodity] database table)
225
264
  timezone: Timezone of the power country/region
226
265
 
227
266
  Returns:
228
267
  Processed dataframe containing day-ahead spot prices
229
268
  """
230
- map_code_to_commodity_mapper = dict(
231
- NL={"commodity_name": "DutchPower", "product_code": "Q0B"}
232
- )
233
-
234
269
  # Initialize output dataframe
235
270
  df_out = pd.DataFrame()
236
271
 
237
272
  # Set commodity name
238
- commodity_name = [
239
- map_code_to_commodity_mapper[mc]["commodity_name"] for mc in df_in["MapCode"]
240
- ]
241
- df_out["CommodityName"] = commodity_name
242
-
243
- # Set product code
244
- product_code = [
245
- map_code_to_commodity_mapper[mc]["product_code"] for mc in df_in["MapCode"]
246
- ]
247
- df_out["ProductCode"] = product_code
273
+ df_out["CommodityName"] = [commodity_name] * df_in.shape[0]
248
274
 
249
275
  # Set trading date
250
276
  trading_date = [d - relativedelta(days=1, hour=0) for d in df_in[f"DateTime({timezone})"]]
@@ -267,8 +293,7 @@ class DetDatabase:
267
293
 
268
294
  def load_entsoe_imbalance_prices(
269
295
  self,
270
- map_code: str,
271
- timezone: str,
296
+ commodity_name: str,
272
297
  start_trading_date: datetime = None,
273
298
  end_trading_date: datetime = None,
274
299
  start_delivery_date: datetime = None,
@@ -280,10 +305,7 @@ class DetDatabase:
280
305
  Loads entsoe imbalance prices from the database.
281
306
 
282
307
  Args:
283
- map_code: Map code of the power country/region
284
- timezone: Timezone of the power country/region. This argument is important because
285
- ENTSOE provides all prices in the UTC timezone. We first convert the dates from
286
- UTC to the local timezone, and then filter for the requested delivery period.
308
+ commodity_name: Commodity name (as defined in the [META].[Commodity] database table)
287
309
  start_trading_date: Start trading date
288
310
  end_trading_date: End trading date
289
311
  Note: The user should provide either 'start_trading_date' and 'end_trading_date',
@@ -302,10 +324,13 @@ class DetDatabase:
302
324
  Dataframe containing imbalance prices
303
325
 
304
326
  Raises:
305
- ValueError: Raises an error when input arguments 'columns' and 'process_data' are
306
- not compatible
307
- ValueError: Raises an error when the combination of trading dates and delivery dates
327
+ ValueError: Raises an error if input arguments 'columns' and 'process_data' are not
328
+ compatible
329
+ ValueError: Raises an error if the combination of trading dates and delivery dates
308
330
  is not valid.
331
+ ValueError: Raises an error if match with input commodity name is not unique
332
+ ValueError: Raises an error if input commodity is not supported
333
+ ValueError: Raises an error if no price data is found for user inputs
309
334
  """
310
335
  # Input validation
311
336
  if process_data and columns is not None:
@@ -349,6 +374,26 @@ class DetDatabase:
349
374
  else:
350
375
  columns_str = f"[{'], ['.join(columns)}]"
351
376
 
377
+ # Get commodity information (map code and local timezone)
378
+ # Note: The local timezone is important because ENTSOE provides all prices in the UTC
379
+ # timezone. We first convert the dates from UTC to the local timezone, and then filter
380
+ # for the requested delivery period.
381
+ commodity_info = self.load_commodities(
382
+ columns=["Timezone", "EntsoeMapCode"], conditions=f"WHERE Name='{commodity_name}'"
383
+ )
384
+ if commodity_info.shape[0] > 1:
385
+ raise ValueError(f"More than one match found with commodity '{commodity_name}'.")
386
+ elif commodity_info.shape[0] == 0:
387
+ supported_commodities = self.load_commodities(columns=["Name"])
388
+ supported_commodities_str = ", ".join(f"'{x}'" for x in supported_commodities["Name"])
389
+ raise ValueError(
390
+ f"Commodity '{commodity_name}' is not supported. Supported commodities: "
391
+ f"{supported_commodities_str}."
392
+ )
393
+ else:
394
+ map_code = commodity_info.loc[0, "EntsoeMapCode"]
395
+ timezone = commodity_info.loc[0, "Timezone"]
396
+
352
397
  # Convert start trading date to start delivery date
353
398
  if start_trading_date is not None:
354
399
  start_delivery_date = pd.Timestamp(start_trading_date).floor("D")
@@ -382,6 +427,9 @@ class DetDatabase:
382
427
  df = self.query_db(query)
383
428
  self.close_connection()
384
429
 
430
+ if df.empty:
431
+ raise ValueError("No price data found for user-defined inputs.")
432
+
385
433
  # Sort data by delivery date
386
434
  df.sort_values(
387
435
  by=["DateTime(UTC)"], axis=0, ascending=True, inplace=True, ignore_index=True
@@ -395,40 +443,30 @@ class DetDatabase:
395
443
 
396
444
  # Process raw data and convert it to standardized format
397
445
  if process_data:
398
- df = DetDatabase.process_imbalance_prices(df, timezone)
446
+ df = DetDatabase.process_imbalance_prices(df, commodity_name, timezone)
399
447
 
400
448
  return df
401
449
 
402
450
  @staticmethod
403
- def process_imbalance_prices(df_in: pd.DataFrame, timezone: str) -> pd.DataFrame:
451
+ def process_imbalance_prices(
452
+ df_in: pd.DataFrame, commodity_name: str, timezone: str
453
+ ) -> pd.DataFrame:
404
454
  """
405
455
  Processes imbalance prices and converts from ENTSOE format to standardized format.
406
456
 
407
457
  Args:
408
458
  df_in: Dataframe containing imbalance prices
459
+ commodity_name: Commodity name (as defined in the [META].[Commodity] database table)
409
460
  timezone: Timezone of the power country/region
410
461
 
411
462
  Returns:
412
463
  Processed dataframe containing imbalance prices
413
464
  """
414
- map_code_to_commodity_mapper = dict(
415
- NL={"commodity_name": "DutchPower", "product_code": "Q0B"}
416
- )
417
-
418
465
  # Initialize output dataframe
419
466
  df_out = pd.DataFrame()
420
467
 
421
468
  # Set commodity name
422
- commodity_name = [
423
- map_code_to_commodity_mapper[mc]["commodity_name"] for mc in df_in["MapCode"]
424
- ]
425
- df_out["CommodityName"] = commodity_name
426
-
427
- # Set product code
428
- product_code = [
429
- map_code_to_commodity_mapper[mc]["product_code"] for mc in df_in["MapCode"]
430
- ]
431
- df_out["ProductCode"] = product_code
469
+ df_out["CommodityName"] = [commodity_name] * df_in.shape[0]
432
470
 
433
471
  # Set trading date
434
472
  df_out["TradingDate"] = df_in[f"DateTime({timezone})"].dt.floor("D").values
@@ -463,16 +501,19 @@ class DetDatabase:
463
501
  of trading dates.
464
502
 
465
503
  Args:
466
- commodity_name: Commodity name
504
+ commodity_name: Commodity name (as defined in the [META].[Commodity] database table)
467
505
  start_trading_date: Start trading date
468
506
  end_trading_date: End trading date
469
507
  tenors: Product tenors (e.g. "Month", "Quarter", "Year")
470
- delivery_type: Delivery type
508
+ delivery_type: Delivery type ("Base", "Peak", "Offpeak")
471
509
  columns: Requested database table columns. Set columns=["*"] (i.e. as list) to get
472
510
  all columns.
473
511
 
474
512
  Returns:
475
513
  Dataframe containing futures end-of-day settlement prices
514
+
515
+ Raises:
516
+ ValueError: Raises an error if no price data is found for user inputs
476
517
  """
477
518
  # Set default column values
478
519
  if columns is None:
@@ -507,6 +548,9 @@ class DetDatabase:
507
548
  df = self.query_db(query)
508
549
  self.close_connection()
509
550
 
551
+ if df.empty:
552
+ raise ValueError("No price data found for user-defined inputs.")
553
+
510
554
  # Sort data
511
555
  df.sort_values(
512
556
  by=["TradingDate", "DeliveryStart", "DeliveryEnd"],
@@ -523,12 +567,84 @@ class DetDatabase:
523
567
 
524
568
  return df
525
569
 
570
+ def load_commodities(self, columns: list = None, conditions: str = None) -> pd.DataFrame:
571
+ """
572
+ General method to load data from the database's commodity table.
573
+
574
+ Args:
575
+ columns: Requested database table columns. Set columns=["*"] (i.e. as list) to get
576
+ all columns.
577
+ conditions: Optional conditions to add to SQL query. E.g. "WHERE Name='DutchPower'".
578
+
579
+ Returns:
580
+ Table data
581
+ """
582
+ # Set default column values
583
+ if columns is None:
584
+ columns = ["*"]
585
+
586
+ # Convert columns from list to string
587
+ if len(columns) == 1:
588
+ columns_str = str(columns[0])
589
+ else:
590
+ columns_str = f"[{'], ['.join(columns)}]"
591
+
592
+ # Create query
593
+ table = DetDatabaseDefinitions.DEFINITIONS["table_name_commodity"]
594
+ query = f"SELECT {columns_str} FROM {table} {conditions}"
595
+
596
+ # Query db
597
+ self.open_connection()
598
+ df = self.query_db(query)
599
+ self.close_connection()
600
+
601
+ return df
602
+
603
+ def get_commodity_info(
604
+ self, filter_column: str, filter_value: str, info_columns: list
605
+ ) -> dict:
606
+ """
607
+ Finds information related to a specific, user-defined commodity.
608
+
609
+ Args:
610
+ filter_column: Column used to filter data for one specific commodity
611
+ filter_value: Value used to filter data for one specific commodity
612
+ info_columns: Columns containing the requested information
613
+
614
+ Returns:
615
+ A dictionary containing the requested information
616
+
617
+ Raises:
618
+ ValueError: Raises an error if match with input filter value is not unique
619
+ ValueError: Raises an error if the input filter value is not found
620
+ """
621
+ # Get commodity information for user-defined filtering criteria
622
+ condition = f"WHERE {filter_column}='{filter_value}'"
623
+ commodity_info = self.load_commodities(columns=info_columns, conditions=condition)
624
+
625
+ # Validate response
626
+ if commodity_info.shape[0] > 1:
627
+ raise ValueError(f"More than one match found for {filter_column}={filter_value}.")
628
+
629
+ elif commodity_info.shape[0] == 0:
630
+ available_values = self.load_commodities(columns=[filter_column])
631
+ available_values_str = ", ".join(f"'{x}'" for x in available_values[filter_column])
632
+ raise ValueError(
633
+ f"Value {filter_value} not found in column '{filter_column}'. Available values: "
634
+ f"{available_values_str}."
635
+ )
636
+
637
+ # Convert dataframe row to dict
638
+ commodity_info = commodity_info.loc[0, :].to_dict()
639
+
640
+ return commodity_info
641
+
526
642
 
527
643
  class DetDatabaseDefinitions:
528
644
  """A class containing some hard-coded definitions related to the DET database."""
529
645
 
530
646
  DEFINITIONS = dict(
531
- driver="{ODBC Driver 18 for SQL Server}",
647
+ table_name_commodity="[META].[Commodity]",
532
648
  table_name_entsoe_day_ahead_spot_price="[ENTSOE].[DayAheadSpotPrice]",
533
649
  table_name_entsoe_imbalance_price="[ENTSOE].[ImbalancePrice]",
534
650
  table_name_futures_eod_settlement_price="[VW].[EODSettlementPrice]",
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "detquantlib"
3
- version = "2.1.1"
3
+ version = "3.0.0"
4
4
  description = "An internal library containing functions and classes that can be used across Quant models."
5
5
  authors = ["DET"]
6
6
  readme = "README.md"
File without changes
File without changes