Add web-app
This commit is contained in:
parent
f5f36ab5c7
commit
a706e90d2d
|
|
@ -7,5 +7,5 @@ eval_results/
|
|||
eval_data/
|
||||
*.egg-info/
|
||||
.env
|
||||
trading_agents/*
|
||||
node_modules/*
|
||||
trading_agents/
|
||||
web_app/frontend/node_modules/*
|
||||
|
|
@ -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()
|
||||
|
|
@ -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]
|
||||
|
|
@ -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)
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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: [],
|
||||
}
|
||||
|
|
@ -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()
|
||||
Loading…
Reference in New Issue