mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-05-01 14:33:10 +03:00
feat: replace per-agent BM25 memory with persistent append-only decision log
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,6 @@ dependencies = [
|
|||||||
"parsel>=1.10.0",
|
"parsel>=1.10.0",
|
||||||
"pytz>=2025.2",
|
"pytz>=2025.2",
|
||||||
"questionary>=2.1.0",
|
"questionary>=2.1.0",
|
||||||
"rank-bm25>=0.2.2",
|
|
||||||
"redis>=6.2.0",
|
"redis>=6.2.0",
|
||||||
"requests>=2.32.4",
|
"requests>=2.32.4",
|
||||||
"rich>=14.0.0",
|
"rich>=14.0.0",
|
||||||
|
|||||||
648
tests/test_memory_log.py
Normal file
648
tests/test_memory_log.py
Normal file
@@ -0,0 +1,648 @@
|
|||||||
|
"""Tests for TradingMemoryLog — storage, deferred reflection, PM injection, legacy removal."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import pandas as pd
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from tradingagents.agents.utils.memory import TradingMemoryLog
|
||||||
|
from tradingagents.graph.reflection import Reflector
|
||||||
|
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||||
|
from tradingagents.graph.propagation import Propagator
|
||||||
|
from tradingagents.agents.managers.portfolio_manager import create_portfolio_manager
|
||||||
|
|
||||||
|
_SEP = TradingMemoryLog._SEPARATOR
|
||||||
|
|
||||||
|
DECISION_BUY = "Rating: Buy\nEnter at $189-192, 6% portfolio cap."
|
||||||
|
DECISION_OVERWEIGHT = (
|
||||||
|
"Rating: Overweight\n"
|
||||||
|
"Executive Summary: Moderate position, await confirmation.\n"
|
||||||
|
"Investment Thesis: Strong fundamentals but near-term headwinds."
|
||||||
|
)
|
||||||
|
DECISION_SELL = "Rating: Sell\nExit position immediately."
|
||||||
|
DECISION_NO_RATING = (
|
||||||
|
"Executive Summary: Complex situation with multiple competing factors.\n"
|
||||||
|
"Investment Thesis: No clear directional signal at this time."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Shared helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def make_log(tmp_path, filename="trading_memory.md"):
|
||||||
|
config = {"memory_log_path": str(tmp_path / filename)}
|
||||||
|
return TradingMemoryLog(config)
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_completed(tmp_path, ticker, date, decision_text, reflection_text, filename="trading_memory.md"):
|
||||||
|
"""Write a completed entry directly to file, bypassing the API."""
|
||||||
|
entry = (
|
||||||
|
f"[{date} | {ticker} | Buy | +1.0% | +0.5% | 5d]\n\n"
|
||||||
|
f"DECISION:\n{decision_text}\n\n"
|
||||||
|
f"REFLECTION:\n{reflection_text}"
|
||||||
|
+ _SEP
|
||||||
|
)
|
||||||
|
with open(tmp_path / filename, "a", encoding="utf-8") as f:
|
||||||
|
f.write(entry)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_entry(log, ticker, date, decision, reflection="Good call."):
|
||||||
|
"""Store a decision then immediately resolve it via the API."""
|
||||||
|
log.store_decision(ticker, date, decision)
|
||||||
|
log.update_with_outcome(ticker, date, 0.05, 0.02, 5, reflection)
|
||||||
|
|
||||||
|
|
||||||
|
def _price_df(prices):
|
||||||
|
"""Minimal DataFrame matching yfinance .history() output shape."""
|
||||||
|
return pd.DataFrame({"Close": prices})
|
||||||
|
|
||||||
|
|
||||||
|
def _make_pm_state(past_context=""):
|
||||||
|
"""Minimal AgentState dict for portfolio_manager_node."""
|
||||||
|
return {
|
||||||
|
"company_of_interest": "NVDA",
|
||||||
|
"past_context": past_context,
|
||||||
|
"risk_debate_state": {
|
||||||
|
"history": "Risk debate history.",
|
||||||
|
"aggressive_history": "",
|
||||||
|
"conservative_history": "",
|
||||||
|
"neutral_history": "",
|
||||||
|
"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": "Research plan.",
|
||||||
|
"trader_investment_plan": "Trader plan.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Core: storage and read path
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestTradingMemoryLogCore:
|
||||||
|
|
||||||
|
def test_store_creates_file(self, tmp_path):
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
assert not (tmp_path / "trading_memory.md").exists()
|
||||||
|
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
|
||||||
|
assert (tmp_path / "trading_memory.md").exists()
|
||||||
|
|
||||||
|
def test_store_appends_not_overwrites(self, tmp_path):
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
|
||||||
|
log.store_decision("AAPL", "2026-01-11", DECISION_OVERWEIGHT)
|
||||||
|
entries = log.load_entries()
|
||||||
|
assert len(entries) == 2
|
||||||
|
assert entries[0]["ticker"] == "NVDA"
|
||||||
|
assert entries[1]["ticker"] == "AAPL"
|
||||||
|
|
||||||
|
def test_store_decision_idempotent(self, tmp_path):
|
||||||
|
"""Calling store_decision twice with same (ticker, date) stores only one entry."""
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
|
||||||
|
assert len(log.load_entries()) == 1
|
||||||
|
|
||||||
|
def test_batch_update_resolves_multiple_entries(self, tmp_path):
|
||||||
|
"""batch_update_with_outcomes resolves multiple pending entries in one write."""
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-05", DECISION_BUY)
|
||||||
|
log.store_decision("NVDA", "2026-01-12", DECISION_SELL)
|
||||||
|
|
||||||
|
updates = [
|
||||||
|
{"ticker": "NVDA", "trade_date": "2026-01-05",
|
||||||
|
"raw_return": 0.05, "alpha_return": 0.02, "holding_days": 5,
|
||||||
|
"reflection": "First correct."},
|
||||||
|
{"ticker": "NVDA", "trade_date": "2026-01-12",
|
||||||
|
"raw_return": -0.03, "alpha_return": -0.01, "holding_days": 5,
|
||||||
|
"reflection": "Second correct."},
|
||||||
|
]
|
||||||
|
log.batch_update_with_outcomes(updates)
|
||||||
|
|
||||||
|
entries = log.load_entries()
|
||||||
|
assert len(entries) == 2
|
||||||
|
assert all(not e["pending"] for e in entries)
|
||||||
|
assert entries[0]["reflection"] == "First correct."
|
||||||
|
assert entries[1]["reflection"] == "Second correct."
|
||||||
|
|
||||||
|
def test_pending_tag_format(self, tmp_path):
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
|
||||||
|
text = (tmp_path / "trading_memory.md").read_text(encoding="utf-8")
|
||||||
|
assert "[2026-01-10 | NVDA | Buy | pending]" in text
|
||||||
|
|
||||||
|
# Rating parsing
|
||||||
|
|
||||||
|
def test_rating_parsed_buy(self, tmp_path):
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
|
||||||
|
assert log.load_entries()[0]["rating"] == "Buy"
|
||||||
|
|
||||||
|
def test_rating_parsed_overweight(self, tmp_path):
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("AAPL", "2026-01-11", DECISION_OVERWEIGHT)
|
||||||
|
assert log.load_entries()[0]["rating"] == "Overweight"
|
||||||
|
|
||||||
|
def test_rating_fallback_hold(self, tmp_path):
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("MSFT", "2026-01-12", DECISION_NO_RATING)
|
||||||
|
assert log.load_entries()[0]["rating"] == "Hold"
|
||||||
|
|
||||||
|
def test_rating_priority_over_prose(self, tmp_path):
|
||||||
|
"""'Rating: X' label wins even when an opposing rating word appears earlier in prose."""
|
||||||
|
decision = (
|
||||||
|
"The sell thesis is weak. The hold case is marginal.\n\n"
|
||||||
|
"Rating: Buy\n\n"
|
||||||
|
"Executive Summary: Strong fundamentals support the position."
|
||||||
|
)
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", decision)
|
||||||
|
assert log.load_entries()[0]["rating"] == "Buy"
|
||||||
|
|
||||||
|
# Delimiter robustness
|
||||||
|
|
||||||
|
def test_decision_with_markdown_separator(self, tmp_path):
|
||||||
|
"""LLM decision containing '---' must not corrupt the entry."""
|
||||||
|
decision = "Rating: Buy\n\n---\n\nRisk: elevated volatility."
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", decision)
|
||||||
|
entries = log.load_entries()
|
||||||
|
assert len(entries) == 1
|
||||||
|
assert "Risk: elevated volatility" in entries[0]["decision"]
|
||||||
|
|
||||||
|
# load_entries
|
||||||
|
|
||||||
|
def test_load_entries_empty_file(self, tmp_path):
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
assert log.load_entries() == []
|
||||||
|
|
||||||
|
def test_load_entries_single(self, tmp_path):
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
|
||||||
|
entries = log.load_entries()
|
||||||
|
assert len(entries) == 1
|
||||||
|
e = entries[0]
|
||||||
|
assert e["date"] == "2026-01-10"
|
||||||
|
assert e["ticker"] == "NVDA"
|
||||||
|
assert e["rating"] == "Buy"
|
||||||
|
assert e["pending"] is True
|
||||||
|
assert e["raw"] is None
|
||||||
|
|
||||||
|
def test_load_entries_multiple(self, tmp_path):
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
|
||||||
|
log.store_decision("AAPL", "2026-01-11", DECISION_OVERWEIGHT)
|
||||||
|
log.store_decision("MSFT", "2026-01-12", DECISION_NO_RATING)
|
||||||
|
entries = log.load_entries()
|
||||||
|
assert len(entries) == 3
|
||||||
|
assert [e["ticker"] for e in entries] == ["NVDA", "AAPL", "MSFT"]
|
||||||
|
|
||||||
|
def test_decision_content_preserved(self, tmp_path):
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
|
||||||
|
assert log.load_entries()[0]["decision"] == DECISION_BUY.strip()
|
||||||
|
|
||||||
|
# get_pending_entries
|
||||||
|
|
||||||
|
def test_get_pending_returns_pending_only(self, tmp_path):
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
_seed_completed(tmp_path, "NVDA", "2026-01-05", "Buy NVDA.", "Correct.")
|
||||||
|
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
|
||||||
|
pending = log.get_pending_entries()
|
||||||
|
assert len(pending) == 1
|
||||||
|
assert pending[0]["ticker"] == "NVDA"
|
||||||
|
assert pending[0]["date"] == "2026-01-10"
|
||||||
|
|
||||||
|
# get_past_context
|
||||||
|
|
||||||
|
def test_get_past_context_empty(self, tmp_path):
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
assert log.get_past_context("NVDA") == ""
|
||||||
|
|
||||||
|
def test_get_past_context_pending_excluded(self, tmp_path):
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
|
||||||
|
assert log.get_past_context("NVDA") == ""
|
||||||
|
|
||||||
|
def test_get_past_context_same_ticker(self, tmp_path):
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
_seed_completed(tmp_path, "NVDA", "2026-01-05", "Buy NVDA — AI capex thesis intact.", "Directionally correct.")
|
||||||
|
ctx = log.get_past_context("NVDA")
|
||||||
|
assert "Past analyses of NVDA" in ctx
|
||||||
|
assert "Buy NVDA" in ctx
|
||||||
|
|
||||||
|
def test_get_past_context_cross_ticker(self, tmp_path):
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
_seed_completed(tmp_path, "AAPL", "2026-01-05", "Buy AAPL — Services growth.", "Correct.")
|
||||||
|
ctx = log.get_past_context("NVDA")
|
||||||
|
assert "Recent cross-ticker lessons" in ctx
|
||||||
|
assert "Past analyses of NVDA" not in ctx
|
||||||
|
|
||||||
|
def test_n_same_limit_respected(self, tmp_path):
|
||||||
|
"""Only the n_same most recent same-ticker entries are included."""
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
for i in range(6):
|
||||||
|
_seed_completed(tmp_path, "NVDA", f"2026-01-{i+1:02d}", f"Buy entry {i}.", "Correct.")
|
||||||
|
ctx = log.get_past_context("NVDA", n_same=5)
|
||||||
|
assert "Buy entry 0" not in ctx
|
||||||
|
assert "Buy entry 5" in ctx
|
||||||
|
|
||||||
|
def test_n_cross_limit_respected(self, tmp_path):
|
||||||
|
"""Only the n_cross most recent cross-ticker entries are included."""
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
for i, ticker in enumerate(["AAPL", "MSFT", "GOOG", "META"]):
|
||||||
|
_seed_completed(tmp_path, ticker, f"2026-01-{i+1:02d}", f"Buy {ticker}.", "Correct.")
|
||||||
|
ctx = log.get_past_context("NVDA", n_cross=3)
|
||||||
|
assert "AAPL" not in ctx
|
||||||
|
assert "META" in ctx
|
||||||
|
|
||||||
|
# No-op when config is None
|
||||||
|
|
||||||
|
def test_no_log_path_is_noop(self):
|
||||||
|
log = TradingMemoryLog(config=None)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
|
||||||
|
assert log.load_entries() == []
|
||||||
|
assert log.get_past_context("NVDA") == ""
|
||||||
|
|
||||||
|
# Rating parsing: markdown bold and numbered list formats
|
||||||
|
|
||||||
|
def test_rating_parsed_from_bold_markdown(self, tmp_path):
|
||||||
|
"""**Rating**: Buy — markdown bold wrapper must not prevent parsing."""
|
||||||
|
decision = "**Rating**: Buy\nEnter at $190."
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", decision)
|
||||||
|
assert log.load_entries()[0]["rating"] == "Buy"
|
||||||
|
|
||||||
|
def test_rating_parsed_from_numbered_list(self, tmp_path):
|
||||||
|
"""1. Rating: Buy — numbered list prefix must not prevent parsing."""
|
||||||
|
decision = "1. Rating: Buy\nEnter at $190."
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", decision)
|
||||||
|
assert log.load_entries()[0]["rating"] == "Buy"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Deferred reflection: update_with_outcome, Reflector, _fetch_returns
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestDeferredReflection:
|
||||||
|
|
||||||
|
# update_with_outcome
|
||||||
|
|
||||||
|
def test_update_replaces_pending_tag(self, tmp_path):
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
|
||||||
|
log.update_with_outcome("NVDA", "2026-01-10", 0.042, 0.021, 5, "Momentum confirmed.")
|
||||||
|
text = (tmp_path / "trading_memory.md").read_text(encoding="utf-8")
|
||||||
|
assert "[2026-01-10 | NVDA | Buy | pending]" not in text
|
||||||
|
assert "+4.2%" in text
|
||||||
|
assert "+2.1%" in text
|
||||||
|
assert "5d" in text
|
||||||
|
|
||||||
|
def test_update_appends_reflection(self, tmp_path):
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
|
||||||
|
log.update_with_outcome("NVDA", "2026-01-10", 0.042, 0.021, 5, "Momentum confirmed.")
|
||||||
|
entries = log.load_entries()
|
||||||
|
assert len(entries) == 1
|
||||||
|
e = entries[0]
|
||||||
|
assert e["pending"] is False
|
||||||
|
assert e["reflection"] == "Momentum confirmed."
|
||||||
|
assert e["decision"] == DECISION_BUY.strip()
|
||||||
|
|
||||||
|
def test_update_preserves_other_entries(self, tmp_path):
|
||||||
|
"""Only the matching entry is modified; all other entries remain unchanged."""
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
|
||||||
|
log.store_decision("AAPL", "2026-01-11", "Rating: Hold\nHold AAPL.")
|
||||||
|
log.store_decision("MSFT", "2026-01-12", DECISION_SELL)
|
||||||
|
log.update_with_outcome("AAPL", "2026-01-11", 0.01, -0.01, 5, "Neutral result.")
|
||||||
|
entries = log.load_entries()
|
||||||
|
assert len(entries) == 3
|
||||||
|
nvda, aapl, msft = entries
|
||||||
|
assert nvda["ticker"] == "NVDA" and nvda["pending"] is True
|
||||||
|
assert aapl["ticker"] == "AAPL" and aapl["pending"] is False
|
||||||
|
assert aapl["reflection"] == "Neutral result."
|
||||||
|
assert msft["ticker"] == "MSFT" and msft["pending"] is True
|
||||||
|
|
||||||
|
def test_update_atomic_write(self, tmp_path):
|
||||||
|
"""A pre-existing .tmp file is overwritten; the log is correctly updated."""
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
|
||||||
|
stale_tmp = tmp_path / "trading_memory.tmp"
|
||||||
|
stale_tmp.write_text("GARBAGE CONTENT — should be overwritten", encoding="utf-8")
|
||||||
|
log.update_with_outcome("NVDA", "2026-01-10", 0.042, 0.021, 5, "Correct.")
|
||||||
|
assert not stale_tmp.exists()
|
||||||
|
entries = log.load_entries()
|
||||||
|
assert len(entries) == 1
|
||||||
|
assert entries[0]["reflection"] == "Correct."
|
||||||
|
assert entries[0]["pending"] is False
|
||||||
|
|
||||||
|
def test_update_noop_when_no_log_path(self):
|
||||||
|
log = TradingMemoryLog(config=None)
|
||||||
|
log.update_with_outcome("NVDA", "2026-01-10", 0.05, 0.02, 5, "Reflection")
|
||||||
|
|
||||||
|
def test_formatting_roundtrip_after_update(self, tmp_path):
|
||||||
|
"""All fields intact and blank line between tag and DECISION preserved after update."""
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
|
||||||
|
log.update_with_outcome("NVDA", "2026-01-10", 0.042, 0.021, 5, "Momentum confirmed.")
|
||||||
|
entries = log.load_entries()
|
||||||
|
assert len(entries) == 1
|
||||||
|
e = entries[0]
|
||||||
|
assert e["pending"] is False
|
||||||
|
assert e["decision"] == DECISION_BUY.strip()
|
||||||
|
assert e["reflection"] == "Momentum confirmed."
|
||||||
|
assert e["raw"] == "+4.2%"
|
||||||
|
assert e["alpha"] == "+2.1%"
|
||||||
|
assert e["holding"] == "5d"
|
||||||
|
raw_text = (tmp_path / "trading_memory.md").read_text(encoding="utf-8")
|
||||||
|
assert "[2026-01-10 | NVDA | Buy | +4.2% | +2.1% | 5d]\n\nDECISION:" in raw_text
|
||||||
|
|
||||||
|
# Reflector.reflect_on_final_decision
|
||||||
|
|
||||||
|
def test_reflect_on_final_decision_returns_llm_output(self):
|
||||||
|
mock_llm = MagicMock()
|
||||||
|
mock_llm.invoke.return_value.content = "Directionally correct. Thesis confirmed."
|
||||||
|
reflector = Reflector(mock_llm)
|
||||||
|
result = reflector.reflect_on_final_decision(
|
||||||
|
final_decision=DECISION_BUY, raw_return=0.042, alpha_return=0.021
|
||||||
|
)
|
||||||
|
assert result == "Directionally correct. Thesis confirmed."
|
||||||
|
mock_llm.invoke.assert_called_once()
|
||||||
|
|
||||||
|
def test_reflect_on_final_decision_includes_returns_in_prompt(self):
|
||||||
|
"""Return figures are present in the human message sent to the LLM."""
|
||||||
|
mock_llm = MagicMock()
|
||||||
|
mock_llm.invoke.return_value.content = "Incorrect call."
|
||||||
|
reflector = Reflector(mock_llm)
|
||||||
|
reflector.reflect_on_final_decision(
|
||||||
|
final_decision=DECISION_SELL, raw_return=-0.08, alpha_return=-0.05
|
||||||
|
)
|
||||||
|
messages = mock_llm.invoke.call_args[0][0]
|
||||||
|
human_content = next(content for role, content in messages if role == "human")
|
||||||
|
assert "-8.0%" in human_content
|
||||||
|
assert "-5.0%" in human_content
|
||||||
|
assert "Exit position immediately." in human_content
|
||||||
|
|
||||||
|
# TradingAgentsGraph._fetch_returns
|
||||||
|
|
||||||
|
def test_fetch_returns_valid_ticker(self):
|
||||||
|
stock_prices = [100.0, 102.0, 104.0, 103.0, 105.0, 106.0]
|
||||||
|
spy_prices = [400.0, 402.0, 404.0, 403.0, 405.0, 406.0]
|
||||||
|
mock_graph = MagicMock(spec=TradingAgentsGraph)
|
||||||
|
with patch("yfinance.Ticker") as mock_ticker_cls:
|
||||||
|
def _make_ticker(sym):
|
||||||
|
m = MagicMock()
|
||||||
|
m.history.return_value = _price_df(spy_prices if sym == "SPY" else stock_prices)
|
||||||
|
return m
|
||||||
|
mock_ticker_cls.side_effect = _make_ticker
|
||||||
|
raw, alpha, days = TradingAgentsGraph._fetch_returns(mock_graph, "NVDA", "2026-01-05")
|
||||||
|
assert raw is not None and alpha is not None and days is not None
|
||||||
|
assert isinstance(raw, float) and isinstance(alpha, float) and isinstance(days, int)
|
||||||
|
assert days == 5
|
||||||
|
|
||||||
|
def test_fetch_returns_too_recent(self):
|
||||||
|
"""Only 1 data point available → returns (None, None, None), no crash."""
|
||||||
|
mock_graph = MagicMock(spec=TradingAgentsGraph)
|
||||||
|
with patch("yfinance.Ticker") as mock_ticker_cls:
|
||||||
|
m = MagicMock()
|
||||||
|
m.history.return_value = _price_df([100.0])
|
||||||
|
mock_ticker_cls.return_value = m
|
||||||
|
raw, alpha, days = TradingAgentsGraph._fetch_returns(mock_graph, "NVDA", "2026-04-19")
|
||||||
|
assert raw is None and alpha is None and days is None
|
||||||
|
|
||||||
|
def test_fetch_returns_delisted(self):
|
||||||
|
"""Empty DataFrame → returns (None, None, None), no crash."""
|
||||||
|
mock_graph = MagicMock(spec=TradingAgentsGraph)
|
||||||
|
with patch("yfinance.Ticker") as mock_ticker_cls:
|
||||||
|
m = MagicMock()
|
||||||
|
m.history.return_value = pd.DataFrame({"Close": []})
|
||||||
|
mock_ticker_cls.return_value = m
|
||||||
|
raw, alpha, days = TradingAgentsGraph._fetch_returns(mock_graph, "XXXXXFAKE", "2026-01-10")
|
||||||
|
assert raw is None and alpha is None and days is None
|
||||||
|
|
||||||
|
def test_fetch_returns_spy_shorter_than_stock(self):
|
||||||
|
"""SPY having fewer rows than the stock must not raise IndexError."""
|
||||||
|
stock_prices = [100.0, 102.0, 104.0, 103.0, 105.0, 106.0]
|
||||||
|
spy_prices = [400.0, 402.0, 403.0]
|
||||||
|
mock_graph = MagicMock(spec=TradingAgentsGraph)
|
||||||
|
with patch("yfinance.Ticker") as mock_ticker_cls:
|
||||||
|
def _make_ticker(sym):
|
||||||
|
m = MagicMock()
|
||||||
|
m.history.return_value = _price_df(spy_prices if sym == "SPY" else stock_prices)
|
||||||
|
return m
|
||||||
|
mock_ticker_cls.side_effect = _make_ticker
|
||||||
|
raw, alpha, days = TradingAgentsGraph._fetch_returns(mock_graph, "NVDA", "2026-01-05")
|
||||||
|
assert raw is not None and alpha is not None and days is not None
|
||||||
|
assert days == 2
|
||||||
|
|
||||||
|
# TradingAgentsGraph._resolve_pending_entries
|
||||||
|
|
||||||
|
def test_resolve_skips_other_tickers(self, tmp_path):
|
||||||
|
"""Pending AAPL entry is not resolved when the run is for NVDA."""
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("AAPL", "2026-01-10", DECISION_BUY)
|
||||||
|
mock_graph = MagicMock(spec=TradingAgentsGraph)
|
||||||
|
mock_graph.memory_log = log
|
||||||
|
mock_graph._fetch_returns = MagicMock(return_value=(0.05, 0.02, 5))
|
||||||
|
TradingAgentsGraph._resolve_pending_entries(mock_graph, "NVDA")
|
||||||
|
mock_graph._fetch_returns.assert_not_called()
|
||||||
|
assert len(log.get_pending_entries()) == 1
|
||||||
|
|
||||||
|
def test_resolve_marks_entry_completed(self, tmp_path):
|
||||||
|
"""After resolve, get_pending_entries() is empty and the entry has a REFLECTION."""
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-05", DECISION_BUY)
|
||||||
|
mock_reflector = MagicMock()
|
||||||
|
mock_reflector.reflect_on_final_decision.return_value = "Momentum confirmed."
|
||||||
|
mock_graph = MagicMock(spec=TradingAgentsGraph)
|
||||||
|
mock_graph.memory_log = log
|
||||||
|
mock_graph.reflector = mock_reflector
|
||||||
|
mock_graph._fetch_returns = MagicMock(return_value=(0.05, 0.02, 5))
|
||||||
|
TradingAgentsGraph._resolve_pending_entries(mock_graph, "NVDA")
|
||||||
|
assert log.get_pending_entries() == []
|
||||||
|
entries = log.load_entries()
|
||||||
|
assert len(entries) == 1
|
||||||
|
assert entries[0]["pending"] is False
|
||||||
|
assert entries[0]["reflection"] == "Momentum confirmed."
|
||||||
|
assert "+5.0%" in entries[0]["raw"]
|
||||||
|
assert "+2.0%" in entries[0]["alpha"]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Portfolio Manager injection: past_context in state and prompt
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestPortfolioManagerInjection:
|
||||||
|
|
||||||
|
# past_context in initial state
|
||||||
|
|
||||||
|
def test_past_context_in_initial_state(self):
|
||||||
|
propagator = Propagator()
|
||||||
|
state = propagator.create_initial_state("NVDA", "2026-01-10", past_context="some context")
|
||||||
|
assert "past_context" in state
|
||||||
|
assert state["past_context"] == "some context"
|
||||||
|
|
||||||
|
def test_past_context_defaults_to_empty(self):
|
||||||
|
propagator = Propagator()
|
||||||
|
state = propagator.create_initial_state("NVDA", "2026-01-10")
|
||||||
|
assert state["past_context"] == ""
|
||||||
|
|
||||||
|
# PM prompt
|
||||||
|
|
||||||
|
def test_pm_prompt_includes_past_context(self):
|
||||||
|
captured = {}
|
||||||
|
mock_llm = MagicMock()
|
||||||
|
mock_llm.invoke.side_effect = lambda prompt: (
|
||||||
|
captured.__setitem__("prompt", prompt) or MagicMock(content="Rating: Hold\nHold.")
|
||||||
|
)
|
||||||
|
pm_node = create_portfolio_manager(mock_llm)
|
||||||
|
state = _make_pm_state(past_context="[2026-01-05 | NVDA | Buy | +5.0% | +2.0% | 5d]\nGreat call.")
|
||||||
|
pm_node(state)
|
||||||
|
assert "Past decisions on this stock" in captured["prompt"]
|
||||||
|
assert "Great call." in captured["prompt"]
|
||||||
|
|
||||||
|
def test_pm_no_past_context_no_section(self):
|
||||||
|
"""PM prompt omits the lessons section entirely when past_context is empty."""
|
||||||
|
captured = {}
|
||||||
|
mock_llm = MagicMock()
|
||||||
|
mock_llm.invoke.side_effect = lambda prompt: (
|
||||||
|
captured.__setitem__("prompt", prompt) or MagicMock(content="Rating: Hold\nHold.")
|
||||||
|
)
|
||||||
|
pm_node = create_portfolio_manager(mock_llm)
|
||||||
|
state = _make_pm_state(past_context="")
|
||||||
|
pm_node(state)
|
||||||
|
assert "Past decisions on this stock" not in captured["prompt"]
|
||||||
|
assert "lessons learned" not in captured["prompt"]
|
||||||
|
|
||||||
|
# get_past_context ordering and limits
|
||||||
|
|
||||||
|
def test_same_ticker_prioritised(self, tmp_path):
|
||||||
|
"""Same-ticker entries in same-ticker section; cross-ticker entries in cross-ticker section."""
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
_resolve_entry(log, "NVDA", "2026-01-05", DECISION_BUY, "Momentum confirmed.")
|
||||||
|
_resolve_entry(log, "AAPL", "2026-01-06", DECISION_SELL, "Overvalued.")
|
||||||
|
result = log.get_past_context("NVDA")
|
||||||
|
assert "Past analyses of NVDA" in result
|
||||||
|
assert "Recent cross-ticker lessons" in result
|
||||||
|
same_block, cross_block = result.split("Recent cross-ticker lessons")
|
||||||
|
assert "NVDA" in same_block
|
||||||
|
assert "AAPL" in cross_block
|
||||||
|
|
||||||
|
def test_cross_ticker_reflection_only(self, tmp_path):
|
||||||
|
"""Cross-ticker entries show only the REFLECTION text, not the full DECISION."""
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
_resolve_entry(log, "AAPL", "2026-01-06", DECISION_SELL, "Overvalued correction.")
|
||||||
|
result = log.get_past_context("NVDA")
|
||||||
|
assert "Overvalued correction." in result
|
||||||
|
assert "Exit position immediately." not in result
|
||||||
|
|
||||||
|
def test_n_same_limit_respected(self, tmp_path):
|
||||||
|
"""More than 5 same-ticker completed entries → only 5 injected."""
|
||||||
|
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}.")
|
||||||
|
result = log.get_past_context("NVDA", n_same=5)
|
||||||
|
lessons_present = sum(1 for i in range(7) if f"Lesson {i}." in result)
|
||||||
|
assert lessons_present == 5
|
||||||
|
|
||||||
|
def test_n_cross_limit_respected(self, tmp_path):
|
||||||
|
"""More than 3 cross-ticker completed entries → only 3 injected."""
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
tickers = ["AAPL", "MSFT", "TSLA", "AMZN", "GOOG"]
|
||||||
|
for i, ticker in enumerate(tickers):
|
||||||
|
_resolve_entry(log, ticker, f"2026-01-{i+1:02d}", DECISION_BUY, f"{ticker} lesson.")
|
||||||
|
result = log.get_past_context("NVDA", n_cross=3)
|
||||||
|
cross_count = sum(result.count(f"{t} lesson.") for t in tickers)
|
||||||
|
assert cross_count == 3
|
||||||
|
|
||||||
|
# Full A→B→C integration cycle
|
||||||
|
|
||||||
|
def test_full_cycle_store_resolve_inject(self, tmp_path):
|
||||||
|
"""store pending → resolve with outcome → past_context non-empty for PM."""
|
||||||
|
log = make_log(tmp_path)
|
||||||
|
log.store_decision("NVDA", "2026-01-05", DECISION_BUY)
|
||||||
|
assert len(log.get_pending_entries()) == 1
|
||||||
|
assert log.get_past_context("NVDA") == ""
|
||||||
|
log.update_with_outcome("NVDA", "2026-01-05", 0.05, 0.02, 5, "Correct call.")
|
||||||
|
assert log.get_pending_entries() == []
|
||||||
|
past_ctx = log.get_past_context("NVDA")
|
||||||
|
assert past_ctx != ""
|
||||||
|
assert "NVDA" in past_ctx
|
||||||
|
assert "Correct call." in past_ctx
|
||||||
|
assert "DECISION:" in past_ctx
|
||||||
|
assert "REFLECTION:" in past_ctx
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Legacy removal: BM25 / FinancialSituationMemory fully gone
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestLegacyRemoval:
|
||||||
|
|
||||||
|
def test_financial_situation_memory_removed(self):
|
||||||
|
"""FinancialSituationMemory must not be importable from the memory module."""
|
||||||
|
import tradingagents.agents.utils.memory as m
|
||||||
|
assert not hasattr(m, "FinancialSituationMemory")
|
||||||
|
|
||||||
|
def test_bm25_not_imported(self):
|
||||||
|
"""rank_bm25 must not be present in the memory module namespace."""
|
||||||
|
import tradingagents.agents.utils.memory as m
|
||||||
|
assert not hasattr(m, "BM25Okapi")
|
||||||
|
|
||||||
|
def test_reflect_and_remember_removed(self):
|
||||||
|
"""TradingAgentsGraph must not expose reflect_and_remember."""
|
||||||
|
assert not hasattr(TradingAgentsGraph, "reflect_and_remember")
|
||||||
|
|
||||||
|
def test_portfolio_manager_no_memory_param(self):
|
||||||
|
"""create_portfolio_manager accepts only llm; passing memory= raises TypeError."""
|
||||||
|
mock_llm = MagicMock()
|
||||||
|
create_portfolio_manager(mock_llm)
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
create_portfolio_manager(mock_llm, memory=MagicMock())
|
||||||
|
|
||||||
|
def test_full_pipeline_no_regression(self, tmp_path):
|
||||||
|
"""propagate() completes without AttributeError after legacy cleanup."""
|
||||||
|
fake_state = {
|
||||||
|
"final_trade_decision": "Rating: Buy\nBuy NVDA.",
|
||||||
|
"company_of_interest": "NVDA",
|
||||||
|
"trade_date": "2026-01-10",
|
||||||
|
"market_report": "",
|
||||||
|
"sentiment_report": "",
|
||||||
|
"news_report": "",
|
||||||
|
"fundamentals_report": "",
|
||||||
|
"investment_debate_state": {
|
||||||
|
"bull_history": "", "bear_history": "", "history": "",
|
||||||
|
"current_response": "", "judge_decision": "",
|
||||||
|
},
|
||||||
|
"investment_plan": "",
|
||||||
|
"trader_investment_plan": "",
|
||||||
|
"risk_debate_state": {
|
||||||
|
"aggressive_history": "", "conservative_history": "",
|
||||||
|
"neutral_history": "", "history": "", "judge_decision": "",
|
||||||
|
"current_aggressive_response": "", "current_conservative_response": "",
|
||||||
|
"current_neutral_response": "", "count": 1, "latest_speaker": "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mock_graph = MagicMock()
|
||||||
|
mock_graph.memory_log = TradingMemoryLog({"memory_log_path": str(tmp_path / "mem.md")})
|
||||||
|
mock_graph.log_states_dict = {}
|
||||||
|
mock_graph.debug = False
|
||||||
|
mock_graph.config = {"results_dir": str(tmp_path)}
|
||||||
|
mock_graph.graph.invoke.return_value = fake_state
|
||||||
|
mock_graph.propagator.create_initial_state.return_value = fake_state
|
||||||
|
mock_graph.propagator.get_graph_args.return_value = {}
|
||||||
|
mock_graph.signal_processor.process_signal.return_value = "Buy"
|
||||||
|
TradingAgentsGraph.propagate(mock_graph, "NVDA", "2026-01-10")
|
||||||
|
entries = mock_graph.memory_log.load_entries()
|
||||||
|
assert len(entries) == 1
|
||||||
|
assert entries[0]["ticker"] == "NVDA"
|
||||||
|
assert entries[0]["pending"] is True
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
from .utils.agent_utils import create_msg_delete
|
from .utils.agent_utils import create_msg_delete
|
||||||
from .utils.agent_states import AgentState, InvestDebateState, RiskDebateState
|
from .utils.agent_states import AgentState, InvestDebateState, RiskDebateState
|
||||||
from .utils.memory import FinancialSituationMemory
|
|
||||||
|
|
||||||
from .analysts.fundamentals_analyst import create_fundamentals_analyst
|
from .analysts.fundamentals_analyst import create_fundamentals_analyst
|
||||||
from .analysts.market_analyst import create_market_analyst
|
from .analysts.market_analyst import create_market_analyst
|
||||||
@@ -20,7 +19,6 @@ from .managers.portfolio_manager import create_portfolio_manager
|
|||||||
from .trader.trader import create_trader
|
from .trader.trader import create_trader
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"FinancialSituationMemory",
|
|
||||||
"AgentState",
|
"AgentState",
|
||||||
"create_msg_delete",
|
"create_msg_delete",
|
||||||
"InvestDebateState",
|
"InvestDebateState",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from tradingagents.agents.utils.agent_utils import build_instrument_context, get_language_instruction
|
from tradingagents.agents.utils.agent_utils import build_instrument_context, get_language_instruction
|
||||||
|
|
||||||
|
|
||||||
def create_portfolio_manager(llm, memory):
|
def create_portfolio_manager(llm):
|
||||||
def portfolio_manager_node(state) -> dict:
|
def portfolio_manager_node(state) -> dict:
|
||||||
|
|
||||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
instrument_context = build_instrument_context(state["company_of_interest"])
|
||||||
@@ -15,12 +15,11 @@ def create_portfolio_manager(llm, memory):
|
|||||||
research_plan = state["investment_plan"]
|
research_plan = state["investment_plan"]
|
||||||
trader_plan = state["trader_investment_plan"]
|
trader_plan = state["trader_investment_plan"]
|
||||||
|
|
||||||
curr_situation = f"{market_research_report}\n\n{sentiment_report}\n\n{news_report}\n\n{fundamentals_report}"
|
past_context = state.get("past_context", "")
|
||||||
past_memories = memory.get_memories(curr_situation, n_matches=2)
|
lessons_line = (
|
||||||
|
f"- Past decisions on this stock and lessons learned:\n{past_context}\n"
|
||||||
past_memory_str = ""
|
if past_context else ""
|
||||||
for i, rec in enumerate(past_memories, 1):
|
)
|
||||||
past_memory_str += rec["recommendation"] + "\n\n"
|
|
||||||
|
|
||||||
prompt = f"""As the Portfolio Manager, synthesize the risk analysts' debate and deliver the final trading decision.
|
prompt = f"""As the Portfolio Manager, synthesize the risk analysts' debate and deliver the final trading decision.
|
||||||
|
|
||||||
@@ -38,7 +37,7 @@ def create_portfolio_manager(llm, memory):
|
|||||||
**Context:**
|
**Context:**
|
||||||
- Research Manager's investment plan: **{research_plan}**
|
- Research Manager's investment plan: **{research_plan}**
|
||||||
- Trader's transaction proposal: **{trader_plan}**
|
- Trader's transaction proposal: **{trader_plan}**
|
||||||
- Lessons from past decisions: **{past_memory_str}**
|
{lessons_line}
|
||||||
|
|
||||||
**Required Output Structure:**
|
**Required Output Structure:**
|
||||||
1. **Rating**: State one of Buy / Overweight / Hold / Underweight / Sell.
|
1. **Rating**: State one of Buy / Overweight / Hold / Underweight / Sell.
|
||||||
|
|||||||
@@ -2,24 +2,13 @@
|
|||||||
from tradingagents.agents.utils.agent_utils import build_instrument_context
|
from tradingagents.agents.utils.agent_utils import build_instrument_context
|
||||||
|
|
||||||
|
|
||||||
def create_research_manager(llm, memory):
|
def create_research_manager(llm):
|
||||||
def research_manager_node(state) -> dict:
|
def research_manager_node(state) -> dict:
|
||||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
instrument_context = build_instrument_context(state["company_of_interest"])
|
||||||
history = state["investment_debate_state"].get("history", "")
|
history = state["investment_debate_state"].get("history", "")
|
||||||
market_research_report = state["market_report"]
|
|
||||||
sentiment_report = state["sentiment_report"]
|
|
||||||
news_report = state["news_report"]
|
|
||||||
fundamentals_report = state["fundamentals_report"]
|
|
||||||
|
|
||||||
investment_debate_state = state["investment_debate_state"]
|
investment_debate_state = state["investment_debate_state"]
|
||||||
|
|
||||||
curr_situation = f"{market_research_report}\n\n{sentiment_report}\n\n{news_report}\n\n{fundamentals_report}"
|
|
||||||
past_memories = memory.get_memories(curr_situation, n_matches=2)
|
|
||||||
|
|
||||||
past_memory_str = ""
|
|
||||||
for i, rec in enumerate(past_memories, 1):
|
|
||||||
past_memory_str += rec["recommendation"] + "\n\n"
|
|
||||||
|
|
||||||
prompt = f"""As the portfolio manager and debate facilitator, your role is to critically evaluate this round of debate and make a definitive decision: align with the bear analyst, the bull analyst, or choose Hold only if it is strongly justified based on the arguments presented.
|
prompt = f"""As the portfolio manager and debate facilitator, your role is to critically evaluate this round of debate and make a definitive decision: align with the bear analyst, the bull analyst, or choose Hold only if it is strongly justified based on the arguments presented.
|
||||||
|
|
||||||
Summarize the key points from both sides concisely, focusing on the most compelling evidence or reasoning. Your recommendation—Buy, Sell, or Hold—must be clear and actionable. Avoid defaulting to Hold simply because both sides have valid points; commit to a stance grounded in the debate's strongest arguments.
|
Summarize the key points from both sides concisely, focusing on the most compelling evidence or reasoning. Your recommendation—Buy, Sell, or Hold—must be clear and actionable. Avoid defaulting to Hold simply because both sides have valid points; commit to a stance grounded in the debate's strongest arguments.
|
||||||
@@ -29,10 +18,7 @@ Additionally, develop a detailed investment plan for the trader. This should inc
|
|||||||
Your Recommendation: A decisive stance supported by the most convincing arguments.
|
Your Recommendation: A decisive stance supported by the most convincing arguments.
|
||||||
Rationale: An explanation of why these arguments lead to your conclusion.
|
Rationale: An explanation of why these arguments lead to your conclusion.
|
||||||
Strategic Actions: Concrete steps for implementing the recommendation.
|
Strategic Actions: Concrete steps for implementing the recommendation.
|
||||||
Take into account your past mistakes on similar situations. Use these insights to refine your decision-making and ensure you are learning and improving. Present your analysis conversationally, as if speaking naturally, without special formatting.
|
Present your analysis conversationally, as if speaking naturally, without special formatting.
|
||||||
|
|
||||||
Here are your past reflections on mistakes:
|
|
||||||
\"{past_memory_str}\"
|
|
||||||
|
|
||||||
{instrument_context}
|
{instrument_context}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
def create_bear_researcher(llm, memory):
|
def create_bear_researcher(llm):
|
||||||
def bear_node(state) -> dict:
|
def bear_node(state) -> dict:
|
||||||
investment_debate_state = state["investment_debate_state"]
|
investment_debate_state = state["investment_debate_state"]
|
||||||
history = investment_debate_state.get("history", "")
|
history = investment_debate_state.get("history", "")
|
||||||
@@ -12,13 +12,6 @@ def create_bear_researcher(llm, memory):
|
|||||||
news_report = state["news_report"]
|
news_report = state["news_report"]
|
||||||
fundamentals_report = state["fundamentals_report"]
|
fundamentals_report = state["fundamentals_report"]
|
||||||
|
|
||||||
curr_situation = f"{market_research_report}\n\n{sentiment_report}\n\n{news_report}\n\n{fundamentals_report}"
|
|
||||||
past_memories = memory.get_memories(curr_situation, n_matches=2)
|
|
||||||
|
|
||||||
past_memory_str = ""
|
|
||||||
for i, rec in enumerate(past_memories, 1):
|
|
||||||
past_memory_str += rec["recommendation"] + "\n\n"
|
|
||||||
|
|
||||||
prompt = f"""You are a Bear Analyst making the case against investing in the stock. Your goal is to present a well-reasoned argument emphasizing risks, challenges, and negative indicators. Leverage the provided research and data to highlight potential downsides and counter bullish arguments effectively.
|
prompt = f"""You are a Bear Analyst making the case against investing in the stock. Your goal is to present a well-reasoned argument emphasizing risks, challenges, and negative indicators. Leverage the provided research and data to highlight potential downsides and counter bullish arguments effectively.
|
||||||
|
|
||||||
Key points to focus on:
|
Key points to focus on:
|
||||||
@@ -37,8 +30,7 @@ Latest world affairs news: {news_report}
|
|||||||
Company fundamentals report: {fundamentals_report}
|
Company fundamentals report: {fundamentals_report}
|
||||||
Conversation history of the debate: {history}
|
Conversation history of the debate: {history}
|
||||||
Last bull argument: {current_response}
|
Last bull argument: {current_response}
|
||||||
Reflections from similar situations and lessons learned: {past_memory_str}
|
Use this information to deliver a compelling bear argument, refute the bull's claims, and engage in a dynamic debate that demonstrates the risks and weaknesses of investing in the stock.
|
||||||
Use this information to deliver a compelling bear argument, refute the bull's claims, and engage in a dynamic debate that demonstrates the risks and weaknesses of investing in the stock. You must also address reflections and learn from lessons and mistakes you made in the past.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = llm.invoke(prompt)
|
response = llm.invoke(prompt)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
def create_bull_researcher(llm, memory):
|
def create_bull_researcher(llm):
|
||||||
def bull_node(state) -> dict:
|
def bull_node(state) -> dict:
|
||||||
investment_debate_state = state["investment_debate_state"]
|
investment_debate_state = state["investment_debate_state"]
|
||||||
history = investment_debate_state.get("history", "")
|
history = investment_debate_state.get("history", "")
|
||||||
@@ -12,13 +12,6 @@ def create_bull_researcher(llm, memory):
|
|||||||
news_report = state["news_report"]
|
news_report = state["news_report"]
|
||||||
fundamentals_report = state["fundamentals_report"]
|
fundamentals_report = state["fundamentals_report"]
|
||||||
|
|
||||||
curr_situation = f"{market_research_report}\n\n{sentiment_report}\n\n{news_report}\n\n{fundamentals_report}"
|
|
||||||
past_memories = memory.get_memories(curr_situation, n_matches=2)
|
|
||||||
|
|
||||||
past_memory_str = ""
|
|
||||||
for i, rec in enumerate(past_memories, 1):
|
|
||||||
past_memory_str += rec["recommendation"] + "\n\n"
|
|
||||||
|
|
||||||
prompt = f"""You are a Bull Analyst advocating for investing in the stock. Your task is to build a strong, evidence-based case emphasizing growth potential, competitive advantages, and positive market indicators. Leverage the provided research and data to address concerns and counter bearish arguments effectively.
|
prompt = f"""You are a Bull Analyst advocating for investing in the stock. Your task is to build a strong, evidence-based case emphasizing growth potential, competitive advantages, and positive market indicators. Leverage the provided research and data to address concerns and counter bearish arguments effectively.
|
||||||
|
|
||||||
Key points to focus on:
|
Key points to focus on:
|
||||||
@@ -35,8 +28,7 @@ Latest world affairs news: {news_report}
|
|||||||
Company fundamentals report: {fundamentals_report}
|
Company fundamentals report: {fundamentals_report}
|
||||||
Conversation history of the debate: {history}
|
Conversation history of the debate: {history}
|
||||||
Last bear argument: {current_response}
|
Last bear argument: {current_response}
|
||||||
Reflections from similar situations and lessons learned: {past_memory_str}
|
Use this information to deliver a compelling bull argument, refute the bear's concerns, and engage in a dynamic debate that demonstrates the strengths of the bull position.
|
||||||
Use this information to deliver a compelling bull argument, refute the bear's concerns, and engage in a dynamic debate that demonstrates the strengths of the bull position. You must also address reflections and learn from lessons and mistakes you made in the past.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = llm.invoke(prompt)
|
response = llm.invoke(prompt)
|
||||||
|
|||||||
@@ -3,25 +3,11 @@ import functools
|
|||||||
from tradingagents.agents.utils.agent_utils import build_instrument_context
|
from tradingagents.agents.utils.agent_utils import build_instrument_context
|
||||||
|
|
||||||
|
|
||||||
def create_trader(llm, memory):
|
def create_trader(llm):
|
||||||
def trader_node(state, name):
|
def trader_node(state, name):
|
||||||
company_name = state["company_of_interest"]
|
company_name = state["company_of_interest"]
|
||||||
instrument_context = build_instrument_context(company_name)
|
instrument_context = build_instrument_context(company_name)
|
||||||
investment_plan = state["investment_plan"]
|
investment_plan = state["investment_plan"]
|
||||||
market_research_report = state["market_report"]
|
|
||||||
sentiment_report = state["sentiment_report"]
|
|
||||||
news_report = state["news_report"]
|
|
||||||
fundamentals_report = state["fundamentals_report"]
|
|
||||||
|
|
||||||
curr_situation = f"{market_research_report}\n\n{sentiment_report}\n\n{news_report}\n\n{fundamentals_report}"
|
|
||||||
past_memories = memory.get_memories(curr_situation, n_matches=2)
|
|
||||||
|
|
||||||
past_memory_str = ""
|
|
||||||
if past_memories:
|
|
||||||
for i, rec in enumerate(past_memories, 1):
|
|
||||||
past_memory_str += rec["recommendation"] + "\n\n"
|
|
||||||
else:
|
|
||||||
past_memory_str = "No past memories found."
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"role": "user",
|
"role": "user",
|
||||||
@@ -31,7 +17,7 @@ def create_trader(llm, memory):
|
|||||||
messages = [
|
messages = [
|
||||||
{
|
{
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": f"""You are a trading agent analyzing market data to make investment decisions. Based on your analysis, provide a specific recommendation to buy, sell, or hold. End with a firm decision and always conclude your response with 'FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL**' to confirm your recommendation. Apply lessons from past decisions to strengthen your analysis. Here are reflections from similar situations you traded in and the lessons learned: {past_memory_str}""",
|
"content": "You are a trading agent analyzing market data to make investment decisions. Based on your analysis, provide a specific recommendation to buy, sell, or hold. End with a firm decision and always conclude your response with 'FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL**' to confirm your recommendation.",
|
||||||
},
|
},
|
||||||
context,
|
context,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -70,3 +70,4 @@ class AgentState(MessagesState):
|
|||||||
RiskDebateState, "Current state of the debate on evaluating risk"
|
RiskDebateState, "Current state of the debate on evaluating risk"
|
||||||
]
|
]
|
||||||
final_trade_decision: Annotated[str, "Final decision made by the Risk Analysts"]
|
final_trade_decision: Annotated[str, "Final decision made by the Risk Analysts"]
|
||||||
|
past_context: Annotated[str, "Memory log context injected at run start (same-ticker decisions + cross-ticker lessons)"]
|
||||||
|
|||||||
@@ -1,144 +1,272 @@
|
|||||||
"""Financial situation memory using BM25 for lexical similarity matching.
|
"""Append-only markdown decision log for TradingAgents."""
|
||||||
|
|
||||||
Uses BM25 (Best Matching 25) algorithm for retrieval - no API calls,
|
from typing import List, Optional
|
||||||
no token limits, works offline with any LLM provider.
|
from pathlib import Path
|
||||||
"""
|
|
||||||
|
|
||||||
from rank_bm25 import BM25Okapi
|
|
||||||
from typing import List, Tuple
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
class FinancialSituationMemory:
|
class TradingMemoryLog:
|
||||||
"""Memory system for storing and retrieving financial situations using BM25."""
|
"""Append-only markdown log of trading decisions and reflections."""
|
||||||
|
|
||||||
def __init__(self, name: str, config: dict = None):
|
RATINGS = {"buy", "overweight", "hold", "underweight", "sell"}
|
||||||
"""Initialize the memory system.
|
# HTML comment: cannot appear in LLM prose output, safe as a hard delimiter
|
||||||
|
_SEPARATOR = "\n\n<!-- ENTRY_END -->\n\n"
|
||||||
|
# Precompiled patterns — avoids re-compilation on every load_entries() call
|
||||||
|
_DECISION_RE = re.compile(r"DECISION:\n(.*?)(?=\nREFLECTION:|\Z)", re.DOTALL)
|
||||||
|
_REFLECTION_RE = re.compile(r"REFLECTION:\n(.*?)$", re.DOTALL)
|
||||||
|
_RATING_LABEL_RE = re.compile(r"rating.*?[:\-]\s*(\w+)", re.IGNORECASE)
|
||||||
|
|
||||||
Args:
|
def __init__(self, config: dict = None):
|
||||||
name: Name identifier for this memory instance
|
self._log_path = None
|
||||||
config: Configuration dict (kept for API compatibility, not used for BM25)
|
path = (config or {}).get("memory_log_path")
|
||||||
"""
|
if path:
|
||||||
self.name = name
|
self._log_path = Path(path).expanduser()
|
||||||
self.documents: List[str] = []
|
self._log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
self.recommendations: List[str] = []
|
|
||||||
self.bm25 = None
|
|
||||||
|
|
||||||
def _tokenize(self, text: str) -> List[str]:
|
# --- Write path (Phase A) ---
|
||||||
"""Tokenize text for BM25 indexing.
|
|
||||||
|
|
||||||
Simple whitespace + punctuation tokenization with lowercasing.
|
def store_decision(
|
||||||
"""
|
self,
|
||||||
# Lowercase and split on non-alphanumeric characters
|
ticker: str,
|
||||||
tokens = re.findall(r'\b\w+\b', text.lower())
|
trade_date: str,
|
||||||
return tokens
|
final_trade_decision: str,
|
||||||
|
) -> None:
|
||||||
|
"""Append pending entry at end of propagate(). No LLM call."""
|
||||||
|
if not self._log_path:
|
||||||
|
return
|
||||||
|
# Idempotency guard: fast raw-text scan instead of full parse
|
||||||
|
if self._log_path.exists():
|
||||||
|
raw = self._log_path.read_text(encoding="utf-8")
|
||||||
|
for line in raw.splitlines():
|
||||||
|
if line.startswith(f"[{trade_date} | {ticker} |") and line.endswith("| pending]"):
|
||||||
|
return
|
||||||
|
rating = self._parse_rating(final_trade_decision)
|
||||||
|
tag = f"[{trade_date} | {ticker} | {rating} | pending]"
|
||||||
|
entry = f"{tag}\n\nDECISION:\n{final_trade_decision}{self._SEPARATOR}"
|
||||||
|
with open(self._log_path, "a", encoding="utf-8") as f:
|
||||||
|
f.write(entry)
|
||||||
|
|
||||||
def _rebuild_index(self):
|
# --- Read path (Phase A) ---
|
||||||
"""Rebuild the BM25 index after adding documents."""
|
|
||||||
if self.documents:
|
|
||||||
tokenized_docs = [self._tokenize(doc) for doc in self.documents]
|
|
||||||
self.bm25 = BM25Okapi(tokenized_docs)
|
|
||||||
else:
|
|
||||||
self.bm25 = None
|
|
||||||
|
|
||||||
def add_situations(self, situations_and_advice: List[Tuple[str, str]]):
|
def load_entries(self) -> List[dict]:
|
||||||
"""Add financial situations and their corresponding advice.
|
"""Parse all entries from log. Returns list of dicts."""
|
||||||
|
if not self._log_path or not self._log_path.exists():
|
||||||
Args:
|
|
||||||
situations_and_advice: List of tuples (situation, recommendation)
|
|
||||||
"""
|
|
||||||
for situation, recommendation in situations_and_advice:
|
|
||||||
self.documents.append(situation)
|
|
||||||
self.recommendations.append(recommendation)
|
|
||||||
|
|
||||||
# Rebuild BM25 index with new documents
|
|
||||||
self._rebuild_index()
|
|
||||||
|
|
||||||
def get_memories(self, current_situation: str, n_matches: int = 1) -> List[dict]:
|
|
||||||
"""Find matching recommendations using BM25 similarity.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current_situation: The current financial situation to match against
|
|
||||||
n_matches: Number of top matches to return
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of dicts with matched_situation, recommendation, and similarity_score
|
|
||||||
"""
|
|
||||||
if not self.documents or self.bm25 is None:
|
|
||||||
return []
|
return []
|
||||||
|
text = self._log_path.read_text(encoding="utf-8")
|
||||||
|
raw_entries = [e.strip() for e in text.split(self._SEPARATOR) if e.strip()]
|
||||||
|
entries = []
|
||||||
|
for raw in raw_entries:
|
||||||
|
parsed = self._parse_entry(raw)
|
||||||
|
if parsed:
|
||||||
|
entries.append(parsed)
|
||||||
|
return entries
|
||||||
|
|
||||||
# Tokenize query
|
def get_pending_entries(self) -> List[dict]:
|
||||||
query_tokens = self._tokenize(current_situation)
|
"""Return entries with outcome:pending (for Phase B)."""
|
||||||
|
return [e for e in self.load_entries() if e.get("pending")]
|
||||||
|
|
||||||
# Get BM25 scores for all documents
|
def get_past_context(self, ticker: str, n_same: int = 5, n_cross: int = 3) -> str:
|
||||||
scores = self.bm25.get_scores(query_tokens)
|
"""Return formatted past context string for agent prompt injection."""
|
||||||
|
entries = [e for e in self.load_entries() if not e.get("pending")]
|
||||||
|
if not entries:
|
||||||
|
return ""
|
||||||
|
|
||||||
# Get top-n indices sorted by score (descending)
|
same, cross = [], []
|
||||||
top_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:n_matches]
|
for e in reversed(entries):
|
||||||
|
if len(same) >= n_same and len(cross) >= n_cross:
|
||||||
|
break
|
||||||
|
if e["ticker"] == ticker and len(same) < n_same:
|
||||||
|
same.append(e)
|
||||||
|
elif e["ticker"] != ticker and len(cross) < n_cross:
|
||||||
|
cross.append(e)
|
||||||
|
|
||||||
# Build results
|
if not same and not cross:
|
||||||
results = []
|
return ""
|
||||||
max_score = float(scores.max()) if len(scores) > 0 and scores.max() > 0 else 1.0
|
|
||||||
|
|
||||||
for idx in top_indices:
|
parts = []
|
||||||
# Normalize score to 0-1 range for consistency
|
if same:
|
||||||
normalized_score = scores[idx] / max_score if max_score > 0 else 0
|
parts.append(f"Past analyses of {ticker} (most recent first):")
|
||||||
results.append({
|
parts.extend(self._format_full(e) for e in same)
|
||||||
"matched_situation": self.documents[idx],
|
if cross:
|
||||||
"recommendation": self.recommendations[idx],
|
parts.append("Recent cross-ticker lessons:")
|
||||||
"similarity_score": normalized_score,
|
parts.extend(self._format_reflection_only(e) for e in cross)
|
||||||
})
|
return "\n\n".join(parts)
|
||||||
|
|
||||||
return results
|
# --- Update path (Phase B) ---
|
||||||
|
|
||||||
def clear(self):
|
def update_with_outcome(
|
||||||
"""Clear all stored memories."""
|
self,
|
||||||
self.documents = []
|
ticker: str,
|
||||||
self.recommendations = []
|
trade_date: str,
|
||||||
self.bm25 = None
|
raw_return: float,
|
||||||
|
alpha_return: float,
|
||||||
|
holding_days: int,
|
||||||
|
reflection: str,
|
||||||
|
) -> None:
|
||||||
|
"""Replace pending tag and append REFLECTION section using atomic write.
|
||||||
|
|
||||||
|
Finds the first pending entry matching (trade_date, ticker), updates
|
||||||
if __name__ == "__main__":
|
its tag with return figures, and appends a REFLECTION section. Uses
|
||||||
# Example usage
|
a temp-file + os.replace() so a crash mid-write never corrupts the log.
|
||||||
matcher = FinancialSituationMemory("test_memory")
|
|
||||||
|
|
||||||
# Example data
|
|
||||||
example_data = [
|
|
||||||
(
|
|
||||||
"High inflation rate with rising interest rates and declining consumer spending",
|
|
||||||
"Consider defensive sectors like consumer staples and utilities. Review fixed-income portfolio duration.",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Tech sector showing high volatility with increasing institutional selling pressure",
|
|
||||||
"Reduce exposure to high-growth tech stocks. Look for value opportunities in established tech companies with strong cash flows.",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Strong dollar affecting emerging markets with increasing forex volatility",
|
|
||||||
"Hedge currency exposure in international positions. Consider reducing allocation to emerging market debt.",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Market showing signs of sector rotation with rising yields",
|
|
||||||
"Rebalance portfolio to maintain target allocations. Consider increasing exposure to sectors benefiting from higher rates.",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add the example situations and recommendations
|
|
||||||
matcher.add_situations(example_data)
|
|
||||||
|
|
||||||
# Example query
|
|
||||||
current_situation = """
|
|
||||||
Market showing increased volatility in tech sector, with institutional investors
|
|
||||||
reducing positions and rising interest rates affecting growth stock valuations
|
|
||||||
"""
|
"""
|
||||||
|
if not self._log_path or not self._log_path.exists():
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
text = self._log_path.read_text(encoding="utf-8")
|
||||||
recommendations = matcher.get_memories(current_situation, n_matches=2)
|
blocks = text.split(self._SEPARATOR)
|
||||||
|
|
||||||
for i, rec in enumerate(recommendations, 1):
|
pending_prefix = f"[{trade_date} | {ticker} |"
|
||||||
print(f"\nMatch {i}:")
|
raw_pct = f"{raw_return:+.1%}"
|
||||||
print(f"Similarity Score: {rec['similarity_score']:.2f}")
|
alpha_pct = f"{alpha_return:+.1%}"
|
||||||
print(f"Matched Situation: {rec['matched_situation']}")
|
|
||||||
print(f"Recommendation: {rec['recommendation']}")
|
|
||||||
|
|
||||||
except Exception as e:
|
updated = False
|
||||||
print(f"Error during recommendation: {str(e)}")
|
new_blocks = []
|
||||||
|
for block in blocks:
|
||||||
|
stripped = block.strip()
|
||||||
|
if not stripped:
|
||||||
|
new_blocks.append(block)
|
||||||
|
continue
|
||||||
|
|
||||||
|
lines = stripped.splitlines()
|
||||||
|
tag_line = lines[0].strip()
|
||||||
|
|
||||||
|
if (
|
||||||
|
not updated
|
||||||
|
and tag_line.startswith(pending_prefix)
|
||||||
|
and tag_line.endswith("| pending]")
|
||||||
|
):
|
||||||
|
# Parse rating from the existing pending tag
|
||||||
|
fields = [f.strip() for f in tag_line[1:-1].split("|")]
|
||||||
|
rating = fields[2]
|
||||||
|
new_tag = (
|
||||||
|
f"[{trade_date} | {ticker} | {rating}"
|
||||||
|
f" | {raw_pct} | {alpha_pct} | {holding_days}d]"
|
||||||
|
)
|
||||||
|
rest = "\n".join(lines[1:])
|
||||||
|
new_blocks.append(
|
||||||
|
f"{new_tag}\n\n{rest.lstrip()}\n\nREFLECTION:\n{reflection}"
|
||||||
|
)
|
||||||
|
updated = True
|
||||||
|
else:
|
||||||
|
new_blocks.append(block)
|
||||||
|
|
||||||
|
if not updated:
|
||||||
|
return
|
||||||
|
|
||||||
|
new_text = self._SEPARATOR.join(new_blocks)
|
||||||
|
tmp_path = self._log_path.with_suffix(".tmp")
|
||||||
|
tmp_path.write_text(new_text, encoding="utf-8")
|
||||||
|
tmp_path.replace(self._log_path)
|
||||||
|
|
||||||
|
def batch_update_with_outcomes(self, updates: List[dict]) -> None:
|
||||||
|
"""Apply multiple outcome updates in a single read + atomic write.
|
||||||
|
|
||||||
|
Each element of updates must have keys: ticker, trade_date,
|
||||||
|
raw_return, alpha_return, holding_days, reflection.
|
||||||
|
"""
|
||||||
|
if not self._log_path or not self._log_path.exists() or not updates:
|
||||||
|
return
|
||||||
|
|
||||||
|
text = self._log_path.read_text(encoding="utf-8")
|
||||||
|
blocks = text.split(self._SEPARATOR)
|
||||||
|
|
||||||
|
# Build lookup keyed by (trade_date, ticker) for O(1) dispatch
|
||||||
|
update_map = {(u["trade_date"], u["ticker"]): u for u in updates}
|
||||||
|
|
||||||
|
new_blocks = []
|
||||||
|
for block in blocks:
|
||||||
|
stripped = block.strip()
|
||||||
|
if not stripped:
|
||||||
|
new_blocks.append(block)
|
||||||
|
continue
|
||||||
|
|
||||||
|
lines = stripped.splitlines()
|
||||||
|
tag_line = lines[0].strip()
|
||||||
|
|
||||||
|
matched = False
|
||||||
|
for (trade_date, ticker), upd in list(update_map.items()):
|
||||||
|
pending_prefix = f"[{trade_date} | {ticker} |"
|
||||||
|
if tag_line.startswith(pending_prefix) and tag_line.endswith("| pending]"):
|
||||||
|
fields = [f.strip() for f in tag_line[1:-1].split("|")]
|
||||||
|
rating = fields[2]
|
||||||
|
raw_pct = f"{upd['raw_return']:+.1%}"
|
||||||
|
alpha_pct = f"{upd['alpha_return']:+.1%}"
|
||||||
|
new_tag = (
|
||||||
|
f"[{trade_date} | {ticker} | {rating}"
|
||||||
|
f" | {raw_pct} | {alpha_pct} | {upd['holding_days']}d]"
|
||||||
|
)
|
||||||
|
rest = "\n".join(lines[1:])
|
||||||
|
new_blocks.append(
|
||||||
|
f"{new_tag}\n\n{rest.lstrip()}\n\nREFLECTION:\n{upd['reflection']}"
|
||||||
|
)
|
||||||
|
del update_map[(trade_date, ticker)]
|
||||||
|
matched = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not matched:
|
||||||
|
new_blocks.append(block)
|
||||||
|
|
||||||
|
new_text = self._SEPARATOR.join(new_blocks)
|
||||||
|
tmp_path = self._log_path.with_suffix(".tmp")
|
||||||
|
tmp_path.write_text(new_text, encoding="utf-8")
|
||||||
|
tmp_path.replace(self._log_path)
|
||||||
|
|
||||||
|
# --- Helpers ---
|
||||||
|
|
||||||
|
def _parse_rating(self, text: str) -> str:
|
||||||
|
# First pass: explicit "Rating: X" label — search handles markdown bold/numbered lists
|
||||||
|
for line in text.splitlines():
|
||||||
|
m = self._RATING_LABEL_RE.search(line)
|
||||||
|
if m and m.group(1).lower() in self.RATINGS:
|
||||||
|
return m.group(1).capitalize()
|
||||||
|
# Fallback: first rating word found anywhere in the text
|
||||||
|
for line in text.splitlines():
|
||||||
|
for word in line.lower().split():
|
||||||
|
clean = word.strip("*:.,")
|
||||||
|
if clean in self.RATINGS:
|
||||||
|
return clean.capitalize()
|
||||||
|
return "Hold"
|
||||||
|
|
||||||
|
def _parse_entry(self, raw: str) -> Optional[dict]:
|
||||||
|
lines = raw.strip().splitlines()
|
||||||
|
if not lines:
|
||||||
|
return None
|
||||||
|
tag_line = lines[0].strip()
|
||||||
|
if not (tag_line.startswith("[") and tag_line.endswith("]")):
|
||||||
|
return None
|
||||||
|
fields = [f.strip() for f in tag_line[1:-1].split("|")]
|
||||||
|
if len(fields) < 4:
|
||||||
|
return None
|
||||||
|
entry = {
|
||||||
|
"date": fields[0],
|
||||||
|
"ticker": fields[1],
|
||||||
|
"rating": fields[2],
|
||||||
|
"pending": fields[3] == "pending",
|
||||||
|
"raw": fields[3] if fields[3] != "pending" else None,
|
||||||
|
"alpha": fields[4] if len(fields) > 4 else None,
|
||||||
|
"holding": fields[5] if len(fields) > 5 else None,
|
||||||
|
}
|
||||||
|
body = "\n".join(lines[1:]).strip()
|
||||||
|
decision_match = self._DECISION_RE.search(body)
|
||||||
|
reflection_match = self._REFLECTION_RE.search(body)
|
||||||
|
entry["decision"] = decision_match.group(1).strip() if decision_match else ""
|
||||||
|
entry["reflection"] = reflection_match.group(1).strip() if reflection_match else ""
|
||||||
|
return entry
|
||||||
|
|
||||||
|
def _format_full(self, e: dict) -> str:
|
||||||
|
raw = e["raw"] or "n/a"
|
||||||
|
alpha = e["alpha"] or "n/a"
|
||||||
|
holding = e["holding"] or "n/a"
|
||||||
|
tag = f"[{e['date']} | {e['ticker']} | {e['rating']} | {raw} | {alpha} | {holding}]"
|
||||||
|
parts = [tag, f"DECISION:\n{e['decision']}"]
|
||||||
|
if e["reflection"]:
|
||||||
|
parts.append(f"REFLECTION:\n{e['reflection']}")
|
||||||
|
return "\n\n".join(parts)
|
||||||
|
|
||||||
|
def _format_reflection_only(self, e: dict) -> str:
|
||||||
|
tag = f"[{e['date']} | {e['ticker']} | {e['rating']} | {e['raw'] or 'n/a'}]"
|
||||||
|
if e["reflection"]:
|
||||||
|
return f"{tag}\n{e['reflection']}"
|
||||||
|
text = e["decision"][:300]
|
||||||
|
suffix = "..." if len(e["decision"]) > 300 else ""
|
||||||
|
return f"{tag}\n{text}{suffix}"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ DEFAULT_CONFIG = {
|
|||||||
"project_dir": os.path.abspath(os.path.join(os.path.dirname(__file__), ".")),
|
"project_dir": os.path.abspath(os.path.join(os.path.dirname(__file__), ".")),
|
||||||
"results_dir": os.getenv("TRADINGAGENTS_RESULTS_DIR", os.path.join(_TRADINGAGENTS_HOME, "logs")),
|
"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")),
|
"data_cache_dir": os.getenv("TRADINGAGENTS_CACHE_DIR", os.path.join(_TRADINGAGENTS_HOME, "cache")),
|
||||||
|
"memory_log_path": os.path.join(_TRADINGAGENTS_HOME, "memory", "trading_memory.md"),
|
||||||
# LLM settings
|
# LLM settings
|
||||||
"llm_provider": "openai",
|
"llm_provider": "openai",
|
||||||
"deep_think_llm": "gpt-5.4",
|
"deep_think_llm": "gpt-5.4",
|
||||||
|
|||||||
@@ -16,13 +16,14 @@ class Propagator:
|
|||||||
self.max_recur_limit = max_recur_limit
|
self.max_recur_limit = max_recur_limit
|
||||||
|
|
||||||
def create_initial_state(
|
def create_initial_state(
|
||||||
self, company_name: str, trade_date: str
|
self, company_name: str, trade_date: str, past_context: str = ""
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Create the initial state for the agent graph."""
|
"""Create the initial state for the agent graph."""
|
||||||
return {
|
return {
|
||||||
"messages": [("human", company_name)],
|
"messages": [("human", company_name)],
|
||||||
"company_of_interest": company_name,
|
"company_of_interest": company_name,
|
||||||
"trade_date": str(trade_date),
|
"trade_date": str(trade_date),
|
||||||
|
"past_context": past_context,
|
||||||
"investment_debate_state": InvestDebateState(
|
"investment_debate_state": InvestDebateState(
|
||||||
{
|
{
|
||||||
"bull_history": "",
|
"bull_history": "",
|
||||||
|
|||||||
@@ -1,120 +1,53 @@
|
|||||||
# TradingAgents/graph/reflection.py
|
# TradingAgents/graph/reflection.py
|
||||||
|
|
||||||
from typing import Any, Dict
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class Reflector:
|
class Reflector:
|
||||||
"""Handles reflection on decisions and updating memory."""
|
"""Handles reflection on trading decisions."""
|
||||||
|
|
||||||
def __init__(self, quick_thinking_llm: Any):
|
def __init__(self, quick_thinking_llm: Any):
|
||||||
"""Initialize the reflector with an LLM."""
|
"""Initialize the reflector with an LLM."""
|
||||||
self.quick_thinking_llm = quick_thinking_llm
|
self.quick_thinking_llm = quick_thinking_llm
|
||||||
self.reflection_system_prompt = self._get_reflection_prompt()
|
self.log_reflection_prompt = self._get_log_reflection_prompt()
|
||||||
|
|
||||||
def _get_reflection_prompt(self) -> str:
|
def _get_log_reflection_prompt(self) -> str:
|
||||||
"""Get the system prompt for reflection."""
|
"""Concise prompt for reflect_on_final_decision (Phase B log entries).
|
||||||
return """
|
|
||||||
You are an expert financial analyst tasked with reviewing trading decisions/analysis and providing a comprehensive, step-by-step analysis.
|
|
||||||
Your goal is to deliver detailed insights into investment decisions and highlight opportunities for improvement, adhering strictly to the following guidelines:
|
|
||||||
|
|
||||||
1. Reasoning:
|
Produces 2-4 sentences of plain prose — compact enough to be re-injected
|
||||||
- For each trading decision, determine whether it was correct or incorrect. A correct decision results in an increase in returns, while an incorrect decision does the opposite.
|
into future agent prompts without bloating the context window.
|
||||||
- Analyze the contributing factors to each success or mistake. Consider:
|
"""
|
||||||
- Market intelligence.
|
return (
|
||||||
- Technical indicators.
|
"You are a trading analyst reviewing your own past decision now that the outcome is known.\n"
|
||||||
- Technical signals.
|
"Write exactly 2-4 sentences of plain prose (no bullets, no headers, no markdown).\n\n"
|
||||||
- Price movement analysis.
|
"Cover in order:\n"
|
||||||
- Overall market data analysis
|
"1. Was the directional call correct? (cite the alpha figure)\n"
|
||||||
- News analysis.
|
"2. Which part of the investment thesis held or failed?\n"
|
||||||
- Social media and sentiment analysis.
|
"3. One concrete lesson to apply to the next similar analysis.\n\n"
|
||||||
- Fundamental data analysis.
|
"Be specific and terse. Your output will be stored verbatim in a decision log "
|
||||||
- Weight the importance of each factor in the decision-making process.
|
"and re-read by future analysts, so every word must earn its place."
|
||||||
|
)
|
||||||
|
|
||||||
2. Improvement:
|
def reflect_on_final_decision(
|
||||||
- For any incorrect decisions, propose revisions to maximize returns.
|
self,
|
||||||
- Provide a detailed list of corrective actions or improvements, including specific recommendations (e.g., changing a decision from HOLD to BUY on a particular date).
|
final_decision: str,
|
||||||
|
raw_return: float,
|
||||||
3. Summary:
|
alpha_return: float,
|
||||||
- Summarize the lessons learned from the successes and mistakes.
|
|
||||||
- Highlight how these lessons can be adapted for future trading scenarios and draw connections between similar situations to apply the knowledge gained.
|
|
||||||
|
|
||||||
4. Query:
|
|
||||||
- Extract key insights from the summary into a concise sentence of no more than 1000 tokens.
|
|
||||||
- Ensure the condensed sentence captures the essence of the lessons and reasoning for easy reference.
|
|
||||||
|
|
||||||
Adhere strictly to these instructions, and ensure your output is detailed, accurate, and actionable. You will also be given objective descriptions of the market from a price movements, technical indicator, news, and sentiment perspective to provide more context for your analysis.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _extract_current_situation(self, current_state: Dict[str, Any]) -> str:
|
|
||||||
"""Extract the current market situation from the state."""
|
|
||||||
curr_market_report = current_state["market_report"]
|
|
||||||
curr_sentiment_report = current_state["sentiment_report"]
|
|
||||||
curr_news_report = current_state["news_report"]
|
|
||||||
curr_fundamentals_report = current_state["fundamentals_report"]
|
|
||||||
|
|
||||||
return f"{curr_market_report}\n\n{curr_sentiment_report}\n\n{curr_news_report}\n\n{curr_fundamentals_report}"
|
|
||||||
|
|
||||||
def _reflect_on_component(
|
|
||||||
self, component_type: str, report: str, situation: str, returns_losses
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Generate reflection for a component."""
|
"""Single reflection call on the final trade decision with outcome context.
|
||||||
|
|
||||||
|
Used by Phase B deferred reflection. The final_trade_decision already
|
||||||
|
synthesises all analyst insights, so no separate market context is needed.
|
||||||
|
"""
|
||||||
messages = [
|
messages = [
|
||||||
("system", self.reflection_system_prompt),
|
("system", self.log_reflection_prompt),
|
||||||
(
|
(
|
||||||
"human",
|
"human",
|
||||||
f"Returns: {returns_losses}\n\nAnalysis/Decision: {report}\n\nObjective Market Reports for Reference: {situation}",
|
(
|
||||||
|
f"Raw return: {raw_return:+.1%}\n"
|
||||||
|
f"Alpha vs SPY: {alpha_return:+.1%}\n\n"
|
||||||
|
f"Final Decision:\n{final_decision}"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
return self.quick_thinking_llm.invoke(messages).content
|
||||||
result = self.quick_thinking_llm.invoke(messages).content
|
|
||||||
return result
|
|
||||||
|
|
||||||
def reflect_bull_researcher(self, current_state, returns_losses, bull_memory):
|
|
||||||
"""Reflect on bull researcher's analysis and update memory."""
|
|
||||||
situation = self._extract_current_situation(current_state)
|
|
||||||
bull_debate_history = current_state["investment_debate_state"]["bull_history"]
|
|
||||||
|
|
||||||
result = self._reflect_on_component(
|
|
||||||
"BULL", bull_debate_history, situation, returns_losses
|
|
||||||
)
|
|
||||||
bull_memory.add_situations([(situation, result)])
|
|
||||||
|
|
||||||
def reflect_bear_researcher(self, current_state, returns_losses, bear_memory):
|
|
||||||
"""Reflect on bear researcher's analysis and update memory."""
|
|
||||||
situation = self._extract_current_situation(current_state)
|
|
||||||
bear_debate_history = current_state["investment_debate_state"]["bear_history"]
|
|
||||||
|
|
||||||
result = self._reflect_on_component(
|
|
||||||
"BEAR", bear_debate_history, situation, returns_losses
|
|
||||||
)
|
|
||||||
bear_memory.add_situations([(situation, result)])
|
|
||||||
|
|
||||||
def reflect_trader(self, current_state, returns_losses, trader_memory):
|
|
||||||
"""Reflect on trader's decision and update memory."""
|
|
||||||
situation = self._extract_current_situation(current_state)
|
|
||||||
trader_decision = current_state["trader_investment_plan"]
|
|
||||||
|
|
||||||
result = self._reflect_on_component(
|
|
||||||
"TRADER", trader_decision, situation, returns_losses
|
|
||||||
)
|
|
||||||
trader_memory.add_situations([(situation, result)])
|
|
||||||
|
|
||||||
def reflect_invest_judge(self, current_state, returns_losses, invest_judge_memory):
|
|
||||||
"""Reflect on investment judge's decision and update memory."""
|
|
||||||
situation = self._extract_current_situation(current_state)
|
|
||||||
judge_decision = current_state["investment_debate_state"]["judge_decision"]
|
|
||||||
|
|
||||||
result = self._reflect_on_component(
|
|
||||||
"INVEST JUDGE", judge_decision, situation, returns_losses
|
|
||||||
)
|
|
||||||
invest_judge_memory.add_situations([(situation, result)])
|
|
||||||
|
|
||||||
def reflect_portfolio_manager(self, current_state, returns_losses, portfolio_manager_memory):
|
|
||||||
"""Reflect on portfolio manager's decision and update memory."""
|
|
||||||
situation = self._extract_current_situation(current_state)
|
|
||||||
judge_decision = current_state["risk_debate_state"]["judge_decision"]
|
|
||||||
|
|
||||||
result = self._reflect_on_component(
|
|
||||||
"PORTFOLIO MANAGER", judge_decision, situation, returns_losses
|
|
||||||
)
|
|
||||||
portfolio_manager_memory.add_situations([(situation, result)])
|
|
||||||
|
|||||||
@@ -18,22 +18,12 @@ class GraphSetup:
|
|||||||
quick_thinking_llm: Any,
|
quick_thinking_llm: Any,
|
||||||
deep_thinking_llm: Any,
|
deep_thinking_llm: Any,
|
||||||
tool_nodes: Dict[str, ToolNode],
|
tool_nodes: Dict[str, ToolNode],
|
||||||
bull_memory,
|
|
||||||
bear_memory,
|
|
||||||
trader_memory,
|
|
||||||
invest_judge_memory,
|
|
||||||
portfolio_manager_memory,
|
|
||||||
conditional_logic: ConditionalLogic,
|
conditional_logic: ConditionalLogic,
|
||||||
):
|
):
|
||||||
"""Initialize with required components."""
|
"""Initialize with required components."""
|
||||||
self.quick_thinking_llm = quick_thinking_llm
|
self.quick_thinking_llm = quick_thinking_llm
|
||||||
self.deep_thinking_llm = deep_thinking_llm
|
self.deep_thinking_llm = deep_thinking_llm
|
||||||
self.tool_nodes = tool_nodes
|
self.tool_nodes = tool_nodes
|
||||||
self.bull_memory = bull_memory
|
|
||||||
self.bear_memory = bear_memory
|
|
||||||
self.trader_memory = trader_memory
|
|
||||||
self.invest_judge_memory = invest_judge_memory
|
|
||||||
self.portfolio_manager_memory = portfolio_manager_memory
|
|
||||||
self.conditional_logic = conditional_logic
|
self.conditional_logic = conditional_logic
|
||||||
|
|
||||||
def setup_graph(
|
def setup_graph(
|
||||||
@@ -85,24 +75,16 @@ class GraphSetup:
|
|||||||
tool_nodes["fundamentals"] = self.tool_nodes["fundamentals"]
|
tool_nodes["fundamentals"] = self.tool_nodes["fundamentals"]
|
||||||
|
|
||||||
# Create researcher and manager nodes
|
# Create researcher and manager nodes
|
||||||
bull_researcher_node = create_bull_researcher(
|
bull_researcher_node = create_bull_researcher(self.quick_thinking_llm)
|
||||||
self.quick_thinking_llm, self.bull_memory
|
bear_researcher_node = create_bear_researcher(self.quick_thinking_llm)
|
||||||
)
|
research_manager_node = create_research_manager(self.deep_thinking_llm)
|
||||||
bear_researcher_node = create_bear_researcher(
|
trader_node = create_trader(self.quick_thinking_llm)
|
||||||
self.quick_thinking_llm, self.bear_memory
|
|
||||||
)
|
|
||||||
research_manager_node = create_research_manager(
|
|
||||||
self.deep_thinking_llm, self.invest_judge_memory
|
|
||||||
)
|
|
||||||
trader_node = create_trader(self.quick_thinking_llm, self.trader_memory)
|
|
||||||
|
|
||||||
# Create risk analysis nodes
|
# Create risk analysis nodes
|
||||||
aggressive_analyst = create_aggressive_debator(self.quick_thinking_llm)
|
aggressive_analyst = create_aggressive_debator(self.quick_thinking_llm)
|
||||||
neutral_analyst = create_neutral_debator(self.quick_thinking_llm)
|
neutral_analyst = create_neutral_debator(self.quick_thinking_llm)
|
||||||
conservative_analyst = create_conservative_debator(self.quick_thinking_llm)
|
conservative_analyst = create_conservative_debator(self.quick_thinking_llm)
|
||||||
portfolio_manager_node = create_portfolio_manager(
|
portfolio_manager_node = create_portfolio_manager(self.deep_thinking_llm)
|
||||||
self.deep_thinking_llm, self.portfolio_manager_memory
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create workflow
|
# Create workflow
|
||||||
workflow = StateGraph(AgentState)
|
workflow = StateGraph(AgentState)
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
# TradingAgents/graph/trading_graph.py
|
# TradingAgents/graph/trading_graph.py
|
||||||
|
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import json
|
import json
|
||||||
from datetime import date
|
from datetime import datetime, timedelta
|
||||||
from typing import Dict, Any, Tuple, List, Optional
|
from typing import Dict, Any, Tuple, List, Optional
|
||||||
|
|
||||||
|
import yfinance as yf
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
from langgraph.prebuilt import ToolNode
|
from langgraph.prebuilt import ToolNode
|
||||||
|
|
||||||
from tradingagents.llm_clients import create_llm_client
|
from tradingagents.llm_clients import create_llm_client
|
||||||
|
|
||||||
from tradingagents.agents import *
|
from tradingagents.agents import *
|
||||||
from tradingagents.default_config import DEFAULT_CONFIG
|
from tradingagents.default_config import DEFAULT_CONFIG
|
||||||
from tradingagents.agents.utils.memory import FinancialSituationMemory
|
from tradingagents.agents.utils.memory import TradingMemoryLog
|
||||||
from tradingagents.agents.utils.agent_states import (
|
from tradingagents.agents.utils.agent_states import (
|
||||||
AgentState,
|
AgentState,
|
||||||
InvestDebateState,
|
InvestDebateState,
|
||||||
@@ -92,12 +97,7 @@ class TradingAgentsGraph:
|
|||||||
self.deep_thinking_llm = deep_client.get_llm()
|
self.deep_thinking_llm = deep_client.get_llm()
|
||||||
self.quick_thinking_llm = quick_client.get_llm()
|
self.quick_thinking_llm = quick_client.get_llm()
|
||||||
|
|
||||||
# Initialize memories
|
self.memory_log = TradingMemoryLog(self.config)
|
||||||
self.bull_memory = FinancialSituationMemory("bull_memory", self.config)
|
|
||||||
self.bear_memory = FinancialSituationMemory("bear_memory", self.config)
|
|
||||||
self.trader_memory = FinancialSituationMemory("trader_memory", self.config)
|
|
||||||
self.invest_judge_memory = FinancialSituationMemory("invest_judge_memory", self.config)
|
|
||||||
self.portfolio_manager_memory = FinancialSituationMemory("portfolio_manager_memory", self.config)
|
|
||||||
|
|
||||||
# Create tool nodes
|
# Create tool nodes
|
||||||
self.tool_nodes = self._create_tool_nodes()
|
self.tool_nodes = self._create_tool_nodes()
|
||||||
@@ -111,11 +111,6 @@ class TradingAgentsGraph:
|
|||||||
self.quick_thinking_llm,
|
self.quick_thinking_llm,
|
||||||
self.deep_thinking_llm,
|
self.deep_thinking_llm,
|
||||||
self.tool_nodes,
|
self.tool_nodes,
|
||||||
self.bull_memory,
|
|
||||||
self.bear_memory,
|
|
||||||
self.trader_memory,
|
|
||||||
self.invest_judge_memory,
|
|
||||||
self.portfolio_manager_memory,
|
|
||||||
self.conditional_logic,
|
self.conditional_logic,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -189,14 +184,90 @@ class TradingAgentsGraph:
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _fetch_returns(
|
||||||
|
self, ticker: str, trade_date: str, holding_days: int = 5
|
||||||
|
) -> Tuple[Optional[float], Optional[float], Optional[int]]:
|
||||||
|
"""Fetch raw and alpha return for ticker over holding_days from trade_date.
|
||||||
|
|
||||||
|
Returns (raw_return, alpha_return, actual_holding_days) or
|
||||||
|
(None, None, None) if price data is unavailable (too recent, delisted,
|
||||||
|
or network error).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
start = datetime.strptime(trade_date, "%Y-%m-%d")
|
||||||
|
end = start + timedelta(days=holding_days + 7) # buffer for weekends/holidays
|
||||||
|
end_str = end.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
stock = yf.Ticker(ticker).history(start=trade_date, end=end_str)
|
||||||
|
spy = yf.Ticker("SPY").history(start=trade_date, end=end_str)
|
||||||
|
|
||||||
|
if len(stock) < 2 or len(spy) < 2:
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
actual_days = min(holding_days, len(stock) - 1, len(spy) - 1)
|
||||||
|
raw = float(
|
||||||
|
(stock["Close"].iloc[actual_days] - stock["Close"].iloc[0])
|
||||||
|
/ stock["Close"].iloc[0]
|
||||||
|
)
|
||||||
|
spy_ret = float(
|
||||||
|
(spy["Close"].iloc[actual_days] - spy["Close"].iloc[0])
|
||||||
|
/ spy["Close"].iloc[0]
|
||||||
|
)
|
||||||
|
alpha = raw - spy_ret
|
||||||
|
return raw, alpha, actual_days
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("_fetch_returns failed for %s@%s: %s", ticker, trade_date, e)
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
def _resolve_pending_entries(self, ticker: str) -> None:
|
||||||
|
"""Resolve pending log entries for ticker at the start of a new run.
|
||||||
|
|
||||||
|
Fetches returns for each same-ticker pending entry, generates reflections,
|
||||||
|
then writes all updates in a single atomic batch write to avoid redundant I/O.
|
||||||
|
Skips entries whose price data is not yet available (too recent or delisted).
|
||||||
|
|
||||||
|
Trade-off: only same-ticker entries are resolved per run. Entries for
|
||||||
|
other tickers accumulate until that ticker is run again.
|
||||||
|
"""
|
||||||
|
pending = [e for e in self.memory_log.get_pending_entries() if e["ticker"] == ticker]
|
||||||
|
if not pending:
|
||||||
|
return
|
||||||
|
|
||||||
|
updates = []
|
||||||
|
for entry in pending:
|
||||||
|
raw, alpha, days = self._fetch_returns(ticker, entry["date"])
|
||||||
|
if raw is None:
|
||||||
|
continue # price not available yet — try again next run
|
||||||
|
reflection = self.reflector.reflect_on_final_decision(
|
||||||
|
final_decision=entry.get("decision", ""),
|
||||||
|
raw_return=raw,
|
||||||
|
alpha_return=alpha,
|
||||||
|
)
|
||||||
|
updates.append({
|
||||||
|
"ticker": ticker,
|
||||||
|
"trade_date": entry["date"],
|
||||||
|
"raw_return": raw,
|
||||||
|
"alpha_return": alpha,
|
||||||
|
"holding_days": days,
|
||||||
|
"reflection": reflection,
|
||||||
|
})
|
||||||
|
|
||||||
|
if updates:
|
||||||
|
self.memory_log.batch_update_with_outcomes(updates)
|
||||||
|
|
||||||
def propagate(self, company_name, trade_date):
|
def propagate(self, company_name, trade_date):
|
||||||
"""Run the trading agents graph for a company on a specific date."""
|
"""Run the trading agents graph for a company on a specific date."""
|
||||||
|
|
||||||
self.ticker = company_name
|
self.ticker = company_name
|
||||||
|
|
||||||
# Initialize state
|
# Resolve any pending log entries for this ticker before the pipeline runs.
|
||||||
|
# This adds the outcome + reflection from the previous run at zero latency cost.
|
||||||
|
self._resolve_pending_entries(company_name)
|
||||||
|
|
||||||
|
# Initialize state — inject memory log context for PM
|
||||||
|
past_context = self.memory_log.get_past_context(company_name)
|
||||||
init_agent_state = self.propagator.create_initial_state(
|
init_agent_state = self.propagator.create_initial_state(
|
||||||
company_name, trade_date
|
company_name, trade_date, past_context=past_context
|
||||||
)
|
)
|
||||||
args = self.propagator.get_graph_args()
|
args = self.propagator.get_graph_args()
|
||||||
|
|
||||||
@@ -221,6 +292,13 @@ class TradingAgentsGraph:
|
|||||||
# Log state
|
# Log state
|
||||||
self._log_state(trade_date, final_state)
|
self._log_state(trade_date, final_state)
|
||||||
|
|
||||||
|
# Store decision for deferred reflection.
|
||||||
|
self.memory_log.store_decision(
|
||||||
|
ticker=company_name,
|
||||||
|
trade_date=trade_date,
|
||||||
|
final_trade_decision=final_state["final_trade_decision"],
|
||||||
|
)
|
||||||
|
|
||||||
# Return decision and processed signal
|
# Return decision and processed signal
|
||||||
return final_state, self.process_signal(final_state["final_trade_decision"])
|
return final_state, self.process_signal(final_state["final_trade_decision"])
|
||||||
|
|
||||||
@@ -264,24 +342,6 @@ class TradingAgentsGraph:
|
|||||||
with open(log_path, "w", encoding="utf-8") as f:
|
with open(log_path, "w", encoding="utf-8") as f:
|
||||||
json.dump(self.log_states_dict[str(trade_date)], f, indent=4)
|
json.dump(self.log_states_dict[str(trade_date)], f, indent=4)
|
||||||
|
|
||||||
def reflect_and_remember(self, returns_losses):
|
|
||||||
"""Reflect on decisions and update memory based on returns."""
|
|
||||||
self.reflector.reflect_bull_researcher(
|
|
||||||
self.curr_state, returns_losses, self.bull_memory
|
|
||||||
)
|
|
||||||
self.reflector.reflect_bear_researcher(
|
|
||||||
self.curr_state, returns_losses, self.bear_memory
|
|
||||||
)
|
|
||||||
self.reflector.reflect_trader(
|
|
||||||
self.curr_state, returns_losses, self.trader_memory
|
|
||||||
)
|
|
||||||
self.reflector.reflect_invest_judge(
|
|
||||||
self.curr_state, returns_losses, self.invest_judge_memory
|
|
||||||
)
|
|
||||||
self.reflector.reflect_portfolio_manager(
|
|
||||||
self.curr_state, returns_losses, self.portfolio_manager_memory
|
|
||||||
)
|
|
||||||
|
|
||||||
def process_signal(self, full_signal):
|
def process_signal(self, full_signal):
|
||||||
"""Process a signal to extract the core decision."""
|
"""Process a signal to extract the core decision."""
|
||||||
return self.signal_processor.process_signal(full_signal)
|
return self.signal_processor.process_signal(full_signal)
|
||||||
|
|||||||
Reference in New Issue
Block a user