diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..3fd6afa2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,266 @@ +# Changelog + +All notable changes to TradingAgents are documented here. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +Breaking changes within the 0.x line are called out explicitly. + +## [0.2.4] — 2026-04-25 + +### Added + +- **Structured-output decision agents.** Research Manager, Trader, and Portfolio + Manager now use `llm.with_structured_output(Schema)` on their primary call + and return typed Pydantic instances. Each provider's native structured-output + mode is used (`json_schema` for OpenAI / xAI, `response_schema` for Gemini, + tool-use for Anthropic, function-calling for OpenAI-compatible providers). + Render helpers preserve the existing markdown shape so memory log, CLI + display, and saved reports keep working unchanged. (#434) +- **LangGraph checkpoint resume** — opt-in via `--checkpoint`. State is saved + after each node so crashed or interrupted runs resume from the last + successful step. Per-ticker SQLite databases under + `~/.tradingagents/cache/checkpoints/`. `--clear-checkpoints` resets them. (#594) +- **Persistent decision log** replacing the per-agent BM25 memory. Decisions + are stored automatically at the end of `propagate()`; the next same-ticker + run resolves prior pending entries with realised return, alpha vs SPY, and + a one-paragraph reflection. Override path with `TRADINGAGENTS_MEMORY_LOG_PATH`. + Optional `memory_log_max_entries` config caps resolved entries; pending + entries are never pruned. (#578, #563, #564, #579) +- **DeepSeek, Qwen (Alibaba DashScope), GLM (Zhipu), and Azure OpenAI** + providers, plus dynamic OpenRouter model selection. +- **Docker support** — multi-stage build with separate dev and runtime images. +- **`scripts/smoke_structured_output.py`** — diagnostic that exercises the + three structured-output agents against any provider so contributors can + verify their setup with one command. +- **5-tier rating scale** (Buy / Overweight / Hold / Underweight / Sell) used + consistently by Research Manager, Portfolio Manager, signal processor, and + the memory log; Trader keeps 3-tier (Buy / Hold / Sell) since transaction + direction is naturally ternary. +- **Pytest fixtures** — lazy LLM client imports plus placeholder API keys so + the test suite runs cleanly without credentials. (#588) + +### Changed + +- **`backend_url` default is now `None`** rather than the OpenAI URL. Each + provider client falls back to its native default. The previous default + leaked the OpenAI URL into non-OpenAI clients (e.g. Gemini), producing + malformed request URLs for Python users who switched providers without + overriding `backend_url`. The CLI flow is unaffected. +- All file I/O passes explicit `encoding="utf-8"` so Windows users no longer + hit `UnicodeEncodeError` with the cp1252 default. (#543, #550, #576) +- Cache and log directories moved to `~/.tradingagents/` to resolve Docker + permission issues. (#519) +- `SignalProcessor` reads the rating from the Portfolio Manager's rendered + markdown via a deterministic heuristic — no extra LLM call. +- OpenAI structured-output calls default to `method="function_calling"` to + avoid noisy `PydanticSerializationUnexpectedValue` warnings emitted by + langchain-openai's Responses-API parse path. Same typed result, no warnings. + +### Fixed + +- Empty memory no longer triggers fabricated past-lessons in agent prompts; + the memory-log redesign makes this structurally impossible since only the + Portfolio Manager consults memory and only when entries exist. (#572) +- Tool-call logging processes every chunk message, not just the last one, and + memory score normalization handles empty score arrays. (#534, #531) + +### Removed + +- `FinancialSituationMemory` (the per-agent BM25 system) and the dead + `reflect_and_remember()` plumbing; subsumed by the persistent decision log. +- Hardcoded Google endpoint that caused 404 when `langchain-google-genai` + changed its API path. (#493, #496) + +### Contributors + +Thanks to everyone who shaped this release through code, design, and reports: + +- [@claytonbrown](https://github.com/claytonbrown) — checkpoint resume (#594), test fixtures (#588), design feedback on cost tracking (#582) and structured validation (#583) +- [@Bcardo](https://github.com/Bcardo) — memory-log redesign (#579), empty-memory hallucination report (#572), encoding fix proposal (#570) +- [@voidborne-d](https://github.com/voidborne-d) — memory persistence design (#564), portfolio manager state fix (#503) +- [@mannubaveja007](https://github.com/mannubaveja007) — structured-output feature request (#434) +- [@kelder66](https://github.com/kelder66) — RAM-only memory issue (#563) +- [@Gujiassh](https://github.com/Gujiassh) — tool-call logging fix (#534), test stub PR (#533) +- [@iuyup](https://github.com/iuyup) — memory score normalization fix (#531) +- [@kaihg](https://github.com/kaihg) — Google base_url fix (#496) +- [@32ryh98yfe](https://github.com/32ryh98yfe) — Gemini 404 report (#493) +- [@uppb](https://github.com/uppb) — OpenRouter dynamic model selection (#482) +- [@guoz14](https://github.com/guoz14) — OpenRouter limited-model report (#337) +- [@samchenku](https://github.com/samchenku) — indicator name normalization (#490) +- [@JasonOA888](https://github.com/JasonOA888) — y_finance pandas import fix (#488) +- [@tiffanychum](https://github.com/tiffanychum) — stale import cleanup (#499) +- [@zaizou](https://github.com/zaizou) — Docker permission issue (#519) +- [@Stosman123](https://github.com/Stosman123), [@mauropuga](https://github.com/mauropuga), [@hotwind2015](https://github.com/hotwind2015) — Windows encoding bug reports (#543, #550, #576) +- [@nnishad](https://github.com/nnishad), [@atharvajoshi01](https://github.com/atharvajoshi01) — encoding fix proposals (#568, #549) + +## [0.2.3] — 2026-03-29 + +### Added + +- **Multi-language output** for analyst reports and final decisions, with a + CLI selector. Internal agent debate stays in English for reasoning quality. (#472) +- **GPT-5.4 family models** in the default catalog, with deep/quick model split. +- **Unified model catalog** as a single source of truth for CLI options and + provider validation. + +### Changed + +- `base_url` is forwarded to Google and Anthropic clients so corporate proxies + work consistently across providers. (#427) +- Standardised the Google `api_key` parameter to the unified `api_key` form. + +### Fixed + +- Backtesting fetchers no longer leak look-ahead data when `curr_date` is in + the middle of a fetched window. (#475) +- Invalid indicator names from the LLM are caught at the tool boundary instead + of crashing the run. (#429) +- yfinance news fetchers respect the same exponential-backoff retry as price + fetchers. (#445) + +### Contributors + +- [@ahmedk20](https://github.com/ahmedk20) — multi-language output (#472) +- [@CadeYu](https://github.com/CadeYu) — model catalog typing (#464) +- [@javierdejesusda](https://github.com/javierdejesusda) — unified Google API key parameter (#453) +- [@voidborne-d](https://github.com/voidborne-d) — yfinance news retry (#445) +- [@kostakost2](https://github.com/kostakost2) — look-ahead bias report (#475) +- [@lu-zhengda](https://github.com/lu-zhengda) — proxy/base_url support request (#427) +- [@VamsiKrishna2021](https://github.com/VamsiKrishna2021) — invalid indicator crash report (#429) + +## [0.2.2] — 2026-03-22 + +### Added + +- **Five-tier rating scale** (Buy / Overweight / Hold / Underweight / Sell) + introduced for the Portfolio Manager. +- **Anthropic effort level** support for Claude models. +- **OpenAI Responses API** path for native OpenAI models. + +### Changed + +- `risk_manager` renamed to `portfolio_manager` to match the role description + shown in the CLI display. +- Exchange-qualified tickers (e.g. `7203.T`, `BRK.B`) preserved across all + agent prompts and tool calls. +- Process-level UTF-8 default attempted for cross-platform consistency + (note: this approach did not actually take effect; replaced in v0.2.4 with + explicit per-call `encoding="utf-8"` arguments). + +### Fixed + +- yfinance rate-limit errors are retried with exponential backoff. (#426) +- HTTP client SSL customisation is supported for environments that need + custom certificate bundles. (#379) +- Report-section writes handle list-of-string content gracefully. + +### Contributors + +- [@CadeYu](https://github.com/CadeYu) — exchange-qualified ticker preservation (#413) +- [@yang1002378395-cmyk](https://github.com/yang1002378395-cmyk) — HTTP client SSL customisation (#379) + +## [0.2.1] — 2026-03-15 + +### Security + +- Patched `langchain-core` vulnerability (LangGrinch). (#335) +- Removed `chainlit` dependency affected by CVE-2026-22218. + +### Added + +- `pyproject.toml` build-system configuration; the project now installs via + modern packaging tooling. + +### Removed + +- `setup.py` — dependencies consolidated to `pyproject.toml`. + +### Fixed + +- Risk manager reads the correct fundamental report source. (#341) +- All `open()` calls receive an explicit UTF-8 encoding (initial pass). +- `get_indicators` tool handles comma-separated indicator names from the LLM. (#368) +- `Propagation` initialises every debate-state field so risk debaters never + see missing keys. +- Stock data parsing tolerates malformed CSVs and NaN values. +- Conditional debate logic respects the configured round count. (#361) + +### Contributors + +- [@RinZ27](https://github.com/RinZ27) — `langchain-core` security patch (#335) +- [@Ljx-007](https://github.com/Ljx-007) — risk manager fundamental-report fix (#341) +- [@makk9](https://github.com/makk9) — debate-rounds config issue (#361) + +## [0.2.0] — 2026-02-04 + +This is the largest release since the initial public version. The framework +moved from single-provider to a multi-provider architecture and grew several +production-ready surfaces. + +### Added + +- **Multi-provider LLM support** (OpenAI, Google, Anthropic, xAI, OpenRouter, + Ollama) via a factory pattern, with provider-specific thinking configurations. +- **Alpha Vantage** integration as a configurable primary data provider, with + yfinance as a community-stability fallback. +- **Footer statistics** in the CLI: real-time tracking of LLM calls, tool + calls, and token usage via LangChain callbacks. +- **Post-analysis report saving** — the framework writes per-section markdown + files (analyst reports, debate transcripts, final decision) when a run + completes. +- **Announcements panel** — fetches updates from `api.tauric.ai/v1/announcements` + for the CLI welcome screen. +- **Tool fallbacks** so a single vendor outage does not stop the pipeline. + +### Changed + +- Risky / Safe risk debaters renamed to **Aggressive / Conservative** for + consistency with the displayed agent labels. +- Default data vendor switched to balance reliability and quota across + community deployments. +- Ollama and OpenRouter model lists updated; default endpoints clarified. + +### Fixed + +- Analyst status tracking and message deduplication in the live display. +- Infinite-loop guard in the agent loop; reflection and logging hardened. +- Various data-vendor implementation bugs and tool-signature mismatches. + +### Contributors + +This release is the first with substantial outside contributions; many community +PRs from late 2025 also landed here. + +- [@luohy15](https://github.com/luohy15) — Alpha Vantage data-vendor integration (#235) +- [@EdwardoSunny](https://github.com/EdwardoSunny) — yfinance fetching optimisations (#245) +- [@Mirza-Samad-Ahmed-Baig](https://github.com/Mirza-Samad-Ahmed-Baig) — infinite-loop guard, reflection, and logging fixes (#89) +- [@ZeroAct](https://github.com/ZeroAct) — saved results path support (#29) +- [@Zhongyi-Lu](https://github.com/Zhongyi-Lu) — `.env` gitignore (#49) +- [@csoboy](https://github.com/csoboy) — local Ollama setup (#53) +- [@chauhang](https://github.com/chauhang) — initial Docker support attempt (#47, later reverted; the merged Docker support shipped in v0.2.4) + +## [0.1.1] — 2025-06-07 + +### Removed + +- Static site assets that had been bundled with v0.1.0; the public site now + lives separately. + +## [0.1.0] — 2025-06-05 + +### Added + +- **Initial public release** of the TradingAgents multi-agent trading + framework: market / sentiment / news / fundamentals analysts; bull and bear + researchers; trader; aggressive, conservative, and neutral risk debaters; + portfolio manager. LangGraph orchestration, yfinance data, per-agent + BM25 memory, single-provider OpenAI integration, interactive CLI. + +[0.2.4]: https://github.com/TauricResearch/TradingAgents/compare/v0.2.3...v0.2.4 +[0.2.3]: https://github.com/TauricResearch/TradingAgents/compare/v0.2.2...v0.2.3 +[0.2.2]: https://github.com/TauricResearch/TradingAgents/compare/v0.2.1...v0.2.2 +[0.2.1]: https://github.com/TauricResearch/TradingAgents/compare/v0.2.0...v0.2.1 +[0.2.0]: https://github.com/TauricResearch/TradingAgents/compare/v0.1.1...v0.2.0 +[0.1.1]: https://github.com/TauricResearch/TradingAgents/compare/v0.1.0...v0.1.1 +[0.1.0]: https://github.com/TauricResearch/TradingAgents/releases/tag/v0.1.0 diff --git a/README.md b/README.md index 6c8f644e..54af501a 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ # TradingAgents: Multi-Agents LLM Financial Trading Framework ## News +- [2026-04] **TradingAgents v0.2.4** released with structured-output agents (Research Manager, Trader, Portfolio Manager), LangGraph checkpoint resume, persistent decision log, DeepSeek/Qwen/GLM/Azure provider support, Docker, and a Windows UTF-8 encoding fix. See [CHANGELOG.md](CHANGELOG.md) for the full list. - [2026-03] **TradingAgents v0.2.3** released with multi-language support, GPT-5.4 family models, unified model catalog, backtesting date fidelity, and proxy support. - [2026-03] **TradingAgents v0.2.2** released with GPT-5.4/Gemini 3.1/Claude 4.6 model coverage, five-tier rating scale, OpenAI Responses API, Anthropic effort control, and cross-platform stability. - [2026-02] **TradingAgents v0.2.0** released with multi-provider LLM support (GPT-5.x, Gemini 3.x, Claude 4.x, Grok 4.x) and improved system architecture. @@ -251,6 +252,8 @@ _, decision = ta.propagate("NVDA", "2026-01-15") We welcome contributions from the community! Whether it's fixing a bug, improving documentation, or suggesting a new feature, your input helps make this project better. If you are interested in this line of research, please consider joining our open-source financial AI research community [Tauric Research](https://tauric.ai/). +Past contributions, including code, design feedback, and bug reports, are credited per release in [`CHANGELOG.md`](CHANGELOG.md). + ## Citation Please reference our work if you find *TradingAgents* provides you with some help :) diff --git a/pyproject.toml b/pyproject.toml index b569504e..07cbbd3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tradingagents" -version = "0.2.3" +version = "0.2.4" description = "TradingAgents: Multi-Agents LLM Financial Trading Framework" readme = "README.md" requires-python = ">=3.10" diff --git a/scripts/smoke_structured_output.py b/scripts/smoke_structured_output.py new file mode 100644 index 00000000..1d3cf681 --- /dev/null +++ b/scripts/smoke_structured_output.py @@ -0,0 +1,176 @@ +"""End-to-end smoke for structured-output agents against a real LLM provider. + +Runs the three decision-making agents (Research Manager, Trader, Portfolio +Manager) directly with their structured-output bindings and prints the +typed Pydantic instance + the rendered markdown for each. Use this to +verify a provider's native structured-output mode (json_schema for +OpenAI / xAI / DeepSeek / Qwen / GLM, response_schema for Gemini, tool-use +for Anthropic) returns clean instances on the schemas we ship. + +Usage: + OPENAI_API_KEY=... python scripts/smoke_structured_output.py openai + GOOGLE_API_KEY=... python scripts/smoke_structured_output.py google + ANTHROPIC_API_KEY=... python scripts/smoke_structured_output.py anthropic + DEEPSEEK_API_KEY=... python scripts/smoke_structured_output.py deepseek + +The script does NOT call propagate(), to keep the surface tight and the +cost low — it exercises only the three structured-output calls we just +added, plus the heuristic SignalProcessor. +""" + +from __future__ import annotations + +import argparse +import os +import sys + +from tradingagents.agents.managers.portfolio_manager import create_portfolio_manager +from tradingagents.agents.managers.research_manager import create_research_manager +from tradingagents.agents.trader.trader import create_trader +from tradingagents.graph.signal_processing import SignalProcessor +from tradingagents.llm_clients import create_llm_client + + +PROVIDER_DEFAULTS = { + "openai": ("gpt-5.4-mini", None), + "google": ("gemini-2.5-flash", None), + "anthropic": ("claude-sonnet-4-6", None), + "deepseek": ("deepseek-chat", None), + "qwen": ("qwen-plus", None), + "glm": ("glm-5", None), + "xai": ("grok-4", None), +} + + +# Minimal but realistic state for the three agents. +DEBATE_HISTORY = """ +Bull Analyst: NVDA's data-center revenue grew 60% YoY last quarter, driven by +Blackwell ramp; sovereign AI deals with multiple governments add a $40B+ +multi-year tailwind. Margins remain above peer average. + +Bear Analyst: Concentration risk is real — top three customers are >40% of +revenue. Any pause in hyperscaler capex would compress the multiple. China +export restrictions still cap a meaningful portion of demand. +""" + + +def _make_rm_state(): + return { + "company_of_interest": "NVDA", + "investment_debate_state": { + "history": DEBATE_HISTORY, + "bull_history": "Bull Analyst: NVDA's data-center revenue grew 60% YoY...", + "bear_history": "Bear Analyst: Concentration risk is real...", + "current_response": "", + "judge_decision": "", + "count": 1, + }, + } + + +def _make_trader_state(investment_plan: str): + return { + "company_of_interest": "NVDA", + "investment_plan": investment_plan, + } + + +def _make_pm_state(investment_plan: str, trader_plan: str): + return { + "company_of_interest": "NVDA", + "past_context": "", + "risk_debate_state": { + "history": "Aggressive: lean in. Conservative: trim. Neutral: balanced sizing.", + "aggressive_history": "Aggressive: ...", + "conservative_history": "Conservative: ...", + "neutral_history": "Neutral: ...", + "judge_decision": "", + "current_aggressive_response": "", + "current_conservative_response": "", + "current_neutral_response": "", + "count": 1, + }, + "market_report": "Market report.", + "sentiment_report": "Sentiment report.", + "news_report": "News report.", + "fundamentals_report": "Fundamentals report.", + "investment_plan": investment_plan, + "trader_investment_plan": trader_plan, + } + + +def _print_section(title: str, content: str) -> None: + bar = "=" * 70 + print(f"\n{bar}\n{title}\n{bar}\n{content}") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("provider", choices=list(PROVIDER_DEFAULTS.keys())) + parser.add_argument("--deep-model", default=None, help="Override deep_think_llm") + parser.add_argument("--quick-model", default=None, help="Override quick_think_llm") + args = parser.parse_args() + + default_model, _ = PROVIDER_DEFAULTS[args.provider] + deep_model = args.deep_model or default_model + quick_model = args.quick_model or default_model + + print(f"Provider: {args.provider}") + print(f"Deep model: {deep_model}") + print(f"Quick model: {quick_model}") + + # Build the LLM clients via the framework's factory. + deep_client = create_llm_client(provider=args.provider, model=deep_model) + quick_client = create_llm_client(provider=args.provider, model=quick_model) + deep_llm = deep_client.get_llm() + quick_llm = quick_client.get_llm() + + # 1) Research Manager + rm = create_research_manager(deep_llm) + rm_result = rm(_make_rm_state()) + investment_plan = rm_result["investment_plan"] + _print_section("[1] Research Manager — investment_plan", investment_plan) + + # 2) Trader (consumes RM's plan) + trader = create_trader(quick_llm) + trader_result = trader(_make_trader_state(investment_plan)) + trader_plan = trader_result["trader_investment_plan"] + _print_section("[2] Trader — trader_investment_plan", trader_plan) + + # 3) Portfolio Manager (consumes both) + pm = create_portfolio_manager(deep_llm) + pm_result = pm(_make_pm_state(investment_plan, trader_plan)) + final_decision = pm_result["final_trade_decision"] + _print_section("[3] Portfolio Manager — final_trade_decision", final_decision) + + # 4) SignalProcessor extracts the rating with zero LLM calls. + sp = SignalProcessor() + rating = sp.process_signal(final_decision) + _print_section("[4] SignalProcessor → rating", rating) + + # 5) Lightweight checks: each rendered output should carry the expected + # section headers so downstream consumers (memory log, CLI display, + # saved reports) keep working. + checks = [ + ("Research Manager", investment_plan, ["**Recommendation**:"]), + ("Trader", trader_plan, ["**Action**:", "FINAL TRANSACTION PROPOSAL:"]), + ("Portfolio Manager", final_decision, ["**Rating**:", "**Executive Summary**:", "**Investment Thesis**:"]), + ] + print("\n" + "=" * 70 + "\nStructure checks\n" + "=" * 70) + failures = 0 + for name, text, required in checks: + for marker in required: + ok = marker in text + print(f" {'PASS' if ok else 'FAIL'} {name}: contains {marker!r}") + failures += int(not ok) + + print() + if failures: + print(f"Smoke FAILED: {failures} structure check(s) missing.") + return 1 + print("Smoke PASSED: structured output → rendered markdown chain works for", args.provider) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_memory_log.py b/tests/test_memory_log.py index e0da15ef..5d7f7f84 100644 --- a/tests/test_memory_log.py +++ b/tests/test_memory_log.py @@ -291,6 +291,59 @@ class TestTradingMemoryLogCore: assert log.load_entries() == [] assert log.get_past_context("NVDA") == "" + # Rotation: opt-in cap on resolved entries + + def test_rotation_disabled_by_default(self, tmp_path): + """Without max_entries, all resolved entries are kept.""" + log = make_log(tmp_path) + for i in range(7): + _resolve_entry(log, "NVDA", f"2026-01-{i+1:02d}", DECISION_BUY, f"Lesson {i}.") + assert len(log.load_entries()) == 7 + + def test_rotation_prunes_oldest_resolved(self, tmp_path): + """When max_entries is set and exceeded, oldest resolved entries are pruned.""" + log = TradingMemoryLog({ + "memory_log_path": str(tmp_path / "trading_memory.md"), + "memory_log_max_entries": 3, + }) + # Resolve 5 entries; rotation should keep only the 3 most recent. + for i in range(5): + _resolve_entry(log, "NVDA", f"2026-01-{i+1:02d}", DECISION_BUY, f"Lesson {i}.") + entries = log.load_entries() + assert len(entries) == 3 + # Confirm the OLDEST were dropped, not the newest. + dates = [e["date"] for e in entries] + assert dates == ["2026-01-03", "2026-01-04", "2026-01-05"] + + def test_rotation_never_prunes_pending(self, tmp_path): + """Pending entries (unresolved) are kept regardless of the cap.""" + log = TradingMemoryLog({ + "memory_log_path": str(tmp_path / "trading_memory.md"), + "memory_log_max_entries": 2, + }) + # 3 resolved + 2 pending. With cap=2, only 2 resolved survive; both pending stay. + for i in range(3): + _resolve_entry(log, "NVDA", f"2026-01-{i+1:02d}", DECISION_BUY, f"Resolved {i}.") + log.store_decision("NVDA", "2026-02-01", DECISION_BUY) + log.store_decision("NVDA", "2026-02-02", DECISION_OVERWEIGHT) + # Trigger rotation by resolving one more entry — pending entries must stay. + _resolve_entry(log, "NVDA", "2026-01-04", DECISION_BUY, "Resolved 3.") + entries = log.load_entries() + pending = [e for e in entries if e["pending"]] + resolved = [e for e in entries if not e["pending"]] + assert len(pending) == 2, "pending entries must never be pruned" + assert len(resolved) == 2, f"expected 2 resolved after rotation, got {len(resolved)}" + + def test_rotation_under_cap_is_noop(self, tmp_path): + """No rotation when resolved count <= max_entries.""" + log = TradingMemoryLog({ + "memory_log_path": str(tmp_path / "trading_memory.md"), + "memory_log_max_entries": 10, + }) + for i in range(3): + _resolve_entry(log, "NVDA", f"2026-01-{i+1:02d}", DECISION_BUY, f"Lesson {i}.") + assert len(log.load_entries()) == 3 + # Rating parsing: markdown bold and numbered list formats def test_rating_parsed_from_bold_markdown(self, tmp_path): diff --git a/tradingagents/agents/utils/memory.py b/tradingagents/agents/utils/memory.py index fee5ac4a..c9471755 100644 --- a/tradingagents/agents/utils/memory.py +++ b/tradingagents/agents/utils/memory.py @@ -17,11 +17,14 @@ class TradingMemoryLog: _REFLECTION_RE = re.compile(r"REFLECTION:\n(.*?)$", re.DOTALL) def __init__(self, config: dict = None): + cfg = config or {} self._log_path = None - path = (config or {}).get("memory_log_path") + path = cfg.get("memory_log_path") if path: self._log_path = Path(path).expanduser() self._log_path.parent.mkdir(parents=True, exist_ok=True) + # Optional cap on resolved entries. None disables rotation. + self._max_entries = cfg.get("memory_log_max_entries") # --- Write path (Phase A) --- @@ -153,6 +156,7 @@ class TradingMemoryLog: if not updated: return + new_blocks = self._apply_rotation(new_blocks) new_text = self._SEPARATOR.join(new_blocks) tmp_path = self._log_path.with_suffix(".tmp") tmp_path.write_text(new_text, encoding="utf-8") @@ -206,6 +210,7 @@ class TradingMemoryLog: if not matched: new_blocks.append(block) + new_blocks = self._apply_rotation(new_blocks) new_text = self._SEPARATOR.join(new_blocks) tmp_path = self._log_path.with_suffix(".tmp") tmp_path.write_text(new_text, encoding="utf-8") @@ -213,6 +218,43 @@ class TradingMemoryLog: # --- Helpers --- + def _apply_rotation(self, blocks: List[str]) -> List[str]: + """Drop oldest resolved blocks when their count exceeds max_entries. + + Pending blocks are always kept (they represent unprocessed work). + Returns ``blocks`` unchanged when rotation is disabled or under cap. + """ + if not self._max_entries or self._max_entries <= 0: + return blocks + + # Tag each block with (kept, is_resolved) by parsing tag-line markers. + decisions = [] + for block in blocks: + stripped = block.strip() + if not stripped: + decisions.append((block, False)) + continue + tag_line = stripped.splitlines()[0].strip() + is_resolved = ( + tag_line.startswith("[") + and tag_line.endswith("]") + and not tag_line.endswith("| pending]") + ) + decisions.append((block, is_resolved)) + + resolved_count = sum(1 for _, r in decisions if r) + if resolved_count <= self._max_entries: + return blocks + + to_drop = resolved_count - self._max_entries + kept: List[str] = [] + for block, is_resolved in decisions: + if is_resolved and to_drop > 0: + to_drop -= 1 + continue + kept.append(block) + return kept + def _parse_entry(self, raw: str) -> Optional[dict]: lines = raw.strip().splitlines() if not lines: diff --git a/tradingagents/default_config.py b/tradingagents/default_config.py index 7498d188..fa6d5742 100644 --- a/tradingagents/default_config.py +++ b/tradingagents/default_config.py @@ -7,6 +7,10 @@ DEFAULT_CONFIG = { "results_dir": os.getenv("TRADINGAGENTS_RESULTS_DIR", os.path.join(_TRADINGAGENTS_HOME, "logs")), "data_cache_dir": os.getenv("TRADINGAGENTS_CACHE_DIR", os.path.join(_TRADINGAGENTS_HOME, "cache")), "memory_log_path": os.getenv("TRADINGAGENTS_MEMORY_LOG_PATH", os.path.join(_TRADINGAGENTS_HOME, "memory", "trading_memory.md")), + # Optional cap on the number of resolved memory log entries. When set, + # the oldest resolved entries are pruned once this limit is exceeded. + # Pending entries are never pruned. None disables rotation entirely. + "memory_log_max_entries": None, # LLM settings "llm_provider": "openai", "deep_think_llm": "gpt-5.4", diff --git a/tradingagents/llm_clients/openai_client.py b/tradingagents/llm_clients/openai_client.py index f943124a..bbfcd39e 100644 --- a/tradingagents/llm_clients/openai_client.py +++ b/tradingagents/llm_clients/openai_client.py @@ -18,6 +18,22 @@ class NormalizedChatOpenAI(ChatOpenAI): def invoke(self, input, config=None, **kwargs): return normalize_content(super().invoke(input, config, **kwargs)) + def with_structured_output(self, schema, *, method=None, **kwargs): + """Wrap with structured output, defaulting to function_calling for OpenAI. + + langchain-openai's Responses-API-parse path (the default for json_schema + when use_responses_api=True) calls response.model_dump(...) on the OpenAI + SDK's union-typed parsed response, which makes Pydantic emit ~20 + PydanticSerializationUnexpectedValue warnings per call. The function-calling + path returns a plain tool-call shape that does not trigger that + serialization, so it is the cleaner choice for our combination of + use_responses_api=True + with_structured_output. Both paths use OpenAI's + strict mode and produce the same typed Pydantic instance. + """ + if method is None: + method = "function_calling" + return super().with_structured_output(schema, method=method, **kwargs) + # Kwargs forwarded from user config to ChatOpenAI _PASSTHROUGH_KWARGS = ( "timeout", "max_retries", "reasoning_effort",