Skip to content

Experiment 1 — the changelog (history)

This page is history. It records every change to the app, but not to the agent, from the start of the project until experiment 2 began. The current changelog is the changelog, and experiment 1's agent changes are on the experiment 1 journey.

The text below is the changelog as it stood when experiment 1 ended, unchanged.


Everything that changed about the app but not about the agent. Deployment, setup, guards, infrastructure, the site, the docs and dependencies. The Claude Code tooling in .claude/, such as hooks, skills and the usage bar, is not the app, and its changes are not recorded here.

Changes to what the agent does, what it is shown, or what its record contains go in the journey instead, and that page states the test. The split exists because the journey answers one question — when did the experiment's question change — and it can only answer it if the reader is not wading through image builds and CSS.

Entries are one or two lines: what changed, and why. Newest first.

2026-10-02

  • Dependencies — The TradingAgents pin moves four commits. propagate() takes an on_chunk callback to follow a run and to cancel it, and passes the graph callbacks to the stream. The Gemini client now takes client_args, so a caller can count each HTTP request, SDK retries included. This app does not use either yet. The analyst change in the same move is in the journey.
  • Docs — The last two sections of docs/dashboard.md named pages that were merged on 2026-09-02 and now only redirect: Tickers, Signals, Alerts, Events, Journey, Digest and Regime. They now describe the current pages, with the four stock cells on the research page's candidate table.

2026-10-01

  • The site — The research page's candidate table shows the four stock cells the agent reads beside each name: Trend, 1m / 3m, Volume vs normal and Range/day. GET /api/watchlist/candidates carries them as the rendered strings the prompt shows, so the page and the prompt cannot say different things, and the static site's snapshot carries them too. The four terms are in the glossary.
  • Tooling — probe_prompt.py keeps the finished samples when one request fails. A failed sample is written as a row with its error. Before, one 503 ended the run and lost every finished sample, four of nine on 2026-10-01.
  • The site — The Decisions page on the live dashboard shows a card while a decision pass runs: when it started, what the agent was told on waking, the turn, what the pass is doing now (asking the model, carrying out orders, or waiting for research), and each analysis it waits on. It asks GET /api/agent/pass every 15 seconds, and when the pass ends it fetches the newest month again so the pass appears. The public site never asks, and the snapshot exporter writes no file for it, because it describes the current second.

2026-09-30

  • The site — The Experiment page no longer quotes "Doing nothing is a valid answer, and often the right one" under a day with no trades. The agent's prompt dropped that rule on 2026-09-30, so the page now says only "It made no trades that day."
  • Tooling — backend/services/watchdog.py now defines its market-hours constants before it imports bars. The two modules import each other, so a script that loaded watchdog first, such as the intraday backfill, failed. A test now imports each of four modules first in a fresh process. The app loads bars first and was not affected.
  • Data — With BROKER=alpaca and no Webull client, the 1-minute bars behind the charts now come from Alpaca's bars endpoint (alpaca_broker.get_minute_bars), on the sip feed and about 16 minutes behind, because a free key may not read newer sip bars and the bars endpoint refuses delayed_sip. Before this, such a deployment had no intraday chart. Daily bars are unchanged.
  • The site — The ticker page has a "Latest analysis" card: the call, the price, the time horizon, why the analysis ran and the rationale. The timeline shows the time of each record on the reader's clock, and its empty text no longer tells the reader to run an analysis.
  • Docs — README, docs/index.md, docs/overview.md and docs/deploying.md now name Alpaca paper as the second broker, alongside the Webull sandbox, instead of only setup.md. The Book page's broker warning now says "paper mode" and "paper credentials" in place of naming Webull, to match the broker-neutral check behind it (broker.is_paper()).

2026-09-29

  • Tooling — probe_prompt.py now sends the real key for a hosted OpenAI-compatible provider, read from the same provider registry analysis._models_auth_header uses. Before this it always sent the placeholder key "ollama", so a probe against any provider but Google or the local pool failed to authenticate.

2026-09-28

  • The site — The Decisions page shows the agent's memory notes. Before this, no page showed them, and only a database query could show a note that was written and then cleared. Each turn now shows the memory orders it sent, with "Cleared every note" or "Removed note N" when the record keeps the action. A turn recorded before 2026-09-28 shows the text only. A new panel, "What it remembers now", shows the current list, with when and where each note was written. GET /api/agent/memory serves the list, and the static site's snapshot export writes it to agent_memory.json.

2026-09-26

  • Broker — A second broker, Alpaca paper, beside the Webull sandbox. BROKER=alpaca selects it, and webull stays the default. Alpaca paper keys need only an email address, so a person without a funded Webull account can run the experiment. backend/services/broker.py picks the module, and alpaca_broker.py applies Webull's four guards in Alpaca's terms. See setup. Fills and an OCO on a real position are not proved yet: run python -m backend.scripts.alpaca_paper_check during a session first.
  • Scripts — python -m backend.scripts.fetch_audit counts each fetch the agent calls on the tool channel, and the passes whose reasoning carries a figure that only the fetch's result held. This is step 1 of PLAN.md.
  • Scripts — python -m backend.scripts.prompt_cost shows where the prompt tokens of a tool-channel pass go: each section of the prompt, each fetch result, and each pass's reported total. It trims nothing.
  • Analysis — tradingagents/dataflows/laya.py is a client for a laya sidecar at LAYA_URL. Nothing depends on it: when the variable is unset, or the sidecar fails, the caller continues with no grade.

