AI-assisted investment analysis for the Pakistan Stock Exchange.
BullBearPK takes a user's budget, sector preference, risk tolerance, time horizon and investment goal, runs it through a ten-stage agent pipeline against real PSX market data, and returns sized, reasoned buy/sell/hold recommendations — then executes whatever the user decides through a single, transactional portfolio-management path.
This README describes what the system actually does today, not an aspirational roadmap. Known gaps are called out explicitly in Current limitations rather than smoothed over.
- What it does
- Architecture
- Agentic pipeline
- Tech stack
- Data model
- Getting started
- Running with Docker
- Testing & CI
- Current limitations
- Contributing
- License
- A user submits an investment profile (budget, sector preference, risk tolerance, time horizon, target profit) from the React frontend.
- The backend hands that profile to a LangGraph-orchestrated pipeline of ten agents, which fetches live PSX data (with a database fallback if the feed is down), computes technical indicators from real accumulated history, gathers and scores news sentiment, checks the user's risk profile, past decisions, and current portfolio, then sizes and reasons about a set of recommendations — the reasoning is written by a real LLM call (Groq) grounded in the computed numbers, with a template fallback if no API key is configured.
- Whatever the user decides (buy / sell / hold) is executed through one transactional code
path (
portfolio_manager.py): cash is checked and deducted atomically, sells target specific lots FIFO, and nothing is written on a partial failure. - A separate, offline backtester validates whether the recommendation logic actually correlates with what stocks did afterward, using the same indicator math that runs in production — not a parallel, unverified implementation.
The frontend never talks to MySQL directly. The Flask API is the only thing that does, and
the agentic core is invoked as a single call from the /api/hybrid route — it is not a
separate service. Standalone diagram source: Workflow/bullbearpk-architecture.svg.
| # | Agent | Responsibility | Source |
|---|---|---|---|
| 1 | Input Taker | Validates budget, risk tolerance, time horizon, sector preference | backend/agents/input_taker.py |
| 2 | Stock Scraper | Live PSX daily-activity data via a direct JSON feed; falls back to the last known DB snapshot, never fabricated prices | backend/agents/fin_scraper.py |
| 3 | Stock Analyzer | RSI, MACD, moving averages, Bollinger Bands computed from real accumulated stock_history |
backend/agents/advanced_stock_analyzer.py |
| 4 | News Scraper | Fetches news only for top movers without an already-fresh (<6h) article set | backend/agents/news_scraper.py |
| 5 | News Analyzer | Sentiment scoring with confidence gating and recency decay | backend/agents/news_analyzer.py |
| 6 | Risk Checker | Quantifies the user's risk profile | backend/agents/risk_checker.py |
| 7 | Past Investments Checker | Reviews this user's prior decisions | backend/agents/past_investments_checker.py |
| 8 | Portfolio Checker | Reads current holdings, cash and diversification at live prices | backend/agents/portfolio_checker.py |
| 9 | Recommendation Agent | Sizes positions by risk tier and budget; Groq LLM writes the reasoning (template fallback if unset) | backend/agents/recommendation_agent.py |
| 10 | Manager Record Agent | Validates and delegates the user's decision to the Portfolio Manager | backend/agents/manager_record_agent.py |
Backtester (backend/agents/backtester.py) sits outside the live request path. It reuses
the Stock Analyzer's real indicator math to walk forward through accumulated history with no
lookahead bias, and to check past recommendations against what actually happened — reporting
sample size honestly rather than presenting a handful of days as a proven track record.
Every stage records why it degraded (stale cache, empty upstream data) instead of silently
substituting fabricated numbers behind a success: true response — see the warnings array
returned alongside each recommendation.
| Layer | Choices |
|---|---|
| Frontend | React 18, Vite, TypeScript, Zustand, Tailwind CSS, Recharts, Framer Motion, Axios |
| Backend | Flask, Gunicorn, JWT auth + bcrypt, Flask-Limiter, mysql-connector-python (pooled) |
| Agentic orchestration | LangGraph state machine (see Agentic pipeline) |
| LLM | Groq (chat assistant + recommendation reasoning); optional — every LLM-backed feature has a real, working fallback |
| Data acquisition | Direct JSON feed for PSX data (no browser automation), RSS/feedparser + BeautifulSoup for news, TextBlob for sentiment |
| Database | MySQL 8, schema-file-driven with a real tracked migration runner |
| Testing | pytest (backend), Vitest (frontend) |
| CI/CD | GitHub Actions |
| Deployment | Docker + Docker Compose |
16 tables, defined in backend/database/mysql_schema.sql
— the single source of truth for the schema (init_database.py applies this file directly;
nothing hand-maintains a parallel copy of it).
| Table | Holds |
|---|---|
users |
Identity, risk tolerance, cash balance, portfolio totals |
stocks / stock_history |
Latest snapshot per PSX ticker, and one real row per stock per trading day for indicator math |
stock_analysis |
Computed technical indicators per stock |
news_records / news_analysis |
Raw scraped articles and their sentiment scores |
portfolios / investments |
Portfolio summaries and individual holdings |
recommendations / user_recommendations_history |
Generated recommendations and the user-facing history of them |
user_form_submissions |
Each investment-profile submission, for returning-user comparisons |
user_feedback |
Ratings/feedback on recommendations |
user_settings |
Per-user preferences |
ai_chat_messages |
Real chat history for the AI assistant |
market_summary |
Daily index-level summary (KSE-100 etc.) |
system_logs |
Structured application log records |
schema_migrations |
Tracks which migration_*.sql files have been applied (created by database/migrate.py) |
Evolving the schema: add a new backend/database/migration_*.sql file, then run
python backend/database/migrate.py. It applies anything not yet recorded as run, rolls
back cleanly if a migration fails partway through, and --status lists applied/pending
without changing anything. mysql_schema.sql already contains every existing migration's
effects, so a fresh database gets them all marked as applied via --baseline without
re-running them (init_database.py does this automatically).
- Python 3.11+
- Node.js 20+
- MySQL 8.0+
cd backend
python -m venv .venv
.venv\Scripts\activate # Windows; use `source .venv/bin/activate` on Linux/macOS
pip install -r requirements.txtCopy .env.example (repo root) to .env and fill in real values — at minimum DB_PASSWORD
and SECRET_KEY. GROQ_API_KEY is optional; without it, the chat assistant returns 503 and
recommendations fall back to template-based reasoning.
python init_database.py # applies mysql_schema.sql and marks it as the baseline
python api_server.py # dev server on :5000cd frontend
npm install
npm run dev # dev server on :3001, proxies /api to :5000Build-time API location is configurable via VITE_API_BASE_URL / VITE_WEBSOCKET_URL
(see frontend/src/constants/index.ts) — required for any deployment where the browser and
backend aren't on the same machine. Defaults to localhost:5000 for local dev.
Runs MySQL, Redis, the backend, and the frontend together, with the schema loaded automatically on first start against an empty volume.
cp .env.example .env
# edit .env: set SECRET_KEY and DB_PASSWORD at minimum; GROQ_API_KEY is optional
docker compose up -d --build- Frontend: http://localhost:3001
- Backend API: http://localhost:5000 (health check at
/) - MySQL:
localhost:3306, for connecting a GUI client — not needed by the app itself - Redis: backs shared rate-limit counters across the backend's gunicorn workers (see
backend/rate_limiter.py) — not exposed on a host port, nothing else needs it
All four services define real Docker healthchecks — frontend won't start until backend
reports healthy, and backend won't start until both mysql and redis do. docker compose ps shows each service's health status.
docker compose down stops everything; add -v to also delete the MySQL data volume.
Schema changes after the first run must be applied manually via
backend/database/migrate.py — the compose file only loads mysql_schema.sql once.
# Backend
cd backend
pip install -r requirements-dev.txt
python -m pytest -v --capture=no # --capture=no works around a pytest capture-teardown
# crash triggered by this project's heavy transitive
# deps (see .github/workflows/ci.yml for details)
# Frontend
cd frontend
npx vitest run # unit tests
npx tsc --noEmit # type check
npx vite build # production bundleGitHub Actions runs backend tests against a real MySQL service container, then type-checks
and builds the frontend — both the type check and the bundle build are blocking; tsc --noEmit
is clean (0 errors). See .github/workflows/ci.yml.
Reported plainly, not rounded up:
- Backtesting is honest about small samples, not about being wrong: with limited
accumulated history, results are reported but explicitly flagged as not yet statistically
meaningful (see
MIN_SIGNALS_FOR_MEANINGFUL_RESULTinbacktester.py). npm cineeds npm 11+. The committed lockfile isn't reliably consumable by the older npm (~10.x) that ships withnode:20-slim/GitHub's Node 20 setup — both the Docker build and CI pinnpm install -g npm@11beforenpm cito work around this rather than weakening the build tonpm install.
See CONTRIBUTING.md for the development workflow, coding conventions, and what to include in a pull request.
MIT — see LICENSE.md.
Built for the Pakistan Stock Exchange investment community.