Building an Agentic Stock Picker with Anthropic

For the past two years I have been hearing a lot about agentic flows. Yet all my work with AI-assisted coding has been deterministic task flows, for high-performance, scalable systems.

So I decided to try my hand at the agentic approach, and build an agentic stock picker. The idea was that the application would download data for stocks, get the fundamental and technical indicators along with the news items for each stock, and then have agents:

  • filter the stocks down on the fundamental and technical indicators
  • read the news items and try to identify why a stock price was dropping or growing
  • come up with a final list — stocks to buy, stocks to watch, and stocks to avoid

The approach was to use regular python to

  • download four years of prices, statements news, and compute every indicator, fundamental and technical namely ROE, ROCE, P/E, RSI, MACD, volatility and the rest
  • filter ~500 companies down to 15, on those numbers alone
  • checks every claim the agents make against the frozen data, and deletes the ones the data contradicts
  • scores whatever survives, and produces the final buy / watch / avoid list

and agents

  • argue about one company at a time with one agent for the business, one for the price chart, one for the news
  • and every claim has to come with the condition that would prove it wrong
  • then a second model attacks the claims that survived, and can only attack — there is no field in its answer for agreeing

Agentic Stock Picker

The dashboard of the Agentic Stock Picker is shown below
On the left we have

  • Sliders: the sliders to choose fundamental and technical ratios to filter on
  • Max candidates to recommend
  • Model: Offline heurestic/Anthropic/Qwen 14b( TBD)
  • Ceiling (USD):
  • Compile spend plan: This option will give the spend estimate and the hard stop
  • Approve and Run : If the amount is fine we can approve the run
  • Past runs: The results of past runs can be selected here
A screenshot of a financial analysis interface displaying stock data, including symbols, ratings, scores based on fundamental and technical analysis, and various financial metrics.

Design Strategy

Governance is the backbone of the implementation.

The agents run offline. When a model is running, there is no network. When the network is live, there is no model.

The network being live and a running model is never true

Text describing the first pillar titled 'Sealed window', highlighting network access during a specific phase.

The goal of the agent-based approach was to put the agents under a strict governance plan. In fact the whole design is built around governing them.

The agents never touch the market data provider at all. The download happens first, in ordinary Python with no model involved, over a fixed list of seven read-only URLs using a single read-only Upstox analytics token. By the time an agent starts, that connection is closed, the token has been dropped from the environment, and the process the agent runs in cannot even import the code that would talk to Upstox. This is least privilege enforced structurally rather than promised.

So the agents cannot reach the market, cannot trade, and cannot see a portfolio. There is no order, holdings or funds code path anywhere in the repository. While a model is running, exactly one address is reachable: the model provider itself.

The budget works the same way. The full cost of a run is computed and approved before the first call, and every call has to claim a one-use ticket against it. Going over budget is not something the system detects and recovers from. A call without a ticket cannot be made at all.

Everything follows from one invariant: the market-data connection and a running model are never live at the same time. Data is collected first and frozen; analysis then runs in a separate process with no route back to the provider. The seal enforces this in both directions — the data network cannot open while a model is running, and a model cannot start while the data network is live.

Collection issues exactly seven read-only GET requests: the NSE instrument master, daily and intraday candles, key ratios, the income statement, the balance sheet, and news by instrument. Every path segment and query parameter is typed and matched in full, so a permitted template cannot be bent into another resource. During analysis the models read the frozen snapshot, may call four probe tools for deeper price history, full statements, peer ranks and further news, and may emit claims that each carry a machine-checkable disproof condition.

No model computes any number and every ranking is fixed arithmetic in Python. Spending is prepaid: the committed total is the hard stop, approved by hash before the first call, with a one-use ticket per request. Every request and model call is recorded in an append-only, hash-chained log.

Controls are fail-closed. A violation stops the run; there is no path that retries around it or falls back to something less governed.

The seven allowlisted reads

#CapabilityHostPath template
1instrument_masterassets.upstox.com/market-quote/instruments/exchange/NSE.json.gz
2daily_candlesapi.upstox.com/v3/historical-candle/{instrument_key}/days/1/{to_date}/{from_date}
3intraday_daily_candleapi.upstox.com/v3/historical-candle/intraday/{instrument_key}/days/1
4key_ratiosapi.upstox.com/v2/fundamentals/{isin}/key-ratios
5income_statementapi.upstox.com/v2/fundamentals/{isin}/income-statement
6balance_sheetapi.upstox.com/v2/fundamentals/{isin}/balance-sheet
7news_by_instrumentapi.upstox.com/v2/news

The parameters are part of the allowlist, not just the paths

Every segment is typed by full-match regex where the instrument_key must be NSE_EQ\|<ISIN>, dates must be YYYY-MM-DD, isin must match the ISIN shape. So the path template alone can’t be bent into another resource.

Query params are typed too, and required where it matters:

  • income_statement — type=consolidated (required), time_period=yearly|quarterly (required)
  • balance_sheet — type=consolidated (required)
  • news_by_instrument — category=instrument_keys (required), instrument_keys (required, up to 30 per request), page_number/page_size optional, capped at 100

Rule one: The model works offline

Market data is downloaded first, saved to disk, and locked. Subsequently AI starts and by that point the program has no route to the market data provider at all. The two never overlap.

Flowchart depicting the interaction between Market Data and an AI Model during various stages of data processing, including collection, filtering, claiming, checking, attacking, and publishing.

This holds whichever model you use. With Claude, the one address reachable while a model runs is the Anthropic API with a model served on your own machine it is 127.0.0.1

Rule two: every claim carries its own disproof

This is the heart of the design. When the model says something, it must also say what would make that statement false which is written as a short formula the computer can evaluate against the saved data.

Text display of financial metrics: ROCE at 47.79% against a sector benchmark of 14.99% and ROE at 36.8% compared to a sector average of 13.06%. Status indication for evaluation criteria.

The claim is “ROCE stands at 47.79% versus…” and the disproof or falsifier is
roce_pct < sector_roce_pct +5. Since the ROCE (Return on Capital Equity) is not less tan sector_roce_pct +5, this falsifier turns out false i.e. 47.79 is not less than 19.99. Because the disproof is false the claim survives.

A table outlining a process with steps related to computing financial metrics like ROCE and RSI, specifying who performs each step, including Python and a model.
Flowchart illustrating the four ways a claim can be invalidated, including steps for checking evidence and conditions for refutation or survival.

Here are some examples of how claims and falsifiers work

A comparison of claims related to company performance metrics, showcasing a correct claim that survives verification and a wrong claim that has been correctly refuted.

The six steps, in order

A run is six phases. Only two of them contain a model, and the market-data connection is closed in five of the six. The figures below are from the live run of 18 September 2026.

1. Collect and freeze (market data: connected model: idle)

This is the only step with a live connection, and no model is anywhere near it. It is ordinary Python making a fixed set of read-only calls.

For each of the roughly 500 companies in the Nifty 500, seven things are fetched. The instrument master, to resolve the company to its ISIN. Four years of daily candles, 1,460 calendar days back from the run date, which works out at about 992 trading sessions per company. Today’s session separately, from the intraday endpoint, because the daily series does not include it until the market closes. Six headline ratios with their sector benchmarks: PE, PB, ROE, ROA, ROCE and EV/EBITDA. The income statement twice, once yearly and once quarterly. The balance sheet. And recent news, where the request asks for a 30 day window but the provider returns roughly a week.

None of the technical indicators come from the provider. RSI, MACD, the moving averages, ATR, volatility, the volume ratio, the returns and the drawdown are all computed here in Python from the candles just downloaded. The same goes for the fundamental derivations: operating margin and its year on year delta, leverage and its delta, the growth rates, the bank ratios. The provider supplies raw material, and every number a model later reasons about is arithmetic this code did itself. That matters because a model asked to compute an average will happily produce a plausible one.

Then the connection is closed, the token is dropped from the environment, the process is sealed, and only then is anything written to disk. That ordering is deliberate. Nothing is persisted while a route to the outside still exists.

The snapshot is seven files: the candles, the raw fundamentals, the news, the instrument records, the computed indicator rows, the evidence index, and a manifest. Each file is written to a temporary path, made read-only, and then moved into place atomically, so a snapshot is never half written. The directory itself is read-only too. The manifest records the hash of every file, and the directory is named after the hash of the manifest. Loading a snapshot re-hashes all seven files and compares them against the manifest before returning anything. Change one digit in one file and the load raises an integrity error rather than running.

That is why it is fingerprinted. The reason is not tamper-proofing so much as reproducibility. A dossier cites a snapshot hash, a screen config hash and a spend plan hash. Given those three, the same run produces the same output, which is what makes “why did it say that?” a question with an answer.

The evidence index is the part that matters most for what the agents do later. Every fact is packaged as an evidence record, and that run has 4,252 of them across nine kinds:

KindCountWhat it holds
key_ratios498the six ratios with sector values
fundamental_derived498the full 26 column computed row
technical_derived498the full 21 column computed row
income_statement_yearly498four years of results
income_statement_quarterly498four quarters
balance_sheet498assets and liabilities by period
price_history498the 30 most recent closes and volumes, plus the session count
news_derived498the headline counts
news_article268individual headlines, summaries truncated to 600 characters

Each record carries its own identifier, of the form ev: followed by sixteen hex characters, derived by hashing the API call that produced it, its parameters, the as-of date, and the row it came from. It also keeps the name of the capability that fetched it and the exact parameters used, so any fact can be traced back to the specific call that produced it.

Those identifiers are the only way anything downstream is permitted to refer to a fact. When an analyst writes a claim in step 3, it must cite evidence IDs, and the validator rejects any ID that is not in this index or that belongs to a different company. It is a chain of custody: every sentence in the final report traces to a claim, every claim to one or more evidence IDs, and every ID to a row in a file whose hash is recorded in a manifest that names the directory.

One consequence worth noticing. An analyst never sees the 992 candles. It sees the computed indicator row, which was calculated from all of them, plus the 30 most recent closes. The heavy data stays on disk; only the derived view reaches a prompt.

2. Filter down to a shortlist (market data: closed model: idle)

This step is plain Python. There is no model in it anywhere, which is also why the slider settings can never leak into a prompt: there is no prompt here to leak into.

It runs in two stages, and they work quite differently. The first is a pass or fail gate. The second is a ranking.

Stage one, the gate. Seven thresholds were active for this run:

SliderSetting
ROE floor12 percent
ROCE floor12 percent, non-banks only
Revenue growth floor0 percent
P/E ceiling80
RSI band30 to 80
ATR ceiling5 percent of price
Candidates kept15

Each is compared straight against the saved number for that company. 498 companies in, 192 out, 306 rejected.

Two refusals are built into that gate. If a filter is switched on and the number it needs is missing, the company is rejected rather than waved through. And if the data is too old to trust, judged per dimension against a freshness limit, it is rejected even when every other number looks perfect. Missing data never gets the benefit of the doubt.

Some filters are scoped by company type. The ROCE floor and the leverage ceiling apply only to non-banks, because those measures are meaningless for a bank, whose balance sheet is supposed to be mostly other people’s money. There is a separate Net NPA ceiling that applies to banks only, which I did not switch on for this run.

Stage two, the ranking. The 192 survivors are then scored on fourteen measures grouped into five themes:

Table detailing investment metrics categorized by group: Quality, Value, Growth, Trend, and Risk, with corresponding weights and measures.

The top 15 go forward; the other 177 are dropped, and the report states that number rather than leaving it implied.

Two deliberate refusals: a company missing a required number is rejected, never given the benefit of the doubt, and a company whose accounts are too old to trust is rejected even if every other number looks perfect.

3. Ask for arguments (market data: closed model: thinking)

Each of the 15 companies gets three separate questions, asked independently, each seeing only its own slice of the frozen data: one about the business (Claude Sonnet 5), one about the price chart (Sonnet 5), one about the news (the cheaper Haiku 4.5). The agent is handed the real numbers, not a summary of them.

Fundamental: PE, PB, ROE, ROA, ROCE, EV/EBITDA and the sector benchmark for each, revenue and profit growth, operating margin and its one year delta, leverage (liabilities over equity) and its delta, the bank ratios (NIM, Net NPA, CASA and their sector benchmarks) when is_bank is 1, and fundamentals_age_days. Twenty six columns in all.

Technical: close, SMA20/50/200 and the price against each as a percentage, RSI-14, MACD with its signal and histogram, ATR-14 and ATR%, 20 day annualised volatility, 20 day volume ratio, 5 day, 30 day, 90 day and 1 year returns, drawdown from the 52 week high, and last_candle_age_days. Twenty one columns.

News: the raw headlines, up to 25, most recent first, plus news_count_7d and news_count_window.

Each analyst returns at most six claims. Every one carries evidence references, a confidence between 0.05 and 0.95, a written justification, and, the part that matters, the condition that would prove it wrong.

The three analysts never see each other’s work, and none of them sees another company. Each also has to stay in its lane when writing that condition: a fundamental claim can only be tested against fundamental columns, a technical claim only against technical ones. The news analyst is the one exception and may reach for the price columns, because there are only two news columns and both are just counts, so a claim like “this headline explains the fall” would have nothing to be tested against otherwise.

There is a toolbox in the code that would let an analyst ask for more of the frozen data, such as deeper price history, the full statement tables, or how the company ranks against its peers. It is written but not wired up. Each agent still gets exactly one call and cannot ask for anything more, and the budget sets aside thirty probe calls per run that can never fire. It is on the list to either connect properly or delete.

What the models cannot do is reach the market data provider, read your files, run code, or call each other. While a model is running, the only address reachable is the model provider itself. News text is wrapped and labelled as data, with a note that nothing inside it is an instruction. That envelope is not really the defence, though. A headline can still persuade a model. The defence is the next step, where every claim is checked against numbers the headline cannot touch.

4. Check every argument (market data: closed model: idle)

No model runs here. Plain code reads each claim’s disproof condition, plugs in the saved numbers, and gets a straight yes or no. If the condition turns out to be true, the claim is wrong and it is deleted — not softened, not down-weighted. Nothing about the model’s tone or confidence can save it. A claim is also discarded if it cites evidence about another company, quotes a figure that appears nowhere in the data, cannot be evaluated, or sets a test that could never fail.

Survivors are then scored by simple addition: each contributes its confidence, positive claims adding and negative claims subtracting, weighted by subject — business 1.0, price chart 0.8, news 0.5. News counts least on purpose: it is the one input written by strangers. In the 18 September run, 137 claims went in and 127 came out.

5. Let an auditor attack market data: closedmodel: thinking

A second model, Sonnet 5 again but on the highest thinking setting and with by far the largest token budget in the run, goes through the claims that survived and tries to destroy them.

It works in batches of three companies at a time, so five batches for a run of 15. For each batch it sees two things. First, the claims themselves: the statement, its direction, the falsifier, and the evidence IDs cited. It does not see the analyst’s justification or reasoning, so it cannot be anchored by the argument that produced the claim. Second, the evidence: every computed indicator row for those companies, plus any specific record a claim cited.

That second part is the interesting bit. The auditor gets a wider view than the analyst whose claim it is attacking. The business analyst only ever saw the fundamental columns, and the chart analyst only the technical ones. The auditor sees the whole computed row, so it can cross check a claim against figures the original analyst never had to reconcile. A bullish claim about margins can be attacked with the price trend, and a bullish claim about the chart can be attacked with the balance sheet.

Its power is deliberately one sided. The output schema has no field for agreement, no way to endorse a claim or raise its confidence. There is nothing it can return except attacks. And every attack carries its own falsifier and is checked exactly like a claim, so an attack that cannot be tested is thrown out on the same rule as everybody else.

The useful consequence is that a broken or overzealous auditor can only ever make the system quieter. It can remove a pick. It can never add one.

6. Publish what survivedmarket data: closedmodel: idle

No model writes the report. It is assembled from surviving claims, so every sentence traces to a claim, every claim to a fact, and every fact to a row in the frozen data. The disproof conditions stay attached, so a reader can see what would make each argument wrong. The 18 September run published fourteen companies: ten to watch, four to avoid, and no buys.

A flowchart illustrating a process with six steps, including data collection, filtering, argument requests, claims checks, auditor evaluations, and publishing results. Each step is color-coded and linked, with annotations for specific tasks like 'Sonnet 5' and 'Haiku 4.5.'

A) Stock Picks

The shortlist itself, as one table: symbol, the call, the total score, the three dimension subtotals for business, chart and news, and how many claims survived for that company. Fourteen rows for this run, the ten to watch and the four to avoid.

A screenshot of an NSE research tool displaying various stock metrics, including fundamental and technical scores for multiple stocks, along with recommended actions like 'WATCH' or 'AVOID'. The layout shows a snapshot titled 'SEALED WINDOW' with selections for filters and criteria adjustments.

B) Why- the justification

Pick a company from the dropdown and read the surviving claims that produced its score, each shown with its confidence, its direction, and the condition that would have killed it. This is the tab that answers “why did it say that”, and every line traces back to a row in the frozen data

Screenshot of an analytics dashboard focusing on financial metrics, including ROE floor, liabilities, and revenue growth, with various performance indicators and spend plan results.

C) Fundamental analysis

The business claims, one company at a time from the dropdown, because across fifteen companies these run to dozens of rows. Each shows the statement, the model’s own justification, and the falsifier it was tested against.

Screenshot of a financial analysis dashboard displaying key metrics for company performance, including ROCE, liabilities, net profit growth, and earnings growth. The section features a spend plan compilation interface with sliders for candidate selection.

D) Technical Analysis

The same for the price chart claims, read the same way, one company at a time.

Screenshot of a trading analysis tool displaying financial metrics and results related to a stock or asset, including performance indicators and a run summary.

E) News analysis
The news claims, shown whole rather than per company, because there are only a handful. Most companies produced none at all: the provider returned no headlines for the majority of the universe in the window.

A screenshot of a financial analysis tool showing data on stock performance, including metrics such as ROE, liabilities, and various forecast claims for different companies.

Next steps

  1. The current data from Upstox does not allow for walk forward evaluation as the ratios are not dated. I will see if I can get data from other sources which provide more detailed granualar data
  2. Use the entire stock universe of ~2K stocks instead of just Nifty 500 from NSE directly instead.
  3. Use open models Qwen 8B or 14B models as the current runs with Sonnet, Haiku cost money. This will require me to host the Qwen on my Mac

Also see

  1. Sea shells on the seashore
  2. Deep Learning from first principles in Python, R and Octave – Part 3
  3. The Science of Innovation
  4. Presentation on “Intelligent Networks, CAMEL protocol, services & applications”