2026-09-25

  • Setup — compose.example.yaml, the README and "Running it yourself" now use the published image, ghcr.io/nandyalu/ten-acre:latest, so a Docker deployment needs two files and no clone or build. The image is x86-64 only. Building the image yourself stays documented, for ARM and for your own changes, with ten-acre:local and pull_policy: never.
  • Trade stream — A fill the stream settles goes through scheduler.announce_fills, the same path the watchdog uses, so it posts to Discord, writes the alert row and wakes the agent. It used to post a stop fill to Discord and nothing else. See JOURNEY.md.
  • Scheduler — The jobs do their work on quiv's threads now, and quiv records each job's real duration and real failure. Until now every handler handed a coroutine to the main loop with run_on_main and returned, so quiv showed a one-millisecond success whatever the job did. Only notify and the analyses still go to the main loop, through quiv's call_on_main, which waits for the result. The alarm, the five-minute backstop and the final pass are one task, agent_pass. What this changes for the agent is in JOURNEY.md.
  • Dependencies — quiv moves from 1.0.0 to 1.2.0, for call_on_main and update_task(run_at=...).
  • Deployment — The app gives the scheduler 10 seconds to stop at shutdown and 3 more for work queued on its loop, and compose.example.yaml sets stop_grace_period: 20s so Docker does not kill it first. Both deployed compose files need the same line.
  • The site — The trade log, the position history and the analysis list are filtered and paged in the browser now, through a shared PagedRows, <app-table-search> and <app-pager>. The trade log and position history used to render every row at once with no limit; the analysis list used to show only the newest 50 with no way to reach an older one. A short table column now takes a col-fit class so it stops sharing width with the one column that needs it.
  • The site — The Journal page parses a day's Markdown into the parts it is written in — the day's book line, its events, the agent's own words as a quotation, any milestone — and renders each on its own, in place of one preformatted block that showed the raw list markers and asterisks.
  • The site — A collapsed day on the Decisions and Journal pages now carries a one-line summary — the passes and what they did, or the day's book line and what happened — so a reader can tell which days are worth opening without opening each one.
  • The site — The watchlist table on the Research page adds a "Last analysed" column, so a ticker's staleness is visible without opening it.
  • The site — The line over a turn on the Decisions page no longer borrows the prompt's own "Why you are awake" / "Why this pass started" headings, which read as though the page were speaking to the reader as "you". It now says "What woke it" on the first turn, and derives "Why it was asked again" on a later one from the prompt's own "Why you are asked again" sentence rather than repeating the first turn's wake line.
  • The site — The home page moves the current equity and return above the fold, drops the logo mark from its header now that the masthead already carries it, and sets the guardrails beside the headline instead of below it. A narrow screen also gets a one-line "Next", naming the first thing in the day's rhythm that has not happened yet.
  • The site — The "By ticker" table on the Scorecard page no longer stacks its rows on a phone; two columns fit as a row, where stacking spent two lines per ticker on one fact. The stat tiles sit two to a row there, and an odd last tile takes the whole row rather than half of it.
  • The site — A pass card on the Decisions page has one "Show the transcript" button in place of three. It opens two columns for each turn: the prompt and what each fetch returned on the left, the model's thinking and its raw JSON answer on the right. The separate answer panel repeated the JSON the turn's own reasoning and orders are read from. The evening-review card opens the same way, both prompts left and the thinking and both answers right. The prompt, the fetch results and the thinking render as Markdown through a new <app-markdown> component on the marked package, with Angular sanitising the output; the copy button beside each block still carries the raw text.
  • The site — Scrollbars follow the theme the page is in, and are thin and drawn in the page's own colours everywhere. The root declared color-scheme: light dark, which let the browser draw them from the OS setting, so a reader on a dark OS who chose the light theme got dark scrollbars.
  • The site — The Book page shows every time on the reader's clock, as the Research and Decisions pages already did; it printed the ledger's UTC stamp. In the trade log the two exit legs a buy rests at the broker carry a stop or target tag and sit muted under their buy. On a phone a stacked row hides the cells that hold only a dash.
  • The site — The Decisions page can show the system prompt: the rules every pass gets before its own prompt, closed by default behind a "Show the system prompt" button. A new GET /api/agent/system-prompt route serves the text in use now, and the static site's snapshot export writes it to agent_system-prompt.json.

2026-09-24

  • Data — A new table, agentreflection, holds the agent's evening review: one row a trading day, with both prompts and both answers verbatim. The migration runs at startup like every other. What the review is and what it changes for the agent is in the journey.
  • Infrastructure — The daily 21:30 UTC job runs the evening review last, after the grading and the journey, under the pass lock, so a pass still running finishes first and none starts under it. The review posts to Discord only when it sent a note or changed memory; most days it says nothing, and that is the expected answer. It runs on the tool channel only, so a deployment on the JSON channel skips it with a log line.
  • The site — The Decisions page shows each evening review inside its day, above the passes, with the notes it sent, the memory it changed, the note it wrote for the next pass, and both prompts and answers behind a disclosure. The Notes page lists the review's notes beside the ones a pass leaves, each with the pass it names. GET /api/agent/reflections serves them, and agent_reflections.json on the static site.
  • Tooling — probe_prompt.py --turn reflection probes the review out of the app: the real prompt from the database as it stands, both calls, nothing applied and nothing recorded.
  • Repository — The notes for each release are now in docs/releases.md, and a new release skill gives the steps to cut one. Before this, the notes were only on GitHub, and v0.2.0 and v0.2.1 had generated notes only.

2026-09-23

  • Setup — A new optional setting, AGENT_DECISION_MODEL, lets the agent decide with a different Gemini model than the analyses. Google limits each model on its own, so the decision model has its own limits in AGENT_LLM_REQUESTS_PER_MINUTE, AGENT_LLM_TOKENS_PER_MINUTE and AGENT_LLM_REQUESTS_PER_DAY, and its own stored day's count. The text fallback asks the same model, so a failed call cannot put a second decision-maker in the record. Unset, nothing changes. A deployment that turns it on needs a journey entry that day, because the model that decides is the experiment.
  • Data — Each turn of a pass records the model that answered and, on the tool channel, the input tokens Google served from its cache. Both are absent on turns before today. Each fetch round sends the whole conversation again, so the cached count shows what that really costs. See the journey.
  • Infrastructure — A function response to Gemini carries the id of the call it answers. Google's migration checklist for gemini-3.8-flash requires it. The app sent the name only, which the models in use accepted.
  • Setup — EXPERIMENT_START_DATE wins over the stored start date, and switching the agent on stamps that date rather than today when it is set. A deployment that ran from 2026-09-03 with no stamp was stamped 2026-09-23 when its settings were saved, and the variable, which its docstring called an override, could not correct it.
  • Tooling — probe_prompt.py probes the decision model, and sends each request once. The SDK retries a 503 by itself, the throttle does not see those retries, and on 2026-09-23 a free key counted five requests for four calls.
  • Repository — A new autouse fixture keeps research charges in memory for every test. agent_book.build_book subtracts every charge in the database from cash, and a developer's data/trading.db copy carries $3.45 of real charges, so 19 book and performance tests saw $996.55 where they expected $1,000. Same fix as isolated_ticker_status.

2026-09-22

  • Infrastructure — The release workflow now also builds and pushes the image to ghcr.io on every tag, tagged with the tag name and latest. Before this, the only artifact a tag produced was the wheel-and-constraints zip; a deployment still had to build the image itself from source.

2026-09-21

  • The site — Every turn of a decision pass on the Decisions page now opens with why the agent was awake: its own chosen time, a move it slept through, the last call before the close, a change to the app. It is the sentence the agent itself read, under the heading its own prompt used — "Why you are awake" on the first turn, "Why this pass started" on every later one, because by then the wake is history. The page could say what a pass did and never what asked it, because nothing stored the reason; see the journey. Absent on a pass recorded before today, and on a skipped pass, which never reaches the model.
  • Order messages — A refused stop or target now says why. adjust_exits names the level, the price and the rule, for example "a stop at $336.00, above the price $335.21: a sell stop above the price triggers at once". Before, it said only "No usable level", and on 2026-09-21 the agent asked for a stop above the price three times in a row (run 73) because nothing told it that this fires at once. A refused leg is also named when the other leg goes through.
  • Order messages — The fill line for a buy names the exits that really rest at the broker. It printed the signal's stop and target before, even when _place had dropped a level the price had passed or put a volatility stop in its place. It now says which level was not placed and why, and when a volatility stop replaced the signal's stop. arm_exits_now is unchanged.

2026-09-20

  • The site — A ticker on the Decisions page — an order, a refusal, a failure, a fetch — now links to its own research page, the same way every other page already links a ticker. It used to be plain text, the one thing on the row a reader would want to follow.
  • The site — An order row on the Decisions page shows what it asked for beyond its ticker and size: limit $330.00 · stays past today for a limit buy, market order for a market one, stop to $334.16 for an adjust. Nothing showed this before — a limit buy and a market buy read the same, and an adjust said only which ticker it touched — because the fields existed and were dropped before anything wrote them down; see the journey. Absent on anything recorded before today.
  • The site — Every turn of a decision pass is now its own bordered block on the Decisions page, one turn included. The turns used to be a flat run of labels and lists, so a turn's orders sat nearer the next turn's label than their own and could read as though they belonged to it — and the blocks needed two turns to appear at all, so a pass that read an analysis and then bought on it showed the buy with no sign of the read behind it, and the same pass was drawn two ways depending on a count no reader could see. A row inside a turn is what the agent asked for on that turn, which the page now says out loud above the refusals and failures under it. A pass that kept no turn — one from before 2026-09-10, or a single-turn pass that fetched nothing — still shows its own summary instead.
  • Market data — Reddit is searched through a public custom feed now, one request for every subreddit in it, on the trawl path and the RSS path both. Measured for NVDA over one week: the same 12 posts as three per-subreddit requests through trawl, in one request and half the time, and 13 through the feed's own Atom search. REDDIT_MULTIREDDIT_URL names your own feed, and the literal off searches the three subreddits one by one. A feed that cannot be read falls back to those three, so a feed this project does not own cannot take the sentiment report down with it. What the analyst reads changed with it — see the journey. docs/setup.md says how to make a feed.
  • Setup — The docs for REDDIT_TRAWL_URL now name what the variable really wants: any service that fetches an HTML page through Cloudflare and a captcha, such as trawl or FlareSolverr. They also state a floor of 3 GB of RAM for it. Three search pages load at once without a custom feed, three browser sessions peaked at 2.5 GB, and at a 2 GB limit one page answered HTTP 500 and the sentiment analyst was told there was no discussion at all.
  • The analysis pipeline — The vendored TradingAgents moves to v0.5.0, upstream's first tagged release since v0.4.0. 29 of its 60 commits are on our branch. The ones that do not change an analysis: vendor API keys are kept out of request errors by one shared helper, a throttled vendor chain answers "unavailable" instead of ending the run, an indicator Alpha Vantage cannot compute falls through to the next vendor, an outage is told apart from an unknown symbol on every Yahoo path, a failed reflection no longer stops the next run, US statements can be served as filed from SEC EDGAR, and six dependencies nothing imports are gone. What changed for the agent is in the journey. The backtest, the portfolio input and the past-date work were left upstream on purpose; .claude/rules/tradingagents-submodule.md lists them for whoever builds the backtester.
  • The site — "mechanical signal-follower" is a glossary term now, defined once and used on the Auto trader page, which had explained it inline. The definition states the whole rule: buy on a Buy signal whenever one of the five slots is free, hold until a Sell signal or the call's maturity date, whichever comes first. It also carries what the baseline can and cannot measure — it follows only the signals the agent paid for, so it weighs the agent's judgement and not its choice of what to research. Both rules behind it changed the same day — see the journey.

2026-09-19

  • Dependencies — quiv moves from 0.10.0 to 1.0.0, which was released on 2026-09-17. The lock was never refreshed after it shipped, so the container ran 0.10.0 while the 2026-09-18 entry below already cited 1.0.0's soak test as a reason for a design choice. The one breaking change in 1.0.0 renames the three injected handler parameters, and no handler here declares any of them; the other six migration steps were already settled when this app moved to 0.10.0. The run_on_main traceback that every shutdown logs is not fixed by the upgrade, because 1.0.0 carries the same code, and the fix for it is not released yet.
  • Infrastructure — The wake for a new change note records its count when the pass completes, not when the wake is scheduled. Today's redeploy replaced the container twice, 35 seconds apart: the first start recorded the count and scheduled the pass, the stop cancelled it, and the second start found nothing new, so the pass never ran. A wake whose pass is lost to a restart now repeats on the next start.
  • Market data — The fundamentals fetch reads Yahoo Finance for a ticker's ratios and its last four quarters, through the same yf_retry as the bar cache. Webull's sandbox host has no route for the /openapi/fundamentals/ family, so the financial indicators and the industry comparison had answered 404 on every call since 2026-09-17. The analyst rating stays on Webull. What the agent sees now is a journey entry.
  • Docs — The "did it buy on a Hold" question in finding-your-edge follows the rule rewrite of the same day (see the journey): the prompt no longer forbids buying on a Hold, so the question is whether the agent read the plan behind one or bought the word.

2026-09-18

  • Infrastructure — The backstop tick that re-reads the agent's stored wakeup runs every five minutes, not every minute. It guards only the startup restore of the alarm: quiv 1.0.0's 24-hour soak test covers the rest, and its roadmap keeps durable persistence out, so the restore stays. The final pass moved out of it into its own one-off, five minutes before the close, which is a journey entry. The container log loses about 3,400 lines a day.
  • Repository — The sandbox-broker tests clear WEBULL_ACCOUNT_CLASS, which backend/main.py loads from a developer's .env. A .env naming the margin account made two of them fail while the code was right.
  • Site — The performance comparison draws the SPY buy-and-hold line again. Since 2026-09-10 it read SPY's current price from the price cache, which holds only tracked tickers, and SPY is never tracked, so the line was silently left out for eight days. It now prices from SPY's own last bar, which already carries the session in progress.
  • Market data — The Yahoo trending candidate source sends a browser User-Agent. Yahoo began answering the default Python one with 429 on 2026-09-17, on every request, and each failure also waited a minute on the retry before giving up. The congress source already sent the header. See the journey for what the agent's menu was missing meanwhile.
  • Market data — quotes.get_snapshots remembers the symbols Webull says do not exist in a category and leaves them out of every later batch in the same process. It already retried once without them, but forgot them on return, so the candidate screen sent the same six names every 15 minutes and paid a refused request plus a retry each time: about 300 refused requests from 2026-09-10 to 2026-09-18. A restart forgets the list, on purpose, so a newly listed symbol gets back in.
  • Setup — The deployment is named ten-acre, after the project: container ten-acre, image ten-acre:local, log file ten-acre.log, and every logger ten-acre.<module>. It was trading-experiment until 2026-09-17. compose.example.yaml, the docs, the VS Code tasks and the rule files use the new name. The rule files also record the move from Dockge to Portainer, the bind-mounted data directory, the dashboard's port, and the start date the database holds. Log files written under the old name stay in the data directory.

2026-09-17

  • Setup — The app installs without Docker. A new workflow, .github/workflows/release.yml, builds a zip on every v* tag: the app as a wheel with the dashboard and the docs built in, the TradingAgents fork as a second wheel because it is not on PyPI, a constraints file exported from uv.lock so the install resolves the same versions as the image, and .env.example. One command installs it, uv tool install ten-acre --find-links . --constraints constraints.txt, and the same command with --reinstall upgrades it. Docker stays the recommended path, and Running it yourself now has all three ways, with a systemd unit for the two without Docker.
  • Setup — One data directory setting, TEN_ACRE_DATA_DIR, replaces four paths the code computed from its own location: the database, the logs, the journey files and the public snapshot. Unset, a checkout keeps data/ beside the code and the container keeps /app/data, so nothing moves for either. An installed copy uses ~/.local/share/ten-acre, because uv tool install --reinstall replaces the code folder whole. The dashboard and the docs now build into backend/web and backend/site, inside the package, and backend/main.py runs the migration itself, so the container's entrypoint is one command. The compose file needs no change.
  • Setup — A new optional setting, AGENT_ANSWER_CHANNEL=json, makes the agent write JSON into its reply on a Gemini deployment instead of calling the decide function it calls by default there since today (see the journey). It is a switch for a comparison or a rollback, flipped in the compose file without a rebuild. compose.example.yaml and .env.example carry it.
  • Market data — A new module, backend/services/fundamentals.py, reads three Webull endpoints on the agent's request: financial indicators, industry comparison and analyst rating. Each call goes through the same pacing as every other market-data call, and a block the vendor refuses is one line in the result rather than a failed fetch.
  • Site and Discord — A pass that the text fallback answered says so, on the Decisions page and in the pass's Discord post, with a pointer to the container log for the reason. See the journey for the field behind it.
  • Site — A pass's "asked to be woken at" now names the day when the wakeup is not the same day as the pass. A 7:55 PM pass that asked for 9:30 AM read as the morning already gone; the agent may ask for up to four days ahead.
  • Site — The Decisions page shows what a turn fetched before it answered, one row per fetch in the same style as an order row, with a fetch the harness refused marked the way a refused order is, and each result verbatim under "Show the prompt".
  • Tooling — backend/scripts/probe_prompt.py makes the same forced decide call the app makes when the provider is google, so a probe tests the channel production uses. --parallel then means --samples at once. It has to run inside a container, because the development host cannot reach Google.
  • Dependencies — The TradingAgents submodule's news analyst can attach Google's search-grounding tool when the LLM is Gemini, behind a new TRADINGAGENTS_GOOGLE_SEARCH_GROUNDING flag that defaults off. Grounding needs a Google Cloud project with billing linked (Tier 1); confirmed live that the Free tier gives Gemini 3 zero grounding quota, and that mixing the built-in search tool with the existing custom tools needs an explicit tool_config.include_server_side_tool_invocations setting that the upstream PR did not set.
  • Dependencies — A new TRADINGAGENTS_GOOGLE_SEARCH_GROUNDING_MODEL setting lets the news analyst run on a different Google model than the rest of the app when grounding is on, e.g. a Gemma model with open grounding quota on a Free-tier key, while quick_think_llm/deep_think_llm stay on the main model.

