detquantlib 3.8.1__tar.gz → 3.10.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 (21) hide show
  1. {detquantlib-3.8.1 → detquantlib-3.10.0}/PKG-INFO +2 -2
  2. {detquantlib-3.8.1 → detquantlib-3.10.0}/detquantlib/data/databases/detdatabase.py +142 -110
  3. {detquantlib-3.8.1 → detquantlib-3.10.0}/pyproject.toml +2 -2
  4. {detquantlib-3.8.1 → detquantlib-3.10.0}/LICENSE.txt +0 -0
  5. {detquantlib-3.8.1 → detquantlib-3.10.0}/README.md +0 -0
  6. {detquantlib-3.8.1 → detquantlib-3.10.0}/detquantlib/__init__.py +0 -0
  7. {detquantlib-3.8.1 → detquantlib-3.10.0}/detquantlib/data/__init__.py +0 -0
  8. {detquantlib-3.8.1 → detquantlib-3.10.0}/detquantlib/data/entsoe/entsoe.py +0 -0
  9. {detquantlib-3.8.1 → detquantlib-3.10.0}/detquantlib/data/sftp/sftp.py +0 -0
  10. {detquantlib-3.8.1 → detquantlib-3.10.0}/detquantlib/dates/__init__.py +0 -0
  11. {detquantlib-3.8.1 → detquantlib-3.10.0}/detquantlib/dates/dates.py +0 -0
  12. {detquantlib-3.8.1 → detquantlib-3.10.0}/detquantlib/figures/__init__.py +0 -0
  13. {detquantlib-3.8.1 → detquantlib-3.10.0}/detquantlib/figures/plotly_figures.py +0 -0
  14. {detquantlib-3.8.1 → detquantlib-3.10.0}/detquantlib/outputs/__init__.py +0 -0
  15. {detquantlib-3.8.1 → detquantlib-3.10.0}/detquantlib/outputs/outputs_interface.py +0 -0
  16. {detquantlib-3.8.1 → detquantlib-3.10.0}/detquantlib/stats/__init__.py +0 -0
  17. {detquantlib-3.8.1 → detquantlib-3.10.0}/detquantlib/stats/data_analysis.py +0 -0
  18. {detquantlib-3.8.1 → detquantlib-3.10.0}/detquantlib/tradable_products/__init__.py +0 -0
  19. {detquantlib-3.8.1 → detquantlib-3.10.0}/detquantlib/tradable_products/tradable_products.py +0 -0
  20. {detquantlib-3.8.1 → detquantlib-3.10.0}/detquantlib/utils/__init__.py +0 -0
  21. {detquantlib-3.8.1 → detquantlib-3.10.0}/detquantlib/utils/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detquantlib
3
- Version: 3.8.1
3
+ Version: 3.10.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
6
  Author: DET
@@ -15,9 +15,9 @@ Requires-Dist: numpy (>=2.2.6,<3.0.0)
15
15
  Requires-Dist: pandas (>=2.3.3,<3.0.0)
16
16
  Requires-Dist: paramiko (>=4.0.0,<5.0.0)
17
17
  Requires-Dist: plotly (>=6.3.1,<7.0.0)
18
- Requires-Dist: pyodbc (>=5.2.0,<6.0.0)
19
18
  Requires-Dist: python-dotenv (>=1.1.0,<2.0.0)
20
19
  Requires-Dist: scipy (>=1.15.2,<2.0.0)
20
+ Requires-Dist: sqlalchemy (>=2.0.43,<3.0.0)
21
21
  Project-URL: Repository, https://github.com/Dynamic-Energy-Trading/detquantlib
22
22
  Description-Content-Type: text/markdown
23
23
 
@@ -1,13 +1,13 @@
1
1
  # Python built-in packages
2
2
  import os
3
- import warnings
4
3
  from datetime import datetime
4
+ from urllib.parse import quote_plus
5
5
  from zoneinfo import ZoneInfo
6
6
 
7
7
  # Third-party packages
8
8
  import pandas as pd
9
- import pyodbc
10
9
  from dateutil.relativedelta import *
10
+ from sqlalchemy import Engine, create_engine, text
11
11
 
12
12
 
13
13
  class DetDatabase:
@@ -15,36 +15,34 @@ class DetDatabase:
15
15
  A class to easily interact with the DET database, including fetching and processing data.
16
16
  """
17
17
 
18
- def __init__(
19
- self,
20
- connection: pyodbc.Connection = None,
21
- driver: str = "{ODBC Driver 18 for SQL Server}",
22
- ):
18
+ def __init__(self, engine: Engine = None, driver: str = "ODBC Driver 18 for SQL Server"):
23
19
  """
24
20
  Constructor method.
25
21
 
26
22
  Args:
27
- connection: Database connection object. This argument does not have to be passed
28
- when creating the object. It can be set after the object has been created, using
29
- the open_connection() method.
23
+ engine: Instance of SQLAlchemy engine. If None, the instance is automatically
24
+ initialized.
30
25
  driver: ODBC driver
26
+ """
27
+ # Only allow object creation if mandatory environment variables exist
28
+ DetDatabase.check_environment_variables()
29
+
30
+ self.driver = driver
31
+ self.engine = engine if engine is not None else self.initialize_engine()
32
+
33
+ @staticmethod
34
+ def check_environment_variables():
35
+ """
36
+ Checks if environment variables needed by the class are defined.
31
37
 
32
38
  Raises:
33
39
  EnvironmentError: Raises an error if environment variables are not defined
34
40
  """
35
- self.connection = connection
36
- self.driver = driver
37
-
38
- # Check if environment variables needed by the class are defined
39
41
  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
- ),
42
+ dict(name="DET_DB_NAME", value=None, description="Database name"),
43
+ dict(name="DET_DB_SERVER", value=None, description="Server name"),
44
+ dict(name="DET_DB_USERNAME", value=None, description="Username"),
45
+ dict(name="DET_DB_PASSWORD", value=None, description="Password"),
48
46
  ]
49
47
  available_env_vars = os.environ
50
48
  for d in required_env_vars:
@@ -57,21 +55,28 @@ class DetDatabase:
57
55
  f"(description: '{d['description']}') not found."
58
56
  )
59
57
 
60
- def open_connection(self):
61
- """Opens a connection to the database."""
58
+ def initialize_engine(self) -> Engine:
59
+ """
60
+ Initializes the SQLAlchemy engine.
61
+
62
+ Returns:
63
+ Instance of SQLAlchemy engine
64
+ """
62
65
  # Create the connection string
63
66
  connection_str = (
64
67
  f"DRIVER={self.driver};"
65
68
  f"SERVER={os.getenv('DET_DB_SERVER')};"
66
69
  f"DATABASE={os.getenv('DET_DB_NAME')};"
67
70
  f"UID={os.getenv('DET_DB_USERNAME')};"
68
- f"PWD={os.getenv('DET_DB_PASSWORD')}"
71
+ f"PWD={quote_plus(os.getenv('DET_DB_PASSWORD'))}"
69
72
  )
70
- self.connection = pyodbc.connect(connection_str)
73
+ url = quote_plus(connection_str)
74
+ engine = create_engine(f"mssql+pyodbc:///?odbc_connect={url}")
75
+ return engine
71
76
 
72
- def close_connection(self):
73
- """Closes the connection to the database."""
74
- self.connection.close()
77
+ def terminate_engine(self):
78
+ """Disconnects the SQLAlchemy engine and releases all underlying resources."""
79
+ self.engine.dispose()
75
80
 
76
81
  def query_db(self, query: str) -> pd.DataFrame:
77
82
  """
@@ -86,20 +91,34 @@ class DetDatabase:
86
91
  Raises:
87
92
  Exception: Raises an error if the SQL query fails
88
93
  """
89
- with warnings.catch_warnings():
90
- # Pandas UserWarning returned when using pandas with pyodbc. Disable warning
91
- # temporarily for the SQL query.
92
- warnings.simplefilter("ignore", category=UserWarning)
93
-
94
- try:
95
- df = pd.read_sql(query, self.connection)
96
- except Exception as e:
97
- # If query fails, close connection before raising the error
98
- self.close_connection()
99
- raise
94
+ try:
95
+ df = pd.read_sql_query(query, con=self.engine)
96
+ except Exception as e:
97
+ # If query fails, close connection before raising the error
98
+ self.terminate_engine()
99
+ raise
100
100
 
101
101
  return df
102
102
 
103
+ @staticmethod
104
+ def get_table_name(def_key: str) -> str:
105
+ """
106
+ Gets the name of a database table, according to the following logic:
107
+ 1) First, try to get the table name from the corresponding environment variable.
108
+ 2) If the environment variable is not defined, revert to the default table name.
109
+
110
+ Args:
111
+ def_key: Definitions dictionary key corresponding to the desired table
112
+
113
+ Returns:
114
+ Database table name
115
+ """
116
+ definition = DetDatabaseDefinitions.DEFINITIONS[def_key]
117
+ table = os.getenv(definition["env_variable"])
118
+ if table is None:
119
+ table = definition["default_table_name"]
120
+ return table
121
+
103
122
  def load_entsoe_day_ahead_spot_prices(
104
123
  self,
105
124
  commodity_name: str,
@@ -210,19 +229,15 @@ class DetDatabase:
210
229
  end_delivery_date = end_delivery_date.astimezone(ZoneInfo("UTC"))
211
230
  end_date_str = end_delivery_date.strftime("%Y-%m-%d %H:%M:%S")
212
231
 
213
- # Create query
214
- table = DetDatabaseDefinitions.DEFINITIONS["table_name_entsoe_day_ahead_spot_price"]
232
+ # Query db
233
+ table = DetDatabase.get_table_name("entsoe_day_ahead_spot_price")
215
234
  query = (
216
235
  f"SELECT {columns_str} FROM {table} "
217
236
  f"WHERE MapCode='{map_code}' "
218
237
  f"AND [DateTime(UTC)]>='{start_date_str}' "
219
238
  f"AND [DateTime(UTC)]<'{end_date_str}' "
220
239
  )
221
-
222
- # Query db
223
- self.open_connection()
224
240
  df = self.query_db(query)
225
- self.close_connection()
226
241
 
227
242
  if df.empty:
228
243
  raise ValueError("No price data found for user-defined inputs.")
@@ -409,19 +424,15 @@ class DetDatabase:
409
424
  end_delivery_date = end_delivery_date.astimezone(ZoneInfo("UTC"))
410
425
  end_date_str = end_delivery_date.strftime("%Y-%m-%d %H:%M:%S")
411
426
 
412
- # Create query
413
- table = DetDatabaseDefinitions.DEFINITIONS["table_name_entsoe_imbalance_price"]
427
+ # Query db
428
+ table = DetDatabase.get_table_name("entsoe_imbalance_price")
414
429
  query = (
415
430
  f"SELECT {columns_str} FROM {table} "
416
431
  f"WHERE MapCode='{map_code}' "
417
432
  f"AND [DateTime(UTC)]>='{start_date_str}' "
418
433
  f"AND [DateTime(UTC)]<'{end_date_str}' "
419
434
  )
420
-
421
- # Query db
422
- self.open_connection()
423
435
  df = self.query_db(query)
424
- self.close_connection()
425
436
 
426
437
  if df.empty:
427
438
  raise ValueError("No price data found for user-defined inputs.")
@@ -542,8 +553,8 @@ class DetDatabase:
542
553
  start_trading_date_str = start_trading_date.strftime("%Y-%m-%d")
543
554
  end_trading_date_str = end_trading_date.strftime("%Y-%m-%d")
544
555
 
545
- # Create query
546
- table = DetDatabaseDefinitions.DEFINITIONS["table_name_futures_eod_settlement_price"]
556
+ # Query db
557
+ table = DetDatabase.get_table_name("futures_tt")
547
558
  query = (
548
559
  f"SELECT {columns_str} FROM {table} "
549
560
  f"WHERE CommodityName='{commodity_name}' "
@@ -552,11 +563,7 @@ class DetDatabase:
552
563
  f"AND Tenor IN {tenors_str} "
553
564
  f"AND DeliveryType='{delivery_type}'"
554
565
  )
555
-
556
- # Query db
557
- self.open_connection()
558
566
  df = self.query_db(query)
559
- self.close_connection()
560
567
 
561
568
  if df.empty:
562
569
  raise ValueError("No price data found for user-defined inputs.")
@@ -614,14 +621,10 @@ class DetDatabase:
614
621
  else:
615
622
  columns_str = f"[{'], ['.join(columns)}]"
616
623
 
617
- # Create query
618
- table = DetDatabaseDefinitions.DEFINITIONS["table_name_commodity"]
619
- query = f"SELECT {columns_str} FROM {table} {conditions}"
620
-
621
624
  # Query db
622
- self.open_connection()
625
+ table = DetDatabase.get_table_name("commodity")
626
+ query = f"SELECT {columns_str} FROM {table} {conditions}"
623
627
  df = self.query_db(query)
624
- self.close_connection()
625
628
 
626
629
  return df
627
630
 
@@ -697,18 +700,14 @@ class DetDatabase:
697
700
  start_trading_date_str = start_trading_date.strftime("%Y-%m-%d")
698
701
  end_trading_date_str = end_trading_date.strftime("%Y-%m-%d")
699
702
 
700
- # Create query
701
- table = DetDatabaseDefinitions.DEFINITIONS["table_name_account_position"]
703
+ # Query db
704
+ table = DetDatabase.get_table_name("account_position")
702
705
  query = (
703
706
  f"SELECT {columns_str} FROM {table} "
704
707
  f"WHERE InsertionTimestamp>='{start_trading_date_str}' "
705
708
  f"AND InsertionTimestamp<'{end_trading_date_str}'"
706
709
  )
707
-
708
- # Query db
709
- self.open_connection()
710
710
  df = self.query_db(query)
711
- self.close_connection()
712
711
 
713
712
  # Assert data
714
713
  if df.empty:
@@ -755,14 +754,10 @@ class DetDatabase:
755
754
  # Convert tenors from list to string
756
755
  identifiers_str = ", ".join(f"'{i}'" for i in identifiers)
757
756
 
758
- # Create query
759
- table = DetDatabaseDefinitions.DEFINITIONS["table_name_instruments"]
760
- query = f"SELECT {columns_str} FROM {table} WHERE [id] IN ({identifiers_str})"
761
-
762
757
  # Query db
763
- self.open_connection()
758
+ table = DetDatabase.get_table_name("instrument")
759
+ query = f"SELECT {columns_str} FROM {table} WHERE [id] IN ({identifiers_str})"
764
760
  df = self.query_db(query)
765
- self.close_connection()
766
761
 
767
762
  # Assert data
768
763
  if df.empty:
@@ -816,19 +811,15 @@ class DetDatabase:
816
811
  start_trading_date_str = start_trading_date.strftime("%Y-%m-%d")
817
812
  end_trading_date_str = end_trading_date.strftime("%Y-%m-%d")
818
813
 
819
- # Create query
820
- table = DetDatabaseDefinitions.DEFINITIONS["table_name_eex_eod_price"]
814
+ # Query db
815
+ table = DetDatabase.get_table_name("futures_eex")
821
816
  query = (
822
817
  f"SELECT {columns_str} FROM {table} "
823
818
  f"WHERE Product LIKE '{product_code}' "
824
819
  f"AND TradingDate>='{start_trading_date_str}' "
825
820
  f"AND TradingDate<='{end_trading_date_str}'"
826
821
  )
827
-
828
- # Query db
829
- self.open_connection()
830
822
  df = self.query_db(query)
831
- self.close_connection()
832
823
 
833
824
  # Assert data
834
825
  if df.empty:
@@ -925,8 +916,8 @@ class DetDatabase:
925
916
  # Convert dates from datetime to string
926
917
  forecast_date_str = forecast_date.strftime("%Y-%m-%d")
927
918
 
928
- # Create query
929
- table = DetDatabaseDefinitions.DEFINITIONS["table_name_forecast_customer_volume"]
919
+ # Query db
920
+ table = DetDatabase.get_table_name("client_volume_forecast")
930
921
  query = (
931
922
  f"SELECT {columns_str} FROM {table} "
932
923
  f"WHERE Profile='{profile}' "
@@ -934,11 +925,7 @@ class DetDatabase:
934
925
  f"AND Datetime>='{start_date_str}' "
935
926
  f"AND Datetime<'{end_date_str}'"
936
927
  )
937
-
938
- # Query db
939
- self.open_connection()
940
928
  df = self.query_db(query)
941
- self.close_connection()
942
929
 
943
930
  # Assert data
944
931
  if df.empty:
@@ -1030,19 +1017,15 @@ class DetDatabase:
1030
1017
  else:
1031
1018
  columns_str = f"[{'], ['.join(columns)}]"
1032
1019
 
1033
- # Create query
1034
- table = DetDatabaseDefinitions.DEFINITIONS["table_name_customer_day_ahead_auction_bids"]
1020
+ # Query db
1021
+ table = DetDatabase.get_table_name("client_day_ahead_auction_bids")
1035
1022
  query = (
1036
1023
  f"SELECT {columns_str} FROM {table} "
1037
1024
  f"WHERE ClientId='{client_id}' "
1038
1025
  f"AND DeliveryStart>='{start_date_str}' "
1039
1026
  f"AND DeliveryStart<'{end_date_str}'"
1040
1027
  )
1041
-
1042
- # Query db
1043
- self.open_connection()
1044
1028
  df = self.query_db(query)
1045
- self.close_connection()
1046
1029
 
1047
1030
  # Assert data
1048
1031
  if df.empty:
@@ -1101,14 +1084,10 @@ class DetDatabase:
1101
1084
  else:
1102
1085
  columns_str = f"[{'], ['.join(columns)}]"
1103
1086
 
1104
- # Create query
1105
- table = DetDatabaseDefinitions.DEFINITIONS["table_name_client"]
1106
- query = f"SELECT {columns_str} FROM {table} {conditions}"
1107
-
1108
1087
  # Query db
1109
- self.open_connection()
1088
+ table = DetDatabase.get_table_name("client")
1089
+ query = f"SELECT {columns_str} FROM {table} {conditions}"
1110
1090
  df = self.query_db(query)
1111
- self.close_connection()
1112
1091
 
1113
1092
  return df
1114
1093
 
@@ -1149,19 +1128,72 @@ class DetDatabase:
1149
1128
 
1150
1129
  return client_info
1151
1130
 
1131
+ def add_table(self, df: pd.DataFrame, table_name: str, schema: str):
1132
+ """
1133
+ Creates a new database table and populates it with the user-input data.
1134
+
1135
+ Args:
1136
+ df: Data used to populate the database table
1137
+ table_name: Table name
1138
+ schema: Schema
1139
+ """
1140
+ df.to_sql(name=table_name, schema=schema, con=self.engine, if_exists="fail", index=False)
1141
+
1142
+ def remove_table(self, table_name: str, schema: str):
1143
+ """
1144
+ Deletes a database table.
1145
+
1146
+ Args:
1147
+ table_name: Table name
1148
+ schema: Schema
1149
+ """
1150
+ with self.engine.connect() as conn:
1151
+ conn.execute(text(f"DROP TABLE IF EXISTS [{schema}].[{table_name}]"))
1152
+ conn.commit()
1153
+
1152
1154
 
1153
1155
  class DetDatabaseDefinitions:
1154
1156
  """A class containing some hard-coded definitions related to the DET database."""
1155
1157
 
1156
1158
  DEFINITIONS = dict(
1157
- table_name_commodity="[META].[Commodity]",
1158
- table_name_entsoe_day_ahead_spot_price="[ENTSOE].[DayAheadSpotPrice]",
1159
- table_name_entsoe_imbalance_price="[ENTSOE].[ImbalancePrice]",
1160
- table_name_futures_eod_settlement_price="[VW].[EODSettlementPrice]",
1161
- table_name_account_position="[TT].[AccountPosition]",
1162
- table_name_instruments="[TT].[Instrument]",
1163
- table_name_eex_eod_price="[EEX].[EODPrice]",
1164
- table_name_forecast_customer_volume="[DISP].[ForecastGold]",
1165
- table_name_customer_day_ahead_auction_bids="[TRADE].[ClientBid]",
1166
- table_name_client="[META].[Client]",
1159
+ commodity=dict(
1160
+ env_variable="DET_DB_TABLE_COMMODITY",
1161
+ default_table_name="[META].[Commodity]",
1162
+ ),
1163
+ entsoe_day_ahead_spot_price=dict(
1164
+ env_variable="DET_DB_TABLE_ENTSOE_DA",
1165
+ default_table_name="[ENTSOE].[DayAheadSpotPrice]",
1166
+ ),
1167
+ entsoe_imbalance_price=dict(
1168
+ env_variable="DET_DB_TABLE_ENTSOE_IB",
1169
+ default_table_name="[ENTSOE].[ImbalancePrice]",
1170
+ ),
1171
+ futures_tt=dict(
1172
+ env_variable="DET_DB_TABLE_FUTURES_TT",
1173
+ default_table_name="[VW].[EODSettlementPrice]",
1174
+ ),
1175
+ futures_eex=dict(
1176
+ env_variable="DET_DB_TABLE_FUTURES_EEX",
1177
+ default_table_name="[EEX].[EODPrice]",
1178
+ ),
1179
+ account_position=dict(
1180
+ env_variable="DET_DB_TABLE_ACCOUNT_POSITION",
1181
+ default_table_name="[TT].[AccountPosition]",
1182
+ ),
1183
+ instrument=dict(
1184
+ env_variable="DET_DB_TABLE_INSTRUMENT",
1185
+ default_table_name="[TT].[Instrument]",
1186
+ ),
1187
+ client_volume_forecast=dict(
1188
+ env_variable="DET_DB_TABLE_CLIENT_VOLUME_FORECAST",
1189
+ default_table_name="[DISP].[ForecastGold]",
1190
+ ),
1191
+ client_day_ahead_auction_bids=dict(
1192
+ env_variable="DET_DB_TABLE_CLIENT_DA_AUCTION_BIDS",
1193
+ default_table_name="[TRADE].[ClientBid]",
1194
+ ),
1195
+ client=dict(
1196
+ env_variable="DET_DB_TABLE_CLIENT",
1197
+ default_table_name="[META].[Client]",
1198
+ ),
1167
1199
  )
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "detquantlib"
3
- version = "3.8.1"
3
+ version = "3.10.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"
@@ -15,9 +15,9 @@ paramiko = "^4.0.0"
15
15
  numpy = "^2.2.6"
16
16
  pandas = "^2.3.3"
17
17
  plotly = "^6.3.1"
18
- pyodbc = "^5.2.0"
19
18
  python-dotenv = "^1.1.0"
20
19
  scipy = "^1.15.2"
20
+ sqlalchemy = "^2.0.43"
21
21
 
22
22
  [tool.poetry.group.dev.dependencies]
23
23
  toml = "^0.10.2"
File without changes
File without changes