writing
A Daily Scheduled IDX Stock Screener on a $0 Budget
Screening the Indonesian stock market (IDX) for trade signals is usually framed as a problem that needs a paid platform. Most screener products charge a subscription, push you toward their own prebuilt filters, and keep the underlying data behind a closed API. If you just want a consistent nightly report of which tickers look interesting, that model is overkill.
This article describes a pipeline I built for exactly that: a daily cron job that pulls a fresh full-IDX snapshot, screens every active ticker locally, and posts a formatted signal summary to Telegram. The whole thing runs on a free-tier cloud server and makes a single API request per day.
The overall shape
The pipeline is three Python scripts plus one cron entry, plus a small SQLite database for OHLCV history.
- A backfill script - fetches daily OHLCV (open, high, low, close, volume) for the full IDX ticker set from a data API and stores it in SQLite. It only requests dates that are missing, so a fully backfilled database costs zero extra requests.
- A screener script - a local screen that reads the SQLite history, computes the signal logic for every active ticker, classifies it, and writes a JSON summary.
- An orchestrator - it refreshes today’s row, runs the backfill, runs the screener, and prints the final Telegram report block.
- A cron rule runs the orchestrator once a day at 20:00 WIB and the OpenClaw gateway delivers the printed summary to a Telegram chat.
The design goal was efficiency: exactly one fresh data request per trading day, with the screener running entirely on local data.
Step one: getting the data
The backfill script connects to a stock-summary endpoint from an Indonesian market data provider. One call returns a snapshot of roughly a thousand tickers with their current OHLCV values.
A stock summary endpoint is a convenient choice because a single request returns the whole market instead of one request per ticker. That is what makes a one-request-per-day budget possible.
The response is split into rows and written into an ohlcv table keyed by (ticker, date):
CREATE TABLE IF NOT EXISTS ohlcv (
ticker TEXT NOT NULL,
date TEXT NOT NULL,
open REAL, high REAL, low REAL, close REAL,
volume INTEGER,
PRIMARY KEY (ticker, date)
);
The backfill phase only fills dates that are not already present. Candidate dates are the last 60 weekdays (IDX trades Monday to Friday). Because the script skips dates that already exist, repeated runs are effectively free once the history is populated.
Rate limiting is handled defensively. A 401 response is treated as a rate limit, the script waits 30 seconds times the attempt number, and retries up to three times. A 200 response is parsed and returned; anything else is retried after a short delay.
Step two: the local screen
The screener never touches the network. It reads the local SQLite history and applies the same signal logic the trading app itself uses, so the “app logic” and the “screener logic” stay in one place.
First it filters to active stocks. Rows with a non-positive close or volume are dropped. It also removes carry-forward rows: on non-trading days the data source returns a snapshot identical to the previous trading day, so any bar whose close and volume exactly match its predecessor is treated as a duplicate and discarded. It then requires enough valid history per ticker for the indicators to be meaningful.
Each ticker is then classified into one of a handful of states by combining a trend condition with a confirmatory condition. A ticker that satisfies both cleanly lands in the “signal triggered” bucket, a weaker combination lands in the “watch” bucket, and a ticker that falls out of the primary trend condition is flagged as bearish. Everything else waits. The exact thresholds are internal to the engine and intentionally not published.
The result is written to signals.json for anything else that wants the structured output, and printed to stdout for the Telegram report.
Step three: the nightly orchestrator
The daily driver ties the pieces together. On every run it:
- Refreshes today’s row with a single fresh stock-summary request. Because a brand new trading day might produce a snapshot different from the previous carry-forward row, today’s data is always re-pulled, not trusted from history.
- Runs the backfill for any other missing dates (zero requests when nothing is missing).
- Imports the screener module and runs the full scan.
- Prints a condensed Telegram report: the total analyzed, the counts per bucket, and the top lookers in each category.
The summary block is the only thing meant to reach the user:
SST FULL-IDX - ~800 emiten - data 2026-08-04
3 ENTRY - 12 WATCH - 5 BEARISH - 40 WAITING
SINYAL ENTRY (3/3):
ABCD 5400 - 3d
...
Scheduling and delivery
A single cron rule runs the orchestrator every day at 20:00 WIB (Asia/Jakarta). The OpenClaw gateway treats the script output as the payload of an isolated agent run and announces it to a Telegram chat, so there is no separate notification code and no email server to maintain.
The daily cost is one API request. With a free allowance of two thousand requests per month, that leaves headroom for the occasional manual re-run or a larger backfill without ever approaching the limit.
Why this approach
The useful part of this setup is not the signal itself but the plumbing: a handful of editable Python files plus a cron entry, all local and all free. Need a different signal? Edit one function. Want to email instead of Telegram? Change the delivery. Want weekly instead of daily? Adjust one cron line. There is no vendor lock-in and no monthly bill standing between the idea and a running report.
Future work
Planned improvements include persisting the daily summary so it can be diffed against previous days, sending a richer per-sector breakdown, and letting the report carry a small note when a ticker enters or leaves the entry list compared with yesterday.
The scripts live alongside the source that drives this site.