trading-tools / utils /report_models.py
Deploy Bot
Deploy Trading Analysis Platform to HuggingFace Spaces
a1bf219
"""Data models for phase-organized analysis reports."""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, List, Optional
from config.models import AnalysisPhase, InvestmentStyle, PhaseConfiguration
@dataclass
class AgentOutput:
"""Output from a single agent within a phase."""
agent_name: str
"""Name of the agent (e.g., 'IndicatorAgent', 'PatternAgent')."""
report: str
"""Markdown-formatted agent analysis report."""
charts: List[str] = field(default_factory=list)
"""List of base64-encoded chart images (PNG format, no data URI prefix)."""
educational_notes: Optional[str] = None
"""Plain-English explanation of agent findings (if educational_mode enabled)."""
execution_time_seconds: float = 0.0
"""Time taken to execute this agent."""
metadata: Dict[str, Any] = field(default_factory=dict)
"""Additional agent-specific metadata (e.g., indicator values, pattern names)."""
@dataclass
class PhaseOutput:
"""Output from a complete analysis phase."""
phase: AnalysisPhase
"""Which phase this output represents."""
agents: List[AgentOutput] = field(default_factory=list)
"""Outputs from all agents in this phase."""
phase_summary: str = ""
"""High-level summary of this phase's findings."""
execution_time_seconds: float = 0.0
"""Total time to execute all agents in this phase."""
score: Optional[float] = None
"""Numeric score (0-10) representing phase analysis outcome.
0=extremely bearish, 5=neutral, 10=extremely bullish."""
@dataclass
class AnalysisReport:
"""Complete analysis report with phase-organized results."""
ticker: str
"""Ticker symbol analyzed."""
timeframe: str
"""Timeframe used for analysis (1d, 1w, 1mo, etc.)."""
investment_style: InvestmentStyle
"""Investment style profile used."""
timestamp: datetime = field(default_factory=datetime.now)
"""When analysis was generated."""
phases: List[PhaseOutput] = field(default_factory=list)
"""Phase-by-phase analysis results."""
executive_summary: str = ""
"""High-level summary of entire analysis (generated by final agent)."""
final_decision: Optional[str] = None
"""Final trading decision (BUY/SELL/HOLD) if DECISION phase was enabled."""
total_execution_time_seconds: float = 0.0
"""End-to-end analysis execution time."""
config: Optional[PhaseConfiguration] = None
"""Configuration used for this analysis."""
cost_summary: Optional[Dict[str, Any]] = None
"""Cost tracking information from analysis (if available).
Includes total_cost, tokens, provider breakdown, and free tier usage."""
def get_phase_output(self, phase: AnalysisPhase) -> Optional[PhaseOutput]:
"""Retrieve output for a specific phase.
Args:
phase: The phase to retrieve
Returns:
PhaseOutput if found, None otherwise
"""
for p in self.phases:
if p.phase == phase:
return p
return None
def to_markdown(self) -> str:
"""Convert report to markdown format for display.
Returns:
Markdown-formatted report string
"""
# Header section
style_name = self.investment_style.value.replace("_", " ").title()
md = f"""# Analysis Report: {self.ticker}
**Timeframe**: {self.timeframe}
**Investment Style**: {style_name}
**Generated**: {self.timestamp.strftime("%Y-%m-%d %H:%M:%S")}
**Execution Time**: {self.total_execution_time_seconds:.1f}s
---
## Executive Summary
{self.executive_summary}
"""
# Final decision (if available)
if self.final_decision:
md += f"\n**Final Decision**: {self.final_decision}\n"
# Cost information (if available)
if self.cost_summary:
md += "\n## Analysis Cost Summary\n\n"
md += f"**Total Cost**: ${self.cost_summary['total_cost']:.4f}\n"
md += f"**Total Tokens**: {self.cost_summary['total_tokens']:,} "
md += f"({self.cost_summary['total_input_tokens']:,} in + {self.cost_summary['total_output_tokens']:,} out)\n"
# Provider breakdown
if self.cost_summary.get("provider_costs"):
md += "\n**Cost by Provider:**\n\n"
md += "| Provider | Cost | Tokens | Free Tier Calls |\n"
md += "|----------|------|--------|----------------|\n"
for provider, cost in sorted(
self.cost_summary["provider_costs"].items(),
key=lambda x: x[1],
reverse=True,
):
tokens = self.cost_summary.get("provider_tokens", {}).get(
provider, 0
)
free_calls = self.cost_summary.get("free_tier_calls", {}).get(
provider, 0
)
if free_calls > 0:
cost_str = (
f"${cost:.4f} (free)" if cost == 0.0 else f"${cost:.4f}"
)
md += (
f"| {provider} | {cost_str} | {tokens:,} | {free_calls} |\n"
)
else:
md += f"| {provider} | ${cost:.4f} | {tokens:,} | 0 |\n"
md += "\n*Costs are estimates based on current pricing. Free tier usage is tracked automatically.*\n"
md += "\n---\n\n"
# Phase-by-phase breakdown
for phase_output in self.phases:
phase_name = phase_output.phase.value.replace("_", " ").title()
md += f"## {phase_name} Phase\n\n"
if phase_output.phase_summary:
md += f"*{phase_output.phase_summary}*\n\n"
md += f"*Execution time: {phase_output.execution_time_seconds:.1f}s*\n\n"
# Agent outputs within this phase
for agent_output in phase_output.agents:
md += f"### {agent_output.agent_name}\n\n"
md += f"{agent_output.report}\n\n"
# Educational notes
if agent_output.educational_notes:
md += f"💡 **Plain English**: {agent_output.educational_notes}\n\n"
# Embedded charts
for chart_b64 in agent_output.charts:
md += f'<img src="data:image/png;base64,{chart_b64}" width="800"/>\n\n'
md += "\n---\n\n"
return md
def to_dict(self) -> Dict[str, Any]:
"""Convert report to dictionary for JSON serialization.
Returns:
Dictionary representation of the report
"""
return {
"ticker": self.ticker,
"timeframe": self.timeframe,
"investment_style": self.investment_style.value,
"timestamp": self.timestamp.isoformat(),
"executive_summary": self.executive_summary,
"final_decision": self.final_decision,
"total_execution_time_seconds": self.total_execution_time_seconds,
"cost_summary": self.cost_summary if self.cost_summary else None,
"config": {
"investment_style": self.config.investment_style.value
if self.config
else None,
"enabled_phases": [p.value for p in self.config.enabled_phases]
if self.config
else [],
"timeframe": self.config.timeframe if self.config else None,
"chart_period_days": self.config.chart_period_days
if self.config
else None,
"educational_mode": self.config.educational_mode
if self.config
else None,
}
if self.config
else None,
"phases": [
{
"phase": phase.phase.value,
"phase_summary": phase.phase_summary,
"execution_time_seconds": phase.execution_time_seconds,
"agents": [
{
"agent_name": agent.agent_name,
"report": agent.report,
"charts": agent.charts,
"educational_notes": agent.educational_notes,
"execution_time_seconds": agent.execution_time_seconds,
"metadata": agent.metadata,
}
for agent in phase.agents
],
}
for phase in self.phases
],
}
def to_html(self) -> str:
"""Convert report to HTML format with enhanced styling.
Returns:
HTML-formatted report string with CSS styling
"""
style_name = self.investment_style.value.replace("_", " ").title()
# CSS styling
html = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Analysis Report: {ticker}</title>
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #333;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}}
.container {{
background-color: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
h1 {{
color: #1a1a1a;
border-bottom: 3px solid #4CAF50;
padding-bottom: 10px;
margin-bottom: 20px;
}}
h2 {{
color: #2c3e50;
margin-top: 40px;
margin-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
padding-bottom: 10px;
}}
h3 {{
color: #34495e;
margin-top: 30px;
margin-bottom: 15px;
}}
.metadata {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
background-color: #f8f9fa;
padding: 20px;
border-radius: 6px;
margin-bottom: 30px;
}}
.metadata-item {{
display: flex;
flex-direction: column;
}}
.metadata-label {{
font-weight: bold;
color: #666;
font-size: 0.9em;
margin-bottom: 5px;
}}
.metadata-value {{
color: #1a1a1a;
font-size: 1.1em;
}}
.executive-summary {{
background-color: #e8f5e9;
border-left: 4px solid #4CAF50;
padding: 20px;
margin: 20px 0;
border-radius: 4px;
}}
.final-decision {{
background-color: #fff3e0;
border-left: 4px solid #FF9800;
padding: 20px;
margin: 20px 0;
border-radius: 4px;
font-size: 1.2em;
font-weight: bold;
}}
.phase-section {{
margin-bottom: 40px;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
}}
.phase-header {{
background-color: #3498db;
color: white;
padding: 15px 20px;
font-size: 1.3em;
font-weight: bold;
}}
.phase-content {{
padding: 20px;
}}
.phase-summary {{
font-style: italic;
color: #555;
margin-bottom: 15px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
}}
.agent-output {{
margin-bottom: 30px;
padding: 15px;
background-color: #fafafa;
border-radius: 6px;
}}
.agent-name {{
font-weight: bold;
color: #2c3e50;
font-size: 1.1em;
margin-bottom: 10px;
}}
.educational-note {{
background-color: #e3f2fd;
border-left: 4px solid #2196F3;
padding: 15px;
margin: 15px 0;
border-radius: 4px;
}}
.educational-note::before {{
content: "💡 ";
font-size: 1.2em;
}}
.chart {{
margin: 20px 0;
text-align: center;
}}
.chart img {{
max-width: 100%;
height: auto;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}}
.execution-time {{
color: #666;
font-size: 0.9em;
font-style: italic;
}}
hr {{
border: none;
border-top: 1px solid #e0e0e0;
margin: 30px 0;
}}
@media print {{
body {{
background-color: white;
}}
.container {{
box-shadow: none;
padding: 0;
}}
}}
</style>
</head>
<body>
<div class="container">
<h1>Analysis Report: {ticker}</h1>
<div class="metadata">
<div class="metadata-item">
<span class="metadata-label">Timeframe</span>
<span class="metadata-value">{timeframe}</span>
</div>
<div class="metadata-item">
<span class="metadata-label">Investment Style</span>
<span class="metadata-value">{style_name}</span>
</div>
<div class="metadata-item">
<span class="metadata-label">Generated</span>
<span class="metadata-value">{timestamp}</span>
</div>
<div class="metadata-item">
<span class="metadata-label">Execution Time</span>
<span class="metadata-value">{execution_time:.1f}s</span>
</div>
</div>
<h2>Executive Summary</h2>
<div class="executive-summary">
{executive_summary}
</div>
""".format(
ticker=self.ticker,
timeframe=self.timeframe,
style_name=style_name,
timestamp=self.timestamp.strftime("%Y-%m-%d %H:%M:%S"),
execution_time=self.total_execution_time_seconds,
executive_summary=self._markdown_to_html(self.executive_summary),
)
# Final decision
if self.final_decision:
html += f"""
<div class="final-decision">
Final Decision: {self.final_decision}
</div>
"""
# Cost summary
if self.cost_summary:
html += """
<h2>Analysis Cost Summary</h2>
<div class="metadata">
<div class="metadata-item">
<span class="metadata-label">Total Cost</span>
<span class="metadata-value">${total_cost:.4f}</span>
</div>
<div class="metadata-item">
<span class="metadata-label">Total Tokens</span>
<span class="metadata-value">{total_tokens:,}</span>
</div>
<div class="metadata-item">
<span class="metadata-label">Input Tokens</span>
<span class="metadata-value">{input_tokens:,}</span>
</div>
<div class="metadata-item">
<span class="metadata-label">Output Tokens</span>
<span class="metadata-value">{output_tokens:,}</span>
</div>
</div>
""".format(
total_cost=self.cost_summary["total_cost"],
total_tokens=self.cost_summary["total_tokens"],
input_tokens=self.cost_summary["total_input_tokens"],
output_tokens=self.cost_summary["total_output_tokens"],
)
# Provider breakdown table
if self.cost_summary.get("provider_costs"):
html += """
<h3>Cost by Provider</h3>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
<thead>
<tr style="background-color: #f0f0f0; border-bottom: 2px solid #ddd;">
<th style="padding: 12px; text-align: left;">Provider</th>
<th style="padding: 12px; text-align: right;">Cost</th>
<th style="padding: 12px; text-align: right;">Tokens</th>
<th style="padding: 12px; text-align: right;">Free Tier Calls</th>
</tr>
</thead>
<tbody>
"""
for provider, cost in sorted(
self.cost_summary["provider_costs"].items(),
key=lambda x: x[1],
reverse=True,
):
tokens = self.cost_summary.get("provider_tokens", {}).get(
provider, 0
)
free_calls = self.cost_summary.get("free_tier_calls", {}).get(
provider, 0
)
if free_calls > 0:
cost_str = (
f"${cost:.4f} (free)" if cost == 0.0 else f"${cost:.4f}"
)
style = "background-color: #e8f5e9;" if cost == 0.0 else ""
else:
cost_str = f"${cost:.4f}"
style = ""
html += f"""
<tr style="border-bottom: 1px solid #ddd; {style}">
<td style="padding: 10px;">{provider}</td>
<td style="padding: 10px; text-align: right;">{cost_str}</td>
<td style="padding: 10px; text-align: right;">{tokens:,}</td>
<td style="padding: 10px; text-align: right;">{free_calls}</td>
</tr>
"""
html += """
</tbody>
</table>
<p style="font-size: 0.9em; color: #666; font-style: italic;">
Costs are estimates based on current pricing. Free tier usage is tracked automatically.
</p>
"""
html += "\n <hr>\n"
# Phase-by-phase breakdown
for phase_output in self.phases:
phase_name = phase_output.phase.value.replace("_", " ").title()
html += f"""
<div class="phase-section">
<div class="phase-header">{phase_name} Phase</div>
<div class="phase-content">
"""
if phase_output.phase_summary:
html += f"""
<div class="phase-summary">{self._markdown_to_html(phase_output.phase_summary)}</div>
"""
html += f"""
<p class="execution-time">Execution time: {phase_output.execution_time_seconds:.1f}s</p>
"""
# Agent outputs within this phase
for agent_output in phase_output.agents:
html += f"""
<div class="agent-output">
<div class="agent-name">{agent_output.agent_name}</div>
<div class="agent-report">
{self._markdown_to_html(agent_output.report)}
</div>
"""
# Educational notes
if agent_output.educational_notes:
html += f"""
<div class="educational-note">
<strong>Plain English:</strong> {self._markdown_to_html(agent_output.educational_notes)}
</div>
"""
# Embedded charts
if agent_output.charts:
for chart_b64 in agent_output.charts:
html += f"""
<div class="chart">
<img src="data:image/png;base64,{chart_b64}" alt="Chart">
</div>
"""
html += """
</div>
"""
html += """
</div>
</div>
"""
html += """
</div>
</body>
</html>
"""
return html
def _markdown_to_html(self, markdown_text: str) -> str:
"""Convert basic markdown to HTML (simplified converter).
Args:
markdown_text: Markdown-formatted text
Returns:
HTML-formatted text with basic markdown converted
"""
if not markdown_text:
return ""
import re
html = markdown_text
# Bold text
html = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", html)
# Italic text
html = re.sub(r"\*(.+?)\*", r"<em>\1</em>", html)
# Links
html = re.sub(r"\[(.+?)\]\((.+?)\)", r'<a href="\2">\1</a>', html)
# Code blocks
html = re.sub(r"`(.+?)`", r"<code>\1</code>", html)
# Line breaks
html = html.replace("\n\n", "</p><p>")
html = html.replace("\n", "<br>")
# Wrap in paragraph if not already
if not html.startswith("<"):
html = f"<p>{html}</p>"
return html