Skip to content

Repository files navigation

Portfolio Agent

Fetches your stock portfolio (Groww or Paytm Money), analyzes each holding with a LangChain tool-calling agent (technicals + news sentiment), tracks day-over-day sentiment shifts, adds portfolio-level risk reasoning, and delivers it via a daily Telegram alert, a conversational Telegram bot, a local web dashboard with its own chat box, and an intraday market watcher that alerts in real time when something notable happens.

Not financial advice. Signals are simple technical indicators plus LLM-read news sentiment — use as a starting point for your own research, not a trading signal to act on blindly.

Architecture

Data sources

  • groww_client.py — calls the Groww Trading API (growwapi package) to pull your actual holdings (quantity, average cost price). Preferred broker source — self-serve API key, no broken onboarding flow.
  • paytm_client.py — calls the Paytm Money Open API get-user-holdings-data endpoint as an alternative holdings source (quantity, cost price, LTP).
  • csv_import.py — fallback: load holdings from a manually-exported CSV if neither broker API is set up yet.
  • technical.py — pulls ~1yr of daily OHLC per symbol from Yahoo Finance (free, no key) and computes SMA20/50, RSI14, 10-day momentum, 52-week range, and sector into a bullish/bearish/neutral trend read. Also used as the price source for P&L when the broker API doesn't return a live price (e.g. Groww).
  • news.py — pulls recent headlines per stock (and general market news) from Google News RSS — free, no API key.

Analysis

  • analysis_agent.py — a real LangChain tool-calling agent (create_agent) per holding: the LLM decides when to call get_technical_signal / get_stock_news (and re-fetches news with a higher limit if headlines look thin) rather than being handed fixed data, then returns structured output (call, sentiment score, reasoning, risk note, one rating per headline used).
  • history.py — persists each day's per-stock call/sentiment (backed by db.py/SQLite); feeds yesterday's read into today's prompt so the agent can note continuations/reversals, and the report/dashboard flag flips.
  • portfolio_analysis.py — a second LLM pass over the whole portfolio together (only runs with 2+ holdings) — sector concentration, correlated risk, overall stance — things no single-stock call can see.
  • snapshot.py — caches the full result of the last run (via db.py) so the dashboard displays instantly without re-running the pipeline.
  • chat_tools.py — the same tools (holdings/technical/news) wrapped as a free-form conversational agent, shared by the Telegram bot and the dashboard's chat box.
  • market_watch.py — cheap, local trigger detection for the intraday watcher (price move, RSI zone change, new headline) plus the LLM call that composes an alert's text once a trigger actually fires.

Infrastructure

  • db.py — SQLite (data/portfolio_agent.db) backing everything above: call history, snapshots, a run log, LLM token/cost tracking, and the intraday watcher's dedup/cooldown state. One file, safe for the daily job/dashboard/chat bot/watcher to all touch concurrently.
  • retry_utils.py — a small exponential-backoff @retry decorator applied to every flaky external call (broker APIs, Yahoo Finance, news RSS, LLM calls), plus a helper to sum token usage off LangChain messages.

Delivery

  • report.py + telegram_alert.py — formats and sends the daily Telegram report.
  • scripts/telegram_chat_bot.py — long-polls Telegram for ad hoc questions, answered live via chat_tools.
  • scripts/market_watch.py — polls every 5 minutes during NSE market hours and sends a Telegram alert only when a real trigger fires (see below).
  • webapp.py + web/index.html — a local FastAPI dashboard (http://127.0.0.1:8765): portfolio table, per-stock cards with the 52-week range/technicals/news, a sentiment-history chart, an Observability section (run log + token/cost totals), a manual refresh button, and a chat box.
  • main.py — orchestrates the whole analysis run (daily job and dashboard refresh both call this). Auto-detects holdings source (Groww → Paytm → error) unless overridden with --source groww|paytm or --csv <path>. --no-alert skips the Telegram send (used by the dashboard's refresh).

Observability

  • Every pipeline run (daily job, dashboard refresh) is logged to a runs table (status, duration, error, holdings analyzed) — visible in the dashboard's Observability section.
  • Every LLM call's real token usage is captured and priced at current gpt-4o-mini rates, so you can see actual running cost, not a guess.
  • Optional LangSmith tracing: set LANGCHAIN_TRACING_V2=true, LANGCHAIN_API_KEY, and LANGCHAIN_PROJECT in .env — LangChain auto-detects these, no code changes needed. Gives you a full per-call trace (which tools fired, in what order, with what latency) at https://smith.langchain.com/.

Setup

1. Install dependencies

cd ~/Desktop/projects/stock-portfolio-agent
uv sync

2. Broker API access (pick one)

Groww (recommended)

  1. Go to https://groww.in/trade-api/api-keys and click "Generate API key".
  2. Put the resulting values in .env (copy .env.example first): GROWW_API_KEY, GROWW_API_SECRET.
  3. The client exchanges these for a short-lived access token on every run (GrowwAPI.get_access_token(...)), so nothing else to refresh manually.

Paytm Money (alternative)

  1. You need a KYC'd Paytm Money equity trading account.
  2. Sign in at https://developer.paytmmoney.com/ and create an app to get an API key and API secret. Put both in .env as PAYTM_MONEY_API_KEY and PAYTM_MONEY_API_SECRET.
  3. Run the interactive helper to get an access token (this step needs a real browser login with your credentials/OTP, so it can't be automated):
    uv run python scripts/generate_paytm_token.py
    It prints a login URL — open it, log in, and you'll be redirected to your app's registered Return URL with ?request_token=... in the address bar. Paste that token back into the prompt; the script exchanges it for an access token and saves it to .env as PAYTM_MONEY_ACCESS_TOKEN.
  4. This access token expires at midnight every day — re-run the script each day before the scheduled job runs (or before testing manually).

3. OpenAI API key

Set OPENAI_API_KEY in .env (used for the LangChain analysis step, via gpt-4o-mini).

4. Telegram bot

  1. Message @BotFather on Telegram, run /newbot, and follow the prompts to get a bot token.
  2. Send any message to your new bot from the Telegram account you want alerts on.
  3. Visit https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates in a browser and find your chat.id in the JSON response.
  4. Put TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID in .env.

5. Test a manual run

uv run run-daily            # full run + Telegram alert
uv run run-daily --no-alert # full run, updates data/*.json, skips Telegram
uv run run-dashboard        # starts the web dashboard at http://127.0.0.1:8765

You should see log output and a Telegram message with your portfolio report.

Intraday market watcher

scripts/market_watch.py polls every 5 minutes during NSE market hours (9:15 AM-3:30 PM IST, weekdays). All checks are free/local — the LLM is only called to write the alert text once a real trigger fires:

Trigger Threshold
Price move ±3% since the last alert for that stock
RSI extreme Entering overbought (≥70) or oversold (≤30) from a normal zone
New headline A headline not seen before (capped at 2 per check, to avoid noise)

A 30-minute cooldown applies to price/RSI triggers per stock (headlines are deduped permanently instead, via seen_headlines in the DB) so the same event doesn't re-alert every 5 minutes. On the very first check for a stock, headline alerts are suppressed (everything looks "new" then) but an already-extreme RSI still alerts, since that's genuinely worth knowing.

Running as background services

Option A: Docker (cross-platform — Mac, Linux, Windows)

Requires Docker installed. One command runs all four services identically on any OS:

cd ~/Desktop/projects/stock-portfolio-agent
docker compose up -d --build   # build once, start all 4 services in the background
docker compose ps               # check status
docker compose logs -f webapp   # tail a service's logs (webapp/chatbot/marketwatch/dailyjob)
docker compose down             # stop everything

The dashboard is at http://127.0.0.1:8765 (bound to loopback only via the port mapping, same as the native setup). ./data is mounted into every container so the SQLite DB persists across restarts. The daily job uses scripts/daily_scheduler.py — a plain Python loop checking the time once a minute — instead of macOS's launchd/cron, since there's no OS-native scheduler inside a container. Note it catches up immediately if a container starts after 8:30 IST and hasn't run yet that day, rather than waiting for the exact time like launchd does.

Verified working live: build succeeds (including native deps like pynacl), all 4 containers start, the dashboard responds on the host, and a full daily-job run completed (Groww holdings → analysis → Telegram alert) with the SQLite writes confirmed on the host filesystem via the volume mount. This is the setup currently running — the native launchd services below are stopped in favor of it (both can't run at once: same port, same DB file, and you'd get duplicate Telegram alerts).

Option B: launchd (macOS-native, no Docker needed)

Four launchd LaunchAgents are set up under ~/Library/LaunchAgents/:

Service Plist What it does
Daily job com.portfolioagent.dailyjob.plist Runs uv run run-daily weekdays at 8:30 AM IST, sends the Telegram report
Chat bot com.portfolioagent.chatbot.plist Runs scripts/telegram_chat_bot.py continuously, answers ad hoc Telegram questions
Dashboard com.portfolioagent.webapp.plist Runs uv run run-dashboard continuously, serves the web UI at http://127.0.0.1:8765
Market watch com.portfolioagent.marketwatch.plist Runs scripts/market_watch.py continuously, sends intraday alerts on real triggers
# Check status
launchctl list | grep portfolioagent

# View logs
tail -f logs/dailyjob.err.log
tail -f logs/chatbot.err.log
tail -f logs/webapp.err.log
tail -f logs/marketwatch.err.log

# Stop / restart a service
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.portfolioagent.<name>.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.portfolioagent.<name>.plist

All four have RunAtLoad (start at login) and KeepAlive on the long-running ones (auto-restart if they crash, throttled so a persistent failure doesn't spin). Note the chat bot, dashboard chat box, and market watcher all make real OpenAI API calls — that's ongoing cost tied to usage, not a one-time thing like the daily job (though the market watcher's cost stays low since most 5-minute polls don't trigger the LLM at all).

Known limitations

  • Groww's API access is a paid subscription (₹499 + taxes/month as of writing) — separate from your Groww trading account itself.
  • The Paytm Money access token is short-lived; running this fully unattended long-term needs the login/session refresh flow automated (or a manual token refresh every so often).
  • Groww's holdings response doesn't include a live price, so P&L in the report relies on the Yahoo Finance close price instead — it can lag the true LTP slightly, especially intraday.
  • Technical + sentiment signals are simple heuristics, not a trained predictive model — treat the "call" as a conversation starter, not a forecast.
  • Google News RSS results can be noisy for less-covered small-cap stocks.
  • The dashboard (http://127.0.0.1:8765) has no authentication — fine since it only binds to localhost on your own machine, but don't expose this port on your network without adding auth first.
  • The dashboard's "Refresh" button re-runs the full pipeline (broker API + yfinance + news + LLM calls per holding) synchronously, so it can take several seconds to a minute depending on portfolio size.
  • The market watcher loads holdings once per day (cached in memory) rather than every 5-minute poll, to avoid hammering the broker API — if you add or remove a holding intraday, it won't be picked up until the next day (or a service restart).
  • Retry logic catches all exceptions indiscriminately (no retryable-vs-fatal distinction), so a genuinely broken credential still retries a couple of times before failing, wasting a few seconds rather than failing fast.

About

LangChain agent that fetches your stock portfolio, analyzes it with technicals + news sentiment, and delivers daily/intraday Telegram alerts + a web dashboard.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages