Skip to content

Repository files navigation

📈 StockWise — AI-Powered Nifty 50 Trading & Research Platform

Paper-trade Indian equities with live prices, ML price forecasts, an AI stock recommender,
and a Groq-powered RAG chat assistant that actually knows your portfolio.

python flask postgres groq sklearn license


Table of Contents


Overview

StockWise is a full-stack paper-trading platform for the Indian stock market (NSE / Nifty 50), built to demonstrate an end-to-end product spanning live market data, classical ML forecasting, a trained ranking model, and a tool-calling LLM agent — all wired into a real Flask backend with persistent Postgres storage.

A user can:

  1. Register/login and browse Nifty 50 stocks with live prices (via yfinance).
  2. View a 6-month or 5-year price forecast with confidence intervals, generated by a gradient-boosted regression model trained per-symbol.
  3. Paper-buy/sell stocks with optional stop-loss / take-profit, tracked automatically by a background scheduler.
  4. Get today's Top-5 AI stock picks, ranked by a trained scikit-learn model over momentum/volatility/RSI/volume features.
  5. Ask a RAG chat agent ("Should I buy more TCS or diversify?") that combines the user's real portfolio + today's recommendations (DB context) with live tool calls (price quotes, top movers, news) via Groq's LPU inference.
  6. Receive SMS / WhatsApp alerts (Twilio) on price triggers, buys, sells, and auto-sell events.

Why StockWise

This project is intentionally built to be interview-defensible — every non-trivial design choice has a documented rationale in the code comments themselves:

Decision Rationale
yfinance as primary price source No API key, no rate limit, works for all .NS/.BO symbols — TwelveData is kept only as an optional fallback.
TF-IDF retrieval instead of embeddings At ~50 stocks / a few hundred short chunks, TF-IDF rebuilt per-request is fast enough to always be fresh (no stale prices) and mirrors the exact interface (retrieve()) you'd swap in FAISS/pgvector for later.
Groq (not OpenAI) for inference Groq's LPU hardware returns completions in a few hundred ms, which matters because the chat feature is embedded in a live trading UI.
HistGradientBoostingRegressor instead of an LSTM TensorFlow-free, Python 3.13-compatible, trains in seconds, and is disk-cached with weekly retraining — no GPU needed for a resume/demo project.
Hard DB-context fallbacks in the chat agent Prevents the classic RAG failure mode where the model says "consult a financial advisor" even though real recommendation/portfolio data was sitting in context unused.
Walk-forward backtesting (no lookahead) Recommendation quality is validated against 40 real trading days rather than trusted blindly — see Benchmarks & Backtests.

Feature Tour

  • 🔐 Auth — session-based login/register, hashed passwords (werkzeug.security, scrypt/pbkdf2).
  • 💹 Live quotes — real-time price, day range, 52-week range for any Nifty 50 symbol.
  • 🔮 ML forecasting — 6-month and 5-year price forecasts with confidence bands + matplotlib chart, base64-embedded.
  • 🧠 AI stock recommender — daily Top-5 picks (rank, target price, predicted gain %, plain-English reason), generated at 9:15 AM IST and SMS-broadcast to all users.
  • 💼 Paper portfolio — buy/sell with stop-loss/take-profit, auto-sell background monitor (every 5 min), realized/unrealized P&L.
  • 💬 RAG chat agent — LangChain + Groq tool-calling agent with 5 live market tools + DB-grounded context.
  • 📊 Portfolio insights — structured JSON risk analysis (risk level, diversification comment, top concern, suggestion).
  • 📰 News digest — retrieval-filtered headlines → LLM sentiment classification (bullish/bearish/neutral/mixed).
  • 🔔 Alerts — SMS + WhatsApp via Twilio, triggered by price thresholds, buys, sells, and auto-sells.
  • ⏱️ Background scheduler — auto-sell checks, daily recommendation generation, closing-price tracking — all on weekday market hours (IST).

Architecture

flowchart TB
    subgraph Client["🖥️ Browser"]
        UI["index.html / auth.html<br/>(vanilla JS + fetch)"]
    end

    subgraph Flask["🐍 Flask App (app.py)"]
        Auth["Auth Routes<br/>/api/register /api/login"]
        Core["Core Routes<br/>/get_current_stock_info<br/>/get_forecast /portfolio/*<br/>/recommendations /set_alert"]
        AIBP["ai_routes.py (Blueprint)<br/>/api/ai/*"]
    end

    subgraph Services["⚙️ Service Layer"]
        ML["ml_model.py<br/>HistGradientBoostingRegressor<br/>forecast engine"]
        REC["recommender.py<br/>Top-5 ranking model +<br/>heuristic scoring"]
        MSG["msg.py<br/>price fetch + Twilio SMS/WhatsApp"]
        SCHED["scheduler.py<br/>background thread loop"]
    end

    subgraph AILayer["🤖 AI / RAG Layer"]
        RAGENGINE["rag_engine.py<br/>TF-IDF retrieval over<br/>DB-derived documents"]
        RAGAGENT["rag_chat_agent.py<br/>LangChain tool-calling agent"]
        TOOLS["market_tools.py<br/>live yfinance + news RSS tools"]
        GROQC["groq_client.py<br/>thin Groq SDK wrapper"]
    end

    subgraph Data["🗄️ Data Layer"]
        DB[("PostgreSQL / Supabase<br/>db.py — connection pool")]
    end

    subgraph External["🌐 External APIs"]
        YF["Yahoo Finance<br/>(yfinance)"]
        TD["TwelveData<br/>(fallback prices)"]
        NEWSAPI["NewsAPI.org"]
        GNEWS["Google News RSS"]
        TWILIO["Twilio<br/>SMS / WhatsApp"]
        GROQ["Groq Cloud<br/>Llama-3.3-70B / 3.1-8B"]
    end

    UI -- "fetch() JSON" --> Auth
    UI -- "fetch() JSON" --> Core
    UI -- "fetch() JSON" --> AIBP

    Auth --> DB
    Core --> ML
    Core --> REC
    Core --> MSG
    Core --> DB
    AIBP --> RAGAGENT
    AIBP --> DB
    AIBP --> GROQC

    ML --> YF
    REC --> YF
    MSG --> YF
    MSG --> TD
    MSG --> TWILIO
    MSG --> NEWSAPI

    RAGAGENT --> RAGENGINE
    RAGAGENT --> TOOLS
    RAGAGENT --> GROQ
    RAGENGINE --> DB
    TOOLS --> YF
    TOOLS --> GNEWS
    GROQC --> GROQ

    SCHED --> MSG
    SCHED --> REC
    SCHED --> DB
Loading

Key architectural traits:

  • Blueprint separation — all GenAI/RAG routes live in ai_routes.py as a Flask Blueprint registered onto the main app, keeping AI concerns out of app.py.
  • Connection poolingdb.py uses a psycopg2.pool.ThreadedConnectionPool(1, 10) (module-level singleton), so every DB function follows the same get_conn()try/finally: release_conn() pattern.
  • Background threads, not Celery — the scheduler (scheduler.py) and background recommendation generation (app.py's _generate_and_save_bg) run as daemon threads inside the same Flask process — simple and sufficient at this scale.
  • In-memory + disk caching everywhere — ML models, forecasts, plots, and "top movers" market data are all TTL-cached to avoid hammering yfinance/DB on every request.

System Workflow Diagrams

1. Authentication Flow

sequenceDiagram
    participant U as User (Browser)
    participant F as Flask (app.py)
    participant D as db.py
    participant PG as PostgreSQL

    U->>F: POST /api/register {username, email, password, phone}
    F->>D: register_user()
    D->>PG: SELECT id FROM users WHERE email=? OR username=?
    alt already exists
        D-->>F: (False, "Email or username already exists.")
        F-->>U: 400 error
    else new user
        D->>D: generate_password_hash(password)
        D->>PG: INSERT INTO users (...) RETURNING id
        D-->>F: (True, {id, username, email, phone})
        F->>F: session["user_id"] = id
        F-->>U: 201 {"message": "Account created!"}
    end

    U->>F: POST /api/login {email, password}
    F->>D: login_user()
    D->>PG: SELECT * FROM users WHERE email=?
    D->>D: check_password_hash(stored_hash, password)
    D-->>F: (True, user) or (False, reason)
    F->>F: session["user_id"], session["username"]
    F-->>U: 200 {"message": "Welcome back, {username}!"}
Loading

2. Buy → Auto-Sell → Alert Flow

sequenceDiagram
    participant U as User
    participant F as Flask (app.py)
    participant M as msg.py
    participant D as db.py
    participant S as scheduler.py (bg thread)
    participant T as Twilio

    U->>F: POST /portfolio/buy {symbol, qty, stop_loss, take_profit}
    F->>M: fetch_current_price(symbol)  [yfinance → TwelveData fallback]
    M-->>F: price
    F->>D: buy_stock(...)  [INSERT portfolio + transactions, same commit]
    D-->>F: portfolio_id
    F->>T: send_alert_sms("Bought...")
    F-->>U: 200 {portfolio_id, buy_price, total, ...}

    loop every 300s (CHECK_INTERVAL)
        S->>D: get_open_positions()
        S->>M: fetch_current_price(symbol) for each position
        S->>D: update_current_price(id, price)
        alt price <= stop_loss OR price >= take_profit
            S->>D: sell_stock(id, price, action="auto_sell")
            S->>T: send_alert_sms("Auto-Sell triggered...")
        end
    end
Loading

3. Daily AI Recommendation Pipeline

flowchart LR
    A["09:15 AM IST<br/>weekday trigger"] --> B["track_daily_prices()"]
    B --> C["load_nifty50()<br/>read companies_india.csv"]
    C --> D["_batch_fetch()<br/>single yf.download() for all symbols"]
    D --> E["_score_from_hist() per symbol<br/>(parallel, ThreadPoolExecutor)"]
    E --> F["momentum + volatility +<br/>volume + gap + RSI features"]
    F --> G{"Trained ranker<br/>model available?"}
    G -- yes --> H["train_ranking_model()<br/>cached HistGradientBoosting ranker"]
    G -- no --> I["_estimate_gain_heuristic()"]
    H --> J["predicted_gain + score per stock"]
    I --> J
    J --> K["Split bullish / bearish<br/>rank by weighted score"]
    K --> L["Top 5 picks"]
    L --> M["save_recommendations()<br/>DELETE + INSERT ai_recommendations"]
    M --> N["_broadcast_recommendations()<br/>SMS to all users with phone numbers<br/>(capped at 100)"]
Loading

4. RAG Chat Agent — Tool-Calling Loop

flowchart TD
    Q["User question<br/>e.g. 'Should I buy more TCS?'"] --> DBX["_build_db_context()"]
    DBX --> RET["rag_engine.get_context()<br/>TF-IDF retrieval over<br/>company/recommendation/price/portfolio docs"]
    DBX --> PICK{"Contains pick keywords?<br/>'best stock', 'should i buy'..."}
    DBX --> PORT{"Contains portfolio keywords?<br/>'my portfolio', 'diversified'..."}
    PICK -- yes --> RECO["+ get_recommendation_context()<br/>hard bypass of TF-IDF"]
    PORT -- yes --> PF["+ get_portfolio_context()<br/>hard bypass of TF-IDF"]

    RET --> SYS["SystemMessage: SYSTEM_PROMPT + DB_CONTEXT"]
    RECO --> SYS
    PF --> SYS
    SYS --> LLM["ChatGroq (llama-3.1-8b-instant)<br/>.bind_tools([...])"]

    LLM --> DECIDE{"Model requests<br/>a tool call?"}
    DECIDE -- yes --> TOOLCALL["Invoke market_tools.py:<br/>get_stock_quote / get_top_movers /<br/>get_stock_news / get_market_news /<br/>read_full_article"]
    TOOLCALL --> APPEND["Append ToolMessage(result)<br/>to conversation"]
    APPEND --> LLM
    DECIDE -- no, final answer --> OUT["Return ai_msg.content<br/>to /api/ai/chat"]

    style OUT fill:#2f9e44,color:#fff
Loading

5. Forecast Generation (ml_model.py)

flowchart LR
    A["GET /get_forecast?symbol=&forecast_type="] --> B{"Cached model<br/>< 7 days old?"}
    B -- fresh --> C["Load bundle from disk<br/>(joblib) / memory cache"]
    B -- stale/missing --> D["fetch_stock_data()<br/>yfinance history"]
    D --> E["_build_training_matrix()<br/>lag features + rolling stats,<br/>LOOKBACK=60 days"]
    E --> F["Pipeline(StandardScaler +<br/>HistGradientBoostingRegressor)"]
    F --> G["_train_and_save()<br/>persist to model_cache/"]
    C --> H["_forecast_days()<br/>recursive multi-step forecast"]
    G --> H
    H --> I["_ci()<br/>confidence interval via<br/>residual std * z-score"]
    I --> J["generate_stock_plot()<br/>matplotlib → base64 PNG"]
    J --> K["JSON: dates, yhat,<br/>yhat_lower, yhat_upper, plot_img"]
Loading

Tech Stack

Layer Technology
Backend framework Flask (blueprints, sessions)
Database PostgreSQL (Supabase-hosted or local), psycopg2 pooled connections
Auth Flask sessions + werkzeug.security password hashing
Market data yfinance (primary), TwelveData (optional fallback), Google News RSS
Forecasting ML scikit-learnHistGradientBoostingRegressor in a Pipeline with StandardScaler
Recommendation ranking scikit-learn trained ranker + heuristic fallback, feature engineering with numpy/pandas
RAG retrieval scikit-learn TfidfVectorizer + cosine similarity (no vector DB needed at this scale)
LLM inference Groq Cloud — llama-3.3-70b-versatile (explain/insights/news) & llama-3.1-8b-instant (chat agent)
Agent framework LangChain (langchain-core, langchain-groq) tool-calling
Notifications Twilio (SMS + WhatsApp)
Charting matplotlib (server-rendered, base64-embedded PNGs)
Frontend Vanilla HTML/CSS/JS (index.html, auth.html), served via Jinja templates
Scheduling Native Python threading daemon loop (IST-aware via pytz)

Repository Structure

stockwiseAI/
├── app.py                 # Main Flask app — auth, portfolio, forecast, recommendation routes
├── ai_routes.py            # Flask Blueprint — /api/ai/* GenAI + RAG endpoints
├── db.py                   # Postgres connection pool + all DB CRUD operations
├── ml_model.py              # Per-symbol price forecasting model (train/cache/predict/plot)
├── recommender.py           # Nifty 50 scoring + trained ranking model → Top-5 picks
├── rag_engine.py            # TF-IDF document index + retrieval + hard-fallback context builders
├── rag_chat_agent.py        # LangChain + Groq tool-calling chat agent (system prompt + loop)
├── market_tools.py          # LangChain @tool live functions: quotes, movers, news, article reader
├── groq_client.py           # Thin wrapper around the Groq Chat Completions API
├── groq_check.py            # Standalone latency benchmark script for Groq calls
├── msg.py                   # Price fetching (yfinance/TwelveData) + Twilio SMS/WhatsApp senders
├── scheduler.py             # Background thread: auto-sell checks + daily recommendation job
├── companies_india.csv      # Nifty 50 symbol → company name reference data
├── templates/
│   ├── index.html           # Main authenticated app UI
│   └── auth.html            # Login / register UI
├── model_cache/              # Disk-cached per-symbol forecast models (joblib), gitignored
├── .env                      # Local secrets (never committed)
└── requirements.txt

Database Schema

Hosted on Supabase (managed Postgres) or any local PostgreSQL instance. Tables are inferred from the queries in db.py, recommender.py, and rag_engine.py.

erDiagram
    USERS ||--o{ PORTFOLIO : owns
    USERS ||--o{ USER_ALERTS : subscribes
    PORTFOLIO ||--o{ TRANSACTIONS : generates
    AI_RECOMMENDATIONS }o--|| DAILY_PRICES : "derived from"

    USERS {
        int id PK
        string username
        string email
        string password_hash
        string phone_number
        timestamp created_at
    }
    PORTFOLIO {
        int id PK
        int user_id FK
        string stock_symbol
        string company_name
        float quantity
        float buy_price
        float current_price
        float stop_loss
        float take_profit
        string status "open | sell | auto_sell"
        string phone_number
        float sell_price
        float pnl
        timestamp bought_at
        timestamp sold_at
    }
    TRANSACTIONS {
        int id PK
        int portfolio_id FK
        string action "buy | sell | auto_sell"
        string stock_symbol
        float quantity
        float price
        float total_value
        string note
    }
    USER_ALERTS {
        int id PK
        int user_id FK
        string stock_symbol
        string phone_number
        bool is_active
    }
    AI_RECOMMENDATIONS {
        int id PK
        date date
        string stock_symbol
        string company_name
        float score
        float predicted_gain
        float current_price
        float target_price
        string reason
        int rank
        timestamp created_at
    }
    DAILY_PRICES {
        int id PK
        string stock_symbol
        date date
        float open_price
        float close_price
        float pct_change
    }
Loading

Design notes:

  • ai_recommendations has no unique constraintsave_recommendations() explicitly DELETEs the current day's rows before inserting, keeping "today's Top 5" idempotent per rank.
  • portfolio.status doubles as the transaction-type marker (open / sell / auto_sell), avoiding a separate status/reason column.
  • Every write path (buy_stock, sell_stock, save_recommendations) wraps its INSERTs in a single transaction with commit()/rollback() on exception.

The ML Layer

Price Forecasting (ml_model.py)

  • No TensorFlow / Keras — deliberately swapped for sklearn.ensemble.HistGradientBoostingRegressor inside a Pipeline([StandardScaler, Regressor]) for Python 3.13 compatibility and fast CPU-only training.
  • Feature engineering: 60-day lookback window (LOOKBACK = 60), lag features + rolling statistics built by _make_features_from_window().
  • Recursive multi-step forecasting: _forecast_days() predicts one step ahead, feeds that prediction back into the window, and repeats for the requested horizon (6 months or 5 years).
  • Confidence intervals: _ci() derives bounds from the residual standard deviation of the training fit scaled by a z-score (z=1.65 ≈ 90% band).
  • Three-tier caching:
    1. In-memory (MODEL_MEMORY_CACHE, DATA_CACHE, FORECAST_CACHE, PLOT_CACHE) with TTLs (10 min for data/forecast/plot).
    2. Disk cache (joblib) under model_cache/, refreshed weekly (MODEL_EXPIRY_DAYS = 7).
    3. Background retraining_spawn_training() kicks off retraining in a thread so a request can still serve a stale-but-usable model immediately rather than blocking on a fresh train.
  • Forecast output is currency-aware ( for .NS/.BO, $ otherwise) and rendered server-side as a base64 PNG via matplotlib.

Stock Recommender / Ranker (recommender.py)

  • Universe: Nifty 50 symbols loaded from companies_india.csv.
  • Batch fetch: a single yf.download() call across all symbols (_batch_fetch) instead of one request per symbol — avoids rate-limiting and is dramatically faster.
  • Feature set (_extract_ranking_features): momentum, volatility, volume trend, opening-gap behavior, and 14-period RSI (_rsi).
  • Two scoring paths:
    • A trained ranking model (train_ranking_model(), cached and refreshed like the forecast models) when available.
    • A heuristic fallback (_estimate_gain_heuristic) when no trained model exists yet, so the feature is never fully unavailable.
  • Ranking logic: candidates are split into bullish (predicted_gain > 0) vs. bearish; if ≥5 bullish candidates exist, only bullish stocks are ranked by score * 0.6 + min(predicted_gain * 10, 40). If the market is broadly bearish, the "least-bad" options are surfaced instead of returning nothing.
  • Persistence: top 5 written to ai_recommendations, idempotent per day via delete-then-insert.

The AI / RAG Layer

1. rag_engine.py — Retrieval

Builds a fresh TF-IDF index per request over four document types generated live from the DB and CSV data:

Doc type Source
company companies_india.csv — symbol/name facts
recommendation Today's AI picks (rank, targets, reason)
price_history Last 5 days of OHLC from daily_prices
portfolio The logged-in user's actual open/closed positions (only if user_id provided)

Retrieval (retrieve()) returns the top-k cosine-similarity hits, but guarantees at least min_reco=3 recommendation docs are present even if their raw similarity score is low — this fixes a real bug where generic phrasing like "best stock for short term purchase" scored too low to surface any recommendation text.

Two hard-fallback helpers bypass retrieval entirely and are always available to the chat agent regardless of TF-IDF score:

  • get_recommendation_context() — today's Top-5 picks, verbatim.
  • get_portfolio_context() — the user's real open/closed positions + summary.

2. rag_chat_agent.py — Tool-Calling Agent

  • Built with LangChain + ChatGroq, model llama-3.1-8b-instant, .bind_tools(TOOLS).
  • SYSTEM_PROMPT is an explicit, exhaustive policy document covering: mandatory tool usage triggers (price/news/movers keywords), portfolio-question handling, when not to use tools (static financial education), and — critically — what to do if a live tool fails (fall back to DB_CONTEXT, name a real stock, mention the gap as a one-sentence caveat, never say only "consult a financial advisor").
  • _build_db_context() scans the query for keyword sets (PICK_KEYWORDS, PORTFOLIO_KEYWORDS) to decide whether to force-inject recommendation/portfolio context.
  • answer() runs a bounded tool-calling loop (max_tool_rounds=6): call the LLM → if it requests tools, execute them and append ToolMessages → repeat until a final text answer, with a hard "answer now with what you have" fallback if the round limit is hit.

3. market_tools.py — Live Tools

Five LangChain @tool-decorated functions the agent can call on demand:

Tool Purpose
get_top_movers(direction, n) Today's top N gainers/losers across the tracked NSE universe (batched yf.download, 5-min cache)
get_stock_quote(symbol) Live price, day range, 52-week range for one symbol — resolves natural-language company names via COMPANY_MAP
get_stock_news(symbol, n) Latest headlines for a specific stock via yfinance's news feed
get_market_news(query, n) General market headlines via Google News RSS (no API key required)
read_full_article(url) Fetches and extracts full article text (via WebBaseLoader) when a headline alone doesn't answer the question

4. groq_client.py — LLM Wrapper

A minimal wrapper around groq.Groq().chat.completions.create() used by the non-agentic AI routes (/explain, /portfolio-insights, /news-digest) — supports temperature, max_tokens, and Groq's OpenAI-style json_mode for guaranteed-valid-JSON structured output. Wraps all failures in a RuntimeError with a clear message so routes can show a friendly "AI temporarily unavailable" response instead of a raw 500.


API Reference

All routes return JSON. Routes marked 🔒 require an active session (login_required).

Auth

Method Route Body / Params Description
GET /login Renders login page
GET /register Renders register page
POST /api/register username, email, password, phone? Creates account, starts session
POST /api/login email, password Authenticates, starts session
POST /api/logout Clears session
GET /api/me Returns current session user info

Core 🔒

Method Route Params Description
GET / Main authenticated app page
GET /get_current_stock_info symbol Live price + company name
GET /get_forecast symbol, forecast_type=6m|5y ML forecast + confidence bands + plot
POST /set_alert stock, phone Registers a price alert + sends confirmation/first-check SMS
POST /portfolio/buy symbol, quantity, stop_loss?, take_profit?, phone? Opens a paper position
POST /portfolio/sell portfolio_id Closes a position at current live price
GET /portfolio All positions (open + closed) with unrealized/realized P&L
GET /recommendations Today's Top-5 AI picks; auto-triggers background generation if missing
POST /recommendations/refresh broadcast?: bool Force-regenerates recommendations (background thread)
POST /recommendations/broadcast Manually re-sends today's picks via SMS
GET /health Liveness check

AI / GenAI (ai_routes.py, prefix /api/ai) 🔒 (except news-digest)

Method Route Body / Params Description
POST /api/ai/chat message RAG + tool-calling chat over live data & portfolio
GET /api/ai/explain/<symbol> 3–4 sentence plain-English rationale for today's recommendation
GET /api/ai/portfolio-insights Structured JSON: risk_level, diversification_comment, top_concern, suggestion
GET /api/ai/news-digest/<symbol> company? Retrieval-filtered headlines → JSON {sentiment, summary, headlines}

Example — chat:

curl -X POST http://localhost:8080/api/ai/chat \
  -H "Content-Type: application/json" \
  -b "session=<cookie>" \
  -d '{"message": "Should I buy more TCS or diversify into something else?"}'
{ "answer": "Your only open position is 10 shares of TCS... TCS is up slightly today per live data... Given your current concentration, consider diversifying rather than adding more TCS... This is AI-generated analysis, not financial advice." }

Example — portfolio insights:

curl http://localhost:8080/api/ai/portfolio-insights -b "session=<cookie>"
{
  "insight": {
    "risk_level": "Medium",
    "diversification_comment": "Holdings are concentrated in IT services.",
    "top_concern": "No exposure outside the technology sector.",
    "suggestion": "Consider adding a position in a different sector, such as banking or FMCG."
  }
}

Getting Started

Prerequisites

  • Python 3.13
  • A PostgreSQL database (Supabase free tier works well) or local Postgres
  • A free Groq API key
  • (Optional) Twilio account for SMS/WhatsApp alerts
  • (Optional) NewsAPI.org key for the news-digest feature

Installation

git clone https://github.com/<your-username>/stockwiseAI.git
cd stockwiseAI

python -m venv venv
# Windows
venv\Scripts\activate
# macOS/Linux
source venv/bin/activate

pip install -r requirements.txt

requirements.txt should include, at minimum:

flask
python-dotenv
psycopg2-binary
werkzeug
yfinance
pandas
numpy
scikit-learn
joblib
matplotlib
requests
twilio
groq
langchain-core
langchain-groq
langchain-community
pytz

Database setup

Create the tables described in Database Schema in your Postgres instance (Supabase SQL editor or psql), then point the app at it via DATABASE_URL (see below).


Environment Variables

Create a .env file in the project root:

# ── Database (choose ONE style) ──────────────────────────────
DATABASE_URL=postgresql://user:pass@host:5432/dbname     # Supabase / cloud
# — or —
DB_HOST=localhost
DB_PORT=5432
DB_NAME=stockwise_db
DB_USER=postgres
DB_PASSWORD=
DB_SSLMODE=require

# ── Flask ─────────────────────────────────────────────────────
FLASK_SECRET_KEY=change_me_in_production!
PORT=8080

# ── LLM (Groq) ────────────────────────────────────────────────
GROQ_API_KEY=gsk_...
GROQ_MODEL=llama-3.3-70b-versatile

# ── Market data fallback (optional) ──────────────────────────
TWELVE_DATA_KEY=

# ── News (optional — used by /api/ai/news-digest and alert news) ─
NEWS_API_KEY=

# ── Twilio (optional — SMS/WhatsApp alerts) ──────────────────
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_SMS_NUMBER=
TWILIO_WHATSAPP_NUMBER=whatsapp:+14155238886

# ── ML model caching ──────────────────────────────────────────
MODEL_CACHE_DIR=model_cache
MODEL_EXPIRY_DAYS=7
RANKER_MODEL_DIR=ranker_cache
RANKER_EXPIRY_DAYS=7

# ── Scheduler ─────────────────────────────────────────────────
AUTO_SELL_INTERVAL=300
Variable Required Notes
DATABASE_URL or DB_* db.py prefers DATABASE_URL and force-appends sslmode=require if missing
FLASK_SECRET_KEY ✅ (prod) Falls back to an insecure default — must be overridden in production
GROQ_API_KEY ✅ (for AI features) Free tier at console.groq.com
TWELVE_DATA_KEY Only used if yfinance fails
NEWS_API_KEY Without it, /api/ai/news-digest returns a 400 and alert SMS skips news enrichment
TWILIO_* Without it, SMS/WhatsApp sends are logged and skipped, not fatal

Running the App

python app.py
  • Runs on http://localhost:8080 by default (PORT env var to override).
  • debug=True, use_reloader=Falsethe reloader is intentionally disabled because Flask's file-watcher detects model-cache writes mid-training and restarts the process, killing in-progress model saves.
  • The background scheduler (scheduler.start()) launches automatically on import — auto-sell checks run every AUTO_SELL_INTERVAL seconds (default 300s), and market jobs (recommendations at 9:15 AM IST, closing-price tracking at 3:30 PM IST) run only on weekdays.

Standalone latency benchmark

pip install groq --break-system-packages
export GROQ_API_KEY=gsk_...
python groq_check.py --n 20

Benchmarks real Groq API latency in isolation from Flask/DB overhead, across three realistic prompt shapes pulled from the actual routes (short = /explain, medium = /portfolio-insights, agentic = chat-agent-style system prompt).


Benchmarks & Backtests

Groq inference latency (measured, llama-3.3-70b-versatile, n=20 per shape)

Prompt shape Mean p50 p95 p99 tok/s
short (/explain, ~150 input tokens) 726 ms 712 ms 1044 ms 1053 ms 193
medium (/portfolio-insights, ~300 tokens) 1513 ms 1518 ms 2617 ms 2627 ms 130
agentic (chat-style, ~900 tokens) 3238 ms 3252 ms 3565 ms 3630 ms 79

"Benchmarked 60 live Groq API calls across 3 prompt shapes on Llama-3.3-70B: p50 latency of 712–3252ms, p95 under 3565ms, ~134 tok/s throughput."

Note on the medium run: calls 11–20 show a distinct step up (~2.4–2.6s vs. ~0.4–0.6s for calls 1–10) — consistent with a mid-run change in Groq-side queueing/load rather than the client. Worth re-running if you need a tight SLA number.

Recommender walk-forward backtest (40 real NSE trading days, no lookahead bias)

Metric Result
Directional accuracy 62.5%
Mean Absolute Error 1.01 pct points
Predicted-vs-actual correlation 0.277
Target price hit rate 54.0%
Avg daily Top-5 return +0.197%
Avg daily market baseline −0.058%
Cumulative Top-5 return (40 days) +7.87%
Cumulative market baseline (40 days) −2.31%

"Walk-forward backtested AI stock recommender against 40 real NSE trading days with no lookahead bias: 62% directional accuracy, +10.2pp cumulative excess return vs. Nifty-50 baseline, 1.01pp MAE."

Known data gap

TATAMOTORS.NS returns 404 — Quote not found from Yahoo Finance in current logs (both .NS and fallback TwelveData, which requires a paid plan for that symbol). The recommender/backtest correctly runs on the remaining 49/50 symbols and logs the miss rather than failing silently.


Security Notes

  • Passwords are hashed with werkzeug.security.generate_password_hash (scrypt/pbkdf2 with per-password random salt) — plaintext is never persisted, and password_hash is explicitly stripped from any object returned to the caller after login.
  • SQL is 100% parameterized (%s placeholders via psycopg2) — no string-interpolated queries anywhere in db.py, recommender.py, or rag_engine.py.
  • Sessions are Flask's signed cookie sessions — FLASK_SECRET_KEY must be set to a strong random value outside local dev; the code ships an intentionally obvious insecure default ("change_me_in_production!") to make this impossible to miss.
  • Ownership checkssell_stock() and portfolio_sell() scope updates to user_id when provided, preventing one user from closing another's position by guessing a portfolio_id.
  • LLM output isolation — the chat agent is explicitly instructed never to invent portfolio holdings or prices, and the system prompt bans exposing tool internals or emitting raw JSON in the conversational chat route.

Known Limitations & Roadmap

  • TF-IDF retrieval is rebuilt on every RAG request — fine at ~50 stocks, but the interface is already vector-DB-shaped (retrieve()) for a future FAISS/pgvector swap without touching callers.
  • TATAMOTORS.NS currently has no working price source (Yahoo delisting flag + TwelveData plan restriction) — needs a tertiary fallback or symbol correction.
  • SMS broadcast is hard-capped at 100 recipients per run to protect Twilio credits — needs pagination/queueing for larger user bases.
  • No automated test suite currently checked in (test_backtest.py exists as a manual validation script, not pytest-wired CI).
  • Add rate limiting on /api/ai/* routes to bound Groq spend per user.
  • Move the background scheduler out of the Flask process into a proper worker (Celery/RQ) for horizontal scalability.

Contributing

  1. Fork the repo and create a feature branch: git checkout -b feature/my-feature
  2. Keep DB functions following the existing conn = cur = Nonetry/except/finally: release_conn() pattern in db.py.
  3. Any new AI route should go in ai_routes.py and use groq_client.chat() (or extend rag_chat_agent.py for tool-calling behavior) rather than calling the Groq SDK directly.
  4. Open a PR with a clear description of behavior change and, where relevant, before/after benchmark numbers (see groq_check.py for the pattern).

License

MIT — see LICENSE for details.


Built as a full end-to-end demonstration of live-data ML, RAG, and tool-calling LLM agents in a real trading-adjacent product.

About

Full-stack paper-trading platform for the Indian stock market (Nifty 50) with live prices, ML price forecasting, a trained stock ranker, and a tool-calling RAG chat agent — Flask + Postgres + Groq.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages