Releases: fidetolabs/qanat
Release list
v0.2.0
The conversation is the console
The Ask box was one line, repeated identically on four pages -- identical because it
had no relationship to any of them. A text field above an interface you drive
yourself is the wrong shape for a console whose agent can already reach every tool
it exposes.
The thread has a side now, and the space beside it stopped being a page and became
a response surface: ask about a table and the table opens, ask for a replay and the
equity curve is what you are looking at when the answer lands. The tab row is gone,
because it named the same four destinations the state spine already names.
Nothing new is reported to make that work. The agent talks to the console's own HTTP
API, so every tool call was already a request arriving at this server; a middleware
notes what was asked and which surface it is about. The page stamps its own fetches
so they stay out of it -- without that the console feeds itself, a repaint landing in
the trace that tells the console to repaint.
The reply streams. Server-sent events carry the text and the tool calls on one
connection, so their order is the order they happened in, and the client walks toward
the text a few characters a frame because a model emits a clause at a time.
Four surfaces, and one of them was missing
Data profiles what you connected: fill, distinct count and the span each column
covers, computed in the database rather than by paging rows. The coverage bar is the
point -- it turns "57 rows" into "this reaches across a seventh of your history".
Alpha is the graph, and steps are wired on it by clicking the tables that feed
them. from: has always been a list and the scaffold's own portfolio alpha reads
three, but every editor offered one.
Live had an engine nobody could reach. It has a page: the forward return drawn
against the rate the out-of-sample half implied, the next pass date, and what it is
holding now.
Fixed
- Live scoring never ran in a project with more than one alpha. The scheduler called
run_backtestwith no alpha, the refusal went to the event log, and the pass was
retried every thirty seconds forever while the console said everything was fine.
live_alphas:names the choice; a project that cannot say gets one error and stops. live_fromwas stamped before the run, and it writes once ever -- so a pass that
then failed left the date defining out-of-sample pointing at a moment nothing was
ever scored at. It is stamped after the pass lands.- Periods that held nothing were counted as losses in the hit rate and as zeroes in
the per-period average.totalsnow carriesheld_periodsandflat_periods
alongside both figures recomputed over the periods that held something. POST /api/stepspassed its body straight toStep, which forbids extras, so
sourcedied in pydantic -- the console had never once created a step.- Opening any alpha and pressing save wrote an empty options block over its
lookbackandtop_n, because those fields were only drawn for a shelf rule. save_projectwasyaml.safe_dump, so the first edit anything made deleted every
comment inqanat.yaml. Values are laid onto the tree parsed from the file now.- The Ask agent wandered: asked to reshape some ideas it read qanat's own installed
source and then~/.claude/projects. It runs withBashalone and a brief that
says where the project ends, and it no longer builds when asked not to. use_alpharefused on a project with no universe and said to editqanat.yamlby
hand, which an agent over MCP cannot do.save_universeis the tool that error
now names.- The four MCP write tools returned
{"files": [...]}that never held files -- it
wasrep.warningsthe whole time. - The Results header on the backtest surface spilled over the strategies above it.
Its rule ended in a comma and no block, so it ran on to the run log's rule and
tookdisplay:flex;flex:0 0 132px-- which stacked the header onto three lines
inside a bar 26px tall and un-hid the drag grip over the book. qanat --versionand the bannerqanat serveprints both said 0.1.3, because
__version__was a literal that two releases bumpedpyproject.tomlwithout.
It is read from the installed metadata now, so it cannot drift again.
Added
profile_tableandsave_universe, bringing the agent surface to 29 tools.ruamel.yaml, so an edit keeps the comments somebody wrote.
Source for qanat-fdtl 0.2.0 on PyPI. Ledger: audit/FINDINGS.md
v0.1.5
Ask it in plain English
The console has an input box on both pages, and it holds no key. It runs whichever
agent CLI is already installed and signed in -- Claude Code, Cursor -- headless, in
the project directory. Nothing to paste, nothing to bill, and the credential stays
where the person put it.
A DuckDB file takes one writer and the console is holding it, so the agent cannot
open a second qanat mcp in the same project. It is pointed at the console's own
HTTP API instead: the same service layer the MCP tools sit on, reached over the
loopback port already serving. One process, one writer, no lock.
What it did shows underneath as it happens -- the calls it made, then a diff of
what actually changed in the project, weights.momentum · 4 → 5 rows. The panel
floats rather than pushing the report down the page, folds away while it is still
working (the bar keeps a one-line ticker so nothing is lost), and ask becomes
stop while it runs. Stopping kills the process; whatever it already did stays done.
Two pages, not one screen
Strategies answers "did my idea make money": the book as a table across the top,
then the report, equity curve first. Pipeline answers "how was this built": the
graph, the tables, the steps that wrote them. You only need the second when the
first gives you a number worth questioning, and the run log is on both because an
agent can be writing a step while you read a result.
The backtest form asks four questions -- alpha, from, to, rebalance -- with the
other eight under costs and controls. Nothing was removed; you are just not asked
about embargo before you have asked anything else.
The console only answers to this machine
POST /api/steps writes a step script and POST /api/jobs/<id>/run runs it, so a
page that can reach the API can run code on the machine serving it. Binding to
loopback does not prevent that: a site you visit can resolve its own domain to
127.0.0.1 and call the console believing it is same-origin, and CORS never enters
into it. Every request is now checked against the name the browser asked for.
Anything but loopback is refused, and serving under a real hostname is a deliberate
setting: QANAT_ALLOWED_HOSTS=qanat.example.com.
Who did it
Every event carries an actor -- agent, you, or schedule -- and the run log
shows it. A project that changes while you are reading something else should say
who changed it.
Fixed
- The console's
api()helper accepted an options argument and ignored it, so every
caller that thought it was posting was quietly issuing a GET. - The README led with the mechanism. It now says what the thing does first.
Source for qanat-fdtl 0.1.5 on PyPI. Ledger: audit/FINDINGS.md
v0.1.4
The console itself, in the terminal
qanat tui is the console without the browser. Three panes: the DAG on top, a chart
selector, and every alpha this project declares or has ever priced with its last run
beside it. Move with the arrow keys or j/k, Enter to open a result, h/l to
change chart between equity, drawdown, per period, turnover and holdings.
Enter on an alpha nobody has priced runs the replay and draws it as it happens. The
DAG fills in from the left once per as-of date, the curve gains a point per closed
rebalance, and the alpha's row counts up -- all off progress.snapshot(), the same
in-memory record qanat serve polls, read from the process doing the work. Only the
lineage the replay actually walks is drawn, so it is four boxes rather than seventeen.
No new dependency. Raw mode, the alternate screen, a repaint that sends only the rows
that moved, and the character grid qanat graph already draws on. Curves are braille,
which makes a 60x8 box a 120x32 plot; --ascii gives that up for terminals that need
it.
q will not quit out from under a running replay -- Q abandons it, and says that
the tables it rewrote need qanat run. A terminal that goes away ends the loop rather
than spinning on it.
The console's picture, in the terminal
qanat graph draws the pipeline where you already are: a column per stage, a box per
table, the step that writes it on the arrow, and the four stage colours the console
uses. It reads build_graph — the console's own read model — so an arrow here is an
arrow there and a row count is the same count(*).
Columns are layers rather than stages, because a features stage is allowed to chain
and an edge inside one column would have to leave it and come back. A stage owns as
many columns as its longest chain, and the rule across the top says which. An edge
that skips a column is given a row to pass through, and nothing else is placed on
that row — two lines sharing a row in a character grid are one line, and would claim
an edge that is not there.
--color auto|always|never (and NO_COLOR), --ascii, --no-labels, --width.
Piped, it keeps its full width and drops the colour; on a terminal too narrow for the
step names it says so rather than wrapping.
Source for qanat-fdtl 0.1.4 on PyPI. Ledger: audit/FINDINGS.md
v0.1.3
Qanat 0.1.3 — source for qanat-fdtl 0.1.3 on PyPI.
The nine findings 0.1.2 left open are closed. Nothing in the ledger is open now:
67 fixed, 6 partly fixed, each of those six with a note saying what remains.
audit/ holds the write-ups, the reproduction scripts and the ledger.
The graph is the real graph
A .sql body could read a table the step never declared. ctx.read has always
refused that and said why; naming the table in SQL went straight round it. Every
consequence was quiet — the console drew an arrow that was not where the data
came from, qanat check warned that a table two steps read "is never read by
anything", and plan().stale() never marked the consumer, so it stayed one
generation behind for ever while reporting ok.
Both doors are shut, at check time and at run time. Comments and string literals
are ignored, so a table name inside a comment is still a comment.
Upgrading: this can fail a qanat check that passed before. If it names a
step, add the table it reads to that step's from: and the check passes.
A job that will not finish
A job had no time limit and no way to stop it, so it held its worker until the
process ended — and four of those stopped the scheduler dead, with nothing but
warn events to say so.
A job may now set timeout: 10m, or a project may set job_timeout: for all of
them. On expiry the scheduler frees the worker and closes the run row as
timeout instead of leaving it saying running. DELETE /api/jobs/{id}/run
does the same on demand, and /api/graph reports workers and busy so
starvation is visible without reading the log.
Nothing in this process can end a running Python thread. The job finishes on its
own and its result is discarded — the message says that rather than implying the
work stopped.
The console, for anyone not using a mouse
- A folded panel no longer keeps its buttons in the tab order.
overflow: hidden
clips pixels and removes nothing, so a closed rail kept every control focusable
while invisible. editon an alpha card is a real button beside the card, not a<span>inside
it — pressing Enter on the card always picked the alpha and never opened it, so
editing had no keyboard path at all.- A column header is a button with
aria-sort. Sorting was bound to<th>,
which cannot take focus. - A rebalance can be opened without dragging across a canvas.
Smaller things
The lookahead guard now names the tables it skipped and why — it returns early
when a table has no clock, and such a table is not filtered by the as-of views
either. A store outside the project directory is called out. payload: true is
documented for the shapes that need it, rather than for one that works without.
A regression this audit caught in its own fix
The 0.1.2 fix for live progress put its two helpers in the wrong one of
backtests.js's two IIFEs. tick threw ReferenceError: backoff is not defined
on its first call, its catch threw the same error again, and nothing ever
rescheduled — the poller was dead from page load, which is precisely what that
fix was for.
Found the same way everything else was: by opening the console in a browser and
reading what it did. It is now checked by counting the requests the poller makes
rather than the code that should make them.
Verified
138 tests, 36 of them one per finding, named for its id. Suite green on 3.10 and
3.12 including Postgres, ruff clean, the demo runs, and the leak battery still
reports the cheating alpha earning the same 3.40% as the honest one.
Full notes: CHANGELOG.md · Ledger: audit/FINDINGS.md
v0.1.2
Qanat 0.1.2 — source for qanat-fdtl 0.1.2 on PyPI.
Qanat was broken on purpose across three surfaces, and this release fixes what
that turned up. 73 findings: 58 fixed, 6 partly fixed, 9 left with a reason.
The write-ups, the scripts that produced them and a ledger of every finding are
in audit/.
Four ways an alpha could see the future
Each was worth 1292% against an honest 3.40%, with no failure, warning or note
in the report:
ctx.store.read()ignored the replay clock — no SQL needed.ctx.sql("… FROM main.raw__bars")stepped around the as-of views, because a
schema-qualified name was not rewritten.- A timezone offset was thrown away, so a
16:00-05:00close was visible four
hours early. - REST
fetched_atlanded as TIMESTAMPTZ, hiding every row from a replay for
the machine's UTC offset.
All four are closed. A step is arbitrary Python and can still read a source file
off disk, which nothing in-process can prevent; that is now documented as the
boundary rather than left implied.
Numbers that looked right and were not
A delisted holding earned zero while still being counted as held, with no note.
A loop between two steps passed qanat check and multiplied feature values
ten-fold on every run. The digest ignored the data, so compare blamed the
engine when the numbers in a source file had changed.
Landing data from the web
Tested against live public APIs — the ECB, Nager.Date, Open-Meteo, USGS,
Coinbase, the World Bank. 005930 no longer becomes 5930. Epoch and year
clocks work. A reordered feed no longer writes into the wrong columns. A column
that is all-null in the first response no longer breaks the second poll.
Crash and concurrency
An interrupted replay is noticed and repaired instead of leaving the tables
truncated to a past date. One replay at a time, whoever asks. qanat.yaml is
written atomically under one lock.
The console
A keyboard path into the pipeline graph (15 tab stops to 51), a visible focus
ring, fetch timeouts so a hung server is noticed, errors that survive the next
repaint, and a confirmation before delete.
Keeping it fixed
tests/test_audit_fixes.py holds one case per finding — 29 of them, each named
for the id in the ledger. The suite is green (131 passed) and ruff is clean.
Nothing changes how a project is written. The demo, the examples and existing
qanat.yaml files still run — several now report things they used to swallow,
and a few now refuse what they used to accept.
v0.1.1
Qanat 0.1.1 — source for qanat-fdtl 0.1.1 on PyPI.
Two console fixes, both found by using it.
The backtest run form could not be scrolled
#editor-body carries the flex/overflow rules that let the alpha editor scroll.
#runner-body never got the matching rule, so the run form kept its full content
height whatever the window: the wheel did nothing, and run it sat below the
fold, out of reach on any display under about 1030px.
An alpha could be pointed at a table that holds no prices
Every script on the shelf reads one table with a symbol, a date and a price,
but the console offered every table in the project and the save checked only
that the table existed. Wiring one to something like a market-wide regime
series saved cleanly, passed check, then died once per as-of date mid-replay
on a bare KeyError: 'date' naming neither the table nor the column. The save
now refuses it, and says which column is missing and what the table holds.
Upgrading
uv tool install --force qanat-fdtl # or: pip install -U qanat-fdtl
Both fixes are console-side. No project file has to change.
PyPI sha256
qanat_fdtl-0.1.1.tar.gz 79edcbd4c7066d87590a16b16aa71daa38b17d16c2f1ec26208bccb0063d9eda
qanat_fdtl-0.1.1-py3-none-any.whl 96c831095ecc55977c9137f9df9c4f165dec811a9e8afdb858fc35e6b4d20a77
v0.1.0
Qanat 0.1.0 — source for qanat-fdtl 0.1.0 on PyPI.
Provenance note
This tag does not point at the commit named in PyPI's attestation. The
repository history was rewritten and republished after 0.1.0 was uploaded,
so the attested commit f878c9f8f018bfc99e96a02544cb8699f7f8e056 no longer
exists. The published artifacts were never rebuilt or replaced.
The source tree is unchanged. To verify:
git checkout v0.1.0
pip download qanat-fdtl==0.1.0 --no-binary :all: --no-deps
tar xzf qanat_fdtl-0.1.0.tar.gz
diff -r qanat_fdtl-0.1.0 . -x .git -x PKG-INFO
PyPI sha256
qanat_fdtl-0.1.0.tar.gz 42cbcb9939f5f0486fb665c918a73700ae4b41956119ffabaeff41f9081e2325
qanat_fdtl-0.1.0-py3-none-any.whl da87e5baff12423a13445d9b8b76150e9c0ccceca2a57cf6db3cbe44196c0dfa