A lightweight Python bot that monitors a Screener.in CANSLIM screen and sends real-time Telegram alerts whenever a new stock enters the screen.
- How It Works
- Architecture
- Flow Diagrams
- Project Structure
- Prerequisites
- Setup Guide
- Environment Variables
- Usage
- Alert Format
- Known Limitations
The bot runs a continuous loop with two jobs happening in parallel:
-
Telegram Poller — Checks every 3 seconds for new
/startmessages. Any user who sends/startgets added to the subscriber list. -
Screener Monitor — Every 30 minutes, logs into Screener.in, scrapes all stocks from your CANSLIM screen (across all pages), and compares with the previously saved list. If any new stock has appeared, it broadcasts an alert to all subscribers via Telegram.
State (subscriber list, last stock list, Telegram update offset) is persisted in Upstash Redis if configured, otherwise falls back to local JSON files.
┌─────────────────────────────────────────────────────────────┐
│ watcher.py │
│ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ Telegram Poller │ │ Screener Monitor │ │
│ │ (every 3 sec) │ │ (every 30 min) │ │
│ │ │ │ │ │
│ │ getUpdates API │ │ Login → Scrape Pages │ │
│ │ /start → add │ │ Compare with last run │ │
│ │ subscriber │ │ Broadcast if new stocks │ │
│ └────────┬─────────┘ └──────────┬───────────────┘ │
│ │ │ │
│ └──────────┬───────────────────┘ │
│ │ │
│ ┌───────▼──────────┐ │
│ │ State Layer │ │
│ │ │ │
│ │ Upstash Redis │ │
│ │ (or local JSON) │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
flowchart TD
A([Bot Starts]) --> B[Validate env vars]
B --> C[Broadcast LIVE message to all subscribers]
C --> D{Main Loop}
D --> E[Process Telegram Updates]
E --> F{New /start message?}
F -- Yes --> G[Add chat_id to subscribers]
G --> H[Send welcome message]
H --> D
F -- No --> I{30 min elapsed?}
I -- No --> J[Sleep 3 seconds]
J --> D
I -- Yes --> K[Fetch stocks from Screener.in]
K --> L[Load last known stock list]
L --> M{New stocks found?}
M -- Yes --> N[Broadcast CANSLIM ALERT to all subscribers]
N --> O[Save updated stock list]
O --> P[Reset 30-min timer]
P --> D
M -- No --> Q[Log: No new stocks]
Q --> P
flowchart TD
A([get_stocks_from_screener called]) --> B{Session exists?}
B -- No --> C[Login to Screener.in]
C --> D[Fetch CSRF token from login page]
D --> E[POST credentials]
E --> F{Login successful?}
F -- No --> G([Raise exception])
F -- Yes --> H[Store session]
B -- Yes --> H
H --> I[Fetch page 1]
I --> J{Redirected to login?}
J -- Yes --> K{Already retried?}
K -- No --> L[Reset session, retry login]
L --> I
K -- Yes --> M([Skip this cycle])
J -- No --> N[Parse all stock names from td.text]
N --> O{More pages?}
O -- Yes --> P[Increment page, sleep 2s]
P --> I
O -- No --> Q([Return full stock list])
flowchart TD
A([process_telegram_updates called]) --> B[GET /getUpdates with offset]
B --> C{Response OK?}
C -- No --> Z([Return silently])
C -- Yes --> D{Any updates?}
D -- No --> Z
D -- Yes --> E[Loop through updates]
E --> F{Message is /start?}
F -- No --> G[Skip update]
F -- Yes --> H{chat_id already subscribed?}
H -- Yes --> I[Send: Already subscribed]
H -- No --> J[Add to subscriber list]
J --> K[Save subscribers to Redis/JSON]
K --> L[Send welcome message]
L --> M[Update offset to update_id + 1]
M --> N[Save offset]
N --> E
flowchart TD
A([load_state / save_state called]) --> B{Upstash Redis configured?}
B -- Yes --> C[Read/Write via REST API]
C --> D{Success?}
D -- Yes --> E([Return data])
D -- No --> F[Log error, return default]
B -- No --> G{Local JSON file exists?}
G -- Yes --> H[Read from key.json]
H --> E
G -- No --> I([Return default value])
stockwatcher/
├── watcher.py # Main bot — all logic lives here
├── requirements.txt # Python dependencies
├── Procfile # For Railway/Heroku deployment
├── .gitignore # Excludes .env, cache, and state files
└── .env # Local secrets (never commit this)
- Python 3.9+
- A Telegram account
- A Screener.in account (free tier works)
- A Railway account (for deployment) — or any server with Python
- An Upstash account (free tier, for persistent state across restarts)
Step 1: Open Telegram and search for @BotFather
Step 2: Send /newbot and follow the prompts to name your bot
Step 3: BotFather will give you a token like:
1234567890:ABCdefGHIjklMNOpqrSTUVwxyz
Save this — it's your TELEGRAM_TOKEN.
Step 4: Get your personal chat ID by messaging @userinfobot on Telegram. It will reply with your numeric chat ID (e.g. 987654321). This is your TELEGRAM_CHAT_ID — used to seed the first subscriber.
Step 1: Register at screener.in
Step 2: Find or create your CANSLIM screen. The default screen URL used is:
https://www.screener.in/screens/2394874/canslim/
To use your own screen, change SCREEN_URL in watcher.py.
Step 3: Note your login email and password. These become SCREENER_EMAIL and SCREENER_PASSWORD.
⚠️ The bot uses session-based scraping (not an official API). If Screener.in changes their login flow, the scraper may break.
Without Redis, state is stored in local .json files — which are wiped on every Railway redeploy. Redis makes state survive restarts.
Step 1: Go to upstash.com and create a free account
Step 2: Create a new Redis database (select the region closest to your deployment)
Step 3: From the database dashboard, copy:
UPSTASH_REDIS_REST_URL(looks likehttps://xxx.upstash.io)UPSTASH_REDIS_REST_TOKEN(a long string)
# Clone the repo
git clone https://github.com/YOUR_USERNAME/stockwatcher.git
cd stockwatcher
# Create and activate a virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
# Create your .env file
cp .env.example .env # or create manuallyEdit .env:
TELEGRAM_TOKEN=your_bot_token_here
TELEGRAM_CHAT_ID=your_personal_chat_id
SCREENER_EMAIL=your@email.com
SCREENER_PASSWORD=yourpassword
UPSTASH_REDIS_REST_URL=https://xxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=your_token_hereRun locally:
python watcher.pyYou should see:
==================================================
STOCK WATCHER - CANSLIM SCREEN (PUBLIC BOT)
==================================================
Logging in to screener.in...
Login successful!
Broadcasted message to 1 subscriber(s).
Railway runs the bot as a persistent background worker using the Procfile.
Step 1: Push your code to GitHub (make sure .env is in .gitignore)
Step 2: Go to railway.app → New Project → Deploy from GitHub repo
Step 3: Select your repository
Step 4: Go to Variables tab and add all environment variables:
| Variable | Value |
|---|---|
TELEGRAM_TOKEN |
Your bot token |
TELEGRAM_CHAT_ID |
Your Telegram numeric ID |
SCREENER_EMAIL |
Your screener.in email |
SCREENER_PASSWORD |
Your screener.in password |
UPSTASH_REDIS_REST_URL |
Upstash URL |
UPSTASH_REDIS_REST_TOKEN |
Upstash token |
Step 5: Railway will auto-detect the Procfile and run python watcher.py as a worker process.
Important: Railway's free tier has usage limits. The bot runs 24/7, so check their pricing if you hit limits.
| Variable | Required | Description |
|---|---|---|
TELEGRAM_TOKEN |
✅ Yes | Bot token from BotFather |
TELEGRAM_CHAT_ID |
Optional | Your chat ID to seed the first subscriber |
SCREENER_EMAIL |
✅ Yes | Screener.in login email |
SCREENER_PASSWORD |
✅ Yes | Screener.in login password |
UPSTASH_REDIS_REST_URL |
Optional | Upstash Redis URL for persistent state |
UPSTASH_REDIS_REST_TOKEN |
Optional | Upstash Redis auth token |
If UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN are missing, the bot falls back to local JSON files (subscribers.json, last_stocks.json, telegram_offset.json).
Subscribe to alerts:
In Telegram, message your bot:
/start
You will receive a welcome message and be added to the subscriber list. Any user who finds and messages your bot can subscribe the same way.
What happens next:
- Every 30 minutes, the bot checks the CANSLIM screen
- If a stock appears that wasn't there before, everyone subscribed gets an alert
- The bot continues running indefinitely
On startup:
Stock Watcher is LIVE
Started: 08:30 06-Jun-2026
Checking CANSLIM screen every 30 minutes.
When new stocks are detected:
CANSLIM ALERT
3 new stock(s) entered the screen
Time: 09:00 06-Jun
→ Tata Consultancy Services
→ Infosys Ltd
→ Dixon Technologies
Check screener.in for details.
- Scraping, not API — Screener.in doesn't offer a public API. The bot scrapes HTML. If they change their page structure or add CAPTCHAs, it will break.
- No
/stopcommand — There is currently no way for a user to unsubscribe. You would need to add a/stophandler that removes the chat ID from the subscriber list. - Session expiry — The bot handles one re-login attempt per cycle. If Screener.in's session TTL is very short, you may miss cycles.
- No deduplication guard on alerts — If Redis goes down and state is lost, the bot will re-alert on all existing stocks as if they are new.
- Rate limits — Screener.in may throttle or block repeated requests. The 2-second delay between pages and 30-minute check interval are conservative by design.