detquantlib 3.11.0__tar.gz → 3.13.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.11.0 → detquantlib-3.13.0}/PKG-INFO +11 -1
  2. {detquantlib-3.11.0 → detquantlib-3.13.0}/README.md +9 -0
  3. detquantlib-3.13.0/detquantlib/converters/__init__.py +2 -0
  4. detquantlib-3.13.0/detquantlib/converters/definitions.py +24 -0
  5. detquantlib-3.13.0/detquantlib/converters/energy.py +140 -0
  6. detquantlib-3.13.0/detquantlib/converters/helpers.py +67 -0
  7. detquantlib-3.13.0/detquantlib/converters/price.py +107 -0
  8. detquantlib-3.13.0/detquantlib/data/__init__.py +6 -0
  9. detquantlib-3.13.0/detquantlib/data/databases/helpers.py +40 -0
  10. {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/dates/dates.py +40 -1
  11. {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/forecasting/forecasting.py +3 -0
  12. {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/tradable_products/tradable_products.py +60 -1
  13. detquantlib-3.13.0/detquantlib/utils/__init__.py +2 -0
  14. detquantlib-3.13.0/detquantlib/utils/logging.py +126 -0
  15. {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/utils/utils.py +8 -0
  16. {detquantlib-3.11.0 → detquantlib-3.13.0}/pyproject.toml +2 -1
  17. detquantlib-3.11.0/detquantlib/data/__init__.py +0 -5
  18. detquantlib-3.11.0/detquantlib/utils/__init__.py +0 -1
  19. {detquantlib-3.11.0 → detquantlib-3.13.0}/LICENSE.txt +0 -0
  20. {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/__init__.py +0 -0
  21. {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/data/databases/detdatabase.py +0 -0
  22. {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/data/entsoe/entsoe.py +0 -0
  23. {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/data/sftp/sftp.py +0 -0
  24. {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/dates/__init__.py +0 -0
  25. {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/figures/__init__.py +0 -0
  26. {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/figures/plotly_figures.py +0 -0
  27. {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/forecasting/__init__.py +0 -0
  28. {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/outputs/__init__.py +0 -0
  29. {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/outputs/outputs_interface.py +0 -0
  30. {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/stats/__init__.py +0 -0
  31. {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/stats/data_analysis.py +0 -0
  32. {detquantlib-3.11.0 → detquantlib-3.13.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.11.0
3
+ Version: 3.13.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
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
13
  Classifier: Programming Language :: Python :: 3.14
14
+ Requires-Dist: colorlog (>=6.10.1,<7.0.0)
14
15
  Requires-Dist: numpy (>=2.2.6,<3.0.0)
15
16
  Requires-Dist: pandas (>=2.3.3,<3.0.0)
16
17
  Requires-Dist: paramiko (>=4.0.0,<5.0.0)
@@ -98,6 +99,15 @@ Classes:
98
99
  - `from detquantlib.data import Sftp`
99
100
  - `from detquantlib.data.sftp.sftp import Sftp`
100
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
+
101
111
  <!-- END EXPOSED SYMBOLS AUTO-GENERATED -->
102
112
 
103
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"]
@@ -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 *
@@ -0,0 +1,126 @@
1
+ # Python built-in packages
2
+ import logging
3
+ import sys
4
+ import threading
5
+ from pathlib import Path
6
+ from types import TracebackType
7
+
8
+ # Third-party packages
9
+ from colorlog import ColoredFormatter
10
+
11
+ # Specify functions to expose
12
+ __all__ = ["setup_logger"]
13
+
14
+
15
+ def setup_logger(log_dir: Path):
16
+ """
17
+ Initializes logging with both console and file handlers.
18
+
19
+ Args:
20
+ log_dir: Full path to the log file
21
+ """
22
+ # Clear existing handlers to avoid duplicate logs
23
+ for handler in logging.root.handlers[:]:
24
+ handler.close()
25
+ logging.root.removeHandler(handler)
26
+
27
+ # Console handler with colors
28
+ console_formatter = CustomFormatter(
29
+ fmt=(
30
+ "%(log_color)s%(asctime)-8s%(reset)s "
31
+ "%(log_color)s[%(levelname)s]%(reset)s "
32
+ "%(log_color)s%(message)s"
33
+ ),
34
+ datefmt="%Y-%m-%d %H:%M:%S",
35
+ reset=True,
36
+ )
37
+ console_handler = logging.StreamHandler()
38
+ console_handler.setLevel(logging.INFO)
39
+ console_handler.setFormatter(console_formatter)
40
+
41
+ # File handler (plain, no colors)
42
+ file_formatter = logging.Formatter(
43
+ fmt="%(asctime)s [%(levelname)s] %(message)s",
44
+ datefmt="%Y-%m-%d %H:%M:%S",
45
+ )
46
+ file_handler = logging.FileHandler(log_dir, mode="a", encoding="utf-8")
47
+ file_handler.setLevel(logging.INFO)
48
+ file_handler.setFormatter(file_formatter)
49
+
50
+ # Configure root logger
51
+ root_logger = logging.getLogger()
52
+ root_logger.setLevel(logging.INFO)
53
+ root_logger.addHandler(console_handler)
54
+ root_logger.addHandler(file_handler)
55
+
56
+ # Capture uncaught exceptions
57
+ sys.excepthook = exception_handler
58
+ threading.excepthook = thread_exception_handler
59
+
60
+
61
+ def exception_handler(
62
+ exc_type: type[BaseException], exc_value: BaseException, exc_traceback: TracebackType
63
+ ):
64
+ """
65
+ Handles uncaught exceptions in the main thread and logs them.
66
+
67
+ Args:
68
+ exc_type: Exception type
69
+ exc_value: Exception instance
70
+ exc_traceback: Traceback object
71
+ """
72
+ if issubclass(exc_type, KeyboardInterrupt):
73
+ sys.__excepthook__(exc_type, exc_value, exc_traceback)
74
+ return
75
+
76
+ logging.critical(
77
+ "Uncaught exception",
78
+ exc_info=(exc_type, exc_value, exc_traceback),
79
+ )
80
+
81
+
82
+ def thread_exception_handler(args: threading.ExceptHookArgs):
83
+ """
84
+ Handles uncaught exceptions in threads and logs them.
85
+
86
+ Args:
87
+ args: Thread exception arguments
88
+ """
89
+ logging.critical(
90
+ f"Uncaught thread exception in {args.thread.name}",
91
+ exc_info=(args.exc_type, args.exc_value, args.exc_traceback),
92
+ )
93
+
94
+
95
+ class CustomFormatter(ColoredFormatter):
96
+ """Custom formatter to override default colorlog colors."""
97
+
98
+ # ANSI color codes
99
+ COLORS = dict(
100
+ BRIGHT_WHITE="\033[97m",
101
+ LIGHT_WHITE="\033[38;5;250m",
102
+ BRIGHT_YELLOW="\033[93m",
103
+ RED="\033[91m",
104
+ RESET="\033[0m",
105
+ )
106
+
107
+ def _get_escape_code(self, log_colors: dict, level_name: str) -> str:
108
+ """
109
+ Returns ANSI escape codes for log levels.
110
+
111
+ Overrides DEBUG/INFO to light-white, CRITICAL to non-bold red. WARNING/ERROR remain
112
+ default.
113
+
114
+ Args:
115
+ log_colors: Mapping of log levels to colors
116
+ level_name: Log level name
117
+
118
+ Returns:
119
+ ANSI escape code string
120
+ """
121
+ if level_name in ("DEBUG", "INFO"):
122
+ return CustomFormatter.COLORS["LIGHT_WHITE"]
123
+ if level_name == "CRITICAL":
124
+ return super()._get_escape_code(log_colors, "ERROR")
125
+ else:
126
+ return super()._get_escape_code(log_colors, level_name)
@@ -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.11.0"
3
+ version = "3.13.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"
@@ -20,6 +20,7 @@ numpy = "^2.2.6"
20
20
  pandas = "^2.3.3"
21
21
  pyarrow = "^21.0.0"
22
22
  scipy = "^1.15.2"
23
+ colorlog = "^6.10.1"
23
24
 
24
25
  [tool.poetry.group.dev.dependencies]
25
26
  toml = "^0.10.2"
@@ -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