Benny-Tang commited on
Commit
16c8b7f
·
verified ·
1 Parent(s): a1c1a89

Create models/forecaster.py

Browse files
Files changed (1) hide show
  1. models/forecaster.py +126 -0
models/forecaster.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ python3 << 'PYEOF'
2
+ code = '''
3
+ import numpy as np, pandas as pd, warnings
4
+ from datetime import datetime, timedelta
5
+ warnings.filterwarnings("ignore")
6
+ try:
7
+ import xgboost as xgb
8
+ from statsmodels.tsa.arima.model import ARIMA
9
+ from sklearn.preprocessing import StandardScaler
10
+ HAS_MODELS = True
11
+ except ImportError:
12
+ HAS_MODELS = False
13
+
14
+ def _gen_history(n=120, base=82.0):
15
+ np.random.seed(42)
16
+ dates = [datetime.today() - timedelta(days=n-i) for i in range(n)]
17
+ prices = [base]
18
+ for _ in range(n-1):
19
+ shock = np.random.normal(0, 1.2)
20
+ drift = 0.05*(base - prices[-1])
21
+ prices.append(max(prices[-1]+drift+shock, 40))
22
+ return pd.DataFrame({"price": prices}, index=pd.to_datetime(dates))
23
+
24
+ def _features(df):
25
+ df = df.copy()
26
+ for lag in [1,3,7,14]: df[f"lag_{lag}"] = df["price"].shift(lag)
27
+ df["rm7"] = df["price"].rolling(7).mean()
28
+ df["rs7"] = df["price"].rolling(7).std()
29
+ df["rm14"] = df["price"].rolling(14).mean()
30
+ df["pct3"] = df["price"].pct_change(3)
31
+ df["dow"] = df.index.dayofweek
32
+ return df.dropna()
33
+
34
+ FEAT = ["lag_1","lag_3","lag_7","lag_14","rm7","rs7","rm14","pct3","dow"]
35
+
36
+ class AegisForecaster:
37
+ def __init__(self):
38
+ self.arima = self.xgb = self.scaler = None
39
+ self.hist = None
40
+ self.fitted = False
41
+
42
+ def fit(self, df=None):
43
+ self.hist = df if df is not None else _gen_history()
44
+ if not HAS_MODELS:
45
+ self.fitted = True
46
+ return self
47
+ try:
48
+ self.arima = ARIMA(self.hist["price"], order=(2,1,2)).fit()
49
+ except:
50
+ self.arima = None
51
+ fd = _features(self.hist)
52
+ X = fd[FEAT].values
53
+ y = fd["price"].values
54
+ self.scaler = StandardScaler()
55
+ Xs = self.scaler.fit_transform(X)
56
+ self.xgb = xgb.XGBRegressor(
57
+ n_estimators=200, max_depth=4,
58
+ learning_rate=0.05, subsample=0.8,
59
+ colsample_bytree=0.8, random_state=42,
60
+ verbosity=0
61
+ ).fit(Xs, y)
62
+ self.fitted = True
63
+ print("Forecaster ready")
64
+ return self
65
+
66
+ def forecast(self, horizon_days=14, crisis_shock=0.0, disruption_factor=0.0):
67
+ if not self.fitted:
68
+ self.fit()
69
+ base = float(self.hist["price"].iloc[-1])
70
+ shocked = base*(1+crisis_shock/100)
71
+ dates = [datetime.today()+timedelta(days=i+1) for i in range(horizon_days)]
72
+ if self.arima:
73
+ arima_p = list(self.arima.forecast(steps=horizon_days))
74
+ else:
75
+ arima_p = [base+np.random.normal(0,1)*(i+1)**0.5 for i in range(horizon_days)]
76
+ rw = list(self.hist["price"].tail(14).values)
77
+ if crisis_shock > 0:
78
+ rw[-1] = shocked
79
+ xgb_p = []
80
+ for step in range(horizon_days):
81
+ pw = rw[-14:]
82
+ f = np.array([[pw[-1],pw[-3],pw[-7],pw[0],
83
+ np.mean(pw[-7:]),np.std(pw[-7:]),np.mean(pw),
84
+ (pw[-1]-pw[-4])/pw[-4] if pw[-4]!=0 else 0,
85
+ (datetime.today().weekday()+step+1)%7]])
86
+ pred = float(self.xgb.predict(self.scaler.transform(f))[0])
87
+ pred *= (1+disruption_factor*0.8*(1-step/horizon_days))
88
+ xgb_p.append(pred)
89
+ rw.append(pred)
90
+ w = 0.7 if crisis_shock > 0 else 0.4
91
+ prices = [round(w*xgb_p[i]+(1-w)*arima_p[i],2) for i in range(horizon_days)]
92
+ std = float(self.hist["price"].pct_change().std())*base
93
+ lower = [round(p-1.96*std*((i+1)**0.4),2) for i,p in enumerate(prices)]
94
+ upper = [round(p+1.96*std*((i+1)**0.4),2) for i,p in enumerate(prices)]
95
+ fp = prices[-1]
96
+ pct = round((fp-base)/base*100,1)
97
+ return {
98
+ "base_price": round(base,2),
99
+ "shocked_price": round(shocked,2),
100
+ "horizon_days": horizon_days,
101
+ "forecast": [{"date":d.strftime("%Y-%m-%d"),"price":p,"lower":l,"upper":u}
102
+ for d,p,l,u in zip(dates,prices,lower,upper)],
103
+ "summary": {
104
+ "final_price": fp, "pct_change": pct,
105
+ "peak_price": round(max(prices),2),
106
+ "peak_day": prices.index(max(prices))+1,
107
+ "risk_score": min(100,round(abs(pct)*1.5+disruption_factor*40+(crisis_shock/100)*30,1)),
108
+ "delay_prob": min(99,round(disruption_factor*65+(pct/100)*20,1)),
109
+ "cost_impact": round(pct*0.35+disruption_factor*18,1),
110
+ },
111
+ "model": "ARIMA+XGBoost hybrid",
112
+ }
113
+
114
+ _fc = None
115
+ def get_forecaster():
116
+ global _fc
117
+ if _fc is None:
118
+ _fc = AegisForecaster().fit()
119
+ return _fc
120
+ '''
121
+ with open("/opt/aegis/models/forecaster.py","w") as f:
122
+ f.write(code)
123
+ print("forecaster.py written OK")
124
+ PYEOF
125
+
126
+ ✅ Should print: forecaster.py written OK