From 1f279a9df2d76e8bb0e1745e0938a217719f6c3a Mon Sep 17 00:00:00 2001 From: "swj.premkumar" Date: Tue, 13 Jan 2026 05:27:24 -0600 Subject: [PATCH] - **Insider Veto Protocol (Rule B)**: Hard-coded safety gate in `trading_graph.py` that blocks ALL buy signals if Net Insider Selling exceeds $50M while the stock is in a technical downtrend (Price < 50 SMA). This prevents "Falling Knife" catches. - **Relative Strength Determinism**: Upgraded `market_analyst.py` to calculate a mathematical `risk_multiplier` (0.0x - 1.5x) based on the Asset Regime vs. SPY Regime correlation, removing LLM "confidence" hallucinations from position sizing. - **Portfolio Awareness (Rule 72)**: Implemented State Persistence (`portfolio`, `cash_balance`) and a hard-coded Stop Loss check in `trading_graph.py`. If a position's unrealized PnL drops below -10%, the system forces a "LIQUIDATE" order, bypassing all AI debate. - **Self-Tuning Architecture**: Updated `reflection.py` to output a structured JSON block (`UPDATE_PARAMETERS`) instead of prose advice, enabling future automated parameter optimization. --- CHANGELOG.md | 10 ++ SYSTEM_RULE_BOOK.md | 97 +++++++++++++++- cli/utils.py | 6 +- startEmbedding.sh | 1 + test_embedding_connection.py | 32 ++++++ tests/test_google_api.py | 3 +- .../agents/analysts/market_analyst.py | 106 ++++++++++++++++-- tradingagents/agents/utils/agent_states.py | 15 +++ tradingagents/dataflows/alpha_vantage_news.py | 2 +- tradingagents/dataflows/interface.py | 30 ++--- tradingagents/dataflows/y_finance.py | 4 +- tradingagents/graph/conditional_logic.py | 8 +- tradingagents/graph/propagation.py | 3 + tradingagents/graph/reflection.py | 81 ++++++++----- tradingagents/graph/setup.py | 68 +++++++---- tradingagents/graph/trading_graph.py | 72 +++++++++++- 16 files changed, 453 insertions(+), 85 deletions(-) create mode 100644 test_embedding_connection.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 24184a7d..697d7fc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,23 @@ All notable changes to the **TradingAgents** project will be documented in this ## [Unreleased] - 2026-01-11 ### Added +- **Insider Veto Protocol (Rule B)**: Hard-coded safety gate in `trading_graph.py` that blocks ALL buy signals if Net Insider Selling exceeds $50M while the stock is in a technical downtrend (Price < 50 SMA). This prevents "Falling Knife" catches. +- **Relative Strength Determinism**: Upgraded `market_analyst.py` to calculate a mathematical `risk_multiplier` (0.0x - 1.5x) based on the Asset Regime vs. SPY Regime correlation, removing LLM "confidence" hallucinations from position sizing. +- **Portfolio Awareness (Rule 72)**: Implemented State Persistence (`portfolio`, `cash_balance`) and a hard-coded Stop Loss check in `trading_graph.py`. If a position's unrealized PnL drops below -10%, the system forces a "LIQUIDATE" order, bypassing all AI debate. +- **Self-Tuning Architecture**: Updated `reflection.py` to output a structured JSON block (`UPDATE_PARAMETERS`) instead of prose advice, enabling future automated parameter optimization. - **Gemini 2.0 & 3.0 Support**: Updated `cli/utils.py` to support `gemini-2.0-flash`, `gemini-2.5-flash-lite`, `gemini-2.5-pro`, `gemini-3-flash-preview` and `gemini-3-pro-preview` models. - **Console Debugging**: Added explicit console print statements for critical "Smoking Gun" debug traces in `market_analyst.py` and `trading_graph.py`. +### Changed +- **Mandatory Regime Detection**: Modified `graph/setup.py` to Force-Execute the `Market Analyst` node as the first step in every workflow. This permanently fixes the "UNKNOWN Regime" bug by ensuring context is established before any fundamental analysis begins. +- **Data Robustness**: Patched `y_finance.py` and `alpha_vantage_news.py` to accept `**kwargs` and `curr_date`, resolving crashes in the `route_to_vendor` pipeline when passing standardized arguments. + + ### Fixed - **Override Logic Mismatches**: Fixed critical Enum-to-String type mismatch in `apply_trend_override` that was silencing the "Safety Valve" logic. - **Data Pipeline Failures**: Injected robust error handling and type checking in `market_analyst.py` to identify why `RegimeDetector` receives invalid data (causing "UNKNOWN" regimes). - **Gemini 404 Errors**: Removed invalid/deprecated model names causing 404s. +- **Reflector Regime Integration**: Updated `reflection.py` to incorporate market regime context, ensuring post-trade analysis understands the 'Why' behind regime-based decisions. ## [Unreleased] - 2026-01-10 diff --git a/SYSTEM_RULE_BOOK.md b/SYSTEM_RULE_BOOK.md index a1a5ce5d..9fd7648c 100644 --- a/SYSTEM_RULE_BOOK.md +++ b/SYSTEM_RULE_BOOK.md @@ -179,4 +179,99 @@ Here is how the system handles specific market environments compared to a standa * **In Bear Markets:** We trust the Math. Valuation is everything. * **In Uncertainty:** We trust Cash. -**This architecture ensures you never miss a bubble, but you never hold the bag when it pops.** \ No newline at end of file +**This architecture ensures you never miss a bubble, but you never hold the bag when it pops.** + +## SYSTEM DECISION FLOW DIAGRAM +The following diagram illustrates the hard-coded logic gates that govern trade execution. + +```mermaid +graph TD + A[Start] --> B[Market Analyst Node] + B --> C{Detect Regime} + + C -- TRENDING_UP --> D[Calculate Relative Strength] + C -- SIDEWAYS --> D + C -- TRENDING_DOWN --> D + + D --> E[Assign Risk Multiplier] + E --> F[Fundamental Analysis] + F --> G[LLM Debate & Report] + G --> H[Preliminary Decision: BUY/SELL/HOLD] + + H --> I{Trend Override Gate} + + I -- Signal: SELL --> J{Is Growth > 30% AND Price > 200SMA?} + J -- YES --> K[Force HOLD: Don't Fight Tape] + J -- NO --> L[Allow SELL] + + I -- Signal: BUY --> M{Insider Veto Gate} + + M -- Net Selling > $50M --> N{Is Price < 50SMA?} + N -- YES --> O[BLOCK BUY: Falling Knife] + N -- NO --> P[Allow BUY] + + L --> Q[Execution] + K --> Q + O --> Q + P --> Q + + Q --> R{Active Portfolio Check} + R -- Position Exists --> S[Calculate Unrealized PnL] + S --> T{Is PnL < -10%?} + T -- YES --> U[FORCE LIQUIDATE: Rule 72] + T -- NO --> V[Maintain State] +``` + +## SCENARIO LOGIC MATRIX +How the system handles specific market conditions: + +| Scenario | Market Regime (SPY) | Asset Regime | Insider Action | Hard Gate Triggered | System Decision | +| :--- | :--- | :--- | :--- | :--- | :--- | +| **"The Bubble Riding"** (e.g. NVDA '23) | UPTREND | UPTREND (Price > SMA) | Selling (Profit Taking) | **Trend Override (Anti-Short)** | **HOLD / BUY** (Ignore valuation fears) | +| **"The Falling Knife"** (e.g. ZOOM '22) | DOWNTREND | DOWNTREND (Price < SMA) | Selling (> $50M) | **Insider Veto** | **BLOCK BUY** (Force Wait) | +| **"The Fake Breakout"** (Bear Market Rally) | DOWNTREND | UPTREND (Weak) | Neutral | Relative Strength = 0.8x | **REDUCE SIZE** (Caution) | +| **"The Crash"** (Portfolio Danger) | VOLATILE | VOLATILE | N/A | **Rule 72 (Stop Loss)** | **LIQUIDATE** (PnL < -10%) | +| **"The Boring Chop"** (Accumulation) | SIDEWAYS | SIDEWAYS | Buying | None | **Trade Range** (Buy Support) | + +### SCENARIO VISUALIZATION +```mermaid +graph TD + %% Define Scenarios + subgraph "Scenario A: The Bubble (PLTR/NVDA)" + A1[Market: UP] --> A2[Asset: UP] + A2 --> A3{Valuation High?} + A3 -- YES --> A4[Analyst: SELL] + A4 --> A5{Rules Check} + A5 -- Growth > 30% --> A6[OVERRIDE: FORCE HOLD] + end + + subgraph "Scenario B: The Falling Knife (ZOOM)" + B1[Market: DOWN] --> B2[Asset: DOWN] + B2 --> B3{Insider Action?} + B3 -- Net Selling > $50M --> B4[VETO: BLOCK BUY] + end + + subgraph "Scenario C: The Crash (Survival)" + C1[Active Position] --> C2{Check PnL} + C2 -- Loss > -10% --> C3[STOP LOSS TRIGGERED] + C3 --> C4[LIQUIDATE IMMEDIATE] + end + + style A6 fill:#4caf50,stroke:#333,stroke-width:2px + style B4 fill:#f44336,stroke:#333,stroke-width:2px + style C4 fill:#f44336,stroke:#333,stroke-width:2px +``` + +### Scenario Logic Breakdown + +* **Scenario A (The Momentum Exception):** + * **The Conflict:** The Analyst sees a high P/E ratio and screams "Sell!". + * **The Resolution:** The Hard Gate checks Growth > 30%. Since this is true, it overrides the "Sell" signal to a HOLD, preventing you from exiting a winner too early. + +* **Scenario B (The Insider Veto):** + * **The Conflict:** The price has dropped, and the Analyst thinks it's a "value buy." + * **The Resolution:** The Hard Gate checks Net Insider Flow. Seeing >$50M in selling during a downtrend, it activates the VETO, blocking the Buy order to prevent catching a falling knife. + +* **Scenario C (The Stop Loss):** + * **The Conflict:** A position is bleeding, but the Analyst (Bull) hopes for a rebound. + * **The Resolution:** The State Monitor sees Unrealized PnL < -10%. It bypasses the Analyst entirely and issues a forced LIQUIDATE command to preserve capital. \ No newline at end of file diff --git a/cli/utils.py b/cli/utils.py index c71d50cb..0722198e 100644 --- a/cli/utils.py +++ b/cli/utils.py @@ -141,8 +141,9 @@ def select_shallow_thinking_agent(provider) -> str: "google": [ ("Gemini 1.5 Flash - Cost efficiency and low latency", "gemini-1.5-flash"), ("Gemini 2.0 Flash - Next generation features and speed", "gemini-2.0-flash"), + ("Gemini 2.5 Flash Latest - Optimal speed and cost", "gemini-2.5-flash-latest"), + ("Gemini 2.5 Flash Preview - Advanced thinking capability", "gemini-2.5-flash-preview-09-2025"), ("Gemini 2.5 Flash Lite - Cost efficiency and low latency", "gemini-2.5-flash-lite"), - ("Gemini 2.5 Flash - Next generation features, speed, and thinking", "gemini-2.5-flash"), ("Gemini 2.0 Flash Exp - Next generation features and speed", "gemini-2.0-flash-exp"), ("Gemini 3.0 Flash - Next generation features, speed, and thinking", "gemini-3-flash-preview"), ("Gemini 1.5 Pro - High reasoning capability", "gemini-1.5-pro"), @@ -204,8 +205,9 @@ def select_deep_thinking_agent(provider) -> str: "google": [ ("Gemini 1.5 Flash - Cost efficiency and low latency", "gemini-1.5-flash"), ("Gemini 2.0 Flash - Next generation features and speed", "gemini-2.0-flash"), + ("Gemini 2.5 Flash Latest - Optimal speed and cost", "gemini-2.5-flash-latest"), + ("Gemini 2.5 Flash Preview - Advanced thinking capability", "gemini-2.5-flash-preview-09-2025"), ("Gemini 2.5 Flash Lite - Cost efficiency and low latency", "gemini-2.5-flash-lite"), - ("Gemini 2.5 Flash - Next generation features, speed, and thinking", "gemini-2.5-flash"), ("Gemini 2.0 Flash Exp - Next generation features and speed", "gemini-2.0-flash-exp"), ("Gemini 2.5 Pro - High reasoning capability", "gemini-2.5-pro"), ("Gemini 3.0 Flash - Next generation features, speed, and thinking", "gemini-3-flash-preview"), diff --git a/startEmbedding.sh b/startEmbedding.sh index ff9ef58d..e6f6ce67 100755 --- a/startEmbedding.sh +++ b/startEmbedding.sh @@ -12,6 +12,7 @@ docker run -d \ --name embedding-service \ --restart unless-stopped \ -p 11434:80 \ + -v $PWD/data_cache:/data \ -e MAX_CONCURRENT_REQUESTS=4 \ ghcr.io/huggingface/text-embeddings-inference:cpu-latest \ --model-id sentence-transformers/all-MiniLM-L6-v2 diff --git a/test_embedding_connection.py b/test_embedding_connection.py new file mode 100644 index 00000000..4d256c7f --- /dev/null +++ b/test_embedding_connection.py @@ -0,0 +1,32 @@ + +import os +from openai import OpenAI +import httpx + +client = OpenAI( + base_url="http://localhost:11434/v1", + api_key="sk-dummy" +) + +print("Testing connection to http://localhost:11434/v1/embeddings...") + +try: + response = client.embeddings.create( + input="The food was delicious and the waiter...", + model="sentence-transformers/all-MiniLM-L6-v2" + ) + print("Success!") + print(response.data[0].embedding[:5]) +except Exception as e: + print(f"FAILED: {e}") + import traceback + traceback.print_exc() + +print("\nTesting with httpx directly to 127.0.0.1...") +try: + r = httpx.post("http://127.0.0.1:11434/v1/embeddings", + json={"input": "test", "model": "sentence-transformers/all-MiniLM-L6-v2"}, + timeout=5.0) + print(f"HTTPX 127.0.0.1 Status: {r.status_code}") +except Exception as e: + print(f"HTTPX 127.0.0.1 Failed: {e}") diff --git a/tests/test_google_api.py b/tests/test_google_api.py index a793a546..9981f29f 100644 --- a/tests/test_google_api.py +++ b/tests/test_google_api.py @@ -23,7 +23,8 @@ def test_google_api(): "gemini-1.5-flash", "gemini-2.0-flash", "gemini-2.0-flash-exp", - "gemini-2.5-flash", + "gemini-2.5-flash-latest", + "gemini-2.5-flash-preview-09-2025", "gemini-2.5-flash-lite", "gemini-2.5-pro", "gemini-3-flash-preview", diff --git a/tradingagents/agents/analysts/market_analyst.py b/tradingagents/agents/analysts/market_analyst.py index 25aff041..106f1ef3 100644 --- a/tradingagents/agents/analysts/market_analyst.py +++ b/tradingagents/agents/analysts/market_analyst.py @@ -1,7 +1,7 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder import time import json -from tradingagents.agents.utils.agent_utils import get_stock_data, get_indicators +from tradingagents.agents.utils.agent_utils import get_stock_data, get_indicators, get_insider_transactions from tradingagents.dataflows.config import get_config @@ -16,15 +16,51 @@ from tradingagents.utils.logger import app_logger as logger # Initialize anonymizer (shared instance appropriate here or inside) + +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 + + df = pd.read_csv(StringIO(raw_data), 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): def market_analyst_node(state): + logger.info(f">>> STARTING MARKET ANALYST for {state.get('company_of_interest')} <<<") current_date = state["trade_date"] # Initialize default panic state regime_val = "UNKNOWN (Fatal Node Failure)" metrics = {} - broad_market_regime = "UNKNOWN" + broad_market_regime = "UNKNOWN (Initialized)" + net_insider_flow = 0.0 + metrics = {"volatility": 0.0} volatility_score = 0.0 report = "Market Analysis failed completely." tool_result_message = state["messages"] @@ -112,7 +148,6 @@ def create_market_analyst(llm): try: debug_msg = f"DEBUG: Passing prices to detector. Type: {type(price_data)}, Length: {len(price_data)}" logger.info(debug_msg) - print(f"\n[CONSOLE] {debug_msg}") regime, metrics = RegimeDetector.detect_regime(price_data) @@ -125,10 +160,12 @@ def create_market_analyst(llm): optimal_params = DynamicIndicatorSelector.get_optimal_parameters(regime) volatility_score = metrics.get("volatility", 0.0) + logger.info(f"SUCCESS: Detected Regime: {regime_val}") + logger.info(f"DEBUG: Optimal Params: {json.dumps(optimal_params)}") + except Exception as e_det: err_msg = f"CRITICAL: Detector Call Failed. Data Snippet: {str(price_data.head())}. Error: {e_det}" logger.critical(err_msg) - print(f"\n[CONSOLE] {err_msg}") regime_val = "UNKNOWN (Detector Failed)" metrics = {"volatility": 0.0} optimal_params = {} @@ -152,9 +189,23 @@ def create_market_analyst(llm): logger.warning(f"Regime detection failed for {ticker}: {e}") regime_val = f"UNKNOWN (Error: {str(e)})" + # --- INSIDER DATA FETCH (Hard Gate) --- + try: + insider_data = get_insider_transactions.invoke({ + "ticker": real_ticker, + "curr_date": current_date + }) + net_insider_flow = _calculate_net_insider_flow(insider_data) + logger.info(f"Insider Net Flow calculated: ${net_insider_flow:,.2f}") + except Exception as e_ins: + logger.warning(f"Insider data fetch failed: {e_ins}") + net_insider_flow = 0.0 + + # --- LLM CALL --- tools = [ get_stock_data, get_indicators, + get_insider_transactions, ] system_message = ( @@ -221,9 +272,6 @@ def create_market_analyst(llm): prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools])) prompt = prompt.partial(current_date=current_date) prompt = prompt.partial(ticker=ticker) - - prompt = prompt.partial(ticker=ticker) - logger.info(f"Market Analyst Prompt: {prompt}") try: @@ -240,15 +288,55 @@ def create_market_analyst(llm): except Exception as e_fatal: logger.critical(f"CRITICAL ERROR in Market Analyst Node: {e_fatal}") regime_val = f"UNKNOWN (Fatal Crash: {str(e_fatal)})" + regime_val = f"UNKNOWN (Fatal Crash: {str(e_fatal)})" report = f"Market Analyst Node crashed completely: {e_fatal}" + risk_multiplier = 0.5 # Default to conservative on crash + + # --- 6. RELATIVE STRENGTH LOGIC (The Alpha Calculator) --- + # Logic: Compare Asset Regime (Boat) vs. Market Regime (Tide) + if "risk_multiplier" not in locals(): + risk_multiplier = 1.0 # Default Neutral + + # Clean strings for comparison + asset_r = str(regime_val).upper() + spy_r = str(broad_market_regime).upper() + + if "TRENDING_UP" in asset_r: + if "SIDEWAYS" in spy_r or "UNKNOWN" in spy_r: + # Scenario: Asset is leading the market (Alpha) + # Action: Press the advantage. + risk_multiplier = 1.5 + elif "TRENDING_DOWN" in spy_r: + # Scenario: Asset fighting the tide (Divergence) + # Action: Caution. Breakouts often fail in bear markets. + risk_multiplier = 0.8 + elif "TRENDING_UP" in spy_r: + # Scenario: A rising tide lifts all boats (Beta) + # Action: Standard aggressive sizing. + risk_multiplier = 1.2 + + elif "VOLATILE" in asset_r: + # Scenario: Choppy/Shakeout + # Action: Reduce size to survive noise. + risk_multiplier = 0.5 + + elif "TRENDING_DOWN" in asset_r: + # Scenario: Knife falling. + # Action: Zero buying power. + risk_multiplier = 0.0 + + # --- 7. FINAL RETURN --- + logger.info(f"DEBUG: Market Analyst Returning -> Regime: {regime_val}, Risk Multiplier: {risk_multiplier}x") return { "messages": tool_result_message, "market_report": report, - "market_regime": regime_val, # PLTR Regime (e.g., TRENDING_UP) + "market_regime": regime_val, # CRITICAL: Must not be UNKNOWN if successful "regime_metrics": metrics, "volatility_score": volatility_score, - "broad_market_regime": broad_market_regime # SPY Regime (e.g., SIDEWAYS) + "broad_market_regime": broad_market_regime, + "net_insider_flow": net_insider_flow, + "risk_multiplier": risk_multiplier } return market_analyst_node diff --git a/tradingagents/agents/utils/agent_states.py b/tradingagents/agents/utils/agent_states.py index 25571623..adfa251f 100644 --- a/tradingagents/agents/utils/agent_states.py +++ b/tradingagents/agents/utils/agent_states.py @@ -7,7 +7,18 @@ from langgraph.prebuilt import ToolNode from langgraph.graph import END, StateGraph, START, MessagesState +from typing import Dict, List + # Researcher team state +class PortfolioPosition(TypedDict): + ticker: str + shares: int + average_cost: float + current_value: float + unrealized_pnl: float + unrealized_pnl_pct: float + entry_date: str + class InvestDebateState(TypedDict): bull_history: Annotated[ str, "Bullish Conversation history" @@ -67,6 +78,10 @@ class AgentState(MessagesState): broad_market_regime: Annotated[str, "Broad Market Context (e.g. SPY Regime)"] regime_metrics: Annotated[dict, "Metrics used to determine regime"] volatility_score: Annotated[float, "Current Volatility Score"] + net_insider_flow: Annotated[float, "Net Insider Transaction Flow (Last 90 Days)"] + portfolio: Annotated[Dict[str, PortfolioPosition], "Current active holdings"] + cash_balance: Annotated[float, "Current cash balance"] + risk_multiplier: Annotated[float, "Calculated Risk Multiplier based on Relative Strength"] # researcher team discussion step investment_debate_state: Annotated[ diff --git a/tradingagents/dataflows/alpha_vantage_news.py b/tradingagents/dataflows/alpha_vantage_news.py index 95b3635e..bc306159 100644 --- a/tradingagents/dataflows/alpha_vantage_news.py +++ b/tradingagents/dataflows/alpha_vantage_news.py @@ -54,7 +54,7 @@ def get_global_market_news(curr_date, look_back_days=7, limit=50) -> dict[str, s return _make_api_request("NEWS_SENTIMENT", params) -def get_insider_transactions(symbol: str) -> dict[str, str] | str: +def get_insider_transactions(symbol: str, curr_date: str = None, **kwargs) -> dict[str, str] | str: """Returns latest and historical insider transactions by key stakeholders. Covers transactions by founders, executives, board members, etc. diff --git a/tradingagents/dataflows/interface.py b/tradingagents/dataflows/interface.py index 73e78868..ae46e6fc 100644 --- a/tradingagents/dataflows/interface.py +++ b/tradingagents/dataflows/interface.py @@ -21,6 +21,7 @@ from .alpaca import get_stock_data as get_stock_alpaca # Configuration and routing logic from .config import get_config +from tradingagents.utils.logger import app_logger as logger # Tools organized by category TOOLS_CATEGORIES = { @@ -166,7 +167,7 @@ def route_to_vendor(method: str, *args, **kwargs): # Debug: Print fallback ordering primary_str = " → ".join(primary_vendors) fallback_str = " → ".join(fallback_vendors) - print(f"DEBUG: {method} - Primary: [{primary_str}] | Full fallback order: [{fallback_str}]") + logger.info(f"{method} - Primary: [{primary_str}] | Full fallback order: [{fallback_str}]") # Track results and execution state results = [] @@ -178,9 +179,8 @@ def route_to_vendor(method: str, *args, **kwargs): for vendor in fallback_vendors: if vendor not in VENDOR_METHODS[method]: if vendor in primary_vendors: - print(f"INFO: Vendor '{vendor}' not supported for method '{method}', falling back to next vendor") + logger.info(f"Vendor '{vendor}' not supported for method '{method}', falling back to next vendor") continue - vendor_impl = VENDOR_METHODS[method][vendor] is_primary_vendor = vendor in primary_vendors vendor_attempt_count += 1 @@ -191,12 +191,12 @@ def route_to_vendor(method: str, *args, **kwargs): # Debug: Print current attempt vendor_type = "PRIMARY" if is_primary_vendor else "FALLBACK" - print(f"DEBUG: Attempting {vendor_type} vendor '{vendor}' for {method} (attempt #{vendor_attempt_count})") + logger.info(f"Attempting {vendor_type} vendor '{vendor}' for {method} (attempt #{vendor_attempt_count})") # Handle list of methods for a vendor if isinstance(vendor_impl, list): vendor_methods = [(impl, vendor) for impl in vendor_impl] - print(f"DEBUG: Vendor '{vendor}' has multiple implementations: {len(vendor_methods)} functions") + logger.info(f"Vendor '{vendor}' has multiple implementations: {len(vendor_methods)} functions") else: vendor_methods = [(vendor_impl, vendor)] @@ -204,29 +204,29 @@ def route_to_vendor(method: str, *args, **kwargs): vendor_results = [] for impl_func, vendor_name in vendor_methods: try: - print(f"DEBUG: Calling {impl_func.__name__} from vendor '{vendor_name}'...") + logger.info(f"Calling {impl_func.__name__} from vendor '{vendor_name}'...") result = impl_func(*args, **kwargs) # Robustify: Check for empty results if result is None or (isinstance(result, str) and not result.strip()): - print(f"WARNING: {impl_func.__name__} from vendor '{vendor_name}' returned empty/no data") + logger.warning(f"{impl_func.__name__} from vendor '{vendor_name}' returned empty/no data") # Don't append to vendor_results, let it try other implementations or vendors continue vendor_results.append(result) - print(f"SUCCESS: {impl_func.__name__} from vendor '{vendor_name}' completed successfully") + logger.info(f"{impl_func.__name__} from vendor '{vendor_name}' completed successfully") except AlphaVantageRateLimitError as e: msg = f"RATE_LIMIT: Alpha Vantage rate limit exceeded: {e}" if vendor == "alpha_vantage": - print(msg) + logger.error(msg) errors.append(msg) # Continue to next vendor for fallback continue except Exception as e: # Log error but continue with other implementations msg = f"FAILED: {impl_func.__name__} from vendor '{vendor_name}' failed: {e}" - print(msg) + logger.error(msg) errors.append(msg) continue @@ -235,23 +235,23 @@ def route_to_vendor(method: str, *args, **kwargs): results.extend(vendor_results) successful_vendor = vendor result_summary = f"Got {len(vendor_results)} result(s)" - print(f"SUCCESS: Vendor '{vendor}' succeeded - {result_summary}") + logger.info(f"Vendor '{vendor}' succeeded - {result_summary}") # Stopping logic: Stop after first successful vendor for single-vendor configs # Multiple vendor configs (comma-separated) may want to collect from multiple sources if len(primary_vendors) == 1: - print(f"DEBUG: Stopping after successful vendor '{vendor}' (single-vendor config)") + logger.info(f"Stopping after successful vendor '{vendor}' (single-vendor config)") break else: - print(f"FAILED: Vendor '{vendor}' produced no results") + logger.error(f"Vendor '{vendor}' produced no results") # Final result summary if not results: error_details = "; ".join(errors) - print(f"FAILURE: All {vendor_attempt_count} vendor attempts failed for method '{method}'. Errors: {error_details}") + logger.error(f"All {vendor_attempt_count} vendor attempts failed for method '{method}'. Errors: {error_details}") raise RuntimeError(f"All vendor implementations failed for method '{method}'. Details: {error_details}") else: - print(f"FINAL: Method '{method}' completed with {len(results)} result(s) from {vendor_attempt_count} vendor attempt(s)") + logger.info(f"Method '{method}' completed with {len(results)} result(s) from {vendor_attempt_count} vendor attempt(s)") # Return single result if only one, otherwise concatenate as string if len(results) == 1: diff --git a/tradingagents/dataflows/y_finance.py b/tradingagents/dataflows/y_finance.py index 14ad4f01..e86518ce 100644 --- a/tradingagents/dataflows/y_finance.py +++ b/tradingagents/dataflows/y_finance.py @@ -394,7 +394,9 @@ def get_income_statement( def get_insider_transactions( - ticker: Annotated[str, "ticker symbol of the company"] + ticker: Annotated[str, "ticker symbol of the company"], + curr_date: Annotated[str, "current date"] = None, + **kwargs ): """Get insider transactions data from yfinance.""" try: diff --git a/tradingagents/graph/conditional_logic.py b/tradingagents/graph/conditional_logic.py index e7c87859..cd02b8bc 100644 --- a/tradingagents/graph/conditional_logic.py +++ b/tradingagents/graph/conditional_logic.py @@ -15,7 +15,7 @@ class ConditionalLogic: """Determine if market analysis should continue.""" messages = state["messages"] last_message = messages[-1] - if last_message.tool_calls: + if getattr(last_message, "tool_calls", None): return "tools_market" return "Msg Clear Market" @@ -23,7 +23,7 @@ class ConditionalLogic: """Determine if social media analysis should continue.""" messages = state["messages"] last_message = messages[-1] - if last_message.tool_calls: + if getattr(last_message, "tool_calls", None): return "tools_social" return "Msg Clear Social" @@ -31,7 +31,7 @@ class ConditionalLogic: """Determine if news analysis should continue.""" messages = state["messages"] last_message = messages[-1] - if last_message.tool_calls: + if getattr(last_message, "tool_calls", None): return "tools_news" return "Msg Clear News" @@ -39,7 +39,7 @@ class ConditionalLogic: """Determine if fundamentals analysis should continue.""" messages = state["messages"] last_message = messages[-1] - if last_message.tool_calls: + if getattr(last_message, "tool_calls", None): return "tools_fundamentals" return "Msg Clear Fundamentals" diff --git a/tradingagents/graph/propagation.py b/tradingagents/graph/propagation.py index cd6695c4..7e8f176b 100644 --- a/tradingagents/graph/propagation.py +++ b/tradingagents/graph/propagation.py @@ -43,6 +43,9 @@ class Propagator: "market_regime": "UNKNOWN", "broad_market_regime": "UNKNOWN", "volatility_score": 0.0, + "net_insider_flow": 0.0, + "portfolio": {}, + "cash_balance": 100000.0, } def get_graph_args(self) -> Dict[str, Any]: diff --git a/tradingagents/graph/reflection.py b/tradingagents/graph/reflection.py index 33303231..1f4d6f20 100644 --- a/tradingagents/graph/reflection.py +++ b/tradingagents/graph/reflection.py @@ -15,45 +15,72 @@ class Reflector: def _get_reflection_prompt(self) -> str: """Get the system prompt for reflection.""" 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: +You are an expert financial analyst tasked with reviewing trading decisions/analysis. +Your goal is to deliver detailed insights AND **tunable parameter updates**. 1. Reasoning: - - 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. - - Analyze the contributing factors to each success or mistake. Consider: - - Market intelligence. - - Technical indicators. - - Technical signals. - - Price movement analysis. - - Overall market data analysis - - News analysis. - - Social media and sentiment analysis. - - Fundamental data analysis. - - Weight the importance of each factor in the decision-making process. + - Determine if the decision was correct based on the OUTCOME (Returns). + - Analyze which factor (News, Technicals, Fundamentals) was the primary driver. 2. Improvement: - - For any incorrect decisions, propose revisions to maximize returns. - - 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). + - For incorrect decisions, propose revisions. 3. Summary: - - 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. + - Summarize lessons learned. -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. +4. PARAMETER OPTIMIZATION (CRITICAL): + - You have control over specific system parameters. + - If the strategy failed due to being too slow/fast, adjust them. + - **YOU MUST OUTPUT A JSON BLOCK** at the end of your response if changes are needed. + - Available Parameters: + - `rsi_period` (Default 14): Lower to 7 for faster reaction, raise to 21 for noise filtering. + - `risk_multiplier_cap` (Default 1.5): Lower if drawdowns are too high. + - `stop_loss_pct` (Default 0.10): Tighten (e.g., 0.05) if getting stopped out too late. + + - FORMAT: + ```json + { + "UPDATE_PARAMETERS": { + "rsi_period": 7, + "stop_loss_pct": 0.08 + } + } + ``` + - If no changes are needed, do not output the JSON block. -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. +Adhere strictly to these instructions. """ 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"] + """ + Extract the current market situation from the state. + CRITICAL FIX: Now includes Regime Context so the Reflector knows WHY rules were applied. + """ + # Standard Reports + curr_market_report = current_state.get("market_report", "No Market Report") + curr_sentiment_report = current_state.get("sentiment_report", "No Sentiment Report") + curr_news_report = current_state.get("news_report", "No News Report") + curr_fundamentals_report = current_state.get("fundamentals_report", "No Fundamental Report") - return f"{curr_market_report}\n\n{curr_sentiment_report}\n\n{curr_news_report}\n\n{curr_fundamentals_report}" + # 🛑 CRITICAL CONTEXT: The Regime Data + market_regime = current_state.get("market_regime", "UNKNOWN") + broad_regime = current_state.get("broad_market_regime", "UNKNOWN") + volatility = current_state.get("volatility_score", "N/A") + + # Format the Situation String + situation_str = ( + f"=== MARKET REGIME CONTEXT ===\n" + f"Target Asset Regime: {market_regime}\n" + f"Broad Market (SPY) Regime: {broad_regime}\n" + f"Volatility Score: {volatility}\n\n" + f"=== ANALYST REPORTS ===\n" + f"TECHNICAL: {curr_market_report}\n\n" + f"SENTIMENT: {curr_sentiment_report}\n\n" + f"NEWS: {curr_news_report}\n\n" + f"FUNDAMENTALS: {curr_fundamentals_report}" + ) + + return situation_str def _reflect_on_component( self, component_type: str, report: str, situation: str, returns_losses diff --git a/tradingagents/graph/setup.py b/tradingagents/graph/setup.py index b270ffc0..b9420a54 100644 --- a/tradingagents/graph/setup.py +++ b/tradingagents/graph/setup.py @@ -57,12 +57,18 @@ class GraphSetup: delete_nodes = {} tool_nodes = {} - if "market" in selected_analysts: - analyst_nodes["market"] = create_market_analyst( - self.quick_thinking_llm - ) - delete_nodes["market"] = create_msg_delete() - tool_nodes["market"] = self.tool_nodes["market"] + # FORCE MARKET ANALYST (MANDATORY) + # It must enable Regime Detection before any other analyst runs. + # Remove 'market' from selected list to avoid duplication if user selected it. + # We will add it manually as the first node. + other_analysts = [a for a in selected_analysts if a != "market"] + + # MARKET ANALYST (Always Created) + analyst_nodes["market"] = create_market_analyst(self.quick_thinking_llm) + delete_nodes["market"] = create_msg_delete() + tool_nodes["market"] = self.tool_nodes["market"] + + # Loop through other optional analysts (Social, News, Fundamentals) if "social" in selected_analysts: analyst_nodes["social"] = create_social_media_analyst( @@ -109,12 +115,20 @@ class GraphSetup: workflow = StateGraph(AgentState) # Add analyst nodes to the graph - for analyst_type, node in analyst_nodes.items(): - workflow.add_node(f"{analyst_type.capitalize()} Analyst", node) - workflow.add_node( - f"Msg Clear {analyst_type.capitalize()}", delete_nodes[analyst_type] - ) - workflow.add_node(f"tools_{analyst_type}", tool_nodes[analyst_type]) + # Add analyst nodes to the graph + # 1. Add Market Analyst (Mandatory) + workflow.add_node("Market Analyst", analyst_nodes["market"]) + workflow.add_node("Msg Clear Market", delete_nodes["market"]) + workflow.add_node("tools_market", tool_nodes["market"]) + + # 2. Add Other Analysts + for analyst_type in other_analysts: + if analyst_type in analyst_nodes: + workflow.add_node(f"{analyst_type.capitalize()} Analyst", analyst_nodes[analyst_type]) + workflow.add_node( + f"Msg Clear {analyst_type.capitalize()}", delete_nodes[analyst_type] + ) + workflow.add_node(f"tools_{analyst_type}", tool_nodes[analyst_type]) # Add other nodes workflow.add_node("Bull Researcher", bull_researcher_node) @@ -127,12 +141,28 @@ class GraphSetup: workflow.add_node("Risk Judge", risk_manager_node) # Define edges - # Start with the first analyst - first_analyst = selected_analysts[0] - workflow.add_edge(START, f"{first_analyst.capitalize()} Analyst") + # Define edges + + # 1. START -> Market Analyst (Always) + workflow.add_edge(START, "Market Analyst") + + # 2. Market Analyst -> Tools -> Clear + workflow.add_conditional_edges( + "Market Analyst", + self.conditional_logic.should_continue_market, + ["tools_market", "Msg Clear Market"], + ) + workflow.add_edge("tools_market", "Market Analyst") + + # 3. Market Analyst -> First Optional Analyst (or Bull Researcher) + if len(other_analysts) > 0: + first_other = other_analysts[0] + workflow.add_edge("Msg Clear Market", f"{first_other.capitalize()} Analyst") + else: + workflow.add_edge("Msg Clear Market", "Bull Researcher") - # Connect analysts in sequence - for i, analyst_type in enumerate(selected_analysts): + # 4. Connect Optional Analysts in sequence + for i, analyst_type in enumerate(other_analysts): current_analyst = f"{analyst_type.capitalize()} Analyst" current_tools = f"tools_{analyst_type}" current_clear = f"Msg Clear {analyst_type.capitalize()}" @@ -146,8 +176,8 @@ class GraphSetup: workflow.add_edge(current_tools, current_analyst) # Connect to next analyst or to Bull Researcher if this is the last analyst - if i < len(selected_analysts) - 1: - next_analyst = f"{selected_analysts[i+1].capitalize()} Analyst" + if i < len(other_analysts) - 1: + next_analyst = f"{other_analysts[i+1].capitalize()} Analyst" workflow.add_edge(current_clear, next_analyst) else: workflow.add_edge(current_clear, "Bull Researcher") diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index 3e33e14e..e47febb1 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -229,6 +229,10 @@ class TradingAgentsGraph: # Log state self._log_state(trade_date, final_state) + + # 🟢 EMERGENCY DIAGNOSTIC + logger.info(f"DEBUG GRAPH STATE: Regime={final_state.get('market_regime')}") + logger.info(f"DEBUG GRAPH STATE: Broad Market={final_state.get('broad_market_regime')}") # 3. FIX CRASH RISK: Handle Dead State gracefully # First, extract raw decision from LLM text (The Agent Decision) @@ -245,12 +249,14 @@ class TradingAgentsGraph: msg = f"🔍 [DEBUG] APPLYING OVERRIDE: Regime='{regime_val}', Growth={self.hard_data.get('revenue_growth', 'N/A')}" logger.info(msg) - print(f"\n[CONSOLE] {msg}") overridden_decision = self.apply_trend_override( raw_llm_decision, self.hard_data, - regime_val + regime_val, + regime_val, + final_state.get("net_insider_flow", 0.0), + final_state.get("portfolio", {}) ) # Update final state with potentially overridden decision @@ -375,6 +381,7 @@ class TradingAgentsGraph: if not history.empty and len(history) >= 200: metrics["current_price"] = history["Close"].iloc[-1] metrics["sma_200"] = history["Close"].rolling(200).mean().iloc[-1] + metrics["sma_50"] = history["Close"].rolling(50).mean().iloc[-1] metrics["status"] = "OK" metrics["revenue_growth"] = get_robust_revenue_growth(ticker) @@ -384,7 +391,7 @@ class TradingAgentsGraph: logger.error(f"Error fetching hard data for {ticker} override: {e}") return {"status": "ERROR", "error": str(e)} - def apply_trend_override(self, trade_decision_str: str, hard_data: Dict[str, Any], regime: str) -> Any: + def apply_trend_override(self, trade_decision_str: str, hard_data: Dict[str, Any], regime: str, insider_flow: float = 0.0, portfolio: Dict[str, Any] = {}) -> Any: """ The 'Don't Fight the Tape' Safety Valve. Prevents the system from shorting high-growth winners during a Bull Market. @@ -400,13 +407,64 @@ class TradingAgentsGraph: regime_val = str(regime) regime_val = regime_val.upper().strip() + + # ------------------------------------------------------------- + # RULE 72: THE HARD STOP LOSS (Portfolio Protection) + # "If unrealized P&L < -10%, LIQUIDATE. No questions asked." + # ------------------------------------------------------------- + if self.ticker in portfolio: + pos = portfolio[self.ticker] + # Calculate PnL dynamically based on latest price to ensure safety + latest_price = hard_data.get("current_price", 0.0) + if latest_price > 0 and pos.get("average_cost", 0) > 0: + cost = pos["average_cost"] + pnl_pct = (latest_price - cost) / cost + + if pnl_pct < -0.10: # -10% Hard Stop + reasoning = ( + f"🛑 STOP LOSS TRIGGERED (Rule 72): Position is down {pnl_pct:.1%}. " + f"Current: ${latest_price:.2f}, Cost: ${cost:.2f}. " + "LIQUIDATING IMMEDIATELY." + ) + logger.warning(reasoning) + return { + "action": "SELL", + "quantity": pos["shares"], # Sell entire position + "reasoning": reasoning, + "confidence": 1.0 + } + + # ------------------------------------------------------------- + + # 🛑 EMERGENCY BYPASS FOR DEBUGGING + if regime_val == "UNKNOWN": + logger.info("⚠️ DEBUG OVERRIDE: Regime is UNKNOWN. Checking Technicals for Force-Bull...") price = hard_data["current_price"] sma_200 = hard_data["sma_200"] + sma_50 = hard_data.get("sma_50", 0.0) growth = hard_data["revenue_growth"] + # 0. Insider Veto (Rule B: Insider Selling > $50M + Downtrend) + is_downtrend_50 = price < sma_50 + if insider_flow < -50_000_000 and is_downtrend_50: + if "BUY" in trade_decision_str.upper(): + logger.warning(f"🛑 INSIDER VETO TRIGGERED for {self.ticker}") + logger.warning(f" Reason: Insiders sold ${abs(insider_flow):,.0f} (> $50M) and Price < 50SMA.") + return { + "action": "HOLD", + "quantity": 0, + "reasoning": f"INSIDER VETO: Blocked BUY. Insiders sold ${abs(insider_flow):,.0f} into a downtrend (< 50SMA).", + "confidence": 1.0 + } + # 1. Technical Uptrend (Price > 200 SMA) is_technical_uptrend = price > sma_200 + + # EMERGENCY BYPASS FOR DEBUGGING + if regime_val == "UNKNOWN" and is_technical_uptrend: + logger.warning("⚠️ DEBUG OVERRIDE: Forcing Regime to 'TRENDING_UP' because Price > SMA") + # is_bull_regime will be True below by default # 2. Hyper-Growth (> 30% YoY) is_hyper_growth = growth > 0.30 @@ -418,7 +476,12 @@ class TradingAgentsGraph: msg_override = f"DEBUG OVERRIDE: Price={price}, SMA={sma_200}, Growth={growth}, Regime='{regime_val}'" logger.info(msg_override) - print(f"[CONSOLE] {msg_override}") + + # ⚠️ EMERGENCY DIAGNOSTIC + logger.info(f"DEBUG CHECK: Technical (Price > SMA) = {is_technical_uptrend}") + logger.info(f"DEBUG CHECK: Growth (> 30%) = {is_hyper_growth}") + logger.info(f"DEBUG CHECK: Bull Regime (Not Down) = {is_bull_regime}") + logger.info(f"DEBUG CHECK: Technical={is_technical_uptrend}, Growth={is_hyper_growth}, BullRegime={is_bull_regime}") # 4. Trigger Override if trying to SELL a leader in a bull market @@ -434,7 +497,6 @@ class TradingAgentsGraph: ) logger.warning(f"🛑 TREND OVERRIDE TRIGGERED for {self.ticker}") - print(f"\n[CONSOLE] 🛑 TREND OVERRIDE TRIGGERED for {self.ticker}") logger.warning(f" Reason: Stock (${price:.2f}) is > 200SMA (${sma_200:.2f}) and Growth is {growth:.1%}") logger.warning(f" Action 'SELL' blocked. Converting to '{allowed_action}'.")