Coverage for tests/test_pfc.py: 99%
162 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-05 14:27 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-05 14:27 +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
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 pfc_shaper = CategoricalRegression(spot_prices=spot_prices, holiday_calendar=holiday_calendar)
177 pfc_fit = pfc_shaper.calibrate()
179 self.assertLess(np.sum((spot_prices.values - pfc_fit)) ** 2, 10 ** (-5))
181 pfc_rollout = pfc_shaper.apply(apply_schedule=apply_schedule)
182 spot_prices_rollout = np.array(list(map(lambda x: spot_price_model(x, **parameter_dict), apply_schedule.get_schedule())))
184 self.assertLess(np.sum((spot_prices_rollout / np.mean(spot_prices_rollout) - pfc_rollout.values)) ** 2, 10 ** (-5))
186 def test_normalization_with_config(self):
187 normalization_config = {"D": 2, "W": 2, "ME": 1}
188 holiday_calendar = holidays.country_holidays("DE", years=[2024, 2025, 2026])
190 pfc_shaper = CategoricalRegression(
191 spot_prices=self.example_spot_price_data, holiday_calendar=holiday_calendar, normalization_config=normalization_config
192 )
194 apply_schedule = BaseSchedule(start=dt.datetime(2025, 12, 31), end=dt.datetime(2027, 1, 1))
196 pfc_spot = pfc_shaper.calibrate()
197 pfc_shape = pfc_shaper.apply(apply_schedule=apply_schedule)
199 self.assertLess(np.abs(pfc_shape.loc[pfc_shape.index < dt.datetime(2026, 1, 1), :].mean().iloc[0] - 1.0), 10 ** (-5))
201 self.assertLess(
202 np.abs(pfc_shape.loc[(dt.datetime(2026, 1, 1) <= pfc_shape.index) & (pfc_shape.index < dt.datetime(2026, 1, 2)), :].mean().iloc[0] - 1.0),
203 10 ** (-5),
204 )
206 self.assertLess(
207 np.abs(pfc_shape.loc[(dt.datetime(2026, 1, 2) <= pfc_shape.index) & (pfc_shape.index < dt.datetime(2026, 1, 5)), :].mean().iloc[0] - 1.0),
208 10 ** (-5),
209 )
211 self.assertLess(
212 np.abs(
213 pfc_shape.loc[(dt.datetime(2026, 1, 5) <= pfc_shape.index) & (pfc_shape.index < dt.datetime(2026, 1, 12)), :].mean().iloc[0] - 1.0
214 ),
215 10 ** (-5),
216 )
218 self.assertLess(
219 np.abs(
220 pfc_shape.loc[(dt.datetime(2026, 1, 12) <= pfc_shape.index) & (pfc_shape.index < dt.datetime(2026, 2, 1)), :].mean().iloc[0] - 1.0
221 ),
222 10 ** (-5),
223 )
225 self.assertLess(
226 np.abs(pfc_shape.loc[(dt.datetime(2026, 2, 1) <= pfc_shape.index) & (pfc_shape.index < dt.datetime(2027, 1, 1)), :].mean().iloc[0] - 1.0),
227 10 ** (-5),
228 )
230 def test_normalization_without_config(self):
232 holiday_calendar = holidays.country_holidays("DE", years=[2024, 2025, 2026])
234 pfc_shaper = CategoricalRegression(spot_prices=self.example_spot_price_data, holiday_calendar=holiday_calendar)
236 apply_schedule = BaseSchedule(start=dt.datetime(2025, 12, 31), end=dt.datetime(2027, 1, 1))
238 pfc_spot = pfc_shaper.calibrate()
239 pfc_shape = pfc_shaper.apply(apply_schedule=apply_schedule)
241 self.assertLess(np.abs(pfc_shape.loc[pfc_shape.index < dt.datetime(2026, 1, 1), :].mean().iloc[0] - 1.0), 10 ** (-5))
243 self.assertLess(
244 np.abs(pfc_shape.loc[(dt.datetime(2026, 1, 1) <= pfc_shape.index) & (pfc_shape.index < dt.datetime(2027, 1, 1)), :].mean().iloc[0] - 1.0),
245 10 ** (-5),
246 )
249class TestEnergyPriceForwardCurve(unittest.TestCase):
250 def __init__(self, *args, **kwargs):
251 super(TestEnergyPriceForwardCurve, self).__init__(*args, **kwargs)
253 self.example_spot_price_data = pd.read_excel(
254 "./tests/data/hpfc_test.xlsx", parse_dates=["Date"], date_format="%d.%m.%Y %H:%M", index_col="Date"
255 )
256 self.parameter_dict = {
257 "spot_price_level": 100,
258 "peak_price_level": 10,
259 "solar_price_level": 8,
260 "weekend_price_level": 10,
261 "winter_price_level": 20,
262 "epsilon_mean": 0,
263 "epsilon_var": 5,
264 }
265 self.date_range = pd.date_range(start="1/1/2023", end="1/1/2025", freq="h", inclusive="left")
266 self.spot_prices = list(map(lambda x: spot_price_model(x, **self.parameter_dict), self.date_range))
267 self.df = pd.DataFrame(data=self.spot_prices, index=self.date_range, columns=["Spot"])
268 base_y = self.df.resample("YE").mean()
269 base_y.index = base_y.index.strftime("%Y")
271 df_spot = self.df.copy()
272 df_spot.index = df_spot.index.strftime("%Y")
274 shape = df_spot.divide(base_y, axis="index")
275 self.shape_df = pd.DataFrame(data=shape["Spot"].tolist(), index=self.date_range, columns=["shape"])
277 def __get_contracts_list(self, contracts_schedules: Dict[str, Dict[str, SimpleSchedule]]) -> Dict[str, Dict[str, EnergyFutureSpecifications]]:
278 contracts = defaultdict(dict)
279 for contract_type, contracts_dict in contracts_schedules.items():
280 for contract_name, schedule in contracts_dict.items():
281 tg = schedule.get_schedule()
282 price = self.df.loc[tg, :].mean().iloc[0]
283 contracts[contract_type][contract_name] = EnergyFutureSpecifications(schedule=schedule, price=price, name=contract_name)
284 return list(itertools.chain(*[list(constracts_spec.values()) for constracts_spec in contracts.values()]))
286 def test_from_shape(self):
287 contracts_schedules = {
288 "base": {
289 "Cal23_Base": BaseSchedule(dt.datetime(2023, 1, 1), dt.datetime(2024, 1, 1)),
290 "Q2/23_Base": BaseSchedule(dt.datetime(2023, 4, 1), dt.datetime(2023, 7, 1)),
291 },
292 "peak": {
293 "Cal23_Peak": PeakSchedule(dt.datetime(2023, 1, 1), dt.datetime(2024, 1, 1)),
294 "Cal24_Peak": PeakSchedule(dt.datetime(2024, 1, 1), dt.datetime(2025, 1, 1)),
295 "Q1/23_Peak": PeakSchedule(dt.datetime(2023, 1, 1), dt.datetime(2023, 4, 1)),
296 "Q2/23_Peak": PeakSchedule(dt.datetime(2023, 4, 1), dt.datetime(2023, 7, 1)),
297 "Q3/23_Peak": PeakSchedule(dt.datetime(2023, 7, 1), dt.datetime(2023, 10, 1)),
298 "Q4/23_Peak": PeakSchedule(dt.datetime(2023, 10, 1), dt.datetime(2024, 1, 1)),
299 "Q2/24_Peak": PeakSchedule(dt.datetime(2024, 4, 1), dt.datetime(2024, 7, 1)),
300 },
301 }
302 contracts_list = self.__get_contracts_list(contracts_schedules=contracts_schedules)
303 pfc_obj = EnergyPriceForwardCurve.from_existing_shape(id=None, refdate=None, pfc_shape=self.shape_df, future_contracts=contracts_list)
305 pfc = pfc_obj.get_pfc()
307 for contract in contracts_list:
308 pfc_price = np.mean(pfc.loc[contract.get_schedule(), :])
309 self.assertAlmostEqual(pfc_price, contract.get_price(), delta=10 ** (-10))
311 def test_from_scratch(self):
312 contracts_schedules = {
313 "base": {
314 "Cal23_Base": SimpleSchedule(dt.datetime(2023, 1, 1), dt.datetime(2024, 1, 1), freq="D"),
315 "Q2/23_Base": BaseSchedule(dt.datetime(2023, 4, 1), dt.datetime(2023, 7, 1)),
316 },
317 "peak": {
318 "Cal23_Peak": PeakSchedule(dt.datetime(2023, 1, 1), dt.datetime(2024, 1, 1)),
319 "Cal24_Peak": PeakSchedule(dt.datetime(2024, 1, 1), dt.datetime(2025, 1, 1)),
320 "Q1/23_Peak": PeakSchedule(dt.datetime(2023, 1, 1), dt.datetime(2023, 4, 1)),
321 "Q2/23_Peak": PeakSchedule(dt.datetime(2023, 4, 1), dt.datetime(2023, 7, 1)),
322 "Q3/23_Peak": PeakSchedule(dt.datetime(2023, 7, 1), dt.datetime(2023, 10, 1)),
323 "Q4/23_Peak": PeakSchedule(dt.datetime(2023, 10, 1), dt.datetime(2024, 1, 1)),
324 "Q2/24_Peak": PeakSchedule(dt.datetime(2024, 4, 1), dt.datetime(2024, 7, 1)),
325 },
326 }
327 contracts_list = self.__get_contracts_list(contracts_schedules=contracts_schedules)
328 normalization_config = {"D": 2, "W": 2, "ME": 1}
329 holiday_calendar = holidays.country_holidays("DE", years=[2024, 2025, 2026])
330 apply_schedule = BaseSchedule(start=dt.datetime(2025, 12, 31), end=dt.datetime(2027, 1, 1))
332 pfc_shaper = CategoricalRegression(
333 spot_prices=self.example_spot_price_data, holiday_calendar=holiday_calendar, normalization_config=normalization_config
334 )
335 # raise ValueError due to different frequencies in the contracts
336 with self.assertRaises(ValueError):
337 pfc_obj = EnergyPriceForwardCurve.from_scratch(
338 id=None, refdate=None, apply_schedule=apply_schedule, pfc_shaper=pfc_shaper, future_contracts=contracts_list
339 )
341 def test_existing_pfc(self):
342 # create shape without DateTimeIndex
343 not_acceptable_shape = pd.DataFrame(data=np.array([1, 2, 3, 4, 5, 6, 7, 8, 9]))
344 with self.assertRaises(TypeError):
345 pfc_obj = EnergyPriceForwardCurve.from_existing_pfc(id=None, refdate=None, pfc=not_acceptable_shape)
347 # create shape which is not a pd.DataFrame
348 not_acceptable_shape = [1, 2, 3, 4, 5, 6, 7, 8, 9]
349 with self.assertRaises(TypeError):
350 pfc_obj = EnergyPriceForwardCurve.from_existing_pfc(id=None, refdate=None, pfc=not_acceptable_shape)
352 with self.assertRaises(ValueError):
353 EnergyPriceForwardCurve(id=None, refdate=None)
356if __name__ == "__main__":
357 unittest.main()