docs: call it the memory log throughout

- docstrings, comments, messages and the README section use "memory log", matching TradingMemoryLog and memory_log_path
- the write, read and settle paths are named for what they do, not by design phase
This commit is contained in:
Yijia-Xiao
2026-09-24 19:38:41 +00:00
parent c50420fc5c
commit 5ac5786d0d
9 changed files with 25 additions and 23 deletions
+4 -4
View File
@@ -304,9 +304,9 @@ An empty `positions` list means a flat book, which is different from passing not
TradingAgents persists two kinds of state across runs. TradingAgents persists two kinds of state across runs.
### Decision log ### Memory log
The decision log is always on. Each completed run appends its decision to `~/.tradingagents/memory/trading_memory.md`. On the next run for the same ticker, TradingAgents fetches the realised return (raw, and alpha against the instrument's regional benchmark), generates a one-paragraph reflection, and injects the most recent same-ticker decisions plus recent cross-ticker lessons into the Portfolio Manager prompt, so each analysis carries forward what worked and what didn't. The memory log is always on. Each completed run appends its decision to `~/.tradingagents/memory/trading_memory.md`. On the next run for the same ticker, TradingAgents fetches the realised return (raw, and alpha against the instrument's regional benchmark), generates a one-paragraph reflection, and injects the most recent same-ticker decisions plus recent cross-ticker lessons into the Portfolio Manager prompt, so each analysis carries forward what worked and what didn't.
Override the path with `TRADINGAGENTS_MEMORY_LOG_PATH`. Override the path with `TRADINGAGENTS_MEMORY_LOG_PATH`.
@@ -330,7 +330,7 @@ _, decision = ta.propagate("NVDA", "2026-09-01")
## Evaluating decisions over time ## Evaluating decisions over time
One run gives one decision, which cannot tell you whether the system decides well. `run_backtest` runs the same pipeline over a grid of tickers and dates, writes to a decision log of its own, and scores the decisions whose holding window has since traded. One run gives one decision, which cannot tell you whether the system decides well. `run_backtest` runs the same pipeline over a grid of tickers and dates, writes to a memory log of its own, and scores the decisions whose holding window has since traded.
```python ```python
from tradingagents.backtest import iter_grid, run_backtest, summarize from tradingagents.backtest import iter_grid, run_backtest, summarize
@@ -346,7 +346,7 @@ From the CLI:
tradingagents backtest NVDA,AAPL --start 2026-06-01 --end 2026-08-01 --every 7 tradingagents backtest NVDA,AAPL --start 2026-06-01 --end 2026-08-01 --every 7
``` ```
Each cell is scored on realized alpha against the instrument's regional benchmark, grouped by rating. Your own decision log is never written to, and re-running the same grid with `run_id=result.run_id` skips the cells that already ran, so an interrupted sweep continues where it stopped. Each cell is scored on realized alpha against the instrument's regional benchmark, grouped by rating. Your own memory log is never written to, and re-running the same grid with `run_id=result.run_id` skips the cells that already ran, so an interrupted sweep continues where it stopped.
## Reproducibility ## Reproducibility
+1 -1
View File
@@ -199,7 +199,7 @@ def run_analysis(checkpoint: bool | None = None, portfolio=None):
) )
update_display(layout, spinner_text, stats_handler=stats_handler, start_time=start_time) update_display(layout, spinner_text, stats_handler=stats_handler, start_time=start_time)
# The same initial state propagate() builds: settled decision log, past # The same initial state propagate() builds: settled memory log, past
# context and resolved instrument identity. # context and resolved instrument identity.
init_agent_state = graph.create_run_state( init_agent_state = graph.create_run_state(
selections["ticker"], selections["analysis_date"], selections["asset_type"], portfolio selections["ticker"], selections["analysis_date"], selections["asset_type"], portfolio
+2 -2
View File
@@ -1,4 +1,4 @@
"""Backtesting: many single-shot decisions, scored by the decision log. """Backtesting: many single-shot decisions, scored by the memory log.
A run already records its rating and later settles it with realized and alpha A run already records its rating and later settles it with realized and alpha
return against the regional benchmark. A backtest is that machinery over a grid return against the regional benchmark. A backtest is that machinery over a grid
@@ -76,7 +76,7 @@ def _config(tmp_path):
@pytest.mark.unit @pytest.mark.unit
def test_the_live_decision_log_is_never_written(tmp_path): def test_the_live_memory_log_is_never_written(tmp_path):
config = _config(tmp_path) config = _config(tmp_path)
result = run_backtest(["NVDA"], ["2026-01-05", "2026-01-12"], config) result = run_backtest(["NVDA"], ["2026-01-05", "2026-01-12"], config)
@@ -1,4 +1,4 @@
"""The CLI must use the decision log the same way propagate() does. """The CLI must use the memory log the same way propagate() does.
The CLI streams the graph itself instead of calling propagate(), so memory steps The CLI streams the graph itself instead of calling propagate(), so memory steps
that lived only in propagate() never ran on the primary entry point: pending that lived only in propagate() never ran on the primary entry point: pending
@@ -160,7 +160,7 @@ def _run_cli(monkeypatch, tmp_path, fake):
@pytest.mark.unit @pytest.mark.unit
def test_cli_run_uses_the_decision_log_like_propagate(tmp_path, monkeypatch): def test_cli_run_uses_the_memory_log_like_propagate(tmp_path, monkeypatch):
fake = _FakeGraph() fake = _FakeGraph()
_run_cli(monkeypatch, tmp_path, fake) _run_cli(monkeypatch, tmp_path, fake)
+4 -4
View File
@@ -2,7 +2,7 @@
One run yields one decision, so it cannot say whether the system decides well. One run yields one decision, so it cannot say whether the system decides well.
This runs the same machinery over many (ticker, date) cells and reads the This runs the same machinery over many (ticker, date) cells and reads the
aggregate. The decision log is the results table: every run already records its aggregate. The memory log is the results table: every run already records its
rating and later settles it with realized and alpha return against the rating and later settles it with realized and alpha return against the
instrument's regional benchmark, so there is nothing to record separately. instrument's regional benchmark, so there is nothing to record separately.
@@ -134,7 +134,7 @@ def run_backtest(
selected_analysts=("market", "social", "news", "fundamentals"), selected_analysts=("market", "social", "news", "fundamentals"),
run_id: str | None = None, run_id: str | None = None,
) -> BacktestResult: ) -> BacktestResult:
"""Analyze every ticker on every date, into a decision log of this run's own. """Analyze every ticker on every date, into a memory log of this run's own.
The live log stays untouched: a sweep would otherwise flood the context that The live log stays untouched: a sweep would otherwise flood the context that
real runs read back. Cells already in this run's log are skipped, so an real runs read back. Cells already in this run's log are skipped, so an
@@ -176,13 +176,13 @@ def run_backtest(
def summarize(source: BacktestResult | str | Path) -> BacktestSummary: def summarize(source: BacktestResult | str | Path) -> BacktestSummary:
"""Score the settled decisions of a backtest, or of a decision log at a path, by rating.""" """Score the settled decisions of a backtest, or of a memory log at a path, by rating."""
if isinstance(source, BacktestResult): if isinstance(source, BacktestResult):
path = source.log_path # a run whose cells all failed wrote no log: nothing to score path = source.log_path # a run whose cells all failed wrote no log: nothing to score
elif Path(source).is_file(): elif Path(source).is_file():
path = Path(source) path = Path(source)
else: else:
raise FileNotFoundError(f"no decision log at {source}") raise FileNotFoundError(f"no memory log at {source}")
entries = TradingMemoryLog({"memory_log_path": str(path)}).load_entries() entries = TradingMemoryLog({"memory_log_path": str(path)}).load_entries()
# A decision with no readable rating has no direction, so it can neither # A decision with no readable rating has no direction, so it can neither
# count for nor against the system; it is reported as unscored instead. # count for nor against the system; it is reported as unscored instead.
+1 -1
View File
@@ -262,7 +262,7 @@ class TradingAgentsGraph:
Settles this ticker's pending decisions first, then injects the lessons Settles this ticker's pending decisions first, then injects the lessons
known by the trade date for the Portfolio Manager (#1251) and the known by the trade date for the Portfolio Manager (#1251) and the
resolved instrument identity for every agent (#814). An entry point that resolved instrument identity for every agent (#814). An entry point that
assembled the state itself would skip the decision log. assembled the state itself would skip the memory log.
""" """
self.settle_pending(company_name) self.settle_pending(company_name)
return self.propagator.create_initial_state( return self.propagator.create_initial_state(
+5 -5
View File
@@ -1,4 +1,4 @@
"""Append-only markdown decision log for TradingAgents.""" """The memory log: an append-only markdown record of each decision and, once settled, its outcome."""
import re import re
from pathlib import Path from pathlib import Path
@@ -25,7 +25,7 @@ class TradingMemoryLog:
# Optional cap on resolved entries. None disables rotation. # Optional cap on resolved entries. None disables rotation.
self._max_entries = cfg.get("memory_log_max_entries") self._max_entries = cfg.get("memory_log_max_entries")
# --- Write path (Phase A) --- # --- Write: a run records its decision ---
def store_decision( def store_decision(
self, self,
@@ -56,7 +56,7 @@ class TradingMemoryLog:
with open(self._log_path, "a", encoding="utf-8") as f: with open(self._log_path, "a", encoding="utf-8") as f:
f.write(entry) f.write(entry)
# --- Read path (Phase A) --- # --- Read ---
def load_entries(self) -> list[dict]: def load_entries(self) -> list[dict]:
"""Parse all entries from log. Returns list of dicts.""" """Parse all entries from log. Returns list of dicts."""
@@ -72,7 +72,7 @@ class TradingMemoryLog:
return entries return entries
def get_pending_entries(self) -> list[dict]: def get_pending_entries(self) -> list[dict]:
"""Return entries with outcome:pending (for Phase B).""" """Return entries with outcome:pending, for settlement."""
return [e for e in self.load_entries() if e.get("pending")] return [e for e in self.load_entries() if e.get("pending")]
def get_past_context( def get_past_context(
@@ -114,7 +114,7 @@ class TradingMemoryLog:
parts.extend(self._format_reflection_only(e) for e in cross) parts.extend(self._format_reflection_only(e) for e in cross)
return "\n\n".join(parts) return "\n\n".join(parts)
# --- Update path (Phase B) --- # --- Settle: record a decision's outcome and reflection ---
def update_with_outcome( def update_with_outcome(
self, self,
+5 -3
View File
@@ -1,3 +1,5 @@
"""Reflection: a settled decision's outcome turned into a short lesson for later runs."""
from typing import Any from typing import Any
@@ -9,7 +11,7 @@ class Reflector:
self.quick_thinking_llm = quick_thinking_llm self.quick_thinking_llm = quick_thinking_llm
def _system_prompt(self, holding_days: int) -> str: def _system_prompt(self, holding_days: int) -> str:
"""Concise prompt for reflect_on_final_decision (Phase B log entries). """Concise prompt for reflect_on_final_decision (settled memory log entries).
Produces 2-4 sentences of plain prose, compact enough to be re-injected Produces 2-4 sentences of plain prose, compact enough to be re-injected
into future agent prompts without bloating the context window. The into future agent prompts without bloating the context window. The
@@ -27,7 +29,7 @@ class Reflector:
"and say so plainly if the window is too short to judge the thesis.\n" "and say so plainly if the window is too short to judge the thesis.\n"
"2. Which part of the investment thesis this window supports or undercuts.\n" "2. Which part of the investment thesis this window supports or undercuts.\n"
"3. One concrete lesson to apply to the next similar analysis.\n\n" "3. One concrete lesson to apply to the next similar analysis.\n\n"
"Be specific and terse. Your output will be stored verbatim in a decision log " "Be specific and terse. Your output will be stored verbatim in a memory log "
"and re-read by future analysts, so every word must earn its place." "and re-read by future analysts, so every word must earn its place."
) )
@@ -41,7 +43,7 @@ class Reflector:
) -> str: ) -> str:
"""Single reflection call on the final trade decision with outcome context. """Single reflection call on the final trade decision with outcome context.
Used by Phase B deferred reflection. The final_trade_decision already Used when a pending decision is settled. The final_trade_decision already
synthesises all analyst insights, so no separate market context is needed. synthesises all analyst insights, so no separate market context is needed.
``benchmark_name`` is the label used for the alpha line (e.g. ``"SPY"`` ``benchmark_name`` is the label used for the alpha line (e.g. ``"SPY"``
for US tickers, ``"^N225"`` for ``.T`` listings); defaults to SPY for for US tickers, ``"^N225"`` for ``.T`` listings); defaults to SPY for
+1 -1
View File
@@ -1,5 +1,5 @@
"""Settling past decisions: once a decision's holding window has traded, score """Settling past decisions: once a decision's holding window has traded, score
it against its benchmark and record a reflection on it in the decision log.""" it against its benchmark and record a reflection on it in the memory log."""
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta