|
|
from fastapi import FastAPI |
|
|
from pydantic import BaseModel |
|
|
import numpy as np |
|
|
import pandas as pd |
|
|
import tensorflow as tf |
|
|
from tensorflow.keras.models import load_model |
|
|
import json |
|
|
import os |
|
|
|
|
|
app = FastAPI( |
|
|
title="Forex LSTM Prediction API", |
|
|
description="Prediksi harga EUR/USD H+1 dengan LSTM menggunakan scaler harian", |
|
|
version="2.0" |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
MODEL_PATH = "lstm_model.h5" |
|
|
PARAMS_PATH = "best_params.json" |
|
|
SCALER_FILE = "scaler_config.json" |
|
|
|
|
|
print("π₯ Loading LSTM model...") |
|
|
model = load_model(MODEL_PATH, compile=False) |
|
|
|
|
|
print("π₯ Loading best parameters...") |
|
|
with open(PARAMS_PATH, "r") as f: |
|
|
best_params = json.load(f) |
|
|
|
|
|
LOOKBACK = best_params.get("lookback", 7) |
|
|
FEATURE_ORDER = best_params.get("features", [ |
|
|
"mood_score", "t_pos", "t_neg", "c_pos", "c_neg", |
|
|
"norm_ema20", "norm_ema50", "norm_close" |
|
|
]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_scaler_config(): |
|
|
if not os.path.exists(SCALER_FILE): |
|
|
print("β οΈ Scaler config not found, using default values.") |
|
|
return {"CLOSE_MIN": 1.05, "CLOSE_MAX": 1.15} |
|
|
with open(SCALER_FILE, "r") as f: |
|
|
return json.load(f) |
|
|
|
|
|
scaler_cfg = load_scaler_config() |
|
|
CLOSE_MIN = scaler_cfg["CLOSE_MIN"] |
|
|
CLOSE_MAX = scaler_cfg["CLOSE_MAX"] |
|
|
|
|
|
print(f"β
Scaler range loaded: {CLOSE_MIN:.5f} - {CLOSE_MAX:.5f}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LSTMInput(BaseModel): |
|
|
data: list |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def prepare_input(data): |
|
|
df = pd.DataFrame(data) |
|
|
missing_cols = [f for f in FEATURE_ORDER if f not in df.columns] |
|
|
if missing_cols: |
|
|
raise ValueError(f"Missing columns: {missing_cols}") |
|
|
|
|
|
X = df[FEATURE_ORDER].values[-LOOKBACK:] |
|
|
if X.shape[0] < LOOKBACK: |
|
|
raise ValueError(f"Need at least {LOOKBACK} timesteps, got {X.shape[0]}") |
|
|
X = np.expand_dims(X, axis=0) |
|
|
return X, df |
|
|
|
|
|
|
|
|
def inverse_scale(norm_value): |
|
|
"""Denormalisasi nilai close dari [0,1] ke skala asli""" |
|
|
return (norm_value * (CLOSE_MAX - CLOSE_MIN)) + CLOSE_MIN |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/predict") |
|
|
def predict_price(input_data: LSTMInput): |
|
|
try: |
|
|
X, df = prepare_input(input_data.data) |
|
|
pred_norm = model.predict(X)[0][0] |
|
|
pred_close = inverse_scale(pred_norm) |
|
|
|
|
|
last_date = pd.to_datetime(df["date"].iloc[-1]) |
|
|
next_date = (last_date + pd.Timedelta(days=1)).strftime("%Y-%m-%d") |
|
|
|
|
|
result = { |
|
|
"next_date": next_date, |
|
|
"predicted_norm_close": float(pred_norm), |
|
|
"predicted_close": float(pred_close), |
|
|
"scaler_used": {"min": CLOSE_MIN, "max": CLOSE_MAX}, |
|
|
"features_used": FEATURE_ORDER |
|
|
} |
|
|
return {"status": "ok", "result": result} |
|
|
|
|
|
except Exception as e: |
|
|
return {"status": "error", "message": str(e)} |
|
|
|
|
|
@app.get("/") |
|
|
def root(): |
|
|
return {"message": "Forex LSTM Prediction API is active!"} |
|
|
|