detquantlib 2.0.4__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.
@@ -0,0 +1 @@
1
+ # Empty file required by poetry. Do not delete.
@@ -0,0 +1,535 @@
1
+ # Python built-in packages
2
+ import warnings
3
+ from datetime import datetime
4
+ from zoneinfo import ZoneInfo
5
+
6
+ # Third-party packages
7
+ import pandas as pd
8
+ import pyodbc
9
+ from dateutil.relativedelta import *
10
+
11
+
12
+ class DetDatabase:
13
+ """
14
+ A class to easily interact with the DET database, including fetching and processing data.
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ username: str,
20
+ password: str,
21
+ server: str,
22
+ database: str,
23
+ connection: pyodbc.Connection = None,
24
+ ):
25
+ """
26
+ Constructor method.
27
+
28
+ Args:
29
+ username: Database username
30
+ password: Database password
31
+ server: Database server name
32
+ database: Database name
33
+ connection: Database connection object. This argument does not have to be passed
34
+ when creating the object. It can be set after the object has been created, using
35
+ the open_connection() method.
36
+ """
37
+ self.username = username
38
+ self.password = password
39
+ self.server = server
40
+ self.database = database
41
+ self.connection = connection
42
+
43
+ # Define the ODBC driver
44
+ self.driver = "{ODBC Driver 18 for SQL Server}"
45
+
46
+ def open_connection(self):
47
+ """Opens a connection to the database."""
48
+ # Create the connection string
49
+ connection_str = (
50
+ f"DRIVER={self.driver};"
51
+ f"SERVER={self.server};"
52
+ f"DATABASE={self.database};"
53
+ f"UID={self.username};"
54
+ f"PWD={self.password}"
55
+ )
56
+ self.connection = pyodbc.connect(connection_str)
57
+
58
+ def close_connection(self):
59
+ """Closes the connection to the database."""
60
+ self.connection.close()
61
+
62
+ def query_db(self, query: str) -> pd.DataFrame:
63
+ """
64
+ Short utility method to make an SQL query to the database.
65
+
66
+ Args:
67
+ query: SQL query
68
+
69
+ Returns:
70
+ Dataframe containing the queried data
71
+
72
+ Raises:
73
+ Exception: Raises an error if the SQL query fails
74
+ """
75
+ with warnings.catch_warnings():
76
+ # Pandas UserWarning returned when using pandas with pyodbc. Disable warning
77
+ # temporarily for the SQL query.
78
+ warnings.simplefilter("ignore", category=UserWarning)
79
+
80
+ try:
81
+ df = pd.read_sql(query, self.connection)
82
+ except Exception as e:
83
+ # If query fails, close connection before raising the error
84
+ self.close_connection()
85
+ raise
86
+
87
+ return df
88
+
89
+ def load_entsoe_day_ahead_spot_prices(
90
+ self,
91
+ map_code: str,
92
+ timezone: str,
93
+ start_trading_date: datetime = None,
94
+ end_trading_date: datetime = None,
95
+ start_delivery_date: datetime = None,
96
+ end_delivery_date: datetime = None,
97
+ columns: list = None,
98
+ process_data: bool = True,
99
+ ) -> pd.DataFrame:
100
+ """
101
+ Loads entsoe day-ahead spot prices from the database.
102
+
103
+ Args:
104
+ map_code: Map code of the power country/region
105
+ start_trading_date: Start trading date
106
+ end_trading_date: End trading date
107
+ Note: The user should provide either 'start_trading_date' and 'end_trading_date',
108
+ or 'start_delivery_date' and 'end_delivery_date'.
109
+ start_delivery_date: Delivery start date. The start datetime is included in the
110
+ filtering (i.e. delivery dates >= start_date).
111
+ end_delivery_date: Delivery end date. The end datetime is excluded from the filtering
112
+ (i.e. delivery dates < end_date).
113
+ Note: The user should provide either 'start_trading_date' and 'end_trading_date',
114
+ 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
+ columns: Requested database table columns. Set columns=["*"] (i.e. as list) to get
119
+ all columns.
120
+ process_data: Indicates if data should be processed convert to standardized format
121
+
122
+ Returns:
123
+ Dataframe containing day-ahead spot prices
124
+
125
+ 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
129
+ is not valid.
130
+ """
131
+ # Input validation
132
+ if process_data and columns is not None:
133
+ raise ValueError(
134
+ "Input argument 'process_data' can only be true if input argument 'columns' "
135
+ "is None."
136
+ )
137
+ if not (
138
+ start_trading_date is not None
139
+ and end_trading_date is not None
140
+ and start_delivery_date is None
141
+ and end_delivery_date is None
142
+ ) and not (
143
+ start_trading_date is None
144
+ and end_trading_date is None
145
+ and start_delivery_date is not None
146
+ and end_delivery_date is not None
147
+ ):
148
+ raise ValueError(
149
+ "Either 'start_trading_date' and 'end_trading_date', or 'start_delivery_date' "
150
+ "and 'end_delivery_date' should be provided."
151
+ )
152
+
153
+ # Set default column values
154
+ if columns is None:
155
+ columns = ["DateTime(UTC)", "MapCode", "Price(Currency/MWh)", "Currency"]
156
+
157
+ # Always add delivery date column
158
+ if "DateTime(UTC)" not in columns and columns != ["*"]:
159
+ columns.append("DateTime(UTC)")
160
+
161
+ # Convert columns from list to string
162
+ if len(columns) == 1:
163
+ columns_str = str(columns[0])
164
+ else:
165
+ columns_str = f"[{'], ['.join(columns)}]"
166
+
167
+ # Convert start trading date to start delivery date
168
+ if start_trading_date is not None:
169
+ start_trading_date = pd.Timestamp(start_trading_date).floor("D")
170
+ start_delivery_date = start_trading_date + relativedelta(days=1)
171
+
172
+ # Convert start delivery date from local timezone to UTC and string
173
+ start_delivery_date = start_delivery_date.replace(tzinfo=ZoneInfo(timezone))
174
+ start_delivery_date = start_delivery_date.astimezone(ZoneInfo("UTC"))
175
+ start_date_str = start_delivery_date.strftime("%Y-%m-%d %H:%M:%S")
176
+
177
+ # Convert end trading date to end delivery date
178
+ if end_trading_date is not None:
179
+ end_trading_date = pd.Timestamp(end_trading_date).floor("D")
180
+ end_delivery_date = end_trading_date + relativedelta(days=2)
181
+
182
+ # Convert end date to UTC and string
183
+ end_delivery_date = end_delivery_date.replace(tzinfo=ZoneInfo(timezone))
184
+ end_delivery_date = end_delivery_date.astimezone(ZoneInfo("UTC"))
185
+ end_date_str = end_delivery_date.strftime("%Y-%m-%d %H:%M:%S")
186
+
187
+ # Create query
188
+ table = DetDatabaseDefinitions.DEFINITIONS["table_name_entsoe_day_ahead_spot_price"]
189
+ query = (
190
+ f"SELECT {columns_str} FROM {table} "
191
+ f"WHERE MapCode='{map_code}' "
192
+ f"AND [DateTime(UTC)]>='{start_date_str}' "
193
+ f"AND [DateTime(UTC)]<'{end_date_str}' "
194
+ )
195
+
196
+ # Query db
197
+ self.open_connection()
198
+ df = self.query_db(query)
199
+ self.close_connection()
200
+
201
+ # Sort data by delivery date
202
+ df.sort_values(
203
+ by=["DateTime(UTC)"], axis=0, ascending=True, inplace=True, ignore_index=True
204
+ )
205
+
206
+ # Add column with delivery date expressed in local timezone
207
+ datetime_column_name = f"DateTime({timezone})"
208
+ df[datetime_column_name] = df["DateTime(UTC)"].dt.tz_localize("UTC")
209
+ df[datetime_column_name] = df[datetime_column_name].dt.tz_convert(timezone)
210
+ df[datetime_column_name] = df[datetime_column_name].dt.tz_localize(None)
211
+
212
+ # Process raw data and convert it to standardized format
213
+ if process_data:
214
+ df = DetDatabase.process_day_ahead_spot_prices(df, timezone)
215
+
216
+ return df
217
+
218
+ @staticmethod
219
+ def process_day_ahead_spot_prices(df_in: pd.DataFrame, timezone: str) -> pd.DataFrame:
220
+ """
221
+ Processes day-ahead spot prices and converts from ENTSOE format to standardized format.
222
+
223
+ Args:
224
+ df_in: Dataframe containing day-ahead spot prices
225
+ timezone: Timezone of the power country/region
226
+
227
+ Returns:
228
+ Processed dataframe containing day-ahead spot prices
229
+ """
230
+ map_code_to_commodity_mapper = dict(
231
+ NL={"commodity_name": "DutchPower", "product_code": "Q0B"}
232
+ )
233
+
234
+ # Initialize output dataframe
235
+ df_out = pd.DataFrame()
236
+
237
+ # 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
248
+
249
+ # Set trading date
250
+ trading_date = [d - relativedelta(days=1, hour=0) for d in df_in[f"DateTime({timezone})"]]
251
+ df_out["TradingDate"] = trading_date
252
+
253
+ # Set delivery start date
254
+ df_out["DeliveryStart"] = df_in[f"DateTime({timezone})"].values
255
+
256
+ # Set delivery end date
257
+ delivery_end = [d + relativedelta(hours=1) for d in df_in[f"DateTime({timezone})"]]
258
+ df_out["DeliveryEnd"] = delivery_end
259
+
260
+ # Set tenor
261
+ df_out["Tenor"] = "Spot"
262
+
263
+ # Set price
264
+ df_out["Price"] = df_in["Price(Currency/MWh)"].values
265
+
266
+ return df_out
267
+
268
+ def load_entsoe_imbalance_prices(
269
+ self,
270
+ map_code: str,
271
+ timezone: str,
272
+ start_trading_date: datetime = None,
273
+ end_trading_date: datetime = None,
274
+ start_delivery_date: datetime = None,
275
+ end_delivery_date: datetime = None,
276
+ columns: list = None,
277
+ process_data: bool = True,
278
+ ) -> pd.DataFrame:
279
+ """
280
+ Loads entsoe imbalance prices from the database.
281
+
282
+ 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.
287
+ start_trading_date: Start trading date
288
+ end_trading_date: End trading date
289
+ Note: The user should provide either 'start_trading_date' and 'end_trading_date',
290
+ or 'start_delivery_date' and 'end_delivery_date'.
291
+ start_delivery_date: Delivery start date. The start datetime is included in the
292
+ filtering (i.e. delivery dates >= start_date).
293
+ end_delivery_date: Delivery end date. The end datetime is excluded from the filtering
294
+ (i.e. delivery dates < end_date).
295
+ Note: The user should provide either 'start_trading_date' and 'end_trading_date',
296
+ or 'start_delivery_date' and 'end_delivery_date'.
297
+ columns: Requested database table columns. Set columns=["*"] (i.e. as list) to get
298
+ all columns.
299
+ process_data: Indicates if data should be processed convert to standardized format
300
+
301
+ Returns:
302
+ Dataframe containing imbalance prices
303
+
304
+ 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
308
+ is not valid.
309
+ """
310
+ # Input validation
311
+ if process_data and columns is not None:
312
+ raise ValueError(
313
+ "Input argument 'process_data' can only be true if input argument 'columns' "
314
+ "is None."
315
+ )
316
+ if not (
317
+ start_trading_date is not None
318
+ and end_trading_date is not None
319
+ and start_delivery_date is None
320
+ and end_delivery_date is None
321
+ ) and not (
322
+ start_trading_date is None
323
+ and end_trading_date is None
324
+ and start_delivery_date is not None
325
+ and end_delivery_date is not None
326
+ ):
327
+ raise ValueError(
328
+ "Either 'start_trading_date' and 'end_trading_date', or 'start_delivery_date' "
329
+ "and 'end_delivery_date' should be provided."
330
+ )
331
+
332
+ # Set default column values
333
+ if columns is None:
334
+ columns = [
335
+ "DateTime(UTC)",
336
+ "MapCode",
337
+ "PositiveImbalancePrice",
338
+ "NegativeImbalancePrice",
339
+ "Currency",
340
+ ]
341
+
342
+ # Always add delivery date column
343
+ if "DateTime(UTC)" not in columns and columns != ["*"]:
344
+ columns.append("DateTime(UTC)")
345
+
346
+ # Convert columns from list to string
347
+ if len(columns) == 1:
348
+ columns_str = str(columns[0])
349
+ else:
350
+ columns_str = f"[{'], ['.join(columns)}]"
351
+
352
+ # Convert start trading date to start delivery date
353
+ if start_trading_date is not None:
354
+ start_delivery_date = pd.Timestamp(start_trading_date).floor("D")
355
+
356
+ # Convert start delivery date from local timezone to UTC and string
357
+ start_delivery_date = start_delivery_date.replace(tzinfo=ZoneInfo(timezone))
358
+ start_delivery_date = start_delivery_date.astimezone(ZoneInfo("UTC"))
359
+ start_date_str = start_delivery_date.strftime("%Y-%m-%d %H:%M:%S")
360
+
361
+ # Convert end trading date to end delivery date
362
+ if end_trading_date is not None:
363
+ end_trading_date = pd.Timestamp(end_trading_date).floor("D")
364
+ end_delivery_date = end_trading_date + relativedelta(days=1)
365
+
366
+ # Convert end date to UTC and string
367
+ end_delivery_date = end_delivery_date.replace(tzinfo=ZoneInfo(timezone))
368
+ end_delivery_date = end_delivery_date.astimezone(ZoneInfo("UTC"))
369
+ end_date_str = end_delivery_date.strftime("%Y-%m-%d %H:%M:%S")
370
+
371
+ # Create query
372
+ table = DetDatabaseDefinitions.DEFINITIONS["table_name_entsoe_imbalance_price"]
373
+ query = (
374
+ f"SELECT {columns_str} FROM {table} "
375
+ f"WHERE MapCode='{map_code}' "
376
+ f"AND [DateTime(UTC)]>='{start_date_str}' "
377
+ f"AND [DateTime(UTC)]<'{end_date_str}' "
378
+ )
379
+
380
+ # Query db
381
+ self.open_connection()
382
+ df = self.query_db(query)
383
+ self.close_connection()
384
+
385
+ # Sort data by delivery date
386
+ df.sort_values(
387
+ by=["DateTime(UTC)"], axis=0, ascending=True, inplace=True, ignore_index=True
388
+ )
389
+
390
+ # Add column with delivery date expressed in local timezone
391
+ datetime_column_name = f"DateTime({timezone})"
392
+ df[datetime_column_name] = df["DateTime(UTC)"].dt.tz_localize("UTC")
393
+ df[datetime_column_name] = df[datetime_column_name].dt.tz_convert(timezone)
394
+ df[datetime_column_name] = df[datetime_column_name].dt.tz_localize(None)
395
+
396
+ # Process raw data and convert it to standardized format
397
+ if process_data:
398
+ df = DetDatabase.process_imbalance_prices(df, timezone)
399
+
400
+ return df
401
+
402
+ @staticmethod
403
+ def process_imbalance_prices(df_in: pd.DataFrame, timezone: str) -> pd.DataFrame:
404
+ """
405
+ Processes imbalance prices and converts from ENTSOE format to standardized format.
406
+
407
+ Args:
408
+ df_in: Dataframe containing imbalance prices
409
+ timezone: Timezone of the power country/region
410
+
411
+ Returns:
412
+ Processed dataframe containing imbalance prices
413
+ """
414
+ map_code_to_commodity_mapper = dict(
415
+ NL={"commodity_name": "DutchPower", "product_code": "Q0B"}
416
+ )
417
+
418
+ # Initialize output dataframe
419
+ df_out = pd.DataFrame()
420
+
421
+ # 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
432
+
433
+ # Set trading date
434
+ df_out["TradingDate"] = df_in[f"DateTime({timezone})"].dt.floor("D").values
435
+
436
+ # Set delivery start date
437
+ df_out["DeliveryStart"] = df_in[f"DateTime({timezone})"].values
438
+
439
+ # Set delivery end date
440
+ delivery_end = [d + relativedelta(minutes=15) for d in df_in[f"DateTime({timezone})"]]
441
+ df_out["DeliveryEnd"] = delivery_end
442
+
443
+ # Set tenor
444
+ df_out["Tenor"] = "Imbalance"
445
+
446
+ # Set price
447
+ df_out["PositiveImbalancePrice"] = df_in["PositiveImbalancePrice"].values
448
+ df_out["NegativeImbalancePrice"] = df_in["NegativeImbalancePrice"].values
449
+
450
+ return df_out
451
+
452
+ def load_futures_eod_settlement_prices(
453
+ self,
454
+ commodity_name: str,
455
+ start_trading_date: datetime,
456
+ end_trading_date: datetime,
457
+ tenors: list,
458
+ delivery_type: str,
459
+ columns: list = None,
460
+ ) -> pd.DataFrame:
461
+ """
462
+ Loads futures end-of-day settlement prices from the database, over a user-defined range
463
+ of trading dates.
464
+
465
+ Args:
466
+ commodity_name: Commodity name
467
+ start_trading_date: Start trading date
468
+ end_trading_date: End trading date
469
+ tenors: Product tenors (e.g. "Month", "Quarter", "Year")
470
+ delivery_type: Delivery type
471
+ columns: Requested database table columns. Set columns=["*"] (i.e. as list) to get
472
+ all columns.
473
+
474
+ Returns:
475
+ Dataframe containing futures end-of-day settlement prices
476
+ """
477
+ # Set default column values
478
+ if columns is None:
479
+ columns = ["*"]
480
+
481
+ # Convert columns from list to string
482
+ if len(columns) == 1:
483
+ columns_str = str(columns[0])
484
+ else:
485
+ columns_str = f"[{'], ['.join(columns)}]"
486
+
487
+ # Convert tenors from list to string
488
+ tenors_str = f"({' ,'.join([repr(item) for item in tenors])})"
489
+
490
+ # Convert dates from datetime to string
491
+ start_trading_date_str = start_trading_date.strftime("%Y-%m-%d")
492
+ end_trading_date_str = end_trading_date.strftime("%Y-%m-%d")
493
+
494
+ # Create query
495
+ table = DetDatabaseDefinitions.DEFINITIONS["table_name_futures_eod_settlement_price"]
496
+ query = (
497
+ f"SELECT {columns_str} FROM {table} "
498
+ f"WHERE CommodityName='{commodity_name}' "
499
+ f"AND TradingDate>='{start_trading_date_str}' "
500
+ f"AND TradingDate<='{end_trading_date_str}' "
501
+ f"AND Tenor IN {tenors_str} "
502
+ f"AND DeliveryType='{delivery_type}'"
503
+ )
504
+
505
+ # Query db
506
+ self.open_connection()
507
+ df = self.query_db(query)
508
+ self.close_connection()
509
+
510
+ # Sort data
511
+ df.sort_values(
512
+ by=["TradingDate", "DeliveryStart", "DeliveryEnd"],
513
+ axis=0,
514
+ ascending=True,
515
+ inplace=True,
516
+ ignore_index=True,
517
+ )
518
+
519
+ # Convert dates from datetime.date to pd.Timestamp
520
+ df["TradingDate"] = pd.DatetimeIndex(df["TradingDate"])
521
+ df["DeliveryStart"] = pd.DatetimeIndex(df["DeliveryStart"])
522
+ df["DeliveryEnd"] = pd.DatetimeIndex(df["DeliveryEnd"])
523
+
524
+ return df
525
+
526
+
527
+ class DetDatabaseDefinitions:
528
+ """A class containing some hard-coded definitions related to the DET database."""
529
+
530
+ DEFINITIONS = dict(
531
+ driver="{ODBC Driver 18 for SQL Server}",
532
+ table_name_entsoe_day_ahead_spot_price="[ENTSOE].[DayAheadSpotPrice]",
533
+ table_name_entsoe_imbalance_price="[ENTSOE].[ImbalancePrice]",
534
+ table_name_futures_eod_settlement_price="[FUT].[EODSettlementPrice]",
535
+ )