- the fundamentals brief reached the model as a Python tuple
- analysts no longer emit a trade call that nothing reads
- a report that was not produced says so instead of appearing as a blank section
- holding_period_days sets it; the price request covers the calendar span those trading days occupy
- reflection states the window it judges, so a short one does not read as a failed thesis
- a Sell that fell counted as a miss and a Sell that rose as a hit
- Hold claims no direction, so it reports mean alpha and no hit rate
- the report names the window the outcomes were measured over
- a non-blank message is text, whatever it would mean as a Python literal
- the live layout renders on the alternate screen, so a tall layout does not scroll
- a run with no readable rating says so instead of looking like a normal result
- the state log keeps non-ASCII readable (#1081)
- the field list lived only in the schema, which a provider without structured output never sees
- the trader maps the five-tier recommendation onto its three actions
- the labelled rating decides, whatever dash separates it, and a scale the model echoed is not one
- prose naming several ratings is reviewed rather than read as the first word in the text
- an unreadable decision is tagged REVIEW everywhere instead of a tradeable Hold
- unrated decisions are counted apart from the backtest figures
- statements are limited to facts filed by the run's date, at the values filed then
- quarterly figures are picked by span, so a year to date total is not read as a quarter
- a fourth quarter is reported unavailable rather than derived
- opt in with sec_edgar in the fundamental_data chain; a non-filer falls through
- settling a past decision is per entry; a provider error leaves it pending
- an explicitly configured benchmark ticker is normalized like any other symbol (#1075)
- analysts, depth, provider, models and language prefill; prompts still shown
- values no longer offered by the current catalog are dropped
- environment variables keep skipping their step
- analysis stays the bare invocation; backtest runs a ticker and date grid
- selections name the models as the config does
- the pending note prints only when a cell is pending
- run_backtest runs the pipeline per cell into its own decision log and resumes by skipping logged cells
- summarize scores settled cells by rating on realized alpha
- settle_pending settles a ticker whose last decision would otherwise stay open
- PortfolioContext input, rendered once into state and read by the trader, risk and portfolio agents
- --portfolio JSON file on the CLI
- a run without it is never treated as a flat book
- the checkpoint signature keys on the portfolio
- the key prompt created .env with the default umask, typically readable by
other local users
- create it 0600 and tighten an existing file before writing the key; a
read-only file is still updated
- FRED and Alpha Vantage authenticate with a query parameter, and requests
quotes the full URL in HTTP, connection and timeout errors, so the key was
written into any log or traceback that recorded one
- route both vendors through one request helper that re-raises a requests error
as the same class with the key removed, carrying no request, response or
exception chain, each of which still held the URL #1324
- anonymous RSS allows about one request per minute per IP, so fetching each
subreddit separately hit a 429 and a 60s back-off on nearly every run
- search the subreddits as one combined feed at Reddit's full page size and
group posts by the subreddit each entry names; each subreddit keeps its own
limit, and a full page is not taken as evidence of absence
- drop the unused JSON search path, still blocked with a 403, and the
per-subreddit pacing; arguments after subreddits are keyword-only
- the sentiment prompt no longer asks for vote and comment counts, which the
RSS feed does not carry
- Yahoo news and the Reddit and StockTwits feeds serve only recent items, so a
historical window trimmed to nothing was reported as "no news" or "no
posts", and the sentiment analyst scored that silence as a neutral signal
- judge each empty window in one shared rule: it is a real absence only when
the feed's coverage reaches the window's first day and the window ends by
today; otherwise report it unavailable with where coverage starts
- coverage comes from the returned timestamps, which are newest-first on these
feeds, plus Reddit's one-week search lookback; merged global-news searches
prove no continuity and are bounded by the present alone
- state in the sentiment analyst that historical sentiment inputs are not
guaranteed to be point-in-time
- OpenAI: gpt-6-astra
- Google: gemini-3.8-flash and gemini-3.5-flash-lite
- Anthropic: claude-opus-5 and claude-fable-5-1
- models taken out of the picker stay valid through LEGACY_MODELS, so configs
that name them run without an unknown-model warning
- reasoning_effort was forwarded only to IDs matching gpt-5 or the o-series, so
GPT-6 models silently dropped the configured effort; match GPT-5 and later,
with a version boundary so unrelated IDs do not match
- Gemini Pro, 3.8+ and the -latest aliases reject thinking_level "minimal"
with a 400; send it only to numbered Flash models before 3.8 and map it to
"low" elsewhere, since aliases move between generations
- both manager prompts and both rating fields listed "materially conflicting"
as a Hold condition, and every bull/bear and risk debate is conflicting, so
a modest directional edge collapsed to Hold
- state one rule at all four sites: conflict alone is not a reason to Hold,
commit to the stronger side sized by how decisively it wins, and Hold only
when the evidence is still balanced after weighing or too thin for a call
- a modest bull edge now resolves to Overweight; a genuinely balanced debate
still resolves to Hold #1321
- keep what the code cannot state itself: which session a closeless bar is,
where the drop actually happens, and why the trim must stay unguarded
- drop the field-by-field enumeration, the account of what the previous
behaviour got wrong, and the restatements of adjacent calls
- list kimi-k3 (flagship, 1M context) and kimi-k2.6 (256K, thinking modes) on
both tiers, replacing the custom-only entry
- keep Custom model ID for models newer than the list
- omit the k2.7-code variants: coding specialists, not analysis models
- a newest bar with no close made load_ohlcv reject the whole frame, so the
routing layer answered with its no-data sentinel: the caller lost the entire
price history and was told the symbol may be invalid, delisted or not
covered, when only the latest session had not settled
- treat a closeless newest bar as an unsettled session instead. The gap fill
already drops it, here and mid-series alike, so the frame ends at the last
settled bar; only a range with no close anywhere is still no data
- the staleness check keeps deciding whether what remains is recent enough, so
falling back cannot resurrect a long-dead series
- log which bars had no close and which date is being used as the latest close
- the console highlights numbers and URLs, so with colour enabled the rendered
output splits asserted substrings with escape codes and two tests fail
- strip the codes before asserting so the result no longer depends on where the
suite runs
- four modules wrote to stdout with print() while ten others use a module
logger; a warning printed into the rendered CLI output is effectively
invisible, which is how the trim failure above went unnoticed
- convert the remaining calls to logger.warning with lazy formatting
- remove save_output, SavePathType, decorate_all_methods and get_next_weekday
from utils, none of which had a caller, along with the pandas and typing
imports that only they needed
- get_stock requests the full daily series up to today, so trimming to the
requested window is the only thing keeping bars after end_date out of a
historical run
- the trim caught every exception, warned, and returned the untrimmed body, so
a parse failure fed future prices into a backtest with no usable signal that
it had happened
- let a parse failure propagate instead: the routing layer already logs the
vendor failure, falls through to the next vendor, and surfaces the real error
if none can serve
- ruff's extend-exclude listed a path that is not part of the repository, so
the entry never matched anything in CI
- keep the generated results/ exclude, which is a real runtime output directory
- create_sentiment_analyst pre-fetches news, StockTwits and Reddit before
prompting, and TestSentimentAnalystAgent invoked it unstubbed, so every run
made live requests and a real 429 stalled the suite for minutes
- stub the three sources as the sibling prompt tests already do; the file drops
from ~84s to under a second, and the suite no longer depends on Reddit or
Yahoo being reachable
- a failed fetch and an empty search both returned [], so a 429 rendered as
'no posts found' and the sentiment analyst read throttling as real silence;
when every subreddit was throttled the summary asserted it outright
- a failed fetch now returns None and renders as unavailable, and the summary
only claims silence for subreddits actually searched
- raise the headerless-429 back-off to 60s, which is where a retry starts
succeeding; pay it at most once per run so three throttled subreddits do not
stall the analysis, and match the Retry-After cap to it #1295
- asking the Trader for concrete entry/stop levels invited a percentage
(stop_loss: '15%'), which is not a price and failed the whole structured
parse, dropping the run to a free-text retry
- state the requirement in the prompt and in both field descriptions
- a percentage now nulls that field instead of failing the proposal; it is
never salvaged, since 15% must not become a 15 stop. Human-formatted
prices with a currency symbol or thousands separator parse #1288
- both fundamentals vendors accepted curr_date and ignored it, serving a
present-day company profile into a run dated in the past: yfinance via
Ticker.info, Alpha Vantage via OVERVIEW
- that profile has no historical vintage, not even name/sector/industry (which
move when a company renames or is reclassified), so a past curr_date now
withholds it and says why; live runs are unchanged
- the rule lives once in date_window next to the existing look-ahead helpers,
so switching data_vendors between the two cannot reintroduce the leak, and
the guard runs before the request rather than discarding a paid-for response
- point-in-time fundamentals for a past date already come from the balance
sheet, income statement and cash flow tools, which filter on curr_date #1300
- ElementTree does not resolve external entities, so the reported XXE flag
doesn't apply; the real residual is an unbounded read of untrusted network XML
- cap both the RSS and JSON reads at 5 MiB; overflow degrades to empty / RSS
fallback through the existing failure paths #1206#1276
- the managers reserved Hold only for "genuinely balanced" evidence and were
told to "be decisive", pressuring a directional call on ambiguous, conflicting,
or insufficient inputs; which side it landed on was model-prior-dependent
- allow Hold for balanced, conflicting, ambiguous, or insufficient evidence in
both manager prompts and both structured rating fields, and weigh cases
independent of speaking order; rating definitions and debate ordering unchanged #1196
- a valid Retry-After: 0 means retry at once but was treated as absent
(`or 5.0`) and waited 5s; honour it exactly now
- jitter our own headerless fallback and the inter-subreddit pacing so several
analyses sharing an IP don't retry in lockstep and re-collide on the limit;
keep the single-retry ceiling (more retries can't fix an exhausted IP budget) #1193
- the unconditional realtime pin 400s when curr_date is ahead of FRED's
US-Central date (a live run's local date), which the router then degrades to
a silent DATA_UNAVAILABLE — an Asia/Pacific run loses macro data
- clamp realtime_start/end to min(curr_date, FRED-today) via pytz Chicago;
a past curr_date pins unchanged, so historical look-ahead safety is preserved
- name the vintage in the empty-result message: widening the window can't fix a
series with no vintage coverage #1275
- GPT-5.6 (sol/terra/luna) is GA and OpenAI's recommended default; add it and
make gpt-5.6 (deep) / gpt-5.6-luna (quick) the defaults
- add GLM-5.3 and GLM-5.3-Flash, Zhipu's current flagship line
- the Trader received only the digested investment plan, so its entry / stop /
sizing levels were not anchored to real price structure (ATR, support and
resistance, current price)
- inject the market report and instruct the Trader to take concrete price levels
from it and direction/strategy from the plan; when the market analyst was not
selected the report is empty, so the section and grounding note are omitted #1167
- OpenRouter exposes DeepSeek as deepseek/<id>, which matched neither the exact
IDs nor the patterns, so a thinking model like deepseek/deepseek-v4-flash fell
through to _DEFAULT and had object-form tool_choice forced on it
- strip the official deepseek/ namespace before lookup so it reuses the native
quirks; deepseek/deepseek-chat still keeps tool_choice, and third-party
finetunes on other publishers stay on _DEFAULT #1199
- the latest in-range bar with a NaN close was dropped before the curr_date
cutoff, so the previous trading day looked like the latest; dates were also
compared without timezone normalization
- normalize bar dates and curr_date to naive midnight (per element, so 5-year
ranges spanning DST and non-US positive-offset markets keep their local date),
then raise NoMarketDataError on a missing latest close rather than falling back
- split the fill step (_fill_price_gaps) from date/price normalization so the
latest bar can be inspected before incomplete rows are dropped #1201
- _fetch_returns settled on min(holding_days, available), so a rerun a day or
two after a decision reflected on a 1-2 day partial return as if final
- require the full holding window in both the stock and benchmark series before
resolving; otherwise leave the entry pending to retry next run
- this also makes the #1251 resolution date the full-window date, not a partial
bar's #1169
- inline _in_news_window, a trivial passthrough left from extracting
dataflows.date_window.in_window; call in_window directly
- drop SignalProcessor's orphaned quick_thinking_llm attribute (unused since
rating extraction became a deterministic parse)
- on resume, the CLI and propagate re-passed the initial state to a thread with
an existing checkpoint; nodes do not re-run, but the message reducer appended
the initial messages again, duplicating them in the resumed state
- feed None on resume (checkpoint_input) so LangGraph continues the interrupted
run, and wrap the CLI stream in try/finally so the checkpointer tears down even
if the stream raises
- correct the _fetch_returns docstring to the 4-tuple return #1249
- get_past_context returned every resolved lesson regardless of the run date, so
a historical run could learn from an outcome that had not happened yet
- record each resolved entry's resolution date (the last price bar used) and
filter get_past_context(as_of=trade_date) on it for a historical run; a
current-date run passes None so live behavior and pre-migration entries (no
stored resolution date, conservatively excluded from backtests) are unaffected #1251
- checkpoint setup lived only inside propagate(); the CLI streamed the
checkpointer-less graph with no thread_id, so --checkpoint neither saved nor
resumed a run
- extract the lifecycle into reusable begin_checkpoint / end_checkpoint /
clear_checkpoint_on_success (checkpoint_scope wraps them for propagate) and use
them around the CLI stream #1249
- an unrecognizable Portfolio Manager decision was coerced to Hold, emitting a
tradeable neutral signal that masked a parsing failure; a fullwidth colon
(Rating:X) defeated the label regex and hit the same path
- add extract_rating() -> str | None with NFKC normalization and whole-word
matching; the graph signal now yields a REVIEW sentinel (with an is_review
guard) when no rating is found
- parse_rating keeps its silent default for compat callers (e.g. the memory log) #1170
- some model/gateway combinations emit unbounded reasoning/output and hang or
trip an idle timeout (e.g. some deepseek-v4-flash deployments)
- add an opt-in max_tokens config knob + TRADINGAGENTS_MAX_TOKENS, forwarded to
every provider when set (Gemini takes it as max_output_tokens); int-coerced,
rejects non-positive/boolean values #1204
- the first speaker in each debate round received an empty opponent response,
yet the prompt demanded a rebuttal, so models fabricated the other side
- substitute an explicit opening marker when an opponent has not spoken, across
all five debators (bull, bear, and the three risk analysts) #1176
- StockTwits and Reddit were fetched with no date, so a historical run showed
today's chatter as if it were from the as-of date
- pass the analysis window to both fetchers, filter to it, and emit a clear
placeholder when nothing qualifies
- centralize the UTC half-open window rule in dataflows/date_window so news,
StockTwits, and Reddit share one look-ahead-safe filter #1220
- FRED defaults both realtime bounds to today, so historical macro requests
served the latest revision and leaked future information into backtests
- set realtime_start=realtime_end=curr_date on both the metadata and
observations requests #1275
- with_structured_output binds a single tool (the schema), so a primed model emitted
an unknown web_search call and the attempt was discarded for a free-text retry,
costing an extra round trip and the typed output
- drop the tool-range wording from the no-tool sentiment analyst and state the
constraint once via a shared NO_EXTERNAL_TOOLS #1130
- Windows terminals without a console buffer raised NoConsoleScreenBufferError
before the first prompt, surfacing a raw traceback with no guidance
- gate the Windows-only import on sys.platform so a broken prompt_toolkit still
surfaces there, and the handler stays inert on other platforms #1138
- the per-day cache was reused unconditionally, so a run started before the day's
bar was final served that snapshot to every later run, feeding a stale close
into technical analysis
- a present row is not sufficient either, since Yahoo publishes a partial intraday
candle; a TTL now governs every current-day cache while historical caches stay
immutable #1150
- the upper bound was inclusive, so an article stamped exactly midnight after
end_date leaked into a historical run
- flat epoch timestamps were parsed in host-local time and offset-aware stamps had
tzinfo stripped without converting, making filtering machine-dependent
- normalize every operand to UTC and use a half-open [start, end + 1 day) #1126
- refresh the Anthropic lineup to the current GA set (Fable 5, Opus 4.8,
Sonnet 5, Opus 4.7, Haiku 4.5)
- extend the effort gate to single-number Claude 5 IDs (claude-sonnet-5,
claude-fable-5) so their effort setting is honored
- pass the token to ChatBedrockConverse as api_key so langchain-aws prefers bearer
auth and an ambient AWS_PROFILE can't override it; no AWS access keys required #1103
- crypto reached StockTwits as Yahoo's BTC-USD (404) instead of BTC.X, and Reddit
searched the dashed pair that barely matches; both now resolve the base via a
shared crypto_base() helper, restoring crypto sentiment
- also fixes a StockTwits resilience test class that pytest never collected #1113
- checkpoint resume keyed only by ticker+date silently continued the old graph
under a different analyst selection / depth / asset mode; fold a run signature
into the thread id #1089
- add llm_max_retries + TRADINGAGENTS_LLM_MAX_RETRIES, forwarded to every provider
when set (int-coerced, rejects negatives/booleans), so a 429 burst can't abort
a run #1091
- should_continue_debate (2 edges) and should_continue_risk_analysis (3 edges)
each returned more targets than any one edge mapped; a fall-through under
prompt/i18n/refactor drift crashed LangGraph mid-run
- share a complete DEBATE_PATH_MAP / RISK_ANALYSIS_PATH_MAP across every edge #1088
- prompt advertised get_news(query, ...) but the tool takes a ticker, so the
model hallucinated free-text query calls
- advertise get_news(ticker, start_date, end_date) #1116
- the payload is a JSON string, so the dict-only guard skipped filtering and
future-dated reports leaked into historical runs, breaking the #475 guarantee
- parse before filtering; non-JSON bodies and an unset curr_date pass through #1115
- CI gate, unified verified data-access contract, provider and data-vendor registry
- env-over-CLI config precedence, current-generation model catalog
- programmatic report output, plus sweep fixes for data and structured output
The README reproducibility example named gpt-4.1 and the structured-output smoke
script listed gemini-2.5-flash / deepseek-chat / qwen-plus / grok-4 — all retired
from the catalog. Generalize the note and refresh the smoke defaults.
The date hint sat at the end of each analyst's system prompt, after a long
indicator block, so weaker models anchored to their training cutoff when
generating tool-call date ranges. Lead each prompt with it instead.
The per-section markdown report tree was written only by the CLI, so programmatic
(TradingAgentsGraph) runs produced no saved reports.
- Extract the writer into tradingagents/reporting.write_report_tree.
- The CLI's save_report_to_disk delegates to it (no behavior change).
- Add TradingAgentsGraph.save_reports(final_state, ticker) so headless/API callers
get the same report tree, defaulting under results_dir.
The committed lockfile is not consumed by the pip-based install or CI; it only
drifts. A deliberate dependency upgrade, if wanted, is its own scoped PR.
The knob was accepted but inert — analysts run strictly sequentially and the
value was never used. Remove it rather than ship a misleading config key.
Parallel analyst execution is tracked for v0.3 (#634/#671/#487).
A weak model can write a placeholder ('None', 'N/A') into an optional price
field, tripping schema validation. Coerce null-ish strings to None on the
trader/PM float fields; real numeric strings still parse.
Nodes after the trader do not append to messages, so the debug stream reprinted
the same trailing message once per node. Print it only when it changes; the
returned state is unchanged.
- Local servers (LM Studio, vLLM) reject the object-form tool_choice langchain
sends for function calling. The generic openai_compatible provider now binds
the schema as a tool without forcing tool_choice.
- A structured call can return no parsed result (a thinking model answering in
plain text); fall back to free text with a clear reason instead of an opaque
render error.
The yfinance news fetch queried the raw ticker while every other path uses the
canonical symbol, so broker/forex/crypto aliases silently returned no news.
Normalize it (XAUUSD -> GC=F) and keep the user's ticker in the report header.
Optional enrichment vendors (FRED macro, Polymarket events) raised on a bad LLM
indicator, a missing key, or a network blip, which aborted the whole run.
- Router: mark macro_data and prediction_markets optional; a sole-vendor failure
returns a sentinel instead of re-raising. Core categories still raise.
- FRED: reject a descriptive phrase up front and return guidance instead of
400ing the API; an unknown series returns a not-found message, not a crash.
Trim each provider to current-generation models and drop the special-casing
they required:
- OpenAI: remove gpt-4.1 (deprecated; the only non-reasoning model).
- Anthropic: remove Claude Sonnet 4.5 (legacy; the only Sonnet that 400s on effort).
- Google: remove the Gemini 2.5 line (superseded by 3.x).
- Gemini client: drop the integer thinking_budget mapping; 3.x takes the string
thinking_level directly.
Effort/reasoning gates stay as defense in depth for custom model IDs. All kept
IDs verified against live APIs.
Interactive selections and flag defaults overrode TRADINGAGENTS_* env vars.
Rule: an explicit env value or CLI flag wins; otherwise the env-applied
default is kept.
- Research depth: skip the prompt when both round-count env vars are set, and
stop overwriting them (#977).
- Checkpoint: --checkpoint/--no-checkpoint is tri-state; omitting it keeps
TRADINGAGENTS_CHECKPOINT_ENABLED (#976).
- Docker ollama: use TRADINGAGENTS_LLM_PROVIDER + OLLAMA_BASE_URL, not a bare
LLM_PROVIDER the overlay never reads (#975).
- Reasoning/thinking knobs: settable via env; the prompt is skipped when set.
- Effort gating: forward effort only to models that accept it (Anthropic
Opus 4.5+/Sonnet 4.6+, OpenAI reasoning models); drop it elsewhere.
- Boolean env values: raise a named error on invalid input instead of
silently becoming False.
Label each OpenRouter model prompt by mode (quick/deep) like the other
providers, so the two consecutive selections are distinguishable. Populate the
dropdown with the newest models from mainstream chat providers rather than the
universal-newest (which surfaced niche/experimental releases); Custom ID still
reaches anything. Cancelled required prompts now exit cleanly instead of
crashing, and the output-language prompt falls back to English.
Add quality guidance to the narrative field so the sentiment report stays
informative and substantive, with each point adding new signal for the trader.
Verified each provider's hard-coded list against current official docs:
- MiniMax: add MiniMax-M3 (1M ctx, multimodal) as the default; keep M2.7 line.
- Qwen: use the live qwen{3.7,3.6}-{plus,max} IDs.
- GLM: add glm-5.2 as the latest flagship.
- xAI: drop deprecated grok-4-fast-* / grok-4-0709 builds.
- DeepSeek: migrate to deepseek-v4-pro / deepseek-v4-flash (the chat/reasoner
aliases are deprecated 2026-07-24 and now map to V4 Flash).
OpenAI, Anthropic, and Gemini were already current and are unchanged.
With the tree clean, the lint job runs ruff check . on every push and PR rather
than only the files a PR changes, so a lint regression is caught anywhere.