513 lines
20 KiB
Python
513 lines
20 KiB
Python
"""Momentum Analyst Agent.
|
|
|
|
Specializes in multi-timeframe momentum analysis using:
|
|
- Rate of Change (ROC) across multiple periods
|
|
- Average Directional Index (ADX) for trend strength
|
|
- RSI momentum confirmation
|
|
- MACD momentum signals
|
|
- Multi-timeframe momentum divergence detection
|
|
|
|
Issue #13: [AGENT-12] Momentum Analyst - multi-TF momentum, ROC, ADX
|
|
"""
|
|
|
|
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
|
from langchain_core.tools import tool
|
|
from typing import Annotated, Dict, Any, List, Optional
|
|
import pandas as pd
|
|
|
|
from tradingagents.agents.utils.agent_utils import get_stock_data, get_indicators
|
|
from tradingagents.dataflows.interface import route_to_vendor
|
|
|
|
|
|
# ============================================================================
|
|
# Momentum-Specific Tools
|
|
# ============================================================================
|
|
|
|
@tool
|
|
def get_multi_timeframe_momentum(
|
|
symbol: Annotated[str, "Ticker symbol of the company"],
|
|
curr_date: Annotated[str, "Current trading date in YYYY-MM-DD format"],
|
|
short_period: Annotated[int, "Short-term ROC period (default: 5)"] = 5,
|
|
medium_period: Annotated[int, "Medium-term ROC period (default: 14)"] = 14,
|
|
long_period: Annotated[int, "Long-term ROC period (default: 30)"] = 30,
|
|
) -> str:
|
|
"""
|
|
Calculate multi-timeframe momentum using Rate of Change (ROC) across periods.
|
|
|
|
ROC measures percentage change over a period:
|
|
- Positive ROC = upward momentum
|
|
- Negative ROC = downward momentum
|
|
- Divergence across timeframes signals potential reversals
|
|
|
|
Returns analysis of short, medium, and long-term momentum alignment.
|
|
"""
|
|
try:
|
|
# Get stock data for analysis
|
|
stock_data = route_to_vendor("get_stock_data", symbol, curr_date, max(long_period * 2, 60))
|
|
|
|
if isinstance(stock_data, str) and "error" in stock_data.lower():
|
|
return f"Error retrieving stock data: {stock_data}"
|
|
|
|
# Parse the data if it's a string (CSV format)
|
|
if isinstance(stock_data, str):
|
|
from io import StringIO
|
|
df = pd.read_csv(StringIO(stock_data))
|
|
else:
|
|
df = stock_data
|
|
|
|
if df.empty or len(df) < long_period:
|
|
return f"Insufficient data for momentum analysis. Need at least {long_period} periods."
|
|
|
|
# Calculate ROC for each timeframe
|
|
close = df['close'] if 'close' in df.columns else df['Close']
|
|
|
|
roc_short = ((close.iloc[-1] - close.iloc[-short_period]) / close.iloc[-short_period]) * 100
|
|
roc_medium = ((close.iloc[-1] - close.iloc[-medium_period]) / close.iloc[-medium_period]) * 100
|
|
roc_long = ((close.iloc[-1] - close.iloc[-long_period]) / close.iloc[-long_period]) * 100
|
|
|
|
# Determine momentum alignment
|
|
all_positive = roc_short > 0 and roc_medium > 0 and roc_long > 0
|
|
all_negative = roc_short < 0 and roc_medium < 0 and roc_long < 0
|
|
|
|
if all_positive:
|
|
alignment = "BULLISH - All timeframes showing positive momentum"
|
|
strength = "STRONG" if min(roc_short, roc_medium, roc_long) > 2 else "MODERATE"
|
|
elif all_negative:
|
|
alignment = "BEARISH - All timeframes showing negative momentum"
|
|
strength = "STRONG" if max(roc_short, roc_medium, roc_long) < -2 else "MODERATE"
|
|
else:
|
|
alignment = "MIXED - Timeframes diverging, potential trend change"
|
|
strength = "WEAK"
|
|
|
|
# Detect acceleration/deceleration
|
|
acceleration = ""
|
|
if roc_short > roc_medium > roc_long:
|
|
acceleration = "ACCELERATING - Short-term momentum exceeding longer-term"
|
|
elif roc_short < roc_medium < roc_long:
|
|
acceleration = "DECELERATING - Short-term momentum lagging longer-term"
|
|
else:
|
|
acceleration = "TRANSITIONING - Mixed momentum dynamics"
|
|
|
|
report = f"""
|
|
## Multi-Timeframe Momentum Analysis for {symbol}
|
|
Analysis Date: {curr_date}
|
|
|
|
### Rate of Change (ROC) by Timeframe
|
|
|
|
| Timeframe | Period | ROC (%) | Signal |
|
|
|-----------|--------|---------|--------|
|
|
| Short-term | {short_period} days | {roc_short:.2f}% | {"🟢 Bullish" if roc_short > 0 else "🔴 Bearish"} |
|
|
| Medium-term | {medium_period} days | {roc_medium:.2f}% | {"🟢 Bullish" if roc_medium > 0 else "🔴 Bearish"} |
|
|
| Long-term | {long_period} days | {roc_long:.2f}% | {"🟢 Bullish" if roc_long > 0 else "🔴 Bearish"} |
|
|
|
|
### Momentum Summary
|
|
|
|
- **Alignment**: {alignment}
|
|
- **Strength**: {strength}
|
|
- **Trend Dynamics**: {acceleration}
|
|
|
|
### Interpretation
|
|
|
|
{"Strong bullish momentum confirmed across all timeframes. Consider long positions with confidence." if all_positive else ""}
|
|
{"Strong bearish momentum confirmed across all timeframes. Consider defensive or short positions." if all_negative else ""}
|
|
{"Mixed signals suggest caution. Wait for clearer alignment before taking significant positions." if not all_positive and not all_negative else ""}
|
|
"""
|
|
return report
|
|
|
|
except Exception as e:
|
|
return f"Error in multi-timeframe momentum analysis: {str(e)}"
|
|
|
|
|
|
@tool
|
|
def get_adx_analysis(
|
|
symbol: Annotated[str, "Ticker symbol of the company"],
|
|
curr_date: Annotated[str, "Current trading date in YYYY-MM-DD format"],
|
|
adx_period: Annotated[int, "ADX calculation period (default: 14)"] = 14,
|
|
look_back_days: Annotated[int, "Days of history to analyze (default: 60)"] = 60,
|
|
) -> str:
|
|
"""
|
|
Analyze trend strength using Average Directional Index (ADX).
|
|
|
|
ADX measures trend strength regardless of direction:
|
|
- 0-20: Weak or absent trend (ranging market)
|
|
- 20-40: Moderate trend developing
|
|
- 40-60: Strong trend
|
|
- 60-80: Very strong trend
|
|
- 80+: Extremely strong trend (rare)
|
|
|
|
Also includes +DI and -DI for directional analysis.
|
|
"""
|
|
try:
|
|
# Get stock data
|
|
stock_data = route_to_vendor("get_stock_data", symbol, curr_date, look_back_days)
|
|
|
|
if isinstance(stock_data, str) and "error" in stock_data.lower():
|
|
return f"Error retrieving stock data: {stock_data}"
|
|
|
|
# Parse data
|
|
if isinstance(stock_data, str):
|
|
from io import StringIO
|
|
df = pd.read_csv(StringIO(stock_data))
|
|
else:
|
|
df = stock_data
|
|
|
|
if df.empty or len(df) < adx_period * 2:
|
|
return f"Insufficient data for ADX analysis. Need at least {adx_period * 2} periods."
|
|
|
|
# Get column names (handle different case conventions)
|
|
high_col = 'high' if 'high' in df.columns else 'High'
|
|
low_col = 'low' if 'low' in df.columns else 'Low'
|
|
close_col = 'close' if 'close' in df.columns else 'Close'
|
|
|
|
high = df[high_col]
|
|
low = df[low_col]
|
|
close = df[close_col]
|
|
|
|
# Calculate True Range
|
|
tr1 = high - low
|
|
tr2 = abs(high - close.shift(1))
|
|
tr3 = abs(low - close.shift(1))
|
|
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
|
|
|
|
# Calculate Directional Movement
|
|
plus_dm = high.diff()
|
|
minus_dm = -low.diff()
|
|
|
|
plus_dm = plus_dm.where((plus_dm > minus_dm) & (plus_dm > 0), 0)
|
|
minus_dm = minus_dm.where((minus_dm > plus_dm) & (minus_dm > 0), 0)
|
|
|
|
# Smooth with EMA
|
|
atr = tr.ewm(span=adx_period, adjust=False).mean()
|
|
plus_di = 100 * (plus_dm.ewm(span=adx_period, adjust=False).mean() / atr)
|
|
minus_di = 100 * (minus_dm.ewm(span=adx_period, adjust=False).mean() / atr)
|
|
|
|
# Calculate ADX
|
|
dx = 100 * abs(plus_di - minus_di) / (plus_di + minus_di)
|
|
adx = dx.ewm(span=adx_period, adjust=False).mean()
|
|
|
|
# Get current values
|
|
current_adx = adx.iloc[-1]
|
|
current_plus_di = plus_di.iloc[-1]
|
|
current_minus_di = minus_di.iloc[-1]
|
|
prev_adx = adx.iloc[-2]
|
|
|
|
# Determine trend strength
|
|
if current_adx < 20:
|
|
trend_strength = "WEAK/ABSENT - Market is ranging"
|
|
recommendation = "Avoid trend-following strategies. Consider range-bound approaches."
|
|
elif current_adx < 40:
|
|
trend_strength = "MODERATE - Trend developing"
|
|
recommendation = "Trend is present but not dominant. Selective entries recommended."
|
|
elif current_adx < 60:
|
|
trend_strength = "STRONG - Clear trend in place"
|
|
recommendation = "Good conditions for trend-following strategies."
|
|
elif current_adx < 80:
|
|
trend_strength = "VERY STRONG - Powerful trend"
|
|
recommendation = "Excellent trend conditions. Watch for exhaustion signs."
|
|
else:
|
|
trend_strength = "EXTREME - Rare strength level"
|
|
recommendation = "Trend may be overextended. Consider taking profits."
|
|
|
|
# Trend direction
|
|
if current_plus_di > current_minus_di:
|
|
direction = "BULLISH (+DI > -DI)"
|
|
direction_signal = "🟢 Uptrend"
|
|
else:
|
|
direction = "BEARISH (-DI > +DI)"
|
|
direction_signal = "🔴 Downtrend"
|
|
|
|
# ADX momentum
|
|
adx_trend = "RISING" if current_adx > prev_adx else "FALLING"
|
|
|
|
report = f"""
|
|
## ADX Trend Strength Analysis for {symbol}
|
|
Analysis Date: {curr_date}
|
|
|
|
### Current Readings
|
|
|
|
| Indicator | Value | Interpretation |
|
|
|-----------|-------|----------------|
|
|
| ADX | {current_adx:.2f} | {trend_strength.split(' - ')[0]} |
|
|
| +DI | {current_plus_di:.2f} | Bullish pressure |
|
|
| -DI | {current_minus_di:.2f} | Bearish pressure |
|
|
| ADX Trend | {adx_trend} | {"Trend strengthening" if adx_trend == "RISING" else "Trend weakening"} |
|
|
|
|
### Analysis Summary
|
|
|
|
- **Trend Strength**: {trend_strength}
|
|
- **Trend Direction**: {direction} {direction_signal}
|
|
- **ADX Momentum**: {adx_trend} (Previous: {prev_adx:.2f})
|
|
|
|
### Trading Recommendation
|
|
|
|
{recommendation}
|
|
|
|
### Key Signals
|
|
|
|
{"✅ +DI crossing above -DI recently suggests bullish momentum building." if current_plus_di > current_minus_di and abs(current_plus_di - current_minus_di) < 5 else ""}
|
|
{"⚠️ -DI crossing above +DI recently suggests bearish momentum building." if current_minus_di > current_plus_di and abs(current_plus_di - current_minus_di) < 5 else ""}
|
|
{"📈 Rising ADX with strong directional bias suggests continuation." if adx_trend == "RISING" and current_adx > 25 else ""}
|
|
{"📉 Falling ADX suggests trend is losing momentum." if adx_trend == "FALLING" and current_adx > 25 else ""}
|
|
{"⏸️ Low ADX indicates consolidation phase." if current_adx < 20 else ""}
|
|
"""
|
|
return report
|
|
|
|
except Exception as e:
|
|
return f"Error in ADX analysis: {str(e)}"
|
|
|
|
|
|
@tool
|
|
def get_momentum_divergence(
|
|
symbol: Annotated[str, "Ticker symbol of the company"],
|
|
curr_date: Annotated[str, "Current trading date in YYYY-MM-DD format"],
|
|
look_back_days: Annotated[int, "Days to analyze for divergence (default: 30)"] = 30,
|
|
) -> str:
|
|
"""
|
|
Detect momentum divergences between price and momentum indicators.
|
|
|
|
Bullish Divergence: Price makes lower low, indicator makes higher low
|
|
Bearish Divergence: Price makes higher high, indicator makes lower high
|
|
|
|
Divergences often precede trend reversals.
|
|
"""
|
|
try:
|
|
# Get stock data
|
|
stock_data = route_to_vendor("get_stock_data", symbol, curr_date, look_back_days * 2)
|
|
|
|
if isinstance(stock_data, str) and "error" in stock_data.lower():
|
|
return f"Error retrieving stock data: {stock_data}"
|
|
|
|
# Parse data
|
|
if isinstance(stock_data, str):
|
|
from io import StringIO
|
|
df = pd.read_csv(StringIO(stock_data))
|
|
else:
|
|
df = stock_data
|
|
|
|
if df.empty or len(df) < look_back_days:
|
|
return f"Insufficient data for divergence analysis."
|
|
|
|
close_col = 'close' if 'close' in df.columns else 'Close'
|
|
close = df[close_col].values[-look_back_days:]
|
|
|
|
# Calculate RSI for divergence detection
|
|
delta = pd.Series(close).diff()
|
|
gain = delta.where(delta > 0, 0).rolling(window=14).mean()
|
|
loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
|
|
rs = gain / loss
|
|
rsi = 100 - (100 / (1 + rs))
|
|
rsi = rsi.values
|
|
|
|
# Find local extremes
|
|
price_highs = []
|
|
price_lows = []
|
|
rsi_highs = []
|
|
rsi_lows = []
|
|
|
|
for i in range(2, len(close) - 2):
|
|
# Price highs
|
|
if close[i] > close[i-1] and close[i] > close[i-2] and close[i] > close[i+1] and close[i] > close[i+2]:
|
|
price_highs.append((i, close[i]))
|
|
# Price lows
|
|
if close[i] < close[i-1] and close[i] < close[i-2] and close[i] < close[i+1] and close[i] < close[i+2]:
|
|
price_lows.append((i, close[i]))
|
|
|
|
# RSI extremes (after RSI is calculated, handling NaN)
|
|
valid_rsi = rsi[~pd.isna(rsi)]
|
|
rsi_start = len(rsi) - len(valid_rsi)
|
|
|
|
for i in range(rsi_start + 2, len(rsi) - 2):
|
|
if pd.notna(rsi[i]) and pd.notna(rsi[i-1]) and pd.notna(rsi[i+1]):
|
|
if rsi[i] > rsi[i-1] and rsi[i] > rsi[i+1]:
|
|
rsi_highs.append((i, rsi[i]))
|
|
if rsi[i] < rsi[i-1] and rsi[i] < rsi[i+1]:
|
|
rsi_lows.append((i, rsi[i]))
|
|
|
|
# Detect divergences
|
|
bullish_divergence = False
|
|
bearish_divergence = False
|
|
divergence_details = []
|
|
|
|
# Check for bearish divergence (higher price high, lower RSI high)
|
|
if len(price_highs) >= 2 and len(rsi_highs) >= 2:
|
|
recent_price = price_highs[-1]
|
|
prev_price = price_highs[-2]
|
|
recent_rsi = rsi_highs[-1] if rsi_highs else None
|
|
prev_rsi = rsi_highs[-2] if len(rsi_highs) >= 2 else None
|
|
|
|
if recent_rsi and prev_rsi:
|
|
if recent_price[1] > prev_price[1] and recent_rsi[1] < prev_rsi[1]:
|
|
bearish_divergence = True
|
|
divergence_details.append(
|
|
f"Bearish: Price high {recent_price[1]:.2f} > {prev_price[1]:.2f}, "
|
|
f"RSI high {recent_rsi[1]:.2f} < {prev_rsi[1]:.2f}"
|
|
)
|
|
|
|
# Check for bullish divergence (lower price low, higher RSI low)
|
|
if len(price_lows) >= 2 and len(rsi_lows) >= 2:
|
|
recent_price = price_lows[-1]
|
|
prev_price = price_lows[-2]
|
|
recent_rsi = rsi_lows[-1] if rsi_lows else None
|
|
prev_rsi = rsi_lows[-2] if len(rsi_lows) >= 2 else None
|
|
|
|
if recent_rsi and prev_rsi:
|
|
if recent_price[1] < prev_price[1] and recent_rsi[1] > prev_rsi[1]:
|
|
bullish_divergence = True
|
|
divergence_details.append(
|
|
f"Bullish: Price low {recent_price[1]:.2f} < {prev_price[1]:.2f}, "
|
|
f"RSI low {recent_rsi[1]:.2f} > {prev_rsi[1]:.2f}"
|
|
)
|
|
|
|
# Generate report
|
|
divergence_status = "NEUTRAL"
|
|
if bullish_divergence and not bearish_divergence:
|
|
divergence_status = "BULLISH DIVERGENCE DETECTED 🟢"
|
|
elif bearish_divergence and not bullish_divergence:
|
|
divergence_status = "BEARISH DIVERGENCE DETECTED 🔴"
|
|
elif bullish_divergence and bearish_divergence:
|
|
divergence_status = "MIXED SIGNALS - CONFLICTING DIVERGENCES ⚠️"
|
|
|
|
report = f"""
|
|
## Momentum Divergence Analysis for {symbol}
|
|
Analysis Date: {curr_date}
|
|
Analysis Period: {look_back_days} days
|
|
|
|
### Divergence Status: {divergence_status}
|
|
|
|
### Detected Patterns
|
|
|
|
{"No significant divergences detected in the analysis period." if not divergence_details else ""}
|
|
{"".join([f"- {detail}" + chr(10) for detail in divergence_details])}
|
|
|
|
### Pattern Summary
|
|
|
|
- **Price Highs Found**: {len(price_highs)}
|
|
- **Price Lows Found**: {len(price_lows)}
|
|
- **RSI Highs Found**: {len(rsi_highs)}
|
|
- **RSI Lows Found**: {len(rsi_lows)}
|
|
|
|
### Interpretation
|
|
|
|
{"**Bullish Divergence**: Price is making lower lows while RSI is making higher lows. This suggests selling pressure is waning and a potential bottom is forming. Consider long entries with confirmation." if bullish_divergence else ""}
|
|
{"**Bearish Divergence**: Price is making higher highs while RSI is making lower highs. This suggests buying pressure is waning and a potential top is forming. Consider reducing exposure or short entries with confirmation." if bearish_divergence else ""}
|
|
{"**No Divergence**: Price and momentum are moving in sync. The current trend appears healthy." if not bullish_divergence and not bearish_divergence else ""}
|
|
"""
|
|
return report
|
|
|
|
except Exception as e:
|
|
return f"Error in divergence analysis: {str(e)}"
|
|
|
|
|
|
# ============================================================================
|
|
# Momentum Analyst Agent Factory
|
|
# ============================================================================
|
|
|
|
def create_momentum_analyst(llm):
|
|
"""
|
|
Create a Momentum Analyst agent that specializes in:
|
|
- Multi-timeframe momentum analysis (ROC)
|
|
- Trend strength measurement (ADX)
|
|
- Momentum divergence detection
|
|
- RSI/MACD momentum confirmation
|
|
|
|
Args:
|
|
llm: Language model for generating analysis
|
|
|
|
Returns:
|
|
Function that processes state and returns momentum analysis
|
|
"""
|
|
|
|
def momentum_analyst_node(state):
|
|
current_date = state["trade_date"]
|
|
ticker = state["company_of_interest"]
|
|
|
|
tools = [
|
|
get_stock_data,
|
|
get_indicators,
|
|
get_multi_timeframe_momentum,
|
|
get_adx_analysis,
|
|
get_momentum_divergence,
|
|
]
|
|
|
|
system_message = """You are a specialized Momentum Analyst with expertise in quantitative momentum analysis. Your role is to provide comprehensive momentum assessments using multiple techniques:
|
|
|
|
## Your Analytical Framework
|
|
|
|
### 1. Multi-Timeframe Momentum (ROC Analysis)
|
|
- Analyze Rate of Change across short (5-day), medium (14-day), and long-term (30-day) periods
|
|
- Identify momentum alignment or divergence across timeframes
|
|
- Detect acceleration/deceleration patterns
|
|
|
|
### 2. Trend Strength (ADX Analysis)
|
|
- Use ADX to measure trend strength (0-100 scale)
|
|
- Analyze +DI/-DI for directional bias
|
|
- Identify trending vs ranging market conditions
|
|
|
|
### 3. Momentum Divergence Detection
|
|
- Identify bullish divergences (price lower low, indicator higher low)
|
|
- Identify bearish divergences (price higher high, indicator lower high)
|
|
- Assess reversal probability based on divergence patterns
|
|
|
|
### 4. Traditional Momentum Indicators
|
|
- RSI for overbought/oversold conditions
|
|
- MACD for momentum confirmation
|
|
- Stochastic for entry timing
|
|
|
|
## Analysis Process
|
|
|
|
1. **Start with get_stock_data** to retrieve price history
|
|
2. **Use get_multi_timeframe_momentum** for ROC analysis across periods
|
|
3. **Apply get_adx_analysis** to measure trend strength
|
|
4. **Check get_momentum_divergence** for reversal signals
|
|
5. **Use get_indicators** for RSI and MACD confirmation
|
|
|
|
## Output Requirements
|
|
|
|
Provide a comprehensive Momentum Report including:
|
|
- Overall momentum score and direction
|
|
- Timeframe alignment assessment
|
|
- Trend strength classification
|
|
- Divergence alerts if detected
|
|
- Trading recommendations based on momentum state
|
|
|
|
**Always include a summary table with key momentum metrics.**
|
|
|
|
Focus on actionable insights. Quantify momentum states where possible (e.g., "RSI at 72 suggests overbought, but ADX at 45 confirms strong uptrend - momentum remains bullish but overextended")."""
|
|
|
|
prompt = ChatPromptTemplate.from_messages(
|
|
[
|
|
(
|
|
"system",
|
|
"You are a specialized Momentum Analyst assistant, collaborating with other analysts."
|
|
" Use the provided momentum analysis tools to assess trend strength and direction."
|
|
" Execute comprehensive momentum analysis to support trading decisions."
|
|
" If you or any other assistant has the FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** or deliverable,"
|
|
" prefix your response with FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** so the team knows to stop."
|
|
" You have access to the following tools: {tool_names}.\n{system_message}"
|
|
" For your reference, the current date is {current_date}. The company we want to analyze is {ticker}.",
|
|
),
|
|
MessagesPlaceholder(variable_name="messages"),
|
|
]
|
|
)
|
|
|
|
prompt = prompt.partial(system_message=system_message)
|
|
prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
|
|
prompt = prompt.partial(current_date=current_date)
|
|
prompt = prompt.partial(ticker=ticker)
|
|
|
|
chain = prompt | llm.bind_tools(tools)
|
|
|
|
result = chain.invoke(state["messages"])
|
|
|
|
report = ""
|
|
|
|
if len(result.tool_calls) == 0:
|
|
report = result.content
|
|
|
|
return {
|
|
"messages": [result],
|
|
"momentum_report": report,
|
|
}
|
|
|
|
return momentum_analyst_node
|