File size: 2,750 Bytes
5841846
 
 
 
 
 
 
94fb523
5841846
 
 
 
94fb523
 
 
 
 
 
5841846
 
94fb523
 
 
 
 
 
5841846
94fb523
 
5841846
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import numpy as np
import pandas as pd
from scipy.stats import t

def calculate_garch_volatility(residuals: pd.Series, omega: float, alpha: float, beta: float, initial_vol: float) -> pd.Series:
    """
    Recursively calculates GARCH conditional volatility on the full series of residuals.
    Handles leading NaNs by starting the recursion at the first valid index.
    
    Formula:
        sigma^2_t = omega + alpha * e^2_{t-1} + beta * sigma^2_{t-1}
    """
    first_valid = residuals.first_valid_index()
    if first_valid is None:
        return pd.Series(np.nan, index=residuals.index)
        
    valid_res = residuals.loc[first_valid:]
    cond_var = np.zeros(len(valid_res))
    cond_var[0] = initial_vol ** 2
    
    for t_idx in range(1, len(valid_res)):
        prev_res = valid_res.iloc[t_idx - 1]
        if np.isnan(prev_res):
            cond_var[t_idx] = cond_var[t_idx - 1]
        else:
            cond_var[t_idx] = omega + alpha * (prev_res ** 2) + beta * cond_var[t_idx - 1]
        
    vol_series = pd.Series(np.sqrt(cond_var), index=valid_res.index)
    return vol_series.reindex(residuals.index)


def simulate_pvar(day_mean_prices: pd.Series, day_vols: pd.Series, nu: float, 
                  n_simulations: int = 10000, confidence_level: float = 0.95):
    """
    Runs a Monte Carlo path simulation using the Student-t distribution 
    parameterized by GARCH volatility and SARIMA expected price mean.
    
    Returns:
        simulated_paths: (24, N_SIMULATIONS) array
        var_vals: (24,) array containing Value-at-Risk limits for each hour
    """
    n_hours = len(day_mean_prices)
    simulated_paths = np.zeros((n_hours, n_simulations))
    var_vals = np.zeros(n_hours)
    
    # Calculate the target percentile (e.g. 95% confidence -> 5th percentile)
    percentile = 100 * (1.0 - confidence_level)
    
    np.random.seed(42)
    for i in range(n_hours):
        mu_t = day_mean_prices.iloc[i]
        sigma_t = day_vols.iloc[i]
        
        # Draw from Student-t
        draws = t.rvs(df=nu, loc=mu_t, scale=sigma_t, size=n_simulations)
        simulated_paths[i, :] = draws
        var_vals[i] = np.percentile(draws, percentile)
        
    return simulated_paths, var_vals


def get_prosumer_action(var_value: float, mean_value: float) -> int:
    """
    Prosumer Traffic Light control logic:
        2 (Green): Inject (Value-at-Risk is positive, risk of loss is 0)
        1 (Yellow): Buffer (Expected price positive, but Value-at-Risk is negative; buffer energy)
        0 (Red): Curtail (Both expected price and Value-at-Risk are negative; halt injects)
    """
    if var_value > 0:
        return 2
    elif mean_value > 0 and var_value <= 0:
        return 1
    else:
        return 0