Spaces:
Sleeping
Sleeping
Upload 3 files
Browse files- Dockerfile.txt +16 -0
- app.py +129 -0
- requirements.txt +6 -0
Dockerfile.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Gunakan image Python versi slim sebagai dasar
|
| 2 |
+
FROM python:3.9-slim
|
| 3 |
+
|
| 4 |
+
# Set direktori kerja di dalam container
|
| 5 |
+
WORKDIR /code
|
| 6 |
+
|
| 7 |
+
# Salin file requirements dulu dan install dependensi
|
| 8 |
+
COPY ./requirements.txt /code/requirements.txt
|
| 9 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
| 10 |
+
|
| 11 |
+
# Salin semua sisa file proyek ke dalam container
|
| 12 |
+
COPY . /code
|
| 13 |
+
|
| 14 |
+
# Perintah untuk menjalankan aplikasi saat container dimulai
|
| 15 |
+
# Hugging Face menggunakan port 7860 secara default
|
| 16 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
app.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# File: main.py
|
| 2 |
+
|
| 3 |
+
# 1. IMPORT LIBRARY
|
| 4 |
+
from fastapi import FastAPI
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
from typing import List
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
import yfinance as yf
|
| 9 |
+
import pandas as pd
|
| 10 |
+
import numpy as np
|
| 11 |
+
from prophet import Prophet
|
| 12 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 13 |
+
|
| 14 |
+
# 2. INISIALISASI APLIKASI FASTAPI
|
| 15 |
+
app = FastAPI(
|
| 16 |
+
title="Stock Risk & Forecast API",
|
| 17 |
+
description="API untuk analisis risiko portfolio dan peramalan harga saham."
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# --- REVISI 1: Tambahkan Middleware CORS ---
|
| 21 |
+
origins = [
|
| 22 |
+
"http://localhost:8080",
|
| 23 |
+
"http://127.0.0.1:8080",
|
| 24 |
+
# PASTE URL FORWARDED DARI FRONTEND ANDA DI SINI
|
| 25 |
+
"https://psychic-goldfish-jvvvxjrwgp9hg56-8080.app.github.dev",
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
app.add_middleware(
|
| 29 |
+
CORSMiddleware,
|
| 30 |
+
allow_origins=origins,
|
| 31 |
+
allow_credentials=True,
|
| 32 |
+
allow_methods=["*"], # Izinkan semua method (GET, POST, dll)
|
| 33 |
+
allow_headers=["*"], # Izinkan semua header
|
| 34 |
+
)
|
| 35 |
+
# --- AKHIR REVISI 1 ---
|
| 36 |
+
|
| 37 |
+
# 3. KUMPULKAN FUNGSI INTI (Versi Terbaru yang Sudah Diperbaiki)
|
| 38 |
+
def get_stock_data(tickers: List[str], start_date: str, end_date: str):
|
| 39 |
+
try:
|
| 40 |
+
# Langsung ambil data 'Close'. yfinance akan mengembalikan DataFrame
|
| 41 |
+
# baik untuk satu maupun banyak ticker.
|
| 42 |
+
stock_data = yf.download(tickers, start=start_date, end=end_date)['Close']
|
| 43 |
+
|
| 44 |
+
# Blok 'if len(tickers) == 1:' yang lama sudah tidak diperlukan dan dihapus.
|
| 45 |
+
# Jika yfinance hanya mengembalikan satu Series (kasus langka),
|
| 46 |
+
# baris berikutnya akan mengubahnya jadi DataFrame secara otomatis.
|
| 47 |
+
if isinstance(stock_data, pd.Series):
|
| 48 |
+
stock_data = stock_data.to_frame(name=tickers[0])
|
| 49 |
+
|
| 50 |
+
return stock_data.dropna()
|
| 51 |
+
except Exception as e:
|
| 52 |
+
print(f"!!! TERJADI ERROR DI get_stock_data: {e}, Tickers: {tickers}")
|
| 53 |
+
return None
|
| 54 |
+
|
| 55 |
+
def calculate_risk_metrics(price_data: pd.DataFrame):
|
| 56 |
+
"""Menghitung metrik risiko untuk portfolio dengan ASUMSI BOBOT SAMA."""
|
| 57 |
+
daily_returns = price_data.pct_change().dropna()
|
| 58 |
+
num_assets = len(price_data.columns)
|
| 59 |
+
weights = np.array([1/num_assets] * num_assets)
|
| 60 |
+
portfolio_returns = daily_returns.dot(weights)
|
| 61 |
+
|
| 62 |
+
expected_return = portfolio_returns.mean() * 252
|
| 63 |
+
volatility = portfolio_returns.std() * np.sqrt(252)
|
| 64 |
+
sharpe_ratio = expected_return / volatility if volatility != 0 else 0
|
| 65 |
+
|
| 66 |
+
return {
|
| 67 |
+
"expected_annual_return": expected_return,
|
| 68 |
+
"annual_volatility": volatility,
|
| 69 |
+
"sharpe_ratio": sharpe_ratio
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
def get_forecast(ticker: str, price_data: pd.DataFrame, forecast_horizon: int):
|
| 73 |
+
"""Membuat prediksi dan mengembalikan DataFrame forecast DAN modelnya."""
|
| 74 |
+
df_prophet = price_data[[ticker]].reset_index()
|
| 75 |
+
df_prophet.columns = ['ds', 'y']
|
| 76 |
+
|
| 77 |
+
model = Prophet(daily_seasonality=False, weekly_seasonality=True, yearly_seasonality=True)
|
| 78 |
+
model.fit(df_prophet)
|
| 79 |
+
|
| 80 |
+
future = model.make_future_dataframe(periods=forecast_horizon)
|
| 81 |
+
forecast = model.predict(future)
|
| 82 |
+
|
| 83 |
+
# Mengubah format tanggal agar bisa dikirim sebagai JSON
|
| 84 |
+
forecast['ds'] = forecast['ds'].dt.strftime('%Y-%m-%d')
|
| 85 |
+
|
| 86 |
+
# Mengembalikan DUA nilai, sesuai revisi Anda
|
| 87 |
+
return forecast, model
|
| 88 |
+
|
| 89 |
+
# 4. DEFINISIKAN MODEL REQUEST (INPUT DARI USER) - Tidak ada perubahan
|
| 90 |
+
class RiskRequest(BaseModel):
|
| 91 |
+
tickers: List[str] = ['BBCA.JK', 'TLKM.JK']
|
| 92 |
+
start_date: str = '2020-01-01'
|
| 93 |
+
end_date: str = datetime.now().strftime('%Y-%m-%d')
|
| 94 |
+
|
| 95 |
+
class ForecastRequest(BaseModel):
|
| 96 |
+
ticker: str = 'BBCA.JK'
|
| 97 |
+
start_date: str = '2020-01-01'
|
| 98 |
+
end_date: str = datetime.now().strftime('%Y-%m-%d')
|
| 99 |
+
forecast_horizon: int = 30
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
# 5. BUAT ENDPOINT API (Dengan Penyesuaian)
|
| 104 |
+
@app.post("/calculate-risk")
|
| 105 |
+
def calculate_risk_endpoint(request: RiskRequest):
|
| 106 |
+
price_data = get_stock_data(request.tickers, request.start_date, request.end_date)
|
| 107 |
+
if price_data is None or price_data.empty:
|
| 108 |
+
return {"error": "Tidak dapat mengambil data saham. Periksa kembali ticker."}
|
| 109 |
+
|
| 110 |
+
risk_metrics = calculate_risk_metrics(price_data)
|
| 111 |
+
return risk_metrics
|
| 112 |
+
|
| 113 |
+
@app.post("/get-forecast")
|
| 114 |
+
def get_forecast_endpoint(request: ForecastRequest):
|
| 115 |
+
price_data = get_stock_data([request.ticker], request.start_date, request.end_date)
|
| 116 |
+
if price_data is None or price_data.empty:
|
| 117 |
+
return {"error": "Tidak dapat mengambil data saham. Periksa kembali ticker."}
|
| 118 |
+
|
| 119 |
+
# --- INI BAGIAN YANG BERUBAH ---
|
| 120 |
+
# Kita panggil fungsi yang mengembalikan 2 nilai
|
| 121 |
+
# Tapi kita hanya butuh yang pertama (forecast_data) untuk dikirim ke frontend
|
| 122 |
+
# _ (underscore) adalah konvensi di Python untuk variabel yang nilainya kita abaikan
|
| 123 |
+
forecast_data, _ = get_forecast(request.ticker, price_data, request.forecast_horizon)
|
| 124 |
+
|
| 125 |
+
# Kita hanya mengirim data forecast yang sudah bersih
|
| 126 |
+
final_forecast_data = forecast_data[['ds', 'yhat', 'yhat_lower', 'yhat_upper']]
|
| 127 |
+
|
| 128 |
+
# Konversi DataFrame ke format JSON yang bisa dikirim
|
| 129 |
+
return final_forecast_data.to_dict('records')
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn[standard]
|
| 3 |
+
yfinance
|
| 4 |
+
prophet
|
| 5 |
+
pandas
|
| 6 |
+
numpy
|