Open source · $0/mo · Python stdlib

Screeners tell you what's cheap. This one tells you why.

A nightly desk for small-cap biotech. It reads the filings behind every drawdown on your watchlist, vetoes the offerings, delistings and cash-runway traps, and pings your phone only when a setup survives. Free data, no API keys, runs while you sleep.

Nightly report59 names0 actionable

Nothing to do today.

2 names entered setup · 1 signal blocked before it reached you

  • SION−91.0%Vetoed

    8-K filed the same session ties the fall to a trial failure. Signal suppressed.

  • OTLK−19.2%Vetoed

    Two share sales in 48 hours, roughly seven weeks of cash. Dilution incoming, not a dip.

  • CAPRrsi 31Setup

    Oversold, no hard veto, above entry zone. Research it — don't chase it.

  • SMMT+6.4%Insider

    Both CEOs bought $103.6M on the open market. Real money, not options.

Illustrative night assembled from real first-week catches. See a real one, redacted →
59watchlist names, read nightly
$0paid data feeds
3m51sper full sweep
267tests, counted at build time

Operating record — computed by tools/site_stats.py from reports/, not typed

Aug 2026running unattended since
17nightly reports written
2weeknights without a report, incl. US market holidays
Sep 2026latest report

The veto layer — financial_vetoes() and exit_signals() in signals.py

Five reasons a biotech is cheap, checked before you ever see a signal

In this sector most large drawdowns are the market being right. The drug failed, the FDA said no, or the company is about to print discounted shares to survive. Every indicator in your broker app lights up green on exactly those names, because RSI measures how far a price moved — never why.

So the desk reads the filings first. Any of these blocks a buy signal outright until it's refuted with evidence from the source document:

  • A priced or announced offering — the dilution is already on its way
  • A delisting or deficiency notice — exchange risk before thesis risk
  • An auditor change — rarely good news, never a coincidence
  • A recent collapse — a −91% day is a verdict, not an entry
  • Under 1.5 quarters of cash — they will raise, and it will be below you

SION — first week live

Fell 91% in one session. Textbook oversold on every indicator. The desk found the 8-K published that same day, tied it to the collapse, and produced no signal. A screener would have handed you the knife.

Form 4, read properly — insider_signal() in signals.py · real filings as test fixtures

Insider buying that means something — and the kind that doesn't

Most "insider buying" alerts are noise, because a Form 4 shows several kinds of acquisition and only one carries signal: an open-market purchase with the executive's own money. Grants and option exercises both show as "acquired" and both are worthless as a tell.

One real filing showed an executive exercising at $0.99 and selling at $5.06 minutes later. Counted carelessly, that reads as insider buying. It is precisely the opposite. The desk parses the transaction codes so it never makes that mistake for you.

Selling gets weighted down hard. When measured, it flagged more than half the watchlist, which makes it useless as a signal — people sell for a dozen innocent reasons and buy for one.

Two filings, six weeks apart
  • CAPR$30.38 → $4.21

    Executives sold $3.1M in late June. Six weeks later the shares traded at $4.21. It was in the filing the whole time.

  • SMMT$103.6M boughtOpen market

    Both chief executives buying their own stock with their own cash. The only Form 4 line that ever matters.

Tier ladder — thresholds in watchlist.toml [settings], applied in signals.py

Four tiers. One of them is a trade.

The gap between setup and act is where most oversold systems bleed. Oversold without volume confirmation underperformed XBI by 2.7 percentage points median over 60 sessions — you'd have done better buying the ETF and going to bed. So act requires capitulation volume, a price inside your entry zone, and a clean financing and catalyst backdrop. Setup means the filter passed. It does not mean buy.

Signal tiers and what each one means
TierWhat it means for you
NoneNothing. Most names, most nights.
WatchApproaching oversold (RSI under 40, %B under 0.25). No alert.
SetupOversold (RSI under 35, %B under 0.15) and no hard veto. Open the filings, not the order ticket.
ActSetup, at or below your entry zone, confirmed by capitulation volume, financing and catalyst backdrop acceptable. Now it's your call.

Built around how you actually trade — STRATEGY.local.md is read before any sizing or routing suggestion

Your plan is the config. The desk enforces it.

Your watchlist, strategy and catalyst calendar live outside the repository, gitignored. They are your trading plan, not shared code — and the desk reads them before it sizes or routes anything.

watchlist.toml

Tier each name A, B or lottery. Tiers set the position-size ceiling, so a lottery ticket can never be sized like a conviction position.

Entry zones

Proposed from each name's own trading range, not a round number you liked. Review the dry run, then apply.

Invalidation

Every name carries a price and a sentence: what would prove this wrong. Exit signals fire when it happens.

Catalyst clock

PDUFA dates, AdComs and readouts with sources. Binary events are the sector; the desk keeps the calendar so you don't.

paper.py

Log the intended trade, and it grades you live against XBI — and tells you plainly when the sample is too small to mean anything.

Monthly screen

Widen the aperture once a month: ~500 biotech registrants outside your list, ranked by the same rules. A research list, not a buy list.

fetch.py and signals.py are deterministic — prompts/daily.md holds the judgement pass and never supplies a number

Your night, automated

  1. Read

    Prices, filings, 8-K item codes, Form 4 insider trades, XBRL financials, short volume and trial records — from SEC EDGAR, FINRA, Nasdaq and ClinicalTrials.gov.

  2. Check

    Fixed arithmetic, no guesswork, then the veto layer runs over every name that moved.

  3. Investigate

    A reasoning pass researches what was flagged, confirms or refutes each veto against the source filing, and writes the analysis.

  4. Report

    A readable briefing in your inbox and a phone alert — only when something actually changed.

A language model is excellent at reading a legal document. It is unreliable at remembering a number. So it is never allowed to be the source of one.

Every figure in your report traces back to a filing, or the report says the figure is unknown — the one rule the analysis pass can't break.

Architecture of the nightly run Public sources feed fetch.py, then signals.py with the veto layer, then a Claude Code analysis pass, then the report and phone alert. A separate heartbeat process watches the reports directory and alarms after three silent weeknights. public sources EDGAR, FINRA, Nasdaq fetch.py no silent defaults signals.py arithmetic + vetoes analysis pass claude -p, judgement report + ntfy only on tier change deterministic — the only place a number is born judgement — argues with the numbers, never invents one heartbeat.py 3 quiet weekdays Architecture of the nightly run Public sources feed fetch.py, then signals.py with the veto layer, then a Claude Code analysis pass, then the report and phone alert. A heartbeat process watches for silence. public sources EDGAR · FINRA · Nasdaq · CT.gov fetch.py records failures, never defaults signals.py arithmetic + veto layer analysis pass claude -p · reads filings report + ntfy only on tier change heartbeat.py alarms after 3 silent weeknights

score_alerts.py grades live alerts, backtest.py replays history — both against the ETF you could just buy

We publish the rules that lost

Every alert is scored against XBI, not against zero. Measuring against zero flatters every strategy ever devised; measuring against the fund you could hold instead is the only honest benchmark. Two of the three rules this started with failed it.

Common rules and their measured performance against the sector fund after three months
Common rulevs XBI, 3mo
Buy stocks near their yearly low−2.68%
The more oversold, the better−9.59%
Oversold, confirmed by heavy selling volume+1.72%

Only the confirmed version survived, so the act tier demands it. The backtests carry survivorship bias, cover a bullish sample and model no spread — a smoke test of the thresholds, not proof of edge. The real answer arrives as live alerts age, which is exactly what the scoring script exists to measure.

Who it's for

Built for one kind of trader

You'll get the most from it if

  • You swing or position trade small-cap biotech and hold through catalysts
  • You want a filter that keeps you out of bad names, not a service that hands you good ones
  • You'll open the filing before you open the ticket
  • You can run one shell script on a Linux box or a Mac that stays on

Look elsewhere if

  • You day trade — this runs once, after the close
  • You want buy signals without the "why"
  • You want it to place orders — it never will
  • You need proof of edge before you'll paper trade it

