wbreport 2.2.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of wbreport might be problematic. Click here for more details.

Files changed (66) hide show
  1. wbreport/__init__.py +1 -0
  2. wbreport/admin.py +87 -0
  3. wbreport/apps.py +6 -0
  4. wbreport/defaults/__init__.py +0 -0
  5. wbreport/defaults/factsheets/__init__.py +0 -0
  6. wbreport/defaults/factsheets/base.py +990 -0
  7. wbreport/defaults/factsheets/menu.py +93 -0
  8. wbreport/defaults/factsheets/mixins.py +35 -0
  9. wbreport/defaults/factsheets/multitheme.py +947 -0
  10. wbreport/dynamic_preferences_registry.py +15 -0
  11. wbreport/factories/__init__.py +8 -0
  12. wbreport/factories/data_classes.py +48 -0
  13. wbreport/factories/reports.py +79 -0
  14. wbreport/filters.py +37 -0
  15. wbreport/migrations/0001_initial_squashed_squashed_0007_report_key.py +238 -0
  16. wbreport/migrations/0008_alter_report_file_content_type.py +25 -0
  17. wbreport/migrations/0009_alter_report_color_palette.py +27 -0
  18. wbreport/migrations/0010_auto_20240103_0947.py +43 -0
  19. wbreport/migrations/0011_auto_20240207_1629.py +35 -0
  20. wbreport/migrations/0012_reportversion_lock.py +17 -0
  21. wbreport/migrations/0013_alter_reportversion_context.py +18 -0
  22. wbreport/migrations/0014_alter_reportcategory_options_and_more.py +25 -0
  23. wbreport/migrations/__init__.py +0 -0
  24. wbreport/mixins.py +183 -0
  25. wbreport/models.py +781 -0
  26. wbreport/pdf/__init__.py +0 -0
  27. wbreport/pdf/charts/__init__.py +0 -0
  28. wbreport/pdf/charts/legend.py +15 -0
  29. wbreport/pdf/charts/pie.py +169 -0
  30. wbreport/pdf/charts/timeseries.py +77 -0
  31. wbreport/pdf/sandbox/__init__.py +0 -0
  32. wbreport/pdf/sandbox/run.py +17 -0
  33. wbreport/pdf/sandbox/templates/__init__.py +0 -0
  34. wbreport/pdf/sandbox/templates/basic_factsheet.py +908 -0
  35. wbreport/pdf/sandbox/templates/fund_factsheet.py +864 -0
  36. wbreport/pdf/sandbox/templates/long_industry_exposure_factsheet.py +898 -0
  37. wbreport/pdf/sandbox/templates/multistrat_factsheet.py +872 -0
  38. wbreport/pdf/sandbox/templates/testfile.pdf +434 -0
  39. wbreport/pdf/tables/__init__.py +0 -0
  40. wbreport/pdf/tables/aggregated_tables.py +156 -0
  41. wbreport/pdf/tables/data_tables.py +75 -0
  42. wbreport/serializers.py +191 -0
  43. wbreport/tasks.py +60 -0
  44. wbreport/templates/__init__.py +0 -0
  45. wbreport/templatetags/__init__.py +0 -0
  46. wbreport/templatetags/portfolio_tags.py +35 -0
  47. wbreport/tests/__init__.py +0 -0
  48. wbreport/tests/conftest.py +24 -0
  49. wbreport/tests/test_models.py +253 -0
  50. wbreport/tests/test_tasks.py +17 -0
  51. wbreport/tests/test_viewsets.py +0 -0
  52. wbreport/tests/tests.py +12 -0
  53. wbreport/urls.py +29 -0
  54. wbreport/urls_public.py +10 -0
  55. wbreport/viewsets/__init__.py +10 -0
  56. wbreport/viewsets/configs/__init__.py +18 -0
  57. wbreport/viewsets/configs/buttons.py +193 -0
  58. wbreport/viewsets/configs/displays.py +116 -0
  59. wbreport/viewsets/configs/endpoints.py +23 -0
  60. wbreport/viewsets/configs/menus.py +8 -0
  61. wbreport/viewsets/configs/titles.py +30 -0
  62. wbreport/viewsets/viewsets.py +330 -0
  63. wbreport-2.2.1.dist-info/METADATA +6 -0
  64. wbreport-2.2.1.dist-info/RECORD +66 -0
  65. wbreport-2.2.1.dist-info/WHEEL +5 -0
  66. wbreport-2.2.1.dist-info/licenses/LICENSE +4 -0
@@ -0,0 +1,990 @@
1
+ from datetime import date
2
+ from decimal import Decimal
3
+ from io import BytesIO
4
+
5
+ import pandas as pd
6
+ from django.template.loader import get_template
7
+ from reportlab import platypus
8
+ from reportlab.graphics.charts.barcharts import HorizontalBarChart
9
+ from reportlab.graphics.shapes import Drawing, String
10
+ from reportlab.lib import colors
11
+ from reportlab.lib.colors import HexColor
12
+ from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_RIGHT
13
+ from reportlab.lib.formatters import DecimalFormatter
14
+ from reportlab.lib.pagesizes import A4
15
+ from reportlab.lib.styles import ParagraphStyle
16
+ from reportlab.lib.units import cm
17
+ from reportlab.pdfbase import pdfmetrics
18
+ from reportlab.pdfbase.pdfmetrics import registerFontFamily, stringWidth
19
+ from reportlab.pdfbase.ttfonts import TTFont
20
+ from reportlab.platypus import (
21
+ BaseDocTemplate,
22
+ Flowable,
23
+ NextPageTemplate,
24
+ PageTemplate,
25
+ Paragraph,
26
+ Spacer,
27
+ Table,
28
+ TableStyle,
29
+ )
30
+ from reportlab.platypus.frames import Frame
31
+ from wbcore.utils.figures import (
32
+ get_factsheet_timeseries_chart,
33
+ get_horizontal_barplot,
34
+ get_piechart,
35
+ )
36
+ from wbportfolio.models.products import InvestmentIndex
37
+ from wbreport.mixins import ReportMixin
38
+ from wbreport.models import ReportAsset
39
+ from wbreport.pdf.charts.pie import (
40
+ get_pie_chart_horizontal,
41
+ get_pie_chart_horizontal_height,
42
+ get_pie_chart_vertical,
43
+ get_pie_chart_vertical_height,
44
+ )
45
+ from wbreport.pdf.charts.timeseries import Scale, get_timeseries_chart
46
+ from wbreport.pdf.flowables.textboxes import TextWithIcon
47
+ from wbreport.pdf.tables.aggregated_tables import get_simple_aggregated_table
48
+ from wbreport.pdf.tables.data_tables import get_simple_data_table
49
+
50
+ from .mixins import FactsheetReportMixin
51
+
52
+
53
+ class ReportClass(FactsheetReportMixin, ReportMixin):
54
+ HTML_TEMPLATE_FILE = "report/factsheet_base.html"
55
+
56
+ @classmethod
57
+ def get_context(cls, version):
58
+ content_object = version.report.content_object
59
+ parameters = cls.parse_parameters(version.parameters)
60
+
61
+ asset_portfolio = content_object.asset_portfolio
62
+ base_portfolio = content_object.primary_portfolio
63
+ context = {"product_title": content_object.title}
64
+
65
+ end = content_object.asset_portfolio.get_latest_asset_position_date(parameters["end"])
66
+ start = content_object.asset_portfolio.get_latest_asset_position_date(parameters["start"])
67
+ prices = content_object.get_prices_df(from_date=end)
68
+ if start and end and not prices.empty:
69
+ context["date"] = end
70
+
71
+ if content_object.group:
72
+ context["funds_table"] = content_object.group.get_fund_product_table(end)
73
+
74
+ context["currency"] = content_object.currency.symbol
75
+ context["external_webpage"] = content_object.external_webpage
76
+ context["monthly_returns"] = content_object.get_monthly_return_summary_dict(end=end)
77
+
78
+ context["introduction"] = content_object.description
79
+ context["all_time"] = content_object.get_price_range(val_date=end)
80
+ context["risk_scale"] = content_object.risk_scale
81
+ context["risk_scale_loop"] = [True] * content_object.risk_scale + [False] * (7 - content_object.risk_scale)
82
+ context["holdings"] = asset_portfolio.get_holding(end).values(
83
+ "underlying_instrument__name_repr", "weighting"
84
+ )
85
+ contributors = (
86
+ asset_portfolio.get_portfolio_contribution_df(start, end, with_cash=False)
87
+ .sort_values(by="contribution_total", ascending=False)
88
+ .underlying_instrument__name_repr
89
+ )
90
+
91
+ if contributors is not None and not contributors.empty:
92
+ contributors_list = contributors.values.tolist()
93
+ context["top_contributors"] = contributors_list[:3]
94
+ contributors_list.reverse()
95
+ context["worst_contributors"] = contributors_list[:3]
96
+
97
+ context["prices"] = prices
98
+ context["geographical_breakdown"] = asset_portfolio.get_geographical_breakdown(end)
99
+ context["currency_exposure"] = asset_portfolio.get_currency_exposure(end)
100
+ context["asset_allocation"] = asset_portfolio.get_asset_allocation(end)
101
+ context["market_cap_distribution"] = asset_portfolio.get_equity_market_cap_distribution(end)
102
+ context["equity_liquidity"] = asset_portfolio.get_equity_liquidity(end)
103
+ context["industry_exposure"] = asset_portfolio.get_industry_exposure(end).sort_values(
104
+ by=["weighting"], ascending=False
105
+ )
106
+ if content_object.investment_index == InvestmentIndex.LONG_SHORT.name:
107
+ context["longshort"] = base_portfolio.get_longshort_distribution(end)
108
+ else:
109
+ raise ValueError("Context cannot be generated")
110
+ return context
111
+
112
+ @classmethod
113
+ def generate_html(cls, context):
114
+ if "funds_table" in context:
115
+ context["funds_table"] = context["funds_table"].to_html(
116
+ border=0,
117
+ index=False,
118
+ classes=["table", "table-funds", "table-colored"],
119
+ index_names=False,
120
+ float_format="%.1f%%",
121
+ na_rep="",
122
+ )
123
+
124
+ context["prices"] = get_factsheet_timeseries_chart(
125
+ context["prices"], color=context["report_base_color"]
126
+ ).to_html(full_html=False, include_plotlyjs=False)
127
+
128
+ nb_labels_first_row = max(
129
+ [
130
+ context["geographical_breakdown"].shape[0],
131
+ context["currency_exposure"].shape[0],
132
+ context["asset_allocation"].shape[0],
133
+ ]
134
+ )
135
+ height_piechart = 300
136
+ context["geographical_breakdown"] = get_piechart(
137
+ context["geographical_breakdown"],
138
+ colors_map=context["colors_palette"],
139
+ height=height_piechart,
140
+ max_number_label=nb_labels_first_row,
141
+ ).to_html(full_html=False, include_plotlyjs=False)
142
+ context["currency_exposure"] = get_piechart(
143
+ context["currency_exposure"],
144
+ colors_map=context["colors_palette"],
145
+ height=height_piechart,
146
+ max_number_label=nb_labels_first_row,
147
+ ).to_html(full_html=False, include_plotlyjs=False)
148
+ context["asset_allocation"] = get_piechart(
149
+ context["asset_allocation"],
150
+ colors_map=context["colors_palette"],
151
+ height=height_piechart,
152
+ max_number_label=nb_labels_first_row,
153
+ ).to_html(full_html=False, include_plotlyjs=False)
154
+
155
+ # SECOND WWIDGET ROW
156
+ context["market_cap_distribution"] = get_piechart(
157
+ context["market_cap_distribution"], height=height_piechart, colors_map=context["colors_palette"]
158
+ ).to_html(full_html=False, include_plotlyjs=False)
159
+ context["equity_liquidity"] = get_piechart(
160
+ context["equity_liquidity"], height=height_piechart, colors_map=context["colors_palette"]
161
+ ).to_html(full_html=False, include_plotlyjs=False)
162
+
163
+ context["industry_exposure"] = get_horizontal_barplot(
164
+ context["industry_exposure"], colors=context["colors_palette"][0]
165
+ ).to_html(full_html=False, include_plotlyjs=False)
166
+ if "strategy_allocation" in context:
167
+ index_allocation_df = context["strategy_allocation"]
168
+ if not index_allocation_df.empty:
169
+ context["strategy_allocation"] = get_piechart(
170
+ index_allocation_df,
171
+ x_label="allocation_end",
172
+ height=height_piechart,
173
+ y_label="underlying_instrument__name_repr",
174
+ hoverinfo="text",
175
+ colors_label="color",
176
+ ).to_html(full_html=False, include_plotlyjs=False)
177
+ renamed_df = index_allocation_df.rename(
178
+ columns={"performance_total": "Monthly Performance", "contribution_total": "Monthly Contribution"}
179
+ )
180
+ context["stragegy_contribution"] = get_horizontal_barplot(
181
+ renamed_df,
182
+ colors_label="color",
183
+ x_label=["Monthly Performance", "Monthly Contribution"],
184
+ y_label="underlying_instrument__name_repr",
185
+ ).to_html(full_html=False, include_plotlyjs=False)
186
+
187
+ if not context.get("longshort", pd.DataFrame()).empty:
188
+ context["longshort"] = get_piechart(
189
+ context["longshort"],
190
+ colors_map=context["colors_palette"],
191
+ hoverinfo="label+text",
192
+ default_normalize=False,
193
+ height=height_piechart,
194
+ y_label="title",
195
+ ).to_html(full_html=False, include_plotlyjs=False)
196
+
197
+ template = get_template(cls.HTML_TEMPLATE_FILE)
198
+ return template.render(context)
199
+
200
+ @classmethod
201
+ def generate_file(cls, context):
202
+ debug = False
203
+ # Product Data
204
+ # Main Feature table as dictionary
205
+ main_features_dict = context["information_table"].get(
206
+ "Main Features", context["information_table"].get("Share Class Information", {})
207
+ )
208
+ # Monthly returns table as dataframe, None is no value
209
+ monthly_returns = context["monthly_returns"]
210
+
211
+ # Pie chart Dataframe
212
+ geographical = context["geographical_breakdown"]
213
+ currencies = context["currency_exposure"]
214
+ allocation = context["asset_allocation"]
215
+ marketcap = context["market_cap_distribution"]
216
+ liquidity = context["equity_liquidity"]
217
+ industry = context["industry_exposure"]
218
+
219
+ # Price time serie as dataframe
220
+ prices = context["prices"]
221
+
222
+ # TOPs as list
223
+ top_3_holdings = list(context["holdings"].values_list("underlying_instrument__name_repr", flat=True))[0:3]
224
+ top_3_holdings = [t.upper() for t in top_3_holdings]
225
+
226
+ top_3_contributors = [c.upper() for c in context["top_contributors"]]
227
+ bottom_3_contributors = [c.upper() for c in context["worst_contributors"]]
228
+
229
+ # {'high': {'price':low, 'date':date}, 'low' : {'price':high, 'date':date}}
230
+ all_time_dict = context["all_time"]
231
+ end = context["date"]
232
+ general_data = {}
233
+ general_data["date"] = end.strftime("%b %Y")
234
+ general_data["colors"] = context["colors_palette"]
235
+ general_data["title"] = context["product_title"].replace("&", "&")
236
+ general_data["risk_scale"] = context["risk_scale"]
237
+
238
+ general_data["introduction"] = context["introduction"]
239
+
240
+ pdfmetrics.registerFont(TTFont("customfont", ReportAsset.objects.get(key="font-default").asset))
241
+ pdfmetrics.registerFont(TTFont("customfont-bd", ReportAsset.objects.get(key="font-bd").asset))
242
+ pdfmetrics.registerFont(TTFont("customfont-it", ReportAsset.objects.get(key="font-it").asset))
243
+ registerFontFamily("customfont", normal="customfont", bold="customfont-bd", italic="customfont-it")
244
+
245
+ # Page Variables
246
+ LINEHEIGHT = 12
247
+
248
+ HEADER_HEIGHT = 2.34 * cm
249
+ FOOTER_HEIGHT = 2.34 * cm
250
+
251
+ SIDE_MARGIN = 0.96 * cm
252
+ TOP_MARGIN = 3.63 * cm
253
+ CONTENT_OFFSET = 0.34 * cm
254
+ CONTENT_MARGIN = SIDE_MARGIN + CONTENT_OFFSET
255
+
256
+ CONTENT_HEIGHT = 22.25 * cm
257
+ CONTENT_WIDTH_PAGE1_LEFT = 13.84 * cm
258
+ CONTENT_WIDTH_PAGE1_RIGHT = 4.23 * cm
259
+ CONTENT_WIDTH_PAGE2 = A4[0] - SIDE_MARGIN - CONTENT_MARGIN
260
+
261
+ CONTENT_X_PAGE1_RIGHT = A4[0] - CONTENT_MARGIN - CONTENT_WIDTH_PAGE1_RIGHT
262
+ CONTENT_Y = A4[1] - CONTENT_HEIGHT - TOP_MARGIN
263
+
264
+ output = BytesIO()
265
+ doc = BaseDocTemplate(
266
+ output,
267
+ pagesize=A4,
268
+ rightMargin=0,
269
+ leftMargin=0,
270
+ topMargin=0,
271
+ bottomMargin=0,
272
+ title=general_data["title"],
273
+ )
274
+ elements = []
275
+
276
+ s_base = ParagraphStyle(name="s_base", fontName="customfont", fontSize=9, leading=10)
277
+ s_base_small_justified = ParagraphStyle(
278
+ name="s_base_small_justified", parent=s_base, fontSize=6.5, leading=7, alignment=TA_JUSTIFY
279
+ )
280
+ # s_base_small_justified_indent = ParagraphStyle(
281
+ # name="s_base_small_justified_indent", parent=s_base_small_justified, leftIndent=CONTENT_OFFSET
282
+ # )
283
+ s_base_indent = ParagraphStyle(name="s_description", parent=s_base, spaceBefore=8, leftIndent=CONTENT_OFFSET)
284
+ s_table_base = ParagraphStyle(
285
+ name="s_table_base",
286
+ fontName="customfont",
287
+ fontSize=6,
288
+ leading=6,
289
+ )
290
+ s_table_medium = ParagraphStyle(name="s_table_medium", parent=s_table_base, fontSize=9, leading=8)
291
+ s_table_medium_leading = ParagraphStyle(name="s_table_medium_leading", parent=s_table_medium, leading=13.9)
292
+ # s_table_large = ParagraphStyle(name="s_table_large", parent=s_table_medium, fontSize=11, leading=11)
293
+ # s_table_large_center = ParagraphStyle(name="s_table_large", parent=s_table_large, alignment=TA_CENTER)
294
+ # s_table_large_center_padding = ParagraphStyle(
295
+ # name="s_table_large", parent=s_table_large_center, spaceBefore=20, spaceAfter=20
296
+ # )
297
+ # s_table_center = ParagraphStyle(
298
+ # name="s_table_center",
299
+ # parent=s_table_base,
300
+ # alignment=TA_CENTER,
301
+ # )
302
+ # s_table_center_high = ParagraphStyle(name="s_table_center", parent=s_table_center, leading=9, fontSize=8)
303
+ s_table_right = ParagraphStyle(name="s_table_right", parent=s_table_base, alignment=TA_RIGHT)
304
+ s_table_center = ParagraphStyle(name="s_table_center", parent=s_table_base, alignment=TA_CENTER)
305
+ s_table_headline = ParagraphStyle(
306
+ name="s_table_headline",
307
+ parent=s_table_base,
308
+ fontSize=16,
309
+ leading=16,
310
+ )
311
+ # s_table_headline_2 = ParagraphStyle(
312
+ # name="s_table_headline_2",
313
+ # parent=s_table_base,
314
+ # fontSize=10,
315
+ # leading=10,
316
+ # )
317
+
318
+ # Setup Colors
319
+ c_product = HexColor(general_data["colors"][0])
320
+ c_product_alpha = HexColor(f"{general_data['colors'][0]}20", hasAlpha=True)
321
+
322
+ c_table_border = HexColor(0x9EA3AC)
323
+ c_grid_color = HexColor(0xB6BAC1)
324
+ c_box_color = HexColor(0x3C4859)
325
+ c_table_background = colors.HexColor(0xE2E3E6)
326
+
327
+ # Frame and Page Layout
328
+ frame_defaults = {
329
+ "showBoundary": debug,
330
+ "leftPadding": 0,
331
+ "rightPadding": 0,
332
+ "topPadding": 0,
333
+ "bottomPadding": 0,
334
+ }
335
+
336
+ text_frame = Frame(
337
+ x1=SIDE_MARGIN,
338
+ y1=CONTENT_Y,
339
+ width=CONTENT_WIDTH_PAGE1_LEFT,
340
+ height=CONTENT_HEIGHT,
341
+ id="text_frame",
342
+ **frame_defaults,
343
+ )
344
+
345
+ main_features_frame = Frame(
346
+ x1=CONTENT_X_PAGE1_RIGHT,
347
+ y1=CONTENT_Y,
348
+ width=CONTENT_WIDTH_PAGE1_RIGHT,
349
+ height=CONTENT_HEIGHT,
350
+ id="main_features_frame",
351
+ **frame_defaults,
352
+ )
353
+
354
+ second_page = Frame(
355
+ x1=SIDE_MARGIN,
356
+ y1=CONTENT_Y,
357
+ width=CONTENT_WIDTH_PAGE2,
358
+ height=CONTENT_HEIGHT,
359
+ id="second_page",
360
+ **frame_defaults,
361
+ )
362
+
363
+ def on_page(canv, dock):
364
+ canv.saveState()
365
+
366
+ # Header
367
+ canv.setFillColor(c_box_color)
368
+ canv.rect(0, A4[1] - HEADER_HEIGHT, A4[0], HEADER_HEIGHT, fill=True, stroke=False)
369
+ colors = [
370
+ HexColor(0xFFB166),
371
+ HexColor(0x8CD66B),
372
+ HexColor(0x05D6A1),
373
+ HexColor(0x01ABAA),
374
+ HexColor(0x70D6FF),
375
+ HexColor(0x0585FF),
376
+ HexColor(0x5724D9),
377
+ HexColor(0xA359E5),
378
+ HexColor(0xEF476F),
379
+ ]
380
+ height = 0.13 * cm
381
+ width = A4[0] / len(colors)
382
+ for index, color in enumerate(colors):
383
+ canv.setFillColor(color)
384
+ canv.rect(0 + index * width, A4[1] - HEADER_HEIGHT - height, width, height, fill=True, stroke=False)
385
+
386
+ # Footer
387
+ canv.setFillColor(c_box_color)
388
+ canv.rect(0, 0, A4[0], FOOTER_HEIGHT, fill=True, stroke=False)
389
+
390
+ for index, color in enumerate(colors):
391
+ canv.setFillColor(color)
392
+ canv.rect(0 + index * width, FOOTER_HEIGHT, width, height, fill=True, stroke=False)
393
+
394
+ canv.restoreState()
395
+
396
+ doc.addPageTemplates(
397
+ [
398
+ PageTemplate(id="page", onPage=on_page, frames=[text_frame, main_features_frame]),
399
+ PageTemplate(id="second_page", onPage=on_page, frames=[second_page]),
400
+ ]
401
+ )
402
+
403
+ elements.append(NextPageTemplate(["second_page"]))
404
+
405
+ def generate_title(title):
406
+ table_data = [[Paragraph("", style=s_table_headline), Paragraph(title, style=s_table_headline)]]
407
+ title_table = Table(table_data, colWidths=[0.14 * cm, None], rowHeights=[0.41 * cm])
408
+ title_table.setStyle(
409
+ TableStyle(
410
+ [
411
+ ("BACKGROUND", (0, 0), (0, 0), c_product),
412
+ ("LEFTPADDING", (0, 0), (-1, -1), 0),
413
+ ("LEFTPADDING", (1, 0), (-1, -1), 0.2 * cm),
414
+ ("RIGHTPADDING", (0, 0), (-1, -1), 0),
415
+ ("TOPPADDING", (0, 0), (-1, -1), 0),
416
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
417
+ ]
418
+ )
419
+ )
420
+ return title_table
421
+
422
+ # Description
423
+ elements.append(generate_title(general_data["title"]))
424
+ elements.append(Paragraph(general_data["introduction"], style=s_base_indent))
425
+ elements.append(Spacer(height=LINEHEIGHT * 2, width=0))
426
+
427
+ # Monthly Returns
428
+ elements.append(generate_title("Monthly Returns (%)"))
429
+ elements.append(Spacer(height=LINEHEIGHT * 1, width=0))
430
+ elements.append(
431
+ get_simple_aggregated_table(
432
+ df=monthly_returns,
433
+ width=CONTENT_WIDTH_PAGE1_LEFT,
434
+ row_height=0.389 * cm,
435
+ header_style=s_table_center,
436
+ row_style=s_table_center,
437
+ data_style=s_table_right,
438
+ grid_color=c_table_border,
439
+ offset=CONTENT_OFFSET,
440
+ debug=debug,
441
+ )
442
+ )
443
+ elements.append(Spacer(height=LINEHEIGHT * 2, width=0))
444
+
445
+ # Price Timeseries Chart
446
+ elements.append(
447
+ get_timeseries_chart(
448
+ data=[list(zip(prices.index, prices.net_value))],
449
+ width=CONTENT_WIDTH_PAGE1_LEFT - CONTENT_OFFSET,
450
+ height=4.34 * cm,
451
+ color=c_product,
452
+ fill_color=c_product_alpha,
453
+ grid_color=c_grid_color,
454
+ scale=Scale.LOGARITHMIC.value,
455
+ debug=debug,
456
+ x=CONTENT_OFFSET,
457
+ )
458
+ )
459
+ elements.append(Spacer(height=LINEHEIGHT * 2, width=0))
460
+
461
+ # Top 3
462
+ elements.append(
463
+ get_simple_data_table(
464
+ headers=[
465
+ "<strong>Top 3 Holdings</strong>",
466
+ "<strong>Top 3 Contributors</strong>",
467
+ "<strong>Bottom 3 Contributors</strong>",
468
+ ],
469
+ data=list(zip(top_3_holdings, top_3_contributors, bottom_3_contributors)),
470
+ width=CONTENT_WIDTH_PAGE1_LEFT,
471
+ header_row_height=0.85 * cm,
472
+ data_row_height=0.39 * cm,
473
+ margin=0.16 * cm,
474
+ header_style=s_table_medium,
475
+ data_style=s_table_base,
476
+ grid_color=c_table_border,
477
+ offset=CONTENT_OFFSET,
478
+ debug=debug,
479
+ )
480
+ )
481
+
482
+ elements.append(platypus.FrameBreak("main_features_frame"))
483
+
484
+ # Main Features Table
485
+
486
+ MAIN_FEATURES_COLOR_BAR_HEIGHT = 0.23 * cm
487
+ MAIN_FEATURES_GAP_HEIGHT = 0.17 * cm
488
+ MAIN_FEATURES_TITLE_HEIGHT = 1.94 * cm
489
+ MAIN_FEATURES_TITLE1_HEIGHT = 0.74 * cm
490
+
491
+ table_data = [
492
+ [Spacer(width=0, height=0)],
493
+ [
494
+ TextWithIcon(
495
+ width=CONTENT_WIDTH_PAGE1_RIGHT,
496
+ height=MAIN_FEATURES_TITLE_HEIGHT,
497
+ text=general_data["date"],
498
+ font="customfont-bd",
499
+ font_size=11,
500
+ icon=context["logo_file"] if "logo_file" in context else None,
501
+ )
502
+ ],
503
+ [Paragraph("<strong>MAIN FEATURES</strong>", style=s_table_center)],
504
+ [Spacer(width=0, height=0)],
505
+ ]
506
+
507
+ for label, value in main_features_dict.items():
508
+ if isinstance(value, date):
509
+ value = value.strftime("%d-%b-%y")
510
+ elif isinstance(value, (Decimal, float)):
511
+ value = "%.2f" % value
512
+ elif isinstance(value, int):
513
+ value = str(value)
514
+ elif value is None:
515
+ value = ""
516
+
517
+ if label.lower() in ["currency", "last price"]:
518
+ table_data.append(
519
+ [
520
+ Paragraph(f"<strong>{label.upper()}</strong>", style=s_table_base),
521
+ Paragraph(f"<strong>{value.upper()}</strong>", style=s_table_base),
522
+ ]
523
+ )
524
+ else:
525
+ table_data.append(
526
+ [
527
+ Paragraph(label.upper(), style=s_table_base),
528
+ Paragraph(value.upper(), style=s_table_base),
529
+ ]
530
+ )
531
+
532
+ table_data.extend([[Spacer(width=0, height=0)], [Spacer(width=0, height=0)]])
533
+
534
+ row_heights = [
535
+ MAIN_FEATURES_COLOR_BAR_HEIGHT,
536
+ MAIN_FEATURES_TITLE_HEIGHT,
537
+ MAIN_FEATURES_TITLE1_HEIGHT,
538
+ MAIN_FEATURES_GAP_HEIGHT,
539
+ ]
540
+ row_heights.extend([None] * (len(table_data) - 6))
541
+ row_heights.extend([MAIN_FEATURES_GAP_HEIGHT, MAIN_FEATURES_COLOR_BAR_HEIGHT])
542
+
543
+ t = Table(table_data, rowHeights=row_heights, colWidths=[CONTENT_WIDTH_PAGE1_RIGHT / 2] * 2)
544
+ table_styles = [
545
+ ("BACKGROUND", (0, 0), (1, 0), c_product), # Top Color Bar
546
+ ("BACKGROUND", (0, -1), (1, -1), c_product), # Bottom Color Bar
547
+ ("BACKGROUND", (0, 2), (-1, 2), c_table_background), # Title Background
548
+ ("LINEABOVE", (0, 2), (-1, 2), 0.25, colors.HexColor("#6d7683")), # Title Line Above
549
+ ("LINEBELOW", (0, 2), (-1, 2), 0.25, colors.HexColor("#6d7683")), # Title Line Below
550
+ ("LINEBEFORE", (1, 3), (1, -2), 0.25, colors.HexColor("#6d7683")), # Data Vertical Seperator
551
+ ("SPAN", (0, 1), (-1, 1)), # Col Span Title
552
+ ("SPAN", (0, 2), (-1, 2)), # Col Span Title1
553
+ ("LEFTPADDING", (0, 1), (0, -1), 0), # Leftpadding Data Labels
554
+ ("LEFTPADDING", (1, 1), (1, -1), 0.28 * cm), # Leftpadding Data Values
555
+ ("VALIGN", (0, 1), (-1, -1), "MIDDLE"),
556
+ ]
557
+
558
+ if debug:
559
+ table_styles.extend(
560
+ [
561
+ ("INNERGRID", (0, 0), (-1, -1), 0.25, colors.black),
562
+ ("BOX", (0, 0), (-1, -1), 0.25, colors.black),
563
+ ]
564
+ )
565
+
566
+ t.setStyle(TableStyle(table_styles))
567
+
568
+ elements.append(t)
569
+ elements.append(Spacer(height=LINEHEIGHT * 1, width=0))
570
+
571
+ # All time table
572
+ table_data = [
573
+ [
574
+ Paragraph("<strong>ALL TIME HIGH</strong>", style=s_table_center),
575
+ Paragraph("<strong>ALL TIME LOW</strong>", style=s_table_center),
576
+ ],
577
+ [
578
+ Paragraph("", style=s_table_center),
579
+ Paragraph("", style=s_table_center),
580
+ ],
581
+ ]
582
+
583
+ table_data.append(
584
+ [
585
+ Paragraph(f'PRICE: {all_time_dict["high"]["price"]:.1f}', style=s_table_center),
586
+ Paragraph(f'PRICE: {all_time_dict["low"]["price"]:.1f}', style=s_table_center),
587
+ ],
588
+ )
589
+ table_data.append(
590
+ [
591
+ Paragraph(f'DATE: {all_time_dict["high"]["date"]:%d-%b-%y}', style=s_table_center),
592
+ Paragraph(f'DATE: {all_time_dict["low"]["date"]:%d-%b-%y}', style=s_table_center),
593
+ ],
594
+ )
595
+ table_data.append(
596
+ [
597
+ Paragraph("", style=s_table_center),
598
+ Paragraph("", style=s_table_center),
599
+ ]
600
+ )
601
+
602
+ row_heights = [0.74 * cm, 0.17 * cm, None, None, 0.17 * cm]
603
+
604
+ t = Table(table_data, rowHeights=row_heights)
605
+ t.setStyle(
606
+ TableStyle(
607
+ [
608
+ ("BACKGROUND", (0, 0), (1, 0), c_table_background),
609
+ ("LINEABOVE", (0, 0), (1, 0), 0.25, colors.HexColor("#6d7683")),
610
+ ("LINEBELOW", (0, 0), (1, 0), 0.25, colors.HexColor("#6d7683")),
611
+ ("LINEBELOW", (0, -1), (1, -1), 0.25, colors.HexColor("#6d7683")),
612
+ ("LINEBEFORE", (1, 0), (1, -1), 0.25, colors.HexColor("#6d7683")),
613
+ ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
614
+ ("LEFTPADDING", (0, 0), (-1, -1), 0),
615
+ ("RIGHTPADDING", (0, 0), (-1, -1), 0),
616
+ ]
617
+ )
618
+ )
619
+
620
+ elements.append(t)
621
+
622
+ # Second Page
623
+ elements.append(platypus.PageBreak("second_page"))
624
+ elements.append(generate_title(general_data["title"]))
625
+ elements.append(Spacer(height=LINEHEIGHT * 1, width=0))
626
+
627
+ def get_bar_chart_height(df, bar_width=0.12 * cm, bar_padding=0.3 * cm, max_label_width=3.35 * cm):
628
+ number_of_items = len(df)
629
+ return number_of_items * (bar_width + 2 * bar_padding)
630
+
631
+ def get_bar_chart(df, width, height, bar_width=0.12 * cm, bar_padding=0.3 * cm, max_label_width=3.35 * cm):
632
+ font_size = 6
633
+ drawing = Drawing(width, height)
634
+ bar_chart = HorizontalBarChart()
635
+
636
+ data = list()
637
+ categories = list()
638
+ for index, row in enumerate(df.itertuples()):
639
+ data.append(row.weighting * 100)
640
+ categories.append(f"{row.aggregated_title} ({row.weighting * 100:.1f}%)")
641
+
642
+ max_label = 0
643
+ _categories = list()
644
+ for category in categories:
645
+ _w = stringWidth(category, "customfont", font_size)
646
+ if _w > max_label_width:
647
+ split_list = category.split(" ")
648
+ splitter = int(len(split_list) * 2 / 3)
649
+
650
+ part_1 = " ".join(split_list[:splitter])
651
+ part_2 = " ".join(split_list[splitter:])
652
+ category = part_1 + "\n" + part_2
653
+ _w1 = stringWidth(part_1, "customfont", font_size)
654
+ _w2 = stringWidth(part_2, "customfont", font_size)
655
+ _w = max(_w1, _w2)
656
+ _categories.append(category)
657
+ max_label = max(max_label, _w - bar_chart.categoryAxis.labels.dx)
658
+
659
+ bar_chart.width = width - max_label
660
+
661
+ bar_chart.height = height
662
+ bar_chart.x = width - bar_chart.width
663
+ bar_chart.y = 0
664
+
665
+ bar_chart.data = [list(reversed(data))]
666
+ bar_chart.categoryAxis.categoryNames = list(reversed(_categories))
667
+ bar_chart.categoryAxis.labels.boxAnchor = "e"
668
+ bar_chart.categoryAxis.labels.textAnchor = "end"
669
+ bar_chart.categoryAxis.labels.fontName = "customfont"
670
+ bar_chart.categoryAxis.labels.fontSize = font_size
671
+ bar_chart.categoryAxis.labels.leading = font_size
672
+
673
+ bar_chart.barWidth = bar_width
674
+ bar_chart.bars.strokeColor = colors.transparent
675
+ bar_chart.bars[0].fillColor = c_product
676
+
677
+ # x-Axis
678
+ bar_chart.valueAxis.labelTextFormat = DecimalFormatter(0, suffix="%")
679
+ bar_chart.valueAxis.labels.fontName = "customfont"
680
+ bar_chart.valueAxis.labels.fontSize = 6
681
+
682
+ bar_chart.valueAxis.strokeWidth = -1
683
+ bar_chart.valueAxis.gridStrokeColor = c_grid_color
684
+ bar_chart.valueAxis.gridStrokeDashArray = (0.2, 0, 0.2)
685
+
686
+ bar_chart.valueAxis.visibleGrid = True
687
+ bar_chart.valueAxis.forceZero = True
688
+
689
+ bar_chart.categoryAxis.strokeWidth = 0.5
690
+ bar_chart.categoryAxis.strokeColor = HexColor(0x6D7683)
691
+ bar_chart.categoryAxis.tickLeft = 0
692
+ bar_chart.categoryAxis.labels.fontName = "customfont"
693
+ bar_chart.categoryAxis.labels.fontSize = 6
694
+ drawing.add(bar_chart)
695
+ drawing.add(String(0, -25, "", fontName="customfont", fontSize=6, fillColor=colors.black))
696
+ return drawing
697
+
698
+ class RiskScaleFlowable(Flowable):
699
+ def __init__(self, height, risk, text=None):
700
+ super().__init__()
701
+ self.risk = risk
702
+ self.height = height
703
+ self.risk_text = (
704
+ text
705
+ or "The actual risk can vary significantly if you cash in at an early stage and you may get back less. You may not be able to sell your product easily or you may have to sell at a price that significantly impacts on how much you get back."
706
+ )
707
+
708
+ def draw(self):
709
+ width = 0.4 * cm
710
+ gap = 1.177 * cm
711
+ # y = (2.15 + 1) * cm
712
+ y = self.height - 0.868 * cm
713
+ x_offset = 0.883 * cm
714
+ self.canv.setFillColor(HexColor(0x9EA3AC))
715
+ for x in range(7):
716
+ _x = x * gap + x_offset
717
+ if x == self.risk - 1:
718
+ self.canv.setFillColor(HexColor(0x3C4859))
719
+ self.canv.circle(_x, y, width, fill=True, stroke=False)
720
+ self.canv.setFillColor(HexColor(0x9EA3AC))
721
+ else:
722
+ self.canv.circle(_x, y, width, fill=True, stroke=False)
723
+
724
+ self.canv.setFillColor(colors.white)
725
+ self.canv.setFont("customfont-bd", 11)
726
+ self.canv.drawCentredString(_x, y - 4, str(x + 1))
727
+ self.canv.setFillColor(HexColor(0x9EA3AC))
728
+
729
+ self.canv.setFillColor(HexColor(0x6D7683))
730
+ self.canv.setStrokeColor(HexColor(0x6D7683))
731
+
732
+ arrow_offset = 0.2 * cm
733
+
734
+ p = self.canv.beginPath()
735
+ origin = (x_offset - arrow_offset, y - 0.868 * cm)
736
+ p.moveTo(*origin)
737
+ p.lineTo(origin[0] + 0.059 * cm, origin[1] + 0.08 * cm)
738
+ p.lineTo(origin[0] - 0.165 * cm, origin[1])
739
+ p.lineTo(origin[0] + 0.059 * cm, origin[1] - 0.08 * cm)
740
+ self.canv.drawPath(p, fill=True, stroke=False)
741
+
742
+ p = self.canv.beginPath()
743
+ origin = (6 * gap + x_offset + arrow_offset, y - 0.868 * cm)
744
+ p.moveTo(origin[0], origin[1])
745
+ p.lineTo(origin[0] - 0.059 * cm, origin[1] + 0.08 * cm)
746
+ p.lineTo(origin[0] + 0.165 * cm, origin[1])
747
+ p.lineTo(origin[0] - 0.059 * cm, origin[1] - 0.08 * cm)
748
+ self.canv.drawPath(p, fill=True, stroke=False)
749
+
750
+ self.canv.setLineWidth(0.02 * cm)
751
+ p = self.canv.beginPath()
752
+ self.canv.line(
753
+ x_offset - arrow_offset, y - 0.868 * cm, 6 * gap + x_offset + arrow_offset, y - 0.868 * cm
754
+ )
755
+
756
+ self.canv.setFont("customfont", 6)
757
+ self.canv.setFillColor(colors.black)
758
+ self.canv.drawString(x_offset - arrow_offset - 0.165 * cm, y - 1.2 * cm, "LOWER RISK")
759
+
760
+ text_width = stringWidth("HIGHER RISK", "customfont", 6)
761
+ self.canv.drawString(
762
+ 6 * gap + x_offset + arrow_offset + 0.165 * cm - text_width, y - 1.2 * cm, "HIGHER RISK"
763
+ )
764
+
765
+ para = Paragraph(self.risk_text, style=s_base_small_justified)
766
+ para.wrapOn(self.canv, 250, 8.954 * cm)
767
+ para.drawOn(self.canv, 0, y - 2.5 * cm)
768
+
769
+ TABLE_MARGIN = 0.56 * cm
770
+ NUM_CHARTS_FIRST_ROW = 4
771
+ WIDTH_CHARTS_FIRST_ROW = (
772
+ (CONTENT_WIDTH_PAGE2 - CONTENT_OFFSET) - ((NUM_CHARTS_FIRST_ROW - 1) * TABLE_MARGIN)
773
+ ) / NUM_CHARTS_FIRST_ROW
774
+
775
+ max_height = max(
776
+ [
777
+ get_pie_chart_vertical_height(geographical, legend_max_cols=10),
778
+ get_pie_chart_vertical_height(currencies),
779
+ get_pie_chart_vertical_height(allocation),
780
+ get_pie_chart_vertical_height(marketcap),
781
+ ]
782
+ )
783
+ geographical_pie_chart = get_pie_chart_vertical(
784
+ geographical, WIDTH_CHARTS_FIRST_ROW, max_height, general_data["colors"], legend_max_cols=10
785
+ )
786
+ currencies_pie_chart = get_pie_chart_vertical(
787
+ currencies, WIDTH_CHARTS_FIRST_ROW, max_height, general_data["colors"]
788
+ )
789
+ allocation_pie_chart = get_pie_chart_vertical(
790
+ allocation, WIDTH_CHARTS_FIRST_ROW, max_height, general_data["colors"]
791
+ )
792
+ marketcap_pie_chart = get_pie_chart_vertical(
793
+ marketcap, WIDTH_CHARTS_FIRST_ROW, max_height, general_data["colors"]
794
+ )
795
+
796
+ top_3_td = [
797
+ [
798
+ Spacer(width=0, height=0),
799
+ Paragraph("<strong>Geographical<br />Breakdown</strong>", style=s_table_medium_leading),
800
+ Spacer(width=0, height=0),
801
+ Paragraph("<strong>Currency<br />Exposure</strong>", style=s_table_medium_leading),
802
+ Spacer(width=0, height=0),
803
+ Paragraph("<strong>Asset<br />Allocation</strong>", style=s_table_medium_leading),
804
+ Spacer(width=0, height=0),
805
+ Paragraph(
806
+ f"<strong>Market Cap<br />Distributions ({context['currency']})</strong>",
807
+ style=s_table_medium_leading,
808
+ ),
809
+ ],
810
+ [
811
+ Spacer(width=0, height=0),
812
+ geographical_pie_chart,
813
+ Spacer(width=0, height=0),
814
+ currencies_pie_chart,
815
+ Spacer(width=0, height=0),
816
+ allocation_pie_chart,
817
+ Spacer(width=0, height=0),
818
+ marketcap_pie_chart,
819
+ ],
820
+ ]
821
+
822
+ cols = [CONTENT_OFFSET]
823
+ cols.extend([WIDTH_CHARTS_FIRST_ROW, TABLE_MARGIN] * NUM_CHARTS_FIRST_ROW)
824
+ cols.pop(-1)
825
+
826
+ top_3_t = Table(
827
+ top_3_td,
828
+ colWidths=cols,
829
+ rowHeights=[
830
+ 1.4 * cm,
831
+ max_height,
832
+ ],
833
+ )
834
+ top_3_table_styles = [
835
+ ("VALIGN", (0, 0), (-1, 0), "MIDDLE"),
836
+ ("LEFTPADDING", (0, 0), (-1, -1), 0),
837
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
838
+ ]
839
+
840
+ for col in range(1, NUM_CHARTS_FIRST_ROW * 2, 2):
841
+ top_3_table_styles.extend(
842
+ [
843
+ ("LINEABOVE", (col, 0), (col, 0), 0.25, c_box_color),
844
+ ("LINEBELOW", (col, 0), (col, 0), 0.25, c_box_color),
845
+ ]
846
+ )
847
+
848
+ if debug:
849
+ top_3_table_styles.extend(
850
+ [
851
+ ("BOX", (0, 0), (-1, -1), 0.25, c_box_color),
852
+ ("INNERGRID", (0, 0), (-1, -1), 0.25, c_box_color),
853
+ ]
854
+ )
855
+
856
+ top_3_t.setStyle(TableStyle(top_3_table_styles))
857
+ elements.append(top_3_t)
858
+
859
+ # top_3_t.setStyle(
860
+ # TableStyle(
861
+ # [
862
+ # ("BOX", (0, 0), (-1, -1), 0.25, c_box_color),
863
+ # ("INNERGRID", (0, 0), (-1, -1), 0.25, c_box_color),
864
+ # ("LINEABOVE", (1, 0), (1, 0), 0.25, c_box_color),
865
+ # ("LINEBELOW", (1, 0), (1, 0), 0.25, c_box_color),
866
+ # ("LINEABOVE", (3, 0), (3, 0), 0.25, c_box_color),
867
+ # ("LINEBELOW", (3, 0), (3, 0), 0.25, c_box_color),
868
+ # ("LINEABOVE", (5, 0), (5, 0), 0.25, c_box_color),
869
+ # ("LINEBELOW", (5, 0), (5, 0), 0.25, c_box_color),
870
+ # ("LINEABOVE", (7, 0), (7, 0), 0.25, c_box_color),
871
+ # ("LINEBELOW", (7, 0), (7, 0), 0.25, c_box_color),
872
+ # ("VALIGN", (0, 0), (-1, 0), "MIDDLE"),
873
+ # ("LEFTPADDING", (0, 0), (-1, -1), 0),
874
+ # ("SPAN", (1, 2), (3, 2)),
875
+ # ("SPAN", (1, 3), (3, 3)),
876
+ # ("SPAN", (1, 4), (3, 4)),
877
+ # ("SPAN", (5, 2), (7, 2)),
878
+ # ("SPAN", (1, 5), (3, 5)),
879
+ # ("SPAN", (1, 4), (1, 4)),
880
+ # ("LINEABOVE", (1, 2), (3, 2), 0.25, c_box_color),
881
+ # ("LINEBELOW", (1, 2), (3, 2), 0.25, c_box_color),
882
+ # ("LINEABOVE", (5, 2), (7, 2), 0.25, c_box_color),
883
+ # ("LINEBELOW", (5, 2), (7, 2), 0.25, c_box_color),
884
+ # ("LINEABOVE", (1, 4), (3, 4), 0.25, c_box_color),
885
+ # ("LINEBELOW", (1, 4), (3, 4), 0.25, c_box_color),
886
+ # ("VALIGN", (1, 2), (-1, 2), "MIDDLE"),
887
+ # ("VALIGN", (1, 4), (3, 4), "MIDDLE"),
888
+ # ("VALIGN", (1, 4), (3, 4), "MIDDLE"),
889
+ # ("VALIGN", (1, 4), (3, 4), "MIDDLE"),
890
+ # ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
891
+ # ("SPAN", (5, 3), (7, 5)),
892
+ # ("VALIGN", (5, 3), (7, 5), "TOP"),
893
+ # ]
894
+ # )
895
+ # )
896
+
897
+ liquid_height = get_pie_chart_horizontal_height(liquidity, legend_max_cols=50)
898
+ liquidity_pie_chart = get_pie_chart_horizontal(
899
+ liquidity,
900
+ 4.23 * cm + 0.56 * cm + 4.23 * cm,
901
+ liquid_height,
902
+ general_data["colors"],
903
+ 4.23 * cm,
904
+ legend_max_cols=50,
905
+ )
906
+
907
+ risk_height = 3.564 * cm
908
+ industry_height = get_bar_chart_height(industry)
909
+
910
+ available_height = CONTENT_HEIGHT - 1.4 * cm - max_height - 0.85 * cm - (0.41 * cm + 12)
911
+
912
+ left_height = liquid_height + risk_height + (1 * 0.85 * cm)
913
+ right_height = max(min(available_height, industry_height), left_height)
914
+
915
+ # max_available_height = CONTENT_HEIGHT - 1.4 * cm - max_height - 0.85 * cm
916
+ industry_chart = get_bar_chart(industry, 8.94 * cm, right_height - 30)
917
+
918
+ last_row_height = right_height - liquid_height - 0.85 * cm
919
+
920
+ # last_height = min(
921
+ # max(industry_height - liquid_height + 6, 3.564 * cm), max_available_height - liquid_height
922
+ # ) # 6 because of the drawn string
923
+
924
+ second_charts = [
925
+ [
926
+ Spacer(width=0, height=0),
927
+ Paragraph(
928
+ "<strong>Equity Liquidity</strong> (on average 3M daily trading volume)",
929
+ style=s_table_medium_leading,
930
+ ),
931
+ Spacer(width=0, height=0),
932
+ Paragraph("<strong>Industry Exposure</strong>", style=s_table_medium_leading),
933
+ ],
934
+ [
935
+ Spacer(width=0, height=0),
936
+ liquidity_pie_chart,
937
+ Spacer(width=0, height=0),
938
+ # Spacer(width=0, height=0),
939
+ industry_chart,
940
+ ],
941
+ [
942
+ Spacer(width=0, height=0),
943
+ Paragraph("<strong>Risk Scale</strong>", style=s_table_medium_leading),
944
+ Spacer(width=0, height=0),
945
+ Spacer(width=0, height=0),
946
+ ],
947
+ [
948
+ Spacer(width=0, height=0),
949
+ RiskScaleFlowable(risk_height, general_data["risk_scale"]),
950
+ Spacer(width=0, height=0),
951
+ Spacer(width=0, height=0),
952
+ ],
953
+ ]
954
+
955
+ col_width = (CONTENT_WIDTH_PAGE2 - CONTENT_OFFSET - TABLE_MARGIN) / 2
956
+
957
+ other_table = Table(
958
+ second_charts,
959
+ colWidths=[CONTENT_OFFSET, col_width, TABLE_MARGIN, col_width],
960
+ rowHeights=[0.85 * cm, liquid_height, 0.85 * cm, last_row_height],
961
+ )
962
+
963
+ other_table_styles = [
964
+ ("VALIGN", (1, 2), (1, -1), "TOP"),
965
+ ("SPAN", (3, 1), (3, -1)),
966
+ ("VALIGN", (3, 1), (3, -1), "TOP"),
967
+ ("LINEABOVE", (1, 0), (1, 0), 0.25, c_box_color),
968
+ ("LINEBELOW", (1, 0), (1, 0), 0.25, c_box_color),
969
+ ("LINEABOVE", (3, 0), (3, 0), 0.25, c_box_color),
970
+ ("LINEBELOW", (3, 0), (3, 0), 0.25, c_box_color),
971
+ ("LINEABOVE", (1, 2), (1, 2), 0.25, c_box_color),
972
+ ("LINEBELOW", (1, 2), (1, 2), 0.25, c_box_color),
973
+ ("LEFTPADDING", (0, 0), (-1, -1), 0),
974
+ ]
975
+ if debug:
976
+ other_table_styles.extend(
977
+ [
978
+ ("BOX", (0, 0), (-1, -1), 0.25, c_box_color),
979
+ ("INNERGRID", (0, 0), (-1, -1), 0.25, c_box_color),
980
+ ]
981
+ )
982
+
983
+ other_table.setStyle(TableStyle(other_table_styles))
984
+
985
+ elements.append(other_table)
986
+
987
+ doc.build(elements)
988
+ output.seek(0)
989
+
990
+ return output