Merge pull request #4 from aguzererler/copilot/review-last-commit

Address review feedback on Global Macro Scanner (PR #2)
This commit is contained in:
ahmet guzererler 2026-03-15 13:14:54 +01:00 committed by GitHub
commit 08575188c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 107 additions and 1554 deletions

7
.gitignore vendored
View File

@ -217,3 +217,10 @@ __marimo__/
# Cache
**/data_cache/
# Scan results and execution plans (generated artifacts)
results/
plans/
# Backup files
*.backup

View File

@ -6,12 +6,12 @@ Multi-agent LLM trading framework using LangGraph for financial analysis and dec
## Development Environment
**Conda Environment**: `trasingagetns`
**Conda Environment**: `tradingagents`
Before starting any development work, activate the conda environment:
```bash
conda activate trasingagetns
conda activate tradingagents
```
## Architecture

View File

@ -1178,6 +1178,30 @@ def run_analysis():
display_complete_report(final_state)
def _is_scanner_error(result: str) -> bool:
"""Return True when *result* indicates an error or missing data from a scanner tool."""
error_prefixes = (
"Error",
"No data",
"No quotes",
"No movers",
"No news",
"No industry",
"Invalid",
"Alpha Vantage",
)
return any(result.startswith(prefix) for prefix in error_prefixes)
def _invoke_and_save(tool, args: dict, save_dir: Path, filename: str, label: str) -> str:
"""Invoke a scanner tool, print a preview, and save the result if it is valid."""
result = tool.invoke(args)
if not _is_scanner_error(result):
(save_dir / filename).write_text(result)
console.print(result[:500] + "..." if len(result) > 500 else result)
return result
def run_scan():
console.print(Panel("[bold green]Global Macro Scanner[/bold green]", border_style="green"))
default_date = datetime.datetime.now().strftime("%Y-%m-%d")
@ -1190,35 +1214,20 @@ def run_scan():
# Call scanner tools
console.print("[bold]1. Market Movers[/bold]")
movers = get_market_movers.invoke({"category": "day_gainers"})
if not (movers.startswith("Error") or movers.startswith("No data")):
(save_dir / "market_movers.txt").write_text(movers)
console.print(movers[:500] + "..." if len(movers) > 500 else movers)
_invoke_and_save(get_market_movers, {"category": "day_gainers"}, save_dir, "market_movers.txt", "Market Movers")
console.print("[bold]2. Market Indices[/bold]")
indices = get_market_indices.invoke({})
if not (indices.startswith("Error") or indices.startswith("No data")):
(save_dir / "market_indices.txt").write_text(indices)
console.print(indices[:500] + "..." if len(indices) > 500 else indices)
_invoke_and_save(get_market_indices, {}, save_dir, "market_indices.txt", "Market Indices")
console.print("[bold]3. Sector Performance[/bold]")
sectors = get_sector_performance.invoke({})
if not (sectors.startswith("Error") or sectors.startswith("No data")):
(save_dir / "sector_performance.txt").write_text(sectors)
console.print(sectors[:500] + "..." if len(sectors) > 500 else sectors)
_invoke_and_save(get_sector_performance, {}, save_dir, "sector_performance.txt", "Sector Performance")
console.print("[bold]4. Industry Performance (Technology)[/bold]")
industry = get_industry_performance.invoke({"sector_key": "technology"})
if not (industry.startswith("Error") or industry.startswith("No data")):
(save_dir / "industry_performance.txt").write_text(industry)
console.print(industry[:500] + "..." if len(industry) > 500 else industry)
_invoke_and_save(get_industry_performance, {"sector_key": "technology"}, save_dir, "industry_performance.txt", "Industry Performance")
console.print("[bold]5. Topic News (Market)[/bold]")
news = get_topic_news.invoke({"topic": "market", "limit": 10})
if not (news.startswith("Error") or news.startswith("No data")):
(save_dir / "topic_news.txt").write_text(news)
console.print(news[:500] + "..." if len(news) > 500 else news)
_invoke_and_save(get_topic_news, {"topic": "market", "limit": 10}, save_dir, "topic_news.txt", "Topic News")
console.print(f"[green]Results saved to {save_dir}[/green]")

File diff suppressed because it is too large Load Diff

View File

@ -1,82 +0,0 @@
# Data Layer Fix and Test Plan for Global Macro Analyzer
## Current State Assessment
- ✅ pyproject.toml configured correctly
- ✅ Removed stray scanner_tools.py files outside tradingagents/
- ✅ yfinance_scanner.py implements all required functions
- ✅ alpha_vantage_scanner.py implements fallback get_market_movers_alpha_vantage correctly
- ✅ scanner_tools.py wrappers properly use route_to_vendor for all scanner methods
- ✅ default_config.py updated with scanner_data vendor configuration
- ✅ All scanner tools import successfully without runtime errors
## Outstanding Issues
- CLI scan command not yet implemented in cli/main.py
- Scanner graph components (MacroScannerGraph) not yet created
- No end-to-end testing of the data layer functionality
## Fix Plan
### 1. Implement Scanner Graph Components
Create the following files in tradingagents/graph/:
- scanner_setup.py: Graph setup logic for scanner components
- scanner_conditional_logic.py: Conditional logic for scanner graph flow
- scanner_graph.py: Main MacroScannerGraph class
### 2. Add Scan Command to CLI
Modify cli/main.py to include:
- @app.command() def scan(): entry point
- Date prompt (default: today)
- LLM provider config prompt (reuse existing helpers)
- MacroScannerGraph instantiation and scan() method call
- Rich panel display for results
- Report saving to results/macro_scan/{date}/ directory
### 3. Create MacroScannerGraph
Implement the scanner graph that:
- Runs parallel Phase 1 scanners (geopolitical, market movers, sectors)
- Coordinates Phase 2 industry deep dive
- Produces Phase 3 macro synthesis output
- Uses ScannerState for state management
### 4. End-to-End Testing
Execute the scan command and verify:
- Rich panels display correctly for each report section
- Top-10 stock watchlist is generated and displayed
- Reports are saved to results/macro_scan/{date}/ directory
- No import or runtime errors occur
## Implementation Steps
1. [ ] Create scanner graph components (scanner_setup.py, scanner_conditional_logic.py, scanner_graph.py)
2. [ ] Add scan command to cli/main.py with proper argument handling
3. [ ] Implement MacroScannerGraph with proper node/edge connections
4. [ ] Test scan command functionality
5. [ ] Verify output formatting and file generation
6. [ ] Document test results and any issues found
## Verification Criteria
- ✅ All scanner tools can be imported and used
- ✅ CLI scan command executes without errors
- ✅ Rich panels display market movers, indices, sector performance, and news
- ✅ Top-10 stock watchlist is generated and displayed
- ✅ Reports saved to results/macro_scan/{date}/ directory
- ✅ No runtime exceptions or import errors
## Contingency
- If errors occur during scan execution, check:
- Vendor routing configuration in default_config.py
- Function implementations in yfinance_scanner.py and alpha_vantage_scanner.py
- Graph node/edge connections in scanner graph components
- Rich panel formatting and output generation logic

View File

@ -1,49 +0,0 @@
# Data Layer Fix and Test Plan
## Goal
Verify and test the data layer for the Global Macro Analyzer implementation.
## Prerequisites
- Python environment with dependencies installed
- yfinance and alpha_vantage configured
## Steps
1. Import and test scanner tools individually
2. Run CLI scan command
3. Validate output
4. Document results
## Testing Scanner Tools
- Test get_market_movers
- Test get_market_indices
- Test get_sector_performance
- Test get_industry_performance
- Test get_topic_news
## Running CLI Scan
- Command: python -m tradingagents scan --date 2026-03-14
- Expected output: Rich panels with market movers, indices, sector performance, news, and top-10 watchlist
## Expected Results
- No import errors
- Successful execution without exceptions
- Output files generated under results/
- Top-10 stock watchlist displayed
## Contingency
- If errors occur, check import paths and configuration
- Verify default_config.py scanner_data setting is correct
- Ensure vendor routing works correctly
## Next Steps
- Address any failures
- Refine output formatting
- Add additional test cases

View File

@ -1,25 +0,0 @@
# Industry Performance: Technology
# Data retrieved on: 2026-03-15 11:17:42
| Company | Symbol | Industry | Market Cap | Change % |
|---------|--------|----------|------------|----------|
| NVIDIA Corporation | N/A | N/A | N/A | N/A |
| Apple Inc. | N/A | N/A | N/A | N/A |
| Microsoft Corporation | N/A | N/A | N/A | N/A |
| Broadcom Inc. | N/A | N/A | N/A | N/A |
| Micron Technology, Inc. | N/A | N/A | N/A | N/A |
| Oracle Corporation | N/A | N/A | N/A | N/A |
| Palantir Technologies Inc. | N/A | N/A | N/A | N/A |
| Advanced Micro Devices, Inc. | N/A | N/A | N/A | N/A |
| Cisco Systems, Inc. | N/A | N/A | N/A | N/A |
| Applied Materials, Inc. | N/A | N/A | N/A | N/A |
| Lam Research Corporation | N/A | N/A | N/A | N/A |
| International Business Machine | N/A | N/A | N/A | N/A |
| Intel Corporation | N/A | N/A | N/A | N/A |
| KLA Corporation | N/A | N/A | N/A | N/A |
| Salesforce, Inc. | N/A | N/A | N/A | N/A |
| Texas Instruments Incorporated | N/A | N/A | N/A | N/A |
| Arista Networks, Inc. | N/A | N/A | N/A | N/A |
| Amphenol Corporation | N/A | N/A | N/A | N/A |
| Shopify Inc. | N/A | N/A | N/A | N/A |
| Uber Technologies, Inc. | N/A | N/A | N/A | N/A |

View File

@ -1,10 +0,0 @@
# Major Market Indices
# Data retrieved on: 2026-03-15 11:17:38
| Index | Current Price | Change | Change % | 52W High | 52W Low |
|-------|---------------|--------|----------|----------|----------|
| S&P 500 | 6632.19 | -40.43 | -0.61% | 7002.28 | 4835.04 |
| Dow Jones | 46558.47 | -119.38 | -0.26% | 50512.79 | 36611.78 |
| NASDAQ | 22105.36 | -206.62 | -0.93% | 24019.99 | 14784.03 |
| VIX (Volatility Index) | 27.19 | -0.10 | -0.37% | 60.13 | 13.38 |
| Russell 2000 | 2480.05 | -8.94 | -0.36% | 2735.10 | 1732.99 |

View File

@ -1,20 +0,0 @@
# Market Movers: Day Gainers
# Data retrieved on: 2026-03-15 11:17:38
| Symbol | Name | Price | Change % | Volume | Market Cap |
|--------|------|-------|----------|--------|------------|
| NP | Neptune Insurance Holdings Inc | $21.87 | 20.23% | 924,853 | $3,021,417,984 |
| VEON | VEON Ltd. | $50.60 | 14.20% | 687,398 | $3,491,177,216 |
| KLAR | Klarna Group plc | $15.91 | 8.82% | 8,979,495 | $6,006,150,656 |
| KYIV | Kyivstar Group Ltd. | $11.07 | 8.53% | 2,498,383 | $2,555,660,032 |
| GLXY | Galaxy Digital Inc. | $22.35 | 8.34% | 7,046,744 | $8,730,140,672 |
| BLLN | BillionToOne, Inc. | $69.11 | 7.93% | 230,655 | $3,165,566,720 |
| IBRX | ImmunityBio, Inc. | $8.39 | 7.29% | 30,384,030 | $8,625,855,488 |
| SNDK | Sandisk Corporation | $661.62 | 6.92% | 18,684,442 | $97,655,758,848 |
| SSL | Sasol Ltd. | $11.31 | 6.70% | 5,267,106 | $7,210,551,296 |
| SEDG | SolarEdge Technologies, Inc. | $37.44 | 6.39% | 1,971,961 | $2,260,113,920 |
| MARA | MARA Holdings, Inc. | $9.32 | 6.39% | 73,011,343 | $3,543,786,496 |
| MUR | Murphy Oil Corporation | $36.81 | 6.02% | 5,770,011 | $5,257,585,664 |
| ADPT | Adaptive Biotechnologies Corpo | $13.17 | 5.78% | 3,892,105 | $2,027,937,280 |
| NIO | NIO Inc. | $5.86 | 5.59% | 57,679,174 | $14,817,943,552 |
| CRDO | Credo Technology Group Holding | $117.69 | 5.49% | 4,460,224 | $21,707,913,216 |

View File

@ -1,16 +0,0 @@
# Sector Performance Overview
# Data retrieved on: 2026-03-15 11:17:40
| Sector | 1-Day % | 1-Week % | 1-Month % | YTD % |
|--------|---------|----------|-----------|-------|
| Communication Services | N/A | N/A | N/A | N/A |
| Consumer Cyclical | N/A | N/A | N/A | N/A |
| Consumer Defensive | N/A | N/A | N/A | N/A |
| Energy | N/A | N/A | N/A | N/A |
| Financial Services | N/A | N/A | N/A | N/A |
| Healthcare | N/A | N/A | N/A | N/A |
| Industrials | N/A | N/A | N/A | N/A |
| Basic Materials | N/A | N/A | N/A | N/A |
| Real Estate | N/A | N/A | N/A | N/A |
| Technology | N/A | N/A | N/A | N/A |
| Utilities | N/A | N/A | N/A | N/A |

View File

@ -1,33 +0,0 @@
# News for Topic: market
# Data retrieved on: 2026-03-15 11:17:42
### Opinion: A Stock Market Crash Is Much More Likely Now Than It Was 2 Months Ago (source: Motley Fool)
Link: https://finance.yahoo.com/m/f5bf5eda-ecb7-3918-9b7a-0b4ebce070cf/opinion%3A-a-stock-market-crash.html
### UBS: AI investment is lone buffer for emerging markets as energy costs soar (source: Investing.com)
Link: https://finance.yahoo.com/news/ubs-ai-investment-lone-buffer-021705455.html
### The Stock Market May Be Shifting From Risky Tech Stocks to Safer Sectors. Here Are 3 Stocks to Buy Before They Soar. (source: Motley Fool)
Link: https://finance.yahoo.com/m/36037b15-c941-3d1a-8b65-3fef291ca4ad/the-stock-market-may-be.html
### BizTips: Boost your business by using the marketing funnel (source: Cape Cod Times)
Link: https://finance.yahoo.com/m/22db7f27-19ca-372f-9eb3-d6bfe6531a87/biztips%3A-boost-your-business.html
### Sandisk (SNDK) Rockets 25.5%, Investors Makes Use of Market Bloodbath for Gains (source: Insider Monkey)
Link: https://finance.yahoo.com/news/sandisk-sndk-rockets-25-5-094041196.html
### Goldman: AI PCs to buck 10% market slump as edge computing demand accelerates (source: Investing.com)
Link: https://finance.yahoo.com/news/goldman-ai-pcs-buck-10-003503068.html
### Fed to weigh interest rates amid Iran war, potential price increases (source: USA TODAY)
Link: https://finance.yahoo.com/m/fd7d2f56-6374-324a-87a7-ead11eed5d62/fed-to-weigh-interest-rates.html
### Is Mobileye (MBLY) Now Offering Value After A 49% One Year Share Price Decline (source: Simply Wall St.)
Link: https://finance.yahoo.com/news/mobileye-mbly-now-offering-value-080612183.html
### Will the Trump Bull Market Come to an Abrupt End Due to the Iran War? History Offers Its Objective and Potentially Uncomfortable Take. (source: Motley Fool)
Link: https://finance.yahoo.com/m/1b1369ec-855e-3380-9ed4-274a58d0c2be/will-the-trump-bull-market.html
### 3 Magnificent High-Yield Dividend Stocks to Buy and Hold (source: Motley Fool)
Link: https://finance.yahoo.com/m/1f5939f6-20c4-3c84-ac7a-f41d6218897c/3-magnificent-high-yield.html

View File

@ -15,7 +15,7 @@ import pytest
# Set up the Python path to include the project root
import sys
sys.path.insert(0, '/Users/Ahmet/Repo/TradingAgents')
sys.path.insert(0, str(Path(__file__).parent.parent))
from tradingagents.agents.utils.scanner_tools import (
get_market_movers,

View File

@ -25,7 +25,7 @@ def get_market_movers_alpha_vantage(
return f"Invalid category '{category}'. Must be one of: day_gainers, day_losers, most_actives"
if category == 'most_actives':
return "Alpha Vantage does not support 'most_actives' category. Please use yfinance instead."
return "Error: Alpha Vantage does not support 'most_actives'. Use yfinance (default vendor) for this category."
# Make API request for TOP_GAINERS_LOSERS endpoint
response = _make_api_request("TOP_GAINERS_LOSERS", {})
@ -38,7 +38,7 @@ def get_market_movers_alpha_vantage(
return f"Error from Alpha Vantage: {data['Error Message']}"
if "Note" in data:
return f"Alpha Vantage API limit reached: {data['Note']}"
return f"Error: Alpha Vantage API limit reached: {data['Note']}"
# Map category to Alpha Vantage response key
if category == 'day_gainers':
@ -46,7 +46,7 @@ def get_market_movers_alpha_vantage(
elif category == 'day_losers':
key = 'top_losers'
else:
return f"Unsupported category: {category}"
return f"Error: unsupported category '{category}'"
if key not in data:
return f"No data found for {category}"
@ -74,7 +74,7 @@ def get_market_movers_alpha_vantage(
if isinstance(price, str):
try:
price = f"${float(price):.2f}"
except:
except (ValueError, TypeError):
pass
if isinstance(change_pct, str):
change_pct = change_pct.rstrip('%') # Remove % if present
@ -83,7 +83,7 @@ def get_market_movers_alpha_vantage(
if isinstance(volume, (int, str)):
try:
volume = f"{int(volume):,}"
except:
except (ValueError, TypeError):
pass
result_str += f"| {symbol} | {price} | {change_pct} | {volume} |\n"

View File

@ -191,8 +191,7 @@ def route_to_vendor(method: str, *args, **kwargs):
try:
return impl_func(*args, **kwargs)
except Exception:
# Continue to next vendor on any exception
continue
except AlphaVantageRateLimitError:
continue # Only rate limits trigger fallback
raise RuntimeError(f"No available vendor for '{method}'")

View File

@ -31,7 +31,7 @@ def get_market_movers_yfinance(
# Use yfinance screener module's screen function
data = yf.screener.screen(screener_keys[category], count=25)
if not data or 'quotes' not in data:
if not data or not isinstance(data, dict) or 'quotes' not in data:
return f"No data found for {category}"
quotes = data['quotes']
@ -97,29 +97,46 @@ def get_market_indices_yfinance() -> str:
result_str += "| Index | Current Price | Change | Change % | 52W High | 52W Low |\n"
result_str += "|-------|---------------|--------|----------|----------|----------|\n"
# Batch-download 1-day history for all symbols in a single request
symbols = list(indices.keys())
indices_history = yf.download(symbols, period="2d", auto_adjust=True, progress=False, threads=True)
for symbol, name in indices.items():
try:
ticker = yf.Ticker(symbol)
info = ticker.info
hist = ticker.history(period="1d")
if hist.empty:
# fast_info is a lightweight cached property (no extra HTTP call)
fast = ticker.fast_info
# Extract history for this symbol from the batch download
try:
if len(symbols) > 1:
closes = indices_history["Close"][symbol].dropna()
else:
closes = indices_history["Close"].dropna()
except KeyError:
closes = None
if closes is None or len(closes) == 0:
result_str += f"| {name} | N/A | - | - | - | - |\n"
continue
current_price = hist['Close'].iloc[-1]
prev_close = info.get('previousClose', current_price)
current_price = closes.iloc[-1]
prev_close = closes.iloc[-2] if len(closes) >= 2 else fast.previous_close
if prev_close is None or prev_close == 0:
prev_close = current_price
change = current_price - prev_close
change_pct = (change / prev_close * 100) if prev_close else 0
high_52w = info.get('fiftyTwoWeekHigh', 'N/A')
low_52w = info.get('fiftyTwoWeekLow', 'N/A')
high_52w = fast.year_high
low_52w = fast.year_low
# Format numbers
current_str = f"{current_price:.2f}"
change_str = f"{change:+.2f}"
change_pct_str = f"{change_pct:+.2f}%"
high_str = f"{high_52w:.2f}" if isinstance(high_52w, (int, float)) else high_52w
low_str = f"{low_52w:.2f}" if isinstance(low_52w, (int, float)) else low_52w
high_str = f"{high_52w:.2f}" if isinstance(high_52w, (int, float)) else str(high_52w)
low_str = f"{low_52w:.2f}" if isinstance(low_52w, (int, float)) else str(low_52w)
result_str += f"| {name} | {current_str} | {change_str} | {change_pct_str} | {high_str} | {low_str} |\n"
@ -140,7 +157,9 @@ def get_sector_performance_yfinance() -> str:
Formatted string containing sector performance data
"""
try:
# Get all GICS sectors
# All 11 standard GICS (Global Industry Classification Standard) sectors.
# These keys are fixed by yfinance's Sector API and cannot be fetched
# dynamically; the GICS taxonomy is maintained by MSCI/S&P and is stable.
sector_keys = [
"communication-services",
"consumer-cyclical",

View File

@ -3,57 +3,47 @@
from typing import Any
from tradingagents.agents.utils.scanner_states import ScannerState
_ERROR_PREFIXES = ("Error", "No data", "No quotes", "No movers", "No news", "No industry", "Invalid")
def _report_is_valid(report: str) -> bool:
"""Return True when *report* contains usable data (non-empty, non-error)."""
if not report or not report.strip():
return False
return not any(report.startswith(prefix) for prefix in _ERROR_PREFIXES)
class ScannerConditionalLogic:
"""Conditional logic for scanner graph flow control."""
def should_continue_geopolitical(self, state: ScannerState) -> bool:
"""
Determine if geopolitical scanning should continue.
Args:
state: Current scanner state
Returns:
bool: Whether to continue geopolitical scanning
Returns True only when the geopolitical report contains usable data.
"""
# Always continue for initial scan - no filtering logic implemented
return True
return _report_is_valid(state.get("geopolitical_report", ""))
def should_continue_movers(self, state: ScannerState) -> bool:
"""
Determine if market movers scanning should continue.
Args:
state: Current scanner state
Returns:
bool: Whether to continue market movers scanning
Returns True only when the market movers report contains usable data.
"""
# Always continue for initial scan - no filtering logic implemented
return True
return _report_is_valid(state.get("market_movers_report", ""))
def should_continue_sector(self, state: ScannerState) -> bool:
"""
Determine if sector scanning should continue.
Args:
state: Current scanner state
Returns:
bool: Whether to continue sector scanning
Returns True only when the sector performance report contains usable data.
"""
# Always continue for initial scan - no filtering logic implemented
return True
return _report_is_valid(state.get("sector_performance_report", ""))
def should_continue_industry(self, state: ScannerState) -> bool:
"""
Determine if industry deep dive should continue.
Args:
state: Current scanner state
Returns:
bool: Whether to continue industry deep dive
Returns True only when the industry deep dive report contains usable data.
"""
# Always continue for initial scan - no filtering logic implemented
return True
return _report_is_valid(state.get("industry_deep_dive_report", ""))