ModelB_API / app.py
Miruzen's picture
Update app.py
a3bd9f8 verified
raw
history blame
6.91 kB
import logging
from fastapi import FastAPI
from pydantic import BaseModel
import pandas as pd
import numpy as np
import yfinance as yf
from datetime import datetime, timedelta
# Konfigurasi logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
app = FastAPI(
title="Model B – EMA & Dynamic Scaling API",
description="API untuk menghitung EMA, normalisasi, dan analisis tren otomatis berdasarkan data yfinance",
version="2.1"
)
PAIR = "EURUSD=X"
BASE_WINDOW = 60
class DateRange(BaseModel):
start_date: str
end_date: str
def ema_manual(prices, span):
ema = [np.nan] * len(prices)
alpha = 2 / (span + 1)
for i in range(len(prices)):
if i < span - 1:
ema[i] = np.nan
elif i == span - 1:
ema[i] = np.mean(prices[:span])
else:
ema[i] = alpha * prices[i] + (1 - alpha) * ema[i - 1]
return ema
def get_dynamic_minmax():
today = datetime.now().date()
start = today - timedelta(days=BASE_WINDOW)
logging.info(f"Mengunduh data untuk min/max: start={start}, end={today + timedelta(days=1)}")
df = yf.download(PAIR, start=start, end=today + timedelta(days=1), auto_adjust=True)
if df.empty:
logging.error("Gagal mengambil data harga terbaru untuk min/max.")
raise ValueError("Gagal mengambil data harga terbaru.")
close_min = df["Close"].min()
close_max = df["Close"].max()
logging.info(f"Min/Max Close: {close_min}/{close_max}")
return close_min, close_max
def normalize_close(value, close_min, close_max):
if close_max == close_min: # Hindari pembagian dengan nol
return 1.0 # Atau nilai default lain yang masuk akal
return (value - close_min) / (close_max - close_min)
def analyze_trend(latest_row):
ema20 = latest_row["EMA20"]
ema50 = latest_row["EMA50"]
close = latest_row["close"]
if ema20 > ema50:
trend = "bullish"
elif ema20 < ema50:
trend = "bearish"
else:
trend = "neutral"
diff = abs(ema20 - ema50) / ema50 * 100
if diff > 0.3:
strength = "strong"
elif diff > 0.1:
strength = "moderate"
else:
strength = "weak"
if close > ema20 and close > ema50:
price_position = "above both EMA β€” possible continuation"
elif close < ema20 and close < ema50:
price_position = "below both EMA β€” possible correction"
else:
price_position = "between EMAs β€” indecision zone"
return {
"trend": trend,
"strength": strength,
"price_position": price_position,
"ema_gap_percent": round(diff, 3)
}
@app.post("/analyze")
def analyze_ema(input_data: DateRange):
try:
logging.info(f"Menerima permintaan /analyze dengan start_date={input_data.start_date}, end_date={input_data.end_date}")
start_date = pd.to_datetime(input_data.start_date)
end_date = pd.to_datetime(input_data.end_date)
extended_start = start_date - timedelta(days=60)
logging.info(f"Mengunduh data dari yfinance: start={extended_start}, end={end_date + timedelta(days=1)}")
df = yf.download(PAIR, start=extended_start, end=end_date + timedelta(days=1), auto_adjust=True)
if df.empty:
logging.warning("Data tidak ditemukan untuk rentang tanggal yang diperluas.")
return {"status": "error", "message": "Data tidak ditemukan untuk rentang tanggal tersebut"}
df = df.reset_index()[["Date", "Close"]]
df.rename(columns={"Date": "date", "Close": "close"}, inplace=True)
if len(df) < 50:
logging.warning(f"Data terlalu sedikit ({len(df)} hari) untuk EMA50.")
return {"status": "error", "message": f"Data terlalu sedikit ({len(df)} hari). Butuh minimal 50 hari untuk EMA50."}
df["EMA20"] = ema_manual(df["close"], 20)
df["EMA50"] = ema_manual(df["close"], 50)
df = df.dropna().reset_index(drop=True)
close_min, close_max = get_dynamic_minmax()
df["norm_close"] = df["close"].apply(lambda x: normalize_close(x, close_min, close_max))
chart_data = {
"dates": df["date"].dt.strftime("%Y-%m-%d").tolist(),
"close": df["close"].round(6).tolist(),
"EMA20": df["EMA20"].round(6).tolist(),
"EMA50": df["EMA50"].round(6).tolist(),
"norm_close": df["norm_close"].round(6).tolist(),
"min_close": float(close_min),
"max_close": float(close_max),
}
logging.info(f"Analisis /analyze berhasil, {len(df)} data point.")
return {
"status": "ok",
"pair": PAIR,
"start_date": str(start_date.date()),
"end_date": str(end_date.date()),
"data_points": len(df),
"chart_data": chart_data
}
except Exception as e:
logging.error(f"Error di /analyze: {e}", exc_info=True) # exc_info=True akan mencetak traceback
return {"status": "error", "message": str(e)}
@app.post("/summary")
def ema_summary(input_data: DateRange):
try:
logging.info(f"Menerima permintaan /summary dengan start_date={input_data.start_date}, end_date={input_data.end_date}")
start_date = pd.to_datetime(input_data.start_date)
end_date = pd.to_datetime(input_data.end_date)
extended_start = start_date - timedelta(days=60)
df = yf.download(PAIR, start=extended_start, end=end_date + timedelta(days=1), auto_adjust=True)
if df.empty:
logging.warning("Data tidak ditemukan untuk rentang tanggal yang diperluas.")
return {"status": "error", "message": "Data tidak ditemukan"}
df = df.reset_index()[["Date", "Close"]]
df.rename(columns={"Date": "date", "Close": "close"}, inplace=True)
if len(df) < 50:
logging.warning(f"Data terlalu sedikit ({len(df)} hari) untuk EMA50.")
return {"status": "error", "message": f"Data terlalu sedikit ({len(df)} hari). Butuh minimal 50 hari untuk EMA50."}
df["EMA20"] = ema_manual(df["close"], 20)
df["EMA50"] = ema_manual(df["close"], 50)
df = df.dropna().reset_index(drop=True)
latest = df.iloc[-1]
analysis = analyze_trend(latest)
logging.info(f"Analisis /summary berhasil, tanggal terakhir: {latest['date'].strftime('%Y-%m-%d')}")
return {
"status": "ok",
"pair": PAIR,
"as_of_date": latest["date"].strftime("%Y-%m-%d"),
"close": round(float(latest["close"]), 6),
"EMA20": round(float(latest["EMA20"]), 6),
"EMA50": round(float(latest["EMA50"]), 6),
"trend_analysis": analysis
}
except Exception as e:
logging.error(f"Error di /summary: {e}", exc_info=True)
return {"status": "error", "message": str(e)}