Experiment 1 — the journey (history)¶
This page is history, not the current agent. It records experiment 1: the single agent that traded one book on Webull's sandbox from 2026-09-01, and every change made to it, until experiment 2 replaced it. Experiment 2 runs four books on the in-process simulated broker; its design and success test are in PLAN.md, and its journal is JOURNEY.md.
A rule file or a code comment that cites a JOURNEY.md entry by a date from before experiment 2 means an entry on this page.
The text below is the journal as it stood when experiment 1 ended, unchanged.
The app writes its own record: what it bought, what that cost, and what the agent said about it, day by day. Every sentence in it comes from a trade, a charge, or a decision pass, so it cannot drift from the book.
Read it at /api/agent/journey, or as files: one per month, under a folder per year, in the data volume beside the database and the logs.
The app rewrites them after grading each evening. python -m backend.scripts.write_journey regenerates them on demand.
Each file opens with the month in four numbers: positions opened, positions closed, research spent, and where the book started and finished. That way a file reads on its own, not only as part of a series. That is what makes them publishable later: a month is a post.
The app generates them, and rewriting them is how they stay true. Do not edit them. The next write discards the edit. Put commentary here instead.
This file is the other half, and it is the half the app cannot write. It knows the agent changed its mind. It does not know that we changed the prompt the week before, or added a research charge, or settled the debate-round count by running an experiment.
Without those causes, a month of record is a list of events nobody can learn from.
Two rules, both learned the hard way elsewhere in this project:
- Write it when it happens, not afterwards. A reason you reconstruct two weeks later is a story about what you would like to have been thinking.
- Record what was wrong, not only what worked. The entries that say "this turned out to be noise" are worth more than the ones that say "this worked". They are what stops someone proposing the same idea again in three weeks.
What belongs here, and what does not¶
One question decides it: does this change make two periods of the experiment non-comparable? If yes, it goes here. If no, it goes in the changelog.
That splits into three tests. Any one of them is enough:
- Behaviour — it changes what the agent is shown, what it may ask for, or what Python refuses.
- Evidence — it changes what the record contains or means. Telemetry counts: a field that is null before a date is exactly what trips up whoever reads the data later.
- Incident — the agent's behaviour changed without anyone intending it, so real days are contaminated.
Everything else is a changelog entry: setup, deployment, guards, infrastructure, site copy, docs, dependencies. Those matter, and they are not this.
Keep an entry to a sentence or two — what changed, and why. Long-form reasoning that constrains a future edit belongs in CLAUDE.md, or in the .claude/rules/ file for that area. Those are read before the code is changed. This file answers "when did the question change", and it can only do that if it stays readable end to end.
Entries before 2026-09-11 were swept under these rules; anything that failed all three tests moved to the changelog.
Every change to the agent, and why¶
This file covers one agent: the merged agent that has run since 2026-09-01. Before that date, two deployments ran side by side — a live bot on a fixed watchlist, and a separate analyst experiment that chose its own tickers. Both ended on 2026-09-01. Their history lives in the two-book experiment and the analyst experiment.
CLAUDE.md and .claude/rules/agent.md describe what the rules are. This describes how they got that way. Add an entry here before changing a rule, not after.
Newest first.
2026-10-05 — incident: a pending limit buy showed as a loss of its full cost, in the equity the agent reads and in the equity each pass records. Since 2026-09-17, agent_book.build_book takes the cost of a pending limit buy out of cash, so that a later pass cannot spend the same money twice. But Book.equity was cash + market_value, so the reserved money was missing from equity too. On the Alpaca book, the SPCX limit buy of 15 shares at $161, placed 2026-10-05 15:05 UTC, showed the book at $7,658.21 (-23.4%) when it was worth $10,073.21. Now the reserve is in Book.reserved_cash and equity counts it. cash is still the money free to spend. The equity of the passes before the fix stays wrong in the record. The main book had one limit buy (AAPL, 2026-09-21), and it filled.
2026-10-02 — the agent and its analyses move from gemini-3.5-flash-lite on the free tier to gemini-3.8-flash on a paid key, for the week of 2026-10-05. This is a measurement for Experiment 2 in PLAN.md: what one book costs in a week, and whether the behaviour changes. On 2026-10-02 the model passed the output check and the price check of the model-change skill. The candidate-menu check is still open: the only run was on a Friday after the close, and all four samples chose to wait for Monday before they bought research. The thinking level stays high. Compare the passes and analyses from 2026-10-05 with the two weeks before by their model field, and decide at the end of the week whether the model stays.
2026-10-02 — the market and fundamentals analysts now fetch their data first and make one model call. Before, the market analyst spent 4 to 6 tool rounds on the same indicator set, and the fundamentals analyst called the same four tools in almost every run. In 14 of 283 runs, one of them wrote its report without data. Now each gets a fixed block: the market analyst gets the verified snapshot, the daily prices and an indicator set for the horizon, and the fundamentals analyst gets the overview and the quarterly balance sheet, cash flow and income statement. A failed fetch shows as an "
2026-10-02 — incident: after any restart in which two jobs started together, the live container had no Webull market-data client, so the candidate menu never carried a congress or trending name, and every realtime quote came from the yfinance fallback. quotes.get_api_client set its done flag before it built the client. The scheduler starts the watchdog and the public snapshot export in the same second at startup, so the second thread read None in that gap, and quotes._get_market_data remembered that None for the life of the process. Found after tonight's redeploy: a fresh process in the same container carried AMZN, NEE and GOOG from the congress table to the menu, and the running process did not, with nothing in its log, because nothing was ever sent. How many days this held is not known. The 2026-09-16 entry's "neither source ever reaching the agent" fits it. Both clients are now built under one lock, the done flag is set after the attempt, and a missing market-data client is asked for again on the next call.
2026-10-02 — adding one exit level no longer cancels the other. When the agent used adjust to add a target to a position that already had a resting stop, Python cancelled every resting exit before it placed the new one, so the stop was lost. The next adjust that added the stop cancelled the target. XOM swapped them six times in one pass, and the agent was told each time that the move worked. adjust now sends the resting side again in the same pair, at its resting price or at the level the agent states. It refuses with a reason when lots rest at different levels, or when the ticker has no filled shares yet, and it no longer reports "placed" for an order it did not send. Days before this date can show a position with one exit where the agent asked for two.
2026-10-01 — incident: a congress "ticker" with a comma in it emptied the candidate menu of its congress and trending names whenever a hundred of them were waiting to be priced. QuiverQuant's trades table listed GLAS FUNDS, LP as a ticker. The screener prices the text sources' names in one Webull request of up to 100 symbols, joined by commas, so that one value read as two symbols at the vendor, a batch of 100 read as 101, and Webull refused it as an illegal parameter rather than naming an invalid symbol. The retry that strips bad names never ran, and the menu fell back to the "most active" rows alone. Every probe build on the night of 2026-10-01 lost both sources this way, which is why every candidate row in the night's probes says via most active. This machine's live log shows no such failure, and the entry below says why: the running process never sent a batch at all. quotes.get_snapshots now leaves out any value with a comma or a space in it, and candidates.fetch_candidates prices only what looks like a US symbol, one to five capital letters or digits with an optional class letter, so a foreign listing, an index or a crypto pair no longer costs a refused request either.
2026-10-01 — every table the agent reads now carries four cells about the stock itself, and the candidate list is a table. The cells are Trend, 1m / 3m, Volume vs normal and Range/day. Trend is the close against its 50-day and 200-day averages, with the word UP, DOWN or MIXED. 1m / 3m is the price change over the last 21 and 63 sessions. Volume vs normal is the average volume of the last five sessions against the average of the last sixty. Range/day is the 14-session average true range as a share of the price, how far the stock moves on an ordinary day. A price history that breaks in one session, a split or a data error, withholds the four cells and says so, because the bar cache stores raw bars: CTVA read $77.65 on 2026-09-30 and $12.57 on 2026-10-01, and the first probe showed the agent DOWN: 50d -84.3%. backend/services/trend.py computes them in Python from completed sessions in the bar cache, for every signal, every tracked ticker and every candidate, and a missing figure shows as a dash. Until now the agent had no view of a stock's trend or momentum unless it paid for an analysis and waited for it, and then the view came as prose. The market analyst computed these figures on every run and the agent never saw them. They are facts about the stock, not predictions, and nothing in Python decides from them. The candidate list changed from one line per name to a table with the same cells, because the agent has been short of names worth researching and has rotated between the ones it already knew. The cells cost no model tokens to compute. The stored prompt of each pass already carries them as the agent saw them, so whether the agent did better when it agreed with the trend can be read from the record later without a new column. The idea began as a Kalman filter, then a trained classifier with a confidence score; both were set aside, see the rejected section dated 2026-10-01 in PLAN.md. Probed the same night, 68 samples in four looks: on the candidate table the cells are read and steer which name is researched, since a row's picks went from 0 of 7 to 5 of 7 when its cells were flipped from DOWN to UP with nothing else changed; on the held name's row they are reachable and not reliable, and a DOWN trend on the holding changed no action in 54 samples. See .claude/rules/agent-probes.md.
2026-10-01 — a market buy may now carry its own stop and target. Until now a buy rested the signal's Stop and Target, or a 2×ATR stop when the signal had none, and the agent could choose its own levels only with adjust on a later turn. The 2026-09-30 legend tells it to judge the entry, stop and target itself, and this makes that possible in one order. A buy that names neither level behaves as before, and a buy that names one keeps the signal's level for the other. Python refuses, and never moves, a stop at or above the price, a target at or below it, and any level on a limit buy, because nothing rests under a limit buy until it fills.
2026-10-01 — the research price now also appears at the top of the candidate list. The 2026-09-30 rewording of "Paying for research" was not confirmed: research went from 7 of 21 probe samples to 12 of 21, and no sample of 42 quoted the new text. That section sits below the tables, where the agent skims. Every sample reads the candidates result before it chooses what to study, so the header of that list now says what a research order costs, that it runs inside the pass, and that a thousand cost $50.
2026-09-30 — a pass that a stop fill started now asks what the freed cash does next. When a stop closed a position, the agent held the cash and asked for the next morning. On this machine cash went from 1% to 81% of the book as the AAPL, INTC and NVDA stops fired, and no stop-fill pass looked for a replacement. The wake reason said only that a stop or target had closed a position. It now also says the sale's money is cash again, and asks the agent to decide what that cash does: go into a position, pay for research on a replacement, or stay as cash by choice.
2026-09-30 — the signals table's legend now says the Entry, Stop and Target are reference points, not limits. Nebula's agent treated the Entry as a price it must not pay more than. From 9/16 to 9/18 it waited in cash for INTC to come back to $100–101, and INTC went to $110. The legend now says that a price past the Entry does not rule a trade out, and that the agent judges its own entry, stop and target from the analysis. A market buy still rests the table's Stop and Target, and the legend says to move them with adjust on the next turn to use other levels. The buy order itself cannot carry levels yet.
2026-09-30 — "Paying for research" now calls research the agent's main tool, and the rule on research no longer talks about money leaving the account. The agent still rationed research after the 2026-09-22 change. It weighed the $0.05 charge in its reasoning on both Gemini deployments. One deployment ordered no research for five days, and on 9/29 the other named a new candidate and then held cash. The section now says what research is for, puts a thousand analyses at $50 next to one stopped trade, and says to research whenever a fresh analysis could change the decision. The rule said "re-researched as often as you judge it worth $0.05" and "paying to study something you then ignore is how the money leaves this account". It now says "as often as you want, the same day included". The charge itself does not change.
2026-09-30 — the rule "Doing nothing is a valid answer, and often the right one" is gone, and so is the "Your recent wakeups" section. Both were added to stop the agent from waking at the minimum interval. On the two Gemini deployments they made it cautious instead. It quoted the rule as its reason to hold in 11 of 80 passes. The wakeup count told it to "ask for a later time" after idle passes, and on most days it then asked for the next morning. The deployment on this machine held 81% of its book in cash for two days. The last pass before the close still runs whatever time the agent names.
2026-09-29 — on an Alpaca paper deployment, the candidate menu is no longer empty. fetch_candidates used to return nothing when no Webull client existed. It now screens Alpaca's own most-actives and day-gainers lists, prices each row with an Alpaca snapshot, and drops ETFs using Nasdaq's daily symbol directory. Without this, an Alpaca deployment had no way to propose a new ticker to the agent.
2026-09-29 — the final pass before the close now runs fifteen minutes ahead, not five. At 3:55 the agent read "closes in 4m" and decided it had no time to order research, read it, and trade. An analysis takes about 7 minutes, 11 at most, so five minutes never left room for one. FINAL_PASS_LEAD and the wakeup text the agent reads both moved to fifteen minutes.
2026-09-28 — incident: Google's model service failed for three hours, the agent's INTC research never ran, and the passes that got no answer left no record. Times are Eastern.
- 9:30 to 10:03. Eight passes asked Gemini and got
503 UNAVAILABLE("This model is currently experiencing high demand") on thedecidecall and on the text fallback. Each pass raised, and none of them wrote anagentrunrow. The Decisions page shows no pass before 10:08. - 10:08. A pass got an answer through the text fallback. At 10:14 it ordered research on INTC, which had dropped 5%.
- 10:14 to 12:26. The analysis ran inside the pass and failed seven times. The errors were 503s and 429s on the free tier's per-day and per-minute limits (
GenerateRequestsPerDayPerProjectPerModel-FreeTier). The wait between attempts grew to ten minutes. The pass held the agent task for 2h 18m, so no other pass could run. - 12:26. The container restarted with a paid-tier key, and the shutdown stopped the analysis. Pass 54 recorded the research with
"refused_by": "broker"and the reasoncall_on_main(<function run_analyses ...>) was stopped: the job's stop event was set. INTC got no signal and the agent paid no charge.
What the record for this morning supports. It shows one pass where the agent was asked nine times. The agent did less than it chose to: it ordered research that never arrived, and for 2h 18m no pass could run. Read the morning as an outage, not as patience. The failure line on pass 54 is wrong about who refused: the broker did nothing. The key change itself is infrastructure, because the model (gemini-3.5-flash-lite) and its settings are the same.
What changes, from this entry forward:
- A pass that gets no answer on its first turn now writes a row.
skippedsays why, and a new column,unanswered, is true. Every reader of "the previous pass" skips these rows. Without that, a failed pass would move the start of "What was noticed since your last pass", drop the note the agent left, and cancel the last pass before the close. - A later turn that gets no answer now ends the pass and records it. Before, the exception discarded the whole pass, including the orders that earlier turns had placed.
- Research that a shutdown stops now says so in plain words, and
refusalslabels a failed researchanalysis, notbroker.
Evidence change. unanswered is false on every row before this date, and passes that got no answer before this date wrote no row. A count of failed passes from the table starts today. On earlier rows, a research entry with "refused_by": "broker" means the analysis failed, not the broker.
Not changed, and still false. A failed research still reaches the next prompt under "Orders the broker would not take", and that section opens by saying the broker rejected each order. That has been false for research since 2026-09-16. A fix changes the prompt, so it waits for its own entry and a probe.
2026-09-28 — a memory order's own record now says whether it added, cleared or removed a note. _order_detail kept the order's text alone before this, so a turn that cleared the memory list read back exactly like one that added a note. Run 81's clear is one of the turns this affects.
2026-09-27 — five changes reached the agent only today, with a redeploy. The container that ran from the evening of 2026-09-25 until today was built at 55b102c, before e135760. So the two entries dated 2026-09-25 — a stop the trade stream catches wakes the agent, and a wake during a pass gets a pass after it — were not live on 2026-09-25 or 2026-09-26. Nor were the three dated 2026-09-26: ask_analyst, the history in a read, and the extra news sources. All five are live from 2026-09-27. The Laya grade on news items is not: LAYA_URL stays unset, because the grades were measured and shelved the same day.
2026-09-26 — the analysts read three more news sources, and each item can carry a grade. get_news, which the news analyst and the sentiment analyst both call, now returns Yahoo's headlines and then Google News, Finnhub (when FINNHUB_API_KEY is set) and the company's SEC 8-K filings. The pipeline said Hold in most analyses, and Yahoo alone gave the analysts thin news. When LAYA_URL names a laya sidecar, each news item, StockTwits post and Reddit post starts with a grade: the chance it mentions the company, and a tone from -1 to +1. Nothing is dropped unless LAYA_DROP_BELOW is set, and it stays unset until a person has read the grades. On twelve hand-labelled headlines laya's "is this about the company" did not separate the classes, so the grade asks whether the item mentions the company. See news_sources.py. This is item 2 of the "Later" list in PLAN.md.
2026-09-26 — a read now lists every other analysis of the ticker. Each line gives the date, the verdict, the price at the time, the app's entry, stop and target, the chance, and the sentiment analyst's score, newest first, up to eight. Before, a read showed one analysis alone, so the agent could see that INTC's newest analysis said Hold but not that the five before it said Buy, or how the stop and the target moved between them. This is item 4 of the "Later" list in PLAN.md.
2026-09-26 — the agent can ask one question about an analysis, with the ask_analyst fetch. A read shows each analyst's summary table. The full reports behind it run to about 20,000 characters, and the agent never saw them. ask_analyst sends the question and every stored report of that analysis to the quick-think model, which is told to answer only from that text and to say so when the text does not hold the answer. The answer is a new kind of fetch result in the record. The tool-channel rule that lists the fetches now names it, and the rule about reading adds one sentence that says when to use it. This is step 3 of PLAN.md.
2026-09-25 — a stop that the trade stream catches now wakes the agent and reaches its prompt. The stream settles a fill within a second and then posted it to Discord only. A settled order is no longer pending, so the watchdog's own settle found nothing, and the agent got no stop_fill alert and no wake. It learned of the sale at its next own wakeup. A limit buy that filled was missed the same way. Only the deployment that holds the stream was affected; the other settles on the 15-minute poll, which always announced both.
2026-09-25 — a wake that comes during a pass now gets a pass after it, and a pass no longer skips when the evening review is running. Every pass now runs in one quiv task, and the jobs do their work on quiv's threads instead of handing it to the main loop. Two things the agent sees change. Before, a watchdog alert or an earnings wake that came while a pass ran was dropped, and the agent heard of it at its next own wakeup. Now it is kept, and the next pass starts when the running one ends. It is dropped only when the running pass built its last prompt after the event, because that pass has already seen it. Before, a pass that came due while the evening review held the lock was skipped. Now it waits for the review. The record changes too: quiv's job history now carries the real duration and the real failure of each pass, where before every job showed a one-millisecond success. See "One quiv task runs every pass" in .claude/rules/agent.md.
2026-09-25 — the account line now gives the equity, not the starting $10,000, and a stop fill no longer says "No pass ordered this sale". The account section opened with "Your account is $10,000.00 in total", and the equity came two lines later. On pass 88 the book was worth $10,942.50. The model's thinking said "I have $10,000 in the account" three times, and it sized its CRWV risk as "1% to 2% of my $10,000 account". The sizing rule says "of the book", and the first number in the section was the wrong one. The line now reads "Your account is worth $X right now", and gives the $10,000 start and the return after it. The same pass read a stop fill as "No pass ordered this sale". That is false: a pass put the stop there, and only the time of the sale was not a decision of a pass. The sentence is removed, and "your resting stop … fired" says the rest.
2026-09-24 — an evening review: the agent reads its own day, and it may rewrite the note the next pass reads and its own memory notes. Once a trading day, after the close and the grading, the agent is shown every pass since its last review, what its trades did, the analyses it bought and what the price did since, its memory notes with their dates, and the note its last pass left. It is asked twice in one conversation. First, through a forced report call, what stood between it and a better decision, for the people who maintain it, each note naming the pass where it was felt. Then, through revise, whether to replace the note for the next pass and whether to add, rewrite or remove a memory note. It can place no order, commission nothing, and cannot move its alarm. The next prompt shows the review's note in place of the last pass's own, labelled as the review's, and the fixed rules now say the review exists and may rewrite the note and the memory. A memory note carries the day it was written and where from, a pass or a review; a note from before today reads as undated. The two questions are asked apart because every probe on record shows one job crowding out the rest, and the maintainer note comes first so the self-review does not make it an afterthought. The review is shown the fixed rules of the decision pass under a heading that says "if it is in these rules, you have it", which stands in for the change-note mechanism removed on 2026-09-21. Probed before it was built, on the week of 2026-09-14 (agent-probes.md): 3 of 4 samples reported nothing to the maintainer and said why, all four cut the restated cash balance from the note for the morning, all three memory notes carried the count "once", and one sample drafted a workaround for the gap it had just reported, quoted the rule, and dropped it. Two guards came from that probe: the example in the memory rule names no ticker in the record, because all three notes opened with the example's words, and memory is for what was learned and not for a rule already given, because two notes restated the sizing rule. Probed again on the shipped prompt the same day, on the two live passes of 2026-09-24, four samples: 4 of 4 reported nothing and walked both passes by number to say why, 4 of 4 rewrote the note for the next pass and 3 of 4 dropped the restated price, 0 of 4 added a memory note, and two quoted the new guards in their own words. The probe also found the clock sentence rendered twice, fixed before the release.
2026-09-24 — the review is its own record, agentreflection, not a pass. One row a trading day when there was a pass to review: both prompts and both answers verbatim, the notes to the maintainer, the note it wrote for the next pass, the memory changes it asked for and what was applied, the thinking, the model and the tokens. A window with no pass records nothing. The passes table is untouched: a review is not a wakeup and does not count as an idle pass. A review's note to the maintainer never reaches the agent's own prompt, because that would make the review a self-training loop; memory is the channel the agent has to itself.
2026-09-24 — every turn of a pass now settles pending orders before it builds the book, and a refused adjust says when the exit already filled. INTC's stop filled eleven seconds after a pass took its lock. The pass had already settled once, at the start, so every later turn was shown fifteen shares the account no longer held. The agent asked to move that stop, and Webull answered OPENAPI_ORDER_CANT_NOT_BE_REPLACE — Order can not be modified, which reads as a broker fault. The agent spent its next turn guessing at unsettled cash and API limits, because nothing it could see said "already filled". run_once now calls settle_pending() at the head of every turn, not once before the loop, and a fill it finds there is carried out for the scheduler to announce, the same as a fill found between passes — a stop or target is also written as an alert the next turn's prompt reads, and a limit buy that landed asks the agent again, because its shares rest with nothing under them. _settle_agent_fills can no longer do either: an order the pass settled itself is one it will never see. adjust_exits now asks the broker what a refused order's real status is: a finished order says "that exit already filled at $X, so there is nothing left to move", and any other refusal still shows the broker's own words.
2026-09-24 — incident: one Gemini request never returned, and the agent missed the rest of the day. An ORCL alert woke the agent at 9:55 ET. At 9:58 the pass bought 10 COIN, with a stop at $182.29 and a target at $253.31, and asked Gemini again. That request got no reply, and no client had a timeout, so the pass waited until the container restarted at 7:59 PM ET. The pass held _pass_lock all that time, so the 3:55 PM last pass and every alarm after it were skipped. What the record lacks: the COIN buy and its exits are in agenttrade, but the pass that made them has no agentrun row, so its reasoning and prompt are lost, and the day has one recorded pass where the agent had two. The days before are not affected: no pass from 2026-09-15 to 2026-09-23 ran longer than 191 seconds, and each day since 2026-09-21 has its last pass. The fix bounds every Gemini request at llm_gemini.REQUEST_TIMEOUT_SECONDS (300), on the decide call, the text fallback and the analysis graph, so a request that stops answering now fails, and the pass falls back to the text channel.
2026-09-23 — the prompt now says that a last pass runs before the close. Five minutes before each close the scheduler runs a pass whatever time the agent named, unless another pass ran in the 30 minutes before it. The prompt did not say so. It said "If you name no next_wakeup, you will next be asked at … the following open", which was false on every pass during a session before that time. The agent planned its day around a morning wakeup that was not the next one. "Your next wakeup" now gives one rule: the agent is asked at the time it names, and if it names none, or a time after the next last pass, it is asked at that last pass or at the following open, whichever comes first. The fixed rule points to that line, where it used to repeat "the following open".
2026-09-23 — a stop or target that fills is now told to the agent as a fact, and one adjust moves the exits on every lot of a ticker. Pass 81 on this machine started because a stop sold 37 INTC shares at $120.00. The prompt said only "A resting stop or target closed one of your positions", and nothing in it said which. The agent spent its first turn searching, and in its last two turns it called the wake a misfire. Each fill of a resting exit is now an alert row in "What was noticed since your last pass", with the shares, the ticker, the price and the level that fired. That section now covers everything after the previous pass built its last prompt, not after it was recorded, so a fill during a pass is not lost. A stop fill wakes the agent even inside the 30-minute cooldown. The only exception is a pass that is running at that time. If that pass ends without seeing the fill, the agent is woken again. The second INTC stop filled at 10:55 ET, inside the cooldown, and until this change nothing woke the agent.
The same pass showed a second fault. INTC was 71 shares in three lots, and each lot had its own bracket. The holdings table showed one stop, and adjust moved the exit of one lot only. The agent raised "its" stop to $120, and 34 shares stayed at $94.36 and $95.46. adjust now moves every resting exit on the ticker. The holdings table shows each level with its share count when the lots have different levels.
A third fault is in the same pass. The agent bought NVDA with unsettled cash, so the broker refused the bracket. The fallback then placed no exit, and the prompt said "the analysis gave no usable stop or target". That was false. The NVDA analysis had levels, but it was older than the three-day window of the signals table. The fallback now uses the same 2×ATR stop that the bracket used, and the prompt gives the real reason: the broker refused the bracket, and why.
2026-09-23 — this deployment moves from the local pool to gemini-3.5-flash-lite on a free key, and a month of harness work begins. Nothing from it is scored. Until today this book ran gemma4-e4b-qat-128k, a 4B model, for every analysis and every decision. It now runs the same setup as the second deployment on nebula: gemini-3.5-flash-lite for both, thinking level high, search grounding off, one analysis at a time, and the free tier's stated limits (15 a minute, 250,000 tokens a minute, 500 a day), on a Google project of its own so the two do not share a quota. The model setting in the database moved with the env var, because the setting wins.
What the agent sees changes at once. The signals table shows only analyses from the configured model, and all 69 on record are gemma's, so the table starts empty. The book keeps its INTC and AAPL positions with no analysis behind them until the agent orders one. The book is not reset: at $11,256 it is up 12.5% on the $10,000 start, and about $1,000 of that is one unrealized INTC position.
Why the switch, and why no score. A review on 2026-09-23 found about 120 agent changes in 22 days, so no two weeks of this record ran the same agent. It also found the 4B model idle in 51 of 79 passes and holding one name at 78% to 99% of the book. Neither can be read as the answer to whether an agent can trade, because the agent never stood still long enough to be measured. So the plan is in two phases. First, about a month of harness work with both deployments on flash-lite, during which the books run and are kept as a record of the harness, not as evidence about trading. Then a freeze: one commit on both deployments, no agent-facing change, and one variable. The variable chosen is the decider. One deployment moves to a billed key with AGENT_DECISION_MODEL=gemini-3.8-flash, and the other stays on flash-lite. A free key cannot run that test: its flash models refused almost every real-size request with 503 on 2026-09-23 (see llm-providers.md).
Two differences between the deployments remain, and both must be settled before the freeze. Nebula trades a margin account (INDIVIDUAL_MARGIN) and this one a cash account, and settlement changes what a cash book can do the same day. The two books also start from different places: this one about 80% invested, nebula's about 86% in cash. Put both on cash accounts and start both from cash, or the comparison measures the account rather than the model.
2026-09-23 — every turn now records which model answered it, and how many of its input tokens Google served from cache. model is the analysis model, or AGENT_DECISION_MODEL when one is set and the tool channel answered; cached_tokens counts only on the tool channel, since the text and JSON channels never cache. Both fields are null on every turn before this date. The record exists because the model that decides can now differ from the model that analyses (see .claude/rules/agent.md), so a turn's own record should say which one actually answered rather than assume the pass's configured default.
2026-09-22 — a watchlist row whose price has left its last analysis behind is marked STALE, and the marker says what that costs. A row that has moved _BRIEF_MOVE_PCT (5%) or more since it was analysed now carries a marker in the It said cell, beside the decision that has gone stale. A held row says more than a watched one: the stop and target resting under the position were set from that analysis, so they no longer say where the thesis breaks. The legend's permissive sentence — "a large move on a stale analysis is the signal that a fresh look may be worth paying for" — is replaced by a definite one that names the marker.
Probed before it was built, in two arms of seven, and the read is the strongest this prompt has produced. The marker was tested by post-processing describe_watchlist's output inside the probe process, so nothing was spent building it first. Samples that reasoned about staleness went from 1 of 7 to 7 of 7. Control samples had the same +13.5% on INTC in front of them and treated it as a number; marker samples used the word three to eight times each, and one quoted the cell back before acting on it. Research orders went 1 of 7 to 4 of 7 and engagement with the stale INTC 0 of 7 to 3 of 7 — both swings of 3, which the noise floor says is a coin, and neither is claimed as an improvement.
The threshold is _BRIEF_MOVE_PCT, reused rather than invented. describe_watchlist_brief already calls 5% "moved enough to mention" on the tool channel. A second number here would be two answers to one question, and the two lines would drift apart the first time either moved.
The marker's own instruction is ignored, and that is the honest half of this entry. It says not to move an exit that came from a stale analysis. Stop adjustments on INTC went 3 of 7 to 4 of 7 — no better, slightly worse. One sample read the marker, concluded that "the 13.5% move makes the old stop/target potentially irrelevant", and raised the stop to a number it invented anyway. A marker read by 7 of 7 and disobeyed on its one instruction says that behaviour is not an information problem, so do not answer it with more prose. Raising a stop is what this model reaches for whenever a holding shows a profit. If it needs an answer, the answer is a refusal in screen, and a refusal is a restriction that needs its own entry here and its own argument first.
What to watch: whether a stale held name gets re-researched before its exits are moved, and whether three marked rows at once ever reads as noise on a fuller watchlist. If the marker starts appearing on most rows it has stopped being a signal — that is the point to revisit the threshold, not to delete the marker.
2026-09-22 — "Paying for research" no longer tells the agent that a bad choice of study is a loss. The section ended "A bad choice of what to study is a loss like any other, so spend it where you actually want a fresh look — not because it is free to ask." That sentence is gone. The price and what the order buys are unchanged, and one sentence takes its place: "That is cheap next to the cost of a trade you get wrong. Research anything you are unsure about."
The old wording rationed the one thing CLAUDE.md says never to ration. An analysis costs $0.05 against a $10,000 book — five thousandths of one percent — and a single trade the agent gets wrong costs a hundred times more. The sentence named the downside of researching and never named the downside of not researching, so the only risk the agent could weigh was the small one. A model that reads "a loss like any other" beside a price with no scale beside it has nothing to measure the price against, and supplies its own caution instead.
This is a change of frame, not of mechanism. Nothing Python refuses has changed: cash still bounds research, the watchlist cap still bounds new tickers, and nothing is still analysed unless the agent asks and pays. The research price itself is untouched.
What to watch: research orders per pass, and whether the agent stops talking itself out of a question it has. The failure this is meant to fix looks like a pass that names an uncertainty in its reasoning and then does not commission the analysis that would answer it.
The rules block still carries the other half of the same idea, deliberately. It says "paying to study something you then ignore is how the money leaves this account", which is about ignoring an answer already bought rather than about hesitating to ask the question. If the probe or the live book still shows the agent rationing research, that line is the next one to cut.
2026-09-21 — how long an analysis takes is now measured over the last seven days, not over the last twenty analyses whenever they happened. The window was a row count with no date bound at all, so on this date it reached back to the first analysis this database ever held and averaged two models together: 11 of the 20 rows were qwen-3.8-27b at 2.3 to 4.4 minutes, a model this deployment stopped using on 2026-09-13, against the current model's 3.2 to 9.1. The prompt read "about 4 minutes here — recently between 2 and 9" when the last five runs had taken 4.5 to 9.1. It now reads "about 5 minutes here — recently between 4 and 9", off 9 runs instead of 20. The agent plans a wakeup around this number, and a pass spent on an answer that has not arrived is a pass wasted, so erring low is the direction that costs something. The function's own docstring already gave the reason — the number moves with the model and the hardware — and a row count cannot express that where a date can. After a quiet week the line says nothing rather than quoting a model no longer in use; the section then carries only what is running right now. Both bounds apply: at most 20 runs, none older than 7 days.
2026-09-21 — the agent is no longer told when the app changes, and the whole mechanism for telling it is gone. "What is no longer true" is deleted, and so is everything behind it: backend/agent_changes.json, the loader, the three-pass seen-counter, the five-note cap, the 240-character truncation, the startup wake that pulled the agent's own alarm forward when a new entry shipped, the counter that recorded the announcement, and the "A change to this app woke you" wake reason. The agent now learns what it can do only from the rules in front of it, and learns nothing about what it used to be able to do.
This is a deliberate removal of a tool, and it is the first one this project has made. CLAUDE.md says a missing tool is a gap in the experiment, so record the cost plainly rather than pretend there is none. The section had behavioural evidence: limit orders went 0 of 8 samples unannounced to 2 of 4 with a change note announcing them, and one sample named the note as its reason — "the rules have changed recently; I can now use limit orders". The fundamentals fetch was called by 1 of 4 on the day a note announced it and by nobody before. What is lost is the agent's only channel for learning that a note it wrote was answered, and its only way to read its own history — the decisions, wakeups and track record lower in the same prompt were produced under older rules, and nothing now says so.
The reason to accept that cost is that the section is about the app's past and every other section is about the present. Everything else the agent reads is a fact it can act on now. A rule that changed last Tuesday is a fact about the experiment, and the experiment has JOURNEY.md for that — read by people, not by the agent. If the live book starts working around a restriction that no longer exists, or re-asks for a tool it already has, this is the first thing to put back, and the probe numbers above are the ones to beat.
2026-09-21 — the agent reads the state of its book before it reads the clock. The "Now" group — the time, why the pass started, and the agent's own standing notes — moved from the top of the prompt to below "What is true now", so the order is: what changed since you last looked, what is true now, when it is and what you told yourself, what you can do. The idea is that a note to itself and a wake reason are read better against an account, a holding and a signals table already in hand than before them, and that the agent is deciding about a book rather than about a time. This inverts the reason the clock was put first on 2026-09-10, which was that everything below is read against it — so it is a hypothesis and it is the riskiest of the three changes made today. Three of the fourteen samples asked what they carried through the prompt said they held the market being closed in mind while reading the rules or the signals, which is an argument for the old order; nine of the same fourteen asked for the book first. Both wake pointers were reversed in the same edit: they said "below" about "What was noticed since your last pass" and "What your last answer did", and both of those sections are now above the line that names them. Nothing was reworded otherwise and nothing left the prompt. Passes either side of today read the same facts in a different order, so they are comparable in substance and not in how the prompt reads.
2026-09-21 — the table of every ticker the agent tracks now sits directly under the table of recent analyst signals, instead of six sections below it. Asked what order it wanted the prompt in, 4 of 7 samples on the everyday prompt asked for exactly this, calling both tables "actionable intelligence"; nothing else in fourteen replies was agreed on and not already true. They were split across a group boundary: the signals in "What is true now", the watchlist in "What you can do", with the track record, the broker failures, the analysis timing, the recent wakeups and the research price between them. Both are the same kind of fact — what the analysts have said about the names on the book — and the watchlist's own job is to show which of them have gone stale or were never looked at, which is a judgement about the signals table above it. Nothing was reworded and no section left the prompt. "Paying for research" and the candidate list stay in "What you can do", where an action really is being offered.
2026-09-21 — a signal for something the agent already holds now says so in its own row, and the column saying why the analysis ran is gone. Eleven of the fourteen samples that were asked what they carried from one part of the prompt to another named the same thing: a holding's shares, average cost and resting stop, held in mind from "What you hold" while reading that ticker's row in "Recent analyst signals". The two sections are already adjacent, so no reorder could fix it — the row simply never said the position existed. It carries 7 since 21 Sep, avg $205.44 now, beside the affordable-share count, which is the other fact in that table about the agent rather than about the stock. "Why it ran" paid for it. Nothing but the agent has commissioned an analysis since 2026-09-12, and the record agrees: 20 of the 21 signals this database has ever held say commissioned, and the single move row is from 2026-09-11, the day before that change. The column had become one sentence repeated on every row. The half of it that was load-bearing is kept and moved: a row the agent paid for in this same pass still says so, appended to when it was analysed, because that marker exists to stop the model reading its own research as somebody else's and it was measured doing exactly that.
2026-09-21 — the agent was asked what order it wants the prompt in, and the answer it gave twice was two different answers. The note action already lets it say "I cannot see X", and this points the same mechanism at the prompt itself: a layout probe variant shows the real prompt, swaps the answer shape for three questions about how the prompt reads, and asks for prose. Nothing in the app changed and no pass is affected — it is a probe, out of the app, and it reaches no broker path, so no second decision-maker enters the record. Fourteen samples, seven on the everyday prompt and seven on the fullest one the script can build. Only one of the three questions produced anything. Asked what it had to hold in its head from one part while reading another, 14 of 14 named something and they named the same two things: the cash figure, carried down into every table below it, and a holding's own shares, average cost and resting stop, carried into that same ticker's signal row. The second is a lead the reorder cannot reach — those two sections are already adjacent, and what the model describes is that a held ticker's row in the signals table never says it is held. The other two questions failed, in the way the plan predicted they would. Asked what order it wanted, it put the account and holdings above the clock on 2 of 7 samples of the short prompt and 7 of 7 of the long one — same model, same book, same day, opposite answers, the only difference being length. Asked which parts it did not open, it named the two sections that are a pointer to a fetch, on a variant that declares no functions, so it could not have opened either. A stated preference is a hypothesis; the behavioural probe is what decides. No sample asked for anything it was not given, and no sample mentioned the four group headings. The full write-up, both tables and the caveats are in .claude/rules/agent-probes.md.
2026-09-21 — a read is now fenced in four backticks, on top of the demotion shipped earlier today, not instead of it. The demotion entry below reasoned that a fence risked turning the read — the section the probes show is studied most closely — into something skimmed rather than read, so it shipped with demotion alone. Probed the same day with a matched pair on identical scenarios, marking every number that appears exactly once in the whole prompt and inside the read as a figure the model can only have taken from the analysis: 7 of 7 samples quoted one, and 7 of 7 mentioned the analysis at all, whether the read was fenced or not. The fence makes no difference to whether the analysis is read. Kept anyway, because a fence marks where the document starts and stops rather than repairing it line by line — the same 60 of 126 stored reports carrying their own ## heading that the demotion already covers. Four backticks, not three, since a report can carry a three-backtick block of its own. The one number that moved was orders, not reading: 3 of 7 to 0 of 7, which the floor run's own swing on identical code says cannot be pinned on the fence alone — but it is what to recheck first if the live book starts reading without acting. Full write-up and the table in .claude/rules/agent-probes.md.
2026-09-21 — the prompt's sections are gathered under four headings that name what each group of them is for. "Now", "Since you last looked", "What is true now" and "What you can do" — the four questions in the order a person would ask them. A group is named once, above its first section that has anything in it, so a group whose sections are all empty never appears. The opening line belongs to no group. Nothing moved and no words changed; the prompt gained four lines. This is the last of the reordering that began this morning, and the shape it produces is the reason the earlier moves were worth making: every section now sits under a heading that says why it is there.
2026-09-21 — an analyst's own markdown can no longer impersonate the prompt's structure. A read is a whole analysis and an outcome line carries the same trimmed rationale, and 60 of the 126 stored analyst reports contain a ## heading — the same level this prompt now uses for a section. A read that announced "## Risk Assessment" would read as the analysis having ended and a new section starting, the same way a bare --- inside one did earlier today. _joined demotes any embedded # or ## to ###, below a section and below a group; deeper headings keep their depth, so the document's own shape survives. Demoted rather than wrapped in a code block: the read is the section the probes show is studied most closely, and a fence risks turning that into skimming for a collision demotion already removes. It has not reached the prompt yet — the four report tables are extracted rather than quoted whole — so no pass before today was affected.
2026-09-21 — what happened since the last pass is read before what is true now, instead of eight sections apart from the line announcing it. "Why you are awake" said a rule had spotted something and the section holding what it spotted sat eight sections below, past the account, the holdings and the pending orders, with a pointer bridging the gap. What changed in the app, what the rules noticed, and what the agent's own last answer did are now three consecutive sections directly under its notes, and the market regime, the account and the tables follow them. The measured constraint is kept and strengthened: "what was noticed" and "what you just did" were 0 of 4 below the tables and 4 of 4 above them, and they are further above them now. The pointer stays, because it is guarded against naming a section that is not there and two sections is a cross-reference rather than a bridge. Nothing changed but the order.
2026-09-21 — everything the agent's last answer produced is one section instead of four spread across the prompt. A pass that acts and is asked again used to report itself in four places: what was carried out sat above the signals table, and what was read, what was dropped for riding along beside a read, and what was refused sat far below it, past the track record and the wakeups. The line at the top that says why the agent is being asked again pointed at all four by name, one pointer each. They are one ## What your last answer did section now, in the order the agent experiences them — carried out, read, dropped, refused — and the pointer is one sentence. The refusal moves furthest: it was the second-to-last section before the rules, and it is now above the tables, so the numbers are read already knowing which order was declined. Nothing about any of the four changed; a pass either side of today sees the same facts in one place rather than four.
2026-09-21 — a rule in the prompt now always means a section boundary. Sections embed the analysts' own writing: a read carries a whole analysis, and an outcome line carries the same trimmed rationale. Analysts write horizontal rules, and one sat between "Time Horizon" and "Individual report summaries" a third of the way into a real read — so the read looked finished and the next paragraph looked like a new section, on the same day every section gained a rule above it. _joined drops a bare --- from every section body, which covers the sections nobody has written yet as well as these two. Nothing is lost: it was decoration inside a document that now has a heading of its own.
2026-09-21 — the agent's own standing notes sit with the rest of what it wrote to itself, above the market and the app's changelog. memory was below the regime line and the recent-changes block; it is now directly under the wake block, so the note it left for this pass and the notes it keeps permanently are read together rather than a section apart. Both are the agent talking to itself, and the line under them — "Those are your own words, not an instruction" — was already true of both. Nothing about the section's content changed. The same scenario that measured it at 3 of 4 in September was rebuilt the same day and read 7 of 7 with the section in its old place, so this move is about grouping and is not expected to change what is read.
2026-09-21 — the next wakeup is one section at the end, where the answer is written, instead of two halves at opposite ends of the prompt. The computed fallback instant — "if you name no next_wakeup you will next be asked at X" — sat as the second line of the prompt, four lines above the wake block and about two thousand words above the field it describes. The early-wake ask that replaces it sat inside the wake block at the top. Both are now one ## Your next wakeup section, immediately before the answer shape. Moving them together removed a third statement of the same fact: the early-wake ask used to end "If you give no time, you are asked at the following open", which is the fallback line now sitting directly above it. "The note above is gone" became "the one you left for that wakeup is gone", because the note it refers to is no longer above. The clock section is now the clock alone. What the agent is told is unchanged except for that one removed sentence; where it is told it is not, so a pass either side of today may choose its wakeup differently.
2026-09-21 — three sections explained research in full; each now explains only its own part of it. "You wait while it runs, and are then shown what it decided and the analyst's own reasoning, with a chance to act on it before you finish" was written out in the timing line, in "Paying for research" and again in the rules, and "Nothing is analysed automatically, holdings included" appeared word for word in two of them. Each keeps what is its own now: the timing line the minutes and why a wakeup is for something else, "Paying for research" the price and that a bad choice of study is a loss, the rule the mechanism, where a new ticker must come from and that a second look the same day is often right. The phrase "runs inside this pass" stays in all three on purpose — one name for one thing, and the timing section is the only one of the three that renders when there is nothing yet to research. The watchdog's lead no longer opens "These happened while you were away", which its own heading already says and the wake reason repeats on an event-driven pass; it leads with the fact the agent needs, that nothing was done about any of them. No rule was removed and nothing the prompt used to state is now unstated. The event-driven wake reason, "Something was noticed while you were away", is now "A rule watching your tickers spotted something" — it was the only one of the eight that named nothing specific, and its old wording repeated both the heading it points at and that section's own lead. Safe to reword because AgentRun.woke_because landed earlier the same day and no run had written a value, so nothing on the Decisions page changes meaning. The prompt is 2.2% shorter.
2026-09-21 — every part of the prompt now sits under its own ## heading, with a rule between each one. Three sections ran together before today because a divider was a literal line somebody had to remember to add: the signals table arrived as the last paragraph of the watchdog's alerts, and "What you just did" had no break above it either. Eleven more sections had no title at all, so the track record, the broker failures, the timing line and the watchlist all opened mid-sentence. build_prompt is now a declared list of sections, each built by its own function that knows nothing about its neighbours, and _joined drops the empty ones and puts one rule between the rest. The words are unchanged: every content line is byte-identical and in the same order, and the only text removed is a title the heading now carries — "Rules:", "You currently hold.", "Recent analyst signals.", "Answer in this shape:" and six more. What the model is shown is therefore the same facts with a different shape, so passes either side of today are comparable in substance but not in how the prompt reads.
2026-09-21 — the record now keeps why a pass started, not only what it did. The agent has read one of scheduler._WOKE_BECAUSE's eight sentences at the top of every prompt since 2026-09-12, and nothing stored it: the Decisions page could say what the agent did on a pass and never why it was asked. run_once now carries the reason from the first turn through the whole pass — a later turn cannot overwrite it, because a pass is woken once — and AgentRun.woke_because reaches the page, which repeats it under the same heading the prompt itself used: "Why you are awake" on the first turn, "Why this pass started" after. Null on a skipped pass, which never reaches the model, and on every row before today; nothing is backfilled.
2026-09-20 — moving a stop or a target now says what level it moved from, not only the level it moved to. "Moved stop to $336.50" cannot be read as raising a stop or loosening one, so adjust_exits now carries the resting price it replaced in its own outcome message. That message is what "What its own orders did" shows the next prompt and what the Decisions page renders, so both change together; a stop moved before today is not backfilled with the level it came from.
2026-09-20 — an order's own terms reach the record: whether it was a market or a limit order, the price a limit named, how long it may wait, and the levels an adjust moves the stop and target to. agent._order_detail carries them into the turn record and the orders-placed record. They were parsed, acted on and dropped before today, so a limit buy and a market buy left the same four fields behind and an adjust said only which ticker it touched. Null, or the key absent on a market order that names no price of its own, on anything recorded before today — never read as "market" or "no move" for that reason.
2026-09-20 — the analysis pipeline moves to TradingAgents v0.5.0, and three of the picks change what an analysis says. The research manager, the portfolio manager and the trader are now told that conflicting arguments alone are not a reason to Hold: commit to the stronger side, and Hold only when the evidence is still balanced after weighing it, or too thin for a call. Every bull and bear debate is conflicting, so the old wording collapsed a modest directional edge into a Hold. The three decision prompts also state the shape of the answer they want, which a model without structured output never saw, and an analyst report that was never produced now says so instead of arriving as a blank section the reader fills in from nothing. Ratings, and therefore the scorecard, are not comparable across this date.
2026-09-20 — the sentiment analyst reads four subreddits through one custom feed, where it read three through three requests. r/tradingwithcongress joins r/stocks, r/investing and r/wallstreetbets, and the three that were fixed in code are now a setting, so what the analyst is shown for any ticker is not comparable across this date. The feed answers in one request on either path — the same 12 NVDA posts as three separate trawl requests, in half the time — and a feed that cannot be read falls back to searching the three subreddits.
2026-09-20 — a Reddit window the feed cannot observe is reported as unavailable, not as "no posts found", and all the subreddits are searched in one request. Silence was scored as neutral sentiment even when the fetch was throttled or the window predated what the feed serves. The feed now answers for every subreddit in one request, which removes the 429 that cost a back-off on nearly every run, so the sentiment analyst gets more of what it asks for. Reddit's HTML search page has no combined form, so the trawl path still loads one page per subreddit. The same rule now covers Yahoo news and StockTwits.
2026-09-20 — an ATR multiple written as a range no longer discards the whole trader proposal. "2-3" reached pydantic, failed validation, and took the reasoning and the win probability out with it. It is dropped like any other unreadable value, leaving one null field.
2026-09-20 — the mechanical signal-follower holds at most five names, sizes each from the money it has now rather than the money it started with, and buys fractional shares. Nothing capped the count before: the buy was sized min(slot, cash) against a fixed fifth of the starting budget, so five positions was only ever an accident of that arithmetic consuming the budget exactly. Measured on seven $90 signals against $1,000: six positions, the sixth a single share at half the weight of the others. Worse, a rule whose cash had grown would have opened a seventh and a tenth. A slot is now the cash divided by the slots still free — a fifth of the book when nothing is held, and self-correcting after that — so the winnings are redeployed instead of sitting idle, and the count is what stops the sixth position. Fractional shares make the five weights exactly rather than roughly equal, which is the whole of what the rule is. An open position counts as one slot rather than being marked to market, so an unrealized gain waits until it is really cash.
2026-09-20 — the mechanical signal-follower sells at a signal's maturity date, not only on a Sell signal, so every equity and return it has ever reported is now computed differently. Both docstrings had promised the maturity exit since the baseline was written; only the Sell half was ever implemented. Nothing has been analysed on a schedule since 2026-09-08, so the agent decides which names get a fresh look, and a name it stopped revisiting produced no Sell signal for the rule to act on — the rule held those positions to today at today's price, and its exits measured the agent's research habits rather than the signals. A buy now also exits at its own signal's evaluation_date, at the graded price_at_evaluation, whichever comes first. Both prices were already stored, so no column and no fetch was added, but the baseline's whole history is recomputed on every page load and the old numbers cannot be reproduced. The rule can still only follow signals the agent paid for, which the site now says plainly: it weighs the agent's judgement, not its choice of what to research.
2026-09-19 — the fundamentals fetch answers from Yahoo Finance, and the industry ranking is gone. Since 2026-09-17 it read three Webull endpoints, and two of them, the financial indicators and the industry comparison, answered "404 Route Not Found" on every call: the sandbox host has no route for the /openapi/fundamentals/ family, so every pass that asked was shown "could not be fetched from the vendor" twice, and only the analyst rating answered. The ratios now come from yfinance's Ticker.info and the last four quarters from its quarterly income statement, the source the fundamentals analyst already reads, so the agent and its analysts see one set of figures. The industry comparison has no equivalent there and is dropped; the profile names the sector and industry instead, and nothing is ranked. The analyst rating stays on Webull. A pass fetches a ticker's figures once and reuses them. Webull's production host was not tried: it would be the first live-host code in the repo. Probed the same day, four samples with the change note in the prompt: 1 of 4 called the fetch and 0 of 4 quoted a figure from it, so the tool works and is unconfirmed as read, the status the analyst-report tables already have; see .claude/rules/agent.md.
2026-09-19 — the fetch ceiling on the tool channel is 30 fetches across 6 rounds, said as a ceiling, and the model is told when a round is its last. It was 12 across 5, worded as an allowance that "runs out", and a live pass planned "I have a limited number of fetch calls, so I need to be efficient", though neither number had ever been reached. A fetch never reaches Google and a round is one request, so the rounds stay few and the fetches need not; the last function response of a round now says when one round is left, and when none are, and that a wakeup 5 minutes out continues the work.
2026-09-19 — a Hold is no longer described as "not a reason to buy". The rule was written for gemma4:e2b after it bought 98% of the budget on a Hold, and the model has been gemini-3.5-flash-lite since about 2026-09-03. The analysis pipeline defines Hold as "maintain current position", a stance for someone who already owns the stock, and it said Hold or Underweight in 18 of 20 analyses over this deployment's first ten days, so the rule left the agent nothing to act on while INTC's 09-14 "Hold" carried a 2:1 plan and rose 14% in three days. The rule now says what the word means and that a Hold carrying an entry, stop, target and chance is a plan to judge on its numbers, after reading it.
2026-09-19 — the rules say how to size a position: from the distance to the stop and the loss you accept, not from the analyst's percentage. Nothing said anything about size before, and the one trade copied the analyst's "3% to 4% of total portfolio capital", written for a fund running many positions: 4 shares, $392 of $10,000. A frame, not a cap; Python still refuses only what cannot be executed as stated, and never resizes.
2026-09-19 — a read and the signals table both say that the entry, stop and target are computed by the app from the verified close and ATR, and that a price inside the analyst's text is the analyst's own. The table called them "the analyst's proposed levels". On 2026-09-15 the agent took the text's $97.00 stop over the table's $89.71, 1.1% under its fill and inside one day's range, and was stopped out in 100 minutes.
2026-09-19 — a change note tells the agent that limit and GTC orders exist. They arrived on 2026-09-17 with no note, and for five passes the agent planned to buy INTC on a pullback to $100 to $101 without ever resting an order there, while INTC went from $102 to $110.
2026-09-18 — the final pass before the close fires five minutes before that session's close, from the market calendar, so a half-day gets one at 12:55 PM ET. It was a fixed 3:55 PM checked by a one-minute tick, which on a 1:00 PM close never came and on any day could land up to a minute late. It is a quiv one-off now, re-armed after each session; the tick is the alarm's backstop only, every five minutes.
2026-09-18 — the candidate menu had no Yahoo trending names from the afternoon of 2026-09-17 until this fix. Yahoo began answering a request with Python's default User-Agent with 429, every time; the same request as a browser gets 200, so the fetch now sends one. Run 32 (2026-09-17, 9:04 AM ET) is the last menu with a trending name, and runs 36 and 37, the next two that called candidates, have none. The Webull screens and the congress source were unaffected.
2026-09-17 — every turn now records which channel answered it: json, tool or text-fallback. A turn the text fallback answered was otherwise identical in the record to a tool turn that fetched nothing, and the reason lived only in the container log; the first live pass on the tool channel was exactly that, and it took the log to see it. The field is null on every turn before this date. A single-turn pass that fell back now keeps its turn, the Decisions page says "Answered through the text fallback" on the pass, and the Discord post for the pass carries the same line, so a run of fallbacks is visible where someone is looking.
2026-09-17 — the tool channel's fetch allowance is 12 fetches across 5 rounds, its own numbers. It spent from the read allowance at first, 6 across 3, and the first live probe hit that cap after three reads: the three table fetches (watchlist, candidates, track_record) replace tables the prompt used to carry, so six left room for three reads, fewer than the JSON channel ever had. The bound that matters is the free tier's request count, and a round is one request however many fetches it carries, so the rounds stay few. The JSON channel keeps 6 reads across 3 turns.
2026-09-17 — on the tool channel the tracked-ticker table, the track record and the wakeup list are one line each; the full versions are a fetch away. The table ran to thirty rows of nine columns on every pass; it is now "You track N of at most M tickers. Never analysed: A, B. Moved 5% or more since their last analysis: C +7.5%, D -8.0%. Call watchlist for every one with its price now, its last analysis and what it said." That keeps what the table was for, a name gone stale or never looked at, and drops the rows that said nothing. The track record is its totals plus the Hold-pattern line, with track_record for the trades; the six wakeup timestamps are a count, with the same advice under it. The signals table, the holdings table, the alerts, the broker failures and the analysis timing are unchanged, because the model called the research reports its main input and the failed-order log useful. The JSON channel is untouched. Measured in the local container: the tool-channel prompt is 7,054 characters against 9,653 for the JSON-channel prompt built from the same database, and 4 of 4 probe samples called watchlist in their first round.
2026-09-17 — on the tool channel the agent fetches what it needs inside the same call, and the candidate menu has left its prompt. Five functions sit beside decide: read (an analysis's reasoning, until now a side that cost a further turn), candidates (the screened names, until now fifteen lines on every pass that the model itself called noise), fundamentals (new: a ticker's last reports' ratios, its rank among industry peers by trailing PE, and the analyst counts, from Webull, in backend/services/fundamentals.py), watchlist (the tracked-ticker table, still in the prompt as well for now) and track_record (the closed trades, likewise). A fetch's result goes back as a function response and the model is asked again in the same conversation, so a pass reads, then decides, without a rebuilt prompt in between; the allowance was the read allowance it always had at first, 6 fetches across 3 rounds (raised the same evening, see the entry above), shared by every act-turn of the pass, and once it is spent decide is the only function left. The universe rule is unchanged in substance: a research of a new ticker must name one that candidates returned this pass, so a model that never asked cannot research a new name, exactly as the menu in the prompt enforced. The vendor screen now runs once per pass, on the first candidates call, instead of on every turn. fundamentals is the two-step answer to the screener the model asked for on 2026-09-17: Webull has no filter screener, so the model fetches the rank lists, then a name's figures. Record: each turn now carries exchanges (every fetch's name, arguments and result, in order), a single-turn pass that fetched stores its turn where before it stored none, and the Decisions page shows what a turn fetched with each result verbatim under the prompt. On the tool channel three rules are reworded for functions and the prompt says to call candidates where it used to list them; the JSON channel is unchanged. Probed live in the local container, 4 samples: every sample called watchlist, candidates and track_record in its first round and read one to three analyses after; 2 of 4 then commissioned research on AMD from the candidate list, 1 hit the six-fetch allowance and was told so, and 1 called decide beside its fetches, was told to call it again, and did. Sample 2's thinking quotes nine prices that appear in a fetch result and nowhere in the prompt, the hard evidence the probe skill asks for. The cost moved with it: a pass is now 3 to 4 requests and 20,000 to 30,000 prompt tokens, because every round resends the conversation with the analyses read so far, against about 5,000 for a single-call pass before, and prompt_tokens on a pass now sums its rounds. Two samples spent a paragraph of thinking on whether the fetches are functions of their own or fields of decide, so the rule now says they are.
2026-09-17 — on Gemini the agent answers by calling a function named decide, through Google's SDK, instead of writing JSON into its reply. The call is forced (mode ANY, with decide the only function allowed), and the function's parameters are the schema in backend/services/decision_schema.py, where every side is its own object: a buy or a sell must now say order_type, market or limit, which closes the gap behind the probe two entries below, where the model wrote "limit" in its reason and sent a market order. The arguments are stored as the same JSON text every earlier answer was stored as, so the record's response column reads the same before and after; what changed is that a prose or malformed answer can no longer happen on this channel. The prompt on this channel ends with one line saying to call decide, in place of the JSON example, and the system message's first paragraph says so too. Every other provider keeps the JSON channel, and AGENT_ANSWER_CHANNEL=json forces it on Gemini, for a comparison or a rollback. Probed live the same evening in a local container running the same image: the API accepts anyOf in a function's parameters, the thinking summary arrives beside the function call at thinking level high (4,100 to 5,500 characters a sample), and 4 of 4 samples answered through decide. The first live pass found one bug: a function response sent under the tool role, as the SDK's README shows, is refused with Role 'tool' is not supported; it goes under user. That pass fell back to the text channel as designed, and its stored answer has no reasoning key, because the tool-channel prompt shows no JSON example for a text answer to copy. The fallback now appends the shape itself, so a fallback answer is whole.
2026-09-17 — the next wakeup now sees every note the previous pass wrote, not only its final turn's. The same gap as the entry below, one boundary later: AgentRun.wakeup_note was overwritten every act-turn, so only whichever turn happened to run last actually reached the next pass — a note written early in a multi-turn pass, never restated in its last answer, was silently dropped. _encode_wakeup_notes now stores the whole pass's distinct notes (plain text for one, a JSON list for several — the same convention already used for turns), and _last_pass_notes reads it back; a row from before this date has no brackets, so it reads as the one note it always was, no migration needed. describe_wakeup shows the full history under "Notes you left yourself last pass", oldest first, beside (not instead of) this pass's own in-progress notes from the entry below.
2026-09-17 — a note written mid-pass is now shown to every later turn of the same pass, not only to the next pass. next_wakeup_note in the prompt was always the previous pass's note — stable for a whole pass's act-turn loop, because the current pass is not recorded until it ends — so a note a turn wrote for its own later turns had nowhere to land: gone by the very next turn of the same call chain unless the model happened to repeat it. run_once now keeps notes_this_pass, accumulating every distinct note the pass has written so far, and describe_wakeup shows it in its own block beside the previous pass's note, not in place of it.
2026-09-17 — agent_book.validate's cash check now includes the broker's 2% buying-power cushion, not only the 0.5% entry-limit slippage buffer. Live on the 3:01 PM ET pass: the agent asked to buy 12 AAPL, the broker refused for insufficient buying power (_BUYING_POWER_MARGIN, added 2026-09-04, only ever fed the "You could buy" display column, never the check that actually gates an order). The agent read the refusal, recomputed affordability from the raw price — the only number it had — landed on the same 12 shares and $13.03 left over, and resent the identical order. buying_power_margin() in agent_book.py now reads the same broker constant (sandbox_broker.BUYING_POWER_MARGIN_PCT) that the display column reads, and both of validate's cash checks (market and limit) apply it, so an order that cannot clear the broker's real requirement is refused here, with the real reason in the rejection text, instead of costing a round trip to the broker and a second identical order.
2026-09-17 — arming a new stop/target now cancels whatever is already resting first, instead of assuming there is nothing. _arm_exits (a bracket refused at purchase, falling back to a plain buy plus separately-armed exits) and adjust_exits's arm-new branch both placed a fresh stop and target without checking whether one was already resting at the broker. AVGO was topped up, its bracket was refused a second time for unsettled cash, and the fallback armed a second stop and target on top of the first — the broker then saw more shares committed to sell than the account owned, and refused three identical adjust retries with OPENAPI_ORDER_NOT_SUPPORT_REVERSE_OPTION before an unrelated sell elsewhere in the pass cancelled the stale order as a side effect. Both call sites now cancel-and-await first, the same guarantee _sell_and_restore_on_failure already gave a plain sell.
2026-09-17 — limit orders added for buy and sell, with the agent choosing day or gtc, and a new cancel order. Every order was a market order (or a marketable-limit bracket the code chose on the agent's behalf) until now. A buy or sell may now carry "order_type": "limit", a "limit_price", and "time_in_force". A limit buy does not bracket — the broker will not hold a combo's exit legs inactive against a master that may not fill for days — so its stop/target are left unplaced; the agent is woken the instant the fill is confirmed (mirroring the existing stop-fill wake) and sets them itself with adjust, off the real fill price. A GTC order can sit for days, so agent_book.build_book now reserves its cost against cash until it fills or is cancelled with the new cancel order — before this, a pending order spent nothing, which was only ever true because every buy used to fill within the same pass or by the next open. build_prompt also shows every still-unfilled limit order in its own table, next to holdings — the outcome line that first announced it belongs to the pass that placed it and would otherwise vanish, so a GTC order could sit for days with nothing in the prompt saying it exists. A pending limit sell reserves its shares the same way the buy reserves cash — holdings is rebuilt from filled trades alone each pass, so an unfilled sell still shows the shares as held, and without the reservation a later pass could sell them again before the first one resolves or is cancelled.
2026-09-17 — probed the new limit-order rules before shipping them, on a synthetic scenario (a $2,000 book, no holdings, one Buy signal with an entry price $7 under the current quote). 4 of 4 gemma4-e4b-qat samples bought at market for the full quantity and never considered a limit order, despite the gap between the signal's entry price and the current price being exactly the case a limit order exists for — a rule that is mechanically correct and never reached for. Consistent with this file's own "tables get read, prose gets skimmed" pattern: order_type/limit_price are explained in prose rules and a JSON example, with nothing in the signals table itself suggesting a limit order as the entry's natural counterpart. Shipped anyway — market orders remain the safe default behavior, and nothing regresses — but this is unconfirmed as read, same status this file gives the price-range column and the four analyst-report tables. Worth a follow-up placement change (e.g., a note in the signals table itself when entry price and current price diverge) and a second probe round rather than treating the current wording as final.
2026-09-17 — probed again with the model told its own plan, rather than left to invent one, and the tool is understood but not reliably invoked. Same book, but wakeup_note now carried an explicit prior instruction — "if AAPL breaks and holds above $234.00, place a limit buy at $236.00 for 8 shares" — with the price at $236.20, confirming the breakout. Both models can place the order correctly: 1 of 4 gemini-3.5-flash-lite samples and 1 of 4 local gemma4-e4b-qat samples produced the exact right JSON, order_type: "limit" and limit_price: 236.00, and Gemini's sample explicitly reasoned through the deferred-exit rule too ("place the limit order correctly, then manage the target and stop after the fill"). Neither model does it reliably even when told the exact price and share count. Gemini's other three samples were honest about dropping the plan — two switched to a plain market buy and said so, one asked to read the analysis first. The local model's failure mode is worse: 3 of 4 samples wrote "executing the disciplined limit buy" or "limit price suggested" in their own reasoning and note text, then submitted an order with no order_type or limit_price field at all — a plain market buy the model believed was a limit order. This is the "keyword mention, not mechanism" failure this file already tracks for other sections, but sharper: here the model's own words describe the correct action while its actual tool call does something else. Not a reason to change the prompt yet — one correct sample per model shows the rule is learnable, and a bigger sample is needed before concluding anything about reliability. Worth revisiting with more samples, and worth checking whether narrating "limit" in reason while omitting order_type is common enough to warrant a Python-side check that flags the mismatch back to the model, the way a refused order already is.
2026-09-17 — the same scenario probed on gemini-3.5-flash-lite (4 samples), and the finding holds across models. 0 of 4 used a limit order either: one bought at market, one passed on the 1% win-probability outright, and two asked to read the analysis first — explicitly because a 1% chance paired with a positive expected value looked contradictory, a distinction gemma4 never raised in eight combined samples. Worth keeping separately: Gemini caught an internally inconsistent signal that gemma4 took at face value both times. But on the question this probe was run for, the result agrees with gemma4's: nobody has picked a limit order yet, on either model, in 8 samples.
2026-09-16 — the persistent memory feature was probed, and the finding is conditional, not a flat yes or no. Two runs against gemini-3.5-flash-lite, 4 samples each. Turn 1 used the real live book — market closed, $43 cash, one holding with no stop resting — and 0 of 4 samples engaged with two seeded memory notes at all; the single obvious job in front of the model (fix the unset stop) crowded everything else out, the same pattern already on record for the price-range column and the four analyst-report tables. A second run built a scenario by hand — market pinned open, $2,000 free cash, no holdings, and a memory note naming a ticker with a live, affordable Buy signal — and 3 of 4 samples cited the note by name, unprompted, as the reason to pass: "persistent memory dictates staying away from INTC after multiple stopped-out breakouts," "keeping persistent memory of the INTC breakout failure rule." Neither phrase is copied from the note, which is the paraphrase test this file uses elsewhere. The section is read when the model has an actual decision in front of it that the note bears on, and skipped when something more urgent has already claimed the pass. Kept the feature; see .claude/rules/agent.md for the full write-up.
2026-09-16 — persistent long-term memory notes added for the agent (side: "memory"). next_wakeup_note only carried notes to the next immediate wakeup pass. The agent now has persistent long-term memory notes that are stored in the database (BotSetting) and injected into every prompt until explicitly cleared or removed. Capped at 10 notes to prevent prompt crowding.
2026-09-16 — explicit multi-buy cash allocation guidance added to prompt rules. The prompt rule for buying now explicitly reminds the model that when placing multiple buys in a single turn, it must allocate quantities so that sum(quantity × price) fits within available cash, reducing overspend refusals on multi-buy orders.
2026-09-16 — trailing stop protection guidance reworded in adjust rule. The adjust rule now explicitly advises the agent to raise stops on unrealized profit or UNSET positions to protect gains and manage risk as trades progress.
2026-09-16 — watchlist untrack guidance added to prompt rules and table headers. The rules and watchlist table header now instruct the agent to untrack watched (unheld) tickers with stale or 'never' analysed status to free watchlist slots for new research orders when the watchlist cap is reached.
2026-09-16 — 'Day High' and 'Day Low' columns added to the tracked tickers and recent analyst signals tables. An agent note asked for immediate session high/low context beside "Price now" to judge daily volatility and support breaks before commissioning research. Built as day_range_today reading today's session range from the bar cache, and displayed in both tables beside "Price now".
2026-09-16 — the candidate menu reserves slots for congressional trades and Yahoo trending, instead of losing them to a volume sort. Both text sources, added 2026-09-15, hand back real tickers that were then sorted into the menu by trading volume alongside the two Webull screens — and a Webull "most active" row is a volume leader by definition, so a congressional trade or a searched-for ticker almost never outranked one. Live containers ran several passes after deploy with neither source ever reaching the agent. MAX_PROPOSED goes from 8 to 10; _RESERVED_SLOTS guarantees congress up to 3 seats and trending up to 2, each filled by its own most-liquid names first, and Webull fills whatever is left. The menu line shown to the agent now also names the source (via congress trade (QuiverQuant), via trending (Yahoo Finance), via most active), so a read of the prompt can tell which screen actually surfaced a pick.
2026-09-16 — an unguarded position wakes the agent, instead of waiting for its own next chosen time. A buy that filled with no usable stop or target, or a failed sell that could not get its exits back, was written to the alert log and to Discord and nothing else — the agent could sit for up to four days not knowing a position of its own had nothing under it. agent.AgentRun.unguarded now records it, and scheduler._run_agent_pass_locked pulls the agent's own alarm forward the moment the pass ends, the same mechanism already used for a new change note.
2026-09-16 — a stop or target actually firing at the broker wakes the agent, instead of only posting to Discord. _settle_agent_fills told a person a resting exit had closed a position and told the agent nothing, so it could learn its own position was gone only whenever it happened to wake for some other reason. It now also calls _maybe_run_agent, the same trigger already used for a sharp move or an earnings date.
2026-09-16 — the table-based read section was probed, and the two full plans read differently than the four report tables. Four samples, a real NVDA read, called through ollama-proxy from outside the deployed container against a read-only copy of the live database (TRADING_DB_PATH env override in backend/database/engine.py, reverted after). 2 of 4 samples explicitly named and cross-referenced Investment plan and Trader's plan, and both caught that the two disagreed on the same signal — Investment plan said Hold, Trader's plan said Buy — a contradiction that could not have been seen before this section existed, since neither plan reached the agent at all before 2026-09-15. 0 of 4 samples quoted anything unique to the Market, Sentiment, News or Fundamentals tables. Kept the plans in full; the four tables stay unconfirmed as read, same status as the 500-char prose version they replaced. See .claude/rules/agent.md for the full writeup and quotes.
2026-09-16 — a read's analyst-report section became each analyst's own summary table, in full, plus both plans in full. The 500-char trim shipped 2026-09-15 cut each of the four reports mid-sentence, wherever the character count happened to land — no better than the rationale it sat beside, since it never reached the report's own conclusion. Every analyst prompt already ends with an instruction to append a markdown table summarising its own findings (see TradingAgents/tradingagents/agents/analysts/), so analysis_reader._last_table pulls that table — whatever the model titled it — instead of guessing a fixed name or trimming by character count. investment_plan and trader_investment_plan, already stored per signal alongside the four reports, are now included whole rather than not at all: both run under 2,000 characters and are what the model's own final call drew on. Also split into its own section, headed What you asked to read in bold, with a rule (---) before it — it used to run on directly from "recent wakeups" with no divider and read as that section's last line. Not yet re-probed against the live model; the 2026-09-15 finding (0 of 2 samples quoted the old prose excerpt) does not necessarily carry over to a table, which this repo's own findings say reads differently than prose between tables does.
2026-09-16 — every major prompt section now has a rule (---) between it and the next. The prompt had grown to fourteen sections run together with only a blank line apart; a horizontal rule is a stronger visual break, and this repo has already measured that a heading location changes what gets read, not only what renders.
2026-09-16 — two rules with no per-pass figure moved from the rebuilt part of the prompt into SYSTEM_PROMPT. "Some signals carry how good the analyst thought the bet was..." and the note action rule quote nothing from this pass's book or signals, so they belong with the rules that never change rather than the ones build_prompt reconstructs on every call. No wording changed.
2026-09-15 — a read now carries a short take from each of the four analysts, not only the Rating and the rationale. market_report, sentiment_report, news_report and fundamentals_report were computed and stored for every analysis and never reached the agent at all — it traded on a verdict it could not check against the evidence behind it, from a report that may have come from the same model with none of this agent's own cash or holdings in view when it was written. analysis_reader.read now trims each of the four to 500 characters and appends them, labeled, after the rationale; a section with nothing on record (an analysis from before this date, or a report row that never saved) is left out rather than shown empty. Probed the same day, and shipped unconfirmed as read: 2 of 2 samples on a live HOOD read reasoned entirely from the signals and holdings tables and quoted nothing unique to the four sections — the same pattern already seen in the change-notes section and the holdings price-range column. Kept anyway, on the same reasoning those were: correct, cheap, and this may be one small model's habit rather than a property of the fact itself. Two samples is thin evidence either way.
2026-09-15 — holdings became a table, and a missing exit now says UNSET in its own column. The prose line had grown to nine clauses; probing it (three fresh runs, one holding, nothing else in the prompt) showed the model quoting every earlier clause and never the range one, buried second-to-last. Converted to a table, the same shape the signals section already uses, with a plain UNSET for a missing stop or target rather than folding it into a sentence that reads the same whether one side is resting or neither is — confirmed read: the model paraphrased UNSET | UNSET as "no stop-loss or target orders set."
2026-09-15 — the price-range column is shipped unconfirmed as read. Nine probe samples across two placements — last column, then moved beside Price now — and no run ever quoted the $208.93–$233.45 shape of the number, including single-holding runs with nothing else to look at. The model reliably reports avg cost, current price, days held and the resting-exit state; the range never joins that set regardless of where it sits. Kept in the table anyway: it is correct, cheap to render, and this is one small model's behavior, not a property of the fact itself. Do not read its presence as proof the agent uses it — see .claude/rules/agent.md.
2026-09-15 — each turn's own reasoning and what it asked for are now kept, not only the pass's final answer. _decide always parsed every turn's reasoning to decide what happened next, then kept only the last one — every earlier turn's reasoning was computed and thrown away, so a two-turn pass published one explanation for what were really two different decisions. Directly relevant now that a pass may run 8 turns rather than 3 (see below): a reader following a long pass needs the story turn by turn, not only its ending. agentrun.turns[i] now also carries that turn's own reasoning and orders — what it asked for at that point, parsed the same way the backend parses every answer, not necessarily what was screened or executed. Null on every turn recorded before this date. The events page shows it beside that turn's own prompt and answer.
2026-09-15 — a pass gets 8 act-turns now, not 3. Three covered one ticker's happy path: commission research, act on the verdict, see the fill — nothing more. It left no room to rest a stop and target the broker refused at purchase, or to correct a mistake once the fill was seen, both real and both this app's own tools already allow. Research now measures 9-10 minutes, down from 16-20, which is most of why 3 was ever this tight: a turn that commissions research holds _pass_lock for the whole wait, and the old figure made even 3 of those turns a possible hour-long pass. Not shown to the model — _MAX_ACT_TURNS is a Python backstop, never a prompt number — so no probe-the-prompt pass is needed, but this does change what a pass can accomplish, which is a comparability line. Watching live whether the agent actually uses the extra turns or still settles a pass in 2-3.
2026-09-15 — a read bundled into the one retry turn a refused order gets no longer runs silently into nothing. Follow-on from the same-day fix below: the refusal-retry is the pass's last turn, so a read asked for there was already dropped by design — _decide throws away the read half and screens whatever else came with it. What was missing was telling the agent: nothing said a read would not work there, so it could spend its one chance to fix a refusal on a read that never happened, and never learn why. The "fix it" prompt now says outright that a read does not run on that turn, before the agent tries. Needs its own probe-the-prompt pass.
2026-09-15 — an order bundled beside a read in the same answer is no longer carried out silently — or silently dropped. Caught live: after an early wake, the agent's second answer asked to read INTC again and, in the same JSON, to buy 50 shares and move the exits. _decide splits a read out before screening because it changes the pass's control flow rather than the book — right, but the buy and the adjust bundled beside it vanished with no trace: not rejected, not failed, not logged anywhere the agent could see. The next turn's prompt now lists exactly what was not carried out and says why, so the agent can resend it once it has read what it asked for; a new fixed rule says the same thing up front. Needs its own probe-the-prompt pass to confirm the model reads the new section rather than skimming past it.
2026-09-15 — a holding's line now names the low and high it has traded since purchase, when the bar cache has one. Until now the agent saw only the buy price, the price at its last research, and the current price — three points that cannot tell a stock that dipped and came back from one that only climbed. agent.price_range_since_purchase reads the daily bar cache from the purchase date to today and adds "has ranged $X to $Y since you bought it" to the holdings line when a range is available. Needs its own probe-the-prompt pass before this counts as confirmed read, not only rendered.
2026-09-15 — a batch snapshot no longer goes empty over one bad ticker. Confirmed live within the hour of shipping the two text sources above: ELN, EA and LBRDK don't exist in Webull's US_STOCK category, and the vendor refused the whole 100-ticker request rather than just those three — get_snapshots had assumed an unrecognized ticker was silently dropped, which held for a single-symbol lookup but not this endpoint. It now parses the rejected symbols out of the vendor's own error text and retries once without them, instead of losing every other candidate that round.
2026-09-15 — the candidate menu adds two text-only sources: congressional stock trades and Yahoo Finance's trending list. Until now every proposed ticker came priced from a Webull screen; a name found only because a member of Congress traded it, or because it is suddenly being searched for, had no way in. Both new sources hand back a bare ticker, not a priced row, so each is verified against a real Webull snapshot and screened by the same price/volume/move floors as everything else before it can reach the shortlist — a ticker pulled from a scraped page is not trusted data until priced. Benzinga and MarketWatch's free feeds were tried and dropped: neither tags a headline with its ticker, and guessing one from a company name is the same invented-fact failure this app already guards against for prices.
2026-09-15 — the signal-stop and target alerts no longer read as an order that already fired, and now name the research date. Probed against the live book (AVGO 100% of the account, no resting stop, only a target): the alert's old wording — "AVGO at $348.50 reached the $354.72 stop from the Hold signal of 2026-09-10" — carries three different AVGO prices in one sentence with no stated direction. Across 16 probe runs (four prompt variants, four samples each) nearly every reasoning trace spent hundreds of words trying to work out whether a stop had already executed instead of noticing the real fact: nothing is resting on the position. Only one of sixteen runs placed the missing stop. The message now states direction plainly, drops the stale alert-time price, and says outright when nothing is resting on the position. Both messages also now say the level came from research on a named date and call themselves a rule firing against that old level, not a new analysis — since the agent picks its own research cadence now rather than a twice-daily sweep, the signal behind a tracked ticker can be old, and the alert must not read as though it carries a fresh verdict.
2026-09-13 — the sentiment analyst reads Reddit through a browser, and a throttled subreddit reads as unavailable, not silent. The logs showed the RSS feed refusing r/stocks and r/investing after the first subreddit of an analysis, and the analyst was told "no posts found" for both. Upstream 7cc478a now marks a failed fetch unavailable, and with REDDIT_TRAWL_URL set the fetcher loads Reddit's search page through the trawl container, with scores, comment counts, the body of every post shown, and no post older than 7 days. Sentiment reports from before the deploy that ships this do not compare with reports after it.
2026-09-13 — a later turn of a pass says why the agent is asked again. The second turn of a pass repeated the reason the pass started — "A change to this app woke you" — as though it were a new wake; on 2026-09-13 the agent still read its own research result correctly, from the section below that line. A later turn now keeps the line as why the pass started, and adds why it is asked again: its orders were carried out, an analysis it asked to read is below, or part of its answer was refused.
2026-09-13 — the four analysts in one analysis run at the same time. They ran one after another, although no analyst reads another's report; the debates still run in turn, so each analyst's first message and what the trader reads do not change. One analysis on the local pool takes about 11 minutes, not 16, so duration_seconds, the analysis time the agent is shown, and the electricity estimate do not compare across this date.
2026-09-13 — a failed analysis is retried, and one that still fails is reported as failed. Since research began to run inside the pass on 2026-09-12, an analysis that failed was reported to the agent as finished, beside the older analysis of the same ticker; no pass met this, because the two failures in the logs are from 2026-09-10 and 2026-09-11. The app now retries an error in its own code once, waits for the model service to answer again with a doubling delay for up to an hour, and tells the agent what stopped the analysis and whether it was charged.
2026-09-13 — an early wake no longer tells the agent that it asked to be woken. A restart with new change notes, a sharp move, and the earnings check all woke the agent by firing its pending alarm early, and the alarm always said "You asked to be woken now". The prompt now names what woke it, shows the note it left beside the time that note was written for, and asks it to choose its next wakeup and note again, because the early pass replaces the planned one.
2026-09-13 — a Gemini pass records its thinking, for every turn and every analysis call. The decision pass read thinking only from OpenAI-compatible endpoints, so every Gemini pass stored an empty thinking column; Gemini also returns no thinking unless TRADINGAGENTS_GOOGLE_THINKING_LEVEL is set. What Gemini returns is a summary that Google writes of the model's thinking, not the raw reasoning an Ollama model returns, so do not compare the two texts as the same kind of evidence.
2026-09-13 — a deployment can state its model's rate limits, and research that cannot finish inside today's requests is refused. LLM_REQUESTS_PER_MINUTE, LLM_TOKENS_PER_MINUTE and LLM_REQUESTS_PER_DAY make the app wait before it sends a call the vendor would refuse. A free Gemini key allows 500 requests a day and one analysis uses about 20, so when too few requests are left, the agent is told so in the same pass and nothing is charged, instead of an analysis failing halfway.
2026-09-12 — the agent is told, beside the order rules, that a closed market refuses a buy or a sell but still takes an adjust. The clock line has always said the session is shut and that was never enough: three probe runs wrote "the market is closed" and placed an order in the same breath, one of them explaining that orders "will not execute until market open" — a fair guess, and wrong, because this broker refuses rather than queues.
The adjust exemption came from the record, not from reasoning. An earlier wording had the broker refusing adjusts too. On Labor Day run 15 adjusted two exits at 09:31 while runs 16, 17 and 18 had five buys and sells refused over the following six hours; no adjust has ever been refused in nine failures.
The behavioural claim is unproven. Seven probe samples with the rule placed no buys or sells, but the baseline placed none either — across every baseline run it is 2 of 11. What is established is that the correction is read: six of seven restate it in their own words. Whether the live book's closed-market failures stop growing is the measurement.
2026-09-12 — the clock had no holidays, and a whole day of the experiment went on refusals because of it. On Labor Day, Monday 2026-09-07, the first line of every prompt read "the market closes in 5h 28m, at 4:00 PM". The agent believed it, as it should, and placed five orders across three passes that the venue refused. The line now names the reason it is shut and when it opens again, skips holidays when working out the next open, and knows about the three half-days a year.
Five of the six market-closed refusals in the whole record were that one day; the sixth was a Saturday. The calendar is computed from the published NYSE rules rather than fetched or listed, so it cannot go stale.
2026-09-12 — five decisions had been silently dropped by the parser, and three of them were the agent asking for something. It was noticed because the agent's note said it had researched NVDA and no analysis had run. Reading back through all 52 stored answers: two notes, and three research orders. A note is the agent telling us a tool is missing, so a silent drop there loses the one message that was meant to reach a person.
Two different faults. The model sometimes puts an order beside orders rather than inside it — a top-level "research" or "note" key — and the parser only ever read payload["orders"]. And run 44 closed a JSON string with an apostrophe instead of a quote, so the whole answer parsed to nothing and its two research orders read in the record as an idle pass the agent chose.
Only research and note are salvaged, and only repairs that cannot change a meaning are applied. A bare "sell": "AVGO" says nothing about how many shares, and a parser that guesses there places a trade nobody chose. Runs 9, 36, 44 and 46 are therefore idle passes in the record that were not idle — the agent asked, and nothing heard it.
2026-09-12 — the agent is told why it is awake, and can leave a note for its own future self. Four things start a pass — its own chosen time, something noticed while it slept, the last call before the close, a change to the app — and it was told none of them; the labels existed as log lines. next_wakeup_note is the other half: the agent has no memory between passes, so everything it works out is otherwise gone. The prompt carries prices and positions, never conclusions.
It is a handover, not a justification. The rule tells it what the next prompt will already contain, so it does not spend the note restating a price. Probed across seven runs: five quoted the note back and reasoned against it — "Did AVGO break $366.16? $361.99 is slightly below" — which is the thing it could not do before.
2026-09-12 — nothing is analysed unless the agent asks for it and pays for it. A sharp move or a volume spike used to commission an analysis on the spot, and the pre-market earnings check did the same for anything reporting soon. Both chose what the agent should study, and both were billed to the agent's own research budget — ten such charges on the live book, none of them asked for. The watchdog reports now: what it saw is a table in the prompt, and the agent decides whether any of it is worth $0.05 and sixteen minutes.
The timing is the argument, not the principle alone. A pass takes about a minute and an analysis sixteen. A sharp move is exactly the moment selling may beat studying, and the agent was not asked until sixteen minutes of research it had not ordered had finished — describing a price that had moved again. It can still order that research, and since this morning it runs inside the pass.
The agent could not see any of this before. The alert table was never read into a prompt, and the tracked-ticker table shows the move since the last analysis, which cannot tell a 5% fall this morning from a 5% drift over three weeks.
Triggered wakes are no longer gated on market hours. That gate existed because a move worth analysing at midday is worth nothing by morning — an argument about analysis, which this removes. Moving a stop, ordering research for the open, untracking and choosing a wakeup all work at any hour. The earnings check proves the point: it runs pre-market, so under the old gate it could never have woken anyone.
2026-09-12 — a pass is a loop now: the agent acts, sees what its own orders did, and is asked again. Research runs inside the pass — it waits the sixteen minutes, then reads the verdict and the analyst's reasoning and can act on it before finishing. Trades report back the same way, including what is actually resting under a buy. A pass ends when an answer does nothing, which is also what "no action, just wake me later" looks like. Bounded at three act-turns, with one read allowance shared across them, and an answer that repeats the previous turn's orders ends the pass rather than placing them twice.
The old shape never worked, and the evidence says so. The prompt promised "you are asked again automatically when one you ordered lands". It was not: the pass dispatched its research and then, still holding _pass_lock, called the wake — which begins if _pass_lock.locked(): return. Every one of those wakes was dropped. Of seventeen analyses finishing across 2026-09-10 and 09-11, none produced a pass. A second fault compounded it: the cooldown was stamped before the attempt, so a pass that was skipped still suppressed every trigger for thirty minutes. Both are gone — the first by construction, since there is nobody left to wake.
The sharp-move rule now says it only covers tracked tickers. It was stated without the qualifier, and the watchdog only ever scanned the watchlist. The agent read it, saw ORCL down 5.4%, and reasoned that an analysis must be coming — for a ticker nothing was watching.
2026-09-11 — created_at on a signal means when the analysis started, for every row. It had meant two things at once: record_signal stored the finish, while the rows recovered by backfill_signal_timestamps carried the start decoded from trace_id. The split is exact and provable — rows up to 31 equal their trace start to the second, rows from 32 equal start plus duration_seconds — so the seam sat at 2026-09-09 with nothing marking it.
Start, not finish, because it is the instant that was actually observed. The old rows hold a real recorded start; correcting them to a finish would mean writing a timestamp nobody watched a clock for. The new rows can be moved to the start exactly, since every row keeps the trace_id that encodes it. The agent reads this field too — _analysed_at puts it in the prompt — so an analysis it is told about now dates from when the work began, roughly 16 minutes earlier than before on this hardware.
2026-09-10 — the agent reported the timing contradiction itself, twice, and it was still half there after the first fix. Two notes, at 09:30 and 15:53: "the top text says about 2 minutes, while the rules say about 20 minutes and that it lands about an hour later". The first pass at this removed "about an hour" and left "about twenty minutes" — a second hardcoded figure in the same rule. One duration is stated in the prompt now, the measured one, and the JSON example was still showing "45 minutes" under an instruction to use an ISO datetime.
This is the note action doing exactly what it exists for. The agent could not act on the contradiction and said so instead, precisely enough to fix — including the part the first fix missed.
Reading is bounded now rather than rationed: six analyses and three rounds of asking, per pass. One read was a restriction with nothing behind it but the fear of a loop, and a bound answers that directly — the analyses cap stops a pass reading the whole watchlist, the turns cap stops a model asking for one more thing every round. A person deciding whether to buy reads the research first, and often more than one piece of it. Reads no longer share the refusal retry's turn either: one is the agent gathering what it needs, the other is Python saying the decision cannot be executed.
The principle behind it is now in CLAUDE.md, because it will decide future arguments. Tokens spent deciding well are not waste; tokens spent guessing are. Cut the confusion, fund the deliberation — and where a limit exists only because an unbounded version might loop, bound it rather than ration it.
Reading an analysis became expected rather than rationed. The first wording said to read "when the reasoning would change what you do, not out of habit", which is advice to hesitate over something that costs nothing. The costs are asymmetric: reading too often spends a turn the pass had anyway, reading too rarely means acting on a single word. A signal line is a verdict; the reasoning is where the case for it lives.
A Hold is named as the one most worth reading. It is the decision that says least: it can mean the analyst saw nothing, or saw a case for holding a position and none for adding to it, and the word is the same either way. The agent spent a long stretch of one pass reasoning about which it had, which is the question a read answers.
What a day on a hosted model costs, measured. qwen-3.8-27b on Cerebras ran 6 analyses and 12 decision passes for $2.54 — 1.38M prompt and 714k completion tokens, 34% completion because the model reasons out loud. That is $0.40 an analysis against roughly $0.04 of marginal electricity for the same six on the local pool, which is ten times, and it is a real figure rather than a projection.
2026-09-10 — six fixes read straight out of one thinking block, two of them real bugs the model spotted before we did. A Cerebras pass at 3:59 PM spent most of its reasoning on things the prompt should have told it, and said so in enough detail to fix each one.
The tracked table was showing a stale analysis, and the agent noticed. It read two INTC rows for the same day — one at $106.24, one at $100.44 — and asked "the tracked table might not be updated to latest?". It was right: that row came from get_recent_signals(limit=1), which orders by signal_date alone, so with two analyses on one day it returned whichever row came first. The agent was being shown a price the stock had already moved 5.5% away from.
A clock time earlier in the day silently became five minutes from now. "09:00" at 3:59 PM resolved to 9 AM that morning, already past, and the clamp turned it into 4:04 PM. The agent avoided this by computing "1021 minutes" by hand across a paragraph of arithmetic — self-defence that cost it tokens and cost the record a legible intention. A clock time now resolves to its next occurrence.
The prompt contradicted itself about research. The rules said an analysis lands "about an hour from now" — a figure written in by hand — while the measured line two sections above said two minutes. It also read as though the agent had to schedule its own return: it does not, because _run_triggered_analyses asks it again when the analyses land. Both now say the measured time and state that the wake is automatic.
Dates carry times, and "price now" carries its own timestamp. A date alone cannot order two analyses of one ticker on one day, which is exactly what the agent could not resolve.
2026-09-10 — the prompt states the year, tables the numbers, and moves its fixed rules into the system message. The agent's own reasoning showed it working out what year it was from a date that never said, and puzzling over whether a price on a signal line was the price now or the price at the analysis. Three fixes to one prompt: the clock line carries the year, the signals and the tracked tickers are tables with named columns instead of run-on sentences, and the rules that never change moved to the system message, leaving the user message to the figures that do.
Measured in three shapes, same data, same model, back to back. The tables alone cut the model's output; moving the fixed rules to the system message cut it again and took a quarter off the wall clock.
| Old | Tables | Tables + split rules | |
|---|---|---|---|
| Seconds | 73.6 | 73.5 | 54.0 (−27%) |
| Prompt tokens | 3,435 | 3,570 | 3,541 (+3%) |
| Completion tokens | 2,383 | 1,952 | 1,709 (−28%) |
| Reasoning characters | 5,707 | 4,325 | 3,609 (−37%) |
All three bought 2 CRWV. The old and tabled runs then bought 6 MARA; the split one bought 1 SMCI instead — a different second choice, not a different thesis. One run each at temperature 1 is one sample, and this project has measured two of twelve paired analyses agreeing, so treat the direction as encouraging and the size as unmeasured.
Measured rather than assumed, on the same data through the deployed model. Both prompts were built from one database copy, so only the formatting differed, and both went to gemma4-e4b-qat-128k back to back. The prompt grew 3.9% and the model's output fell: completion 2,396 to 2,006 tokens (-16%), reasoning 5,231 to 4,878 characters, wall clock unchanged at 73 and 75 seconds. Both runs reached the same decision — buy 2 CRWV — so the format changed how much working-out it took, not what it concluded. One run each, at temperature 1, is one sample and not a result.
Every line the model receives is now one complete thought. The rules are wrapped in the source so agent.py stays readable, and that wrapping was reaching the prompt — a rule arrived as five lines, four of them beginning mid-sentence. This repo already forbids hard-wrapping prose in Markdown for exactly that reason. Continuation lines are folded before the prompt is sent; the JSON example is indented by one space rather than two and is left alone.
The rules split by whether they vary, not by importance. A rule holding a number from this pass — the cash limit, the watchlist cap, the horizon — has to be rebuilt every time, so it stays in the user message. The rest are constant across every pass of the experiment and were being re-sent each time.
2026-09-10 — the agent can ask to read an analysis it has already paid for, and the record now holds every turn of a pass. It saw one line per signal — the decision and the levels — and never the reasoning, so a Hold that meant "keep a fifth of the position and defend it below 102.70" reached it as the same word as a flat Hold. Naming a date lets it compare the analysis it bought on against today's.
Reading is free and does not count as acting. The $0.05 charge exists so that choosing what to study costs something; re-reading what it already bought teaches nothing about that choice. And a pass that only read is still an idle pass, for the same reason a pass that only left a note is — otherwise "let me look at the analysis" becomes this model's way of not deciding.
One extra turn per pass, shared with the refusal retry. The retry has been capped at one since it was built, on the reasoning that a loop arguing with a small model would spend the market open doing it. A read is the same cost with the same risk, so the two draw on one budget: read, or retry, not both.
agentrun records every turn, which it did not before. A refusal retry rebuilt the prompt and overwrote the first one, so a two-turn pass was published as though it were one — and this site's claim is that every prompt the agent saw is on the record, word for word. That was a small gap while retries were rare; a read-then-decide pass makes it the normal shape. Passes before today hold one turn, which is what they were.
2026-09-10 — the research rule no longer promises the next pass will be inside market hours. It said an analysis comes back "within the hour, while the market is still open", which stopped being true the moment the agent could be woken at any hour — and it contradicted the same prompt's advice to wake early and have the open's research ready.
2026-09-10 — the agent is asked when the market is shut, instead of being turned away at the door. run_once refused every pass outside market hours, so the pre-open research the prompt invites it to do produced nothing at all — no research, no exit adjustment, not even a note. The refusal now falls on the order, which the broker rejects and reports into the next prompt.
2026-09-09 — the model's reasoning is kept, after being generated, paid for and discarded since the day the agent started. Roughly nine tenths of what the model produced was never recorded, so the record held every decision and none of the working-out. Passes before this date store nothing, and nothing is backfilled.
2026-09-09 — a decision pass records what it cost: prompt tokens, completion tokens and seconds. Nothing had ever counted them, so no question about what a prompt change costs could be answered. Stored NULL before this date and never zero, because a zero would read as a free call.
2026-09-09 — the agent may sell whenever it judges it right, and is no longer told the money is fake. A resting stop is a floor under a position, not a reason to leave it alone — and an agent told the stakes are imaginary is not being asked the question this experiment exists to ask.
2026-09-09 — two tickers the agent commissioned on 2026-09-08 were never analysed, and nothing said so. SMR and CRWV produced no signal, no charge and no error, so the agent spent the next day deciding around research it had ordered and never received.
2026-09-08 — a signal records the time of day it was created, not only the calendar date. Every event on a day had collapsed onto one daily candle. Signal.created_at is null for anything before this date.
2026-09-08 — the compulsory morning analysis of the whole watchlist is gone. Everything tracked was analysed and charged for at 11:00 UTC whether the agent wanted the answer or not; it now commissions every look itself, holdings included, bounded by cash rather than by a schedule.
2026-09-08 — the failures section no longer tells the agent a retry will usually fail. That stopped being true once the underlying bug was fixed, and the agent appears to have believed it anyway — holding a position it had just given a good reason to sell.
2026-09-08 — the agent is told when a note it left has been acted on. A note reached maintainers and nothing ever told the agent the ground had moved, so it kept asking for things that already existed. Entries in backend/agent_changes.json from the last three days now appear in the prompt.
2026-09-08 — a sell waits for the exit cancellation to take effect, not merely to be accepted. cancel_order returns before the broker has acted, and AVGO's sell landed in that gap on two consecutive days.
2026-09-05 — the wakeup is a real alarm, fired at the second the agent asked for. Polling for it meant a chosen time could arrive minutes late.
2026-09-05 — the agent owns its own schedule, day and night. The fixed 13:35 UTC pass is gone: it is woken when it asked to be woken, and at no other time.
2026-09-05 — the wakeup checker ran every five minutes, so a chosen time could be four minutes late. Four of eight self-scheduled wakeups waited more than four minutes, which makes the agent's own timing decisions unreadable afterwards.
2026-09-05 — the agent could not sell anything it had bought. Its own resting exits made every sell look like a position reversal to the broker, so the exits are now cancelled before the sell goes out.
2026-09-03 — the agent sets its own cadence, and can finally tell the time. It manages a book through the day instead of deciding once at the open and going quiet.
2026-09-03 — the agent chooses when its research arrives, and is told why each analysis exists. It had been deciding on information whose timing and origin it could neither control nor see.
2026-09-03 — the experiment's intent is written down. Give the agent proper tools inside reasonable restrictions and let it trade; where the two conflict the tie goes to the tool, because a restriction that exists only because nobody built the tool yet is a gap and not a rule.
2026-09-03 — the prompt no longer claims research arrives tomorrow. On the first trading day the agent commissioned AVGO at 13:35 and bought it an hour later, on an analysis it had been told would not exist until the morning.
2026-09-03 — the first trading day, and three defects it exposed. Five researches and a 29-share AVGO buy, all recorded correctly; the three faults had been live since the two-book removal and none of them touched the record.
2026-09-03 — the simulated-account check was too narrow and stopped the agent. It required a DEM prefix and the reset paper account came back as DEL546C9, so widening it to DE corrected a wrong observation rather than relaxing a guard.
2026-09-02 — the experiment starts. The container is deployed, the paper account is reset, and the agent is switched on with an empty book and nothing on its watchlist.
2026-09-02 — the watchlist cap goes from 12 to 30, on a measurement. The old number was derived rather than guessed, and both figures behind the derivation had stopped holding.
2026-09-02 — the agent can say what it needs, and is told what went wrong. It had been acting blind to its own refusals and broker failures, with no way to report a tool it was missing.
2026-09-01 — everything except the agent is removed. Both previous deployments stop and every manual control goes with them, because a person who can nudge the book puts a second decision-maker in the record and afterwards nothing can say which one produced a result. The tag v1-two-book-experiment marks the commit before the removal.
2026-09-01 — the agent's prompt and answer are recorded, and there is a page for them. Behaviour here is mostly prompt, so a run whose prompt was not kept cannot be analysed afterwards.
2026-09-01 — a negative balance no longer offers negative shares. Every signal line had been reading "With your $-8.00 cash you can afford -1 share(s)."
2026-09-01 — an empty or negative balance says so, instead of quoting itself as a spending limit. "The buys you place must cost $-8.00 or less in total" is not an instruction anybody can follow.
2026-09-01 — the watchlist section states what it costs, in dollars. One line, after five days of the agent never once mentioning the watchlist.