2026-09-16

  • Site — A research order the agent commissioned now shows on the Decisions page as failed, not blank, when the analysis never finished. The order record only ever said research TICKER; whether it finished or failed was computed and then thrown away. A failed research now appears in the same red "not analysed" list a broker failure appears in, instead of looking identical to one that succeeded.

2026-09-14

  • Dependencies — The TradingAgents submodule takes three upstream fixes. A FRED error no longer carries the API key (upstream #1324, and our own fix for connection errors and timeouts, which that PR missed). The key could reach the model's DATA_UNAVAILABLE message, the logs, and a report on the public site. A StockTwits read stops at 5 MiB (#1328). A newest daily bar with no close no longer turns a stock's whole price history into "no data" (ef383df).
  • Deployment — The public site showed no data from 2026-09-12 17:16 UTC to 2026-09-14. The publisher image was built without SNAPSHOT_BASE_URL, so the site looked for its data beside the page, but the data was on R2. The publisher now refuses to start when the bundle and R2_SNAPSHOT_BUCKET disagree, so this mistake stops the container with an error in its log.
  • Site — The equity curve no longer asks Webull for today's bar. It sent one request for each ticker at three seconds a call, so /api/agent/curve took 9 to 12 seconds and the home page waited for it. Today's point now uses the price cache, which the watchdog updates every 15 minutes. The agent still trades on live prices.
  • Infrastructure — The Ollama pool proxy sends each chat turn to the card that read the earlier turn of the same conversation, if that card is free. The card then takes the earlier prompt from its cache and reads only the new messages: 21 tokens in 0.1 seconds, against 5,214 tokens in 4.7 seconds. Reading is about 10% of the model time of an analysis, so this can save at most about 10%. The proxy dashboard now shows, for each request, the cached tokens, the wait for a free card, the read and write speeds, and whether the request went to the card that read its earlier turn.
  • Infrastructure — gemma4-e4b-qat-128k runs with num_thread 1, down from Ollama's default of 4. Seven runners at 4 threads each asked for 28 threads from the host's 8, and the threads waited for each other. With 7 cards at once, the pool writes 239 tok/s against 206 (+16%) and reads about 5% faster, and a card alone reads at the same speed. The numbers are in the Modelfile.
  • Infrastructure — Two Ollama builds, qwen35-9b-64k and neohorse-9b-64k, with a test of both against the analysis pipeline in ollama/README.md. Both use real prices and choose from the menu 4 times of 4. Neither replaces gemma4-e4b-qat-128k, because one analysis took 24-27 minutes on Qwen and 50-51 minutes on NeoHorse.

2026-09-13

  • Setup — REDDIT_TRAWL_URL is a new optional setting in compose.example.yaml, .env.example and docs/setup.md. It points the sentiment analyst's Reddit fetcher at a self-hosted trawl container. Unset keeps the RSS feed. What this changes for the analyst is in the journey.
  • Docs — The TradingAgents submodule rule states the real base, upstream v0.4.1, and how to find upstream versions that have no git tag. It also records that upstream 1c44dd1 is declined, because it asks the Trader for prices that Python computes here.
  • Docs — CLAUDE.md is 12 KB, down from 103 KB. Claude Code loads it in full into every session, and every step of a session reads it again. The notes for one area of the code moved to path-scoped rules in .claude/rules/, which load only when Claude reads a matching file. The quoted prompt moved to .claude/rules/agent.md, and its test reads it there. The moved notes no longer name the files that commit 99dc4f9 deleted on 2026-09-01: broker.py, fix_import_dates.py, scrub_implausible_levels.py and clear_wrong_side_levels.py.
  • Docs — A study of running the whole experiment on Google's free Gemini tier, with no local GPU. Not built: it measures whether the limits fit and lists what the app would need first.
  • Infrastructure — The LLM throttle reaches Gemini. It wrapped only client.create, which Gemini's client does not have, so a Gemini deployment had no throttle at all. It also reads the wait from Gemini's error body, because Gemini sends no retry-after header.
  • Setup — compose.example.yaml, .env.example and the deploy docs list TRADINGAGENTS_GOOGLE_THINKING_LEVEL, LLM_REQUESTS_PER_MINUTE, LLM_TOKENS_PER_MINUTE, LLM_REQUESTS_PER_DAY and LLM_DAY_TIMEZONE. Each is unset by default, and unset costs nothing.
  • Docs — probe_prompt.py builds a change variant: a change to the app wakes the agent while its planned wakeup is still ahead. A change_turn2 variant is the second turn of that pass, after its research landed.
  • Setup — A Gemini deployment no longer reports "not ready to trade". The setup check asks for the model list, which had no Gemini branch and so was always empty; it now asks Google, and the settings page gets a model dropdown for Gemini from the same list. Each list call counts against the stated request limits and the answer is kept for six hours, because a Google model list may count against the key's limits and both pages load often.
  • Site — "Show the thinking" on the Decisions page shows every turn's thinking, labelled by turn. It showed the last turn's alone, so a two-turn pass looked as though it had thought once. The hint no longer says the thinking is never a summary: Gemini's is.

2026-09-12

  • Agent — is_us_market_hours() knows about holidays and the three half-days a year. It answered True all Labor Day afternoon, and its five callers all mean "is the session genuinely open" — one refuses a reset that needs a market order, one queues an exit-arm rather than attempting it, one takes a live quote instead of the last completed close. The NYSE calendar moved into its own module that imports nothing from the project, because the two modules that need it already import each other.
  • Agent — The clock line knows about market holidays, half-days, and when the market next opens. It had none of that: on Labor Day it promised a session that did not exist. The NYSE calendar is computed from its published rules — no dependency, and nothing to update each year.
  • Agent — The rule for the agent's note to its future self carries two worked examples rather than only a prohibition. Told not to spend the note on prices the next prompt already shows, two of seven probe runs did it anyway; with examples, notes naming a concrete price level went from 0 of 7 to 4 of 7.
  • Data — agentrun.wakeup_note records what the agent said it wanted to remember, in its own words. NULL on every row before 2026-09-12 and nothing is backfilled: those passes left no note, and inventing one would be writing a record nobody kept.
  • Docs — A probe-the-prompt skill and backend/scripts/probe_prompt.py: send the real prompt to the real model out of the app and read the reasoning. It cannot trade — it never reaches run_once or any broker path. --parallel pins one sample to each GPU and returns seven in about 73 seconds instead of seven minutes, with the caveat that wall clock from a parallel run is not comparable with a serial one.
  • Agent — Two prompt sections moved above the signal table and the watchdog alerts gained a time bound, both decided by reading the model's own reasoning rather than by assuming. Four probe runs against the live book ignored the sections entirely at 42% and 47% of the prompt; after the move the next runs quoted alert figures that appear nowhere else in it. The alerts had no age limit, so a Saturday pass was shown Thursday's moves under a heading saying "noticed".
  • Site — The agent's "recent changes" section costs 1,183 characters instead of 6,374, an 82% cut, and reads as corrections rather than a changelog. It is bounded by passes rather than days, capped at five notes, collapsed by date, and budgeted at 240 characters each. Days were uneven to the point of uselessness: the agent chooses its own cadence and ran between 3 and 11 passes a day, so the same note was read about 25 times or twice depending only on which day it landed.
  • Data — trigger on a signal will no longer take the values move or earnings. Nothing commissions an analysis except the agent, so every new signal is commissioned. The existing rows keep their meaning; the scorecard's move-triggered history is now a closed set of 10.
  • Infrastructure — A decision pass can run an analysis and wait for it. agent.run_once is synchronous and runs on a worker thread, so the scheduler installs a bridge at startup that hands the work to the main event loop and blocks until it lands. A deployment with no scheduler — a script, a test — has no bridge and is told so, rather than hanging.
  • Infrastructure — A skipped pass no longer spends the agent's 30-minute trigger cooldown. It was stamped before the attempt, so a pass blocked by the lock still suppressed every later trigger for half an hour.

2026-09-11

  • Data — A signal's created_at is when the analysis started, for every row. It had meant the finish on rows written by record_signal and the start on rows recovered from trace_id, about sixteen minutes apart on this hardware, with nothing marking the seam at 2026-09-09. The start won because it is the instant that was really observed — trace_id encodes it to the second — where a finish has to be computed from it. backfill_signal_timestamps.py resets every row carrying a trace_id and is safe to re-run; on the live book it corrected 18 rows, left 31 already-correct ones alone, and found none it could not recover. This is also in the journey, because it changes what the record means.
  • Site — The research page's limit now applies to rows already ordered by time. A day here holds up to eleven analyses, and a limit landing inside one kept whichever rows the table offered first — the day's oldest — while its newest were never fetched, so sorting afterwards could not bring them back. Measured on the live book: asking for 10 returned 2026-09-10's four oldest and dropped its seven newest outright.

  • Site — The research page shows the time an analysis ran, not only its date, and lists analyses newest first within a day as well as across days. Several a day is the normal case here — eight on 2026-09-10, nine on 2026-09-08, and two of them 27 seconds apart — and signal_date is a calendar date, so the page had shown a day's 06:02 analysis above its 16:57 one with nothing to tell them apart. The three filter branches also disagreed: "pending" came back in whatever order the rows sat in, and "resolved" came back oldest first and was then sliced, so asking for the newest 20 resolved analyses returned the 20 oldest. Sorted in the route, which is the same function the static export calls, so the published site gets it too. The time is on the reader's clock, checked in three zones.

2026-09-10

  • Deployment — The exported JSON goes to an R2 bucket on its own domain, and Cloudflare Pages holds only the Angular shell, deployed once per container build. One Pages deployment used to carry both, so the half that changes monthly paid the bill for the half that changes every 15 minutes: 96 deployments a day, and Cloudflare refuses to delete a project with more than a hundred of them. The publisher now uploads only the files whose contents changed — measured live, the exporter rewrites all 89 files (3.9 MB) every round and almost none of them differ, because a graded signal's JSON is frozen the moment it is written. R2_SNAPSHOT_BUCKET unset keeps the old shape exactly, which is what a self-hosted copy gets.
  • Deployment — SNAPSHOT_BASE_URL is a build argument on Dockerfile.pages-publisher, because the bundle learns where to fetch its JSON at build time. It defaults to /data, meaning beside the page. It and R2_SNAPSHOT_BUCKET are one setting in two places, and a bundle pointed at a bucket nobody fills renders an empty site rather than an error.

  • Repository — Renamed to ten-acre, from the-allowance. The old name is two ordinary words in a phrase people already search for other reasons, so nothing could find the site: "the allowance trading" still returns pages about pocket money, while "tenacre" beside any trading or AI word returns nothing at all. The mark does not change — T and A still spell the name — and the corner brackets now read as the fence around the plot as well as the guardrails. The GitHub repository, the Cloudflare Pages project and the ten-acre.nandyalu.com subdomain move with it. Dated entries that name an older repository keep it, because they record what was true then.

  • Site — The API reference page said Trading Helper, the name the repository had before 2026-09-05. backend/app.py set the title once and no rename since had touched it.

  • Site — The home page reads the price cache instead of fetching a live quote per ticker. /api/agent measured 32.2 seconds; building the same book from the cache takes 0.006 seconds and makes zero vendor calls. Webull's market-data endpoint is paced at three seconds a call since 2026-09-09 — it refused 524 in one day without the pace — so ten tracked tickers cost half a minute, serialised, on every cold load. The watchdog already refreshes every tracked ticker every fifteen minutes and writes each through to that cache; measured live it was 8 to 10 minutes old across the whole watchlist. The agent keeps the live path, because it trades on these numbers and a stale one is an order at a price that no longer exists. A test pins the split in both directions.

  • Site — The rhythm column stretches to the height of the column beside it instead of being pinned to the window. It was sticky, and a sticky panel cannot exceed the viewport — at 1440x900 it showed 797px of a 1569px list and hid 772px, while the money column beside it ran to 1543px. The space was there and the panel could not reach it. Measured after: nothing hidden at 1920, 1440, 1280, 900 or 420. The cost is that the rhythm scrolls away with the page rather than following the reader down, which is the right trade against hiding half the list.

  • Site — The rhythm column on the home page grows with the window instead of sitting at a fixed 17rem, and its scrollbar is themed rather than the browser's grey default. It carries every pass the agent chose to make — fifteen rows on a busy day against the five it was built for — so a wider column wraps fewer of them. Measured at 1440px: the column goes 272px to 317px and the hidden part of the list 877px to 772px. It still scrolls, because fifteen rows do not fit a viewport-height sticky panel at any sensible width.

  • Site — The analyst rationale is parsed into its sections instead of printed raw. It is Markdown, so a reader saw literal **Rating**: asterisks and lost every section break — one blob in a 680px reading measure, beside analyst reports running to 1198px, which is what made it look wrapped when the reports did not. The shape is fixed and known: all 44 stored rationales, from two different models, carry Rating, Executive Summary, Investment Thesis and Time Horizon, and 38 carry Price Target.

  • Site — Copying works on a plain-http address, which is how a self-hosted copy is reached. navigator.clipboard needs a secure origin and is absent otherwise, so every copy button on a LAN install said "Cannot copy" and copied nothing. A hidden textarea plus document.execCommand('copy') covers that case, and the modern call is still tried first where it exists. Measured against this machine's own LAN address: isSecureContext false, navigator.clipboard undefined, and the text pasted back identical to the source at 11,536 characters.
  • Site — The copy button sits in each block's own top-right corner, the way a code fence carries one. In the toolbar there were three of them in a row all reading "Copy", which said nothing about which was which. It fades in on hover or keyboard focus, and stays visible where there is no hover to reveal it with.
  • Site — Copy buttons on every long block: each analyst report, and a decision pass's prompt, answer, thinking and each individual turn. These run to tens of thousands of characters and are exactly what a reader wants to take elsewhere. The button says "Cannot copy" rather than failing silently when the browser refuses.

  • Site — The snapshot tooltip said the page is "rebuilt every 15 minutes" and it can be about twice that. Two independent 15-minute loops sit between the data and the page — the exporter writes, the publisher pushes — and they are never in phase, so a reader legitimately sees "Updated 22 minutes ago". The number was right; the explanation was not.

  • Docs — The model-change skill's completion-share test now says it does not transfer to a reasoning model. Two containers analysed INTC the same day: qwen-3.8-27b measured 31% completion, inside the band that disqualified lfm2.5:8b, while every price in its report was real and it read 243k prompt tokens — nearly double the local model. A thinking trace counts as completion, so the proxy mis-flags a model that reasons out loud. Check the prices against the close instead.

  • Site — The mark and the name now appear on the GitHub README and in the documentation site's header, from the SVG variants that already existed for exactly this. logo-dark.svg, not logo-mono.svg: the header renders it as an <img>, and currentColor inside an SVG loaded that way resolves to black rather than inheriting the header ink.

  • Site — The published copy shows how stale it is (Updated 12 minutes ago) in the masthead. It takes its own row below 75rem, which is every width where the masthead row is already full: the first attempt hid it between 52rem and 75rem, and that band covers most laptop windows, so it was invisible to the majority of desktop readers. It is a static export refreshed on a loop, and a reader could not otherwise tell a quiet afternoon from a publisher that stopped three days ago. Absent on the live app, where the data is live and there is nothing to date.
  • Site — A GitHub link sits in the masthead rather than the footer. Every claim this site makes rests on the code and the journal being readable, so the way to check it should not be the last link on the page. The two links that were there pointed at trading-helper, the name the repository had before 2026-09-05.

  • Setup — The account check asks the broker whether the named account exists, instead of only checking the variable holds something. A second deployment reported ready with a number the sandbox no longer issues, while every pass logged "this deployment will not place orders" — the one page whose job is to make that visible was the page saying it was fine. The LLM check already drew this distinction; the account check had not.

  • Docs — Three more skills in .claude/skills/, each built from something that actually went wrong: review-contribution for the rules a test cannot judge, model-change for the acid test that rejected five of six models, and incident for working out which days a silent fault contaminated. Every SQL query in the last one was run before it shipped.

  • Docs — CONTRIBUTING.md and AI_POLICY.md added, both on the docs site and linked from the README, which also gains a licence section it never had. Contributing asks for a Signed-off-by line plus a grant to release the contribution under any licence, so PolyForm Noncommercial's commercial option survives contact with contributed code.
  • Tests — Four invariants that were held by prose alone are now pinned by tests: every order path asserts the sandbox and the order API is reachable from one module only; the write surface is exactly the two routes that decide nothing; CLAUDE.md's quoted prompt matches the code, and the three rules that exist because of a live failure are still in it; and no module awaits a single-ticker analysis inside a loop. Each was verified by breaking the invariant and watching the test catch it.
  • Tests — The frontend suite ran with eight unhandled errors, dismissed for weeks as a lightweight-charts quirk. jsdom does not implement matchMedia; src/test-setup.ts polyfills it and the count is zero. The real gain is that chart-theme.ts calls it unguarded, so the theme path had been throwing on every chart test and is now exercised.
  • Docs — Every page audited against the code. Three instructions could not have worked: the setup page named a variable the app never reads (LLM_BACKEND_URL, against the real TRADINGAGENTS_LLM_BACKEND_URL), the README and two pages pointed at a compose file that .gitignore excludes, and the now-required WEBULL_ACCOUNT_ID appeared nowhere at all.
  • Setup — compose.example.yaml is tracked, and .env.example rewritten. The old one still offered DISCORD_BOT_TOKEN for a bot deleted on 2026-09-01 and marked WEBULL_SANDBOX optional.
  • Docs — A second pass fixed nine more: the README scheduled the day's final pass in UTC when the code reads it off the Eastern close, three removed slash commands were still documented, four renamed pages were still linked by their old names, and two figures were the previous model's.
  • Site — Copy no longer states this deployment's history as fact. A decision card read "Passes before 2026-09-01 were not recorded" on a container built that morning, four strings promised a weekday schedule removed on 2026-09-05, and one told the reader to press a "Decide now" button deleted on 2026-09-01.
  • Site — The research charge is served by /api/settings rather than written into six templates as "$0.05". Free research is a supported mode, so a deployment charging nothing had been telling every reader it charged five cents.

2026-09-09

  • Setup — A first-run page at /setup reports what is missing and shows the lines to paste. It reports whether each requirement is satisfied and never what it is satisfied with, because the settings payload is published to the static site.
  • Deployment — WEBULL_ACCOUNT_ID names the one simulated account a container may trade, with no default. The three existing guards resolve to the same account for every deployment applying the same rule, so a second container would have traded the first one's book.
  • Deployment — The experiment's start date is stamped when the agent is first switched on, instead of being compiled into the frontend bundle. Every image built from this repo had claimed the first deployment's start date. It is also the floor for the 1-minute bar backfill, so a deployment starting months from now does not page back to somebody else's date.
  • Infrastructure — LLM calls back off when a vendor returns 429, using the vendor's own retry-after header rather than a guessed ladder. One pace is shared across concurrent analyses, because the limit belongs to the account and not to the run.
  • Infrastructure — The model list is authenticated and sends a real User-Agent. A metered endpoint returned 403 for both reasons at once, and the settings page silently degraded to a free-text field.
  • Data — Webull had been rate-limiting most market-data calls all day and nothing said so, because every path falls back to yfinance. The watchdog's fifteen-minute tick was firing about twenty unpaced requests, and a 429 was being read as a failed category probe, which doubled the traffic causing it.

2026-09-08

  • Data — The daily bar cache tries Webull before yfinance, with yfinance kept as the fallback rather than removed. The same endpoint used for intraday bars pages back at daily granularity with no real depth ceiling, and a Webull outage must not take the whole cache down.
  • Data — A 1-minute price history is collected and backfilled, so a chart can show a signal, an alert or a trade at the minute it happened rather than on that day's single candle. Covers roughly the last 90 days; anything older is still daily bars.
  • Dependencies — quiv 0.8.0 to 0.10.0, removing three workarounds in the scheduler that had been filed against quiv from this codebase.

2026-09-05

  • Docs — The documentation site publishes itself, and the generated journal publishes with it.
  • Repository — Renamed to the-allowance, and the real account number taken out of the source.
  • Repository — Working notes and plan files removed; three GPU write-ups moved to the docs site, and the roadmap's tail kept.
  • Site — The home page is one column of content beside one column of timeline, and that timeline shows the agent's own passes. It had been hardcoded, and still advertised a 13:35 decision pass removed that morning.
  • Dependencies — quiv 0.6.0 to 0.8.0. The pin allowed it and the lock had not moved, so two releases of fixes sat unused.
  • Infrastructure — A correction: quiv can schedule a one-off task, so the earlier note claiming otherwise was wrong. The wakeup still polls, because quiv's task store is deleted on shutdown and an alarm set on Friday for Monday would not survive a rebuild.

2026-09-03

  • Site — Times are shown on the reader's clock. UTC made a reader do arithmetic before knowing whether they had missed anything.
  • Notifications — Discord becomes a webhook and the bot is deleted. Nothing changes about what is posted; a great deal changes about what a person has to set up to receive it.
  • Analysis — The vendored TradingAgents is rebased onto upstream v0.4.1, before a single analysis has run. Three of the fixes are data-correctness bugs in the prices the agent would reason over, and doing it later would have split the experiment with no way to tell a framework change from a market one.