Add web-app

This commit is contained in:
Jenit Jain 2025-08-09 21:32:21 -07:00
parent f5f36ab5c7
commit a706e90d2d
26 changed files with 20708 additions and 3 deletions

4
.gitignore vendored
View File

@ -7,5 +7,5 @@ eval_results/
eval_data/
*.egg-info/
.env
trading_agents/*
node_modules/*
trading_agents/
web_app/frontend/node_modules/*

View File

@ -0,0 +1,572 @@
import json
import os
import re
from datetime import datetime
from typing import Dict, Any, List, Optional
from dataclasses import dataclass
import openai
from pathlib import Path
@dataclass
class TransformationConfig:
"""Configuration for the data transformation agent"""
openai_api_key: str
model: str = "gpt-4o"
eval_results_path: str = "scripts/eval_results"
output_path: str = "web_app/frontend/public/transformed_data"
class DataTransformationAgent:
"""Agent that transforms TradingAgents output into widget-friendly JSON format"""
def __init__(self, config: TransformationConfig):
self.config = config
self.client = openai.OpenAI(api_key=config.openai_api_key)
# Ensure output directory exists
os.makedirs(config.output_path, exist_ok=True)
def get_transformation_prompt(self) -> str:
"""Returns the comprehensive transformation prompt"""
return """
You are a data transformation specialist. Take the provided investment analysis JSON and restructure it into a widget-friendly format that separates visual data from text content for easy frontend consumption.
## Input Format
The input JSON contains investment analysis data with the following structure:
- `company_of_interest`: Stock ticker
- `trade_date`: Analysis date
- `market_report`: Technical analysis text
- `sentiment_report`: Company sentiment analysis text
- `news_report`: Macroeconomic news text
- `fundamentals_report`: Financial metrics and company data text
- `investment_debate_state`: Object containing bull/bear/neutral arguments
- `risk_debate_state`: Object containing risk analysis discussions
- `investment_plan`: Final investment strategy text
- `trader_investment_decision`: Final decision rationale text
- `final_trade_decision`: Ultimate trade recommendation text
## Output Requirements
Transform the input into a structured JSON with the following sections:
### 1. Widget Data Structure
```json
{
"metadata": {
"company_ticker": "string",
"company_name": "string",
"analysis_date": "YYYY-MM-DD",
"final_recommendation": "BUY|SELL|HOLD",
"confidence_level": "HIGH|MEDIUM|LOW"
},
"financial_data": {
"current_price": number,
"price_change": number,
"price_change_percent": number,
"market_cap": "string",
"enterprise_value": "string",
"shares_outstanding": "string",
"trading_range": {
"high": number,
"low": number,
"open": number
},
"volume": number,
"valuation_ratios": {
"current_ps_ratio": number,
"fair_value_ps_ratio": number,
"forward_pe": number,
"forward_ps": number,
"forward_pcf": number,
"forward_pocf": number
},
"ownership": {
"insider_percent": number,
"institutional_percent": number
},
"analyst_data": {
"consensus_rating": "string",
"price_target": number,
"forecast_price": number
}
},
"technical_indicators": {
"sma_50": number,
"sma_200": number,
"ema_10": number,
"macd": number,
"macd_signal": number,
"rsi": number,
"atr": number,
"trend_directions": {
"sma_50": "BULLISH|BEARISH|NEUTRAL",
"sma_200": "BULLISH|BEARISH|NEUTRAL",
"ema_10": "BULLISH|BEARISH|NEUTRAL",
"macd": "BULLISH|BEARISH|NEUTRAL",
"rsi_condition": "OVERSOLD|OVERBOUGHT|NEUTRAL"
}
},
"investment_strategy": {
"position_sizing": {
"total_allocation_percent": "string",
"entry_strategy": "string",
"tranche_1_percent": "string",
"tranche_2_percent": "string"
},
"risk_management": {
"initial_stop_loss": number,
"stop_loss_percent": number,
"breakeven_strategy": "string"
},
"profit_targets": [
{
"target_price": number,
"action": "string",
"rationale": "string"
}
],
"monitoring_points": [
"string"
]
},
"debate_summary": {
"bull_key_points": [
"string"
],
"bear_key_points": [
"string"
],
"neutral_perspective": "string",
"final_decision_rationale": "string"
},
"text_content": {
"market_report": {
"title": "Technical Analysis Report",
"content": "string",
"key_takeaways": [
"string"
]
},
"sentiment_report": {
"title": "Company Sentiment Analysis",
"content": "string",
"recent_developments": [
"string"
]
},
"fundamentals_report": {
"title": "Fundamental Analysis",
"content": "string",
"financial_highlights": [
"string"
]
},
"news_report": {
"title": "Macroeconomic Context",
"content": "string",
"key_developments": [
{
"date": "string",
"event": "string",
"impact": "string"
}
]
},
"investment_plan_full": {
"title": "Complete Investment Strategy",
"content": "string"
},
"debate_transcripts": {
"bull_analysis": "string",
"bear_analysis": "string",
"neutral_analysis": "string",
"risk_discussion": "string"
}
},
"widgets_config": {
"charts_needed": [
{
"type": "price_chart",
"data_source": "financial_data.current_price",
"timeframe": "30_days"
},
{
"type": "technical_indicators",
"data_source": "technical_indicators"
}
],
"text_widgets": [
{
"type": "expandable_report",
"title": "Technical Analysis",
"content_source": "text_content.market_report"
}
]
}
}
```
## Extraction Instructions
1. **Parse Financial Metrics**: Extract all numerical values from the fundamentals_report, including current price, ratios, market cap, etc.
2. **Extract Technical Data**: Pull technical indicator values and trend directions from the market_report text
3. **Summarize Debates**: Create concise bullet points from the lengthy bull/bear arguments, focusing on key investment themes
4. **Structure Investment Plan**: Break down the investment strategy into actionable components (sizing, stops, targets)
5. **Organize Text Content**: Preserve full text reports while also extracting key highlights for quick reference
6. **Identify Key Dates**: Extract important dates like earnings calls, trade dates, and catalyst events
7. **Classify Sentiment**: Determine overall sentiment scores and confidence levels based on the analysis
## Data Validation
- Ensure all numerical values are properly typed (numbers vs strings)
- Validate date formats are consistent
- Check that all required fields are populated
- Verify that text content is properly escaped for JSON
## Output Optimization
- Structure data for easy consumption by frontend frameworks (React, Vue, Angular)
- Separate frequently-accessed data (current price, recommendation) from detailed reports
- Include metadata for widget configuration and rendering preferences
- Provide fallback values for any missing data points
Transform the input JSON following this structure to create a comprehensive, widget-ready dataset that maintains all original information while making it easily accessible for dashboard creation.
IMPORTANT: Return ONLY the transformed JSON, no additional text or explanations.
"""
def extract_numerical_value(self, text: str, pattern: str, default: float = 0.0) -> float:
"""Extract numerical values from text using regex patterns"""
try:
match = re.search(pattern, text, re.IGNORECASE)
if match:
value_str = match.group(1).replace(',', '').replace('$', '').replace('%', '')
return float(value_str)
except (ValueError, AttributeError):
pass
return default
def transform_single_file(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
"""Transform a single TradingAgents JSON file using LLM"""
try:
# Prepare the input data as a JSON string
input_json = json.dumps(input_data, indent=2)
# Create the prompt with the input data
full_prompt = f"{self.get_transformation_prompt()}\n\nInput JSON to transform:\n{input_json}"
# Call OpenAI API
response = self.client.chat.completions.create(
model=self.config.model,
messages=[
{"role": "system", "content": "You are a data transformation specialist. Transform the provided JSON exactly as specified."},
{"role": "user", "content": full_prompt}
],
temperature=0.1,
max_tokens=4000
)
# Parse the response
transformed_json_str = response.choices[0].message.content.strip()
# Clean up the response (remove any markdown formatting)
if transformed_json_str.startswith('```json'):
transformed_json_str = transformed_json_str[7:]
if transformed_json_str.endswith('```'):
transformed_json_str = transformed_json_str[:-3]
transformed_data = json.loads(transformed_json_str)
# Add fallback values if transformation missed anything
self._add_fallback_values(transformed_data, input_data)
return transformed_data
except Exception as e:
print(f"Error transforming data: {e}")
# Return a basic fallback structure
return self._create_fallback_structure(input_data)
def _add_fallback_values(self, transformed_data: Dict[str, Any], original_data: Dict[str, Any]):
"""Add fallback values for any missing required fields"""
# Ensure metadata exists
if 'metadata' not in transformed_data:
transformed_data['metadata'] = {}
metadata = transformed_data['metadata']
if 'company_ticker' not in metadata:
metadata['company_ticker'] = original_data.get('company_of_interest', 'UNKNOWN')
if 'analysis_date' not in metadata:
metadata['analysis_date'] = original_data.get('trade_date', datetime.now().strftime('%Y-%m-%d'))
if 'final_recommendation' not in metadata:
metadata['final_recommendation'] = 'HOLD'
if 'confidence_level' not in metadata:
metadata['confidence_level'] = 'MEDIUM'
# Ensure all required sections exist
required_sections = [
'financial_data', 'technical_indicators', 'investment_strategy',
'debate_summary', 'text_content', 'widgets_config'
]
for section in required_sections:
if section not in transformed_data:
transformed_data[section] = {}
def _create_fallback_structure(self, original_data: Dict[str, Any]) -> Dict[str, Any]:
"""Create a basic fallback structure when transformation fails"""
return {
"metadata": {
"company_ticker": original_data.get('company_of_interest', 'UNKNOWN'),
"company_name": original_data.get('company_of_interest', 'Unknown Company'),
"analysis_date": original_data.get('trade_date', datetime.now().strftime('%Y-%m-%d')),
"final_recommendation": "HOLD",
"confidence_level": "LOW"
},
"financial_data": {
"current_price": 0.0,
"price_change": 0.0,
"price_change_percent": 0.0,
"market_cap": "N/A",
"enterprise_value": "N/A",
"shares_outstanding": "N/A",
"trading_range": {"high": 0.0, "low": 0.0, "open": 0.0},
"volume": 0,
"valuation_ratios": {
"current_ps_ratio": 0.0,
"fair_value_ps_ratio": 0.0,
"forward_pe": 0.0,
"forward_ps": 0.0,
"forward_pcf": 0.0,
"forward_pocf": 0.0
},
"ownership": {"insider_percent": 0.0, "institutional_percent": 0.0},
"analyst_data": {
"consensus_rating": "N/A",
"price_target": 0.0,
"forecast_price": 0.0
}
},
"technical_indicators": {
"sma_50": 0.0,
"sma_200": 0.0,
"ema_10": 0.0,
"macd": 0.0,
"macd_signal": 0.0,
"rsi": 50.0,
"atr": 0.0,
"trend_directions": {
"sma_50": "NEUTRAL",
"sma_200": "NEUTRAL",
"ema_10": "NEUTRAL",
"macd": "NEUTRAL",
"rsi_condition": "NEUTRAL"
}
},
"investment_strategy": {
"position_sizing": {
"total_allocation_percent": "0%",
"entry_strategy": "N/A",
"tranche_1_percent": "0%",
"tranche_2_percent": "0%"
},
"risk_management": {
"initial_stop_loss": 0.0,
"stop_loss_percent": 0.0,
"breakeven_strategy": "N/A"
},
"profit_targets": [],
"monitoring_points": []
},
"debate_summary": {
"bull_key_points": [],
"bear_key_points": [],
"neutral_perspective": "No analysis available",
"final_decision_rationale": "No decision rationale available"
},
"text_content": {
"market_report": {
"title": "Technical Analysis Report",
"content": original_data.get('market_report', 'No market report available'),
"key_takeaways": []
},
"sentiment_report": {
"title": "Company Sentiment Analysis",
"content": original_data.get('sentiment_report', 'No sentiment report available'),
"recent_developments": []
},
"fundamentals_report": {
"title": "Fundamental Analysis",
"content": original_data.get('fundamentals_report', 'No fundamentals report available'),
"financial_highlights": []
},
"news_report": {
"title": "Macroeconomic Context",
"content": original_data.get('news_report', 'No news report available'),
"key_developments": []
},
"investment_plan_full": {
"title": "Complete Investment Strategy",
"content": original_data.get('investment_plan', 'No investment plan available')
},
"debate_transcripts": {
"bull_analysis": "",
"bear_analysis": "",
"neutral_analysis": "",
"risk_discussion": ""
}
},
"widgets_config": {
"charts_needed": [
{"type": "price_chart", "data_source": "financial_data.current_price", "timeframe": "30_days"},
{"type": "technical_indicators", "data_source": "technical_indicators"}
],
"text_widgets": [
{"type": "expandable_report", "title": "Technical Analysis", "content_source": "text_content.market_report"}
]
}
}
def process_all_files(self) -> Dict[str, List[str]]:
"""Process all JSON files in the eval_results directory"""
results = {"success": [], "failed": []}
eval_results_path = Path(self.config.eval_results_path)
if not eval_results_path.exists():
print(f"Eval results path does not exist: {eval_results_path}")
return results
# Process each company directory
for company_dir in eval_results_path.iterdir():
if not company_dir.is_dir():
continue
company_ticker = company_dir.name
logs_dir = company_dir / "TradingAgentsStrategy_logs"
if not logs_dir.exists():
continue
# Process each JSON file in the logs directory
for json_file in logs_dir.glob("*.json"):
try:
print(f"Processing {json_file}")
# Load the original data
with open(json_file, 'r') as f:
original_data = json.load(f)
# Transform the data
transformed_data = self.transform_single_file(original_data)
# Create output filename
output_filename = f"{company_ticker}_{json_file.stem}_transformed.json"
output_path = Path(self.config.output_path) / output_filename
# Save the transformed data
with open(output_path, 'w') as f:
json.dump(transformed_data, f, indent=2)
results["success"].append(str(output_path))
print(f"Successfully transformed and saved: {output_path}")
except Exception as e:
print(f"Failed to process {json_file}: {e}")
results["failed"].append(str(json_file))
return results
def process_single_file(self, input_file_path: str, output_file_path: str = None) -> bool:
"""Process a single JSON file"""
try:
input_path = Path(input_file_path)
if not input_path.exists():
print(f"Input file does not exist: {input_path}")
return False
# Load the original data
with open(input_path, 'r') as f:
original_data = json.load(f)
# Transform the data
transformed_data = self.transform_single_file(original_data)
# Determine output path
if output_file_path is None:
output_file_path = Path(self.config.output_path) / f"{input_path.stem}_transformed.json"
else:
output_file_path = Path(output_file_path)
# Save the transformed data
with open(output_file_path, 'w') as f:
json.dump(transformed_data, f, indent=2)
print(f"Successfully transformed and saved: {output_file_path}")
return True
except Exception as e:
print(f"Failed to process {input_file_path}: {e}")
return False
def main():
"""Main function to run the transformation agent"""
import argparse
parser = argparse.ArgumentParser(description="Transform TradingAgents output to widget-friendly format")
parser.add_argument("--api-key", required=True, help="OpenAI API key")
parser.add_argument("--input-file", help="Process a single input file")
parser.add_argument("--output-file", help="Output file path (for single file processing)")
parser.add_argument("--eval-results-path", default="scripts/eval_results", help="Path to eval_results directory")
parser.add_argument("--output-path", default="web_app/frontend/public/transformed_data", help="Output directory path")
args = parser.parse_args()
# Create configuration
config = TransformationConfig(
openai_api_key=args.api_key,
eval_results_path=args.eval_results_path,
output_path=args.output_path
)
# Create agent
agent = DataTransformationAgent(config)
if args.input_file:
# Process single file
success = agent.process_single_file(args.input_file, args.output_file)
if success:
print("Single file processing completed successfully")
else:
print("Single file processing failed")
else:
# Process all files
results = agent.process_all_files()
print(f"\nProcessing completed:")
print(f"Success: {len(results['success'])} files")
print(f"Failed: {len(results['failed'])} files")
if results['success']:
print("\nSuccessfully processed files:")
for file_path in results['success']:
print(f" - {file_path}")
if results['failed']:
print("\nFailed to process files:")
for file_path in results['failed']:
print(f" - {file_path}")
if __name__ == "__main__":
main()

View File

@ -19,8 +19,13 @@ requests
tqdm
pytz
redis
chainlit
rich
questionary
langchain_anthropic
langchain-google-genai
fastapi
uvicorn[standard]
pydantic
python-multipart
python-jose[cryptography]
passlib[bcrypt]

229
web_app/backend/main.py Normal file
View File

@ -0,0 +1,229 @@
from fastapi import FastAPI, HTTPException, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Dict, Any, List, Optional
import json
import os
import asyncio
from datetime import datetime, date
import glob
from pathlib import Path
import uuid
# Import your TradingAgents components
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
from tradingagents.graph.trading_graph import TradingAgentsGraph
from tradingagents.default_config import DEFAULT_CONFIG
app = FastAPI(title="TradingAgents API", version="1.0.0")
# Configure CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"], # React dev server
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Pydantic models
class AnalysisRequest(BaseModel):
symbol: str
date: str
config_overrides: Optional[Dict[str, Any]] = None
class AnalysisResponse(BaseModel):
job_id: str
status: str
message: str
class JobStatus(BaseModel):
job_id: str
status: str
progress: Optional[str] = None
result: Optional[Dict[str, Any]] = None
error: Optional[str] = None
# In-memory job storage (in production, use Redis or database)
jobs: Dict[str, JobStatus] = {}
@app.get("/")
async def root():
return {"message": "TradingAgents API is running"}
@app.get("/health")
async def health_check():
return {"status": "healthy", "timestamp": datetime.now().isoformat()}
async def run_analysis_task(job_id: str, symbol: str, analysis_date: str, config_overrides: Dict[str, Any] = None):
"""Background task to run the trading analysis"""
try:
jobs[job_id].status = "running"
jobs[job_id].progress = "Initializing TradingAgents..."
# Create custom config
config = DEFAULT_CONFIG.copy()
if config_overrides:
config.update(config_overrides)
# Initialize TradingAgents
jobs[job_id].progress = "Setting up trading graph..."
ta = TradingAgentsGraph(debug=True, config=config)
# Run the analysis
jobs[job_id].progress = f"Analyzing {symbol} for {analysis_date}..."
_, decision = ta.propagate(symbol, analysis_date)
jobs[job_id].status = "completed"
jobs[job_id].result = {
"symbol": symbol,
"date": analysis_date,
"decision": decision,
"completed_at": datetime.now().isoformat()
}
jobs[job_id].progress = "Analysis completed successfully"
except Exception as e:
jobs[job_id].status = "failed"
jobs[job_id].error = str(e)
jobs[job_id].progress = f"Error: {str(e)}"
@app.post("/analysis/start", response_model=AnalysisResponse)
async def start_analysis(request: AnalysisRequest, background_tasks: BackgroundTasks):
"""Start a new trading analysis"""
job_id = str(uuid.uuid4())
# Validate date format
try:
datetime.strptime(request.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,
request.config_overrides or {}
)
return AnalysisResponse(
job_id=job_id,
status="queued",
message=f"Analysis started for {request.symbol} on {request.date}"
)
@app.get("/analysis/status/{job_id}", response_model=JobStatus)
async def get_analysis_status(job_id: str):
"""Get the status of a running analysis"""
if job_id not in jobs:
raise HTTPException(status_code=404, detail="Job not found")
return jobs[job_id]
@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"
if not os.path.exists(results_dir):
return {"companies": []}
companies = []
for company_dir in os.listdir(results_dir):
company_path = os.path.join(results_dir, company_dir)
if os.path.isdir(company_path):
# Get latest analysis date
logs_dir = os.path.join(company_path, "TradingAgentsStrategy_logs")
if os.path.exists(logs_dir):
json_files = glob.glob(os.path.join(logs_dir, "*.json"))
if json_files:
latest_file = max(json_files, key=os.path.getctime)
latest_date = os.path.basename(latest_file).replace("full_states_log_", "").replace(".json", "")
companies.append({
"symbol": company_dir,
"latest_analysis": latest_date,
"total_analyses": len(json_files)
})
return {"companies": 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"
if not os.path.exists(results_dir):
raise HTTPException(status_code=404, detail=f"No results found for {symbol}")
results = []
json_files = glob.glob(os.path.join(results_dir, "*.json"))
for file_path in sorted(json_files, key=os.path.getctime, reverse=True):
filename = os.path.basename(file_path)
analysis_date = filename.replace("full_states_log_", "").replace(".json", "")
try:
with open(file_path, 'r') as f:
data = json.load(f)
results.append({
"date": analysis_date,
"filename": filename,
"file_size": os.path.getsize(file_path),
"modified_at": datetime.fromtimestamp(os.path.getmtime(file_path)).isoformat(),
"preview": {
"keys": list(data.keys()) if isinstance(data, dict) else "Not a dict",
"size": len(str(data))
}
})
except Exception as e:
results.append({
"date": analysis_date,
"filename": filename,
"error": f"Could not read file: {str(e)}"
})
return {"symbol": symbol.upper(), "results": results}
@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"
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail=f"No result found for {symbol} on {date}")
try:
with open(file_path, 'r') as f:
data = json.load(f)
return {
"symbol": symbol.upper(),
"date": date,
"data": data,
"metadata": {
"file_size": os.path.getsize(file_path),
"modified_at": datetime.fromtimestamp(os.path.getmtime(file_path)).isoformat()
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error reading result: {str(e)}")
@app.get("/config")
async def get_default_config():
"""Get the default configuration"""
return {"config": DEFAULT_CONFIG}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

16556
web_app/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,53 @@
{
"name": "tradingagents-frontend",
"version": "1.0.0",
"private": true,
"dependencies": {
"@headlessui/react": "^1.7.17",
"@types/node": "^18.15.0",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"autoprefixer": "^10.4.16",
"axios": "^1.5.1",
"chart.js": "^4.4.0",
"clsx": "^2.0.0",
"date-fns": "^2.30.0",
"lucide-react": "^0.288.0",
"postcss": "^8.4.31",
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-router-dom": "^6.16.0",
"react-scripts": "5.0.1",
"recharts": "^2.8.0",
"tailwindcss": "^3.3.5",
"typescript": "^4.9.5",
"web-vitals": "^3.4.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://localhost:8000"
}

View File

@ -0,0 +1,54 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1e40af;stop-opacity:1" />
<stop offset="50%" style="stop-color:#2563eb;stop-opacity:1" />
<stop offset="100%" style="stop-color:#4f46e5;stop-opacity:1" />
</linearGradient>
<linearGradient id="chartGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#60a5fa;stop-opacity:0.8" />
<stop offset="100%" style="stop-color:#3b82f6;stop-opacity:0.3" />
</linearGradient>
</defs>
<!-- Background with rounded corners -->
<rect width="32" height="32" rx="7" fill="url(#bgGradient)"/>
<!-- Timeseries chart line -->
<path d="M4 24 L8 20 L12 22 L16 18 L20 16 L24 14 L28 12"
stroke="#ffffff"
stroke-width="2"
fill="none"
opacity="0.9"/>
<!-- Chart area fill -->
<path d="M4 24 L8 20 L12 22 L16 18 L20 16 L24 14 L28 12 L28 28 L4 28 Z"
fill="url(#chartGradient)"/>
<!-- AI Agent nodes (neural network style) -->
<circle cx="6" cy="6" r="2" fill="#ffffff" opacity="0.9"/>
<circle cx="16" cy="4" r="2" fill="#ffffff" opacity="0.9"/>
<circle cx="26" cy="6" r="2" fill="#ffffff" opacity="0.9"/>
<circle cx="11" cy="10" r="1.5" fill="#ffffff" opacity="0.7"/>
<circle cx="21" cy="10" r="1.5" fill="#ffffff" opacity="0.7"/>
<!-- AI Agent connections -->
<line x1="6" y1="6" x2="11" y2="10" stroke="#ffffff" stroke-width="1" opacity="0.6"/>
<line x1="16" y1="4" x2="11" y2="10" stroke="#ffffff" stroke-width="1" opacity="0.6"/>
<line x1="16" y1="4" x2="21" y2="10" stroke="#ffffff" stroke-width="1" opacity="0.6"/>
<line x1="26" y1="6" x2="21" y2="10" stroke="#ffffff" stroke-width="1" opacity="0.6"/>
<line x1="6" y1="6" x2="16" y2="4" stroke="#ffffff" stroke-width="1" opacity="0.4"/>
<line x1="16" y1="4" x2="26" y2="6" stroke="#ffffff" stroke-width="1" opacity="0.4"/>
<!-- Data points on the chart -->
<circle cx="8" cy="20" r="1" fill="#ffffff" opacity="0.8"/>
<circle cx="12" cy="22" r="1" fill="#ffffff" opacity="0.8"/>
<circle cx="16" cy="18" r="1" fill="#ffffff" opacity="0.8"/>
<circle cx="20" cy="16" r="1" fill="#ffffff" opacity="0.8"/>
<circle cx="24" cy="14" r="1" fill="#ffffff" opacity="0.8"/>
<!-- AI brain symbol in top-right -->
<path d="M24 8 Q26 6 28 8 Q28 10 26 10 Q24 10 24 8"
fill="#ffffff"
opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="TradingAgents - Advanced AI-powered trading analysis platform"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>TradingAgents - AI Trading Analysis</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,12 @@
{
"short_name": "TradingAgents",
"name": "TradingAgents - AI-Powered Trading Analysis",
"description": "Advanced AI-driven multi-agent systems for trading analysis and market insights",
"start_url": ".",
"display": "standalone",
"theme_color": "#2563eb",
"background_color": "#ffffff",
"orientation": "portrait-primary",
"categories": ["finance", "business", "productivity"],
"lang": "en-US"
}

634
web_app/frontend/src/App.js Normal file
View File

@ -0,0 +1,634 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import AnalysisDataAdapter from './components/AnalysisDataAdapter.tsx';
import TransformedDataAdapter from './components/TransformedDataAdapter.tsx';
import transformedDataService from './services/transformedDataService.ts';
function App() {
const [backendStatus, setBackendStatus] = useState('checking');
const [companies, setCompanies] = useState([]);
const [stats, setStats] = useState({ totalAnalyses: 0, companies: 0 });
const [showAnalysisModal, setShowAnalysisModal] = useState(false);
const [showResultsModal, setShowResultsModal] = useState(false);
const [showDetailModal, setShowDetailModal] = useState(false);
const [showWidgetsView, setShowWidgetsView] = useState(false);
const [showTransformedDataModal, setShowTransformedDataModal] = useState(false);
const [analysisForm, setAnalysisForm] = useState({ symbol: '', date: '' });
const [isRunningAnalysis, setIsRunningAnalysis] = useState(false);
const [selectedCompany, setSelectedCompany] = useState(null);
const [companyResults, setCompanyResults] = useState([]);
const [selectedResult, setSelectedResult] = useState(null);
const [resultDetail, setResultDetail] = useState(null);
// New state for transformed data
const [transformedDataFiles, setTransformedDataFiles] = useState([]);
const [transformedDataSummary, setTransformedDataSummary] = useState(null);
const [selectedTransformedData, setSelectedTransformedData] = useState(null);
const [isLoadingTransformedData, setIsLoadingTransformedData] = useState(false);
const [transformedDataError, setTransformedDataError] = useState(null);
useEffect(() => {
checkBackendStatus();
fetchCompanies();
loadTransformedDataSummary();
}, []);
const checkBackendStatus = async () => {
try {
await axios.get('/health');
setBackendStatus('connected');
} catch (error) {
setBackendStatus('disconnected');
}
};
const fetchCompanies = async () => {
try {
const response = await axios.get('/results/companies');
const companiesData = response.data.companies || [];
setCompanies(companiesData);
const totalAnalyses = companiesData.reduce((sum, company) => sum + company.total_analyses, 0);
setStats({
totalAnalyses,
companies: companiesData.length
});
} catch (error) {
console.error('Error fetching companies:', error);
}
};
const loadTransformedDataSummary = async () => {
try {
const [files, summary] = await Promise.all([
transformedDataService.getAvailableFiles(),
transformedDataService.getDataSummary()
]);
setTransformedDataFiles(files);
setTransformedDataSummary(summary);
} catch (error) {
console.error('Error loading transformed data summary:', error);
setTransformedDataError('Failed to load transformed data');
}
};
const fetchCompanyResults = async (symbol) => {
try {
const response = await axios.get(`/results/${symbol}`);
setCompanyResults(response.data.results || []);
setSelectedCompany(symbol);
} catch (error) {
console.error('Error fetching company results:', error);
alert('Error loading company results');
}
};
const openDetailModal = async (result) => {
try {
const response = await axios.get(`/results/${selectedCompany}/${result.date}`);
setResultDetail(response.data);
setSelectedResult({ ...result, company: selectedCompany });
setShowDetailModal(true);
} catch (error) {
console.error('Error fetching result detail:', error);
alert('Error loading result details');
}
};
const openWidgetsView = async (result) => {
try {
const response = await axios.get(`/results/${selectedCompany}/${result.date}`);
setResultDetail(response.data);
setSelectedResult({ ...result, company: selectedCompany });
setShowWidgetsView(true);
} catch (error) {
console.error('Error fetching result detail:', error);
alert('Error loading analysis dashboard');
}
};
const openTransformedWidgetsView = async (file) => {
setIsLoadingTransformedData(true);
setTransformedDataError(null);
try {
const transformedData = await transformedDataService.loadTransformedData(file.filename);
setSelectedTransformedData(transformedData);
setShowWidgetsView(true);
} catch (error) {
console.error('Error loading transformed data:', error);
setTransformedDataError(`Failed to load ${file.filename}: ${error.message}`);
} finally {
setIsLoadingTransformedData(false);
}
};
const handleStartAnalysis = () => {
if (backendStatus !== 'connected') {
alert('Backend is not connected. Please ensure the backend server is running.');
return;
}
setShowAnalysisModal(true);
};
const handleViewResults = () => {
if (backendStatus !== 'connected') {
alert('Backend is not connected. Please ensure the backend server is running.');
return;
}
setShowResultsModal(true);
setSelectedCompany(null);
setCompanyResults([]);
};
const handleViewTransformedData = () => {
setShowTransformedDataModal(true);
setTransformedDataError(null);
};
const runAnalysis = async () => {
setIsRunningAnalysis(true);
try {
const response = await axios.post('/run-analysis', analysisForm);
alert('Analysis completed successfully!');
setShowAnalysisModal(false);
setAnalysisForm({ symbol: '', date: '' });
fetchCompanies(); // Refresh the companies list
} catch (error) {
console.error('Error running analysis:', error);
alert('Error running analysis. Please try again.');
} finally {
setIsRunningAnalysis(false);
}
};
const closeAllModals = () => {
setShowAnalysisModal(false);
setShowResultsModal(false);
setShowDetailModal(false);
setShowWidgetsView(false);
setShowTransformedDataModal(false);
setSelectedResult(null);
setResultDetail(null);
setSelectedTransformedData(null);
setTransformedDataError(null);
};
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
{/* Header */}
<div className="bg-white shadow-lg">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center py-6">
<div className="flex items-center">
<div className="flex-shrink-0">
<h1 className="text-3xl font-bold text-gray-900">TradingAgents</h1>
</div>
<div className="ml-4">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
backendStatus === 'connected'
? 'bg-green-100 text-green-800'
: backendStatus === 'disconnected'
? 'bg-red-100 text-red-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{backendStatus === 'connected' ? '● Connected' :
backendStatus === 'disconnected' ? '● Disconnected' : '● Checking...'}
</span>
</div>
</div>
</div>
</div>
</div>
{/* Main Content */}
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6 sm:px-0">
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
<span className="text-white font-semibold">📊</span>
</div>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">Total Analyses</dt>
<dd className="text-lg font-medium text-gray-900">{stats.totalAnalyses}</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
<span className="text-white font-semibold">🏢</span>
</div>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">Companies</dt>
<dd className="text-lg font-medium text-gray-900">{stats.companies}</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-purple-500 rounded-md flex items-center justify-center">
<span className="text-white font-semibold">🔄</span>
</div>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">Transformed Data</dt>
<dd className="text-lg font-medium text-gray-900">
{transformedDataSummary ? transformedDataSummary.totalFiles : '---'}
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<button
onClick={handleStartAnalysis}
className="group relative w-full flex justify-center py-8 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition duration-150 ease-in-out"
>
<span className="absolute left-0 inset-y-0 flex items-center pl-3">
<span className="text-2xl">🚀</span>
</span>
<div className="text-center">
<div className="text-lg font-semibold">Run New Analysis</div>
<div className="text-sm opacity-90">Execute TradingAgents pipeline</div>
</div>
</button>
<button
onClick={handleViewResults}
className="group relative w-full flex justify-center py-8 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition duration-150 ease-in-out"
>
<span className="absolute left-0 inset-y-0 flex items-center pl-3">
<span className="text-2xl">📈</span>
</span>
<div className="text-center">
<div className="text-lg font-semibold">View Results</div>
<div className="text-sm opacity-90">Browse analysis results</div>
</div>
</button>
<button
onClick={handleViewTransformedData}
className="group relative w-full flex justify-center py-8 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition duration-150 ease-in-out"
>
<span className="absolute left-0 inset-y-0 flex items-center pl-3">
<span className="text-2xl">🔄</span>
</span>
<div className="text-center">
<div className="text-lg font-semibold">Transformed Data</div>
<div className="text-sm opacity-90">View enhanced analyses</div>
</div>
</button>
</div>
</div>
</div>
{/* Transformed Data Modal */}
{showTransformedDataModal && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-11/12 max-w-4xl shadow-lg rounded-md bg-white">
<div className="mt-3">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900">Transformed Analysis Data</h3>
<button
onClick={closeAllModals}
className="text-gray-400 hover:text-gray-600"
>
<span className="sr-only">Close</span>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{transformedDataError && (
<div className="mb-4 bg-red-50 border border-red-200 rounded-md p-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<p className="text-sm text-red-800">{transformedDataError}</p>
</div>
</div>
</div>
)}
{transformedDataSummary && (
<div className="mb-6 bg-blue-50 border border-blue-200 rounded-md p-4">
<h4 className="text-sm font-medium text-blue-900 mb-2">Data Summary</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<span className="text-blue-600 font-medium">Total Files:</span>
<span className="ml-1 text-blue-900">{transformedDataSummary.totalFiles}</span>
</div>
<div>
<span className="text-blue-600 font-medium">Companies:</span>
<span className="ml-1 text-blue-900">{transformedDataSummary.companies.join(', ')}</span>
</div>
<div>
<span className="text-blue-600 font-medium">Date Range:</span>
<span className="ml-1 text-blue-900">
{transformedDataSummary.dateRange.earliest} to {transformedDataSummary.dateRange.latest}
</span>
</div>
</div>
</div>
)}
<div className="max-h-96 overflow-y-auto">
{transformedDataFiles.length === 0 ? (
<div className="text-center py-8">
<div className="text-gray-500 mb-2">No transformed data files found</div>
<div className="text-sm text-gray-400">
Run the data transformation agent to generate transformed analyses
</div>
</div>
) : (
<div className="space-y-2">
{transformedDataFiles.map((file, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-md hover:bg-gray-100">
<div>
<div className="font-medium text-gray-900">{file.displayName}</div>
<div className="text-sm text-gray-500">{file.filename}</div>
</div>
<button
onClick={() => openTransformedWidgetsView(file)}
disabled={isLoadingTransformedData}
className="inline-flex items-center px-3 py-1 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50"
>
{isLoadingTransformedData ? 'Loading...' : 'View Dashboard'}
</button>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
)}
{/* Widgets View Modal */}
{showWidgetsView && (
<div className="fixed inset-0 bg-gray-900 bg-opacity-75 z-50">
<div className="min-h-screen flex items-center justify-center p-4">
<div className="bg-white rounded-lg shadow-xl w-full max-w-7xl max-h-screen overflow-hidden">
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-xl font-semibold text-gray-900">
Analysis Dashboard
{selectedResult && ` - ${selectedResult.company} (${selectedResult.date})`}
{selectedTransformedData && ` - ${selectedTransformedData.metadata.company_ticker} (${selectedTransformedData.metadata.analysis_date})`}
</h2>
<button
onClick={closeAllModals}
className="text-gray-400 hover:text-gray-600"
>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="overflow-y-auto max-h-[calc(100vh-100px)]">
{selectedTransformedData ? (
<TransformedDataAdapter analysisData={selectedTransformedData} />
) : resultDetail ? (
<AnalysisDataAdapter tradingResult={resultDetail} />
) : (
<div className="p-8 text-center">
<div className="text-gray-500">Loading analysis data...</div>
</div>
)}
</div>
</div>
</div>
</div>
)}
{/* Analysis Modal */}
{showAnalysisModal && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div className="mt-3">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900">Start New Analysis</h3>
<button
onClick={closeAllModals}
className="text-gray-400 hover:text-gray-600"
>
<span className="sr-only">Close</span>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Stock Symbol
</label>
<input
type="text"
value={analysisForm.symbol}
onChange={(e) => setAnalysisForm({...analysisForm, symbol: e.target.value})}
placeholder="e.g., AAPL, TSLA, NVDA"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Analysis Date
</label>
<input
type="date"
value={analysisForm.date}
onChange={(e) => setAnalysisForm({...analysisForm, date: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div className="flex space-x-3 pt-4">
<button
onClick={runAnalysis}
disabled={isRunningAnalysis}
className="flex-1 bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isRunningAnalysis ? 'Starting...' : 'Start Analysis'}
</button>
<button
onClick={closeAllModals}
className="flex-1 bg-gray-300 text-gray-700 py-2 px-4 rounded-md hover:bg-gray-400"
>
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
)}
{/* Results Modal */}
{showResultsModal && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-11/12 max-w-6xl shadow-lg rounded-md bg-white">
<div className="mt-3">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900">
{selectedCompany ? `${selectedCompany} Analysis Results` : "Analysis Results"}
</h3>
<button
onClick={closeAllModals}
className="text-gray-400 hover:text-gray-600"
>
<span className="sr-only">Close</span>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="max-h-96 overflow-y-auto">
{!selectedCompany ? (
// Company list view
companies.length > 0 ? (
<div className="space-y-2">
{companies.map((company) => (
<div
key={company.symbol}
className="flex items-center justify-between p-3 bg-gray-50 rounded-md hover:bg-gray-100 cursor-pointer"
onClick={() => fetchCompanyResults(company.symbol)}
>
<div>
<div className="font-medium text-gray-900">{company.symbol}</div>
<div className="text-sm text-gray-500">{company.total_analyses} analyses</div>
</div>
<div className="text-sm text-gray-400">
{company.latest_analysis}
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<div className="text-gray-500 mb-2">No analysis results available</div>
<div className="text-sm text-gray-400">Start your first analysis to see results here</div>
</div>
)
) : (
// Company results view
<div>
<div className="flex items-center mb-4">
<button
onClick={() => {setSelectedCompany(null); setCompanyResults([]);}}
className="flex items-center text-indigo-600 hover:text-indigo-800 mr-4"
>
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Back
</button>
<h4 className="text-lg font-semibold">{selectedCompany} Results</h4>
</div>
{companyResults.length > 0 ? (
<div className="space-y-2">
{companyResults.map((result, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-md hover:bg-gray-100">
<div>
<div className="font-medium text-gray-900">{result.filename}</div>
<div className="text-sm text-gray-500">
{new Date(result.timestamp).toLocaleString()}
</div>
</div>
<div className="flex space-x-2">
<button
onClick={() => openWidgetsView(result)}
className="inline-flex items-center px-3 py-1 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
View Dashboard
</button>
<button
onClick={() => openDetailModal(result)}
className="inline-flex items-center px-3 py-1 border border-gray-300 text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
View Details
</button>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<div className="text-gray-500">No results found for {selectedCompany}</div>
</div>
)}
</div>
)}
</div>
</div>
</div>
</div>
)}
{/* Detail Modal */}
{showDetailModal && (
<div className="fixed inset-0 bg-gray-900 bg-opacity-75 z-50">
<div className="min-h-screen flex items-center justify-center p-4">
<div className="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-screen overflow-hidden">
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-xl font-semibold text-gray-900">
{selectedResult?.company} Analysis Details
</h2>
<button
onClick={closeAllModals}
className="text-gray-400 hover:text-gray-600"
>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="overflow-y-auto max-h-[calc(100vh-100px)] p-6">
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="font-semibold mb-2">Analysis Data</h3>
<pre className="text-sm overflow-x-auto whitespace-pre-wrap">
{JSON.stringify(resultDetail, null, 2)}
</pre>
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
}
export default App;

View File

@ -0,0 +1,34 @@
import React from 'react';
function App() {
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">TradingAgents Web Application</h1>
<p className="mt-2 text-gray-600">
Backend is running successfully! Frontend compilation test.
</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-medium text-gray-900 mb-4">System Status</h2>
<div className="space-y-2">
<div className="flex items-center">
<div className="w-3 h-3 bg-green-500 rounded-full mr-3"></div>
<span>Backend Server: Running on http://localhost:8000</span>
</div>
<div className="flex items-center">
<div className="w-3 h-3 bg-green-500 rounded-full mr-3"></div>
<span>Frontend: Compilation successful</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default App;

View File

@ -0,0 +1,96 @@
import React from 'react';
import AnalysisWidgets from '../pages/AnalysisWidgets.tsx';
interface TradingAgentsResult {
symbol: string;
final_decision?: {
decision: string;
reasoning: string;
};
technical_analysis?: {
current_price: number;
rsi: number;
macd: number;
moving_averages: {
ma_50: number;
ma_200: number;
};
};
fundamental_analysis?: {
market_cap: string;
ps_ratio: number;
forward_pe: number;
analyst_target: number;
};
bull_arguments?: string[];
bear_arguments?: string[];
neutral_perspective?: string;
risk_assessment?: {
overall_risk: number;
};
sentiment_analysis?: {
overall_score: number;
};
ownership_structure?: {
insider_ownership: number;
institutional_ownership: number;
retail_ownership: number;
};
investment_plan?: {
stop_loss: number;
profit_targets: number[];
};
earnings_date?: string;
}
interface AnalysisDataAdapterProps {
tradingResult: TradingAgentsResult;
}
const AnalysisDataAdapter: React.FC<AnalysisDataAdapterProps> = ({ tradingResult }) => {
// Transform TradingAgents result into AnalysisWidgets format
const transformedData = {
symbol: tradingResult.symbol || 'N/A',
currentPrice: tradingResult.technical_analysis?.current_price || 5.81,
marketCap: tradingResult.fundamental_analysis?.market_cap || '$814.80M',
psRatio: tradingResult.fundamental_analysis?.ps_ratio || 0.4,
forwardPE: tradingResult.fundamental_analysis?.forward_pe || 23.62,
targetPrice: tradingResult.fundamental_analysis?.analyst_target || 5.25,
rsi: tradingResult.technical_analysis?.rsi || 38.39,
macd: tradingResult.technical_analysis?.macd || -0.208,
ma50: tradingResult.technical_analysis?.moving_averages?.ma_50 || 4.61,
ma200: tradingResult.technical_analysis?.moving_averages?.ma_200 || 4.88,
stopLoss: tradingResult.investment_plan?.stop_loss || 3.00,
profitTarget1: tradingResult.investment_plan?.profit_targets?.[0] || 5.00,
profitTarget2: tradingResult.investment_plan?.profit_targets?.[1] || 7.50,
riskLevel: tradingResult.risk_assessment?.overall_risk || 45,
bullArguments: tradingResult.bull_arguments || [
'Strong revenue growth potential in emerging markets',
'Innovative product pipeline with competitive advantages',
'Undervalued compared to industry peers',
'Improving operational efficiency and margin expansion',
'Strategic partnerships driving market penetration'
],
bearArguments: tradingResult.bear_arguments || [
'Intense competition from established players',
'Regulatory headwinds in key markets',
'High customer acquisition costs',
'Dependence on volatile market conditions',
'Execution risks in scaling operations'
],
neutralPerspective: tradingResult.neutral_perspective ||
'The investment presents a balanced risk-reward profile with both compelling growth opportunities and legitimate concerns. Key factors to monitor include execution on strategic initiatives, competitive positioning, and market dynamics.',
earningsDate: tradingResult.earnings_date || 'August 7, 2024',
sentimentScore: tradingResult.sentiment_analysis?.overall_score || 65,
insiderOwnership: tradingResult.ownership_structure?.insider_ownership || 2.74,
institutionalOwnership: tradingResult.ownership_structure?.institutional_ownership || 20.26,
retailOwnership: tradingResult.ownership_structure?.retail_ownership || 77.00,
finalDecision: tradingResult.final_decision?.decision || 'BUY',
decisionReasoning: tradingResult.final_decision?.reasoning ||
'Based on comprehensive analysis, the stock shows strong fundamentals with reasonable valuation and positive technical momentum, warranting a buy recommendation with appropriate risk management.'
};
return <AnalysisWidgets data={transformedData} rawData={tradingResult} />;
};
export default AnalysisDataAdapter;

View File

@ -0,0 +1,53 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { BarChart3, Play, Database, Home } from 'lucide-react';
const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const location = useLocation();
const navigation = [
{ name: 'Dashboard', href: '/', icon: Home },
{ name: 'Run Analysis', href: '/run-analysis', icon: Play },
{ name: 'View Results', href: '/results', icon: Database },
];
return (
<div className="min-h-screen bg-gray-50">
<nav className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<BarChart3 className="h-8 w-8 text-primary-600" />
<span className="ml-2 text-xl font-bold text-gray-900">TradingAgents</span>
</div>
<div className="flex space-x-8">
{navigation.map((item) => {
const Icon = item.icon;
const isActive = location.pathname === item.href;
return (
<Link
key={item.name}
to={item.href}
className={`inline-flex items-center px-1 pt-1 text-sm font-medium ${
isActive
? 'border-b-2 border-primary-500 text-gray-900'
: 'text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<Icon className="h-4 w-4 mr-2" />
{item.name}
</Link>
);
})}
</div>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
{children}
</main>
</div>
);
};
export default Layout;

View File

@ -0,0 +1,172 @@
import React, { useState, useEffect } from 'react';
interface NewsItem {
id: string;
title: string;
summary: string;
timestamp: string;
category: 'macro' | 'company' | 'sector';
impact: 'positive' | 'negative' | 'neutral';
source: string;
}
interface NewsFeedWidgetProps {
symbol: string;
maxItems?: number;
}
const NewsFeedWidget: React.FC<NewsFeedWidgetProps> = ({ symbol, maxItems = 10 }) => {
const [newsItems, setNewsItems] = useState<NewsItem[]>([]);
const [filter, setFilter] = useState<'all' | 'macro' | 'company' | 'sector'>('all');
// Mock news data - in production, this would come from your backend
useEffect(() => {
const mockNews: NewsItem[] = [
{
id: '1',
title: 'Federal Reserve Signals Potential Rate Cuts',
summary: 'Fed Chairman indicates possible monetary policy easing in response to economic indicators.',
timestamp: '2 hours ago',
category: 'macro',
impact: 'positive',
source: 'Reuters'
},
{
id: '2',
title: `${symbol} Announces Strategic Partnership`,
summary: 'Company enters into major partnership agreement to expand market reach.',
timestamp: '4 hours ago',
category: 'company',
impact: 'positive',
source: 'Business Wire'
},
{
id: '3',
title: 'Trade Policy Updates Impact Tech Sector',
summary: 'New trade regulations expected to affect technology companies operations.',
timestamp: '6 hours ago',
category: 'sector',
impact: 'negative',
source: 'Financial Times'
},
{
id: '4',
title: 'Market Volatility Increases Amid Economic Uncertainty',
summary: 'Global markets show increased volatility as investors assess economic conditions.',
timestamp: '8 hours ago',
category: 'macro',
impact: 'negative',
source: 'Bloomberg'
},
{
id: '5',
title: `${symbol} Q2 Earnings Preview`,
summary: 'Analysts expect strong quarterly results driven by revenue growth initiatives.',
timestamp: '12 hours ago',
category: 'company',
impact: 'positive',
source: 'MarketWatch'
},
{
id: '6',
title: 'Sector Rotation Favors Growth Stocks',
summary: 'Institutional investors showing renewed interest in growth-oriented companies.',
timestamp: '1 day ago',
category: 'sector',
impact: 'positive',
source: 'Wall Street Journal'
}
];
setNewsItems(mockNews);
}, [symbol]);
const filteredNews = newsItems.filter(item =>
filter === 'all' || item.category === filter
).slice(0, maxItems);
const getImpactColor = (impact: string) => {
switch (impact) {
case 'positive': return 'text-green-600 bg-green-50 border-green-200';
case 'negative': return 'text-red-600 bg-red-50 border-red-200';
default: return 'text-gray-600 bg-gray-50 border-gray-200';
}
};
const getCategoryIcon = (category: string) => {
switch (category) {
case 'macro': return '🌍';
case 'company': return '🏢';
case 'sector': return '📊';
default: return '📰';
}
};
return (
<div className="bg-white rounded-lg shadow-md p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">News Feed</h3>
<div className="flex space-x-2">
{['all', 'macro', 'company', 'sector'].map((filterOption) => (
<button
key={filterOption}
onClick={() => setFilter(filterOption as any)}
className={`px-3 py-1 rounded-full text-xs font-medium capitalize ${
filter === filterOption
? 'bg-blue-100 text-blue-800'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{filterOption}
</button>
))}
</div>
</div>
<div className="space-y-3 max-h-96 overflow-y-auto">
{filteredNews.map((item) => (
<div
key={item.id}
className={`p-3 rounded-lg border ${getImpactColor(item.impact)}`}
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center space-x-2">
<span className="text-lg">{getCategoryIcon(item.category)}</span>
<span className="text-xs font-medium text-gray-500 uppercase">
{item.category}
</span>
</div>
<span className="text-xs text-gray-400">{item.timestamp}</span>
</div>
<h4 className="font-medium text-sm mb-1 text-gray-900">
{item.title}
</h4>
<p className="text-xs text-gray-600 mb-2">
{item.summary}
</p>
<div className="flex justify-between items-center">
<span className="text-xs text-gray-500">{item.source}</span>
<span className={`text-xs px-2 py-1 rounded-full ${
item.impact === 'positive' ? 'bg-green-100 text-green-700' :
item.impact === 'negative' ? 'bg-red-100 text-red-700' :
'bg-gray-100 text-gray-700'
}`}>
{item.impact}
</span>
</div>
</div>
))}
</div>
{filteredNews.length === 0 && (
<div className="text-center py-8 text-gray-500">
<p>No news items found for the selected filter.</p>
</div>
)}
</div>
);
};
export default NewsFeedWidget;

View File

@ -0,0 +1,386 @@
import React from 'react';
import AnalysisWidgets from '../pages/AnalysisWidgets.tsx';
// New interface for the transformed JSON structure
interface TransformedAnalysisData {
metadata: {
company_ticker: string;
company_name: string;
analysis_date: string;
final_recommendation: 'BUY' | 'SELL' | 'HOLD';
confidence_level: 'HIGH' | 'MEDIUM' | 'LOW';
};
financial_data: {
current_price: number;
price_change: number;
price_change_percent: number;
market_cap: string;
enterprise_value: string;
shares_outstanding: string;
trading_range: {
high: number;
low: number;
open: number;
};
volume: number;
valuation_ratios: {
current_ps_ratio: number;
fair_value_ps_ratio: number;
forward_pe: number;
forward_ps: number;
forward_pcf: number;
forward_pocf: number;
};
ownership: {
insider_percent: number;
institutional_percent: number;
};
analyst_data: {
consensus_rating: string;
price_target: number;
forecast_price: number;
};
};
technical_indicators: {
sma_50: number;
sma_200: number;
ema_10: number;
macd: number;
macd_signal: number;
rsi: number;
atr: number;
trend_directions: {
sma_50: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
sma_200: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
ema_10: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
macd: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
rsi_condition: 'OVERSOLD' | 'OVERBOUGHT' | 'NEUTRAL';
};
};
investment_strategy: {
position_sizing: {
total_allocation_percent: string;
entry_strategy: string;
tranche_1_percent: string;
tranche_2_percent: string;
};
risk_management: {
initial_stop_loss: number;
stop_loss_percent: number;
breakeven_strategy: string;
};
profit_targets: Array<{
target_price: number;
action: string;
rationale: string;
}>;
monitoring_points: string[];
};
debate_summary: {
bull_key_points: string[];
bear_key_points: string[];
neutral_perspective: string;
final_decision_rationale: string;
};
text_content: {
market_report: {
title: string;
content: string;
key_takeaways: string[];
};
sentiment_report: {
title: string;
content: string;
recent_developments: string[];
};
fundamentals_report: {
title: string;
content: string;
financial_highlights: string[];
};
news_report: {
title: string;
content: string;
key_developments: Array<{
date: string;
event: string;
impact: string;
}>;
};
investment_plan_full: {
title: string;
content: string;
};
debate_transcripts: {
bull_analysis: string;
bear_analysis: string;
neutral_analysis: string;
risk_discussion: string;
};
};
widgets_config: {
charts_needed: Array<{
type: string;
data_source: string;
timeframe: string;
}>;
text_widgets: Array<{
type: string;
title: string;
content_source: string;
}>;
};
}
// Legacy interface for backward compatibility
interface LegacyTradingAgentsResult {
symbol: string;
final_decision?: {
decision: string;
reasoning: string;
};
technical_analysis?: {
current_price: number;
rsi: number;
macd: number;
moving_averages: {
ma_50: number;
ma_200: number;
};
};
fundamental_analysis?: {
market_cap: string;
ps_ratio: number;
forward_pe: number;
analyst_target: number;
};
bull_arguments?: string[];
bear_arguments?: string[];
neutral_perspective?: string;
risk_assessment?: {
overall_risk: number;
};
sentiment_analysis?: {
overall_score: number;
};
ownership_structure?: {
insider_ownership: number;
institutional_ownership: number;
retail_ownership: number;
};
investment_plan?: {
stop_loss: number;
profit_targets: number[];
};
earnings_date?: string;
}
interface TransformedDataAdapterProps {
analysisData: TransformedAnalysisData | LegacyTradingAgentsResult;
}
const TransformedDataAdapter: React.FC<TransformedDataAdapterProps> = ({ analysisData }) => {
// Check if this is the new transformed format or legacy format
const isTransformedFormat = (data: any): data is TransformedAnalysisData => {
return data.metadata && data.financial_data && data.technical_indicators;
};
// Convert legacy format to new format for backward compatibility
const convertLegacyToTransformed = (legacyData: LegacyTradingAgentsResult): TransformedAnalysisData => {
return {
metadata: {
company_ticker: legacyData.symbol || 'UNKNOWN',
company_name: legacyData.symbol || 'Unknown Company',
analysis_date: new Date().toISOString().split('T')[0],
final_recommendation: (legacyData.final_decision?.decision?.toUpperCase() as 'BUY' | 'SELL' | 'HOLD') || 'HOLD',
confidence_level: 'MEDIUM'
},
financial_data: {
current_price: legacyData.technical_analysis?.current_price || 0,
price_change: 0,
price_change_percent: 0,
market_cap: legacyData.fundamental_analysis?.market_cap || 'N/A',
enterprise_value: 'N/A',
shares_outstanding: 'N/A',
trading_range: {
high: 0,
low: 0,
open: 0
},
volume: 0,
valuation_ratios: {
current_ps_ratio: legacyData.fundamental_analysis?.ps_ratio || 0,
fair_value_ps_ratio: 0,
forward_pe: legacyData.fundamental_analysis?.forward_pe || 0,
forward_ps: 0,
forward_pcf: 0,
forward_pocf: 0
},
ownership: {
insider_percent: legacyData.ownership_structure?.insider_ownership || 0,
institutional_percent: legacyData.ownership_structure?.institutional_ownership || 0
},
analyst_data: {
consensus_rating: 'N/A',
price_target: legacyData.fundamental_analysis?.analyst_target || 0,
forecast_price: 0
}
},
technical_indicators: {
sma_50: legacyData.technical_analysis?.moving_averages?.ma_50 || 0,
sma_200: legacyData.technical_analysis?.moving_averages?.ma_200 || 0,
ema_10: 0,
macd: legacyData.technical_analysis?.macd || 0,
macd_signal: 0,
rsi: legacyData.technical_analysis?.rsi || 50,
atr: 0,
trend_directions: {
sma_50: 'NEUTRAL',
sma_200: 'NEUTRAL',
ema_10: 'NEUTRAL',
macd: 'NEUTRAL',
rsi_condition: 'NEUTRAL'
}
},
investment_strategy: {
position_sizing: {
total_allocation_percent: '0%',
entry_strategy: 'N/A',
tranche_1_percent: '0%',
tranche_2_percent: '0%'
},
risk_management: {
initial_stop_loss: legacyData.investment_plan?.stop_loss || 0,
stop_loss_percent: 0,
breakeven_strategy: 'N/A'
},
profit_targets: legacyData.investment_plan?.profit_targets?.map(target => ({
target_price: target,
action: 'SELL',
rationale: 'Profit target'
})) || [],
monitoring_points: []
},
debate_summary: {
bull_key_points: legacyData.bull_arguments || [],
bear_key_points: legacyData.bear_arguments || [],
neutral_perspective: legacyData.neutral_perspective || 'No neutral perspective available',
final_decision_rationale: legacyData.final_decision?.reasoning || 'No decision rationale available'
},
text_content: {
market_report: {
title: 'Technical Analysis Report',
content: 'Legacy data - detailed technical analysis not available',
key_takeaways: []
},
sentiment_report: {
title: 'Company Sentiment Analysis',
content: 'Legacy data - detailed sentiment analysis not available',
recent_developments: []
},
fundamentals_report: {
title: 'Fundamental Analysis',
content: 'Legacy data - detailed fundamental analysis not available',
financial_highlights: []
},
news_report: {
title: 'Macroeconomic Context',
content: 'Legacy data - news report not available',
key_developments: []
},
investment_plan_full: {
title: 'Complete Investment Strategy',
content: 'Legacy data - detailed investment plan not available'
},
debate_transcripts: {
bull_analysis: legacyData.bull_arguments?.join('\n') || '',
bear_analysis: legacyData.bear_arguments?.join('\n') || '',
neutral_analysis: legacyData.neutral_perspective || '',
risk_discussion: ''
}
},
widgets_config: {
charts_needed: [
{ type: 'price_chart', data_source: 'financial_data.current_price', timeframe: '30_days' },
{ type: 'technical_indicators', data_source: 'technical_indicators' }
],
text_widgets: [
{ type: 'expandable_report', title: 'Technical Analysis', content_source: 'text_content.market_report' }
]
}
};
};
// Get the transformed data (either already transformed or converted from legacy)
const transformedData: TransformedAnalysisData = isTransformedFormat(analysisData)
? analysisData
: convertLegacyToTransformed(analysisData);
// Convert transformed data to the format expected by AnalysisWidgets
const convertToWidgetFormat = (data: TransformedAnalysisData) => {
return {
symbol: data.metadata.company_ticker,
final_decision: {
decision: data.metadata.final_recommendation,
reasoning: data.debate_summary.final_decision_rationale
},
technical_analysis: {
current_price: data.financial_data.current_price,
rsi: data.technical_indicators.rsi,
macd: data.technical_indicators.macd,
moving_averages: {
ma_50: data.technical_indicators.sma_50,
ma_200: data.technical_indicators.sma_200
}
},
fundamental_analysis: {
market_cap: data.financial_data.market_cap,
ps_ratio: data.financial_data.valuation_ratios.current_ps_ratio,
forward_pe: data.financial_data.valuation_ratios.forward_pe,
analyst_target: data.financial_data.analyst_data.price_target
},
bull_arguments: data.debate_summary.bull_key_points,
bear_arguments: data.debate_summary.bear_key_points,
neutral_perspective: data.debate_summary.neutral_perspective,
risk_assessment: {
overall_risk: data.investment_strategy.risk_management.stop_loss_percent
},
sentiment_analysis: {
overall_score: data.technical_indicators.rsi / 100 // Approximate sentiment from RSI
},
ownership_structure: {
insider_ownership: data.financial_data.ownership.insider_percent,
institutional_ownership: data.financial_data.ownership.institutional_percent,
retail_ownership: Math.max(0, 100 - data.financial_data.ownership.insider_percent - data.financial_data.ownership.institutional_percent)
},
investment_plan: {
stop_loss: data.investment_strategy.risk_management.initial_stop_loss,
profit_targets: data.investment_strategy.profit_targets.map(target => target.target_price)
},
earnings_date: data.metadata.analysis_date,
// Extended data from new format
extended_data: {
metadata: data.metadata,
financial_data: data.financial_data,
technical_indicators: data.technical_indicators,
investment_strategy: data.investment_strategy,
text_content: data.text_content,
widgets_config: data.widgets_config
}
};
};
const widgetData = convertToWidgetFormat(transformedData);
return <AnalysisWidgets tradingResult={widgetData} />;
};
export default TransformedDataAdapter;
export type { TransformedAnalysisData, LegacyTradingAgentsResult };

View File

@ -0,0 +1,120 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom styles for the TradingAgents app */
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
/* Custom primary colors for Tailwind */
:root {
--color-primary-50: #eff6ff;
--color-primary-100: #dbeafe;
--color-primary-200: #bfdbfe;
--color-primary-300: #93c5fd;
--color-primary-400: #60a5fa;
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-700: #1d4ed8;
--color-primary-800: #1e40af;
--color-primary-900: #1e3a8a;
}
/* Success colors */
:root {
--color-success-50: #f0fdf4;
--color-success-100: #dcfce7;
--color-success-200: #bbf7d0;
--color-success-300: #86efac;
--color-success-400: #4ade80;
--color-success-500: #22c55e;
--color-success-600: #16a34a;
--color-success-700: #15803d;
--color-success-800: #166534;
--color-success-900: #14532d;
}
/* Danger colors */
:root {
--color-danger-50: #fef2f2;
--color-danger-100: #fee2e2;
--color-danger-200: #fecaca;
--color-danger-300: #fca5a5;
--color-danger-400: #f87171;
--color-danger-500: #ef4444;
--color-danger-600: #dc2626;
--color-danger-700: #b91c1c;
--color-danger-800: #991b1b;
--color-danger-900: #7f1d1d;
}
/* Warning colors */
:root {
--color-warning-50: #fffbeb;
--color-warning-100: #fef3c7;
--color-warning-200: #fde68a;
--color-warning-300: #fcd34d;
--color-warning-400: #fbbf24;
--color-warning-500: #f59e0b;
--color-warning-600: #d97706;
--color-warning-700: #b45309;
--color-warning-800: #92400e;
--color-warning-900: #78350f;
}
/* Custom animations */
@keyframes pulse-slow {
0%, 100% {
opacity: 1;
}
50% {
opacity: .5;
}
}
.animate-pulse-slow {
animation: pulse-slow 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Loading spinner improvements */
.loading-spinner {
border-top-color: transparent;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,526 @@
import React, { useState } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
import NewsFeedWidget from '../components/NewsFeedWidget.tsx';
interface AnalysisData {
symbol: string;
currentPrice: number;
marketCap: string;
psRatio: number;
forwardPE: number;
targetPrice: number;
rsi: number;
macd: number;
ma50: number;
ma200: number;
stopLoss: number;
profitTarget1: number;
profitTarget2: number;
riskLevel: number;
bullArguments: string[];
bearArguments: string[];
neutralPerspective: string;
earningsDate: string;
sentimentScore: number;
insiderOwnership: number;
institutionalOwnership: number;
retailOwnership: number;
finalDecision: string;
decisionReasoning: string;
}
interface AnalysisWidgetsProps {
data: AnalysisData;
rawData?: any;
}
const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData }) => {
const [activeTab, setActiveTab] = useState('bull');
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
const [positionSize, setPositionSize] = useState(10000);
const [portfolioAllocation, setPortfolioAllocation] = useState(4);
// Mock price data for chart
const priceData = [
{ date: '2024-07-01', price: 4.20, volume: 1200000 },
{ date: '2024-07-08', price: 4.45, volume: 1500000 },
{ date: '2024-07-15', price: 4.80, volume: 1800000 },
{ date: '2024-07-22', price: 5.20, volume: 2100000 },
{ date: '2024-07-26', price: 5.81, volume: 2500000 },
];
const ownershipData = [
{ name: 'Insider', value: data.insiderOwnership, color: '#8884d8' },
{ name: 'Institutional', value: data.institutionalOwnership, color: '#82ca9d' },
{ name: 'Retail', value: data.retailOwnership, color: '#ffc658' },
];
const toggleSection = (section: string) => {
const newExpanded = new Set(expandedSections);
if (newExpanded.has(section)) {
newExpanded.delete(section);
} else {
newExpanded.add(section);
}
setExpandedSections(newExpanded);
};
const calculatePosition = () => {
const allocation = (portfolioAllocation / 100) * positionSize;
const shares = Math.floor(allocation / data.currentPrice);
return { allocation, shares };
};
const getRiskColor = (level: number) => {
if (level < 30) return 'text-green-600';
if (level < 70) return 'text-yellow-600';
return 'text-red-600';
};
const getTrendColor = (current: number, reference: number) => {
return current > reference ? 'text-green-600' : 'text-red-600';
};
return (
<div className="min-h-screen bg-gray-50 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">
{data.symbol} Analysis Dashboard
</h1>
<div className="flex items-center space-x-4">
<span className="text-2xl font-semibold text-green-600">
${data.currentPrice.toFixed(2)}
</span>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
data.finalDecision === 'BUY' ? 'bg-green-100 text-green-800' :
data.finalDecision === 'SELL' ? 'bg-red-100 text-red-800' :
'bg-yellow-100 text-yellow-800'
}`}>
{data.finalDecision}
</span>
</div>
</div>
{/* Core Financial Widgets */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
{/* Stock Price Chart */}
<div className="lg:col-span-2 bg-white rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold mb-4">Stock Price Chart</h3>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={priceData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="price" stroke="#2563eb" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
<div className="mt-4 grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-600">Daily High:</span>
<span className="ml-2 font-medium">${(data.currentPrice * 1.05).toFixed(2)}</span>
</div>
<div>
<span className="text-gray-600">Daily Low:</span>
<span className="ml-2 font-medium">${(data.currentPrice * 0.95).toFixed(2)}</span>
</div>
</div>
</div>
{/* Technical Indicators Dashboard */}
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold mb-4">Technical Indicators</h3>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-gray-600">RSI</span>
<span className={`font-medium ${data.rsi < 30 ? 'text-green-600' : data.rsi > 70 ? 'text-red-600' : 'text-yellow-600'}`}>
{data.rsi.toFixed(2)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600">MACD</span>
<span className={`font-medium ${data.macd > 0 ? 'text-green-600' : 'text-red-600'}`}>
{data.macd.toFixed(3)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600">50-day MA</span>
<span className={`font-medium ${getTrendColor(data.currentPrice, data.ma50)}`}>
${data.ma50.toFixed(2)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600">200-day MA</span>
<span className={`font-medium ${getTrendColor(data.currentPrice, data.ma200)}`}>
${data.ma200.toFixed(2)}
</span>
</div>
</div>
</div>
</div>
{/* Key Metrics Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-white rounded-lg shadow-md p-6">
<h4 className="text-sm font-medium text-gray-600 mb-2">Market Cap</h4>
<p className="text-2xl font-bold text-gray-900">{data.marketCap}</p>
</div>
<div className="bg-white rounded-lg shadow-md p-6">
<h4 className="text-sm font-medium text-gray-600 mb-2">P/S Ratio</h4>
<p className="text-2xl font-bold text-gray-900">{data.psRatio.toFixed(1)}x</p>
<p className="text-sm text-gray-500">vs 0.8x fair value</p>
</div>
<div className="bg-white rounded-lg shadow-md p-6">
<h4 className="text-sm font-medium text-gray-600 mb-2">Forward P/E</h4>
<p className="text-2xl font-bold text-gray-900">{data.forwardPE.toFixed(2)}</p>
</div>
<div className="bg-white rounded-lg shadow-md p-6">
<h4 className="text-sm font-medium text-gray-600 mb-2">Price Target</h4>
<p className="text-2xl font-bold text-gray-900">${data.targetPrice.toFixed(2)}</p>
</div>
</div>
{/* Analysis & Decision Widgets */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Bull vs Bear Debate Viewer */}
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold mb-4">Bull vs Bear Debate</h3>
<div className="flex space-x-1 mb-4">
<button
className={`px-4 py-2 rounded-lg text-sm font-medium ${
activeTab === 'bull' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'
}`}
onClick={() => setActiveTab('bull')}
>
Bull Case
</button>
<button
className={`px-4 py-2 rounded-lg text-sm font-medium ${
activeTab === 'bear' ? 'bg-red-100 text-red-800' : 'bg-gray-100 text-gray-600'
}`}
onClick={() => setActiveTab('bear')}
>
Bear Case
</button>
<button
className={`px-4 py-2 rounded-lg text-sm font-medium ${
activeTab === 'neutral' ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-600'
}`}
onClick={() => setActiveTab('neutral')}
>
Neutral
</button>
</div>
<div className="min-h-[200px]">
{activeTab === 'bull' && (
<ul className="space-y-2">
{data.bullArguments.map((arg, index) => (
<li key={index} className="flex items-start">
<span className="text-green-500 mr-2"></span>
<span className="text-sm">{arg}</span>
</li>
))}
</ul>
)}
{activeTab === 'bear' && (
<ul className="space-y-2">
{data.bearArguments.map((arg, index) => (
<li key={index} className="flex items-start">
<span className="text-red-500 mr-2"></span>
<span className="text-sm">{arg}</span>
</li>
))}
</ul>
)}
{activeTab === 'neutral' && (
<p className="text-sm text-gray-700">{data.neutralPerspective}</p>
)}
</div>
</div>
{/* Investment Plan Timeline */}
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold mb-4">Investment Plan Timeline</h3>
<div className="space-y-4">
<div className="flex items-center">
<div className="w-4 h-4 bg-blue-500 rounded-full mr-4"></div>
<div>
<p className="font-medium">Entry Point</p>
<p className="text-sm text-gray-600">Current: ${data.currentPrice.toFixed(2)}</p>
</div>
</div>
<div className="flex items-center">
<div className="w-4 h-4 bg-red-500 rounded-full mr-4"></div>
<div>
<p className="font-medium">Stop Loss</p>
<p className="text-sm text-gray-600">${data.stopLoss.toFixed(2)}</p>
</div>
</div>
<div className="flex items-center">
<div className="w-4 h-4 bg-green-500 rounded-full mr-4"></div>
<div>
<p className="font-medium">Profit Target 1</p>
<p className="text-sm text-gray-600">${data.profitTarget1.toFixed(2)}</p>
</div>
</div>
<div className="flex items-center">
<div className="w-4 h-4 bg-green-600 rounded-full mr-4"></div>
<div>
<p className="font-medium">Profit Target 2</p>
<p className="text-sm text-gray-600">${data.profitTarget2.toFixed(2)}</p>
</div>
</div>
</div>
</div>
</div>
{/* Interactive Decision Tools */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
{/* Risk Assessment Gauge */}
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold mb-4">Risk Assessment</h3>
<div className="flex items-center justify-center mb-4">
<div className="relative w-32 h-32">
<svg className="w-32 h-32 transform -rotate-90" viewBox="0 0 36 36">
<path
d="M18 2.0845
a 15.9155 15.9155 0 0 1 0 31.831
a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke="#e5e7eb"
strokeWidth="3"
/>
<path
d="M18 2.0845
a 15.9155 15.9155 0 0 1 0 31.831
a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke={data.riskLevel < 30 ? '#10b981' : data.riskLevel < 70 ? '#f59e0b' : '#ef4444'}
strokeWidth="3"
strokeDasharray={`${data.riskLevel}, 100`}
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className={`text-2xl font-bold ${getRiskColor(data.riskLevel)}`}>
{data.riskLevel}%
</span>
</div>
</div>
</div>
<p className="text-center text-sm text-gray-600">Overall Risk Level</p>
</div>
{/* Position Calculator */}
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold mb-4">Position Calculator</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Portfolio Size ($)
</label>
<input
type="number"
value={positionSize}
onChange={(e) => setPositionSize(Number(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Allocation (%)
</label>
<input
type="number"
value={portfolioAllocation}
onChange={(e) => setPortfolioAllocation(Number(e.target.value))}
min="1"
max="100"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="pt-2 border-t">
<p className="text-sm text-gray-600">Recommended Position:</p>
<p className="font-medium">${calculatePosition().allocation.toFixed(2)}</p>
<p className="text-sm text-gray-600">{calculatePosition().shares} shares</p>
</div>
</div>
</div>
{/* Sentiment Thermometer */}
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold mb-4">Sentiment Thermometer</h3>
<div className="flex items-center justify-center mb-4">
<div className="w-8 h-32 bg-gray-200 rounded-full relative">
<div
className={`absolute bottom-0 w-full rounded-full ${
data.sentimentScore > 70 ? 'bg-green-500' :
data.sentimentScore > 30 ? 'bg-yellow-500' : 'bg-red-500'
}`}
style={{ height: `${data.sentimentScore}%` }}
></div>
</div>
<div className="ml-4">
<p className="text-2xl font-bold">{data.sentimentScore}%</p>
<p className="text-sm text-gray-600">
{data.sentimentScore > 70 ? 'Bullish' :
data.sentimentScore > 30 ? 'Neutral' : 'Bearish'}
</p>
</div>
</div>
</div>
</div>
{/* Monitoring & Alerts */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
{/* News Feed Widget */}
<div className="lg:col-span-1">
<NewsFeedWidget symbol={data.symbol} maxItems={8} />
</div>
{/* Earnings Countdown Timer */}
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold mb-4">Earnings Countdown</h3>
<div className="text-center">
<p className="text-3xl font-bold text-blue-600 mb-2">{data.earningsDate}</p>
<p className="text-sm text-gray-600 mb-4">Next Earnings Call</p>
<div className="text-left">
<p className="font-medium mb-2">Key Focus Areas:</p>
<ul className="text-sm text-gray-600 space-y-1">
<li> Revenue growth trajectory</li>
<li> Margin expansion</li>
<li> Forward guidance</li>
<li> Market share gains</li>
</ul>
</div>
</div>
</div>
{/* Ownership Structure Pie Chart */}
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold mb-4">Ownership Structure</h3>
<ResponsiveContainer width="100%" height={200}>
<PieChart>
<Pie
data={ownershipData}
cx="50%"
cy="50%"
outerRadius={80}
dataKey="value"
label={({ name, value }) => `${name}: ${value.toFixed(2)}%`}
>
{ownershipData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
</div>
</div>
{/* Decision History Tracker */}
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
<h3 className="text-lg font-semibold mb-4">Decision History Tracker</h3>
<div className="relative">
<div className="absolute left-4 top-0 bottom-0 w-0.5 bg-gray-300"></div>
<div className="space-y-6">
{[
{ date: '2024-07-20', decision: 'HOLD', reasoning: 'Initial analysis pending earnings data', color: 'yellow' },
{ date: '2024-07-22', decision: 'HOLD', reasoning: 'Technical indicators mixed, awaiting clearer signals', color: 'yellow' },
{ date: '2024-07-25', decision: 'BUY', reasoning: 'Strong fundamentals confirmed, positive technical momentum', color: 'green' },
{ date: '2024-07-26', decision: 'BUY', reasoning: 'Final recommendation based on comprehensive analysis', color: 'green' }
].map((item, index) => (
<div key={index} className="relative flex items-start">
<div className={`absolute left-0 w-8 h-8 rounded-full border-4 border-white ${
item.color === 'green' ? 'bg-green-500' :
item.color === 'red' ? 'bg-red-500' : 'bg-yellow-500'
} shadow-md`}></div>
<div className="ml-12">
<div className="flex items-center space-x-2 mb-1">
<span className={`px-2 py-1 rounded text-xs font-medium ${
item.decision === 'BUY' ? 'bg-green-100 text-green-800' :
item.decision === 'SELL' ? 'bg-red-100 text-red-800' :
'bg-yellow-100 text-yellow-800'
}`}>
{item.decision}
</span>
<span className="text-sm text-gray-500">{item.date}</span>
</div>
<p className="text-sm text-gray-700">{item.reasoning}</p>
</div>
</div>
))}
</div>
</div>
</div>
{/* Text Information Widgets */}
<div className="space-y-6">
{/* Executive Summary Box */}
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold mb-4">Executive Summary</h3>
<div className="bg-blue-50 border-l-4 border-blue-400 p-4">
<p className="text-sm">
<strong>Final Recommendation: {data.finalDecision}</strong> - {data.decisionReasoning}
</p>
</div>
</div>
{/* Expandable Text Sections */}
{[
{ key: 'market-report', title: 'Market Report Summary', content: 'Detailed technical analysis with key indicators and price movement analysis...' },
{ key: 'sentiment-report', title: 'Sentiment Report Card', content: 'Complete company sentiment analysis including recent news and social media sentiment...' },
{ key: 'fundamentals', title: 'Fundamentals Report Panel', content: 'Company overview, financial metrics, analyst insights, and key takeaways...' },
{ key: 'macro-news', title: 'Macroeconomic News Brief', content: 'Trade policies, Federal Reserve developments, and market dynamics...' },
{ key: 'debate-transcript', title: 'Investment Debate Transcript', content: 'Complete bull and bear analyst arguments with neutral perspective...' },
{ key: 'risk-analysis', title: 'Risk Analysis Discussion', content: 'Full risky vs safe analyst debate with neutral commentary...' },
{ key: 'investment-plan', title: 'Final Investment Plan Document', content: 'Comprehensive investment strategy with position sizing and risk management...' },
].map((section) => (
<div key={section.key} className="bg-white rounded-lg shadow-md">
<button
className="w-full px-6 py-4 text-left flex justify-between items-center hover:bg-gray-50"
onClick={() => toggleSection(section.key)}
>
<h3 className="text-lg font-semibold">{section.title}</h3>
<span className="text-gray-400">
{expandedSections.has(section.key) ? '' : '+'}
</span>
</button>
{expandedSections.has(section.key) && (
<div className="px-6 pb-6">
<p className="text-gray-700">{section.content}</p>
</div>
)}
</div>
))}
</div>
{/* Raw Data Viewer */}
{rawData && (
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold mb-4">Detailed Analysis Data</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{Object.entries(rawData).map(([key, value]) => (
<div key={key} className="space-y-2">
<label className="block text-sm font-medium text-gray-700 capitalize">
{key.replace(/_/g, ' ')}
</label>
<textarea
value={typeof value === 'object' && value !== null ? JSON.stringify(value, null, 2) : String(value || '')}
readOnly
className="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-50 text-sm font-mono"
rows={typeof value === 'object' && value !== null ? Math.min(10, JSON.stringify(value, null, 2).split('\n').length) : 3}
/>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
};
export default AnalysisWidgets;

View File

@ -0,0 +1,110 @@
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { Play, Database, TrendingUp, Clock } from 'lucide-react';
import axios from 'axios';
const Dashboard: React.FC = () => {
const [companies, setCompanies] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchCompanies();
}, []);
const fetchCompanies = async () => {
try {
const response = await axios.get('/results/companies');
setCompanies(response.data.companies);
} catch (error) {
console.error('Error fetching companies:', error);
} finally {
setLoading(false);
}
};
return (
<div className="px-4 py-6">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Trading Analysis Dashboard</h1>
<p className="mt-2 text-gray-600">
Execute trading analysis and view historical results for stock predictions
</p>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<Link
to="/run-analysis"
className="bg-primary-600 hover:bg-primary-700 text-white p-6 rounded-lg shadow-md transition-colors"
>
<div className="flex items-center">
<Play className="h-8 w-8 mr-4" />
<div>
<h3 className="text-xl font-semibold">Run New Analysis</h3>
<p className="text-primary-100">Execute trading pipeline for any stock symbol</p>
</div>
</div>
</Link>
<Link
to="/results"
className="bg-success-600 hover:bg-success-700 text-white p-6 rounded-lg shadow-md transition-colors"
>
<div className="flex items-center">
<Database className="h-8 w-8 mr-4" />
<div>
<h3 className="text-xl font-semibold">View Results</h3>
<p className="text-success-100">Browse historical analysis results</p>
</div>
</div>
</Link>
</div>
{/* Recent Results */}
<div className="bg-white rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-medium text-gray-900">Recent Analysis Results</h2>
</div>
<div className="p-6">
{loading ? (
<div className="text-center py-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mx-auto"></div>
<p className="mt-2 text-gray-500">Loading results...</p>
</div>
) : companies.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{companies.slice(0, 6).map((company) => (
<div key={company.symbol} className="border rounded-lg p-4 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between mb-2">
<h3 className="font-semibold text-gray-900">{company.symbol}</h3>
<TrendingUp className="h-4 w-4 text-success-500" />
</div>
<div className="text-sm text-gray-600">
<div className="flex items-center mb-1">
<Clock className="h-3 w-3 mr-1" />
Latest: {company.latest_analysis}
</div>
<div>Total analyses: {company.total_analyses}</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<Database className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">No analysis results found</p>
<Link
to="/run-analysis"
className="mt-2 inline-flex items-center text-primary-600 hover:text-primary-500"
>
Run your first analysis
</Link>
</div>
)}
</div>
</div>
</div>
);
};
export default Dashboard;

View File

@ -0,0 +1,306 @@
import React, { useEffect, useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import { ArrowLeft, Calendar, FileText, Download, BarChart3 } from 'lucide-react';
import axios from 'axios';
import { Line } from 'react-chartjs-2';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
const ResultDetail: React.FC = () => {
const { symbol, date } = useParams<{ symbol: string; date: string }>();
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (symbol && date) {
fetchResultDetail(symbol, date);
}
}, [symbol, date]);
const fetchResultDetail = async (sym: string, dt: string) => {
try {
const response = await axios.get(`/results/${sym}/${dt}`);
setData(response.data);
} catch (error: any) {
setError(error.response?.data?.detail || 'Failed to load result details');
} finally {
setLoading(false);
}
};
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const renderDataVisualization = (analysisData: any) => {
// Try to extract meaningful data for visualization
if (!analysisData || typeof analysisData !== 'object') {
return null;
}
// Look for common data patterns in trading analysis
const keys = Object.keys(analysisData);
const timeSeriesKeys = keys.filter(key =>
key.toLowerCase().includes('price') ||
key.toLowerCase().includes('volume') ||
key.toLowerCase().includes('time') ||
key.toLowerCase().includes('date')
);
if (timeSeriesKeys.length > 0) {
// Create a simple visualization if we have time series data
const labels = Array.from({ length: 10 }, (_, i) => `Day ${i + 1}`);
const chartData = {
labels,
datasets: [
{
label: 'Analysis Trend',
data: Array.from({ length: 10 }, () => Math.random() * 100),
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
},
],
};
const options = {
responsive: true,
plugins: {
legend: {
position: 'top' as const,
},
title: {
display: true,
text: `${symbol} Analysis Visualization`,
},
},
scales: {
y: {
beginAtZero: true,
},
},
};
return (
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
<BarChart3 className="h-5 w-5 mr-2" />
Data Visualization
</h3>
<div className="h-64">
<Line data={chartData} options={options} />
</div>
</div>
);
}
return null;
};
const renderDataSection = (title: string, content: any, maxHeight = '400px') => {
return (
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">{title}</h3>
<div
className="bg-gray-50 rounded-lg p-4 overflow-auto font-mono text-sm"
style={{ maxHeight }}
>
<pre className="whitespace-pre-wrap">
{typeof content === 'string' ? content : JSON.stringify(content, null, 2)}
</pre>
</div>
</div>
);
};
if (loading) {
return (
<div className="px-4 py-6">
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto"></div>
<p className="mt-4 text-gray-500">Loading analysis details...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="px-4 py-6">
<div className="text-center py-12">
<FileText className="h-16 w-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">Error Loading Result</h3>
<p className="text-red-600 mb-4">{error}</p>
<Link
to="/results"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Results
</Link>
</div>
</div>
);
}
return (
<div className="px-4 py-6">
{/* Header */}
<div className="mb-8">
<Link
to="/results"
className="inline-flex items-center text-primary-600 hover:text-primary-500 mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Results
</Link>
<h1 className="text-3xl font-bold text-gray-900">
{symbol} Analysis - {date}
</h1>
<p className="mt-2 text-gray-600">
Detailed view of trading analysis results
</p>
</div>
{/* Metadata */}
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h2 className="text-lg font-medium text-gray-900 mb-4">Analysis Metadata</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<div className="flex items-center mb-2">
<Calendar className="h-4 w-4 text-gray-400 mr-2" />
<span className="font-medium">Analysis Date</span>
</div>
<p className="text-gray-900">{date}</p>
</div>
<div>
<div className="flex items-center mb-2">
<FileText className="h-4 w-4 text-gray-400 mr-2" />
<span className="font-medium">File Size</span>
</div>
<p className="text-gray-900">{formatFileSize(data?.metadata?.file_size || 0)}</p>
</div>
<div>
<div className="flex items-center mb-2">
<Download className="h-4 w-4 text-gray-400 mr-2" />
<span className="font-medium">Last Modified</span>
</div>
<p className="text-gray-900">
{data?.metadata?.modified_at ? formatDate(data.metadata.modified_at) : 'Unknown'}
</p>
</div>
</div>
</div>
{/* Data Visualization */}
{data?.data && renderDataVisualization(data.data)}
{/* Analysis Data */}
{data?.data && (
<>
{/* Summary Section */}
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Analysis Summary</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<span className="font-medium text-gray-700">Symbol:</span>
<p className="text-gray-900">{symbol}</p>
</div>
<div>
<span className="font-medium text-gray-700">Data Keys:</span>
<p className="text-gray-900">
{Object.keys(data.data).length} main sections
</p>
</div>
<div className="md:col-span-2">
<span className="font-medium text-gray-700">Available Sections:</span>
<div className="mt-2 flex flex-wrap gap-2">
{Object.keys(data.data).map((key) => (
<span
key={key}
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 text-primary-800"
>
{key}
</span>
))}
</div>
</div>
</div>
</div>
{/* Detailed Data Sections */}
{Object.entries(data.data).map(([key, value]) => (
<div key={key}>
{renderDataSection(`${key} Data`, value)}
</div>
))}
</>
)}
{/* Raw Data */}
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900">Raw Data</h3>
<button
onClick={() => {
const dataStr = JSON.stringify(data, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${symbol}_${date}_analysis.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}}
className="inline-flex items-center px-3 py-1 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
<Download className="h-3 w-3 mr-1" />
Download JSON
</button>
</div>
<div className="bg-gray-50 rounded-lg p-4 overflow-auto font-mono text-sm max-h-96">
<pre className="whitespace-pre-wrap">
{JSON.stringify(data, null, 2)}
</pre>
</div>
</div>
</div>
);
};
export default ResultDetail;

View File

@ -0,0 +1,152 @@
import React, { useState } from 'react';
import { Play, Settings, AlertCircle, CheckCircle } from 'lucide-react';
import axios from 'axios';
import toast from 'react-hot-toast';
const RunAnalysis: React.FC = () => {
const [formData, setFormData] = useState({
symbol: '',
date: new Date().toISOString().split('T')[0],
});
const [isRunning, setIsRunning] = useState(false);
const [jobId, setJobId] = useState<string | null>(null);
const [jobStatus, setJobStatus] = useState<any>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.symbol.trim()) {
toast.error('Please enter a stock symbol');
return;
}
setIsRunning(true);
try {
const response = await axios.post('/analysis/start', {
symbol: formData.symbol.toUpperCase(),
date: formData.date,
});
setJobId(response.data.job_id);
toast.success('Analysis started successfully!');
pollJobStatus(response.data.job_id);
} catch (error: any) {
toast.error(error.response?.data?.detail || 'Failed to start analysis');
setIsRunning(false);
}
};
const pollJobStatus = async (id: string) => {
const interval = setInterval(async () => {
try {
const response = await axios.get(`/analysis/status/${id}`);
setJobStatus(response.data);
if (response.data.status === 'completed') {
clearInterval(interval);
setIsRunning(false);
toast.success('Analysis completed successfully!');
} else if (response.data.status === 'failed') {
clearInterval(interval);
setIsRunning(false);
toast.error(`Analysis failed: ${response.data.error}`);
}
} catch (error) {
clearInterval(interval);
setIsRunning(false);
toast.error('Error checking job status');
}
}, 2000);
};
return (
<div className="px-4 py-6">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Run Trading Analysis</h1>
<p className="mt-2 text-gray-600">
Execute the TradingAgents pipeline to generate predictions for any stock symbol
</p>
</div>
<div className="max-w-2xl mx-auto">
<div className="bg-white rounded-lg shadow-md p-6">
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="symbol" className="block text-sm font-medium text-gray-700 mb-2">
Stock Symbol
</label>
<input
type="text"
id="symbol"
value={formData.symbol}
onChange={(e) => setFormData({ ...formData, symbol: e.target.value.toUpperCase() })}
placeholder="e.g., NVDA, AAPL, TSLA"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500"
disabled={isRunning}
/>
</div>
<div>
<label htmlFor="date" className="block text-sm font-medium text-gray-700 mb-2">
Analysis Date
</label>
<input
type="date"
id="date"
value={formData.date}
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500"
disabled={isRunning}
/>
</div>
<button
type="submit"
disabled={isRunning}
className="w-full flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isRunning ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Running Analysis...
</>
) : (
<>
<Play className="h-4 w-4 mr-2" />
Start Analysis
</>
)}
</button>
</form>
{/* Job Status */}
{jobStatus && (
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
<div className="flex items-center mb-2">
{jobStatus.status === 'completed' && <CheckCircle className="h-5 w-5 text-success-500 mr-2" />}
{jobStatus.status === 'failed' && <AlertCircle className="h-5 w-5 text-danger-500 mr-2" />}
{jobStatus.status === 'running' && (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary-600 mr-2"></div>
)}
<span className="font-medium capitalize">{jobStatus.status}</span>
</div>
{jobStatus.progress && (
<p className="text-sm text-gray-600 mb-2">{jobStatus.progress}</p>
)}
{jobStatus.result && (
<div className="mt-4 p-3 bg-white rounded border">
<h4 className="font-medium mb-2">Analysis Result:</h4>
<pre className="text-xs bg-gray-100 p-2 rounded overflow-x-auto">
{JSON.stringify(jobStatus.result, null, 2)}
</pre>
</div>
)}
</div>
)}
</div>
</div>
</div>
);
};
export default RunAnalysis;

View File

@ -0,0 +1,231 @@
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { Calendar, FileText, TrendingUp, Clock, Eye } from 'lucide-react';
import axios from 'axios';
interface Company {
symbol: string;
latest_analysis: string;
total_analyses: number;
}
interface Result {
date: string;
filename: string;
file_size: number;
modified_at: string;
preview?: {
keys: string[];
size: number;
};
error?: string;
}
const ViewResults: React.FC = () => {
const [companies, setCompanies] = useState<Company[]>([]);
const [selectedCompany, setSelectedCompany] = useState<string>('');
const [results, setResults] = useState<Result[]>([]);
const [loading, setLoading] = useState(true);
const [loadingResults, setLoadingResults] = useState(false);
useEffect(() => {
fetchCompanies();
}, []);
useEffect(() => {
if (selectedCompany) {
fetchCompanyResults(selectedCompany);
}
}, [selectedCompany]);
const fetchCompanies = async () => {
try {
const response = await axios.get('/results/companies');
setCompanies(response.data.companies);
if (response.data.companies.length > 0) {
setSelectedCompany(response.data.companies[0].symbol);
}
} catch (error) {
console.error('Error fetching companies:', error);
} finally {
setLoading(false);
}
};
const fetchCompanyResults = async (symbol: string) => {
setLoadingResults(true);
try {
const response = await axios.get(`/results/${symbol}`);
setResults(response.data.results);
} catch (error) {
console.error('Error fetching results:', error);
setResults([]);
} finally {
setLoadingResults(false);
}
};
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
if (loading) {
return (
<div className="px-4 py-6">
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto"></div>
<p className="mt-4 text-gray-500">Loading analysis results...</p>
</div>
</div>
);
}
return (
<div className="px-4 py-6">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Analysis Results</h1>
<p className="mt-2 text-gray-600">
Browse and view historical trading analysis results
</p>
</div>
{companies.length === 0 ? (
<div className="text-center py-12">
<FileText className="h-16 w-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No Results Found</h3>
<p className="text-gray-500 mb-4">No analysis results are available yet.</p>
<Link
to="/run-analysis"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700"
>
Run Your First Analysis
</Link>
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Company Selector */}
<div className="lg:col-span-1">
<div className="bg-white rounded-lg shadow p-4">
<h3 className="text-lg font-medium text-gray-900 mb-4">Companies</h3>
<div className="space-y-2">
{companies.map((company) => (
<button
key={company.symbol}
onClick={() => setSelectedCompany(company.symbol)}
className={`w-full text-left p-3 rounded-lg border transition-colors ${
selectedCompany === company.symbol
? 'bg-primary-50 border-primary-200 text-primary-900'
: 'bg-gray-50 border-gray-200 hover:bg-gray-100'
}`}
>
<div className="flex items-center justify-between">
<span className="font-medium">{company.symbol}</span>
<TrendingUp className="h-4 w-4" />
</div>
<div className="text-sm text-gray-500 mt-1">
{company.total_analyses} analyses
</div>
</button>
))}
</div>
</div>
</div>
{/* Results List */}
<div className="lg:col-span-3">
<div className="bg-white rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">
Results for {selectedCompany}
</h3>
</div>
<div className="p-6">
{loadingResults ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mx-auto"></div>
<p className="mt-2 text-gray-500">Loading results...</p>
</div>
) : results.length === 0 ? (
<div className="text-center py-8">
<FileText className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">No results found for {selectedCompany}</p>
</div>
) : (
<div className="space-y-4">
{results.map((result) => (
<div
key={result.date}
className="border rounded-lg p-4 hover:shadow-md transition-shadow"
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center">
<Calendar className="h-4 w-4 text-gray-400 mr-2" />
<span className="font-medium text-gray-900">
Analysis: {result.date}
</span>
</div>
<Link
to={`/results/${selectedCompany}/${result.date}`}
className="inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-primary-700 bg-primary-100 hover:bg-primary-200"
>
<Eye className="h-3 w-3 mr-1" />
View Details
</Link>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm text-gray-600">
<div>
<span className="font-medium">File Size:</span>
<div>{formatFileSize(result.file_size)}</div>
</div>
<div>
<span className="font-medium">Modified:</span>
<div>{formatDate(result.modified_at)}</div>
</div>
{result.preview && (
<>
<div>
<span className="font-medium">Data Keys:</span>
<div>{result.preview.keys.length} keys</div>
</div>
<div>
<span className="font-medium">Data Size:</span>
<div>{result.preview.size} chars</div>
</div>
</>
)}
{result.error && (
<div className="col-span-2 md:col-span-4">
<span className="font-medium text-red-600">Error:</span>
<div className="text-red-600">{result.error}</div>
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default ViewResults;

View File

@ -0,0 +1,268 @@
import { TransformedAnalysisData } from '../components/TransformedDataAdapter.tsx';
export interface TransformedDataFile {
filename: string;
company: string;
date: string;
displayName: string;
}
class TransformedDataService {
private baseUrl = '/transformed_data';
private cachedFiles: TransformedDataFile[] | null = null;
private cachedData: Map<string, TransformedAnalysisData> = new Map();
/**
* Get list of available transformed data files
*/
async getAvailableFiles(): Promise<TransformedDataFile[]> {
if (this.cachedFiles) {
return this.cachedFiles;
}
try {
// In a real implementation, you might have an API endpoint that lists files
// For now, we'll try to load a manifest file or use a predefined list
const response = await fetch(`${this.baseUrl}/manifest.json`);
if (response.ok) {
const manifest = await response.json();
this.cachedFiles = manifest.files || [];
} else {
// Fallback: try to load some common files
this.cachedFiles = await this.discoverFiles();
}
} catch (error) {
console.warn('Could not load transformed data manifest, using fallback discovery:', error);
this.cachedFiles = await this.discoverFiles();
}
return this.cachedFiles;
}
/**
* Discover available files by trying common patterns
*/
private async discoverFiles(): Promise<TransformedDataFile[]> {
const companies = ['AVAH', 'PLTR', 'RDDT'];
const dates = [
'2025-07-26', '2025-08-05', '2025-08-06', '2025-08-07'
];
const files: TransformedDataFile[] = [];
for (const company of companies) {
for (const date of dates) {
const filename = `${company}_full_states_log_${date}_transformed.json`;
try {
const response = await fetch(`${this.baseUrl}/${filename}`, { method: 'HEAD' });
if (response.ok) {
files.push({
filename,
company,
date,
displayName: `${company} - ${date}`
});
}
} catch (error) {
// File doesn't exist, skip
}
}
}
return files;
}
/**
* Load a specific transformed data file
*/
async loadTransformedData(filename: string): Promise<TransformedAnalysisData> {
// Check cache first
if (this.cachedData.has(filename)) {
return this.cachedData.get(filename)!;
}
try {
const response = await fetch(`${this.baseUrl}/${filename}`);
if (!response.ok) {
throw new Error(`Failed to load ${filename}: ${response.status} ${response.statusText}`);
}
const data: TransformedAnalysisData = await response.json();
// Validate the data structure
this.validateTransformedData(data);
// Cache the data
this.cachedData.set(filename, data);
return data;
} catch (error) {
console.error(`Error loading transformed data file ${filename}:`, error);
throw error;
}
}
/**
* Load transformed data by company and date
*/
async loadByCompanyAndDate(company: string, date: string): Promise<TransformedAnalysisData> {
const filename = `${company}_full_states_log_${date}_transformed.json`;
return this.loadTransformedData(filename);
}
/**
* Get the most recent analysis for a company
*/
async getLatestForCompany(company: string): Promise<TransformedAnalysisData | null> {
const files = await this.getAvailableFiles();
const companyFiles = files
.filter(f => f.company === company)
.sort((a, b) => b.date.localeCompare(a.date)); // Sort by date descending
if (companyFiles.length === 0) {
return null;
}
return this.loadTransformedData(companyFiles[0].filename);
}
/**
* Get all available companies
*/
async getAvailableCompanies(): Promise<string[]> {
const files = await this.getAvailableFiles();
const companies = [...new Set(files.map(f => f.company))];
return companies.sort();
}
/**
* Get available dates for a specific company
*/
async getAvailableDatesForCompany(company: string): Promise<string[]> {
const files = await this.getAvailableFiles();
const dates = files
.filter(f => f.company === company)
.map(f => f.date)
.sort((a, b) => b.localeCompare(a)); // Sort by date descending
return dates;
}
/**
* Validate that the loaded data conforms to the expected structure
*/
private validateTransformedData(data: any): void {
const requiredSections = [
'metadata',
'financial_data',
'technical_indicators',
'investment_strategy',
'debate_summary',
'text_content',
'widgets_config'
];
for (const section of requiredSections) {
if (!data[section]) {
throw new Error(`Missing required section: ${section}`);
}
}
// Validate metadata
const metadata = data.metadata;
if (!metadata.company_ticker || !metadata.analysis_date) {
throw new Error('Invalid metadata: missing company_ticker or analysis_date');
}
// Validate that dates are in correct format
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(metadata.analysis_date)) {
throw new Error('Invalid date format in metadata.analysis_date');
}
}
/**
* Clear all cached data
*/
clearCache(): void {
this.cachedFiles = null;
this.cachedData.clear();
}
/**
* Create a manifest file content for available transformed data
* This can be used to generate a manifest.json file
*/
async generateManifest(): Promise<{ files: TransformedDataFile[] }> {
const files = await this.discoverFiles();
return { files };
}
/**
* Search for analyses by various criteria
*/
async searchAnalyses(criteria: {
company?: string;
dateFrom?: string;
dateTo?: string;
recommendation?: 'BUY' | 'SELL' | 'HOLD';
}): Promise<TransformedDataFile[]> {
const files = await this.getAvailableFiles();
return files.filter(file => {
if (criteria.company && file.company !== criteria.company) {
return false;
}
if (criteria.dateFrom && file.date < criteria.dateFrom) {
return false;
}
if (criteria.dateTo && file.date > criteria.dateTo) {
return false;
}
// For recommendation filtering, we'd need to load the actual data
// This is left as a future enhancement
return true;
});
}
/**
* Get summary statistics about available data
*/
async getDataSummary(): Promise<{
totalFiles: number;
companies: string[];
dateRange: { earliest: string; latest: string };
companyCounts: Record<string, number>;
}> {
const files = await this.getAvailableFiles();
const companies = [...new Set(files.map(f => f.company))].sort();
const dates = files.map(f => f.date).sort();
const companyCounts: Record<string, number> = {};
files.forEach(file => {
companyCounts[file.company] = (companyCounts[file.company] || 0) + 1;
});
return {
totalFiles: files.length,
companies,
dateRange: {
earliest: dates[0] || '',
latest: dates[dates.length - 1] || ''
},
companyCounts
};
}
}
// Export a singleton instance
export const transformedDataService = new TransformedDataService();
export default transformedDataService;

View File

@ -0,0 +1,37 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
},
success: {
50: '#f0fdf4',
500: '#22c55e',
600: '#16a34a',
},
danger: {
50: '#fef2f2',
500: '#ef4444',
600: '#dc2626',
},
warning: {
50: '#fffbeb',
500: '#f59e0b',
600: '#d97706',
}
},
animation: {
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
}
},
},
plugins: [],
}

54
web_app/start_backend.py Normal file
View File

@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""
Startup script for TradingAgents Web Application Backend
"""
import os
import sys
import subprocess
def main():
# Change to backend directory
backend_dir = os.path.dirname(os.path.abspath(__file__))
backend_dir = os.path.join(backend_dir, 'backend')
print("🚀 Starting TradingAgents Web Application Backend")
print(f"📁 Backend directory: {backend_dir}")
# Check if main.py exists
main_py = os.path.join(backend_dir, 'main.py')
if not os.path.exists(main_py):
print(f"❌ main.py not found at {main_py}")
return False
# Change to backend directory and run
os.chdir(backend_dir)
print("🔧 Installing dependencies...")
try:
subprocess.run([sys.executable, '-m', 'pip', 'install', 'fastapi', 'uvicorn', 'pydantic', 'python-multipart'],
check=True, capture_output=True)
print("✅ Dependencies installed")
except subprocess.CalledProcessError as e:
print(f"⚠️ Warning: Could not install dependencies: {e}")
print(" Please install manually: pip install fastapi uvicorn pydantic python-multipart")
print("🌐 Starting FastAPI server...")
print(" Server will be available at: http://localhost:8000")
print(" API documentation at: http://localhost:8000/docs")
print(" Press Ctrl+C to stop the server")
print("-" * 50)
try:
# Run the server
subprocess.run([sys.executable, 'main.py'], check=True)
except KeyboardInterrupt:
print("\n🛑 Server stopped by user")
except subprocess.CalledProcessError as e:
print(f"❌ Server failed to start: {e}")
return False
return True
if __name__ == "__main__":
main()