- **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.
This commit is contained in:
swj.premkumar 2026-01-13 05:27:24 -06:00
parent e88a01d0ea
commit 1f279a9df2
16 changed files with 453 additions and 85 deletions

View File

@ -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

View File

@ -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.**
**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.

View File

@ -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"),

View File

@ -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

View File

@ -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}")

View File

@ -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",

View File

@ -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

View File

@ -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[

View File

@ -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.

View File

@ -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:

View File

@ -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:

View File

@ -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"

View File

@ -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]:

View File

@ -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

View File

@ -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")

View File

@ -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}'.")