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.
- {detquantlib-3.11.0 → detquantlib-3.13.0}/PKG-INFO +11 -1
- {detquantlib-3.11.0 → detquantlib-3.13.0}/README.md +9 -0
- detquantlib-3.13.0/detquantlib/converters/__init__.py +2 -0
- detquantlib-3.13.0/detquantlib/converters/definitions.py +24 -0
- detquantlib-3.13.0/detquantlib/converters/energy.py +140 -0
- detquantlib-3.13.0/detquantlib/converters/helpers.py +67 -0
- detquantlib-3.13.0/detquantlib/converters/price.py +107 -0
- detquantlib-3.13.0/detquantlib/data/__init__.py +6 -0
- detquantlib-3.13.0/detquantlib/data/databases/helpers.py +40 -0
- {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/dates/dates.py +40 -1
- {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/forecasting/forecasting.py +3 -0
- {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/tradable_products/tradable_products.py +60 -1
- detquantlib-3.13.0/detquantlib/utils/__init__.py +2 -0
- detquantlib-3.13.0/detquantlib/utils/logging.py +126 -0
- {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/utils/utils.py +8 -0
- {detquantlib-3.11.0 → detquantlib-3.13.0}/pyproject.toml +2 -1
- detquantlib-3.11.0/detquantlib/data/__init__.py +0 -5
- detquantlib-3.11.0/detquantlib/utils/__init__.py +0 -1
- {detquantlib-3.11.0 → detquantlib-3.13.0}/LICENSE.txt +0 -0
- {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/__init__.py +0 -0
- {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/data/databases/detdatabase.py +0 -0
- {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/data/entsoe/entsoe.py +0 -0
- {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/data/sftp/sftp.py +0 -0
- {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/dates/__init__.py +0 -0
- {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/figures/__init__.py +0 -0
- {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/figures/plotly_figures.py +0 -0
- {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/forecasting/__init__.py +0 -0
- {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/outputs/__init__.py +0 -0
- {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/outputs/outputs_interface.py +0 -0
- {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/stats/__init__.py +0 -0
- {detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/stats/data_analysis.py +0 -0
- {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.
|
|
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,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,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
|
{detquantlib-3.11.0 → detquantlib-3.13.0}/detquantlib/tradable_products/tradable_products.py
RENAMED
|
@@ -8,7 +8,66 @@ import pandas as pd
|
|
|
8
8
|
from dateutil.relativedelta import *
|
|
9
9
|
|
|
10
10
|
# Internal modules
|
|
11
|
-
from detquantlib.dates
|
|
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,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.
|
|
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 +0,0 @@
|
|
|
1
|
-
from .utils import *
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|