Spaces:
Running
Running
Upload 6 files
Browse files- app.py +67 -0
- data/nifty50_daily.parquet +3 -0
- data_updater.py +105 -0
- forecaster_engine.py +123 -0
- predictions.json +251 -0
- requirements.txt +7 -0
app.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
from datetime import datetime, time, date
|
| 4 |
+
from fastapi import FastAPI, BackgroundTasks, HTTPException
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
from zoneinfo import ZoneInfo
|
| 7 |
+
from data_updater import update_daily_data, is_trading_day
|
| 8 |
+
from forecaster_engine import generate_predictions
|
| 9 |
+
|
| 10 |
+
IST = ZoneInfo("Asia/Kolkata")
|
| 11 |
+
MARKET_CLOSE_BUFFER = time(15, 45) # Update runs after 3:45 PM
|
| 12 |
+
PREDICTIONS_FILE = os.path.join(os.path.dirname(__file__), "predictions.json")
|
| 13 |
+
|
| 14 |
+
app = FastAPI(title="HF NIFTY Forecaster Backend")
|
| 15 |
+
|
| 16 |
+
app.add_middleware(
|
| 17 |
+
CORSMiddleware,
|
| 18 |
+
allow_origins=["*"],
|
| 19 |
+
allow_methods=["*"],
|
| 20 |
+
allow_headers=["*"],
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
def run_update_pipeline():
|
| 24 |
+
try:
|
| 25 |
+
# Step 1: Update data
|
| 26 |
+
res = update_daily_data()
|
| 27 |
+
if res.get("status") == "error":
|
| 28 |
+
print(f"Update failed: {res.get('reason')}")
|
| 29 |
+
return
|
| 30 |
+
|
| 31 |
+
# Step 2: Generate predictions
|
| 32 |
+
generate_predictions()
|
| 33 |
+
except Exception as e:
|
| 34 |
+
print(f"Pipeline error: {e}")
|
| 35 |
+
|
| 36 |
+
@app.get("/predictions")
|
| 37 |
+
def get_predictions():
|
| 38 |
+
if not os.path.exists(PREDICTIONS_FILE):
|
| 39 |
+
raise HTTPException(status_code=404, detail="Predictions not yet generated")
|
| 40 |
+
|
| 41 |
+
with open(PREDICTIONS_FILE, "r") as f:
|
| 42 |
+
data = json.load(f)
|
| 43 |
+
|
| 44 |
+
return data
|
| 45 |
+
|
| 46 |
+
@app.post("/cron/update")
|
| 47 |
+
def cron_trigger(background_tasks: BackgroundTasks):
|
| 48 |
+
now = datetime.now(IST)
|
| 49 |
+
today = now.date()
|
| 50 |
+
current_time = now.time()
|
| 51 |
+
|
| 52 |
+
# 1. Check if it's a trading day
|
| 53 |
+
if not is_trading_day(today):
|
| 54 |
+
return {"status": "skipped", "reason": f"{today} is a holiday or weekend"}
|
| 55 |
+
|
| 56 |
+
# 2. Check if it's past 3:45 PM
|
| 57 |
+
if current_time < MARKET_CLOSE_BUFFER:
|
| 58 |
+
return {"status": "skipped", "reason": "Market is still open or buffer not reached. Runs after 3:45 PM IST."}
|
| 59 |
+
|
| 60 |
+
# Trigger the full pipeline in the background so Netlify doesn't timeout
|
| 61 |
+
background_tasks.add_task(run_update_pipeline)
|
| 62 |
+
|
| 63 |
+
return {"status": "triggered", "message": "Update and forecast pipeline started in the background."}
|
| 64 |
+
|
| 65 |
+
@app.get("/health")
|
| 66 |
+
def health_check():
|
| 67 |
+
return {"status": "alive", "server_time_ist": datetime.now(IST).isoformat()}
|
data/nifty50_daily.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e1ec56e84aa6a133397cbc6000ac563883df0f9287d2cafb434e9c7efcf7778a
|
| 3 |
+
size 1377561
|
data_updater.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import time
|
| 3 |
+
import requests
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from datetime import datetime, date, timedelta
|
| 6 |
+
from zoneinfo import ZoneInfo
|
| 7 |
+
import pandas_market_calendars as mcal
|
| 8 |
+
|
| 9 |
+
IST = ZoneInfo("Asia/Kolkata")
|
| 10 |
+
DATA_FILE = os.path.join(os.path.dirname(__file__), "data", "nifty50_daily.parquet")
|
| 11 |
+
|
| 12 |
+
TICKERS = [
|
| 13 |
+
'ADANIENT', 'ADANIPORTS', 'APOLLOHOSP', 'ASIANPAINT', 'AXISBANK', 'BAJAJ-AUTO', 'BAJAJFINSV', 'BAJFINANCE',
|
| 14 |
+
'BHARTIARTL', 'BPCL', 'BRITANNIA', 'CIPLA', 'COALINDIA', 'DIVISLAB', 'DRREDDY', 'EICHERMOT', 'GRASIM',
|
| 15 |
+
'HCLTECH', 'HDFCBANK', 'HDFCLIFE', 'HEROMOTOCO', 'HINDALCO', 'HINDUNILVR', 'ICICIBANK', 'INDUSINDBK',
|
| 16 |
+
'INFY', 'ITC', 'JSWSTEEL', 'KOTAKBANK', 'LT', 'M&M', 'MARUTI', 'NESTLEIND', 'NTPC', 'ONGC', 'POWERGRID',
|
| 17 |
+
'RELIANCE', 'SBILIFE', 'SBIN', 'SUNPHARMA', 'TATACONSUM', 'TATAMOTORS', 'TATASTEEL', 'TCS', 'TECHM',
|
| 18 |
+
'TITAN', 'ULTRACEMCO', 'UPL', 'WIPRO'
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
def is_trading_day(target_date: date) -> bool:
|
| 22 |
+
try:
|
| 23 |
+
nse = mcal.get_calendar('NSE')
|
| 24 |
+
schedule = nse.schedule(start_date=target_date, end_date=target_date)
|
| 25 |
+
return not schedule.empty
|
| 26 |
+
except Exception as e:
|
| 27 |
+
print(f"Calendar check failed: {e}")
|
| 28 |
+
# Fallback: assume Monday-Friday is trading day
|
| 29 |
+
return target_date.weekday() < 5
|
| 30 |
+
|
| 31 |
+
def fetch_groww_data(ticker: str, start_ts: int, end_ts: int):
|
| 32 |
+
url = f"https://groww.in/v1/api/charting_service/v2/chart/exchange/NSE/segment/CASH/{ticker}?endTimeInMillis={end_ts}&intervalInMinutes=1&startTimeInMillis={start_ts}"
|
| 33 |
+
headers = {
|
| 34 |
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
|
| 35 |
+
"Accept": "application/json"
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
try:
|
| 39 |
+
response = requests.get(url, headers=headers, timeout=10)
|
| 40 |
+
if response.status_code == 200:
|
| 41 |
+
data = response.json()
|
| 42 |
+
if data and 'candles' in data and len(data['candles']) > 0:
|
| 43 |
+
# Candles format: [timestamp, open, high, low, close, volume]
|
| 44 |
+
last_candle = data['candles'][-1]
|
| 45 |
+
return last_candle[4] # Close price
|
| 46 |
+
return None
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f"Error fetching {ticker}: {e}")
|
| 49 |
+
return None
|
| 50 |
+
|
| 51 |
+
def update_daily_data():
|
| 52 |
+
now = datetime.now(IST)
|
| 53 |
+
today = now.date()
|
| 54 |
+
|
| 55 |
+
if not is_trading_day(today):
|
| 56 |
+
print(f"{today} is not a trading day. Skipping update.")
|
| 57 |
+
return {"status": "skipped", "reason": "not a trading day"}
|
| 58 |
+
|
| 59 |
+
if not os.path.exists(DATA_FILE):
|
| 60 |
+
print(f"Data file {DATA_FILE} not found!")
|
| 61 |
+
return {"status": "error", "reason": "data file missing"}
|
| 62 |
+
|
| 63 |
+
# Read existing data
|
| 64 |
+
df = pd.read_parquet(DATA_FILE)
|
| 65 |
+
|
| 66 |
+
# Check if we already updated today
|
| 67 |
+
if not df.empty and pd.to_datetime(today) in df['date'].dt.date.values:
|
| 68 |
+
# We might have partial data or want to overwrite, but for safety:
|
| 69 |
+
# Let's delete today's entries if they exist so we can cleanly append
|
| 70 |
+
df = df[df['date'].dt.date != today]
|
| 71 |
+
|
| 72 |
+
print(f"Fetching data for {today}...")
|
| 73 |
+
|
| 74 |
+
# Market hours: 09:15 to 15:30 IST
|
| 75 |
+
start_dt = datetime.combine(today, datetime.strptime("09:15", "%H:%M").time()).replace(tzinfo=IST)
|
| 76 |
+
end_dt = datetime.combine(today, datetime.strptime("15:30", "%H:%M").time()).replace(tzinfo=IST)
|
| 77 |
+
|
| 78 |
+
start_ts = int(start_dt.timestamp() * 1000)
|
| 79 |
+
end_ts = int(end_dt.timestamp() * 1000)
|
| 80 |
+
|
| 81 |
+
new_rows = []
|
| 82 |
+
|
| 83 |
+
for ticker in TICKERS:
|
| 84 |
+
close_price = fetch_groww_data(ticker, start_ts, end_ts)
|
| 85 |
+
if close_price is not None:
|
| 86 |
+
new_rows.append({
|
| 87 |
+
'date': pd.to_datetime(today),
|
| 88 |
+
'close': float(close_price),
|
| 89 |
+
'ticker': ticker
|
| 90 |
+
})
|
| 91 |
+
time.sleep(0.5) # Rate limiting
|
| 92 |
+
|
| 93 |
+
if new_rows:
|
| 94 |
+
new_df = pd.DataFrame(new_rows)
|
| 95 |
+
updated_df = pd.concat([df, new_df], ignore_index=True)
|
| 96 |
+
updated_df.sort_values(by=['ticker', 'date'], inplace=True)
|
| 97 |
+
updated_df.to_parquet(DATA_FILE)
|
| 98 |
+
print(f"Successfully updated {len(new_rows)} tickers for {today}")
|
| 99 |
+
return {"status": "success", "updated_count": len(new_rows)}
|
| 100 |
+
else:
|
| 101 |
+
print("No new data fetched.")
|
| 102 |
+
return {"status": "error", "reason": "fetch failed for all tickers"}
|
| 103 |
+
|
| 104 |
+
if __name__ == "__main__":
|
| 105 |
+
update_daily_data()
|
forecaster_engine.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import numpy as np
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
DATA_FILE = os.path.join(os.path.dirname(__file__), "data", "nifty50_daily.parquet")
|
| 8 |
+
PREDICTIONS_FILE = os.path.join(os.path.dirname(__file__), "predictions.json")
|
| 9 |
+
|
| 10 |
+
def generate_predictions():
|
| 11 |
+
if not os.path.exists(DATA_FILE):
|
| 12 |
+
print(f"Data file missing: {DATA_FILE}")
|
| 13 |
+
return
|
| 14 |
+
|
| 15 |
+
df_all = pd.read_parquet(DATA_FILE)
|
| 16 |
+
tickers = df_all['ticker'].unique()
|
| 17 |
+
|
| 18 |
+
predictions = {}
|
| 19 |
+
forecast_date = None
|
| 20 |
+
|
| 21 |
+
for ticker in tickers:
|
| 22 |
+
df = df_all[df_all['ticker'] == ticker].copy()
|
| 23 |
+
df.sort_values('date', inplace=True)
|
| 24 |
+
df.set_index('date', inplace=True)
|
| 25 |
+
|
| 26 |
+
if len(df) < 130:
|
| 27 |
+
continue
|
| 28 |
+
|
| 29 |
+
forecast_date_ts = df.index[-1] + pd.Timedelta(days=1)
|
| 30 |
+
# Advance past weekends roughly for display
|
| 31 |
+
if forecast_date_ts.weekday() >= 5:
|
| 32 |
+
forecast_date_ts += pd.Timedelta(days=(7 - forecast_date_ts.weekday()))
|
| 33 |
+
|
| 34 |
+
if forecast_date is None:
|
| 35 |
+
forecast_date = forecast_date_ts.strftime('%Y-%m-%d')
|
| 36 |
+
|
| 37 |
+
daily_close = df['close']
|
| 38 |
+
|
| 39 |
+
# Features
|
| 40 |
+
delta = daily_close.diff()
|
| 41 |
+
gain = (delta.where(delta > 0, 0)).rolling(window=2).mean()
|
| 42 |
+
loss = (-delta.where(delta < 0, 0)).rolling(window=2).mean()
|
| 43 |
+
rs = gain / loss
|
| 44 |
+
rsi_2 = 100 - (100 / (1 + rs))
|
| 45 |
+
|
| 46 |
+
sma_3 = daily_close.rolling(window=3).mean()
|
| 47 |
+
dist_sma3 = daily_close / sma_3
|
| 48 |
+
|
| 49 |
+
sma_5 = daily_close.rolling(window=5).mean()
|
| 50 |
+
dist_sma5 = daily_close / sma_5
|
| 51 |
+
|
| 52 |
+
df_eval = pd.DataFrame({
|
| 53 |
+
'close': daily_close,
|
| 54 |
+
'rsi_2': rsi_2,
|
| 55 |
+
'dist_sma3': dist_sma3,
|
| 56 |
+
'dist_sma5': dist_sma5,
|
| 57 |
+
'1d_ret': daily_close.pct_change()
|
| 58 |
+
}).dropna()
|
| 59 |
+
|
| 60 |
+
# Target for historical testing
|
| 61 |
+
df_eval['actual_dir'] = np.where(df_eval['close'].shift(-1) > df_eval['close'], 1, -1)
|
| 62 |
+
|
| 63 |
+
# The last row is TODAY. We don't have tomorrow's close, so actual_dir is wrong for the last row.
|
| 64 |
+
# We test on the 120 days BEFORE today
|
| 65 |
+
test_set = df_eval.iloc[-121:-1]
|
| 66 |
+
today_data = df_eval.iloc[-1]
|
| 67 |
+
|
| 68 |
+
best_acc = 0
|
| 69 |
+
best_rule = None
|
| 70 |
+
|
| 71 |
+
# 1. RSI-2 threshold
|
| 72 |
+
for thresh in [10, 20, 30, 40, 50, 60, 70, 80, 90]:
|
| 73 |
+
for op in ['<', '>']:
|
| 74 |
+
sig = np.where(test_set['rsi_2'] < thresh if op == '<' else test_set['rsi_2'] > thresh, 1, -1)
|
| 75 |
+
acc = (sig == test_set['actual_dir']).mean()
|
| 76 |
+
if acc > best_acc:
|
| 77 |
+
best_acc = acc
|
| 78 |
+
best_rule = ('rsi_2', thresh, op)
|
| 79 |
+
|
| 80 |
+
# 2. SMA distance threshold
|
| 81 |
+
for feature in ['dist_sma3', 'dist_sma5']:
|
| 82 |
+
for thresh in [0.95, 0.98, 1.0, 1.02, 1.05]:
|
| 83 |
+
sig = np.where(test_set[feature] < thresh, 1, -1)
|
| 84 |
+
acc = (sig == test_set['actual_dir']).mean()
|
| 85 |
+
if acc > best_acc:
|
| 86 |
+
best_acc = acc
|
| 87 |
+
best_rule = (feature, thresh, '<')
|
| 88 |
+
|
| 89 |
+
# 3. 1d return
|
| 90 |
+
sig = np.where(test_set['1d_ret'] < 0, 1, -1)
|
| 91 |
+
acc = (sig == test_set['actual_dir']).mean()
|
| 92 |
+
if acc > best_acc:
|
| 93 |
+
best_acc = acc
|
| 94 |
+
best_rule = ('1d_ret', 0, '<')
|
| 95 |
+
|
| 96 |
+
feature, thresh, op = best_rule
|
| 97 |
+
val = today_data[feature]
|
| 98 |
+
|
| 99 |
+
if op == '<':
|
| 100 |
+
prediction = 1 if val < thresh else -1
|
| 101 |
+
else:
|
| 102 |
+
prediction = 1 if val > thresh else -1
|
| 103 |
+
|
| 104 |
+
predictions[ticker] = {
|
| 105 |
+
"prediction": "UP" if prediction == 1 else "DOWN",
|
| 106 |
+
"probability": round(best_acc * 100, 2),
|
| 107 |
+
"rule_used": f"{feature} {op} {thresh}"
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
output = {
|
| 111 |
+
"generated_at": datetime.now().isoformat(),
|
| 112 |
+
"forecast_date": forecast_date,
|
| 113 |
+
"predictions": predictions
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
with open(PREDICTIONS_FILE, "w") as f:
|
| 117 |
+
json.dump(output, f, indent=4)
|
| 118 |
+
|
| 119 |
+
print(f"Generated predictions for {forecast_date}. Saved to {PREDICTIONS_FILE}")
|
| 120 |
+
return output
|
| 121 |
+
|
| 122 |
+
if __name__ == "__main__":
|
| 123 |
+
generate_predictions()
|
predictions.json
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"generated_at": "2026-06-18T16:56:32.759921",
|
| 3 |
+
"forecast_date": "2026-06-16",
|
| 4 |
+
"predictions": {
|
| 5 |
+
"ADANIENT": {
|
| 6 |
+
"prediction": "DOWN",
|
| 7 |
+
"probability": 55.83,
|
| 8 |
+
"rule_used": "rsi_2 < 10"
|
| 9 |
+
},
|
| 10 |
+
"ADANIPORTS": {
|
| 11 |
+
"prediction": "UP",
|
| 12 |
+
"probability": 56.67,
|
| 13 |
+
"rule_used": "dist_sma5 < 1.0"
|
| 14 |
+
},
|
| 15 |
+
"APOLLOHOSP": {
|
| 16 |
+
"prediction": "DOWN",
|
| 17 |
+
"probability": 55.83,
|
| 18 |
+
"rule_used": "rsi_2 > 10"
|
| 19 |
+
},
|
| 20 |
+
"ASIANPAINT": {
|
| 21 |
+
"prediction": "DOWN",
|
| 22 |
+
"probability": 55.83,
|
| 23 |
+
"rule_used": "dist_sma5 < 0.95"
|
| 24 |
+
},
|
| 25 |
+
"AXISBANK": {
|
| 26 |
+
"prediction": "UP",
|
| 27 |
+
"probability": 59.17,
|
| 28 |
+
"rule_used": "rsi_2 > 90"
|
| 29 |
+
},
|
| 30 |
+
"BAJAJ-AUTO": {
|
| 31 |
+
"prediction": "UP",
|
| 32 |
+
"probability": 57.5,
|
| 33 |
+
"rule_used": "rsi_2 < 40"
|
| 34 |
+
},
|
| 35 |
+
"BAJAJFINSV": {
|
| 36 |
+
"prediction": "DOWN",
|
| 37 |
+
"probability": 55.0,
|
| 38 |
+
"rule_used": "rsi_2 < 20"
|
| 39 |
+
},
|
| 40 |
+
"BAJFINANCE": {
|
| 41 |
+
"prediction": "DOWN",
|
| 42 |
+
"probability": 56.67,
|
| 43 |
+
"rule_used": "dist_sma5 < 1.0"
|
| 44 |
+
},
|
| 45 |
+
"BHARTIARTL": {
|
| 46 |
+
"prediction": "DOWN",
|
| 47 |
+
"probability": 55.0,
|
| 48 |
+
"rule_used": "rsi_2 < 20"
|
| 49 |
+
},
|
| 50 |
+
"BPCL": {
|
| 51 |
+
"prediction": "DOWN",
|
| 52 |
+
"probability": 64.17,
|
| 53 |
+
"rule_used": "rsi_2 < 10"
|
| 54 |
+
},
|
| 55 |
+
"BRITANNIA": {
|
| 56 |
+
"prediction": "DOWN",
|
| 57 |
+
"probability": 54.17,
|
| 58 |
+
"rule_used": "rsi_2 < 70"
|
| 59 |
+
},
|
| 60 |
+
"CIPLA": {
|
| 61 |
+
"prediction": "DOWN",
|
| 62 |
+
"probability": 56.67,
|
| 63 |
+
"rule_used": "dist_sma3 < 0.95"
|
| 64 |
+
},
|
| 65 |
+
"COALINDIA": {
|
| 66 |
+
"prediction": "DOWN",
|
| 67 |
+
"probability": 57.5,
|
| 68 |
+
"rule_used": "rsi_2 < 30"
|
| 69 |
+
},
|
| 70 |
+
"DIVISLAB": {
|
| 71 |
+
"prediction": "UP",
|
| 72 |
+
"probability": 57.5,
|
| 73 |
+
"rule_used": "dist_sma5 < 1.0"
|
| 74 |
+
},
|
| 75 |
+
"DRREDDY": {
|
| 76 |
+
"prediction": "UP",
|
| 77 |
+
"probability": 65.0,
|
| 78 |
+
"rule_used": "rsi_2 < 60"
|
| 79 |
+
},
|
| 80 |
+
"EICHERMOT": {
|
| 81 |
+
"prediction": "DOWN",
|
| 82 |
+
"probability": 57.5,
|
| 83 |
+
"rule_used": "rsi_2 < 90"
|
| 84 |
+
},
|
| 85 |
+
"GRASIM": {
|
| 86 |
+
"prediction": "DOWN",
|
| 87 |
+
"probability": 63.33,
|
| 88 |
+
"rule_used": "1d_ret < 0"
|
| 89 |
+
},
|
| 90 |
+
"HCLTECH": {
|
| 91 |
+
"prediction": "UP",
|
| 92 |
+
"probability": 54.17,
|
| 93 |
+
"rule_used": "rsi_2 > 20"
|
| 94 |
+
},
|
| 95 |
+
"HDFCBANK": {
|
| 96 |
+
"prediction": "DOWN",
|
| 97 |
+
"probability": 59.17,
|
| 98 |
+
"rule_used": "dist_sma3 < 0.98"
|
| 99 |
+
},
|
| 100 |
+
"HDFCLIFE": {
|
| 101 |
+
"prediction": "DOWN",
|
| 102 |
+
"probability": 63.33,
|
| 103 |
+
"rule_used": "dist_sma3 < 0.98"
|
| 104 |
+
},
|
| 105 |
+
"HEROMOTOCO": {
|
| 106 |
+
"prediction": "DOWN",
|
| 107 |
+
"probability": 55.0,
|
| 108 |
+
"rule_used": "rsi_2 < 30"
|
| 109 |
+
},
|
| 110 |
+
"HINDALCO": {
|
| 111 |
+
"prediction": "UP",
|
| 112 |
+
"probability": 63.33,
|
| 113 |
+
"rule_used": "dist_sma5 < 1.05"
|
| 114 |
+
},
|
| 115 |
+
"HINDUNILVR": {
|
| 116 |
+
"prediction": "UP",
|
| 117 |
+
"probability": 53.33,
|
| 118 |
+
"rule_used": "rsi_2 > 20"
|
| 119 |
+
},
|
| 120 |
+
"ICICIBANK": {
|
| 121 |
+
"prediction": "DOWN",
|
| 122 |
+
"probability": 55.83,
|
| 123 |
+
"rule_used": "dist_sma5 < 0.98"
|
| 124 |
+
},
|
| 125 |
+
"INDUSINDBK": {
|
| 126 |
+
"prediction": "DOWN",
|
| 127 |
+
"probability": 54.17,
|
| 128 |
+
"rule_used": "rsi_2 < 30"
|
| 129 |
+
},
|
| 130 |
+
"INFY": {
|
| 131 |
+
"prediction": "DOWN",
|
| 132 |
+
"probability": 55.0,
|
| 133 |
+
"rule_used": "dist_sma3 < 0.95"
|
| 134 |
+
},
|
| 135 |
+
"ITC": {
|
| 136 |
+
"prediction": "DOWN",
|
| 137 |
+
"probability": 56.67,
|
| 138 |
+
"rule_used": "dist_sma3 < 0.95"
|
| 139 |
+
},
|
| 140 |
+
"JSWSTEEL": {
|
| 141 |
+
"prediction": "DOWN",
|
| 142 |
+
"probability": 61.67,
|
| 143 |
+
"rule_used": "rsi_2 < 40"
|
| 144 |
+
},
|
| 145 |
+
"KOTAKBANK": {
|
| 146 |
+
"prediction": "DOWN",
|
| 147 |
+
"probability": 53.33,
|
| 148 |
+
"rule_used": "rsi_2 < 10"
|
| 149 |
+
},
|
| 150 |
+
"LTIM": {
|
| 151 |
+
"prediction": "UP",
|
| 152 |
+
"probability": 60.0,
|
| 153 |
+
"rule_used": "1d_ret < 0"
|
| 154 |
+
},
|
| 155 |
+
"LT": {
|
| 156 |
+
"prediction": "DOWN",
|
| 157 |
+
"probability": 58.33,
|
| 158 |
+
"rule_used": "1d_ret < 0"
|
| 159 |
+
},
|
| 160 |
+
"MARUTI": {
|
| 161 |
+
"prediction": "DOWN",
|
| 162 |
+
"probability": 61.67,
|
| 163 |
+
"rule_used": "rsi_2 < 40"
|
| 164 |
+
},
|
| 165 |
+
"MM": {
|
| 166 |
+
"prediction": "DOWN",
|
| 167 |
+
"probability": 55.0,
|
| 168 |
+
"rule_used": "rsi_2 < 30"
|
| 169 |
+
},
|
| 170 |
+
"NESTLEIND": {
|
| 171 |
+
"prediction": "UP",
|
| 172 |
+
"probability": 57.5,
|
| 173 |
+
"rule_used": "rsi_2 < 70"
|
| 174 |
+
},
|
| 175 |
+
"NTPC": {
|
| 176 |
+
"prediction": "UP",
|
| 177 |
+
"probability": 57.5,
|
| 178 |
+
"rule_used": "rsi_2 < 80"
|
| 179 |
+
},
|
| 180 |
+
"ONGC": {
|
| 181 |
+
"prediction": "UP",
|
| 182 |
+
"probability": 58.33,
|
| 183 |
+
"rule_used": "rsi_2 < 40"
|
| 184 |
+
},
|
| 185 |
+
"POWERGRID": {
|
| 186 |
+
"prediction": "DOWN",
|
| 187 |
+
"probability": 55.0,
|
| 188 |
+
"rule_used": "rsi_2 > 70"
|
| 189 |
+
},
|
| 190 |
+
"RELIANCE": {
|
| 191 |
+
"prediction": "DOWN",
|
| 192 |
+
"probability": 55.0,
|
| 193 |
+
"rule_used": "dist_sma5 < 0.98"
|
| 194 |
+
},
|
| 195 |
+
"SBILIFE": {
|
| 196 |
+
"prediction": "DOWN",
|
| 197 |
+
"probability": 60.83,
|
| 198 |
+
"rule_used": "1d_ret < 0"
|
| 199 |
+
},
|
| 200 |
+
"SBIN": {
|
| 201 |
+
"prediction": "UP",
|
| 202 |
+
"probability": 55.83,
|
| 203 |
+
"rule_used": "rsi_2 > 10"
|
| 204 |
+
},
|
| 205 |
+
"SUNPHARMA": {
|
| 206 |
+
"prediction": "UP",
|
| 207 |
+
"probability": 54.17,
|
| 208 |
+
"rule_used": "rsi_2 > 60"
|
| 209 |
+
},
|
| 210 |
+
"TATACONSUM": {
|
| 211 |
+
"prediction": "UP",
|
| 212 |
+
"probability": 57.5,
|
| 213 |
+
"rule_used": "dist_sma3 < 1.0"
|
| 214 |
+
},
|
| 215 |
+
"TATASTEEL": {
|
| 216 |
+
"prediction": "UP",
|
| 217 |
+
"probability": 55.83,
|
| 218 |
+
"rule_used": "rsi_2 < 10"
|
| 219 |
+
},
|
| 220 |
+
"TCS": {
|
| 221 |
+
"prediction": "UP",
|
| 222 |
+
"probability": 57.5,
|
| 223 |
+
"rule_used": "rsi_2 > 20"
|
| 224 |
+
},
|
| 225 |
+
"TECHM": {
|
| 226 |
+
"prediction": "UP",
|
| 227 |
+
"probability": 57.5,
|
| 228 |
+
"rule_used": "dist_sma3 < 1.02"
|
| 229 |
+
},
|
| 230 |
+
"TITAN": {
|
| 231 |
+
"prediction": "DOWN",
|
| 232 |
+
"probability": 55.0,
|
| 233 |
+
"rule_used": "rsi_2 < 40"
|
| 234 |
+
},
|
| 235 |
+
"ULTRACEMCO": {
|
| 236 |
+
"prediction": "DOWN",
|
| 237 |
+
"probability": 57.5,
|
| 238 |
+
"rule_used": "rsi_2 < 50"
|
| 239 |
+
},
|
| 240 |
+
"UPL": {
|
| 241 |
+
"prediction": "DOWN",
|
| 242 |
+
"probability": 60.0,
|
| 243 |
+
"rule_used": "dist_sma5 < 1.0"
|
| 244 |
+
},
|
| 245 |
+
"WIPRO": {
|
| 246 |
+
"prediction": "UP",
|
| 247 |
+
"probability": 60.83,
|
| 248 |
+
"rule_used": "rsi_2 > 10"
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.103.1
|
| 2 |
+
uvicorn==0.23.2
|
| 3 |
+
pandas==2.1.0
|
| 4 |
+
requests==2.31.0
|
| 5 |
+
pandas_market_calendars==4.3.0
|
| 6 |
+
pyarrow==13.0.0
|
| 7 |
+
fastparquet==2023.8.0
|