- **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:
parent
e88a01d0ea
commit
1f279a9df2
10
CHANGELOG.md
10
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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[
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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}'.")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue