Add web-app
This commit is contained in:
parent
f5f36ab5c7
commit
a706e90d2d
|
|
@ -7,5 +7,5 @@ eval_results/
|
||||||
eval_data/
|
eval_data/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.env
|
.env
|
||||||
trading_agents/*
|
trading_agents/
|
||||||
node_modules/*
|
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
|
tqdm
|
||||||
pytz
|
pytz
|
||||||
redis
|
redis
|
||||||
chainlit
|
|
||||||
rich
|
rich
|
||||||
questionary
|
questionary
|
||||||
langchain_anthropic
|
langchain_anthropic
|
||||||
langchain-google-genai
|
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