Why you can trust it at 3am — architecture, veto rules and the XBRL trap catalogue in CLAUDE.md

Engineered for the nights nobody is watching

Zero dependencies, zero rot

The runtime uses only what ships with Python 3.11. Nothing to install, nothing that breaks because an unrelated package updated six months from now. test_runtime_has_no_third_party_imports fails the build if anything under scripts/ grows an import.

Silence is checked, not assumed

Since "nothing to report" is the normal output, a broken run and a quiet market look identical. A separate process, heartbeat.py, raises an alarm if no report appears for three weekdays.

Every data trap gave a wrong answer once

Cash counted without investments understated one balance sheet six-fold. A retired XBRL field returned a six-year-old $24.9M for a firm holding $816M. A company's own filing produced a float ratio of 15,401%. Each is now guarded, and each guard has a test — the catalogue is in CLAUDE.md.

Changes verified byte-for-byte

Rewriting the signal engine meant capturing the exact output for all 59 names, making the change, and confirming the new output was identical. Not "looks fine". Identical.

Python 3.11+ · systemd or launchd units included · one contact email for SEC fair access · CI on 3.11 and 3.12

From clone to first report

If you already have Python 3.11, this is one sitting: clone, point it at your watchlist, run the free pass. The first sweep prints the signal table in a few minutes. Then let it propose entry zones from each name's own range and schedule the timers.

# clone and configure
git clone https://github.com/svedbg/pharma.git ~/projects/pharma
cd ~/projects/pharma
cp pharma.env.example ~/.config/pharma/pharma.env

# data and signals only: fast, free, no model calls
./run_daily.sh --no-llm

# entry zones from each name's own trading range
python3 scripts/propose_zones.py --apply

# schedule it — weekday timers ship for Linux and macOS
systemctl --user enable --now pharma-desk.timer pharma-heartbeat.timer

What it will never do

  • Recommend a name carrying an unrefuted hard veto
  • Manufacture a trade to justify the run — "nothing to do today" is a valid and frequent output
  • Size a lottery-bucket name like a conviction position
  • Report a number it did not read from a primary source
  • Touch your broker. It places no orders, ever.

Questions traders ask

Straight answers

What is Biotech desk?

An open-source, unattended research system for small-cap biotech stocks. Every weeknight it reads SEC filings, prices, insider trades and short interest for your watchlist, computes signals, writes a report, and alerts only when a name changes tier. Python, no third-party dependencies, MIT licence.

How is it different from a stock screener?

A screener reports what's cheap. This also checks why. Its veto layer reads the filings and blocks any buy signal sitting on a priced offering, a delisting notice, an auditor change, a recent collapse, or under 1.5 quarters of cash — until that veto is refuted with evidence.

Does it beat the market?

Measured against XBI, the plain oversold signal did not. Only oversold confirmed by capitulation volume did, at +1.72% over three months, so the act tier requires it. The backtests carry survivorship bias and model no transaction costs. Paper trade it before you fund it.

What does it cost to run?

Nothing. SEC EDGAR, FINRA Reg SHO, Nasdaq and ClinicalTrials.gov are all free and keyless, and the runtime is stdlib-only. A full sweep of about 60 names takes two to four minutes.

Which data does it read?

Daily prices and short interest from Nasdaq with a Yahoo fallback; filings, 8-K item codes, Form 4 insider trades and XBRL financials from SEC EDGAR; daily short volume from FINRA; trial completion dates from ClinicalTrials.gov. News and PDUFA dates come from web search during the analysis pass.

Is it financial advice?

No. It's research support for your own decisions. It places no orders and it isn't a product. Your trades are yours.

Stop guessing which dip is real.

Clone it, point it at your own watchlist, and run the free pass tonight — no API keys, no cost, no orders placed on your behalf.

This is research support for your own decisions. It is not financial advice, it places no orders, and it is not a product — it is a tool built for one person and published so the reasoning can be examined. Small-company biotechnology has near-binary outcomes and any individual position can go to zero.