Add FastAPI backend for energy trading system
Implements FastAPI backend with ML model support for energy trading, including price prediction models and RL-based battery trading policy. Features dashboard, trading, backtest, and settings API routes with WebSocket support for real-time updates.
This commit is contained in:
3
backend/app/ml/price_prediction/__init__.py
Normal file
3
backend/app/ml/price_prediction/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from app.ml.price_prediction import PricePredictor, PricePredictionTrainer
|
||||
|
||||
__all__ = ["PricePredictor", "PricePredictionTrainer"]
|
||||
52
backend/app/ml/price_prediction/model.py
Normal file
52
backend/app/ml/price_prediction/model.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import pickle
|
||||
from typing import Optional
|
||||
import xgboost as xgb
|
||||
import numpy as np
|
||||
|
||||
|
||||
class PricePredictionModel:
|
||||
def __init__(self, horizon: int, model_id: Optional[str] = None):
|
||||
self.horizon = horizon
|
||||
self.model_id = model_id or f"price_prediction_{horizon}m"
|
||||
self.model: Optional[xgb.XGBRegressor] = None
|
||||
self.feature_names = []
|
||||
|
||||
def fit(self, X, y):
|
||||
self.model = xgb.XGBRegressor(
|
||||
n_estimators=200,
|
||||
max_depth=6,
|
||||
learning_rate=0.1,
|
||||
subsample=0.8,
|
||||
colsample_bytree=0.8,
|
||||
random_state=42,
|
||||
)
|
||||
|
||||
if isinstance(X, np.ndarray):
|
||||
self.feature_names = [f"feature_{i}" for i in range(X.shape[1])]
|
||||
else:
|
||||
self.feature_names = list(X.columns)
|
||||
|
||||
self.model.fit(X, y)
|
||||
|
||||
def predict(self, X):
|
||||
if self.model is None:
|
||||
raise ValueError("Model not trained")
|
||||
return self.model.predict(X)
|
||||
|
||||
def save(self, filepath: str):
|
||||
with open(filepath, "wb") as f:
|
||||
pickle.dump(self, f)
|
||||
|
||||
@classmethod
|
||||
def load(cls, filepath: str):
|
||||
with open(filepath, "rb") as f:
|
||||
return pickle.load(f)
|
||||
|
||||
@property
|
||||
def feature_importances_(self):
|
||||
if self.model is None:
|
||||
raise ValueError("Model not trained")
|
||||
return self.model.feature_importances_
|
||||
|
||||
|
||||
__all__ = ["PricePredictionModel"]
|
||||
86
backend/app/ml/price_prediction/predictor.py
Normal file
86
backend/app/ml/price_prediction/predictor.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from typing import Dict, Optional
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from app.ml.price_prediction.model import PricePredictionModel
|
||||
from app.ml.price_prediction.trainer import PricePredictionTrainer
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PricePredictor:
|
||||
def __init__(self, models_dir: str = "models/price_prediction"):
|
||||
self.models_dir = models_dir
|
||||
self.models: Dict[int, PricePredictionModel] = {}
|
||||
self._load_models()
|
||||
|
||||
def _load_models(self):
|
||||
self.models = PricePredictionTrainer.load_models(self.models_dir)
|
||||
logger.info(f"Loaded {len(self.models)} prediction models")
|
||||
|
||||
def predict(
|
||||
self, current_data: pd.DataFrame, horizon: int = 15, region: Optional[str] = None
|
||||
) -> float:
|
||||
if horizon not in self.models:
|
||||
raise ValueError(f"No model available for horizon {horizon}")
|
||||
|
||||
model = self.models[horizon]
|
||||
|
||||
from app.ml.features import build_price_features
|
||||
|
||||
df_features = build_price_features(current_data)
|
||||
|
||||
feature_cols = [col for col in df_features.columns if col not in ["timestamp", "region", "day_ahead_price", "real_time_price"]]
|
||||
|
||||
if region and "region" in df_features.columns:
|
||||
df_features = df_features[df_features["region"] == region]
|
||||
|
||||
latest_row = df_features.iloc[-1:][feature_cols]
|
||||
|
||||
prediction = model.predict(latest_row.values)
|
||||
|
||||
return float(prediction[0])
|
||||
|
||||
def predict_all_horizons(self, current_data: pd.DataFrame, region: Optional[str] = None) -> Dict[int, float]:
|
||||
predictions = {}
|
||||
|
||||
for horizon in sorted(self.models.keys()):
|
||||
try:
|
||||
pred = self.predict(current_data, horizon, region)
|
||||
predictions[horizon] = pred
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to predict for horizon {horizon}: {e}")
|
||||
predictions[horizon] = None
|
||||
|
||||
return predictions
|
||||
|
||||
def predict_with_confidence(
|
||||
self, current_data: pd.DataFrame, horizon: int = 15, region: Optional[str] = None
|
||||
) -> Dict:
|
||||
prediction = self.predict(current_data, horizon, region)
|
||||
|
||||
return {
|
||||
"prediction": prediction,
|
||||
"confidence_lower": prediction * 0.95,
|
||||
"confidence_upper": prediction * 1.05,
|
||||
"horizon": horizon,
|
||||
}
|
||||
|
||||
def get_feature_importance(self, horizon: int) -> pd.DataFrame:
|
||||
if horizon not in self.models:
|
||||
raise ValueError(f"No model available for horizon {horizon}")
|
||||
|
||||
model = self.models[horizon]
|
||||
|
||||
importances = model.feature_importances_
|
||||
feature_names = model.feature_names
|
||||
|
||||
df = pd.DataFrame({
|
||||
"feature": feature_names,
|
||||
"importance": importances,
|
||||
}).sort_values("importance", ascending=False)
|
||||
|
||||
return df
|
||||
|
||||
|
||||
__all__ = ["PricePredictor"]
|
||||
142
backend/app/ml/price_prediction/trainer.py
Normal file
142
backend/app/ml/price_prediction/trainer.py
Normal file
@@ -0,0 +1,142 @@
|
||||
from typing import List, Dict, Tuple, Optional
|
||||
from pathlib import Path
|
||||
import pandas as pd
|
||||
from app.ml.price_prediction.model import PricePredictionModel
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PricePredictionTrainer:
|
||||
def __init__(self, config=None):
|
||||
self.config = config
|
||||
self.data: Optional[pd.DataFrame] = None
|
||||
self.models: Dict[int, PricePredictionModel] = {}
|
||||
|
||||
def load_data(self, data_path: Optional[str] = None) -> pd.DataFrame:
|
||||
if data_path is None:
|
||||
data_path = "~/energy-test-data/data/processed"
|
||||
|
||||
path = Path(data_path).expanduser()
|
||||
dfs = []
|
||||
|
||||
for region in ["FR", "BE", "DE", "NL", "UK"]:
|
||||
file_path = path / f"{region.lower()}_processed.parquet"
|
||||
if file_path.exists():
|
||||
df = pd.read_parquet(file_path)
|
||||
df["region"] = region
|
||||
dfs.append(df)
|
||||
|
||||
if dfs:
|
||||
self.data = pd.concat(dfs, ignore_index=True)
|
||||
logger.info(f"Loaded data: {len(self.data)} rows")
|
||||
|
||||
return self.data
|
||||
|
||||
def prepare_data(self, df: pd.DataFrame) -> Tuple[pd.DataFrame, List[str]]:
|
||||
from app.ml.features import build_price_features
|
||||
|
||||
df_features = build_price_features(df)
|
||||
|
||||
df_features = df_features.dropna()
|
||||
|
||||
feature_cols = [col for col in df_features.columns if col not in ["timestamp", "region", "day_ahead_price", "real_time_price"]]
|
||||
|
||||
return df_features, feature_cols
|
||||
|
||||
def train_for_horizon(
|
||||
self, df_features: pd.DataFrame, feature_cols: List[str], horizon: int
|
||||
) -> Dict:
|
||||
logger.info(f"Training model for {horizon} minute horizon")
|
||||
|
||||
df_features = df_features.sort_values("timestamp")
|
||||
|
||||
n_total = len(df_features)
|
||||
n_train = int(n_total * 0.70)
|
||||
n_val = int(n_total * 0.85)
|
||||
|
||||
train_data = df_features.iloc[:n_train]
|
||||
val_data = df_features.iloc[n_train:n_val]
|
||||
|
||||
X_train = train_data[feature_cols]
|
||||
y_train = train_data["real_time_price"].shift(-horizon).dropna()
|
||||
X_train = X_train.loc[y_train.index]
|
||||
|
||||
X_val = val_data[feature_cols]
|
||||
y_val = val_data["real_time_price"].shift(-horizon).dropna()
|
||||
X_val = X_val.loc[y_val.index]
|
||||
|
||||
model = PricePredictionModel(horizon=horizon)
|
||||
model.fit(X_train, y_train)
|
||||
|
||||
val_preds = model.predict(X_val)
|
||||
|
||||
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
|
||||
|
||||
mae = mean_absolute_error(y_val, val_preds)
|
||||
rmse = mean_squared_error(y_val, val_preds, squared=False)
|
||||
r2 = r2_score(y_val, val_preds)
|
||||
|
||||
self.models[horizon] = model
|
||||
|
||||
results = {
|
||||
"horizon": horizon,
|
||||
"mae": mae,
|
||||
"rmse": rmse,
|
||||
"r2": r2,
|
||||
"n_train": len(X_train),
|
||||
"n_val": len(X_val),
|
||||
}
|
||||
|
||||
logger.info(f"Training complete for {horizon}m: MAE={mae:.2f}, RMSE={rmse:.2f}, R2={r2:.3f}")
|
||||
|
||||
return results
|
||||
|
||||
def train_all(self, horizons: Optional[List[int]] = None) -> Dict:
|
||||
if horizons is None:
|
||||
horizons = [1, 5, 15, 60]
|
||||
|
||||
if self.data is None:
|
||||
self.load_data()
|
||||
|
||||
df_features, feature_cols = self.prepare_data(self.data)
|
||||
|
||||
all_results = {}
|
||||
for horizon in horizons:
|
||||
try:
|
||||
result = self.train_for_horizon(df_features, feature_cols, horizon)
|
||||
all_results[horizon] = result
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to train for horizon {horizon}: {e}")
|
||||
all_results[horizon] = {"error": str(e)}
|
||||
|
||||
return all_results
|
||||
|
||||
def save_models(self, output_dir: str = "models/price_prediction") -> None:
|
||||
output_path = Path(output_dir)
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for horizon, model in self.models.items():
|
||||
filepath = output_path / f"model_{horizon}min.pkl"
|
||||
model.save(filepath)
|
||||
logger.info(f"Saved model for {horizon}m to {filepath}")
|
||||
|
||||
@classmethod
|
||||
def load_models(cls, models_dir: str = "models/price_prediction", horizons: Optional[List[int]] = None) -> Dict[int, PricePredictionModel]:
|
||||
models = {}
|
||||
path = Path(models_dir)
|
||||
|
||||
if horizons is None:
|
||||
horizons = [1, 5, 15, 60]
|
||||
|
||||
for horizon in horizons:
|
||||
filepath = path / f"model_{horizon}min.pkl"
|
||||
if filepath.exists():
|
||||
model = PricePredictionModel.load(filepath)
|
||||
models[horizon] = model
|
||||
logger.info(f"Loaded model for {horizon}m")
|
||||
|
||||
return models
|
||||
|
||||
|
||||
__all__ = ["PricePredictionTrainer"]
|
||||
Reference in New Issue
Block a user