detquantlib 3.12.0__tar.gz → 3.14.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 (32) hide show
  1. {detquantlib-3.12.0 → detquantlib-3.14.0}/PKG-INFO +10 -1
  2. {detquantlib-3.12.0 → detquantlib-3.14.0}/README.md +9 -0
  3. detquantlib-3.14.0/detquantlib/converters/__init__.py +2 -0
  4. detquantlib-3.14.0/detquantlib/converters/definitions.py +24 -0
  5. detquantlib-3.14.0/detquantlib/converters/energy.py +140 -0
  6. detquantlib-3.14.0/detquantlib/converters/helpers.py +67 -0
  7. detquantlib-3.14.0/detquantlib/converters/price.py +107 -0
  8. detquantlib-3.14.0/detquantlib/data/__init__.py +6 -0
  9. {detquantlib-3.12.0 → detquantlib-3.14.0}/detquantlib/data/databases/detdatabase.py +34 -48
  10. detquantlib-3.14.0/detquantlib/data/databases/helpers.py +40 -0
  11. {detquantlib-3.12.0 → detquantlib-3.14.0}/detquantlib/dates/dates.py +40 -1
  12. {detquantlib-3.12.0 → detquantlib-3.14.0}/detquantlib/forecasting/forecasting.py +3 -0
  13. {detquantlib-3.12.0 → detquantlib-3.14.0}/detquantlib/tradable_products/tradable_products.py +60 -1
  14. detquantlib-3.14.0/detquantlib/utils/__init__.py +2 -0
  15. {detquantlib-3.12.0 → detquantlib-3.14.0}/detquantlib/utils/logging.py +4 -0
  16. {detquantlib-3.12.0 → detquantlib-3.14.0}/detquantlib/utils/utils.py +8 -0
  17. {detquantlib-3.12.0 → detquantlib-3.14.0}/pyproject.toml +1 -1
  18. detquantlib-3.12.0/detquantlib/data/__init__.py +0 -5
  19. detquantlib-3.12.0/detquantlib/utils/__init__.py +0 -1
  20. {detquantlib-3.12.0 → detquantlib-3.14.0}/LICENSE.txt +0 -0
  21. {detquantlib-3.12.0 → detquantlib-3.14.0}/detquantlib/__init__.py +0 -0
  22. {detquantlib-3.12.0 → detquantlib-3.14.0}/detquantlib/data/entsoe/entsoe.py +0 -0
  23. {detquantlib-3.12.0 → detquantlib-3.14.0}/detquantlib/data/sftp/sftp.py +0 -0
  24. {detquantlib-3.12.0 → detquantlib-3.14.0}/detquantlib/dates/__init__.py +0 -0
  25. {detquantlib-3.12.0 → detquantlib-3.14.0}/detquantlib/figures/__init__.py +0 -0
  26. {detquantlib-3.12.0 → detquantlib-3.14.0}/detquantlib/figures/plotly_figures.py +0 -0
  27. {detquantlib-3.12.0 → detquantlib-3.14.0}/detquantlib/forecasting/__init__.py +0 -0
  28. {detquantlib-3.12.0 → detquantlib-3.14.0}/detquantlib/outputs/__init__.py +0 -0
  29. {detquantlib-3.12.0 → detquantlib-3.14.0}/detquantlib/outputs/outputs_interface.py +0 -0
  30. {detquantlib-3.12.0 → detquantlib-3.14.0}/detquantlib/stats/__init__.py +0 -0
  31. {detquantlib-3.12.0 → detquantlib-3.14.0}/detquantlib/stats/data_analysis.py +0 -0
  32. {detquantlib-3.12.0 → detquantlib-3.14.0}/detquantlib/tradable_products/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detquantlib
3
- Version: 3.12.0
3
+ Version: 3.14.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
@@ -99,6 +99,15 @@ Classes:
99
99
  - `from detquantlib.data import Sftp`
100
100
  - `from detquantlib.data.sftp.sftp import Sftp`
101
101
 
102
+ Functions:
103
+
104
+ - `list_to_sql_columns`:
105
+ - `from detquantlib.data import list_to_sql_columns`
106
+ - `from detquantlib.data.databases.helpers import list_to_sql_columns`
107
+ - `list_to_sql_set`:
108
+ - `from detquantlib.data import list_to_sql_set`
109
+ - `from detquantlib.data.databases.helpers import list_to_sql_set`
110
+
102
111
  <!-- END EXPOSED SYMBOLS AUTO-GENERATED -->
103
112
 
104
113
  ## Configuration
@@ -73,6 +73,15 @@ Classes:
73
73
  - `from detquantlib.data import Sftp`
74
74
  - `from detquantlib.data.sftp.sftp import Sftp`
