|
|
import os |
|
|
import traceback |
|
|
from reportlab.lib import colors |
|
|
from reportlab.lib import pagesizes |
|
|
from reportlab.platypus import ( |
|
|
SimpleDocTemplate, |
|
|
Frame, |
|
|
Paragraph, |
|
|
Image, |
|
|
PageTemplate, |
|
|
FrameBreak, |
|
|
Spacer, |
|
|
Table, |
|
|
TableStyle, |
|
|
NextPageTemplate, |
|
|
PageBreak, |
|
|
) |
|
|
from reportlab.lib.units import inch |
|
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle |
|
|
from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT |
|
|
|
|
|
from ..data_source import FMPUtils, YFinanceUtils |
|
|
from .analyzer import ReportAnalysisUtils |
|
|
from typing import Annotated |
|
|
|
|
|
|
|
|
class ReportLabUtils: |
|
|
|
|
|
def build_annual_report( |
|
|
ticker_symbol: Annotated[str, "ticker symbol"], |
|
|
save_path: Annotated[str, "path to save the annual report pdf"], |
|
|
income_summarization: Annotated[ |
|
|
str, |
|
|
"a paragraph of text: the company's income summarization from its financial report", |
|
|
], |
|
|
business_highlights: Annotated[ |
|
|
str, |
|
|
"a paragraph of text: the company's business highlights from its financial report", |
|
|
], |
|
|
company_description: Annotated[ |
|
|
str, |
|
|
"a paragraph of text: the company's description and current situation from its financial report", |
|
|
], |
|
|
risk_assessment: Annotated[ |
|
|
str, |
|
|
"a paragraph of text: the company's risk assessment from its financial report", |
|
|
], |
|
|
share_performance_image_path: Annotated[ |
|
|
str, "path to the share performance image" |
|
|
], |
|
|
pe_eps_performance_image_path: Annotated[ |
|
|
str, "path to the PE and EPS performance image" |
|
|
], |
|
|
filing_date: Annotated[str, "filing date of the analyzed financial report"], |
|
|
) -> str: |
|
|
""" |
|
|
Aggregate a company's income summarization, business highlights, company description, |
|
|
risk assessment and share performance, PE & EPS performance charts all into a PDF report. |
|
|
""" |
|
|
try: |
|
|
|
|
|
|
|
|
page_width, page_height = pagesizes.A4 |
|
|
left_column_width = page_width * 2 / 3 |
|
|
right_column_width = page_width - left_column_width |
|
|
margin = 4 |
|
|
|
|
|
|
|
|
pdf_path = ( |
|
|
os.path.join(save_path, f"{ticker_symbol}_report.pdf") |
|
|
if os.path.isdir(save_path) |
|
|
else save_path |
|
|
) |
|
|
os.makedirs(os.path.dirname(pdf_path), exist_ok=True) |
|
|
doc = SimpleDocTemplate(pdf_path, pagesize=pagesizes.A4) |
|
|
|
|
|
|
|
|
frame_left = Frame( |
|
|
margin, |
|
|
margin, |
|
|
left_column_width - margin * 2, |
|
|
page_height - margin * 2, |
|
|
id="left", |
|
|
) |
|
|
frame_right = Frame( |
|
|
left_column_width, |
|
|
margin, |
|
|
right_column_width - margin * 2, |
|
|
page_height - margin * 2, |
|
|
id="right", |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
left_column_width_p2 = (page_width - margin * 3) // 2 |
|
|
right_column_width_p2 = left_column_width_p2 |
|
|
frame_left_p2 = Frame( |
|
|
margin, |
|
|
margin, |
|
|
left_column_width_p2 - margin * 2, |
|
|
page_height - margin * 2, |
|
|
id="left", |
|
|
) |
|
|
frame_right_p2 = Frame( |
|
|
left_column_width_p2, |
|
|
margin, |
|
|
right_column_width_p2 - margin * 2, |
|
|
page_height - margin * 2, |
|
|
id="right", |
|
|
) |
|
|
|
|
|
|
|
|
page_template = PageTemplate( |
|
|
id="TwoColumns", frames=[frame_left, frame_right] |
|
|
) |
|
|
page_template_p2 = PageTemplate( |
|
|
id="TwoColumns_p2", frames=[frame_left_p2, frame_right_p2] |
|
|
) |
|
|
|
|
|
|
|
|
single_frame = Frame( |
|
|
margin, |
|
|
margin, |
|
|
page_width - 2 * margin, |
|
|
page_height - 2 * margin, |
|
|
id="single", |
|
|
) |
|
|
|
|
|
|
|
|
single_column_layout = PageTemplate(id="OneCol", frames=[single_frame]) |
|
|
|
|
|
doc.addPageTemplates( |
|
|
[page_template, single_column_layout, page_template_p2] |
|
|
) |
|
|
|
|
|
styles = getSampleStyleSheet() |
|
|
|
|
|
|
|
|
custom_style = ParagraphStyle( |
|
|
name="Custom", |
|
|
parent=styles["Normal"], |
|
|
fontName="Helvetica", |
|
|
fontSize=10, |
|
|
|
|
|
alignment=TA_JUSTIFY, |
|
|
) |
|
|
|
|
|
title_style = ParagraphStyle( |
|
|
name="TitleCustom", |
|
|
parent=styles["Title"], |
|
|
fontName="Helvetica-Bold", |
|
|
fontSize=16, |
|
|
leading=20, |
|
|
alignment=TA_LEFT, |
|
|
spaceAfter=10, |
|
|
) |
|
|
|
|
|
subtitle_style = ParagraphStyle( |
|
|
name="Subtitle", |
|
|
parent=styles["Heading2"], |
|
|
fontName="Helvetica-Bold", |
|
|
fontSize=14, |
|
|
leading=12, |
|
|
alignment=TA_LEFT, |
|
|
spaceAfter=6, |
|
|
) |
|
|
|
|
|
table_style2 = TableStyle( |
|
|
[ |
|
|
("BACKGROUND", (0, 0), (-1, -1), colors.white), |
|
|
("BACKGROUND", (0, 0), (-1, 0), colors.white), |
|
|
("FONT", (0, 0), (-1, -1), "Helvetica", 7), |
|
|
("FONT", (0, 0), (-1, 0), "Helvetica-Bold", 14), |
|
|
("VALIGN", (0, 0), (-1, -1), "MIDDLE"), |
|
|
|
|
|
("ALIGN", (0, 0), (-1, -1), "LEFT"), |
|
|
|
|
|
("LINEBELOW", (0, 0), (-1, 0), 2, colors.black), |
|
|
|
|
|
("LINEBELOW", (0, -1), (-1, -1), 2, colors.black), |
|
|
] |
|
|
) |
|
|
|
|
|
name = YFinanceUtils.get_stock_info(ticker_symbol)["shortName"] |
|
|
|
|
|
|
|
|
content = [] |
|
|
|
|
|
content.append( |
|
|
Paragraph( |
|
|
f"Equity Research Report: {name}", |
|
|
title_style, |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
content.append(Paragraph("Income Summarization", subtitle_style)) |
|
|
content.append(Paragraph(income_summarization, custom_style)) |
|
|
|
|
|
content.append(Paragraph("Business Highlights", subtitle_style)) |
|
|
content.append(Paragraph(business_highlights, custom_style)) |
|
|
|
|
|
content.append(Paragraph("Company Situation", subtitle_style)) |
|
|
content.append(Paragraph(company_description, custom_style)) |
|
|
|
|
|
content.append(Paragraph("Risk Assessment", subtitle_style)) |
|
|
content.append(Paragraph(risk_assessment, custom_style)) |
|
|
|
|
|
|
|
|
df = FMPUtils.get_financial_metrics(ticker_symbol, years=5) |
|
|
df.reset_index(inplace=True) |
|
|
currency = YFinanceUtils.get_stock_info(ticker_symbol)["currency"] |
|
|
df.rename(columns={"index": f"FY ({currency} mn)"}, inplace=True) |
|
|
table_data = [["Financial Metrics"]] |
|
|
table_data += [df.columns.to_list()] + df.values.tolist() |
|
|
|
|
|
col_widths = [(left_column_width - margin * 4) / df.shape[1]] * df.shape[1] |
|
|
table = Table(table_data, colWidths=col_widths) |
|
|
table.setStyle(table_style2) |
|
|
content.append(table) |
|
|
|
|
|
content.append(FrameBreak()) |
|
|
|
|
|
table_style = TableStyle( |
|
|
[ |
|
|
("BACKGROUND", (0, 0), (-1, -1), colors.white), |
|
|
("BACKGROUND", (0, 0), (-1, 0), colors.white), |
|
|
("FONT", (0, 0), (-1, -1), "Helvetica", 8), |
|
|
("FONT", (0, 0), (-1, 0), "Helvetica-Bold", 12), |
|
|
("VALIGN", (0, 0), (-1, -1), "MIDDLE"), |
|
|
|
|
|
("ALIGN", (0, 1), (0, -1), "LEFT"), |
|
|
|
|
|
("ALIGN", (1, 1), (1, -1), "RIGHT"), |
|
|
|
|
|
("LINEBELOW", (0, 0), (-1, 0), 2, colors.black), |
|
|
] |
|
|
) |
|
|
full_length = right_column_width - 2 * margin |
|
|
|
|
|
data = [ |
|
|
["FinRobot"], |
|
|
["https://ai4finance.org/"], |
|
|
["https://github.com/AI4Finance-Foundation/FinRobot"], |
|
|
[f"Report date: {filing_date}"], |
|
|
] |
|
|
col_widths = [full_length] |
|
|
table = Table(data, colWidths=col_widths) |
|
|
table.setStyle(table_style) |
|
|
content.append(table) |
|
|
|
|
|
|
|
|
content.append(Spacer(1, 0.15 * inch)) |
|
|
key_data = ReportAnalysisUtils.get_key_data(ticker_symbol, filing_date) |
|
|
|
|
|
data = [["Key data", ""]] |
|
|
data += [[k, v] for k, v in key_data.items()] |
|
|
col_widths = [full_length // 3 * 2, full_length // 3] |
|
|
table = Table(data, colWidths=col_widths) |
|
|
table.setStyle(table_style) |
|
|
content.append(table) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
data = [["Share Performance"]] |
|
|
col_widths = [full_length] |
|
|
table = Table(data, colWidths=col_widths) |
|
|
table.setStyle(table_style) |
|
|
content.append(table) |
|
|
|
|
|
plot_path = share_performance_image_path |
|
|
width = right_column_width |
|
|
height = width // 2 |
|
|
content.append(Image(plot_path, width=width, height=height)) |
|
|
|
|
|
|
|
|
data = [["PE & EPS"]] |
|
|
col_widths = [full_length] |
|
|
table = Table(data, colWidths=col_widths) |
|
|
table.setStyle(table_style) |
|
|
content.append(table) |
|
|
|
|
|
plot_path = pe_eps_performance_image_path |
|
|
width = right_column_width |
|
|
height = width // 2 |
|
|
content.append(Image(plot_path, width=width, height=height)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
doc.build(content) |
|
|
|
|
|
return "Annual report generated successfully." |
|
|
|
|
|
except Exception: |
|
|
return traceback.format_exc() |
|
|
|