Coverage for tests / test_pfc.py: 99%
166 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-27 14:36 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-27 14:36 +0000
1import unittest
2import holidays
3import itertools
4import pandas as pd
5import numpy as np
6import datetime as dt
7from rivapy.tools.scheduler import SimpleSchedule, OffPeakSchedule, PeakSchedule, GasSchedule, BaseSchedule
8from rivapy.marketdata_tools import PFCShifter
9from rivapy.marketdata_tools.pfc_shaper import PFCShaper, CategoricalRegression, SimpleCategoricalRegression, CategoricalFourierShaper
10from rivapy.instruments.energy_futures_specifications import EnergyFutureSpecifications
11from rivapy.sample_data.dummy_power_spot_price import spot_price_model
12from rivapy.marketdata.curves import EnergyPriceForwardCurve
13from typing import Dict
14from collections import defaultdict
17class TestPFCShifter(unittest.TestCase):
18 def __init__(self, *args, **kwargs):
19 super(TestPFCShifter, self).__init__(*args, **kwargs)
20 self.parameter_dict = {
21 "spot_price_level": 100,
22 "peak_price_level": 10,
23 "solar_price_level": 8,
24 "weekend_price_level": 10,
25 "winter_price_level": 20,
26 "epsilon_mean": 0,
27 "epsilon_var": 5,
28 }
29 self.date_range = pd.date_range(start="1/1/2023", end="1/1/2025", freq="h", inclusive="left")
30 self.spot_prices = list(map(lambda x: spot_price_model(x, **self.parameter_dict), self.date_range))
31 self.df = pd.DataFrame(data=self.spot_prices, index=self.date_range, columns=["Spot"])
32 base_y = self.df.resample("YE").mean()
33 base_y.index = base_y.index.strftime("%Y")
35 df_spot = self.df.copy()
36 df_spot.index = df_spot.index.strftime("%Y")
38 shape = df_spot.divide(base_y, axis="index")
39 self.shape_df = pd.DataFrame(data=shape["Spot"].tolist(), index=self.date_range, columns=["shape"])
41 def __get_contracts_dict(self, contracts_schedules: Dict[str, Dict[str, SimpleSchedule]]) -> Dict[str, Dict[str, EnergyFutureSpecifications]]:
42 contracts = defaultdict(dict)
43 for contract_type, contracts_dict in contracts_schedules.items():
44 for contract_name, schedule in contracts_dict.items():
45 tg = schedule.get_schedule()
46 price = self.df.loc[tg, :].mean().iloc[0]
47 contracts[contract_type][contract_name] = EnergyFutureSpecifications(schedule=schedule, price=price, name=contract_name)
48 return dict(contracts)
50 def test_pfc_shifter(self):
51 contracts_schedules = {
52 "off_peak": {
53 "Cal23_OffPeak": OffPeakSchedule(dt.datetime(2023, 1, 1), dt.datetime(2024, 1, 1)),
54 "Q2/23_OffPeak": OffPeakSchedule(dt.datetime(2023, 4, 1), dt.datetime(2023, 7, 1)),
55 },
56 "peak": {
57 "Cal23_Peak": PeakSchedule(dt.datetime(2023, 1, 1), dt.datetime(2024, 1, 1)),
58 "Cal24_Peak": PeakSchedule(dt.datetime(2024, 1, 1), dt.datetime(2025, 1, 1)),
59 "Q1/23_Peak": PeakSchedule(dt.datetime(2023, 1, 1), dt.datetime(2023, 4, 1)),
60 "Q2/23_Peak": PeakSchedule(dt.datetime(2023, 4, 1), dt.datetime(2023, 7, 1)),
61 "Q3/23_Peak": PeakSchedule(dt.datetime(2023, 7, 1), dt.datetime(2023, 10, 1)),
62 "Q4/23_Peak": PeakSchedule(dt.datetime(2023, 10, 1), dt.datetime(2024, 1, 1)),
63 "Q2/24_Peak": PeakSchedule(dt.datetime(2024, 4, 1), dt.datetime(2024, 7, 1)),
64 },
65 }
66 contracts = self.__get_contracts_dict(contracts_schedules=contracts_schedules)
67 data_dict = {
68 "off_peak": self.shape_df.loc[
69 SimpleSchedule(
70 dt.datetime(2023, 1, 1),
71 dt.datetime(2024, 1, 1),
72 freq="h",
73 hours=[0, 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 23],
74 ignore_hours_for_weekdays=[5, 6],
75 ).get_schedule()
76 ],
77 "peak": self.shape_df.loc[
78 SimpleSchedule(
79 dt.datetime(2023, 1, 1),
80 dt.datetime(2025, 1, 1),
81 freq="h",
82 hours=[8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
83 weekdays=[0, 1, 2, 3, 4],
84 ).get_schedule()
85 ],
86 }
87 result = []
88 for contracts_type, data in data_dict.items():
89 pfc_shifter = PFCShifter(shape=data, contracts=list(contracts[contracts_type].values()))
90 result.append(pfc_shifter.compute())
92 df_shifted = pd.concat(result, axis=0)
93 df_shifted.sort_index(inplace=True, ascending=True)
95 contracts_all = self.__get_contracts_dict(contracts_schedules=contracts_schedules)
96 for contracts_type, contracts_dict in contracts_all.items():
97 for contract_name, contract_spec in contracts_dict.items():
98 shift_price = df_shifted.loc[contract_spec.get_schedule(), :].mean().iloc[0]
99 self.assertAlmostEqual(contract_spec.price, shift_price, delta=10 ** (-10))
101 def test_only_overlapping_contracts(self):
102 contracts_schedules = {
103 "GasBase": {
104 "Cal23_GasBase": GasSchedule(dt.datetime(2023, 1, 1), dt.datetime(2024, 1, 1)),
105 "Q1/23_GasBase": GasSchedule(dt.datetime(2023, 1, 1), dt.datetime(2023, 4, 1)),
106 "Q2/23_GasBase": GasSchedule(dt.datetime(2023, 4, 1), dt.datetime(2023, 7, 1)),
107 "Q3/23_GasBase": GasSchedule(dt.datetime(2023, 7, 1), dt.datetime(2023, 10, 1)),
108 "Q4/23_GasBase": GasSchedule(dt.datetime(2023, 10, 1), dt.datetime(2024, 1, 1)),
109 },
110 }
111 contracts = self.__get_contracts_dict(contracts_schedules=contracts_schedules)
112 data_dict = {
113 "GasBase": self.shape_df.loc[GasSchedule(dt.datetime(2023, 1, 1), dt.datetime(2024, 1, 1)).get_schedule()],
114 }
115 result = []
116 for contracts_type, data in data_dict.items():
117 pfc_shifter = PFCShifter(shape=data, contracts=list(contracts[contracts_type].values()))
118 result.append(pfc_shifter.compute())
120 df_shifted = pd.concat(result, axis=0)
121 df_shifted.sort_index(inplace=True, ascending=True)
123 contracts_all = self.__get_contracts_dict(contracts_schedules=contracts_schedules)
124 for contracts_type, contracts_dict in contracts_all.items():
125 for contract_name, contract_spec in contracts_dict.items():
126 shift_price = df_shifted.loc[contract_spec.get_schedule(), :].mean().iloc[0]
127 self.assertAlmostEqual(contract_spec.price, shift_price, delta=10 ** (-10))
129 def test_non_coverage_of_shape(self):
130 contracts_schedules = {
131 "Peak": {
132 "Cal23_GasBase": PeakSchedule(dt.datetime(2023, 1, 1), dt.datetime(2024, 1, 1)),
133 "Q1/23_GasBase": PeakSchedule(dt.datetime(2023, 1, 1), dt.datetime(2023, 4, 1)),
134 "Q2/23_GasBase": PeakSchedule(dt.datetime(2023, 4, 1), dt.datetime(2023, 7, 1)),
135 "Q3/23_GasBase": PeakSchedule(dt.datetime(2023, 7, 1), dt.datetime(2023, 10, 1)),
136 "Q4/23_GasBase": PeakSchedule(dt.datetime(2023, 10, 1), dt.datetime(2024, 1, 1)),
137 },
138 }
139 contracts = self.__get_contracts_dict(contracts_schedules=contracts_schedules)
140 data_dict = {
141 "Peak": self.shape_df.loc[SimpleSchedule(dt.datetime(2023, 1, 1), dt.datetime(2024, 1, 1)).get_schedule()],
142 }
143 result = []
144 with self.assertRaises(ValueError):
145 for contracts_type, data in data_dict.items():
146 pfc_shifter = PFCShifter(shape=data, contracts=list(contracts[contracts_type].values()))
147 result.append(pfc_shifter())
150class TestPFCShaper(unittest.TestCase):
151 def __init__(self, *args, **kwargs):
152 super(TestPFCShaper, self).__init__(*args, **kwargs)
154 self.example_spot_price_data = pd.read_excel(
155 "./tests/data/hpfc_test.xlsx", parse_dates=["Date"], date_format="%d.%m.%Y %H:%M", index_col="Date"
156 )
158 def test_categoricalregression(self):
159 parameter_dict = {
160 "spot_price_level": 100,
161 "peak_price_level": 10,
162 "solar_price_level": 8,
163 "weekend_price_level": 10,
164 "winter_price_level": 20,
165 "epsilon_mean": 0,
166 "epsilon_var": 5,
167 }
168 date_range = pd.date_range(start="1/1/2024", end="1/1/2025", freq="h", inclusive="left")
169 spot_prices = list(map(lambda x: spot_price_model(x, **parameter_dict), date_range))
170 spot_prices = pd.DataFrame(data=spot_prices, index=date_range, columns=["Spot"])
172 holiday_calendar = holidays.country_holidays("DE", years=[2024])
174 apply_schedule = SimpleSchedule(start=dt.datetime(2025, 1, 1), end=dt.datetime(2026, 1, 1), freq="h")
176 for regression_obj in [CategoricalRegression, SimpleCategoricalRegression, CategoricalFourierShaper]:
177 pfc_shaper = regression_obj(spot_prices=spot_prices, holiday_calendar=holiday_calendar)
178 pfc_shaper.calibrate()
179 pfc_fit = pfc_shaper.apply(spot_prices.index)
181 self.assertLess(np.sum((spot_prices.values / np.mean(spot_prices.values) - pfc_fit.to_numpy())) ** 2, 10 ** (-5))
183 pfc_rollout = pfc_shaper.apply(apply_schedule=apply_schedule.get_schedule())
184 spot_prices_rollout = np.array(list(map(lambda x: spot_price_model(x, **parameter_dict), apply_schedule.get_schedule())))
186 self.assertLess(np.sum((spot_prices_rollout / np.mean(spot_prices_rollout) - pfc_rollout.values)) ** 2, 10 ** (-5))
188 def test_normalization_with_config(self):
189 normalization_config = {"D": 2, "W": 2, "ME": 1}
190 holiday_calendar = holidays.country_holidays("DE", years=[2024, 2025, 2026])
192 apply_schedule = BaseSchedule(start=dt.datetime(2025, 12, 31), end=dt.datetime(2027, 1, 1))
194 for regression_obj in [CategoricalRegression, SimpleCategoricalRegression, CategoricalFourierShaper]:
195 pfc_shaper = regression_obj(
196 spot_prices=self.example_spot_price_data, holiday_calendar=holiday_calendar, normalization_config=normalization_config
197 )
198 pfc_shaper.calibrate()
199 pfc_shape = pfc_shaper.apply(apply_schedule=apply_schedule.get_schedule())
201 self.assertLess(np.abs(pfc_shape.loc[pfc_shape.index < dt.datetime(2026, 1, 1), :].mean().iloc[0] - 1.0), 10 ** (-5))
203 self.assertLess(
204 np.abs(
205 pfc_shape.loc[(dt.datetime(2026, 1, 1) <= pfc_shape.index) & (pfc_shape.index < dt.datetime(2026, 1, 2)), :].mean().iloc[0] - 1.0
206 ),
207 10 ** (-5),
208 )
210 self.assertLess(
211 np.abs(
212 pfc_shape.loc[(dt.datetime(2026, 1, 2) <= pfc_shape.index) & (pfc_shape.index < dt.datetime(2026, 1, 5)), :].mean().iloc[0] - 1.0
213 ),
214 10 ** (-5),
215 )
217 self.assertLess(
218 np.abs(
219 pfc_shape.loc[(dt.datetime(2026, 1, 5) <= pfc_shape.index) & (pfc_shape.index < dt.datetime(2026, 1, 12)), :].mean().iloc[0] - 1.0
220 ),
221 10 ** (-5),
222 )
224 self.assertLess(
225 np.abs(
226 pfc_shape.loc[(dt.datetime(2026, 1, 12) <= pfc_shape.index) & (pfc_shape.index < dt.datetime(2026, 2, 1)), :].mean().iloc[0] - 1.0
227 ),
228 10 ** (-5),
229 )
231 self.assertLess(
232 np.abs(
233 pfc_shape.loc[(dt.datetime(2026, 2, 1) <= pfc_shape.index) & (pfc_shape.index < dt.datetime(2027, 1, 1)), :].mean().iloc[0] - 1.0
234 ),
235 10 ** (-5),
236 )
238 def test_normalization_without_config(self):
239 holiday_calendar = holidays.country_holidays("DE", years=[2024, 2025, 2026])
240 apply_schedule = BaseSchedule(start=dt.datetime(2025, 12, 31), end=dt.datetime(2027, 1, 1))
241 for regression_obj in [CategoricalRegression, SimpleCategoricalRegression, CategoricalFourierShaper]:
242 pfc_shaper = regression_obj(spot_prices=self.example_spot_price_data, holiday_calendar=holiday_calendar)
243 pfc_shaper.calibrate()
244 pfc_shape = pfc_shaper.apply(apply_schedule=apply_schedule.get_schedule())
246 self.assertLess(np.abs(pfc_shape.loc[pfc_shape.index < dt.datetime(2026, 1, 1), :].mean().iloc[0] - 1.0), 10 ** (-5))
248 self.assertLess(
249 np.abs(
250 pfc_shape.loc[(dt.datetime(2026, 1, 1) <= pfc_shape.index) & (pfc_shape.index < dt.datetime(2027, 1, 1)), :].mean().iloc[0] - 1.0
251 ),
252 10 ** (-5),
253 )
256class TestEnergyPriceForwardCurve(unittest.TestCase):
257 def __init__(self, *args, **kwargs):
258 super(TestEnergyPriceForwardCurve, self).__init__(*args, **kwargs)
260 self.example_spot_price_data = pd.read_excel(
261 "./tests/data/hpfc_test.xlsx", parse_dates=["Date"], date_format="%d.%m.%Y %H:%M", index_col="Date"
262 )
263 self.parameter_dict = {
264 "spot_price_level": 100,
265 "peak_price_level": 10,
266 "solar_price_level": 8,
267 "weekend_price_level": 10,
268 "winter_price_level": 20,
269 "epsilon_mean": 0,
270 "epsilon_var": 5,
271 }
272 self.date_range = pd.date_range(start="1/1/2023", end="1/1/2025", freq="h", inclusive="left")
273 self.spot_prices = list(map(lambda x: spot_price_model(x, **self.parameter_dict), self.date_range))
274 self.df = pd.DataFrame(data=self.spot_prices, index=self.date_range, columns=["Spot"])
275 base_y = self.df.resample("YE").mean()
276 base_y.index = base_y.index.strftime("%Y")
278 df_spot = self.df.copy()
279 df_spot.index = df_spot.index.strftime("%Y")
281 shape = df_spot.divide(base_y, axis="index")
282 self.shape_df = pd.DataFrame(data=shape["Spot"].tolist(), index=self.date_range, columns=["shape"])
284 def __get_contracts_list(self, contracts_schedules: Dict[str, Dict[str, SimpleSchedule]]) -> Dict[str, Dict[str, EnergyFutureSpecifications]]:
285 contracts = defaultdict(dict)
286 for contract_type, contracts_dict in contracts_schedules.items():
287 for contract_name, schedule in contracts_dict.items():
288 tg = schedule.get_schedule()
289 price = self.df.loc[tg, :].mean().iloc[0]
290 contracts[contract_type][contract_name] = EnergyFutureSpecifications(schedule=schedule, price=price, name=contract_name)
291 return list(itertools.chain(*[list(constracts_spec.values()) for constracts_spec in contracts.values()]))
293 def test_from_shape(self):
294 contracts_schedules = {
295 "base": {
296 "Cal23_Base": BaseSchedule(dt.datetime(2023, 1, 1), dt.datetime(2024, 1, 1)),
297 "Q2/23_Base": BaseSchedule(dt.datetime(2023, 4, 1), dt.datetime(2023, 7, 1)),
298 },
299 "peak": {
300 "Cal23_Peak": PeakSchedule(dt.datetime(2023, 1, 1), dt.datetime(2024, 1, 1)),
301 "Cal24_Peak": PeakSchedule(dt.datetime(2024, 1, 1), dt.datetime(2025, 1, 1)),
302 "Q1/23_Peak": PeakSchedule(dt.datetime(2023, 1, 1), dt.datetime(2023, 4, 1)),
303 "Q2/23_Peak": PeakSchedule(dt.datetime(2023, 4, 1), dt.datetime(2023, 7, 1)),
304 "Q3/23_Peak": PeakSchedule(dt.datetime(2023, 7, 1), dt.datetime(2023, 10, 1)),
305 "Q4/23_Peak": PeakSchedule(dt.datetime(2023, 10, 1), dt.datetime(2024, 1, 1)),
306 "Q2/24_Peak": PeakSchedule(dt.datetime(2024, 4, 1), dt.datetime(2024, 7, 1)),
307 },
308 }
309 contracts_list = self.__get_contracts_list(contracts_schedules=contracts_schedules)
310 pfc_obj = EnergyPriceForwardCurve.from_existing_shape(id=None, refdate=None, pfc_shape=self.shape_df, future_contracts=contracts_list)
312 pfc = pfc_obj.get_pfc()
314 for contract in contracts_list:
315 pfc_price = np.mean(pfc.loc[contract.get_schedule(), :])
316 self.assertAlmostEqual(pfc_price, contract.get_price(), delta=10 ** (-10))
318 def test_from_scratch(self):
319 contracts_schedules = {
320 "base": {
321 "Cal23_Base": SimpleSchedule(dt.datetime(2023, 1, 1), dt.datetime(2024, 1, 1), freq="D"),
322 "Q2/23_Base": BaseSchedule(dt.datetime(2023, 4, 1), dt.datetime(2023, 7, 1)),
323 },
324 "peak": {
325 "Cal23_Peak": PeakSchedule(dt.datetime(2023, 1, 1), dt.datetime(2024, 1, 1)),
326 "Cal24_Peak": PeakSchedule(dt.datetime(2024, 1, 1), dt.datetime(2025, 1, 1)),
327 "Q1/23_Peak": PeakSchedule(dt.datetime(2023, 1, 1), dt.datetime(2023, 4, 1)),
328 "Q2/23_Peak": PeakSchedule(dt.datetime(2023, 4, 1), dt.datetime(2023, 7, 1)),
329 "Q3/23_Peak": PeakSchedule(dt.datetime(2023, 7, 1), dt.datetime(2023, 10, 1)),
330 "Q4/23_Peak": PeakSchedule(dt.datetime(2023, 10, 1), dt.datetime(2024, 1, 1)),
331 "Q2/24_Peak": PeakSchedule(dt.datetime(2024, 4, 1), dt.datetime(2024, 7, 1)),
332 },
333 }
334 contracts_list = self.__get_contracts_list(contracts_schedules=contracts_schedules)
335 normalization_config = {"D": 2, "W": 2, "ME": 1}
336 holiday_calendar = holidays.country_holidays("DE", years=[2024, 2025, 2026])
337 apply_schedule = BaseSchedule(start=dt.datetime(2025, 12, 31), end=dt.datetime(2027, 1, 1))
339 pfc_shaper = CategoricalRegression(
340 spot_prices=self.example_spot_price_data, holiday_calendar=holiday_calendar, normalization_config=normalization_config
341 )
342 # raise ValueError due to different frequencies in the contracts
343 with self.assertRaises(ValueError):
344 pfc_obj = EnergyPriceForwardCurve.from_scratch(
345 id=None, refdate=None, apply_schedule=apply_schedule, pfc_shaper=pfc_shaper, future_contracts=contracts_list
346 )
348 def test_existing_pfc(self):
349 # create shape without DateTimeIndex
350 not_acceptable_shape = pd.DataFrame(data=np.array([1, 2, 3, 4, 5, 6, 7, 8, 9]))
351 with self.assertRaises(TypeError):
352 pfc_obj = EnergyPriceForwardCurve.from_existing_pfc(id=None, refdate=None, pfc=not_acceptable_shape)
354 # create shape which is not a pd.DataFrame
355 not_acceptable_shape = [1, 2, 3, 4, 5, 6, 7, 8, 9]
356 with self.assertRaises(TypeError):
357 pfc_obj = EnergyPriceForwardCurve.from_existing_pfc(id=None, refdate=None, pfc=not_acceptable_shape)
359 with self.assertRaises(ValueError):
360 EnergyPriceForwardCurve(id=None, refdate=None)
363if __name__ == "__main__":
364 unittest.main()