diff --git a/.gitignore b/.gitignore index 2de22850..032eab18 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ __pycache__/ .DS_Store *.csv # src/ -# eval_results/ +eval_results/ eval_data/ *.egg-info/ .env diff --git a/web_app/backend/main.py b/web_app/backend/main.py index 8029ea3d..9e6d5171 100644 --- a/web_app/backend/main.py +++ b/web_app/backend/main.py @@ -1,13 +1,11 @@ from fastapi import FastAPI, HTTPException, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel -from typing import Dict, Any, List, Optional +from typing import Dict, Any, Optional import json import os -import asyncio -from datetime import datetime, date +from datetime import datetime import glob -from pathlib import Path import uuid # Import your TradingAgents components @@ -18,6 +16,9 @@ from tradingagents.default_config import DEFAULT_CONFIG app = FastAPI(title="TradingAgents API", version="1.0.0") +# Centralized results directory to avoid repetition +RESULTS_BASE = "/home/brabus61/Desktop/Github Repos/TradingAgents/scripts/eval_results" + # Configure CORS app.add_middleware( CORSMiddleware, @@ -69,6 +70,8 @@ async def run_analysis_task(job_id: str, symbol: str, analysis_date: str, config # Initialize TradingAgents jobs[job_id].progress = "Setting up trading graph..." + + # Do not set API keys in code. Use environment variables or a secure secret manager. ta = TradingAgentsGraph(debug=True, config=config) # Run the analysis @@ -93,33 +96,37 @@ async def run_analysis_task(job_id: str, symbol: str, analysis_date: str, config async def start_analysis(request: AnalysisRequest, background_tasks: BackgroundTasks): """Start a new trading analysis""" job_id = str(uuid.uuid4()) - + + # Normalize inputs + symbol = request.symbol.upper().strip() + date = request.date.strip() + # Validate date format try: - datetime.strptime(request.date, "%Y-%m-%d") + datetime.strptime(date, "%Y-%m-%d") except ValueError: raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD") - + # Initialize job jobs[job_id] = JobStatus( job_id=job_id, status="queued", progress="Analysis queued" ) - + # Start background task background_tasks.add_task( run_analysis_task, job_id, - request.symbol.upper(), - request.date, + symbol, + date, request.config_overrides or {} ) - + return AnalysisResponse( job_id=job_id, status="queued", - message=f"Analysis started for {request.symbol} on {request.date}" + message=f"Analysis started for {symbol} on {date}" ) @app.get("/analysis/status/{job_id}", response_model=JobStatus) @@ -133,7 +140,7 @@ async def get_analysis_status(job_id: str): @app.get("/results/companies") async def get_companies(): """Get list of companies with analysis results""" - results_dir = "/home/brabus61/Desktop/Github Repos/TradingAgents/scripts/eval_results" + results_dir = RESULTS_BASE if not os.path.exists(results_dir): return {"companies": []} @@ -176,7 +183,7 @@ async def get_companies(): @app.get("/results/{symbol}") async def get_company_results(symbol: str): """Get all analysis results for a specific company""" - results_dir = f"/home/brabus61/Desktop/Github Repos/TradingAgents/scripts/eval_results/{symbol.upper()}/TradingAgentsStrategy_logs" + results_dir = os.path.join(RESULTS_BASE, symbol.upper(), "TradingAgentsStrategy_logs") if not os.path.exists(results_dir): raise HTTPException(status_code=404, detail=f"No results found for {symbol}") @@ -214,7 +221,7 @@ async def get_company_results(symbol: str): @app.get("/transformed-results/{symbol}") async def get_transformed_company_results(symbol: str): """Get all transformed analysis results for a specific company""" - results_dir = f"/home/brabus61/Desktop/Github Repos/TradingAgents/scripts/eval_results/{symbol.upper()}/TradingAgentsStrategy_transformed_logs" + results_dir = os.path.join(RESULTS_BASE, symbol.upper(), "TradingAgentsStrategy_transformed_logs") if not os.path.exists(results_dir): raise HTTPException(status_code=404, detail=f"No transformed results found for {symbol}") @@ -255,7 +262,12 @@ async def get_transformed_company_results(symbol: str): @app.get("/results/{symbol}/{date}") async def get_specific_result(symbol: str, date: str): """Get specific analysis result""" - file_path = f"/home/brabus61/Desktop/Github Repos/TradingAgents/scripts/eval_results/{symbol.upper()}/TradingAgentsStrategy_logs/full_states_log_{date}.json" + file_path = os.path.join( + RESULTS_BASE, + symbol.upper(), + "TradingAgentsStrategy_logs", + f"full_states_log_{date}.json", + ) if not os.path.exists(file_path): raise HTTPException(status_code=404, detail=f"No result found for {symbol} on {date}") @@ -279,7 +291,12 @@ async def get_specific_result(symbol: str, date: str): @app.get("/transformed-results/{symbol}/{date}") async def get_specific_transformed_result(symbol: str, date: str): """Get specific transformed analysis result""" - file_path = f"/home/brabus61/Desktop/Github Repos/TradingAgents/scripts/eval_results/{symbol.upper()}/TradingAgentsStrategy_transformed_logs/full_states_log_{date}_transformed.json" + file_path = os.path.join( + RESULTS_BASE, + symbol.upper(), + "TradingAgentsStrategy_transformed_logs", + f"full_states_log_{date}_transformed.json", + ) if not os.path.exists(file_path): raise HTTPException(status_code=404, detail=f"Transformed result not found for {symbol} on {date}") diff --git a/web_app/frontend/src/App.js b/web_app/frontend/src/App.js index d5e527ff..ad6ca535 100644 --- a/web_app/frontend/src/App.js +++ b/web_app/frontend/src/App.js @@ -304,7 +304,7 @@ function App() { const runAnalysis = async () => { setIsRunningAnalysis(true); try { - const response = await axios.post('/run-analysis', analysisForm); + const response = await axios.post('/analysis/start', analysisForm); alert('Analysis completed successfully!'); setShowAnalysisModal(false); setAnalysisForm({ symbol: '', date: '' }); diff --git a/web_app/frontend/src/components/TransformedDataAdapter.tsx b/web_app/frontend/src/components/TransformedDataAdapter.tsx index e0b26a7e..8bf572fd 100644 --- a/web_app/frontend/src/components/TransformedDataAdapter.tsx +++ b/web_app/frontend/src/components/TransformedDataAdapter.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import AnalysisWidgets from '../pages/AnalysisWidgets.tsx'; // New interface for the transformed JSON structure interface TransformedAnalysisData { @@ -379,58 +378,226 @@ const TransformedDataAdapter: React.FC = ({ analysi widgets_config: transformedData?.widgets_config || buildTransformedWidgetsConfig(transformedData) }; - // Minimalist Technical Indicators widget (single-card dashboard) - const TechnicalIndicatorsOnly: React.FC<{ data: TransformedAnalysisData }> = ({ data }) => { - const ti = data?.technical_indicators || {} as any; + // Minimalist dashboard that shows ALL main sections of transformed JSON + const MinimalTransformedDashboard: React.FC<{ data: TransformedAnalysisData }> = ({ data }) => { + const md = data?.metadata as any || {}; + const fd = data?.financial_data as any || {}; + const ti = data?.technical_indicators as any || {}; + const istrat = data?.investment_strategy as any || {}; + const ds = data?.debate_summary as any || {}; + const txt = data?.text_content as any || {}; + const trends = ti?.trend_directions || {}; const fmt = (v: any, d = 0) => { const n = Number(v); - return Number.isFinite(n) ? n.toFixed(d) : 'โ€”'; + if (v === null || v === undefined || Number.isNaN(n)) return '-'; + return Number.isFinite(n) ? n.toFixed(d) : String(v); }; - const trendChip = (label: string, dir?: string) => { - const map: Record = { BULLISH: 'bg-green-100 text-green-800', BEARISH: 'bg-red-100 text-red-800', NEUTRAL: 'bg-gray-100 text-gray-700' }; - const cls = map[String(dir || 'NEUTRAL').toUpperCase()] || map.NEUTRAL; - return {label}: {dir || 'NEUTRAL'}; + const fmtNumber = (v: any, d = 0) => { + const n = Number(v); + if (!Number.isFinite(n)) return '-'; + return n.toLocaleString(undefined, { maximumFractionDigits: d, minimumFractionDigits: d }); }; + const fmtCurrency = (v: any, d = 2) => { + const n = Number(v); + if (!Number.isFinite(n)) return '-'; + return n.toLocaleString(undefined, { style: 'currency', currency: 'USD', maximumFractionDigits: d, minimumFractionDigits: d }); + }; + const fmtPercent = (v: any, d = 2) => { + const n = Number(v); + if (!Number.isFinite(n)) return '-'; + return `${n.toFixed(d)}%`; + }; + const toneFromNumber = (v: any): 'pos' | 'neg' | 'neutral' => { + const n = Number(v); + if (!Number.isFinite(n)) return 'neutral'; + if (n > 0) return 'pos'; + if (n < 0) return 'neg'; + return 'neutral'; + }; + const Stat: React.FC<{ label: string; value: React.ReactNode; sub?: React.ReactNode; tone?: 'pos' | 'neg' | 'neutral' }> + = ({ label, value, sub, tone = 'neutral' }) => { + const toneCls = tone === 'pos' ? 'text-green-600' : tone === 'neg' ? 'text-red-600' : 'text-gray-900'; + const ringCls = tone === 'pos' ? 'ring-green-200' : tone === 'neg' ? 'ring-red-200' : 'ring-gray-200'; + return ( +
+

{label}

+

{value}

+ {sub &&

{sub}

} +
+ ); + }; + + const row = (label: string, value: any, fmtDigits?: number) => ( +
+ {label} + {fmtDigits === undefined ? (value ?? '-') : fmt(value, fmtDigits)} +
+ ); + const chip = (label: string, val?: string) => ( + {label}: {val ?? '-'} + ); + + // Tabs state and memoized content for text-heavy sections + const debateTabs = React.useMemo(() => ([ + { key: 'bull', label: 'Bull', content: Array.isArray(ds?.bull_key_points) ? ds.bull_key_points : [] as any[] }, + { key: 'bear', label: 'Bear', content: Array.isArray(ds?.bear_key_points) ? ds.bear_key_points : [] as any[] }, + { key: 'neutral', label: 'Neutral', content: ds?.neutral_perspective ? [ds.neutral_perspective] : [] as any[] }, + { key: 'final', label: 'Final', content: ds?.final_decision_rationale ? [ds.final_decision_rationale] : [] as any[] }, + ]), [ds]); + const firstDebateWithContent = React.useMemo( + () => debateTabs.find(t => (t.content?.length ?? 0) > 0)?.key || 'bull', + [debateTabs] + ); + const [activeDebateTab, setActiveDebateTab] = React.useState(firstDebateWithContent); + React.useEffect(() => { setActiveDebateTab(firstDebateWithContent); }, [firstDebateWithContent]); + + const reportTabsAll = React.useMemo(() => ([ + { key: 'market', title: txt?.market_report?.title || 'Market Report', bullets: txt?.market_report?.key_takeaways, content: txt?.market_report?.content }, + { key: 'sentiment', title: txt?.sentiment_report?.title || 'Sentiment Report', bullets: txt?.sentiment_report?.recent_developments, content: txt?.sentiment_report?.content }, + { key: 'fundamentals', title: txt?.fundamentals_report?.title || 'Fundamentals Report', bullets: txt?.fundamentals_report?.financial_highlights, content: txt?.fundamentals_report?.content }, + ]), [txt]); + const availableReports = React.useMemo( + () => reportTabsAll.filter(t => t.title || (Array.isArray(t.bullets) && t.bullets.length) || t.content), + [reportTabsAll] + ); + const [activeReportTab, setActiveReportTab] = React.useState(availableReports[0]?.key || 'market'); + React.useEffect(() => { setActiveReportTab(availableReports[0]?.key || 'market'); }, [availableReports]); + return ( -
-
-
-

Technical Indicators

-
+
+
+ {/* Metadata - Full width */} +
+

Company Information

+
+ + + + +
+
+ + {/* Two column layout for remaining sections */} +
+ {/* Debate Summary with sub-tabs */} +
+

Debate Summary

-

RSI

-

{fmt(ti?.rsi, 2)}

-
-
-

MACD

-

{fmt(ti?.macd, 3)}

-

Signal: {fmt(ti?.macd_signal, 3)}

-
-
-

SMA 50

-

{fmt(ti?.sma_50, 2)}

-
-
-

SMA 200

-

{fmt(ti?.sma_200, 2)}

-
- {ti?.ema_10 !== undefined && ( -
-

EMA 10

-

{fmt(ti?.ema_10, 2)}

+
+ {debateTabs.map(t => ( + + ))} +
+
+ {debateTabs.find(t => t.key === activeDebateTab)?.content?.length ? ( +
    + {(debateTabs.find(t => t.key === activeDebateTab)?.content as any[]).map((c, i) =>
  • {c}
  • )} +
+ ) : ( +

No content.

+ )}
- )} -
-

ATR

-

{fmt(ti?.atr, 3)}

-
- {trendChip('SMA 50', trends?.sma_50)} - {trendChip('SMA 200', trends?.sma_200)} - {trends?.ema_10 !== undefined && trendChip('EMA 10', trends?.ema_10)} - {trends?.price_action !== undefined && trendChip('Price', trends?.price_action)} + + {/* Text Content with sub-tabs */} +
+

Reports

+ {!availableReports.length ? ( +

No reports available.

+ ) : ( +
+
+ {availableReports.map(t => ( + + ))} +
+ {(() => { + const activeTab = availableReports.find(t => t.key === activeReportTab); + if (!activeTab) return null; + return ( +
+ {Array.isArray(activeTab.bullets) && activeTab.bullets.length > 0 && ( +
    + {activeTab.bullets.map((k: string, i: number) =>
  • {k}
  • )} +
+ )} + {activeTab.content && ( +

{activeTab.content}

+ )} +
+ ); + })()} +
+ )} +
+ + {/* Financial Data */} +
+

Financial Data

+
+ + + + + + + + + + +
+
+ + {/* Technical Indicators */} +
+

Technical Indicators

+
+ + + + + + + + + + + + +
+
+ {chip('Trend SMA 50', trends?.sma_50)} + {chip('Trend SMA 200', trends?.sma_200)} + {trends?.ema_10 !== undefined && chip('Trend EMA 10', trends?.ema_10)} + {trends?.price_action !== undefined && chip('Price', trends?.price_action)} +
+
+ + {/* Investment Strategy */} +
+

Investment Strategy

+
+ + + ( +
  • + {fmtCurrency(t?.target_price)} + {t?.action && {t.action}} + {t?.rationale && {t.rationale}} +
    +
  • + ))} /> +
    @@ -438,8 +605,8 @@ const TransformedDataAdapter: React.FC = ({ analysi ); }; - // Only render the minimalist Technical Indicators widget for transformed data - return ; + // Render minimalist dashboard with all keys from the transformed JSON + return ; }; export default TransformedDataAdapter; diff --git a/web_app/frontend/src/services/transformedDataService.ts b/web_app/frontend/src/services/transformedDataService.ts index 1ac0173b..54bcb825 100644 --- a/web_app/frontend/src/services/transformedDataService.ts +++ b/web_app/frontend/src/services/transformedDataService.ts @@ -101,6 +101,21 @@ class TransformedDataService { return `${symbol}|${date}`; } + private buildFileEntry(companySymbol: string, result: any): TransformedAnalysis { + const id = this.makeCacheKey(companySymbol, result.date); + return { + filename: result.filename, + date: result.date, + modified_at: result.modified_at, + file_size: result.file_size, + preview: result.preview, + error: result.error, + symbol: companySymbol, + displayName: `${companySymbol} - ${result.date}`, + id + }; + } + async getAvailableFiles(): Promise { if (this.cachedFiles) { return this.cachedFiles; @@ -108,38 +123,23 @@ class TransformedDataService { try { const companiesResponse = await axios.get(`${this.baseUrl}/results/companies`); - const companiesData = companiesResponse.data; - const companies = companiesData.companies || []; - - const files: TransformedAnalysis[] = []; - - for (const company of companies) { - if (company.transformed_analyses > 0) { - try { - const resultsResponse = await axios.get(`${this.baseUrl}/transformed-results/${company.symbol}`); - const resultsData = resultsResponse.data; - const results = resultsData.results || []; - - for (const result of results) { - const id = this.makeCacheKey(company.symbol, result.date); - files.push({ - filename: result.filename, - date: result.date, - modified_at: result.modified_at, - file_size: result.file_size, - preview: result.preview, - error: result.error, - symbol: company.symbol, - displayName: `${company.symbol} - ${result.date}`, - id - }); - } - } catch (error) { - console.warn(`Failed to fetch transformed results for ${company.symbol}:`, error); - } + const companies = (companiesResponse.data?.companies || []) + .filter((c: any) => (c?.transformed_analyses ?? 0) > 0); + + // Fetch each company's transformed results in parallel + const companyFetches = companies.map(async (company: any) => { + try { + const resultsResponse = await axios.get(`${this.baseUrl}/transformed-results/${company.symbol}`); + const results = resultsResponse.data?.results || []; + return results.map((r: any) => this.buildFileEntry(company.symbol, r)); + } catch (err) { + console.warn(`Failed to fetch transformed results for ${company.symbol}:`, err); + return [] as TransformedAnalysis[]; } - } - + }); + + const resultsByCompany = await Promise.all(companyFetches); + const files = resultsByCompany.flat(); this.cachedFiles = files; } catch (error) { console.warn('Could not load transformed data files:', error); @@ -164,7 +164,12 @@ class TransformedDataService { const files = await this.getAvailableFiles(); const fileEntry = files.find(f => f.symbol === symbol && f.date === date); if (!fileEntry) { - throw new Error(`File not found in index: ${symbol} ${date}`); + // Fallback: fetch directly if not in index + const response = await axios.get(`${this.baseUrl}/transformed-results/${symbol}/${date}`); + const data: TransformedData = response.data.data; + this.validateTransformedData(data); + this.cachedData.set(cacheKey, data); + return data; } const response = await axios.get(`${this.baseUrl}/transformed-results/${fileEntry.symbol}/${date}`); diff --git a/web_app/test_backend.py b/web_app/test_backend.py new file mode 100644 index 00000000..8b4caa48 --- /dev/null +++ b/web_app/test_backend.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +""" +Test script to verify TradingAgents backend API endpoints +""" + +import requests +import json +import sys +import os + +def test_backend(): + """Test all backend endpoints""" + base_url = "http://localhost:8000" + + print("๐Ÿงช Testing TradingAgents Backend API") + print("=" * 50) + + # Test 1: Health check + print("\n1. Testing health check...") + try: + response = requests.get(f"{base_url}/health", timeout=5) + if response.status_code == 200: + print("โœ… Health check passed") + print(f" Response: {response.json()}") + else: + print(f"โŒ Health check failed: {response.status_code}") + return False + except requests.exceptions.ConnectionError: + print("โŒ Backend not running. Please start the backend server first:") + print(" cd web_app/backend && python main.py") + return False + except Exception as e: + print(f"โŒ Health check error: {e}") + return False + + # Test 2: Companies endpoint + print("\n2. Testing companies endpoint...") + try: + response = requests.get(f"{base_url}/results/companies", timeout=10) + if response.status_code == 200: + data = response.json() + companies = data.get('companies', []) + print(f"โœ… Companies endpoint working - Found {len(companies)} companies") + for company in companies: + print(f" - {company['symbol']}: {company['total_analyses']} analyses, {company.get('transformed_analyses', 0)} transformed") + else: + print(f"โŒ Companies endpoint failed: {response.status_code}") + print(f" Response: {response.text}") + except Exception as e: + print(f"โŒ Companies endpoint error: {e}") + + # Test 3: Transformed data endpoints + print("\n3. Testing transformed data endpoints...") + try: + # Get companies first + response = requests.get(f"{base_url}/results/companies", timeout=10) + if response.status_code == 200: + companies = response.json().get('companies', []) + + for company in companies: + if company.get('transformed_analyses', 0) > 0: + symbol = company['symbol'] + print(f"\n Testing {symbol} transformed data...") + + # Test company transformed results + response = requests.get(f"{base_url}/transformed-results/{symbol}", timeout=10) + if response.status_code == 200: + results = response.json().get('results', []) + print(f" โœ… {symbol} has {len(results)} transformed analyses") + + # Test specific transformed result + if results: + first_result = results[0] + date = first_result['date'] + response = requests.get(f"{base_url}/transformed-results/{symbol}/{date}", timeout=10) + if response.status_code == 200: + data = response.json() + print(f" โœ… Specific analysis loaded for {symbol} on {date}") + + # Check data structure + if 'metadata' in data and 'financial_data' in data: + print(f" โœ… Data structure is correct") + metadata = data['metadata'] + print(f" - Company: {metadata.get('company_ticker', 'N/A')}") + print(f" - Recommendation: {metadata.get('final_recommendation', 'N/A')}") + print(f" - Confidence: {metadata.get('confidence_level', 'N/A')}") + print(f" - Current Price: ${metadata.get('current_price', 0)}") + else: + print(f" โš ๏ธ Data structure may be incomplete") + else: + print(f" โŒ Failed to load specific analysis: {response.status_code}") + else: + print(f" โŒ Failed to load {symbol} transformed data: {response.status_code}") + print(f" Response: {response.text}") + + break # Test only first company with transformed data + + except Exception as e: + print(f"โŒ Transformed data endpoints error: {e}") + + print("\n" + "=" * 50) + print("๐Ÿ Backend API test completed") + return True + +if __name__ == "__main__": + test_backend()