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.
- Overview
- Why StockWise
- Feature Tour
- Architecture
- System Workflow Diagrams
- Tech Stack
- Repository Structure
- Database Schema
- The ML Layer
- The AI / RAG Layer
- API Reference
- Getting Started
- Environment Variables
- Running the App
- Benchmarks & Backtests
- Security Notes
- Known Limitations & Roadmap
- Contributing
- License
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:
- Register/login and browse Nifty 50 stocks with live prices (via
yfinance). - View a 6-month or 5-year price forecast with confidence intervals, generated by a gradient-boosted regression model trained per-symbol.
- Paper-buy/sell stocks with optional stop-loss / take-profit, tracked automatically by a background scheduler.
- Get today's Top-5 AI stock picks, ranked by a trained scikit-learn model over momentum/volatility/RSI/volume features.
- 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.
- Receive SMS / WhatsApp alerts (Twilio) on price triggers, buys, sells, and auto-sell events.
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. |
- 🔐 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).
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
Key architectural traits:
- Blueprint separation — all GenAI/RAG routes live in
ai_routes.pyas a Flask Blueprint registered onto the main app, keeping AI concerns out ofapp.py. - Connection pooling —
db.pyuses apsycopg2.pool.ThreadedConnectionPool(1, 10)(module-level singleton), so every DB function follows the sameget_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.
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}!"}
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
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)"]
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
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"]
| 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-learn — HistGradientBoostingRegressor 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) |
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
Hosted on Supabase (managed Postgres) or any local PostgreSQL instance. Tables are inferred from the queries in
db.py,recommender.py, andrag_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
}
Design notes:
ai_recommendationshas no unique constraint —save_recommendations()explicitlyDELETEs the current day's rows before inserting, keeping "today's Top 5" idempotent per rank.portfolio.statusdoubles 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 itsINSERTs in a single transaction withcommit()/rollback()on exception.
- No TensorFlow / Keras — deliberately swapped for
sklearn.ensemble.HistGradientBoostingRegressorinside aPipeline([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:
- In-memory (
MODEL_MEMORY_CACHE,DATA_CACHE,FORECAST_CACHE,PLOT_CACHE) with TTLs (10 min for data/forecast/plot). - Disk cache (
joblib) undermodel_cache/, refreshed weekly (MODEL_EXPIRY_DAYS = 7). - 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.
- In-memory (
- Forecast output is currency-aware (
₹for.NS/.BO,$otherwise) and rendered server-side as a base64 PNG viamatplotlib.
- 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.
- A trained ranking model (
- Ranking logic: candidates are split into bullish (
predicted_gain > 0) vs. bearish; if ≥5 bullish candidates exist, only bullish stocks are ranked byscore * 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.
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.
- Built with LangChain +
ChatGroq, modelllama-3.1-8b-instant,.bind_tools(TOOLS). SYSTEM_PROMPTis 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 appendToolMessages → repeat until a final text answer, with a hard "answer now with what you have" fallback if the round limit is hit.
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 |
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.
All routes return JSON. Routes marked 🔒 require an active session (login_required).
| 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 |
| 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 |
| 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."
}
}- 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
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.txtrequirements.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
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).
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 |
python app.py- Runs on
http://localhost:8080by default (PORTenv var to override). debug=True, use_reloader=False— the 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 everyAUTO_SELL_INTERVALseconds (default 300s), and market jobs (recommendations at 9:15 AM IST, closing-price tracking at 3:30 PM IST) run only on weekdays.
pip install groq --break-system-packages
export GROQ_API_KEY=gsk_...
python groq_check.py --n 20Benchmarks 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).
| 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.
| 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."
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.
- Passwords are hashed with
werkzeug.security.generate_password_hash(scrypt/pbkdf2 with per-password random salt) — plaintext is never persisted, andpassword_hashis explicitly stripped from any object returned to the caller after login. - SQL is 100% parameterized (
%splaceholders viapsycopg2) — no string-interpolated queries anywhere indb.py,recommender.py, orrag_engine.py. - Sessions are Flask's signed cookie sessions —
FLASK_SECRET_KEYmust 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 checks —
sell_stock()andportfolio_sell()scope updates touser_idwhen provided, preventing one user from closing another's position by guessing aportfolio_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.
- 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.NScurrently 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.pyexists as a manual validation script, notpytest-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.
- Fork the repo and create a feature branch:
git checkout -b feature/my-feature - Keep DB functions following the existing
conn = cur = None→try/except/finally: release_conn()pattern indb.py. - Any new AI route should go in
ai_routes.pyand usegroq_client.chat()(or extendrag_chat_agent.pyfor tool-calling behavior) rather than calling the Groq SDK directly. - Open a PR with a clear description of behavior change and, where relevant, before/after benchmark numbers (see
groq_check.pyfor the pattern).
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.