75
75
 
76
+ Functions:
77
+
78
+ - `list_to_sql_columns`:
79
+ - `from detquantlib.data import list_to_sql_columns`
80
+ - `from detquantlib.data.databases.helpers import list_to_sql_columns`
81
+ - `list_to_sql_set`:
82
+ - `from detquantlib.data import list_to_sql_set`
83
+ - `from detquantlib.data.databases.helpers import list_to_sql_set`
84
+
76
85
  <!-- END EXPOSED SYMBOLS AUTO-GENERATED -->
77
86
 
78
87
  ## Configuration
@@ -0,0 +1,2 @@
1
+ from .energy import *
2
+ from .price import *
@@ -0,0 +1,24 @@
1
+ class PowerUnits:
2
+ """A class that contains hard-coded information about power units."""
3
+
4
+ CONVERSIONS = [
5
+ {"unit": "W", "value_in_base_unit": 1},
6
+ {"unit": "kW", "value_in_base_unit": 1e3},
7
+ {"unit": "MW", "value_in_base_unit": 1e6},
8
+ {"unit": "GW", "value_in_base_unit": 1e9},
9
+ {"unit": "TW", "value_in_base_unit": 1e12},
10
+ {"unit": "MJ/s", "value_in_base_unit": 1e6},
11
+ ]
12
+
13
+
14
+ class EnergyUnits:
15
+ """A class that contains hard-coded information about energy units."""
16
+
17
+ CONVERSIONS = [
18
+ {"unit": "Wh", "value_in_base_unit": 1},
19
+ {"unit": "kWh", "value_in_base_unit": 1e3},
20
+ {"unit": "MWh", "value_in_base_unit": 1e6},
21
+ {"unit": "GWh", "value_in_base_unit": 1e9},
22
+ {"unit": "TWh", "value_in_base_unit": 1e12},
23
+ {"unit": "MJ", "value_in_base_unit": 1e6 / 3600},
24
+ ]
@@ -0,0 +1,140 @@
1
+ # Python built-in packages
2
+ from datetime import datetime
3
+
4
+ # Third-party packages
5
+ import numpy as np
6
+
7
+ # Internal modules
8
+ from detquantlib.converters.definitions import EnergyUnits, PowerUnits
9
+ from detquantlib.converters.helpers import convert_unit_same_cat
10
+ from detquantlib.dates import count_nr_hours
11
+ from detquantlib.tradable_products import tenor_to_nr_hours
12
+
13
+ # Specify functions to expose
14
+ __all__ = ["convert_power", "convert_energy", "power_to_energy", "energy_to_power"]
15
+
16
+
17
+ def convert_power(value_in: float | np.ndarray, unit_in: str, unit_out: str) -> float | np.ndarray:
18
+ """
19
+ Converts power from one unit to another.
20
+
21
+ Args:
22
+ value_in: Power quantity
23
+ unit_in: Unit of input power quantity
24
+ unit_out: Unit of output power quantity
25
+
26
+ Returns:
27
+ Converted power quantity
28
+ """
29
+ return convert_unit_same_cat(
30
+ conversions=PowerUnits.CONVERSIONS, value_in=value_in, unit_in=unit_in, unit_out=unit_out
31
+ )
32
+
33
+
34
+ def convert_energy(
35
+ value_in: float | np.ndarray, unit_in: str, unit_out: str
36
+ ) -> float | np.ndarray:
37
+ """
38
+ Converts energy from one unit to another.
39
+
40
+ Args:
41
+ value_in: Energy quantity
42
+ unit_in: Unit of input energy quantity
43
+ unit_out: Unit of output energy quantity
44
+
45
+ Returns:
46
+ Converted energy quantity
47
+ """
48
+ return convert_unit_same_cat(
49
+ conversions=EnergyUnits.CONVERSIONS, value_in=value_in, unit_in=unit_in, unit_out=unit_out
50
+ )
51
+
52
+
53
+ def power_to_energy(
54
+ power: float | np.ndarray,
55
+ power_unit: str,
56
+ energy_unit: str,
57
+ tenor: str = None,
58
+ delivery_start: datetime = None,
59
+ delivery_end: datetime = None,
60
+ ):
61
+ """
62
+ Converts a power quantity into its corresponding energy quantity.
63
+
64
+ Args:
65
+ power: Power quantity
66
+ power_unit: Unit of input power quantity
67
+ energy_unit: Unit of output energy quantity
68
+ tenor: (Optional) Product tenor. If not provided, delivery_start and delivery_end must be
69
+ provided.
70
+ delivery_start: (Optional) Delivery start date. Only needed if tenor is not provided,
71
+ or if tenor > 1 hour to account for DST switches.
72
+ delivery_end: (Optional) Delivery end date. Only needed if tenor is not provided, or
73
+ if tenor > 1 hour to account for DST switches.
74
+
75
+ Returns:
76
+ Energy quantity
77
+ """
78
+ # Count number of hours in delivery period
79
+ if tenor is None:
80
+ nr_hours = count_nr_hours(start_date=delivery_start, end_date=delivery_end)
81
+ else:
82
+ nr_hours = tenor_to_nr_hours(
83
+ tenor=tenor, delivery_start=delivery_start, delivery_end=delivery_end
84
+ )
85
+
86
+ # Convert power from input unit to W
87
+ power_w = convert_power(value_in=power, unit_in=power_unit, unit_out="w")
88
+
89
+ # Convert power (W) to energy (Wh)
90
+ energy_wh = power_w * nr_hours
91
+
92
+ # Convert energy from Wh to output unit
93
+ energy = convert_energy(value_in=energy_wh, unit_in="wh", unit_out=energy_unit)
94
+
95
+ return energy
96
+
97
+
98
+ def energy_to_power(
99
+ energy: float | np.ndarray,
100
+ energy_unit: str,
101
+ power_unit: str,
102
+ tenor: str = None,
103
+ delivery_start: datetime = None,
104
+ delivery_end: datetime = None,
105
+ ):
106
+ """
107
+ Converts an energy quantity into its corresponding power quantity.
108
+
109
+ Args:
110
+ energy: Energy quantity
111
+ energy_unit: Unit of input energy quantity
112
+ power_unit: Unit of output power quantity
113
+ tenor: (Optional) Product tenor. If not provided, delivery_start and delivery_end must be
114
+ provided.
115
+ delivery_start: (Optional) Delivery start date. Only needed if tenor is not provided,
116
+ or if tenor > 1 hour to account for DST switches.
117
+ delivery_end: (Optional) Delivery end date. Only needed if tenor is not provided, or
118
+ if tenor > 1 hour to account for DST switches.
119
+
120
+ Returns:
121
+ Power quantity
122
+ """
123
+ # Count number of hours in delivery period
124
+ if tenor is None:
125
+ nr_hours = count_nr_hours(start_date=delivery_start, end_date=delivery_end)
126
+ else:
127
+ nr_hours = tenor_to_nr_hours(
128
+ tenor=tenor, delivery_start=delivery_start, delivery_end=delivery_end
129
+ )
130
+
131
+ # Convert energy from input unit to Wh
132
+ energy_wh = convert_energy(value_in=energy, unit_in=energy_unit, unit_out="wh")
133
+
134
+ # Convert energy (Wh) to power (W)
135
+ power_w = energy_wh / nr_hours
136
+
137
+ # Convert power from W to output unit
138
+ power = convert_power(value_in=power_w, unit_in="w", unit_out=power_unit)
139
+
140
+ return power
@@ -0,0 +1,67 @@
1
+ # Third-party packages
2
+ import numpy as np
3
+
4
+ # Internal modules
5
+ from detquantlib.utils import list_to_str
6
+
7
+
8
+ def get_base_unit(conversions: list[dict]) -> str:
9
+ """
10
+ Gets the base unit of a given unit group.
11
+
12
+ Args:
13
+ conversions: List of conversion dicts of a given unit group
14
+
15
+ Returns:
16
+ Base unit
17
+ """
18
+ return next((d for d in conversions if d["value_in_base_unit"] == 1))["unit"]
19
+
20
+
21
+ def get_unit_dict(unit: str, conversions: list[dict]) -> dict:
22
+ """
23
+ Gets the unit conversion dict of a given unit.
24
+
25
+ Args:
26
+ unit: Unit
27
+ conversions: List of conversion dicts of a given unit group
28
+
29
+ Returns:
30
+ Conversion dict of the input unit
31
+
32
+ Raises:
33
+ ValueError: Raises an error if the input unit is not supported.
34
+ """
35
+ unit_dict = next((d for d in conversions if d["unit"].lower() == unit.lower()), None)
36
+ if unit_dict is None:
37
+ units_str = list_to_str([d["unit"] for d in conversions])
38
+ raise ValueError(f"Unsupported unit '{unit}'. Accepted values: {units_str}.")
39
+ return unit_dict
40
+
41
+
42
+ def convert_unit_same_cat(
43
+ conversions: list[dict], value_in: float | np.ndarray, unit_in: str, unit_out: str
44
+ ) -> float | np.ndarray:
45
+ """
46
+ Converts a quantity from a given unit to another unit of the same unit group.
47
+
48
+ Args:
49
+ conversions: List of conversion dicts of a given unit group
50
+ value_in: Quantity to convert
51
+ unit_in: Unit of the input quantity
52
+ unit_out: Unit of the output quantity
53
+
54
+ Returns:
55
+ Converted quantity
56
+ """
57
+ # Get unit dicts
58
+ unit_dict_in = get_unit_dict(unit_in, conversions)
59
+ unit_dict_out = get_unit_dict(unit_out, conversions)
60
+
61
+ # Convert input value to base unit
62
+ value_base = value_in * unit_dict_in["value_in_base_unit"]
63
+
64
+ # Convert base value to output unit
65
+ value_out = value_base / unit_dict_out["value_in_base_unit"]
66
+
67
+ return value_out
@@ -0,0 +1,107 @@
1
+ # Third-party packages
2
+ import numpy as np
3
+
4
+ # Internal modules
5
+ from detquantlib.converters.definitions import EnergyUnits
6
+ from detquantlib.converters.helpers import convert_unit_same_cat
7
+
8
+ # Specify functions to expose
9
+ __all__ = ["convert_price", "flag_cents"]
10
+
11
+
12
+ def convert_price(
13
+ price_in: float | np.ndarray,
14
+ currency_in: str,
15
+ unit_in: str,
16
+ currency_out: str,
17
+ unit_out: str,
18
+ commodity_category: str,
19
+ ) -> float | np.ndarray:
20
+ """
21
+ Converts a price from a given currency and volume unit to another.
22
+
23
+ Args:
24
+ price_in: Price
25
+ currency_in: Currency of input price
26
+ unit_in: Volume unit of current price
27
+ currency_out: Currency of output price
28
+ unit_out: Volume unit of output price
29
+ commodity_category: Commodity category
30
+
31
+ Returns:
32
+ Converted price
33
+
34
+ Raises:
35
+ ValueError: Raises an error if the commodity category is not supported.
36
+ ValueError: Raises an error if the currency conversion is not supported.
37
+ """
38
+ # Split currency and cents indicator
39
+ currency_pre_in, is_cent_in = flag_cents(currency_in)
40
+ currency_pre_out, is_cent_out = flag_cents(currency_out)
41
+
42
+ # Get unit converters
43
+ match commodity_category.lower():
44
+ case "energy":
45
+ conversions = EnergyUnits.CONVERSIONS
46
+ case _:
47
+ raise ValueError(f"Commodity category '{commodity_category}' is not supported.")
48
+
49
+ # 1. Convert price based on currency
50
+ if currency_pre_in.lower() != currency_pre_out.lower():
51
+ raise ValueError(
52
+ f"Price conversion from currency prefix '{currency_pre_in}' to currency prefix "
53
+ f"'{currency_pre_out}' is not supported."
54
+ )
55
+
56
+ # 2. Convert price based on units
57
+ unit_factor = convert_unit_same_cat(
58
+ conversions=conversions, value_in=1, unit_in=unit_in, unit_out=unit_out
59
+ )
60
+ price_out = price_in / unit_factor
61
+
62
+ # 3. Convert price based on cents indicators
63
+ if is_cent_in and not is_cent_out:
64
+ price_out /= 100
65
+ elif not is_cent_in and is_cent_out:
66
+ price_out *= 100
67
+
68
+ return price_out
69
+
70
+
71
+ def flag_cents(currency_str: str) -> (str, bool):
72
+ """
73
+ Checks if the input currency is expressed in cents or not.
74
+
75
+ Args:
76
+ currency_str: Currency string (e.g. "EUR", "EUR_CENTS")
77
+
78
+ Returns:
79
+ tuple:
80
+ - Currency, excluding cents flag (e.g. "EUR")
81
+ - Cents flag, given as Boolean
82
+
83
+ Raises:
84
+ ValueError: Raises an error if the suffix of the currency string is not recognized.
85
+ ValueError: Raises an error if the currency string could not be parsed.
86
+ """
87
+ # Split currency and cents flag
88
+ currency_list = currency_str.split("_")
89
+
90
+ currency = currency_list[0]
91
+ if len(currency_list) == 1:
92
+ is_cent = False
93
+ elif len(currency_list) == 2:
94
+ if currency_list[1].lower() == "cents":
95
+ is_cent = True
96
+ else:
97
+ raise ValueError(
98
+ f"Suffix '{currency_list[1]}' of currency string '{currency_str}' not recognized "
99
+ f"as valid cents indicator."
100
+ )
101
+ else:
102
+ raise ValueError(
103
+ f"Currency string '{currency_str}' could not be parsed into currency and cents "
104
+ f"indicator."
105
+ )
106
+
107
+ return currency, is_cent
@@ -0,0 +1,6 @@
1
+ from .databases.detdatabase import DetDatabase
2
+ from .databases.helpers import *
3
+ from .entsoe.entsoe import Entsoe
4
+ from .sftp.sftp import Sftp
5
+
6
+ __all__ = ["DetDatabase", "Entsoe", "Sftp", "list_to_sql_set", "list_to_sql_columns"]
@@ -9,21 +9,36 @@ import pandas as pd
9
9
  from dateutil.relativedelta import *
10
10
  from sqlalchemy import Engine, create_engine, text
11
11
 
12
+ # Internal modules
13
+ from detquantlib.converters import convert_energy
14
+ from detquantlib.data.databases.helpers import list_to_sql_columns, list_to_sql_set
15
+ from detquantlib.utils import list_to_str
16
+
12
17
 
13
18
  class DetDatabase:
14
19
  """
15
20
  A class to easily interact with the DET database, including fetching and processing data.
16
21
  """
17
22
 
18
- def __init__(self, engine: Engine = None, driver: str = "ODBC Driver 18 for SQL Server"):
23
+ def __init__(
24
+ self,
25
+ db_name: str = None,
26
+ engine: Engine = None,
27
+ driver: str = "ODBC Driver 18 for SQL Server",
28
+ ):
19
29
  """
20
30
  Constructor method.
21
31
 
22
32
  Args:
33
+ db_name: Database name. If None, the name is fetched from environment variables.
23
34
  engine: Instance of SQLAlchemy engine. If None, the instance is automatically
24
35
  initialized.
25
36
  driver: ODBC driver
26
37
  """
38
+ # Store database name if it is provided
39
+ if db_name:
40
+ os.environ["DET_DB_NAME"] = db_name
41
+
27
42
  # Only allow object creation if mandatory environment variables exist
