Spaces:
Sleeping
Sleeping
File size: 8,806 Bytes
67b5ff4 6425a88 687eaa2 67b5ff4 687eaa2 67b5ff4 687eaa2 67b5ff4 6425a88 67b5ff4 6425a88 67b5ff4 6425a88 67b5ff4 6425a88 67b5ff4 6425a88 67b5ff4 8bd360d 67b5ff4 8bd360d 67b5ff4 8bd360d 67b5ff4 8bd360d 67b5ff4 8bd360d 67b5ff4 8bd360d 67b5ff4 8bd360d 67b5ff4 8bd360d 67b5ff4 1b6bd43 67b5ff4 6425a88 67b5ff4 72196d5 687eaa2 79e1a72 72196d5 67b5ff4 6425a88 72196d5 67b5ff4 6425a88 72196d5 67b5ff4 6425a88 67b5ff4 6425a88 67b5ff4 6425a88 67b5ff4 6425a88 67b5ff4 6425a88 6ad2808 6425a88 67b5ff4 6425a88 67b5ff4 6425a88 67b5ff4 687eaa2 67b5ff4 687eaa2 67b5ff4 687eaa2 6425a88 67b5ff4 6425a88 6ad2808 6425a88 67b5ff4 687eaa2 6425a88 67b5ff4 687eaa2 72196d5 67b5ff4 72196d5 687eaa2 |
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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
# ===============================================================
# Forex EMA + Dynamic Normalization API (Model B)
# Versi Final β Aman untuk Hugging Face Spaces (tanpa cache)
# ===============================================================
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
import logging
import tempfile
import os
# ===============================================================
# Konfigurasi Logging
# ===============================================================
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s β %(levelname)s β %(message)s"
)
logger = logging.getLogger(__name__)
# ===============================================================
# Konfigurasi FastAPI
# ===============================================================
app = FastAPI(
title="Model B β EMA & Dynamic Scaling API",
description="API untuk menghitung EMA, normalisasi, dan analisis tren otomatis berdasarkan data yfinance",
version="2.3"
)
PAIR = "EURUSD=X"
BASE_WINDOW = 60
# Matikan cache yfinance agar tidak menulis ke /.cache
os.environ["YFINANCE_CACHE_DISABLE"] = "1"
os.environ["YFINANCE_NO_TZ_CACHE"] = "1"
yf.set_tz_cache_location(tempfile.gettempdir())
# ===============================================================
# Schema
# ===============================================================
class DateRange(BaseModel):
start_date: str
end_date: str
# ===============================================================
# Helper Function β Load data langsung dari yfinance
# ===============================================================
def load_yf_data(pair, start, end):
"""
Ambil data yfinance tanpa cache, hanya return kolom ['date', 'close'].
"""
try:
df = yf.download(pair, start=start, end=end, auto_adjust=True, progress=False)
if df.empty:
raise ValueError("YFinance gagal mengambil data, data kosong.")
# Jika MultiIndex, ambil level pertama
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
# Ambil hanya kolom Close
close_col = [c for c in df.columns if "close" in c.lower()]
if not close_col:
raise ValueError(f"Tidak ada kolom 'Close' ditemukan di {df.columns.tolist()}")
df = df.reset_index()[["Date", close_col[0]]]
df.columns = ["date", "close"]
df["date"] = pd.to_datetime(df["date"]).dt.date
df["close"] = pd.to_numeric(df["close"], errors="coerce")
df = df.dropna(subset=["close"]).reset_index(drop=True)
logger.info(f"β
Berhasil ambil data {len(df)} baris dari {pair}")
return df
except Exception as e:
logger.error(f"β Error load_yf_data(): {e}", exc_info=True)
raise ValueError(str(e))
logger.info(f"Requested summary start={start_date.date()} end={end_date.date()}")
logger.info(f"Loaded rows from yfinance: {len(df)}")
if len(df) < 50:
return {
"status": "error",
"message": "Data terlalu sedikit (butuh minimal 50 hari).",
"data_points": len(df)
}
# ===============================================================
# Helper Function β Manual EMA
# ===============================================================
def ema_manual(prices, span):
if len(prices) < span:
return [np.nan] * len(prices)
ema = [np.nan] * len(prices)
alpha = 2 / (span + 1)
ema[span - 1] = np.mean(prices[:span])
for i in range(span, len(prices)):
ema[i] = alpha * prices[i] + (1 - alpha) * ema[i - 1]
return ema
# ===============================================================
# Helper Function β Dynamic Scaling
# ===============================================================
def get_dynamic_minmax():
today = datetime.now().date()
start = today - timedelta(days=BASE_WINDOW)
df = load_yf_data(PAIR, start, today + timedelta(days=1))
close_min = df["close"].min()
close_max = df["close"].max()
logger.info(f"Dynamic Min/Max Close: {close_min:.5f} / {close_max:.5f}")
return float(close_min), float(close_max)
def normalize_close(value, close_min, close_max):
if close_max == close_min:
return 0.5
return (value - close_min) / (close_max - close_min)
# ===============================================================
# Helper Function β Trend Analysis
# ===============================================================
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 ema50 != 0 else 0
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)
}
# ===============================================================
# Endpoint: /analyze
# ===============================================================
@app.post("/analyze")
def analyze_ema_endpoint(input_data: DateRange):
try:
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 = load_yf_data(PAIR, extended_start, end_date + timedelta(days=1))
if len(df) < 50:
return {"status": "error", "message": "Data terlalu sedikit (butuh minimal 50 hari)."}
df["EMA20"] = ema_manual(df["close"].values.tolist(), 20)
df["EMA50"] = ema_manual(df["close"].values.tolist(), 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": [str(d) for d in df["date"].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": close_min,
"max_close": close_max
}
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:
logger.error(f"Error di /analyze: {e}", exc_info=True)
return {"status": "error", "message": str(e)}
# ===============================================================
# Endpoint: /summary
# ===============================================================
@app.post("/summary")
def ema_summary_endpoint(input_data: DateRange):
try:
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 = load_yf_data(PAIR, extended_start, end_date + timedelta(days=1))
if len(df) < 50:
return {"status": "error", "message": "Data terlalu sedikit (butuh minimal 50 hari)."}
df["EMA20"] = ema_manual(df["close"].values.tolist(), 20)
df["EMA50"] = ema_manual(df["close"].values.tolist(), 50)
df = df.dropna().reset_index(drop=True)
latest = df.iloc[-1]
analysis = analyze_trend(latest)
return {
"status": "ok",
"pair": PAIR,
"as_of_date": str(latest["date"]),
"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:
logger.error(f"Error di /summary: {e}", exc_info=True)
return {"status": "error", "message": str(e)}
# ===============================================================
# Root Test
# ===============================================================
@app.get("/")
def root():
return {"message": "Model B API (EMA + Trend Summary) aktif π"}
|