TradingAgents/tradingagents/agents/analysts/market_analyst.py

202 lines
8.4 KiB
Python

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
import time
import json
import pandas as pd
from io import StringIO
from datetime import datetime, timedelta
from tradingagents.agents.utils.agent_utils import normalize_agent_output
from tradingagents.engines.regime_detector import RegimeDetector, DynamicIndicatorSelector
from tradingagents.utils.anonymizer import TickerAnonymizer
from tradingagents.utils.logger import app_logger as logger
from tradingagents.dataflows.config import get_config
def _calculate_net_insider_flow(raw_data: str) -> float:
"""Calculate net insider transaction value from report string."""
try:
if not raw_data or "Error" in raw_data or "No insider" in raw_data:
return 0.0
# Robust CSV parsing - YFinance uses whitespace delimiter
try:
df = pd.read_csv(StringIO(raw_data), sep='\s+', comment='#')
except:
# Fallback: auto-detect separator
df = pd.read_csv(StringIO(raw_data), sep=None, engine='python', comment='#')
# Standardize columns
df.columns = [c.strip().lower() for c in df.columns]
if 'value' not in df.columns:
return 0.0
net_flow = 0.0
# Iterate and sum
for _, row in df.iterrows():
# Check for sale/purchase in text or other columns
text = str(row.get('text', '')).lower() + str(row.get('transaction', '')).lower()
val = float(row['value']) if pd.notnull(row['value']) else 0.0
if 'sale' in text or 'sold' in text:
net_flow -= val
elif 'purchase' in text or 'buy' in text or 'bought' in text:
net_flow += val
return net_flow
except Exception as e:
logger.warning(f"Failed to parse insider flow: {e}")
return 0.0
def create_market_analyst(llm):
# PARANOIA CHECK: Ensure we aren't passing a bind_tools wrapped LLM
if hasattr(llm, "tools") and llm.tools:
logger.critical("SECURITY VIOLATION: Market Analyst has access to tools! This violates Phase 1 Architecture.")
def market_analyst_node(state):
logger.info(f">>> STARTING MARKET ANALYST for {state.get('company_of_interest')} <<<")
current_date = state["trade_date"]
# 1. READ FROM LEDGER (No Tool Calls)
ledger = state.get("fact_ledger")
if not ledger:
raise RuntimeError("CRITICAL: Market Analyst woke up but FactLedger is missing! Registrar failed.")
# Extract Canonically Fetched Data
raw_price_data = ledger.get("price_data")
raw_insider_data = ledger.get("insider_data")
# Initialize default state
report = "Market Analysis Initialized..."
regime_val = "UNKNOWN (Start)"
metrics = {"volatility": 0.0}
broad_market_regime = "UNKNOWN (Initialized)"
net_insider_flow = 0.0
volatility_score = 0.0
result = None
try:
# Re-initialize or reload anonymizer state
anonymizer = TickerAnonymizer()
real_ticker = state["company_of_interest"]
ticker = anonymizer.anonymize_ticker(real_ticker)
optimal_params = {}
regime_context = "REGIME DETECTION FAILED or DATA UNAVAILABLE"
# --- PROCESS LEDGER DATA ---
try:
# RegimeDetector now handles all input types (DataFrame, Series, CSV String)
# Just pass the raw data directly - no need to parse here
if raw_price_data:
regime, metrics = RegimeDetector.detect_regime(raw_price_data)
regime_val = regime.value if hasattr(regime, "value") else str(regime)
# Dynamic Tuning
overrides = {}
try:
config_path = get_config().get("runtime_config_relative_path", "data_cache/runtime_config.json")
import os
if os.path.exists(config_path):
with open(config_path, 'r') as f:
overrides = json.load(f)
except:
pass
optimal_params = DynamicIndicatorSelector.get_optimal_parameters(regime, overrides)
volatility_score = metrics.get("volatility", 0.0)
logger.info(f"SUCCESS: Detected Regime: {regime_val}")
# Construct Context
regime_context = f"MARKET REGIME DETECTED: {regime_val}\n"
# Escape Braces for LangChain
metrics_str = json.dumps(metrics).replace("{", "{{").replace("}", "}}")
regime_context += f"METRICS: {metrics_str}\n"
regime_context += f"RECOMMENDED STRATEGY: {optimal_params.get('strategy', 'N/A')}\n"
else:
regime_val = "UNKNOWN (Ledger Data Empty/Error)"
except Exception as e:
logger.warning(f"Regime detection failed from Ledger: {e}")
# DEBUG: Print raw data on failure
if isinstance(raw_price_data, str):
print(f"DEBUG: Parsing Failed. Raw Data Start: {raw_price_data[:250]}...")
regime_val = f"UNKNOWN (Error: {str(e)})"
# --- PROCESS INSIDER DATA ---
try:
# We trust the ledger's insider data
if isinstance(raw_insider_data, str):
net_insider_flow = _calculate_net_insider_flow(raw_insider_data)
logger.info(f"Insider Net Flow calculated from Ledger: ${net_insider_flow:,.2f}")
except Exception as e_ins:
net_insider_flow = 0.0
# --- LLM CALL (NO TOOLS) ---
system_message = (
f"""ROLE: Quantitative Technical Analyst.
CONTEXT: You are analyzing an ANONYMIZED ASSET (ASSET_XXX).
DATA SOURCE: TRUSTED FACT LEDGER ID {ledger.get('ledger_id', 'UNKNOWN')}.
DYNAMIC MARKET REGIME CONTEXT:
{regime_context}
TASK: Write a technical analysis report based on the PROVIDED DATA.
DO NOT ATTEMPT TO CALL TOOLS. YOU HAVE NO TOOLS.
Analyze the trends, volatility, and insider flow based on the metrics provided above.
INDICATOR GUIDANCE:
Use the regime metrics (volatility, slope, adx) to infer the technical state.
STRICT COMPLIANCE:
1. DO NOT HALLUCINATE DATA not present in the context.
2. Cite "FactLedger" as your source.
3. If data is missing, state "Insufficient Data".
Make sure to append a Markdown table at the end of the report."""
)
prompt = ChatPromptTemplate.from_messages(
[
("system", system_message),
MessagesPlaceholder(variable_name="messages"),
]
)
# NOTE: NO BIND TOOLS
chain = prompt | llm
# Fix: Must pass dict to Chain when using MessagesPlaceholder
result = chain.invoke({"messages": state["messages"]})
report = result.content
except Exception as e_fatal:
logger.critical(f"CRITICAL ERROR in Market Analyst Node: {e_fatal}")
if "UNKNOWN" in str(regime_val):
regime_val = f"UNKNOWN (Fatal Crash: {str(e_fatal)})"
report = f"Market Analyst Node crashed: {e_fatal}"
risk_multiplier = 0.5
# --- ALPHA CALCULATOR ---
if "risk_multiplier" not in locals(): risk_multiplier = 1.0
# Simple Regime Logic (since we lost live broad market for now)
if "TRENDING_UP" in str(regime_val).upper():
risk_multiplier = 1.2
elif "TRENDING_DOWN" in str(regime_val).upper():
risk_multiplier = 0.0
elif "VOLATILE" in str(regime_val).upper():
risk_multiplier = 0.5
return {
"messages": [result] if result else [],
"market_report": normalize_agent_output(report),
"market_regime": regime_val,
"regime_metrics": metrics,
"volatility_score": volatility_score,
"broad_market_regime": broad_market_regime,
"net_insider_flow": net_insider_flow,
"risk_multiplier": risk_multiplier
}
return market_analyst_node