feat(backtest): add ReportGenerator for PDF/HTML reports - Issue #44 (44 tests)
Implements comprehensive report generation for backtest results: - ReportFormat, ReportSection, ChartType enums for configuration - ReportConfig dataclass for customization (format, sections, colors) - ChartData, ReportContent, ReportResult dataclasses - ReportGenerator class with multi-format output: - HTML reports with embedded CSS styling - PDF reports (with HTML fallback) - JSON structured data export - Markdown text reports - SVG chart generation for equity curves and drawdowns - Monthly returns heatmap visualization - Configurable color schemes and sections Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
722b88d5b4
commit
3d1267a818
|
|
@ -0,0 +1,805 @@
|
|||
"""Tests for report generator module.
|
||||
|
||||
Issue #44: [BT-43] Report generator - PDF/HTML reports
|
||||
"""
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tradingagents.backtest import (
|
||||
# Backtest engine
|
||||
BacktestResult,
|
||||
BacktestTrade,
|
||||
BacktestSnapshot,
|
||||
BacktestConfig,
|
||||
OrderSide,
|
||||
# Report generator
|
||||
ReportFormat,
|
||||
ReportSection,
|
||||
ChartType,
|
||||
ReportConfig,
|
||||
ChartData,
|
||||
ReportContent,
|
||||
ReportResult,
|
||||
ReportGenerator,
|
||||
create_report_generator,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test Fixtures
|
||||
# ============================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def sample_config() -> BacktestConfig:
|
||||
"""Create sample backtest configuration."""
|
||||
return BacktestConfig(
|
||||
initial_capital=Decimal("100000"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_trades() -> list[BacktestTrade]:
|
||||
"""Create sample trades for testing."""
|
||||
trades = []
|
||||
base_date = datetime(2023, 1, 3)
|
||||
|
||||
# Winning trade
|
||||
trades.append(BacktestTrade(
|
||||
timestamp=base_date,
|
||||
symbol="AAPL",
|
||||
side=OrderSide.BUY,
|
||||
quantity=Decimal("100"),
|
||||
price=Decimal("150.00"),
|
||||
commission=Decimal("1.00"),
|
||||
slippage=Decimal("0.50"),
|
||||
pnl=Decimal("500.00"),
|
||||
))
|
||||
|
||||
# Losing trade
|
||||
trades.append(BacktestTrade(
|
||||
timestamp=base_date + timedelta(days=5),
|
||||
symbol="GOOGL",
|
||||
side=OrderSide.BUY,
|
||||
quantity=Decimal("50"),
|
||||
price=Decimal("100.00"),
|
||||
commission=Decimal("1.00"),
|
||||
slippage=Decimal("0.25"),
|
||||
pnl=Decimal("-200.00"),
|
||||
))
|
||||
|
||||
# Another winning trade
|
||||
trades.append(BacktestTrade(
|
||||
timestamp=base_date + timedelta(days=10),
|
||||
symbol="MSFT",
|
||||
side=OrderSide.BUY,
|
||||
quantity=Decimal("75"),
|
||||
price=Decimal("300.00"),
|
||||
commission=Decimal("1.50"),
|
||||
slippage=Decimal("0.75"),
|
||||
pnl=Decimal("750.00"),
|
||||
))
|
||||
|
||||
return trades
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_snapshots() -> list[BacktestSnapshot]:
|
||||
"""Create sample snapshots for testing."""
|
||||
snapshots = []
|
||||
base_date = datetime(2023, 1, 1)
|
||||
|
||||
for i in range(30):
|
||||
equity = Decimal("100000") + Decimal(str(i * 100 - 50 * (i % 5)))
|
||||
snapshots.append(BacktestSnapshot(
|
||||
timestamp=base_date + timedelta(days=i),
|
||||
cash=equity * Decimal("0.3"),
|
||||
positions_value=equity * Decimal("0.7"),
|
||||
total_value=equity,
|
||||
))
|
||||
|
||||
return snapshots
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_result(sample_trades, sample_snapshots) -> BacktestResult:
|
||||
"""Create sample backtest result."""
|
||||
return BacktestResult(
|
||||
initial_capital=Decimal("100000"),
|
||||
final_value=Decimal("101050"),
|
||||
total_return=Decimal("1.05"),
|
||||
total_trades=3,
|
||||
winning_trades=2,
|
||||
losing_trades=1,
|
||||
win_rate=Decimal("66.67"),
|
||||
profit_factor=Decimal("6.25"),
|
||||
max_drawdown=Decimal("0.50"),
|
||||
sharpe_ratio=Decimal("1.85"),
|
||||
sortino_ratio=Decimal("2.50"),
|
||||
total_commission=Decimal("3.50"),
|
||||
total_slippage=Decimal("1.50"),
|
||||
start_date=datetime(2023, 1, 1),
|
||||
end_date=datetime(2023, 1, 30),
|
||||
trades=sample_trades,
|
||||
snapshots=sample_snapshots,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test Enums
|
||||
# ============================================================================
|
||||
|
||||
class TestReportFormat:
|
||||
"""Tests for ReportFormat enum."""
|
||||
|
||||
def test_values(self):
|
||||
"""Test enum values."""
|
||||
assert ReportFormat.HTML.value == "html"
|
||||
assert ReportFormat.PDF.value == "pdf"
|
||||
assert ReportFormat.JSON.value == "json"
|
||||
assert ReportFormat.MARKDOWN.value == "markdown"
|
||||
|
||||
def test_all_formats(self):
|
||||
"""Test all formats exist."""
|
||||
formats = list(ReportFormat)
|
||||
assert len(formats) == 4
|
||||
|
||||
|
||||
class TestReportSection:
|
||||
"""Tests for ReportSection enum."""
|
||||
|
||||
def test_values(self):
|
||||
"""Test enum values."""
|
||||
assert ReportSection.SUMMARY.value == "summary"
|
||||
assert ReportSection.EQUITY_CURVE.value == "equity_curve"
|
||||
assert ReportSection.DRAWDOWN.value == "drawdown"
|
||||
assert ReportSection.TRADE_LIST.value == "trade_list"
|
||||
|
||||
def test_all_sections(self):
|
||||
"""Test all sections exist."""
|
||||
sections = list(ReportSection)
|
||||
assert len(sections) >= 8
|
||||
|
||||
|
||||
class TestChartType:
|
||||
"""Tests for ChartType enum."""
|
||||
|
||||
def test_values(self):
|
||||
"""Test enum values."""
|
||||
assert ChartType.LINE.value == "line"
|
||||
assert ChartType.BAR.value == "bar"
|
||||
assert ChartType.HEATMAP.value == "heatmap"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test Data Classes
|
||||
# ============================================================================
|
||||
|
||||
class TestReportConfig:
|
||||
"""Tests for ReportConfig dataclass."""
|
||||
|
||||
def test_default_creation(self):
|
||||
"""Test default configuration."""
|
||||
config = ReportConfig()
|
||||
assert config.title == "Backtest Report"
|
||||
assert config.author == ""
|
||||
assert config.chart_width == 800
|
||||
assert config.chart_height == 400
|
||||
assert config.decimal_places == 2
|
||||
assert config.include_trade_list is True
|
||||
assert config.max_trades_shown == 100
|
||||
assert config.include_timestamp is True
|
||||
|
||||
def test_custom_creation(self):
|
||||
"""Test custom configuration."""
|
||||
config = ReportConfig(
|
||||
title="My Report",
|
||||
author="Test Author",
|
||||
chart_width=1024,
|
||||
chart_height=768,
|
||||
decimal_places=4,
|
||||
include_trade_list=False,
|
||||
max_trades_shown=50,
|
||||
)
|
||||
assert config.title == "My Report"
|
||||
assert config.author == "Test Author"
|
||||
assert config.chart_width == 1024
|
||||
assert config.chart_height == 768
|
||||
assert config.decimal_places == 4
|
||||
assert config.include_trade_list is False
|
||||
assert config.max_trades_shown == 50
|
||||
|
||||
def test_color_scheme(self):
|
||||
"""Test default color scheme."""
|
||||
config = ReportConfig()
|
||||
assert "primary" in config.color_scheme
|
||||
assert "secondary" in config.color_scheme
|
||||
assert "success" in config.color_scheme
|
||||
assert "danger" in config.color_scheme
|
||||
|
||||
def test_include_sections_default(self):
|
||||
"""Test default sections include all."""
|
||||
config = ReportConfig()
|
||||
# Default includes all sections
|
||||
assert len(config.include_sections) > 0
|
||||
|
||||
|
||||
class TestChartData:
|
||||
"""Tests for ChartData dataclass."""
|
||||
|
||||
def test_creation(self):
|
||||
"""Test chart data creation."""
|
||||
chart = ChartData(
|
||||
chart_type=ChartType.LINE,
|
||||
title="Test Chart",
|
||||
x_data=[1, 2, 3],
|
||||
y_data=[10, 20, 30],
|
||||
x_label="X Axis",
|
||||
y_label="Y Axis",
|
||||
)
|
||||
assert chart.chart_type == ChartType.LINE
|
||||
assert chart.title == "Test Chart"
|
||||
assert chart.x_data == [1, 2, 3]
|
||||
assert chart.y_data == [10, 20, 30]
|
||||
assert chart.x_label == "X Axis"
|
||||
assert chart.y_label == "Y Axis"
|
||||
|
||||
def test_default_values(self):
|
||||
"""Test default values."""
|
||||
chart = ChartData(
|
||||
chart_type=ChartType.BAR,
|
||||
title="Test",
|
||||
x_data=[],
|
||||
y_data=[],
|
||||
)
|
||||
assert chart.x_label == ""
|
||||
assert chart.y_label == ""
|
||||
assert chart.series_names == []
|
||||
assert chart.colors == []
|
||||
|
||||
|
||||
class TestReportContent:
|
||||
"""Tests for ReportContent dataclass."""
|
||||
|
||||
def test_creation(self):
|
||||
"""Test content creation."""
|
||||
now = datetime.now()
|
||||
content = ReportContent(
|
||||
title="Test Report",
|
||||
generated_at=now,
|
||||
)
|
||||
assert content.title == "Test Report"
|
||||
assert content.generated_at == now
|
||||
assert content.sections == {}
|
||||
assert content.charts == {}
|
||||
assert content.metadata == {}
|
||||
|
||||
|
||||
class TestReportResult:
|
||||
"""Tests for ReportResult dataclass."""
|
||||
|
||||
def test_success_result(self):
|
||||
"""Test successful result."""
|
||||
result = ReportResult(
|
||||
success=True,
|
||||
output_path=Path("/tmp/report.html"),
|
||||
format=ReportFormat.HTML,
|
||||
file_size_bytes=1024,
|
||||
generation_time_ms=500,
|
||||
)
|
||||
assert result.success is True
|
||||
assert result.output_path == Path("/tmp/report.html")
|
||||
assert result.format == ReportFormat.HTML
|
||||
assert result.file_size_bytes == 1024
|
||||
assert result.generation_time_ms == 500
|
||||
assert result.error is None
|
||||
|
||||
def test_failure_result(self):
|
||||
"""Test failure result."""
|
||||
result = ReportResult(
|
||||
success=False,
|
||||
format=ReportFormat.PDF,
|
||||
error="PDF library not available",
|
||||
)
|
||||
assert result.success is False
|
||||
assert result.output_path is None
|
||||
assert result.error == "PDF library not available"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test ReportGenerator
|
||||
# ============================================================================
|
||||
|
||||
class TestReportGenerator:
|
||||
"""Tests for ReportGenerator class."""
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test generator initialization."""
|
||||
generator = ReportGenerator()
|
||||
assert generator.config is not None
|
||||
assert generator.analyzer is not None
|
||||
|
||||
def test_initialization_with_config(self):
|
||||
"""Test initialization with custom config."""
|
||||
config = ReportConfig(title="Custom Title")
|
||||
generator = ReportGenerator(config=config)
|
||||
assert generator.config.title == "Custom Title"
|
||||
|
||||
def test_generate_html(self, sample_result):
|
||||
"""Test HTML generation."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.html"
|
||||
result = generator.generate_html(sample_result, output_path)
|
||||
|
||||
assert result.success is True
|
||||
assert result.output_path == output_path
|
||||
assert result.format == ReportFormat.HTML
|
||||
assert result.file_size_bytes > 0
|
||||
assert output_path.exists()
|
||||
|
||||
def test_generate_html_content(self, sample_result):
|
||||
"""Test HTML content contains expected sections."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.html"
|
||||
generator.generate_html(sample_result, output_path)
|
||||
|
||||
content = output_path.read_text()
|
||||
|
||||
# Check structure
|
||||
assert "<!DOCTYPE html>" in content
|
||||
assert "<html" in content
|
||||
assert "</html>" in content
|
||||
|
||||
# Check sections
|
||||
assert "Summary" in content
|
||||
assert "Trade Statistics" in content
|
||||
assert "Risk Metrics" in content
|
||||
|
||||
def test_generate_json(self, sample_result):
|
||||
"""Test JSON generation."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.json"
|
||||
result = generator.generate_json(sample_result, output_path)
|
||||
|
||||
assert result.success is True
|
||||
assert result.format == ReportFormat.JSON
|
||||
assert output_path.exists()
|
||||
|
||||
# Verify JSON is valid
|
||||
data = json.loads(output_path.read_text())
|
||||
assert "report" in data
|
||||
assert "backtest" in data
|
||||
assert "performance" in data
|
||||
assert "trades" in data
|
||||
|
||||
def test_generate_json_structure(self, sample_result):
|
||||
"""Test JSON structure is correct."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.json"
|
||||
generator.generate_json(sample_result, output_path)
|
||||
|
||||
data = json.loads(output_path.read_text())
|
||||
|
||||
# Check report metadata
|
||||
assert "title" in data["report"]
|
||||
assert "generated_at" in data["report"]
|
||||
|
||||
# Check backtest data
|
||||
assert "initial_capital" in data["backtest"]
|
||||
assert "final_capital" in data["backtest"]
|
||||
|
||||
# Check performance metrics
|
||||
assert "total_return" in data["performance"]
|
||||
assert "sharpe_ratio" in data["performance"]
|
||||
|
||||
def test_generate_markdown(self, sample_result):
|
||||
"""Test Markdown generation."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.md"
|
||||
result = generator.generate_markdown(sample_result, output_path)
|
||||
|
||||
assert result.success is True
|
||||
assert result.format == ReportFormat.MARKDOWN
|
||||
assert output_path.exists()
|
||||
|
||||
def test_generate_markdown_content(self, sample_result):
|
||||
"""Test Markdown content structure."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.md"
|
||||
generator.generate_markdown(sample_result, output_path)
|
||||
|
||||
content = output_path.read_text()
|
||||
|
||||
# Check structure
|
||||
assert "# " in content # Title
|
||||
assert "## Summary" in content
|
||||
assert "## Trade Statistics" in content
|
||||
assert "## Risk Metrics" in content
|
||||
assert "|" in content # Tables
|
||||
|
||||
def test_generate_pdf_fallback(self, sample_result):
|
||||
"""Test PDF generation falls back to HTML if libraries unavailable."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.pdf"
|
||||
result = generator.generate_pdf(sample_result, output_path)
|
||||
|
||||
# Should succeed but might fallback to HTML
|
||||
assert result.success is True
|
||||
# Either PDF worked or fell back to HTML
|
||||
assert result.output_path is not None
|
||||
|
||||
def test_generate_generic(self, sample_result):
|
||||
"""Test generic generate method."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# Test HTML
|
||||
html_path = Path(tmpdir) / "report.html"
|
||||
result = generator.generate(sample_result, html_path, ReportFormat.HTML)
|
||||
assert result.success is True
|
||||
assert result.format == ReportFormat.HTML
|
||||
|
||||
# Test JSON
|
||||
json_path = Path(tmpdir) / "report.json"
|
||||
result = generator.generate(sample_result, json_path, ReportFormat.JSON)
|
||||
assert result.success is True
|
||||
assert result.format == ReportFormat.JSON
|
||||
|
||||
def test_custom_title(self, sample_result):
|
||||
"""Test custom report title."""
|
||||
config = ReportConfig(title="My Custom Report")
|
||||
generator = ReportGenerator(config=config)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.html"
|
||||
generator.generate_html(sample_result, output_path)
|
||||
|
||||
content = output_path.read_text()
|
||||
assert "My Custom Report" in content
|
||||
|
||||
def test_custom_author(self, sample_result):
|
||||
"""Test custom author in report."""
|
||||
config = ReportConfig(author="Test Author")
|
||||
generator = ReportGenerator(config=config)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.html"
|
||||
generator.generate_html(sample_result, output_path)
|
||||
|
||||
content = output_path.read_text()
|
||||
assert "Test Author" in content
|
||||
|
||||
def test_max_trades_shown(self, sample_result):
|
||||
"""Test max trades limit in report."""
|
||||
config = ReportConfig(max_trades_shown=1)
|
||||
generator = ReportGenerator(config=config)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.json"
|
||||
generator.generate_json(sample_result, output_path)
|
||||
|
||||
data = json.loads(output_path.read_text())
|
||||
# Should only show 1 trade
|
||||
assert len(data["trade_list"]) <= 1
|
||||
|
||||
|
||||
class TestHTMLRendering:
|
||||
"""Tests for HTML rendering."""
|
||||
|
||||
def test_summary_metrics(self, sample_result):
|
||||
"""Test summary metrics in HTML."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.html"
|
||||
generator.generate_html(sample_result, output_path)
|
||||
|
||||
content = output_path.read_text()
|
||||
|
||||
# Check metrics are present
|
||||
assert "Total Return" in content
|
||||
assert "Win Rate" in content
|
||||
assert "Sharpe Ratio" in content
|
||||
|
||||
def test_trade_list_rendering(self, sample_result):
|
||||
"""Test trade list in HTML."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.html"
|
||||
generator.generate_html(sample_result, output_path)
|
||||
|
||||
content = output_path.read_text()
|
||||
|
||||
# Check trade table
|
||||
assert "Trade List" in content
|
||||
assert "Symbol" in content
|
||||
assert "AAPL" in content
|
||||
|
||||
def test_css_styling(self, sample_result):
|
||||
"""Test CSS is included."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.html"
|
||||
generator.generate_html(sample_result, output_path)
|
||||
|
||||
content = output_path.read_text()
|
||||
|
||||
# Check CSS is present
|
||||
assert "<style>" in content
|
||||
assert "font-family" in content
|
||||
assert "color" in content
|
||||
|
||||
def test_responsive_design(self, sample_result):
|
||||
"""Test responsive design elements."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.html"
|
||||
generator.generate_html(sample_result, output_path)
|
||||
|
||||
content = output_path.read_text()
|
||||
|
||||
# Check viewport meta
|
||||
assert "viewport" in content
|
||||
# Check grid layout
|
||||
assert "grid" in content
|
||||
|
||||
def test_color_scheme_applied(self, sample_result):
|
||||
"""Test custom color scheme is applied."""
|
||||
config = ReportConfig(
|
||||
color_scheme={
|
||||
"primary": "#ff0000",
|
||||
"secondary": "#00ff00",
|
||||
"success": "#0000ff",
|
||||
"danger": "#ffff00",
|
||||
"warning": "#ff00ff",
|
||||
"background": "#ffffff",
|
||||
"text": "#000000",
|
||||
"border": "#cccccc",
|
||||
}
|
||||
)
|
||||
generator = ReportGenerator(config=config)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.html"
|
||||
generator.generate_html(sample_result, output_path)
|
||||
|
||||
content = output_path.read_text()
|
||||
assert "#ff0000" in content # primary color
|
||||
|
||||
|
||||
class TestSVGCharts:
|
||||
"""Tests for SVG chart generation."""
|
||||
|
||||
def test_equity_curve_chart(self, sample_result):
|
||||
"""Test equity curve SVG chart."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.html"
|
||||
generator.generate_html(sample_result, output_path)
|
||||
|
||||
content = output_path.read_text()
|
||||
|
||||
# Check SVG is present
|
||||
assert "<svg" in content
|
||||
assert "Equity Curve" in content
|
||||
|
||||
def test_drawdown_chart(self, sample_result):
|
||||
"""Test drawdown SVG chart."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.html"
|
||||
generator.generate_html(sample_result, output_path)
|
||||
|
||||
content = output_path.read_text()
|
||||
|
||||
# Check drawdown section
|
||||
assert "Drawdown" in content
|
||||
|
||||
|
||||
class TestMonthlyReturns:
|
||||
"""Tests for monthly returns heatmap."""
|
||||
|
||||
def test_monthly_returns_section(self, sample_result):
|
||||
"""Test monthly returns heatmap is generated."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.html"
|
||||
generator.generate_html(sample_result, output_path)
|
||||
|
||||
content = output_path.read_text()
|
||||
|
||||
# Check monthly returns section
|
||||
assert "Monthly Returns" in content
|
||||
assert "heatmap" in content
|
||||
|
||||
|
||||
class TestErrorHandling:
|
||||
"""Tests for error handling."""
|
||||
|
||||
def test_invalid_output_path(self, sample_result):
|
||||
"""Test handling of invalid output path."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
# Path to non-existent nested directory
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "nested" / "dirs" / "report.html"
|
||||
result = generator.generate_html(sample_result, output_path)
|
||||
|
||||
# Should succeed (creates directories)
|
||||
assert result.success is True
|
||||
|
||||
def test_empty_result(self):
|
||||
"""Test handling of empty result."""
|
||||
empty_result = BacktestResult(
|
||||
initial_capital=Decimal("100000"),
|
||||
final_value=Decimal("100000"),
|
||||
total_return=Decimal("0"),
|
||||
total_trades=0,
|
||||
winning_trades=0,
|
||||
losing_trades=0,
|
||||
trades=[],
|
||||
snapshots=[],
|
||||
)
|
||||
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
output_path = Path(tmpdir) / "report.html"
|
||||
result = generator.generate_html(empty_result, output_path)
|
||||
|
||||
assert result.success is True
|
||||
assert output_path.exists()
|
||||
|
||||
|
||||
class TestFactoryFunction:
|
||||
"""Tests for factory function."""
|
||||
|
||||
def test_create_report_generator(self):
|
||||
"""Test factory function."""
|
||||
generator = create_report_generator(
|
||||
title="Factory Report",
|
||||
author="Factory Author",
|
||||
include_trade_list=True,
|
||||
max_trades=50,
|
||||
)
|
||||
|
||||
assert generator.config.title == "Factory Report"
|
||||
assert generator.config.author == "Factory Author"
|
||||
assert generator.config.include_trade_list is True
|
||||
assert generator.config.max_trades_shown == 50
|
||||
|
||||
def test_create_report_generator_defaults(self):
|
||||
"""Test factory function with defaults."""
|
||||
generator = create_report_generator()
|
||||
|
||||
assert generator.config.title == "Backtest Report"
|
||||
assert generator.config.author == ""
|
||||
|
||||
|
||||
class TestModuleExports:
|
||||
"""Tests for module exports."""
|
||||
|
||||
def test_imports_from_package(self):
|
||||
"""Test imports work from package."""
|
||||
from tradingagents.backtest import (
|
||||
ReportFormat,
|
||||
ReportSection,
|
||||
ChartType,
|
||||
ReportConfig,
|
||||
ChartData,
|
||||
ReportContent,
|
||||
ReportResult,
|
||||
ReportGenerator,
|
||||
create_report_generator,
|
||||
)
|
||||
|
||||
# All imports should work
|
||||
assert ReportFormat is not None
|
||||
assert ReportSection is not None
|
||||
assert ChartType is not None
|
||||
assert ReportConfig is not None
|
||||
assert ChartData is not None
|
||||
assert ReportContent is not None
|
||||
assert ReportResult is not None
|
||||
assert ReportGenerator is not None
|
||||
assert create_report_generator is not None
|
||||
|
||||
|
||||
class TestIntegration:
|
||||
"""Integration tests."""
|
||||
|
||||
def test_full_workflow(self, sample_result):
|
||||
"""Test complete report generation workflow."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# Generate all formats
|
||||
html_result = generator.generate_html(
|
||||
sample_result,
|
||||
Path(tmpdir) / "report.html",
|
||||
)
|
||||
json_result = generator.generate_json(
|
||||
sample_result,
|
||||
Path(tmpdir) / "report.json",
|
||||
)
|
||||
md_result = generator.generate_markdown(
|
||||
sample_result,
|
||||
Path(tmpdir) / "report.md",
|
||||
)
|
||||
|
||||
# All should succeed
|
||||
assert html_result.success is True
|
||||
assert json_result.success is True
|
||||
assert md_result.success is True
|
||||
|
||||
# All files should exist
|
||||
assert (Path(tmpdir) / "report.html").exists()
|
||||
assert (Path(tmpdir) / "report.json").exists()
|
||||
assert (Path(tmpdir) / "report.md").exists()
|
||||
|
||||
def test_with_pre_computed_analysis(self, sample_result):
|
||||
"""Test using pre-computed analysis."""
|
||||
from tradingagents.backtest import ResultsAnalyzer
|
||||
|
||||
analyzer = ResultsAnalyzer()
|
||||
analysis = analyzer.analyze(sample_result)
|
||||
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
result = generator.generate_html(
|
||||
sample_result,
|
||||
Path(tmpdir) / "report.html",
|
||||
analysis=analysis,
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
|
||||
def test_generation_time_tracked(self, sample_result):
|
||||
"""Test that generation time is tracked."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
result = generator.generate_html(
|
||||
sample_result,
|
||||
Path(tmpdir) / "report.html",
|
||||
)
|
||||
|
||||
assert result.generation_time_ms >= 0
|
||||
|
||||
def test_file_size_tracked(self, sample_result):
|
||||
"""Test that file size is tracked."""
|
||||
generator = ReportGenerator()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
result = generator.generate_html(
|
||||
sample_result,
|
||||
Path(tmpdir) / "report.html",
|
||||
)
|
||||
|
||||
assert result.file_size_bytes > 0
|
||||
|
|
@ -127,6 +127,22 @@ from .results_analyzer import (
|
|||
create_results_analyzer,
|
||||
)
|
||||
|
||||
from .report_generator import (
|
||||
# Enums
|
||||
ReportFormat,
|
||||
ReportSection,
|
||||
ChartType,
|
||||
# Data Classes
|
||||
ReportConfig,
|
||||
ChartData,
|
||||
ReportContent,
|
||||
ReportResult,
|
||||
# Main Classes
|
||||
ReportGenerator,
|
||||
# Factory Functions
|
||||
create_report_generator,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Enums
|
||||
"OrderSide",
|
||||
|
|
@ -171,4 +187,17 @@ __all__ = [
|
|||
"BenchmarkComparison",
|
||||
"DrawdownAnalysis",
|
||||
"AnalysisResult",
|
||||
# Report Generator Enums
|
||||
"ReportFormat",
|
||||
"ReportSection",
|
||||
"ChartType",
|
||||
# Report Generator Data Classes
|
||||
"ReportConfig",
|
||||
"ChartData",
|
||||
"ReportContent",
|
||||
"ReportResult",
|
||||
# Report Generator Main Classes
|
||||
"ReportGenerator",
|
||||
# Report Generator Factory Functions
|
||||
"create_report_generator",
|
||||
]
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue