Spaces:
Running
on
Zero
Running
on
Zero
Commit
Β·
22e48cb
1
Parent(s):
4cc5533
feat: display ML forecasts in Gradio UI
Browse filesAdd ensemble forecast visualisation to portfolio analysis results:
- Display 30-day price predictions with direction indicators
- Show model count and confidence information
- Include Phase 2.5 duration in performance metrics
- Add phase_2_5_duration_ms to AgentState model
- Format forecasts with emoji indicators (π/π/β‘οΈ)
- Display below key metrics and above LLM synthesis
- app.py +27 -1
- backend/agents/workflow.py +1 -0
- backend/models/agent_state.py +1 -0
app.py
CHANGED
|
@@ -207,12 +207,15 @@ def format_performance_metrics(final_state: AgentState) -> str:
|
|
| 207 |
# Phase Execution Times
|
| 208 |
phase_1_ms = final_state.get("phase_1_duration_ms", 0) or 0
|
| 209 |
phase_2_ms = final_state.get("phase_2_duration_ms", 0) or 0
|
|
|
|
| 210 |
phase_3_ms = final_state.get("phase_3_duration_ms", 0) or 0
|
| 211 |
-
total_ms = phase_1_ms + phase_2_ms + phase_3_ms
|
| 212 |
|
| 213 |
metrics_md += "**Execution Timeline:**\n\n"
|
| 214 |
metrics_md += f"- Phase 1 (Data Collection): {phase_1_ms:,}ms\n"
|
| 215 |
metrics_md += f"- Phase 2 (Computation): {phase_2_ms:,}ms\n"
|
|
|
|
|
|
|
| 216 |
metrics_md += f"- Phase 3 (LLM Synthesis): {phase_3_ms:,}ms\n"
|
| 217 |
metrics_md += f"- **Total**: {total_ms:,}ms ({total_ms / 1000:.2f}s)\n\n"
|
| 218 |
|
|
@@ -304,6 +307,28 @@ def format_analysis_result(final_state: AgentState, holdings: List[Dict[str, Any
|
|
| 304 |
- **CVaR (95%)**: ${cvar_95:,.2f}
|
| 305 |
"""
|
| 306 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
# Get LLM synthesis and recommendations
|
| 308 |
ai_synthesis = final_state.get("ai_synthesis", "Analysis completed successfully.")
|
| 309 |
recommendations = final_state.get("recommendations", [])
|
|
@@ -322,6 +347,7 @@ def format_analysis_result(final_state: AgentState, holdings: List[Dict[str, Any
|
|
| 322 |
|
| 323 |
{metrics_md}
|
| 324 |
|
|
|
|
| 325 |
{ai_synthesis}
|
| 326 |
|
| 327 |
{recs_md}
|
|
|
|
| 207 |
# Phase Execution Times
|
| 208 |
phase_1_ms = final_state.get("phase_1_duration_ms", 0) or 0
|
| 209 |
phase_2_ms = final_state.get("phase_2_duration_ms", 0) or 0
|
| 210 |
+
phase_2_5_ms = final_state.get("phase_2_5_duration_ms", 0) or 0
|
| 211 |
phase_3_ms = final_state.get("phase_3_duration_ms", 0) or 0
|
| 212 |
+
total_ms = phase_1_ms + phase_2_ms + phase_2_5_ms + phase_3_ms
|
| 213 |
|
| 214 |
metrics_md += "**Execution Timeline:**\n\n"
|
| 215 |
metrics_md += f"- Phase 1 (Data Collection): {phase_1_ms:,}ms\n"
|
| 216 |
metrics_md += f"- Phase 2 (Computation): {phase_2_ms:,}ms\n"
|
| 217 |
+
if phase_2_5_ms > 0:
|
| 218 |
+
metrics_md += f"- Phase 2.5 (ML Predictions): {phase_2_5_ms:,}ms\n"
|
| 219 |
metrics_md += f"- Phase 3 (LLM Synthesis): {phase_3_ms:,}ms\n"
|
| 220 |
metrics_md += f"- **Total**: {total_ms:,}ms ({total_ms / 1000:.2f}s)\n\n"
|
| 221 |
|
|
|
|
| 307 |
- **CVaR (95%)**: ${cvar_95:,.2f}
|
| 308 |
"""
|
| 309 |
|
| 310 |
+
# Format ML forecasts if available
|
| 311 |
+
forecasts_md = ""
|
| 312 |
+
ensemble_forecasts = final_state.get("ensemble_forecasts", {})
|
| 313 |
+
if ensemble_forecasts:
|
| 314 |
+
forecasts_md = "### ML Price Forecasts (30-day)\n\n"
|
| 315 |
+
for ticker, forecast_data in ensemble_forecasts.items():
|
| 316 |
+
if isinstance(forecast_data, dict):
|
| 317 |
+
models_used = forecast_data.get("models_used", [])
|
| 318 |
+
num_models = len(models_used) if models_used else forecast_data.get("metadata", {}).get("num_models", "N/A")
|
| 319 |
+
|
| 320 |
+
# Get first and last prediction (current vs 30-day)
|
| 321 |
+
predictions = forecast_data.get("predictions", [])
|
| 322 |
+
if predictions and len(predictions) > 0:
|
| 323 |
+
first_pred = float(predictions[0])
|
| 324 |
+
last_pred = float(predictions[-1])
|
| 325 |
+
change_pct = ((last_pred - first_pred) / first_pred * 100) if first_pred > 0 else 0
|
| 326 |
+
|
| 327 |
+
direction = "π" if change_pct > 0 else "π" if change_pct < 0 else "β‘οΈ"
|
| 328 |
+
forecasts_md += f"- **{ticker}**: {direction} {change_pct:+.1f}% (using {num_models} models)\n"
|
| 329 |
+
|
| 330 |
+
forecasts_md += "\n*Note: Forecasts combine Chronos foundation model with statistical baselines.*\n\n"
|
| 331 |
+
|
| 332 |
# Get LLM synthesis and recommendations
|
| 333 |
ai_synthesis = final_state.get("ai_synthesis", "Analysis completed successfully.")
|
| 334 |
recommendations = final_state.get("recommendations", [])
|
|
|
|
| 347 |
|
| 348 |
{metrics_md}
|
| 349 |
|
| 350 |
+
{forecasts_md}
|
| 351 |
{ai_synthesis}
|
| 352 |
|
| 353 |
{recs_md}
|
backend/agents/workflow.py
CHANGED
|
@@ -393,6 +393,7 @@ class PortfolioAnalysisWorkflow:
|
|
| 393 |
|
| 394 |
# Track phase duration
|
| 395 |
phase_duration_ms = int((time.perf_counter() - phase_start) * 1000)
|
|
|
|
| 396 |
|
| 397 |
logger.info(
|
| 398 |
f"PHASE 2.5 COMPLETE: Generated forecasts for {len(ensemble_forecasts)} assets ({phase_duration_ms}ms)"
|
|
|
|
| 393 |
|
| 394 |
# Track phase duration
|
| 395 |
phase_duration_ms = int((time.perf_counter() - phase_start) * 1000)
|
| 396 |
+
state["phase_2_5_duration_ms"] = phase_duration_ms
|
| 397 |
|
| 398 |
logger.info(
|
| 399 |
f"PHASE 2.5 COMPLETE: Generated forecasts for {len(ensemble_forecasts)} assets ({phase_duration_ms}ms)"
|
backend/models/agent_state.py
CHANGED
|
@@ -67,6 +67,7 @@ class AgentState(TypedDict):
|
|
| 67 |
# Performance Metrics
|
| 68 |
phase_1_duration_ms: Optional[int]
|
| 69 |
phase_2_duration_ms: Optional[int]
|
|
|
|
| 70 |
phase_3_duration_ms: Optional[int]
|
| 71 |
llm_input_tokens: Optional[int]
|
| 72 |
llm_output_tokens: Optional[int]
|
|
|
|
| 67 |
# Performance Metrics
|
| 68 |
phase_1_duration_ms: Optional[int]
|
| 69 |
phase_2_duration_ms: Optional[int]
|
| 70 |
+
phase_2_5_duration_ms: Optional[int]
|
| 71 |
phase_3_duration_ms: Optional[int]
|
| 72 |
llm_input_tokens: Optional[int]
|
| 73 |
llm_output_tokens: Optional[int]
|