Coverage for rivapy/models/residual_demand_fwd_model.py: 45%

336 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-05 14:27 +0000

1import numpy as np 

2import pandas as pd 

3import datetime as dt 

4import matplotlib.pyplot as plt 

5import abc 

6from typing import Union, Callable, List, Tuple, Dict, Protocol, Set 

7import scipy 

8from scipy.special import comb 

9from rivapy.tools.interfaces import FactoryObject 

10from rivapy.models.factory import create as _create 

11from rivapy.models.ornstein_uhlenbeck import OrnsteinUhlenbeck 

12from rivapy.models.base_model import BaseFwdModel 

13 

14def _logit(x): 

15 return np.log(x/(1-x)) 

16 

17def _inv_logit(x): 

18 return 1.0/(1+np.exp(-x)) 

19 

20class ForwardSimulationResult(abc.ABC): 

21 @abc.abstractmethod 

22 def n_forwards(self)->float: 

23 pass 

24 

25 @abc.abstractmethod 

26 def udls(self)->Set[str]: 

27 pass 

28 

29 def keys(self)->List[str]: 

30 result = set() 

31 for udl in self.udls(): 

32 for i in range(self.n_forwards()): 

33 result.add(BaseFwdModel.get_key(udl, i)) 

34 return result 

35 

36 @abc.abstractmethod 

37 def get(self, key: str)->np.ndarray: 

38 pass 

39 

40class WindPowerForecastModelParameter(FactoryObject): 

41 def __init__(self, n_call_strikes:int = 20, min_strike: float=-5.0, max_strike: float=5.0): 

42 """Parameter class for the wind power forecast model. 

43 

44 Args: 

45 n_call_strikes (int, optional): Number of calls used to approximate the expectation of inverse logit applied to Ornsetin-Uhlenbeck. Defaults to 20. 

46 min_strike (float, optional): Minimal strike used to compute the approximation of inverse logit applied to Ornsetin-Uhlenbeck. Defaults to -5.0. 

47 max_strike (float, optional): Maximal strike used to compute the approximation of inverse logit applied to Ornsetin-Uhlenbeck. Defaults to 5.0. 

48 """ 

49 self.n_call_strikes = n_call_strikes 

50 self.min_strike = min_strike 

51 self.max_strike = max_strike 

52 

53 def _to_dict(self) -> dict: 

54 return {'n_call_strikes': self.n_call_strikes, 

55 'min_strike': self.min_strike, 

56 'max_strike': self.max_strike} 

57 

58class WindPowerForecastModel(BaseFwdModel): 

59 

60 

61 class ForwardSimulationResult(ForwardSimulationResult): 

62 def __init__(self, paths:np.ndarray, 

63 wind_forecast_model, 

64 timegrid, 

65 expiries, 

66 initial_forecasts): 

67 self._paths = paths 

68 self._timegrid = timegrid 

69 self._model = wind_forecast_model 

70 self._result = None 

71 self.expiries = expiries 

72 self.initial_forecasts = initial_forecasts 

73 self._ou_additive_forward_corrections = wind_forecast_model._compute_ou_additive_forward_correction(self.expiries, self.initial_forecasts) 

74 

75 def n_forwards(self)->float: 

76 return len(self.expiries) 

77 

78 def udls(self)->Set[str]: 

79 return self._model.udls() 

80 

81 def get(self, key: str, 

82 forecast_timepoints: List[int]=None)->np.ndarray: 

83 expiry = BaseFwdModel.get_expiry_from_key(key) 

84 return self._get(expiry, forecast_timepoints) 

85 

86 def _get(self, expiry: int, forecast_timepoints: List[int])->np.ndarray: 

87 result = np.empty(self._paths.shape) 

88 for i in range(self._timegrid.shape[0]): 

89 #result[i,:] = self._model.get_forward(self._paths[i,:], self.expiries[expiry]-self._timegrid[i], self._ou_additive_forward_corrections[expiry]) 

90 self._model._compute_expectation_inv_logit(result[i,:], 

91 self._paths[i,:], 

92 self.expiries[expiry]-self._timegrid[i], 

93 self._ou_additive_forward_corrections[expiry]) 

94 if forecast_timepoints is not None: 

95 ftp_prev = 1 

96 for ftp in forecast_timepoints: 

97 for i in range(ftp_prev, ftp): 

98 if abs(self._timegrid[i]-self.expiries[expiry])>1e-6: 

99 result[i,:] = result[i-1,:] 

100 ftp_prev = ftp+1 

101 for i in range(ftp_prev, self._timegrid.shape[0]): 

102 if abs(self._timegrid[i]-self.expiries[expiry])>1e-6: 

103 result[i,:] = result[i-1,:] 

104 return result 

105 

106 def __init__(self, region: str, 

107 speed_of_mean_reversion: float, 

108 volatility: float, 

109 params: WindPowerForecastModelParameter=None 

110 ): 

111 """Simple model to simulate forecasts of wind efficiencies (power production by wind as percentage of total wind capacity) based on the Ornstein-Uhlenbeck process. 

112 

113 The base model used within this model is the Ornstein-Uhlenbeck process and uses internally the class :class:`rivapy.models.OrnsteinUhlenbeck` with a mean level of zero to simulate the forecast. 

114 Here, the forecast of wind efficiency :math:`w_{t,T}` at time :math:`T` is computed as the expected value of the standard logistic function (sigmoid) applied to the Ornstein-Uhlenbeck process conditioned on current simulated value, 

115 i.e. 

116 

117 .. math:: w_{t, T} = \\frac{1}{1+e^{-(X_{t,T}+\\nu(T))}}  

118 

119 where :math:`X_{t,T}:=E[X_T\mid X_t]` with 

120 

121 .. math:: dX = -\\lambda X dt + \\sigma dW_t, X(0) = 0 

122 

123 and :math:`\\nu(T)` is a correction term to ensure that the initial value of :math:`w_{t,T}` is equal to  

124 a given initial forecast :math:`\\bar{w}_{0,T}`. It is computed by  

125 

126 .. math:: \\nu(T) := \log\\frac{\\bar{w}_{0,T}}{(1.0-\\bar{w}_{0,T})}. 

127 

128 The sigmoid function is applied to ensure that the forecast is between 0 and 1 (as percentage of total wind capacity) 

129 

130 Args: 

131 speed_of_mean_reversion (float): The speed of mean reversion of the underlying Ornstein-Uhlenbeck process (:class:`rivapy.models.OrnsteinUhlenbeck`). 

132 volatility (float): The volatility of the underlying Ornstein-Uhlenbeck process (:class:`rivapy.models.OrnsteinUhlenbeck`). 

133 region (str): The name of the respective wind region. 

134 """ 

135 #if len(expiries) != len(initial_forecasts): 

136 # raise Exception('Number of forward expiries does not equal number of initial forecasts. Each forward expiry needs an own forecast.') 

137 self.ou = OrnsteinUhlenbeck(speed_of_mean_reversion, volatility, mean_reversion_level=0.0) 

138 self.region = region 

139 if params is None: 

140 self.params = WindPowerForecastModelParameter() 

141 else: 

142 self.params = params 

143 self.call_strikes, self.call_weights = WindPowerForecastModel._compute_strikes_weights(self.params.n_call_strikes, 

144 self.params.min_strike, 

145 self.params.max_strike) 

146 def _compute_expectation_inv_logit(self, result: np.ndarray, spots: np.ndarray, 

147 ttm: float, strike_offset: float=0.0): 

148 """Compute the expectation of the inverse logit applied to the ou model. 

149 

150 Args: 

151 result (np.ndarray): Array to store the result 

152 spots (np.ndarray): Array of spots 

153 ttm (float): Time to maturity 

154 strike_offset (float, optional): Offset to the strike (needed to fit the forecast at initial time). Defaults to 0.0. 

155 """ 

156 strikes = self.call_strikes 

157 ref_spots = np.linspace(spots.min(), spots.max(), num=self.params.n_call_strikes, endpoint=True) 

158 prices = np.empty((strikes.shape[0], self.params.n_call_strikes,)) 

159 for i in range(strikes.shape[0]): 

160 prices[i,:] = self.ou.compute_call_price(ref_spots, strikes[i]-strike_offset, ttm) 

161 

162 result[:] = self.call_weights[0]*np.interp(spots, ref_spots, prices[0,:]) 

163 for i in range(1, prices.shape[0]): 

164 result += self.call_weights[i]*np.interp(spots, ref_spots, prices[i,:]) 

165 

166 def _compute_ou_additive_forward_correction(self, expiries, initial_forecasts): 

167 # we determine the additive correction term such that the expected value  

168 # of the inverse logit is equal to the initial forecast 

169 def error(i): 

170 def _error(correction): 

171 tmp = 0 

172 for j in range(self.call_strikes.shape[0]): 

173 tmp += self.call_weights[j]*self.ou.compute_call_price(0.0, self.call_strikes[j]-correction, expiries[i]) 

174 return tmp-initial_forecasts[i] 

175 return _error 

176 

177 result = np.empty((len(expiries))) 

178 for i in range(len(expiries)): 

179 result[i] = scipy.optimize.brentq(error(i), -15.0, 15.0, xtol=1e-6) 

180 return result 

181 

182 def _to_dict(self)->dict: 

183 return {'speed_of_mean_reversion': self.ou.speed_of_mean_reversion, 

184 'volatility': self.ou.volatility, 

185 'region': self.region} 

186 

187 @staticmethod 

188 def _compute_strikes_weights(n_strikes: int=20, min_strike: float=-5.0, max_strike: float=5.0): 

189 strikes = np.linspace(start=min_strike, stop=max_strike, num=n_strikes) 

190 f = _inv_logit(strikes) 

191 weights = np.zeros((strikes.shape[0]+1)) 

192 h = strikes[1]-strikes[0] 

193 butterfly_weights = [1, -2, 1]/h 

194 for s in range(1,strikes.shape[0]): 

195 weights[s-1] += f[s]*butterfly_weights[0] 

196 weights[s] += f[s]*butterfly_weights[1] 

197 weights[s+1] += f[s]*butterfly_weights[2] 

198 weights[-2] -= 0.5* butterfly_weights[1]*f[s] 

199 return strikes, weights[:-1] 

200 

201 

202 @staticmethod 

203 def eval_call_functions(strikes, weights, x): 

204 approx = np.copy(weights[0]*np.maximum(x-strikes[0],0.0)) 

205 for i in range(1,weights.shape[0]): 

206 approx += weights[i]*np.maximum(x-strikes[i],0.0) 

207 return approx 

208 

209 def get_forward(self, paths, ttm, ou_additive_forward_correction: float): 

210 expected_ou = self.ou.compute_expected_value(paths, ttm)#+correction 

211 result = _inv_logit(expected_ou + ou_additive_forward_correction) 

212 #expected_ou = self.ou.compute_expected_value(paths[1], ttm)#+correction 

213 #result += (1.0-ttm)*_inv_logit(expected_ou + ou_additive_forward_correction) 

214 return result 

215 

216 def rnd_shape(self, n_sims: int, n_timesteps: int)->tuple: 

217 return (n_timesteps-1, n_sims) 

218 

219 def simulate(self, timegrid, 

220 rnd: np.ndarray, 

221 expiries: List[float], 

222 initial_forecasts: List[float], 

223 startvalue=0.0)->ForwardSimulationResult: 

224 #paths = np.empty((2,timegrid.shape[0], rnd.shape[2])) 

225 paths = self.ou.simulate(timegrid, startvalue, rnd) 

226 #paths[1,:,:] = self.ou.simulate(timegrid, startvalue, rnd[1]) 

227 return WindPowerForecastModel.ForwardSimulationResult(paths, self, timegrid, expiries, initial_forecasts) 

228 

229 def udls(self)->Set[str]: 

230 return set([self.region]) 

231 

232class MultiRegionWindForecastModel(BaseFwdModel): 

233 class ForwardSimulationResult(ForwardSimulationResult): 

234 def __init__(self, model, regions_result): 

235 self._results = regions_result 

236 self._model = model 

237 

238 def n_forwards(self)->float: 

239 for v in self._results.values(): 

240 return v.n_forwards() 

241 

242 def udls(self)->Set[str]: 

243 return self._model.udls() 

244 

245 def get(self, key: str, forecast_timepoints: List[int]=None)->np.ndarray: 

246 udl = BaseFwdModel.get_udl_from_key(key) 

247 if udl == self._model.name: 

248 expiry = BaseFwdModel.get_expiry_from_key(key) 

249 return self._get(expiry, forecast_timepoints) 

250 for v in self._results.values(): 

251 if udl in v.udls(): 

252 return v.get(key, forecast_timepoints) 

253 raise Exception('Cannot find key '+ key) 

254 

255 def _get(self, expiry: int, forecast_timepoints: List[int])->np.ndarray: 

256 result = None 

257 for udl in self.udls(): 

258 if udl == self._model.name: 

259 continue 

260 if result is None: 

261 result = self._model.region_relative_capacity(udl)*np.copy(self.get(BaseFwdModel.get_key(udl, expiry), forecast_timepoints)) 

262 else: 

263 result += self._model.region_relative_capacity(udl)*np.copy(self.get(BaseFwdModel.get_key(udl, expiry), forecast_timepoints)) 

264 return result 

265 

266 class Region(FactoryObject): 

267 def __init__(self, model: WindPowerForecastModel, capacity: float, rnd_weights: List[float]): 

268 self.model = _create(model) 

269 self.capacity = capacity 

270 self.rnd_weights = rnd_weights 

271 

272 def name(self): 

273 return self.model.region 

274 

275 def n_random(self): 

276 return len(self.rnd_weights) 

277 

278 def udls(self)->Set[str]: 

279 return self.model.udls() 

280 

281 def _to_dict(self)->dict: 

282 return {'model': self.model.to_dict(), 'capacity': self.capacity, 'rnd_weights': self.rnd_weights} 

283 

284 def __init__(self, name: str, region_forecast_models: List[Region]): 

285 """Simple model to simulate power forecasts for more then one wind region and a total efficiency over all regions. 

286 

287 This model relates the wind models for the different regions within the simulation by just using a linear sum of normal random variates, i.e. 

288 for a region :math:`r` we have weights :math:`w_{r,i}`, :math:`1\leq i\leq N`, so that within the simulation based on :math:`n` different normal random 

289 variates :math:`X_i` we compute the random variate for the region by 

290 

291 .. math:: X_{r,i} = \\frac{\sum_i w_{r,i} X_i}{\sqrt{\sum_i w_{r,i}^2}}. 

292 

293 Args: 

294 name (str): Name of the overall region. 

295 region_forecast_models (List[Region]): List of regions that will be simulated. 

296 

297 Examples: 

298 >>> wind_onshore = WindPowerForecastModel(region='Onshore', speed_of_mean_reversion=0.1, volatility=4.80) 

299 >>> wind_offshore = WindPowerForecastModel(region='Offshore', speed_of_mean_reversion=0.5, volatility=4.80) 

300 >>> regions = [ MultiRegionWindForecastModel.Region(  

301 wind_onshore, 

302 capacity=1000.0, 

303 rnd_weights=[0.8,0.2] 

304 ), 

305 MultiRegionWindForecastModel.Region(  

306 wind_offshore, 

307 capacity=100.0, 

308 rnd_weights=[0.2,0.8] 

309 ) 

310 ] 

311 >>> wind = MultiRegionWindForecastModel('Wind_Germany', regions) 

312 >>> # after model setup we simulate forecasts 

313 >>> days = 10 

314 >>> timegrid = np.linspace(0.0, days*1.0/365.0, days*24) 

315 >>> forward_expiries = [timegrid[-1] + i/(365.0*24.0) for i in range(4)] 

316 >>> rnd = np.random.normal(size=wind.rnd_shape(n_sims, timegrid.shape[0])) 

317 >>> results = wind.simulate(timegrid, rnd, expiries=forward_expiries,  

318 initial_forecasts={'Onshore': [0.8, 0.7,0.6,0.5], 

319 'Offshore': [0.6, 0.6, 0.5, 0.5]} 

320 ) 

321 """ 

322 self.name = name 

323 if len(region_forecast_models)==0: 

324 raise Exception('Empty list of models is not allowed') 

325 self._region_forecast_models = [ _create(r) for r in region_forecast_models] 

326 n_rnd_ref_model = self._region_forecast_models[0].n_random() 

327 for i in range(1, len(self._region_forecast_models)): 

328 if self._region_forecast_models[i].n_random() != n_rnd_ref_model: 

329 raise Exception('All regions must have the same number of random variables.') 

330 

331 

332 

333 def _to_dict(self)->dict: 

334 return {'name': self.name, 

335 'region_forecast_models':[v.to_dict() for v in self._region_forecast_models] 

336 } 

337 

338 def rnd_shape(self, n_sims: int, n_timesteps: int)->tuple: 

339 return (self._region_forecast_models[0].rnd_weights, n_timesteps-1, n_sims) 

340 

341 def total_capacity(self): 

342 result = 0.0 

343 for r in self._region_forecast_models: 

344 result += r.capacity 

345 return result 

346 

347 def region_relative_capacity(self, region: str): 

348 for r in self._region_forecast_models: 

349 if r.name() == region: 

350 return r.capacity/self.total_capacity() 

351 raise Exception('Model does not contain a region with name ' + region) 

352 

353 def region_names(self)->List[str]: 

354 return [r.name() for r in self._region_forecast_models] 

355 

356 def simulate(self, timegrid, rnd: np.ndarray, expiries: List[float], 

357 

358 initial_forecasts: Dict[str,List[float]], startvalue=0.0): 

359 results = {} 

360 for region in self._region_forecast_models: 

361 rnd_ = region.rnd_weights[0]*rnd[0,:,:] 

362 for i in range(1, region.n_random()): 

363 rnd_ += region.rnd_weights[i]*rnd[i,:,:] 

364 results[region.name()] = region.model.simulate(timegrid, rnd_, expiries, initial_forecasts[region.name()], startvalue) 

365 return MultiRegionWindForecastModel.ForwardSimulationResult(self, results) 

366 

367 def rnd_shape(self, n_sims: int, n_timesteps: int)->tuple: 

368 return (len(self._region_forecast_models), n_timesteps-1, n_sims) 

369 

370 def udls(self)->Set[str]: 

371 result = set([self.name]) 

372 for v in self._region_forecast_models: 

373 result.update(v.udls()) 

374 return result 

375 

376class LinearDemandForwardModel(BaseFwdModel): 

377 class ForwardSimulationResult(ForwardSimulationResult): 

378 def __init__(self, model, highest_price, wind_results, additive_correction): 

379 self._model = model 

380 self._highest_price = highest_price 

381 self._wind = wind_results 

382 self._additive_correction = additive_correction 

383 

384 def n_forwards(self)->float: 

385 return self._wind.n_forwards() 

386 

387 def udls(self)->Set[str]: 

388 return self._model.udls() 

389 

390 def get(self, key: str, forecast_timepoints: List[int]=None)->np.ndarray: 

391 udl = BaseFwdModel.get_udl_from_key(key) 

392 if udl == self._model.power_name: 

393 expiry = BaseFwdModel.get_expiry_from_key(key) 

394 return self._get(expiry, forecast_timepoints) 

395 else: 

396 return self._wind.get(key, forecast_timepoints) 

397 

398 def _get(self, expiry: int, forecast_timepoints: List[int])->np.ndarray: 

399 total_produced = self._wind.get(BaseFwdModel.get_key(self._wind._model.name, expiry), forecast_timepoints) 

400 power_fwd = np.empty((total_produced.shape[0], total_produced.shape[1])) 

401 for i in range(total_produced.shape[0]): 

402 power_fwd[i,:] = (1.0-total_produced[i,:] )*(self._highest_price[i,:]+self._additive_correction[expiry]) 

403 return power_fwd 

404 

405 def __init__(self, wind_power_forecast: MultiRegionWindForecastModel, 

406 x_volatility: float, 

407 x_mean_reversion_speed: float, 

408 power_name:str = None): 

409 self.wind_power_forecast = _create(wind_power_forecast) 

410 self.highest_price_ou_model: OrnsteinUhlenbeck = OrnsteinUhlenbeck(x_mean_reversion_speed, x_volatility, 0.0) 

411 if power_name is not None: 

412 self.power_name = power_name 

413 else: 

414 self.power_name = 'POWER' 

415 #self.region_to_capacity = region_to_capacity 

416 

417 def _to_dict(self)->dict: 

418 return {'wind_power_forecast': self.wind_power_forecast.to_dict(), 

419 'x_volatility': self.highest_price_ou_model.volatility, 

420 'x_mean_reversion_speed': self.highest_price_ou_model.speed_of_mean_reversion, 

421 'power_name': self.power_name, 

422 } 

423 

424 def rnd_shape(self, n_sims: int, n_timesteps: int)->tuple: 

425 rnd_shape = self.wind_power_forecast.rnd_shape(n_sims, n_timesteps) 

426 if len(rnd_shape) == 3: 

427 return (rnd_shape[0]+1, rnd_shape[1], rnd_shape[2]) 

428 return (2, rnd_shape[0], rnd_shape[1]) 

429 

430 def get_technology(self)->str: 

431 """Return name of the technology modeled. 

432 

433 Returns: 

434 str: Name of instrument. 

435 """ 

436 return self.wind_power_forecast.region 

437 

438 def _compute_additive_correction(self, expiries: List[float], 

439 power_fwd_prices: List[float], 

440 initial_forecasts: Dict[str,List[float]], startvalue=0.0 ): 

441 result = np.empty((len(expiries))) 

442 for i in range(len(expiries)): 

443 tmp = 0 

444 for k,v in initial_forecasts.items(): 

445 tmp += v[i]*self.wind_power_forecast.region_relative_capacity(k) 

446 if abs(1.0-tmp) < 1e-5: 

447 raise Exception('Initial forecasts sum to 1.0. Cannot compute additive correction.') 

448 result[i] = power_fwd_prices[i]/(1.0-tmp) - self.highest_price_ou_model.compute_expected_value(startvalue, expiries[i]) 

449 return result 

450 

451 def simulate(self, timegrid: np.ndarray, 

452 rnd: np.ndarray, 

453 expiries: List[float], 

454 power_fwd_prices: List[float], 

455 initial_forecasts: Dict[str, List[float]]): 

456 additive_correction = self._compute_additive_correction(expiries, power_fwd_prices, initial_forecasts, startvalue=0.0) 

457 highest_prices = self.highest_price_ou_model.simulate(timegrid, 0.0, rnd[0,:]) 

458 simulated_wind = self.wind_power_forecast.simulate(timegrid, rnd[1:,:], expiries, initial_forecasts) 

459 return LinearDemandForwardModel.ForwardSimulationResult(self, highest_prices, simulated_wind, additive_correction) 

460 

461 def udls(self)->Set[str]: 

462 result = self.wind_power_forecast.udls() 

463 result.add(self.power_name) 

464 return result 

465 

466 

467 

468 

469class ResidualDemandForwardModel(BaseFwdModel): 

470 class ForwardSimulationResult(ForwardSimulationResult): 

471 def __init__(self, model, highest_price, wind_results): 

472 self._model = model 

473 self._highest_price = highest_price 

474 self._wind = wind_results 

475 

476 def n_forwards(self)->float: 

477 return self._wind.n_forwards() 

478 

479 def udls(self)->Set[str]: 

480 return self._model.udls() 

481 

482 def get(self, key: str, forecast_timepoints: List[int])->np.ndarray: 

483 udl = BaseFwdModel.get_udl_from_key(key) 

484 if udl == self._model.power_name: 

485 expiry = BaseFwdModel.get_expiry_from_key(key) 

486 return self._get(expiry, forecast_timepoints) 

487 else: 

488 return self._wind.get(key, forecast_timepoints) 

489 

490 def _get(self, expiry: int, forecast_timepoints: List[int])->np.ndarray: 

491 total_produced = self._wind.get(BaseFwdModel.get_key(self._wind._model.name, expiry), forecast_timepoints) 

492 power_fwd = np.empty((total_produced.shape[0], total_produced.shape[1])) 

493 for i in range(total_produced.shape[0]): 

494 power_fwd[i,:] = self._model.supply_curve(1.0-total_produced[i,:] )*self._highest_price[i,:] 

495 return power_fwd 

496 

497 def __init__(self, wind_power_forecast, 

498 highest_price_ou_model, 

499 supply_curve: Callable[[float], float], 

500 max_price: float, 

501 power_name:str = None): 

502 #print(wind_power_forecast) 

503 self.wind_power_forecast = _create(wind_power_forecast) 

504 self.highest_price_ou_model = _create(highest_price_ou_model) 

505 self.supply_curve = _create(supply_curve) 

506 self.max_price = max_price 

507 if power_name is not None: 

508 self.power_name = power_name 

509 else: 

510 self.power_name = 'POWER' 

511 #self.region_to_capacity = region_to_capacity 

512 

513 def _to_dict(self)->dict: 

514 return {'wind_power_forecast': self.wind_power_forecast.to_dict(), 

515 'supply_curve': self.supply_curve.to_dict(), 

516 'highest_price_ou_model': self.highest_price_ou_model.to_dict(), 

517 'max_price': self.max_price, 

518 'power_name': self.power_name, 

519 #'region_to_capacity': self.region_to_capacity 

520 } 

521 

522 def rnd_shape(self, n_sims: int, n_timesteps: int)->tuple: 

523 rnd_shape = self.wind_power_forecast.rnd_shape(n_sims, n_timesteps) 

524 if len(rnd_shape) == 3: 

525 return (rnd_shape[0]+1, rnd_shape[1], rnd_shape[2]) 

526 return (2, rnd_shape[0], rnd_shape[1]) 

527 

528 def get_technology(self)->str: 

529 """Return name of the technology modeled. 

530 

531 Returns: 

532 str: Name of instrument. 

533 """ 

534 return self.wind_power_forecast.region 

535 

536 def simulate(self, timegrid: np.ndarray, 

537 rnd: np.ndarray, 

538 expiries: List[float], 

539 initial_forecasts: Dict[str, List[float]]): 

540 highest_prices = self.highest_price_ou_model.simulate(timegrid, 1.0, rnd[0,:])*self.max_price 

541 simulated_wind = self.wind_power_forecast.simulate(timegrid, rnd[1:,:], expiries, initial_forecasts) 

542 return ResidualDemandForwardModel.ForwardSimulationResult(self, highest_prices, simulated_wind) 

543 

544 def udls(self)->Set[str]: 

545 result = self.wind_power_forecast.udls() 

546 result.add(self.power_name) 

547 return result 

548 

549 

550 

551if __name__=='__main__': 

552 params = WindPowerForecastModelParameter(n_call_strikes=40, min_strike=-7.0, max_strike=7.0) 

553 # setup  

554 wind_region_model = {} 

555 vols = [1.0] 

556 mean_reversion_speed = [0.5] 

557 capacities = [10_000.0] 

558 rnd_weights = [ [1.0] 

559 ] 

560 np.random.seed(42) 

561 regions = [] 

562 for i in range(len(vols)): 

563 model = WindPowerForecastModel(region='Region_' + str(i), 

564 speed_of_mean_reversion=mean_reversion_speed[i], 

565 volatility=vols[i], params=params) 

566 regions.append(MultiRegionWindForecastModel.Region( 

567 model, 

568 capacity=capacities[i], 

569 rnd_weights=rnd_weights[i] 

570 ) ) 

571 wind = MultiRegionWindForecastModel('Wind_Germany', regions) 

572 highest_price = OrnsteinUhlenbeck(speed_of_mean_reversion=1.0, volatility=0.5, mean_reversion_level=0.0) 

573 model = LinearDemandForwardModel(wind_power_forecast=wind, 

574 highest_price_ou_model= highest_price, 

575 power_name= 'Power_Germany') 

576 timegrid = np.linspace(0.0,1.0, 365) 

577 np.random.seed(42) 

578 rnd = np.random.normal(size=model.rnd_shape(10_000, timegrid.shape[0])) 

579 results = model.simulate(timegrid, rnd, expiries=[1.0], 

580 power_fwd_prices=[100.0], 

581 initial_forecasts={'Region_0': [0.8]}) 

582 results.get('Power_Germany_FWD0')