Anurag33Gaikwad commited on
Commit
1243c9b
·
verified ·
1 Parent(s): 72ecc6b

Upload 15 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ WORKDIR /code
4
+
5
+ # Fix Python path for imports
6
+ ENV PYTHONPATH=/code
7
+
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir --upgrade pip \
10
+ && pip install --no-cache-dir -r requirements.txt
11
+
12
+ COPY . .
13
+
14
+ EXPOSE 7860
15
+
16
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # just marks package
app/api/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+ from .routes_root import router as root_router
3
+ from .routes_prediction import router as prediction_router
4
+ from .routes_price import router as price_router
5
+
6
+ api_router = APIRouter()
7
+ api_router.include_router(root_router)
8
+ api_router.include_router(prediction_router)
9
+ api_router.include_router(price_router)
app/api/routes_prediction.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Query
2
+ from app.schemas import PredictionResponse, ChartForecastResponse
3
+ from app.services.prediction import predict_path
4
+
5
+ router = APIRouter(prefix="/api")
6
+
7
+ @router.get("/prediction", response_model=PredictionResponse)
8
+ def get_prediction(ticker: str = Query(..., min_length=1), days: int = 7):
9
+ try:
10
+ prediction_payload, _chart_payload, _loaded = predict_path(ticker, days)
11
+ return prediction_payload
12
+ except HTTPException:
13
+ raise
14
+ except Exception as e:
15
+ raise HTTPException(status_code=500, detail=str(e))
16
+
17
+ @router.get("/price/forecast", response_model=ChartForecastResponse)
18
+ def get_price_forecast(ticker: str = Query(..., min_length=1), days: int = 7):
19
+ try:
20
+ _prediction_payload, chart_payload, _loaded = predict_path(ticker, days)
21
+ return chart_payload
22
+ except HTTPException:
23
+ raise
24
+ except Exception as e:
25
+ raise HTTPException(status_code=500, detail=str(e))
app/api/routes_price.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Query
2
+ from app.schemas import HistoryResponse
3
+ from app.services.market_data import get_enriched_history, last_n_candles
4
+ from app.config import get_settings
5
+
6
+ router = APIRouter(prefix="/api")
7
+ settings = get_settings()
8
+
9
+ @router.get("/price/history", response_model=HistoryResponse)
10
+ def get_price_history(
11
+ ticker: str = Query(..., min_length=1),
12
+ days: int = Query(60, ge=30, le=365),
13
+ ):
14
+ try:
15
+ df_raw, _df_tech = get_enriched_history(ticker)
16
+ candles = last_n_candles(df_raw, min(days, settings.history_window))
17
+ return HistoryResponse(ticker=ticker.upper(), historicalPrices=candles)
18
+ except HTTPException:
19
+ raise
20
+ except Exception as e:
21
+ raise HTTPException(status_code=500, detail=str(e))
app/api/routes_root.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+ from app.schemas import HealthResponse
3
+ from app.services.prediction import MODEL_LOADED
4
+
5
+ router = APIRouter()
6
+
7
+ @router.get("/", response_model=HealthResponse)
8
+ def root() -> HealthResponse:
9
+ return HealthResponse(
10
+ status="QuantMind backend is live 🤖",
11
+ modelLoaded=MODEL_LOADED,
12
+ )
app/config.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from functools import lru_cache
3
+
4
+ class Settings:
5
+ app_name: str = "QuantMind Backend"
6
+ environment: str = os.getenv("ENVIRONMENT", "dev")
7
+ cors_origins: list[str] = os.getenv("CORS_ORIGINS", "*").split(",")
8
+ model_path: str = os.getenv(
9
+ "MODEL_PATH",
10
+ "model/universal_multivariate_model_v1.h5",
11
+ )
12
+ yfinance_period: str = os.getenv("YF_PERIOD", "1y")
13
+ history_window: int = int(os.getenv("HISTORY_WINDOW", "60"))
14
+ max_forecast_days: int = int(os.getenv("MAX_FORECAST_DAYS", "14"))
15
+
16
+ @lru_cache
17
+ def get_settings() -> Settings:
18
+ return Settings()
app/indicators.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+ def add_technical_indicators(df: pd.DataFrame) -> pd.DataFrame:
5
+ df = df.copy()
6
+
7
+ # RSI
8
+ delta = df["Close"].diff()
9
+ gain = delta.where(delta > 0, 0).rolling(window=14).mean()
10
+ loss = -delta.where(delta < 0, 0).rolling(window=14).mean()
11
+ rs = gain / loss
12
+ df["RSI"] = 100 - (100 / (1 + rs))
13
+
14
+ # MACD
15
+ exp1 = df["Close"].ewm(span=12, adjust=False).mean()
16
+ exp2 = df["Close"].ewm(span=26, adjust=False).mean()
17
+ df["MACD"] = exp1 - exp2
18
+
19
+ # Simple 50-day MA
20
+ df["MA50"] = df["Close"].rolling(window=50).mean()
21
+
22
+ # Log Volume
23
+ df["Log_Volume"] = np.log(df["Volume"] + 1)
24
+
25
+ df = df.dropna()
26
+ return df
27
+
28
+ def classify_signal(predicted_change: float) -> str:
29
+ if predicted_change > 1.0:
30
+ return "STRONG BUY"
31
+ if predicted_change > 0:
32
+ return "BUY"
33
+ if predicted_change < -1.0:
34
+ return "STRONG SELL"
35
+ if predicted_change < 0:
36
+ return "SELL"
37
+ return "HOLD"
38
+
39
+ def classify_volatility(df: pd.DataFrame) -> str:
40
+ returns = df["Close"].pct_change().dropna()
41
+ vol = returns.std()
42
+ if vol < 0.01:
43
+ return "Low"
44
+ if vol < 0.025:
45
+ return "Medium"
46
+ return "High"
app/schemas.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Literal, Optional
2
+ from pydantic import BaseModel, Field
3
+
4
+ SignalType = Literal["BUY", "SELL", "HOLD", "STRONG BUY", "STRONG SELL"]
5
+ VolatilityLevel = Literal["Low", "Medium", "High"]
6
+
7
+ class Candle(BaseModel):
8
+ date: str
9
+ price: float
10
+
11
+ class ForecastPoint(BaseModel):
12
+ day: int
13
+ price: float
14
+ lower: float
15
+ upper: float
16
+
17
+ class PredictionResponse(BaseModel):
18
+ ticker: str
19
+ currentPrice: float
20
+ targetPrice: float
21
+ predictedChange: float
22
+ signal: SignalType
23
+ rsi: float
24
+ macd: str
25
+ volatility: VolatilityLevel
26
+ historicalPrices: List[Candle]
27
+ predictedPrice: float
28
+ forecastData: List[ForecastPoint]
29
+
30
+ class HistoryResponse(BaseModel):
31
+ ticker: str
32
+ historicalPrices: List[Candle]
33
+
34
+ class ChartForecastPoint(BaseModel):
35
+ date: str
36
+ price: float
37
+ lower: float
38
+ upper: float
39
+
40
+ class ChartForecastResponse(BaseModel):
41
+ ticker: str
42
+ points: List[ChartForecastPoint]
43
+
44
+ class HealthResponse(BaseModel):
45
+ status: str
46
+ modelLoaded: bool
47
+ version: str = Field(default="1.0.0")
app/services/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # service namespace
app/services/market_data.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from typing import Tuple
3
+ import pandas as pd
4
+ import yfinance as yf
5
+ from fastapi import HTTPException
6
+
7
+ from app.config import get_settings
8
+ from app.indicators import add_technical_indicators
9
+
10
+ settings = get_settings()
11
+
12
+ def fetch_raw_history(ticker: str) -> pd.DataFrame:
13
+ df = yf.download(
14
+ ticker,
15
+ period=settings.yfinance_period,
16
+ progress=False,
17
+ auto_adjust=False,
18
+ )
19
+ if df.empty or len(df) < settings.history_window + 60:
20
+ raise HTTPException(
21
+ status_code=400,
22
+ detail="Not enough historical data for this ticker.",
23
+ )
24
+ df.index = df.index.tz_localize(None)
25
+ return df
26
+
27
+ def get_enriched_history(ticker: str) -> Tuple[pd.DataFrame, pd.DataFrame]:
28
+ df_raw = fetch_raw_history(ticker)
29
+ df_tech = add_technical_indicators(df_raw)
30
+ return df_raw, df_tech
31
+
32
+ def last_n_candles(df: pd.DataFrame, n: int) -> list[dict]:
33
+ tail = df.tail(n)
34
+ return [
35
+ {
36
+ "date": idx.strftime("%Y-%m-%d"),
37
+ "price": float(row["Close"]),
38
+ }
39
+ for idx, row in tail.iterrows()
40
+ ]
app/services/prediction.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ import numpy as np
3
+ import pandas as pd
4
+ from tensorflow.keras.models import load_model
5
+ from fastapi import HTTPException
6
+
7
+ from app.config import get_settings
8
+ from app.indicators import classify_signal, classify_volatility
9
+ from app.services.market_data import get_enriched_history, last_n_candles
10
+
11
+ settings = get_settings()
12
+
13
+ try:
14
+ _model = load_model(settings.model_path)
15
+ MODEL_LOADED = True
16
+ except Exception as e:
17
+ print(f"[Prediction] Failed to load model: {e}")
18
+ _model = None
19
+ MODEL_LOADED = False
20
+
21
+ def _normalize_window(window: np.ndarray) -> np.ndarray:
22
+ """
23
+ window shape: (60, 5) -> [Close, RSI, MACD, Log_Volume, MA50]
24
+ """
25
+ start_p = window[0, 0]
26
+ norm = np.zeros_like(window)
27
+ norm[:, 0] = (window[:, 0] / start_p) - 1
28
+ norm[:, 1] = window[:, 1] / 100.0
29
+ norm[:, 2] = window[:, 2] / start_p
30
+ norm[:, 3] = (window[:, 3] / np.mean(window[:, 3])) - 1
31
+ norm[:, 4] = (window[:, 4] / start_p) - 1
32
+ return norm, start_p
33
+
34
+ def predict_path(ticker: str, days: int):
35
+ if _model is None or not MODEL_LOADED:
36
+ raise HTTPException(status_code=500, detail="Prediction model not loaded.")
37
+
38
+ if days > settings.max_forecast_days:
39
+ days = settings.max_forecast_days
40
+
41
+ df_raw, df_tech = get_enriched_history(ticker)
42
+ features = df_tech[["Close", "RSI", "MACD", "Log_Volume", "MA50"]].values
43
+ last_window = features[-settings.history_window :]
44
+ current_price = float(last_window[-1, 0])
45
+
46
+ temp_window = last_window.copy()
47
+ forecast_points: List[dict] = []
48
+
49
+ for i in range(days):
50
+ norm, start_p = _normalize_window(temp_window)
51
+ pred_pct = _model.predict(norm.reshape(1, 60, 5), verbose=0)[0][0]
52
+ next_price = float(start_p * (1 + pred_pct))
53
+
54
+ volatility = 0.015 * (i + 1)
55
+ lower_bound = float(next_price * (1 - volatility))
56
+ upper_bound = float(next_price * (1 + volatility))
57
+
58
+ forecast_points.append(
59
+ {
60
+ "day": i + 1,
61
+ "price": next_price,
62
+ "lower": lower_bound,
63
+ "upper": upper_bound,
64
+ }
65
+ )
66
+
67
+ new_row = temp_window[-1].copy()
68
+ new_row[0] = next_price
69
+ temp_window = np.vstack([temp_window[1:], new_row])
70
+
71
+ # summary metrics
72
+ target_price = float(forecast_points[-1]["price"])
73
+ predicted_change = ((target_price - current_price) / current_price) * 100
74
+
75
+ vol_label = classify_volatility(df_raw)
76
+ signal = classify_signal(predicted_change)
77
+ rsi_latest = float(df_tech["RSI"].iloc[-1])
78
+ macd_latest = float(df_tech["MACD"].iloc[-1])
79
+ macd_label = "Bullish" if macd_latest >= 0 else "Bearish"
80
+
81
+ historical = last_n_candles(df_raw, settings.history_window)
82
+
83
+ prediction_payload = {
84
+ "ticker": ticker.upper(),
85
+ "currentPrice": current_price,
86
+ "targetPrice": target_price,
87
+ "predictedChange": predicted_change,
88
+ "signal": signal,
89
+ "rsi": rsi_latest,
90
+ "macd": macd_label,
91
+ "volatility": vol_label,
92
+ "historicalPrices": historical,
93
+ "predictedPrice": target_price,
94
+ "forecastData": [
95
+ {
96
+ "day": p["day"],
97
+ "price": p["price"],
98
+ "changePct": ((p["price"] - current_price) / current_price) * 100,
99
+ }
100
+ for p in forecast_points
101
+ ],
102
+ }
103
+
104
+ chart_forecast_payload = {
105
+ "ticker": ticker.upper(),
106
+ "points": [
107
+ {
108
+ "date": historical[-1]["date"], # last known date
109
+ "price": current_price,
110
+ "lower": current_price,
111
+ "upper": current_price,
112
+ },
113
+ # add synthetic forward dates
114
+ ],
115
+ }
116
+
117
+ # add future dates
118
+ from datetime import datetime, timedelta
119
+
120
+ last_date = datetime.strptime(historical[-1]["date"], "%Y-%m-%d")
121
+ for i, p in enumerate(forecast_points, start=1):
122
+ d = last_date + timedelta(days=i)
123
+ chart_forecast_payload["points"].append(
124
+ {
125
+ "date": d.strftime("%Y-%m-%d"),
126
+ "price": p["price"],
127
+ "lower": p["lower"],
128
+ "upper": p["upper"],
129
+ }
130
+ )
131
+
132
+ return prediction_payload, chart_forecast_payload, MODEL_LOADED
main.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+
4
+ from app.api import api_router
5
+ from app.config import get_settings
6
+
7
+ settings = get_settings()
8
+
9
+ app = FastAPI(
10
+ title="QuantMind Backend",
11
+ version="1.0.0",
12
+ description="LSTM-based stock prediction API for the QuantMind dashboard.",
13
+ )
14
+
15
+ app.add_middleware(
16
+ CORSMiddleware,
17
+ allow_origins=["*"], # tighten later
18
+ allow_credentials=True,
19
+ allow_methods=["*"],
20
+ allow_headers=["*"],
21
+ )
22
+
23
+ app.include_router(api_router)
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn[standard]==0.30.0
3
+ numpy==1.26.4
4
+ pandas==2.2.2
5
+ yfinance==0.2.44
6
+ tensorflow==2.17.0
7
+ python-multipart==0.0.9
tests/test_health.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi.testclient import TestClient
2
+ from main import app
3
+
4
+ client = TestClient(app)
5
+
6
+ def test_root_health():
7
+ resp = client.get("/")
8
+ assert resp.status_code == 200
9
+ body = resp.json()
10
+ assert "status" in body