Add reflection for each analysis
This commit is contained in:
parent
ce86231796
commit
4c7f636097
|
|
@ -50,6 +50,11 @@ class FinancialSituationMemory:
|
||||||
ids=ids,
|
ids=ids,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_latest_situation(self):
|
||||||
|
"""Retrieve the latest entry from a situation_collection."""
|
||||||
|
latest_entry = self.situation_collection.query(sort_by="id:descending", limit=1)
|
||||||
|
return latest_entry
|
||||||
|
|
||||||
def get_memories(self, current_situation, n_matches=1):
|
def get_memories(self, current_situation, n_matches=1):
|
||||||
"""Find matching recommendations using OpenAI embeddings"""
|
"""Find matching recommendations using OpenAI embeddings"""
|
||||||
query_embedding = self.get_embedding(current_situation)
|
query_embedding = self.get_embedding(current_situation)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from pydantic import BaseModel
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import glob
|
import glob
|
||||||
import uuid
|
import uuid
|
||||||
|
|
@ -59,10 +60,12 @@ class AnalysisResponse(BaseModel):
|
||||||
|
|
||||||
class JobStatus(BaseModel):
|
class JobStatus(BaseModel):
|
||||||
job_id: str
|
job_id: str
|
||||||
|
start_time: Optional[float] = None
|
||||||
status: str
|
status: str
|
||||||
progress: Optional[str] = None
|
progress: Optional[str] = None
|
||||||
result: Optional[Dict[str, Any]] = None
|
result: Optional[Dict[str, Any]] = None
|
||||||
error: Optional[str] = None
|
error: Optional[str] = None
|
||||||
|
trading_agent: Any = None # Store the trading agent instance here
|
||||||
|
|
||||||
# In-memory job storage (in production, use Redis or database)
|
# In-memory job storage (in production, use Redis or database)
|
||||||
jobs: Dict[str, JobStatus] = {}
|
jobs: Dict[str, JobStatus] = {}
|
||||||
|
|
@ -77,6 +80,15 @@ async def health_check():
|
||||||
|
|
||||||
async def run_analysis_task(job_id: str, symbol: str, analysis_date: str, config_overrides: Dict[str, Any] = None):
|
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 without blocking the event loop"""
|
"""Background task to run the trading analysis without blocking the event loop"""
|
||||||
|
|
||||||
|
def execution_time() -> Optional[str]:
|
||||||
|
if jobs[job_id].status == "completed" or jobs[job_id].status == "failed":
|
||||||
|
return f"{time.time() - jobs[job_id].start_time:.2f} seconds"
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
jobs[job_id].start_time = time.time() # Save the start time in the job
|
||||||
|
|
||||||
try:
|
try:
|
||||||
jobs[job_id].status = "running"
|
jobs[job_id].status = "running"
|
||||||
jobs[job_id].progress = "Initializing TradingAgents..."
|
jobs[job_id].progress = "Initializing TradingAgents..."
|
||||||
|
|
@ -87,16 +99,13 @@ async def run_analysis_task(job_id: str, symbol: str, analysis_date: str, config
|
||||||
config.update(config_overrides)
|
config.update(config_overrides)
|
||||||
|
|
||||||
jobs[job_id].progress = "Setting up trading graph..."
|
jobs[job_id].progress = "Setting up trading graph..."
|
||||||
|
# Create and store the trading agent instance
|
||||||
|
ta = TradingAgentsGraph(debug=True, config=config)
|
||||||
|
jobs[job_id].trading_agent = ta # Store the instance
|
||||||
|
jobs[job_id].progress = f"Analyzing {symbol} for {analysis_date}..."
|
||||||
|
|
||||||
# Define blocking work as sync function
|
# Run the propagate method in a threadpool
|
||||||
def _do_work():
|
_, decision = await run_in_threadpool(ta.propagate, symbol, analysis_date)
|
||||||
ta = TradingAgentsGraph(debug=True, config=config)
|
|
||||||
jobs[job_id].progress = f"Analyzing {symbol} for {analysis_date}..."
|
|
||||||
_, decision = ta.propagate(symbol, analysis_date)
|
|
||||||
return decision
|
|
||||||
|
|
||||||
# Run blocking work in threadpool so the event loop stays responsive
|
|
||||||
decision = await run_in_threadpool(_do_work)
|
|
||||||
|
|
||||||
jobs[job_id].status = "completed"
|
jobs[job_id].status = "completed"
|
||||||
jobs[job_id].result = {
|
jobs[job_id].result = {
|
||||||
|
|
@ -104,6 +113,20 @@ async def run_analysis_task(job_id: str, symbol: str, analysis_date: str, config
|
||||||
"date": analysis_date,
|
"date": analysis_date,
|
||||||
"decision": decision,
|
"decision": decision,
|
||||||
"completed_at": datetime.now().isoformat(),
|
"completed_at": datetime.now().isoformat(),
|
||||||
|
"execution_time": execution_time()
|
||||||
|
}
|
||||||
|
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)}"
|
||||||
|
jobs[job_id].result = {
|
||||||
|
"symbol": symbol,
|
||||||
|
"date": analysis_date,
|
||||||
|
"decision": decision,
|
||||||
|
"completed_at": datetime.now().isoformat(),
|
||||||
|
"execution_time": execution_time()
|
||||||
}
|
}
|
||||||
jobs[job_id].progress = "Analysis completed successfully"
|
jobs[job_id].progress = "Analysis completed successfully"
|
||||||
|
|
||||||
|
|
@ -342,15 +365,81 @@ async def get_jobs():
|
||||||
"""Get all jobs"""
|
"""Get all jobs"""
|
||||||
job_lst = []
|
job_lst = []
|
||||||
for job_id, job in jobs.items():
|
for job_id, job in jobs.items():
|
||||||
job_lst.append({
|
job_dict = {
|
||||||
"job_id": job_id,
|
"job_id": job_id,
|
||||||
"status": job.status,
|
"status": job.status,
|
||||||
"progress": job.progress,
|
"progress": job.progress,
|
||||||
"result": job.result,
|
"result": job.result,
|
||||||
"error": job.error
|
"error": job.error,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
# Add execution_time if available
|
||||||
|
if hasattr(job, 'start_time') and job.start_time is not None:
|
||||||
|
if job.status in ["completed", "failed"]:
|
||||||
|
job_dict["execution_time"] = f"{time.time() - job.start_time:.2f} seconds"
|
||||||
|
|
||||||
|
job_lst.append(job_dict)
|
||||||
|
|
||||||
return {"jobs": job_lst}
|
return {"jobs": job_lst}
|
||||||
|
|
||||||
|
@app.post("/reflect-on-analysis/{symbol}/{date}")
|
||||||
|
async def reflect_on_analysis(symbol: str, date: str, request: dict):
|
||||||
|
"""Get latest financial situation memory for a specific analysis"""
|
||||||
|
returns_losses = request.get("returns_losses")
|
||||||
|
if returns_losses is None:
|
||||||
|
raise HTTPException(status_code=400, detail="returns_losses is required in request body")
|
||||||
|
|
||||||
|
# Find the job that matches the symbol and date
|
||||||
|
matching_job = None
|
||||||
|
for job_id, job in jobs.items():
|
||||||
|
if (job.result.get("symbol") == symbol.upper() and
|
||||||
|
job.result.get("date") == date and
|
||||||
|
hasattr(job, 'trading_agent') and
|
||||||
|
job.trading_agent):
|
||||||
|
matching_job = job
|
||||||
|
break
|
||||||
|
|
||||||
|
if not matching_job:
|
||||||
|
raise HTTPException(status_code=404, detail=f"No active job found for {symbol} on {date}")
|
||||||
|
|
||||||
|
if not hasattr(matching_job.trading_agent, 'memory') or not matching_job.trading_agent.memory:
|
||||||
|
raise HTTPException(status_code=404, detail="No memory found for this analysis")
|
||||||
|
|
||||||
|
matching_job.trading_agent.reflect_and_remember(returns_losses)
|
||||||
|
|
||||||
|
try:
|
||||||
|
bull_memory = matching_job.trading_agent.bull_memory
|
||||||
|
bear_memory = matching_job.trading_agent.bear_memory
|
||||||
|
trader_memory = matching_job.trading_agent.trader_memory
|
||||||
|
invest_judge_memory = matching_job.trading_agent.invest_judge_memory
|
||||||
|
risk_manager_memory = matching_job.trading_agent.risk_manager_memory
|
||||||
|
|
||||||
|
reflections = {}
|
||||||
|
|
||||||
|
latest_entry = bull_memory.get_latest_situation()
|
||||||
|
reflections["bull_memory"] = latest_entry
|
||||||
|
|
||||||
|
latest_entry = bear_memory.get_latest_situation()
|
||||||
|
reflections["bear_memory"] = latest_entry
|
||||||
|
|
||||||
|
latest_entry = trader_memory.get_latest_situation()
|
||||||
|
reflections["trader_memory"] = latest_entry
|
||||||
|
|
||||||
|
latest_entry = invest_judge_memory.get_latest_situation()
|
||||||
|
reflections["invest_judge_memory"] = latest_entry
|
||||||
|
|
||||||
|
latest_entry = risk_manager_memory.get_latest_situation()
|
||||||
|
reflections["risk_manager_memory"] = latest_entry
|
||||||
|
|
||||||
|
return {
|
||||||
|
"symbol": symbol.upper(),
|
||||||
|
"date": date,
|
||||||
|
"job_id": matching_job.job_id,
|
||||||
|
"reflections": reflections
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Error retrieving latest situation: {str(e)}")
|
||||||
|
|
||||||
@app.get("/config")
|
@app.get("/config")
|
||||||
async def get_default_config():
|
async def get_default_config():
|
||||||
"""Get the default configuration"""
|
"""Get the default configuration"""
|
||||||
|
|
@ -358,4 +447,4 @@ async def get_default_config():
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
||||||
|
|
|
||||||
|
|
@ -687,7 +687,13 @@ function App() {
|
||||||
{selectedTransformedData ? (
|
{selectedTransformedData ? (
|
||||||
<TransformedDataAdapter analysisData={selectedTransformedData} />
|
<TransformedDataAdapter analysisData={selectedTransformedData} />
|
||||||
) : resultDetail ? (
|
) : resultDetail ? (
|
||||||
<AnalysisDataAdapter tradingResult={resultDetail} />
|
<AnalysisDataAdapter
|
||||||
|
tradingResult={{
|
||||||
|
...resultDetail,
|
||||||
|
symbol: selectedResult?.company || resultDetail?.symbol,
|
||||||
|
date: selectedResult?.date || resultDetail?.date
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="p-8 text-center">
|
<div className="p-8 text-center">
|
||||||
<div className="text-gray-500 mb-2">Loading analysis data...</div>
|
<div className="text-gray-500 mb-2">Loading analysis data...</div>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import AnalysisWidgets from '../pages/AnalysisWidgets.tsx';
|
||||||
|
|
||||||
interface TradingAgentsResult {
|
interface TradingAgentsResult {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
|
date?: string;
|
||||||
final_decision?: {
|
final_decision?: {
|
||||||
decision: string;
|
decision: string;
|
||||||
reasoning: string;
|
reasoning: string;
|
||||||
|
|
@ -90,7 +91,7 @@ const AnalysisDataAdapter: React.FC<AnalysisDataAdapterProps> = ({ tradingResult
|
||||||
'Based on comprehensive analysis, the stock shows strong fundamentals with reasonable valuation and positive technical momentum, warranting a buy recommendation with appropriate risk management.'
|
'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} />;
|
return <AnalysisWidgets symbol={tradingResult.symbol} date={tradingResult.date} data={transformedData} rawData={tradingResult} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AnalysisDataAdapter;
|
export default AnalysisDataAdapter;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
// New interface for the transformed JSON structure
|
// New interface for the transformed JSON structure
|
||||||
interface TransformedAnalysisData {
|
interface TransformedAnalysisData {
|
||||||
|
|
@ -458,12 +458,77 @@ const TransformedDataAdapter: React.FC<TransformedDataAdapterProps> = ({ analysi
|
||||||
const [activeReportTab, setActiveReportTab] = React.useState<string>(availableReports[0]?.key || 'market');
|
const [activeReportTab, setActiveReportTab] = React.useState<string>(availableReports[0]?.key || 'market');
|
||||||
React.useEffect(() => { setActiveReportTab(availableReports[0]?.key || 'market'); }, [availableReports]);
|
React.useEffect(() => { setActiveReportTab(availableReports[0]?.key || 'market'); }, [availableReports]);
|
||||||
|
|
||||||
|
// Reflection functionality state
|
||||||
|
const [showReflectionModal, setShowReflectionModal] = React.useState(false);
|
||||||
|
const [lossResults, setLossResults] = React.useState('');
|
||||||
|
const [isReflecting, setIsReflecting] = React.useState(false);
|
||||||
|
const [reflectionResults, setReflectionResults] = React.useState(null);
|
||||||
|
const [reflectionError, setReflectionError] = React.useState('');
|
||||||
|
|
||||||
|
// Handle reflection API call
|
||||||
|
const handleReflection = async () => {
|
||||||
|
const symbolToUse = data.metadata.company_ticker;
|
||||||
|
const dateToUse = data.metadata.analysis_date;
|
||||||
|
|
||||||
|
console.log('Reflection attempt:', { symbolToUse, dateToUse, lossResults });
|
||||||
|
|
||||||
|
if (!lossResults.trim()) {
|
||||||
|
setReflectionError('Please enter your loss results');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsReflecting(true);
|
||||||
|
setReflectionError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://localhost:8000/reflect-on-analysis/${symbolToUse}/${dateToUse}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
returns_losses: lossResults
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.detail || 'Failed to perform reflection');
|
||||||
|
}
|
||||||
|
|
||||||
|
const reflectionData = await response.json();
|
||||||
|
setReflectionResults(reflectionData);
|
||||||
|
setReflectionError('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Reflection error:', error);
|
||||||
|
setReflectionError(error.message || 'Failed to perform reflection');
|
||||||
|
} finally {
|
||||||
|
setIsReflecting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModalClose = () => {
|
||||||
|
setShowReflectionModal(false);
|
||||||
|
setLossResults('');
|
||||||
|
setReflectionResults(null);
|
||||||
|
setReflectionError('');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="mx-auto max-w-6xl space-y-4">
|
<div className="mx-auto max-w-6xl space-y-4">
|
||||||
{/* Metadata - Full width */}
|
{/* Metadata - Full width */}
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
<h2 className="text-lg font-semibold mb-3">Company Information</h2>
|
<div className="flex justify-between items-center mb-3">
|
||||||
|
<h2 className="text-lg font-semibold">Company Information</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowReflectionModal(true)}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm font-medium"
|
||||||
|
title="Reflect on Analysis"
|
||||||
|
>
|
||||||
|
🤔 Reflect on Analysis
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<Stat label="Company" value={`${md?.company_name ?? '-'} (${md?.company_ticker ?? '-'})`} />
|
<Stat label="Company" value={`${md?.company_name ?? '-'} (${md?.company_ticker ?? '-'})`} />
|
||||||
<Stat label="Analysis Date" value={md?.analysis_date ?? '-'} />
|
<Stat label="Analysis Date" value={md?.analysis_date ?? '-'} />
|
||||||
|
|
@ -595,12 +660,85 @@ const TransformedDataAdapter: React.FC<TransformedDataAdapterProps> = ({ analysi
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Reflection Modal */}
|
||||||
|
{showReflectionModal && (
|
||||||
|
<div className="fixed inset-0 bg-gray-900 bg-opacity-75 flex justify-center items-center z-50">
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-6 w-full max-w-md mx-4">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-lg font-semibold">Reflect on Analysis</h2>
|
||||||
|
<button
|
||||||
|
onClick={handleModalClose}
|
||||||
|
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>
|
||||||
|
|
||||||
|
<form onSubmit={(e) => { e.preventDefault(); handleReflection(); }}>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Loss Results:
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={lossResults}
|
||||||
|
onChange={(e) => setLossResults(e.target.value)}
|
||||||
|
placeholder="e.g., Lost 15% due to unexpected earnings miss, market volatility, etc."
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
{reflectionError && (
|
||||||
|
<p className="mt-2 text-sm text-red-500">{reflectionError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleModalClose}
|
||||||
|
className="px-4 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isReflecting}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isReflecting ? 'Reflecting...' : 'Submit Reflection'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Display reflection results */}
|
||||||
|
{reflectionResults && (
|
||||||
|
<div className="mt-4 p-4 bg-gray-50 rounded-md">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 mb-2">Reflection Results:</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{Object.entries(reflectionResults.reflections || {}).map(([agentType, reflection]) => (
|
||||||
|
<div key={agentType} className="text-xs">
|
||||||
|
<span className="font-medium capitalize">{agentType.replace('_', ' ')}:</span>
|
||||||
|
<p className="text-gray-600 ml-2">{typeof reflection === 'string' ? reflection : JSON.stringify(reflection)}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render minimalist dashboard with all keys from the transformed JSON
|
// Render minimalist dashboard with all keys from the transformed JSON
|
||||||
return <MinimalTransformedDashboard data={transformedWithConfig} />;
|
return (
|
||||||
|
<div>
|
||||||
|
<MinimalTransformedDashboard data={transformedWithConfig} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TransformedDataAdapter;
|
export default TransformedDataAdapter;
|
||||||
|
|
|
||||||
|
|
@ -34,14 +34,24 @@ interface AnalysisWidgetsProps {
|
||||||
rawData?: any;
|
rawData?: any;
|
||||||
onBackWidget?: () => void;
|
onBackWidget?: () => void;
|
||||||
onRefreshWidget?: () => void;
|
onRefreshWidget?: () => void;
|
||||||
|
symbol?: string;
|
||||||
|
date?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData, onBackWidget, onRefreshWidget }) => {
|
const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData, onBackWidget, onRefreshWidget, symbol, date }) => {
|
||||||
const [activeTab, setActiveTab] = useState('bull');
|
const [activeTab, setActiveTab] = useState('bull');
|
||||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
|
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
|
||||||
const [positionSize, setPositionSize] = useState(10000);
|
const [positionSize, setPositionSize] = useState(10000);
|
||||||
const [portfolioAllocation, setPortfolioAllocation] = useState(4);
|
const [portfolioAllocation, setPortfolioAllocation] = useState(4);
|
||||||
|
|
||||||
|
const [showReflectionModal, setShowReflectionModal] = useState(false);
|
||||||
|
const [lossResults, setLossResults] = useState('');
|
||||||
|
const [isReflecting, setIsReflecting] = useState(false);
|
||||||
|
const [reflectionResults, setReflectionResults] = useState(null);
|
||||||
|
const [reflectionError, setReflectionError] = useState('');
|
||||||
|
|
||||||
|
console.log('AnalysisWidgets props:', { symbol, date, hasRawData: !!rawData });
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
if (onBackWidget) return onBackWidget();
|
if (onBackWidget) return onBackWidget();
|
||||||
if (typeof window !== 'undefined' && window.history) window.history.back();
|
if (typeof window !== 'undefined' && window.history) window.history.back();
|
||||||
|
|
@ -51,6 +61,47 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData, onBack
|
||||||
if (typeof window !== 'undefined') window.location.reload();
|
if (typeof window !== 'undefined') window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleReflection = async () => {
|
||||||
|
const symbolToUse = symbol || data.symbol || 'UNKNOWN';
|
||||||
|
const dateToUse = date || new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
console.log('Reflection attempt:', { symbolToUse, dateToUse, lossResults });
|
||||||
|
|
||||||
|
if (!lossResults.trim()) {
|
||||||
|
setReflectionError('Please enter your loss results');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsReflecting(true);
|
||||||
|
setReflectionError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://localhost:8000/reflect-on-analysis/${symbolToUse}/${dateToUse}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
returns_losses: lossResults
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.detail || 'Failed to perform reflection');
|
||||||
|
}
|
||||||
|
|
||||||
|
const reflectionData = await response.json();
|
||||||
|
setReflectionResults(reflectionData);
|
||||||
|
setReflectionError('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Reflection error:', error);
|
||||||
|
setReflectionError(error.message || 'Failed to perform reflection');
|
||||||
|
} finally {
|
||||||
|
setIsReflecting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const WidgetHeader: React.FC<{ title: string }> = ({ title }) => (
|
const WidgetHeader: React.FC<{ title: string }> = ({ title }) => (
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<button
|
<button
|
||||||
|
|
@ -63,57 +114,33 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData, onBack
|
||||||
←
|
←
|
||||||
</button>
|
</button>
|
||||||
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
|
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
|
||||||
<button
|
<div className="flex items-center space-x-2">
|
||||||
type="button"
|
<button
|
||||||
onClick={handleRefresh}
|
type="button"
|
||||||
className="p-2 rounded hover:bg-gray-100 text-gray-600"
|
onClick={() => setShowReflectionModal(true)}
|
||||||
aria-label="Refresh"
|
className="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm"
|
||||||
title="Refresh"
|
title={`Reflect on Analysis (Symbol: ${symbol || data.symbol || 'N/A'}, Date: ${date || 'N/A'})`}
|
||||||
>
|
>
|
||||||
⟳
|
🤔 Reflect
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
className="p-2 rounded hover:bg-gray-100 text-gray-600"
|
||||||
|
aria-label="Refresh"
|
||||||
|
title="Refresh"
|
||||||
|
>
|
||||||
|
⟳
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mock price data for chart
|
const handleModalClose = () => {
|
||||||
const priceData = [
|
setShowReflectionModal(false);
|
||||||
{ date: '2024-07-01', price: 4.20, volume: 1200000 },
|
setLossResults('');
|
||||||
{ date: '2024-07-08', price: 4.45, volume: 1500000 },
|
setReflectionResults(null);
|
||||||
{ date: '2024-07-15', price: 4.80, volume: 1800000 },
|
setReflectionError('');
|
||||||
{ 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 (
|
return (
|
||||||
|
|
@ -554,6 +581,59 @@ const AnalysisWidgets: React.FC<AnalysisWidgetsProps> = ({ data, rawData, onBack
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Reflection Modal */}
|
||||||
|
{showReflectionModal && (
|
||||||
|
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity">
|
||||||
|
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||||
|
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
|
<div className="sm:flex sm:items-start">
|
||||||
|
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
|
<svg className="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
|
||||||
|
Reflect on Analysis
|
||||||
|
</h3>
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Please enter your loss results to reflect on the analysis.
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={lossResults}
|
||||||
|
onChange={(e) => setLossResults(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"
|
||||||
|
/>
|
||||||
|
{reflectionError && (
|
||||||
|
<p className="text-sm text-red-500">{reflectionError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleReflection}
|
||||||
|
disabled={isReflecting}
|
||||||
|
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm"
|
||||||
|
>
|
||||||
|
{isReflecting ? 'Reflecting...' : 'Reflect'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleModalClose}
|
||||||
|
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue