Skip to main content

A qanat cut open: shaft mouths and fields on the surface, the tunnel running beneath them, and a man walking it

Qanat

A data platform for processing metrics from filings.

License Python Status Discord members

What Qanat is

Qanat is a data platform for processing metrics from filings. It reads company filings, computes the metrics you define, and answers as of any past date.

What you came for is a screen: define a metric, set a threshold, get the companies that pass. Every stock screener on the internet screens today. This one screens as of any past filing date, with no look-ahead -- which is the whole difference, and the reason for everything below.

Filings are the subject, not a limitation of the plumbing. The tables are called documents, facts and spans, and the SEC is one connector among five. What narrows is the promise: this reads filings and makes metrics, and that is the thing it intends to be good at.

It does four things:

ingest pull data in from wherever it lives
save keep it with its dates, the way it arrived
transform clean it, and work out what you define from it
serve hand it back to your agent over MCP

A scheduler keeps the ingesting and transforming going. A source pulls on its own clock, and a step starts when the table it reads is ready.

Why the dates matter

A company publishes its results weeks or months after the period they cover. So if you ask what a company looked like in March 2022, you should only see what was public in March 2022.

Qanat keeps two dates on every number: the period it covers, and the day it became public. Ask about a past date and you get what was public by then.

How it is put together

One store. Parquet files read by DuckDB. Point store: at a directory and the tables are files in it; point it at a .duckdb path for a single file, or a postgresql:// DSN for a server.

A directory is the default, and the reason is the second person. A DuckDB file takes an exclusive lock, so while one person is ingesting, a teammate opening the same project is refused -- not slowed, refused. With parquet the writer adds a file nobody is reading, so a reader is never blocked. The forty filings below also take 6.7 MB as parquet against 37 MB in a DuckDB file.

Layers. Data moves forward through them. The first layer holds what arrived and nobody edits it. The ones after hold what you worked out. You name them.

Steps. A .sql file, or a .py file with a run(ctx). A step says which tables it reads and writes, and it can only read what it declared.

Sources. Five connectors: SEC EDGAR, Yahoo Finance, HTTP, SQL and CSV.

Metrics. A formula with a name and a version. Shared, because price / diluted EPS is arithmetic with no judgement in it. The threshold you compare it against is yours and never lives in the project.

The MCP server. How you talk to it. There is no website. Your agent is the screen.

How to use it

You need Python 3.10 or newer.

uv tool install qanat-fdtl                 # or: pip install qanat-fdtl
export SEC_USER_AGENT="your name you@example.com"
qanat init my-research --demo && cd my-research

The demo pulls four real 10-K filings from SEC EDGAR and reads the numbers out of them, in under a minute. There is no API key and no account: the SEC asks only for a contact address so it can see who is making the requests.

Then point an agent at it:

claude mcp add qanat -- qanat mcp --scope research

Any MCP client works:

{ "mcpServers": { "qanat": { "command": "qanat", "args": ["mcp"], "cwd": "/path/to/my-research" } } }

Now you can ask for things:

"What data is in here, and where did it come from?"

"Which companies passed my rule in 2022?"

"What did those companies do over the next year?"

Pick how much the agent can do

--scope sets which tools your agent gets. Each level includes the one before it.

Tools What it can do
--scope data 12 read the tables as they stood on a past date, and read the filings
--scope research 15 and read a step, and screen
(default) 23 and write steps, sources and universes

Every tool description is sent to the model on every request. A smaller scope costs fewer tokens and gives the agent a shorter list to choose from, so pick the smallest one that does your job.

The project file

Everything is in one qanat.yaml.

project: research
store: ./data            # a directory: parquet files, read by DuckDB

stages:
  - { id: raw,        kind: raw }         # what arrived, never edited
  - { id: normalized, kind: normalized }  # typed and de-duplicated

# Which column says when a row became public. Without this, a screen reads
# every row ever written, including the ones published after the date it is
# pretending to be.
time_columns:
  raw.documents: published_at

sources:
  - id: filings
    to: [raw.documents]
    connector: sec
    options:
      companies: [AAPL, MSFT]
      form: 10-K
      years: 5
      user_agent: ${SEC_USER_AGENT}

steps:
  - id: parse_facts
    from: [raw.documents]
    to:   [normalized.facts]
    script: steps/parse_facts.py
    options: { reads: raw.documents }

A step in Python:

def run(ctx):
    docs = ctx.read("raw.documents")   # only tables listed in `from`
    return df

ctx.read() refuses any table the step did not declare, so a missing dependency is an error you see instead of a wrong number you don't.

You choose the layers and what they are called. Qanat enforces four properties and no names: a landed layer is never written by a step, a step reads and writes only what it declared, the graph has no cycles, and every row carries both dates. raw and normalized above are a worked example, not a required shape.

A pipeline stops at clean dated facts. There is no metrics layer and no results layer, because a metric is a query over the facts and a screen is a call that returns a list. Neither is a table Qanat owes you, so neither needs a stage to land in. qanat check warns that nothing reads normalized.facts, and that warning is correct.

Run qanat check before anything else. It reads the file and tells you what is wrong with it.

Screening

One verb, one date or a window. A window of one is a single date, so the rows come back the same shape either way and you never branch on which you asked for.

qanat screen --filter '[{"metric":"pe","op":"<","value":30}]' --as-of 2025-12-31
qanat screen --filter '[{"metric":"pe","op":"<","value":30}]' --from 2021-12-31 --to 2025-12-31

Three things come back, and nothing else:

the list who passed, on each date
the return per company, from its own filing to its next one
the count how many could be evaluated, and how many could not

The count is there because a list is a claim. A screen that quietly dropped half the market has told you something false about who passed.

Nothing is added up. No average across years, no comparison against the companies that failed, no equity curve. Adding returns together needs a decision about how much of each company to hold, and that decision is yours and is made outside this tool.

The returns run between each company's own dates. Costco files in October and Walmart in March, so there is no shared calendar to price them on.

Forty real filings to try it on

examples/sec-10k is eight companies over five years, downloaded from EDGAR: Apple, Microsoft, Alphabet, Meta, NVIDIA, Walmart, Target and Costco. 43,104 tagged numbers and 904 document sections. Their fiscal years end in January, February, June, August, September and December, so the two-date clock gets a real test rather than a convenient one.

More in docs/contract.md, docs/words.md and docs/agents.md.

Cautions

It is beta. It works for my own use and few other people have run it. If something breaks, open an issue or say so on Discord.

One connector so far. SEC EDGAR, and it reads 10-K filings. Anything else you bring yourself through the HTTP, SQL or CSV connectors. More will be added one at a time, each finished before the next is started.

No prices. A filing does not contain one, and nothing in here fetches one yet. So you can screen on what a company reported and you cannot yet ask what the list did next.

You write the formulas and pick the numbers. Qanat works out what you asked for and reports what came out. Choosing a threshold is your job.

The dates protect a step, not you. A step cannot see past its date. You and your agent still can, while you are deciding what to try.

It cannot place an order, and no agent it serves ever will.

Prices come from Yahoo, through yfinance. That is not an API with a contract behind it, and Yahoo's terms do not grant redistribution. Every row carries source: yfinance so a later cleanup has something to act on, and it is behind the prices extra rather than installed by default. It is also unofficial: Yahoo changes its endpoints and the library catches up afterwards.

Section detection is a heuristic. A 10-K names every Item on its contents page and again throughout the prose, so most matches for "Item 1A" are not the heading. It gets all 904 sections in the bundled example right and it will meet documents it has not seen. Every span carries the rule that found it, so you can tell how much to trust one.

Still missing: industry grouping, so a ranking is against whatever list of companies you hand it. Also backfills and incremental updates.

For general pipelines, use something else. Airflow, Dagster and Prefect run any DAG and run it across many machines. Qanat runs one shape.

Pull requests welcome. See CONTRIBUTING.md.

License

MIT. See LICENSE.

Copyright (c) 2026 fidetolabs

Metadata

Release files for qanat-fdtl 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for qanat-fdtl 0.6.0
File Size Uploaded Provenance
qanat_fdtl-0.6.0.tar.gz 715.4 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for qanat-fdtl 0.6.0
File Interpreter ABI Platform
qanat_fdtl-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 843.3 kB

Release files / qanat_fdtl-0.6.0.tar.gz

Download URL qanat_fdtl-0.6.0.tar.gz
Size 715.4 kB
Tags Source
SHA-256 checksum
How to use checksums
bb77cdf87c9b3fe707a7338226ac5eda6f442096c97f932653db527258efc11f
BLAKE2b-256 checksum
How to use checksums
8825302d095b20e497e307f813c0b1aeee97aa116e1ac184dc30b64e7611ea2c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Release files / qanat_fdtl-0.6.0-py3-none-any.whl

Download URL qanat_fdtl-0.6.0-py3-none-any.whl
Size 127.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1a9e6ddc16d19d3c8efc6897d77bfd93f1082be421ede32919795d0ff9f6ae03
BLAKE2b-256 checksum
How to use checksums
a64de6eb0e39b693b6c815204c1d4c2cf6ad0c2852ca8e5329b38cdcf99f4096
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Security for qanat-fdtl 0.6.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for qanat_fdtl-0.6.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page