28
43
  DetDatabase.check_environment_variables()
29
44
 
@@ -48,7 +63,7 @@ class DetDatabase:
48
63
  for d in required_env_vars:
49
64
  if d["name"] not in available_env_vars:
50
65
  required_env_vars_names = [x["name"] for x in required_env_vars]
51
- required_env_vars_str = ", ".join(f"'{x}'" for x in required_env_vars_names)
66
+ required_env_vars_str = list_to_str(required_env_vars_names)
52
67
  raise EnvironmentError(
53
68
  f"The DetDatabase class requires the following environment variables: "
54
69
  f"{required_env_vars_str}. Environment variable '{d['name']}' "
@@ -198,10 +213,7 @@ class DetDatabase:
198
213
  columns.append("DateTime(UTC)")
199
214
 
200
215
  # Convert columns from list to string
201
- if len(columns) == 1:
202
- columns_str = str(columns[0])
203
- else:
204
- columns_str = f"[{'], ['.join(columns)}]"
216
+ columns_str = list_to_sql_columns(columns)
205
217
 
206
218
  # Get commodity information (map code and local timezone)
207
219
  # Note: The local timezone is important because ENTSOE provides all prices in the UTC
@@ -415,10 +427,7 @@ class DetDatabase:
415
427
  columns.append("DateTime(UTC)")
416
428
 
417
429
  # Convert columns from list to string
418
- if len(columns) == 1:
419
- columns_str = str(columns[0])
420
- else:
421
- columns_str = f"[{'], ['.join(columns)}]"
430
+ columns_str = list_to_sql_columns(columns)
422
431
 
423
432
  # Get commodity information (map code and local timezone)
424
433
  # Note: The local timezone is important because ENTSOE provides all prices in the UTC
@@ -567,13 +576,10 @@ class DetDatabase:
567
576
  columns = ["*"]
568
577
 
569
578
  # Convert columns from list to string
570
- if len(columns) == 1:
571
- columns_str = str(columns[0])
572
- else:
573
- columns_str = f"[{'], ['.join(columns)}]"
579
+ columns_str = list_to_sql_columns(columns)
574
580
 
575
581
  # Convert tenors from list to string
576
- tenors_str = f"({' ,'.join([repr(item) for item in tenors])})"
582
+ tenors_str = list_to_sql_set(tenors)
577
583
 
578
584
  # Convert dates from datetime to string
579
585
  start_trading_date_str = start_trading_date.strftime("%Y-%m-%d")
@@ -639,10 +645,7 @@ class DetDatabase:
639
645
  columns = ["*"]
640
646
 
641
647
  # Convert columns from list to string
642
- if len(columns) == 1:
643
- columns_str = str(columns[0])
644
- else:
645
- columns_str = f"[{'], ['.join(columns)}]"
648
+ columns_str = list_to_sql_columns(columns)
646
649
 
647
650
  # Query db
648
651
  table = DetDatabase.get_table_name("commodity")
@@ -679,7 +682,7 @@ class DetDatabase:
679
682
 
680
683
  elif commodity_info.shape[0] == 0:
681
684
  available_values = self.load_commodities(columns=[filter_column])
682
- available_values_str = ", ".join(f"'{x}'" for x in available_values[filter_column])
685
+ available_values_str = list_to_str(available_values[filter_column])
683
686
  raise ValueError(
684
687
  f"Value {filter_value} not found in column '{filter_column}'. Available values: "
685
688
  f"{available_values_str}."
@@ -713,10 +716,7 @@ class DetDatabase:
713
716
  columns = ["*"]
714
717
 
715
718
  # Convert columns from list to string
716
- if len(columns) == 1:
717
- columns_str = str(columns[0])
718
- else:
719
- columns_str = f"[{'], ['.join(columns)}]"
719
+ columns_str = list_to_sql_columns(columns)
720
720
 
721
721
  # Convert dates from datetime to string
722
722
  end_trading_date = pd.Timestamp(end_trading_date).floor("D") + relativedelta(days=1)
@@ -769,13 +769,10 @@ class DetDatabase:
769
769
  columns = ["*"]
770
770
 
771
771
  # Convert columns from list to string
772
- if len(columns) == 1:
773
- columns_str = str(columns[0])
774
- else:
775
- columns_str = f"[{'], ['.join(columns)}]"
772
+ columns_str = list_to_sql_columns(columns)
776
773
 
777
774
  # Convert tenors from list to string
778
- identifiers_str = ", ".join(f"'{i}'" for i in identifiers)
775
+ identifiers_str = list_to_sql_set(identifiers)
779
776
 
780
777
  # Query db
781
778
  table = DetDatabase.get_table_name("instrument")
@@ -819,10 +816,7 @@ class DetDatabase:
819
816
  columns = ["*"]
820
817
 
821
818
  # Convert columns from list to string
822
- if len(columns) == 1:
823
- columns_str = f"[{columns[0]}]"
824
- else:
825
- columns_str = f"[{'], ['.join(columns)}]"
819
+ columns_str = list_to_sql_columns(columns)
826
820
 
827
821
  # Convert dates from datetime to string
828
822
  start_trading_date_str = start_trading_date.strftime("%Y-%m-%d")
@@ -922,10 +916,7 @@ class DetDatabase:
922
916
  columns = ["*"]
923
917
 
924
918
  # Convert columns from list to string
925
- if len(columns) == 1:
926
- columns_str = str(columns[0])
927
- else:
928
- columns_str = f"[{'], ['.join(columns)}]"
919
+ columns_str = list_to_sql_columns(columns)
929
920
 
930
921
  # Convert dates from datetime to string
931
922
  forecast_date_str = forecast_date.strftime("%Y-%m-%d")
@@ -975,10 +966,11 @@ class DetDatabase:
975
966
  df[c] = df[c].dt.tz_localize(None)
976
967
 
977
968
  # Rescale column values
978
- df["kWh"] = df["kWh"] / 1000
969
+ df["Volume(MWh)"] = convert_energy(df["kWh"], unit_in="kWh", unit_out="MWh")
970
+ df.drop(columns="kWh", inplace=True)
979
971
 
980
972
  # Rename columns
981
- df = df.rename(columns={"kWh": "Volume(MWh)", "Datetime": "DeliveryStart"})
973
+ df = df.rename(columns={"Datetime": "DeliveryStart"})
982
974
 
983
975
  return df
984
976
 
@@ -1026,10 +1018,7 @@ class DetDatabase:
1026
1018
  columns = ["*"]
1027
1019
 
1028
1020
  # Convert columns from list to string
1029
- if len(columns) == 1:
1030
- columns_str = str(columns[0])
1031
- else:
1032
- columns_str = f"[{'], ['.join(columns)}]"
1021
+ columns_str = list_to_sql_columns(columns)
1033
1022
 
1034
1023
  # Query db
1035
1024
  table = DetDatabase.get_table_name("client_day_ahead_auction_bids")
@@ -1093,10 +1082,7 @@ class DetDatabase:
1093
1082
  columns = ["*"]
1094
1083
 
1095
1084
  # Convert columns from list to string
1096
- if len(columns) == 1:
1097
- columns_str = str(columns[0])
1098
- else:
1099
- columns_str = f"[{'], ['.join(columns)}]"
1085
+ columns_str = list_to_sql_columns(columns)
1100
1086
 
1101
1087
  # Query db
1102
1088
  table = DetDatabase.get_table_name("client")
@@ -1131,7 +1117,7 @@ class DetDatabase:
1131
1117
 
1132
1118
  elif client_info.shape[0] == 0:
1133
1119
  available_values = self.load_clients(columns=[filter_column])
1134
- available_values_str = ", ".join(f"'{x}'" for x in available_values[filter_column])
1120
+ available_values_str = list_to_str(available_values[filter_column])
1135
1121
  raise ValueError(
1136
1122
  f"Value {filter_value} not found in column '{filter_column}'. Available values: "
1137
1123
  f"{available_values_str}."
@@ -0,0 +1,40 @@
1
+ # Internal modules
2
+ from detquantlib.utils import list_to_str
3
+
4
+ # Specify functions to expose
5
+ __all__ = ["list_to_sql_set", "list_to_sql_columns"]
6
+
7
+
8
+ def list_to_sql_set(list_in: list[str]) -> str:
9
+ """
10
+ Converts a list of the form ["a", "b", "c"] into a string of the form "('a', 'b', 'c')".
11
+ This string can be used in SQL queries applying the IN operator to filter data. Example:
12
+ >> "SELECT * FROM my_table WHERE my_column IN ('a', 'b', 'c')").
13
+
14
+ Args:
15
+ list_in: Input list of values
16
+
17
+ Returns:
18
+ Set of values reformatted in SQL string format
19
+ """
20
+ return "(" + list_to_str(list_in) + ")"
21
+
22
+
23
+ def list_to_sql_columns(list_in: list[str]) -> str:
24
+ """
25
+ Converts a list of the form ["a", "b", "c"] into a string of the form "[a], [b], [c]".
26
+ This string can be used in SQL queries to select specific columns. Example:
27
+ >> "SELECT [a], [b], [c] FROM my_table
28
+
29
+ Args:
30
+ list_in: Input list of values
31
+
32
+ Returns:
33
+ String containing reformatted values
34
+ """
35
+ # Convert columns to string
36
+ if len(list_in) == 1:
37
+ columns = str(list_in[0])
38
+ else:
39
+ columns = f"[{'], ['.join(list_in)}]"
40
+ return columns
@@ -1,11 +1,50 @@
1
1
  # Python built-in packages
2
- from datetime import datetime
2
+ from datetime import datetime, timezone
3
3
  from typing import Literal
4
4
 
5
5
  # Third-party packages
6
6
  import pandas as pd
7
7
  from dateutil.relativedelta import *
8
8
 
9
+ # Specify functions to expose
10
+ __all__ = [
11
+ "count_nr_hours",
12
+ "count_delivery_periods",
13
+ "calc_months_diff",
14
+ "datetime_to_month_code",
15
+ ]
16
+
17
+
18
+ def count_nr_hours(start_date: datetime, end_date: datetime) -> float:
19
+ """
20
+ Counts the number of hours in a given time interval.
21
+
22
+ Note:
23
+ - datetime.timedelta does not take into account timezone-awareness. As such, it does
24
+ not take into account DST switches in the hours count.
25
+ - To correct for this, we first convert the dates to UTC, and then count the number of
26
+ hours.
27
+
28
+ Args:
29
+ start_date: Start date
30
+ end_date: End date
31
+
32
+ Returns:
33
+ Number of hours in the given time interval
34
+ """
35
+
36
+ def _convert_to_utc(dt: datetime) -> datetime:
37
+ utc = timezone.utc
38
+ return dt.replace(tzinfo=timezone.utc) if dt.tzinfo is None else dt.astimezone(tz=utc)
39
+
40
+ # Convert dates to UTC
41
+ start_date = _convert_to_utc(start_date)
42
+ end_date = _convert_to_utc(end_date)
43
+
44
+ # Calculate nr hours
45
+ td = end_date - start_date
46
+ return td.days * 24 + td.seconds / 3600
47
+
9
48
 
10
49
  def count_delivery_periods(
11
50
  start_date: datetime,
@@ -5,6 +5,9 @@ from datetime import datetime
5
5
  import numpy as np
6
6
  import pandas as pd
7
7
 
8
+ # Specify functions to expose
9
+ __all__ = ["forecast_knife_strategy"]
10
+
8
11
 
9
12
  def forecast_knife_strategy(
10
13
  dates: list[datetime] | list[pd.Timestamp] | pd.DatetimeIndex, values: list | np.ndarray
@@ -8,7 +8,66 @@ import pandas as pd
8
8
  from dateutil.relativedelta import *
9
9
 
10
10
  # Internal modules
11
- from detquantlib.dates.dates import calc_months_diff
11
+ from detquantlib.dates import calc_months_diff, count_nr_hours
12
+ from detquantlib.utils import list_to_str
13
+
14
+ # Specify functions to expose
15
+ __all__ = [
16
+ "convert_delivery_start_date_to_maturity",
17
+ "convert_maturity_to_delivery_start_date",
18
+ "tenor_to_pd_offset_alias",
19
+ "tenor_to_nr_hours",
20
+ ]
21
+
22
+
23
+ def tenor_to_pd_offset_alias(tenor: str) -> str:
24
+ """A short helper function mapping a product tenor to the corresponding pandas offset alias."""
25
+ tenor = tenor.upper()
26
+ mapper = dict(QUARTERHOUR="15min", HALFHOUR="30min", HOUR="h", DAY="D", WEEK="W")
27
+ if tenor not in mapper.keys():
28
+ tenors_str = list_to_str(list(mapper.keys()))
29
+ raise ValueError(f"Input tenor '{tenor}' is not supported. Accepted values: {tenors_str}.")
30
+ return mapper[tenor]
31
+
32
+
33
+ def tenor_to_nr_hours(
34
+ tenor: str, delivery_start: datetime = None, delivery_end: datetime = None
35
+ ) -> float:
36
+ """
37
+ Counts the number of hours in a given tenor.
38
+
39
+ Args:
40
+ tenor: Tenor
41
+ delivery_start: Delivery start date (only used if tenor > 1 hour, to account for DST
42
+ switches)
43
+ delivery_end: Delivery end date (only used if tenor > 1 hour, to account for DST switches)
44
+
45
+ Returns:
46
+ Number of hours in tenor
47
+
48
+ Raises:
49
+ ValueError: Raises an error if the tenor delivery period is flexible and delivery dates
50
+ are not provided.
51
+ ValueError: Raises an error if he input tenor is not supported.
52
+ """
53
+ tenor = tenor.upper()
54
+ if tenor == "QUARTERHOUR":
55
+ nr_hours = 0.25
56
+ elif tenor == "HALFHOUR":
57
+ nr_hours = 0.5
58
+ elif tenor == "HOUR":
59
+ nr_hours = 1.0
60
+ elif tenor in ["DAY", "WEEK", "WEEKEND", "MONTH", "QUARTER", "YEAR"]:
61
+ if delivery_start and delivery_end:
62
+ nr_hours = count_nr_hours(delivery_start, delivery_end)
63
+ else:
64
+ raise ValueError(
65
+ f"Missing input(s) 'delivery_start' and/or 'delivery_end'. They must be provided "
66
+ f"for tenor '{tenor}', to account for DST switches and leap years."
67
+ )
68
+ else:
69
+ raise ValueError(f"Input tenor '{tenor}' is not supported.")
70
+ return nr_hours
12
71
 
13
72
 
14
73
  def convert_delivery_start_date_to_maturity(
@@ -0,0 +1,2 @@
1
+ from .logging import *
2
+ from .utils import *
@@ -8,6 +8,9 @@ from types import TracebackType
8
8
  # Third-party packages
9
9
  from colorlog import ColoredFormatter
10
10
 
11
+ # Specify functions to expose
12
+ __all__ = ["setup_logger"]
13
+
11
14
 
12
15
  def setup_logger(log_dir: Path):
13
16
  """
@@ -18,6 +21,7 @@ def setup_logger(log_dir: Path):
18
21
  """
19
22
  # Clear existing handlers to avoid duplicate logs
20
23
  for handler in logging.root.handlers[:]:
24
+ handler.close()
21
25
  logging.root.removeHandler(handler)
22
26
 
23
27
  # Console handler with colors
@@ -4,6 +4,9 @@ from datetime import datetime
4
4
  # Third-party packages
5
5
  import numpy as np
6
6
 
7
+ # Specify functions to expose
8
+ __all__ = ["add_log", "list_to_str", "divide_with_nan"]
9
+
7
10
 
8
11
  def add_log(message: str):
9
12
  """
@@ -15,6 +18,11 @@ def add_log(message: str):
15
18
  print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {message}")
16
19
 
17
20
 
21
+ def list_to_str(lt: list[str | int | float | bool]) -> str:
22
+ """Converts a list of the form [a, b, c] into a string of the form "'a', 'b', 'c'"."""
23
+ return ", ".join(f"'{x}'" for x in lt)
24
+
25
+
18
26
  def divide_with_nan(
19
27
  numerator: np.array, denominator: np.array, nan=np.nan, posinf=np.nan, neginf=np.nan
20
28
  ) -> np.array:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "detquantlib"
3
- version = "3.12.0"
3
+ version = "3.14.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"
@@ -1,5 +0,0 @@
1
- from .databases.detdatabase import DetDatabase
2
- from .entsoe.entsoe import Entsoe
3
- from .sftp.sftp import Sftp
4
-
5
- __all__ = ["DetDatabase", "Entsoe", "Sftp"]
@@ -1 +0,0 @@
1
- from .utils import *
File without changes