Zerodha · Gemini · GitHub Actions

A research desk
that runs itself.

On the first of every month this pipeline logs into Zerodha, prices your holdings against live market data, reads what the institutions did, and emails you a written research note on your own portfolio. Nothing to open, nothing to run, nothing to pay.

python main.py --demo
09:30:01 INFO  Step 1/7 — Authenticating with Kite… [DEMO — skipped]
09:30:01 WARN  DEMO MODE — hypothetical portfolio (8 stocks, 2 funds)
09:30:01 INFO  Step 2/7 — Fetching holdings… [DEMO fixture]
09:30:01 INFO  Fetched 8 equity, 2 MF, 0 open positions
09:30:02 INFO  Step 3/7 — Enriching with live prices…
09:30:09 INFO  GOLDBEES → GOLDBEES.NS  ₹132.43
09:30:14 INFO  HDFCBANK → HDFCBANK.NS  ₹727.75
09:30:21 INFO  Step 4/7 — Building portfolio summary…
09:30:21 INFO  invested ₹1,00,237 · current ₹1,11,953 · +11.69%
09:30:22 INFO  Step 5/7 — Gathering market research…
09:30:48 INFO  Research context gathered (4,827 chars)
09:30:48 INFO  Step 6/7 — Generating report via Gemini…
09:31:07 INFO  Tokens — prompt: 4,968, response: 1,516
09:31:07 INFO  Step 7/7 — Sending HTML email…
09:31:09 INFO  Done. Report sent.
₹0 Cost per month
~5 min Run time
7 Sections per note
0 Manual steps

The output

What lands in your inbox

One email, built as a printed research note: a masthead, a headline figure, ruled tables of every position, and a set of calls you could place as orders tomorrow. It renders the same in Gmail, Outlook and Apple Mail, on a laptop or a phone.

Top of the report email: a Portfolio Desk masthead, the heading Monthly Review, a net portfolio value of ₹1,11,953 up 11.69%, and an allocation bar splitting equity from mutual funds.
Masthead, headline figure and allocation
Holdings section of the report email: a ruled table of eight equity positions with quantity, average cost, market price, value, profit and loss, and a one-word call, followed by a mutual funds table.
Every position, priced and called
01

The month in one line

The single thing that moved the portfolio, and the three figures that matter.

02

Holdings

Every equity position and every fund, with a one-word call: add, hold, trim or exit.

03

What moved

Three winners and three laggards, each with the reason — drawn from the research pulled that morning, or marked as unexplained.

04

Allocation

Sector weights, and a plain reading of where the book is concentrated.

05

Market backdrop

India first — FII and DII flows in ₹ crore, the RBI's stance, earnings themes — then the Fed, US yields, Brent and the dollar.

06

The calls

Three to five actions, most important first, each with the price that would trigger it. Not sentiments — orders.

07

Watchlist

Two or three names not currently held, with the level worth buying below.

How it works

Seven steps, seven modules

Orchestrated by GitHub Actions on the 1st of every month at 9:30 AM IST. Every network call is bounded, and a failure emails you the traceback rather than failing silently.

01 auth.py

Authenticate

Posts credentials to Kite's login endpoint, answers the TOTP challenge, and captures the enctoken its own web app uses — no Kite Connect subscription, no browser.

02 portfolio.py

Fetch holdings

Pulls equity holdings, mutual fund holdings from Coin, and open positions, discarding anything fully sold.

03 prices.py

Price them

Each position is valued against live Yahoo Finance data — price, P/E, sector, 52-week range — falling back to Kite's own last price if a lookup fails.

04 prices.py

Aggregate

Totals, sector weights, and the winners and laggards — computed so a holding can never appear in both lists.

05 research.py

Gather research

Scrapes FII/DII flow data, downloads brokerage PDFs from ICICI Direct and HDFC Securities, and extracts their text as grounding context.

06 analyst.py

Write the note

Holdings plus that morning's research go to Gemini under a prompt that fixes the sections, the table shapes and the column alignment. Three models and three API keys form the fallback chain.

07 emailer.py

Send it

The markdown is rendered into a table-based HTML email — no flexbox, so no client can collapse the layout — and sent over Gmail SMTP.

Design decisions

Why it keeps working

The parts that took the most thought, and the failure each one prevents.

Login without an API subscription

Kite Connect costs ₹2,000 a month and needs an OAuth redirect a cron job cannot complete. This logs in the way the web frontend does and reuses the enctoken, so the whole thing runs headless and free.

Grounded, not remembered

FII/DII flows and brokerage PDFs are fetched at run time and put in the prompt, so the note cites this month's numbers instead of whatever the model absorbed in training.

Fallback chains everywhere

Qwen 3.8 on Groq, then Gemini across three keys — seven combinations. A Yahoo lookup that fails falls back to Kite's last price. One provider outage does not cost you the month's report.

Half-written notes never ship

A response that is blocked, truncated, or missing most of its sections is rejected and the next model is tried. A note cut off mid-table is worse than none at all, because it still looks deliverable.

Email built as email

The template is nested tables, not flexbox or grid. Gmail and Outlook strip modern layout CSS, which silently collapses a flex row into a stack — the tables render identically everywhere.

IST, not UTC

CI runners are UTC. A report generated just after midnight IST on the 1st would carry the previous month's name, so every timestamp goes through one shared clock.

Failures announce themselves

Any uncaught exception emails you the traceback before exiting non-zero. A silent monthly job that quietly stopped six months ago is worse than no job.

A demo mode that stays honest

--demo swaps in a hypothetical portfolio and logs the substitution loudly. Everything downstream — pricing, research, Gemini, delivery — still runs for real.

Built with

The stack

Python 3.11+Core runtime
requests + pyotpKite auth & TOTP
Kite Web APIHoldings & positions
yfinanceMarket data
pypdfBrokerage reports
Gemini APIAnalysis
Groq · Qwen 3.8First choice
markdownNote rendering
GitHub ActionsScheduler & CI

Getting started

Running in five minutes

Clone it, add your secrets, and leave it alone.

01

Clone and configure

git clone https://github.com/akamaurya/portfolio-monitor.git
cd portfolio-monitor
pip install -r requirements.txt
cp .env.example .env
02

Try it on a hypothetical portfolio first

python main.py --demo --preview

Runs the whole pipeline against invented holdings and writes the email to out/preview.html instead of sending it. Drop --preview to have it delivered.

03

Add your GitHub secrets

Under Settings → Secrets and variables → Actions, add KITE_USER_ID, KITE_PASSWORD, KITE_TOTP_SECRET, GEMINI_API_KEY1, GMAIL_ADDRESS, GMAIL_APP_PASSWORD and RECIPIENT_EMAIL.

04

Leave it alone

The workflow runs on the 1st of every month. You can trigger it by hand from the Actions tab, and you will hear from it either way — the report, or the traceback.

Running cost

Every service stays inside its free tier

ServiceFree tierThis uses
Zerodha KiteFree — no Kite Connect app needed1 login / month
Yahoo FinanceUnmetered~20–50 lookups
Google Gemini API50 requests / day1 request / month
Gmail SMTP500 emails / day1 email / month
GitHub Actions2,000 minutes / month~5 minutes / month
Total₹0