mirror of
https://github.com/farcasclaudiu/TradingAgents.git
synced 2026-06-22 09:01:24 +03:00
The previous per-agent BM25 memory was effectively dead code — its only caller was a commented-out line in main.py. Replace it with a single append-only markdown decision log driven by the propagate() lifecycle. Lifecycle: - store_decision() appends a pending entry at the end of every run - _resolve_pending_entries() runs at the start of the next same-ticker run, fetches yfinance returns + alpha vs SPY, and writes one LLM reflection per resolved entry through an atomic temp-file rename - Portfolio Manager consumes state["past_context"] (5 most recent same-ticker entries plus 3 cross-ticker reflection-only excerpts) Storage at ~/.tradingagents/memory/trading_memory.md (override: TRADINGAGENTS_MEMORY_LOG_PATH). Tag schema: - Pending: [YYYY-MM-DD | TICKER | Rating | pending] - Resolved: [YYYY-MM-DD | TICKER | Rating | +X.X% | +Y.Y% | Nd] Removes rank-bm25 dependency and the legacy reflect_and_remember() plumbing across reflection.py, trading_graph.py, and the agent factories. 49 new tests in tests/test_memory_log.py cover the storage, deferred reflection, prompt injection, and legacy-removal paths. Full suite (58 tests) passes in under 2 seconds without API keys.
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
from .utils.agent_utils import create_msg_delete
|
||||
from .utils.agent_states import AgentState, InvestDebateState, RiskDebateState
|
||||
from .utils.memory import FinancialSituationMemory
|
||||
|
||||
from .analysts.fundamentals_analyst import create_fundamentals_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
|
||||
|
||||
__all__ = [
|
||||
"FinancialSituationMemory",
|
||||
"AgentState",
|
||||
"create_msg_delete",
|
||||
"InvestDebateState",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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:
|
||||
|
||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
||||
@@ -15,21 +15,14 @@ def create_portfolio_manager(llm, memory):
|
||||
research_plan = state["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_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"
|
||||
|
||||
past_context = state.get("past_context", "")
|
||||
lessons_line = (
|
||||
f"- Lessons from past decisions: **{past_memory_str.strip()}**\n"
|
||||
if past_memories
|
||||
else ""
|
||||
f"- Lessons from prior decisions and outcomes:\n{past_context}\n"
|
||||
if past_context else ""
|
||||
)
|
||||
thesis_instruction = (
|
||||
"3. **Investment Thesis**: Detailed reasoning anchored in the analysts' debate and past reflections."
|
||||
if past_memories
|
||||
"3. **Investment Thesis**: Detailed reasoning anchored in the analysts' debate and the lessons from prior decisions."
|
||||
if past_context
|
||||
else "3. **Investment Thesis**: Detailed reasoning anchored in the analysts' debate."
|
||||
)
|
||||
|
||||
@@ -50,6 +43,7 @@ def create_portfolio_manager(llm, memory):
|
||||
- Research Manager's investment plan: **{research_plan}**
|
||||
- Trader's transaction proposal: **{trader_plan}**
|
||||
{lessons_line}
|
||||
|
||||
**Required Output Structure:**
|
||||
1. **Rating**: State one of Buy / Overweight / Hold / Underweight / Sell.
|
||||
2. **Executive Summary**: A concise action plan covering entry strategy, position sizing, key risk levels, and time horizon.
|
||||
|
||||
@@ -2,30 +2,13 @@
|
||||
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:
|
||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
||||
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"]
|
||||
|
||||
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"
|
||||
|
||||
past_memory_block = (
|
||||
f'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. \n\nHere are your past reflections on mistakes:\n"{past_memory_str.strip()}"\n\n'
|
||||
if past_memories
|
||||
else ""
|
||||
)
|
||||
|
||||
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.
|
||||
@@ -35,7 +18,9 @@ Additionally, develop a detailed investment plan for the trader. This should inc
|
||||
Your Recommendation: A decisive stance supported by the most convincing arguments.
|
||||
Rationale: An explanation of why these arguments lead to your conclusion.
|
||||
Strategic Actions: Concrete steps for implementing the recommendation.
|
||||
{past_memory_block}{instrument_context}
|
||||
Present your analysis conversationally, as if speaking naturally, without special formatting.
|
||||
|
||||
{instrument_context}
|
||||
|
||||
Here is the debate:
|
||||
Debate History:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
def create_bear_researcher(llm, memory):
|
||||
def create_bear_researcher(llm):
|
||||
def bear_node(state) -> dict:
|
||||
investment_debate_state = state["investment_debate_state"]
|
||||
history = investment_debate_state.get("history", "")
|
||||
@@ -12,24 +12,6 @@ def create_bear_researcher(llm, memory):
|
||||
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 = ""
|
||||
for i, rec in enumerate(past_memories, 1):
|
||||
past_memory_str += rec["recommendation"] + "\n\n"
|
||||
|
||||
memory_section = (
|
||||
f"Reflections from similar situations and lessons learned: {past_memory_str.strip()}\n"
|
||||
if past_memories
|
||||
else ""
|
||||
)
|
||||
memory_instruction = (
|
||||
" You must also address reflections and learn from lessons and mistakes you made in the past."
|
||||
if past_memories
|
||||
else ""
|
||||
)
|
||||
|
||||
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:
|
||||
@@ -48,7 +30,7 @@ Latest world affairs news: {news_report}
|
||||
Company fundamentals report: {fundamentals_report}
|
||||
Conversation history of the debate: {history}
|
||||
Last bull argument: {current_response}
|
||||
{memory_section}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.{memory_instruction}
|
||||
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.
|
||||
"""
|
||||
|
||||
response = llm.invoke(prompt)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
def create_bull_researcher(llm, memory):
|
||||
def create_bull_researcher(llm):
|
||||
def bull_node(state) -> dict:
|
||||
investment_debate_state = state["investment_debate_state"]
|
||||
history = investment_debate_state.get("history", "")
|
||||
@@ -12,24 +12,6 @@ def create_bull_researcher(llm, memory):
|
||||
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 = ""
|
||||
for i, rec in enumerate(past_memories, 1):
|
||||
past_memory_str += rec["recommendation"] + "\n\n"
|
||||
|
||||
memory_section = (
|
||||
f"Reflections from similar situations and lessons learned: {past_memory_str.strip()}\n"
|
||||
if past_memories
|
||||
else ""
|
||||
)
|
||||
memory_instruction = (
|
||||
" You must also address reflections and learn from lessons and mistakes you made in the past."
|
||||
if past_memories
|
||||
else ""
|
||||
)
|
||||
|
||||
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:
|
||||
@@ -46,7 +28,7 @@ Latest world affairs news: {news_report}
|
||||
Company fundamentals report: {fundamentals_report}
|
||||
Conversation history of the debate: {history}
|
||||
Last bear argument: {current_response}
|
||||
{memory_section}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.{memory_instruction}
|
||||
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.
|
||||
"""
|
||||
|
||||
response = llm.invoke(prompt)
|
||||
|
||||
@@ -3,29 +3,11 @@ import functools
|
||||
from tradingagents.agents.utils.agent_utils import build_instrument_context
|
||||
|
||||
|
||||
def create_trader(llm, memory):
|
||||
def create_trader(llm):
|
||||
def trader_node(state, name):
|
||||
company_name = state["company_of_interest"]
|
||||
instrument_context = build_instrument_context(company_name)
|
||||
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"
|
||||
|
||||
memory_instruction = (
|
||||
f" 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.strip()}"
|
||||
if past_memories
|
||||
else ""
|
||||
)
|
||||
|
||||
context = {
|
||||
"role": "user",
|
||||
@@ -35,7 +17,7 @@ def create_trader(llm, memory):
|
||||
messages = [
|
||||
{
|
||||
"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.{memory_instruction}",
|
||||
"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,
|
||||
]
|
||||
|
||||
@@ -70,3 +70,4 @@ class AgentState(MessagesState):
|
||||
RiskDebateState, "Current state of the debate on evaluating risk"
|
||||
]
|
||||
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,
|
||||
no token limits, works offline with any LLM provider.
|
||||
"""
|
||||
|
||||
from rank_bm25 import BM25Okapi
|
||||
from typing import List, Tuple
|
||||
from typing import List, Optional
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
class FinancialSituationMemory:
|
||||
"""Memory system for storing and retrieving financial situations using BM25."""
|
||||
class TradingMemoryLog:
|
||||
"""Append-only markdown log of trading decisions and reflections."""
|
||||
|
||||
def __init__(self, name: str, config: dict = None):
|
||||
"""Initialize the memory system.
|
||||
RATINGS = {"buy", "overweight", "hold", "underweight", "sell"}
|
||||
# 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:
|
||||
name: Name identifier for this memory instance
|
||||
config: Configuration dict (kept for API compatibility, not used for BM25)
|
||||
"""
|
||||
self.name = name
|
||||
self.documents: List[str] = []
|
||||
self.recommendations: List[str] = []
|
||||
self.bm25 = None
|
||||
def __init__(self, config: dict = None):
|
||||
self._log_path = None
|
||||
path = (config or {}).get("memory_log_path")
|
||||
if path:
|
||||
self._log_path = Path(path).expanduser()
|
||||
self._log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _tokenize(self, text: str) -> List[str]:
|
||||
"""Tokenize text for BM25 indexing.
|
||||
# --- Write path (Phase A) ---
|
||||
|
||||
Simple whitespace + punctuation tokenization with lowercasing.
|
||||
"""
|
||||
# Lowercase and split on non-alphanumeric characters
|
||||
tokens = re.findall(r'\b\w+\b', text.lower())
|
||||
return tokens
|
||||
def store_decision(
|
||||
self,
|
||||
ticker: str,
|
||||
trade_date: str,
|
||||
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):
|
||||
"""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
|
||||
# --- Read path (Phase A) ---
|
||||
|
||||
def add_situations(self, situations_and_advice: List[Tuple[str, str]]):
|
||||
"""Add financial situations and their corresponding advice.
|
||||
|
||||
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:
|
||||
def load_entries(self) -> List[dict]:
|
||||
"""Parse all entries from log. Returns list of dicts."""
|
||||
if not self._log_path or not self._log_path.exists():
|
||||
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
|
||||
query_tokens = self._tokenize(current_situation)
|
||||
def get_pending_entries(self) -> List[dict]:
|
||||
"""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
|
||||
scores = self.bm25.get_scores(query_tokens)
|
||||
def get_past_context(self, ticker: str, n_same: int = 5, n_cross: int = 3) -> str:
|
||||
"""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)
|
||||
top_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:n_matches]
|
||||
same, cross = [], []
|
||||
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
|
||||
results = []
|
||||
max_score = float(scores.max()) if len(scores) > 0 and scores.max() > 0 else 1.0
|
||||
if not same and not cross:
|
||||
return ""
|
||||
|
||||
for idx in top_indices:
|
||||
# Normalize score to 0-1 range for consistency
|
||||
normalized_score = scores[idx] / max_score if max_score > 0 else 0
|
||||
results.append({
|
||||
"matched_situation": self.documents[idx],
|
||||
"recommendation": self.recommendations[idx],
|
||||
"similarity_score": normalized_score,
|
||||
})
|
||||
parts = []
|
||||
if same:
|
||||
parts.append(f"Past analyses of {ticker} (most recent first):")
|
||||
parts.extend(self._format_full(e) for e in same)
|
||||
if cross:
|
||||
parts.append("Recent cross-ticker lessons:")
|
||||
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):
|
||||
"""Clear all stored memories."""
|
||||
self.documents = []
|
||||
self.recommendations = []
|
||||
self.bm25 = None
|
||||
def update_with_outcome(
|
||||
self,
|
||||
ticker: str,
|
||||
trade_date: str,
|
||||
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
|
||||
its tag with return figures, and appends a REFLECTION section. Uses
|
||||
a temp-file + os.replace() so a crash mid-write never corrupts the log.
|
||||
"""
|
||||
if not self._log_path or not self._log_path.exists():
|
||||
return
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
matcher = FinancialSituationMemory("test_memory")
|
||||
text = self._log_path.read_text(encoding="utf-8")
|
||||
blocks = text.split(self._SEPARATOR)
|
||||
|
||||
# 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.",
|
||||
),
|
||||
]
|
||||
pending_prefix = f"[{trade_date} | {ticker} |"
|
||||
raw_pct = f"{raw_return:+.1%}"
|
||||
alpha_pct = f"{alpha_return:+.1%}"
|
||||
|
||||
# Add the example situations and recommendations
|
||||
matcher.add_situations(example_data)
|
||||
updated = False
|
||||
new_blocks = []
|
||||
for block in blocks:
|
||||
stripped = block.strip()
|
||||
if not stripped:
|
||||
new_blocks.append(block)
|
||||
continue
|
||||
|
||||
# Example query
|
||||
current_situation = """
|
||||
Market showing increased volatility in tech sector, with institutional investors
|
||||
reducing positions and rising interest rates affecting growth stock valuations
|
||||
"""
|
||||
lines = stripped.splitlines()
|
||||
tag_line = lines[0].strip()
|
||||
|
||||
try:
|
||||
recommendations = matcher.get_memories(current_situation, n_matches=2)
|
||||
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)
|
||||
|
||||
for i, rec in enumerate(recommendations, 1):
|
||||
print(f"\nMatch {i}:")
|
||||
print(f"Similarity Score: {rec['similarity_score']:.2f}")
|
||||
print(f"Matched Situation: {rec['matched_situation']}")
|
||||
print(f"Recommendation: {rec['recommendation']}")
|
||||
if not updated:
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during recommendation: {str(e)}")
|
||||
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}"
|
||||
|
||||
Reference in New Issue
Block a user