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,864 @@
1
+ import itertools
2
+ from datetime import date
3
+ from decimal import Decimal
4
+ from io import BytesIO
5
+
6
+ from reportlab import platypus
7
+ from reportlab.graphics import renderPDF
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
+ Image,
23
+ PageTemplate,
24
+ Paragraph,
25
+ Spacer,
26
+ Table,
27
+ TableStyle,
28
+ )
29
+ from reportlab.platypus.flowables import KeepTogether, TopPadder
30
+ from reportlab.platypus.frames import Frame
31
+ from svglib.svglib import svg2rlg
32
+ from wbreport.models import ReportAsset
33
+ from wbreport.pdf.charts.pie import (
34
+ get_pie_chart_horizontal,
35
+ get_pie_chart_horizontal_height,
36
+ )
37
+ from wbreport.pdf.charts.timeseries import Scale, get_timeseries_chart
38
+ from wbreport.pdf.flowables.risk import RiskScale
39
+ from wbreport.pdf.flowables.textboxes import TextBox, TextBoxWithImage, TextWithIcon
40
+ from wbreport.pdf.tables.aggregated_tables import (
41
+ get_fund_table,
42
+ get_simple_aggregated_table,
43
+ )
44
+ from wbreport.pdf.tables.data_tables import get_simple_data_table
45
+
46
+
47
+ def generate_report(context):
48
+ debug = False
49
+
50
+ main_features_dict_tmp = context["information_table"]
51
+ main_features_dict = {}
52
+ for k, v in main_features_dict_tmp.items():
53
+ main_features_dict.update(v)
54
+ # Monthly returns table as dataframe, None is no value
55
+ monthly_returns = context["monthly_returns"]
56
+
57
+ # Pie chart Dataframe
58
+ geographical = context["geographical_breakdown"]
59
+ currencies = context["currency_exposure"]
60
+ allocation = context["asset_allocation"]
61
+ marketcap = context["market_cap_distribution"]
62
+ liquidity = context["equity_liquidity"]
63
+ industry = context["industry_exposure"]
64
+
65
+ # Price time serie as dataframe
66
+ prices = context["prices"]
67
+
68
+ # TOPs as list
69
+
70
+ top_10_holdings = context["holdings"][0:10]
71
+
72
+ top_3_contributors = context["top_contributors"]
73
+ bottom_3_contributors = context["worst_contributors"]
74
+
75
+ # {'high': {'price':low, 'date':date}, 'low' : {'price':high, 'date':date}}
76
+ all_time_dict = context["all_time"]
77
+ end = context["date"]
78
+ general_data = {}
79
+ general_data["date"] = end.strftime("%b %Y")
80
+ general_data["colors"] = context["colors_palette"]
81
+ general_data["title"] = context["title"].replace("&", "&")
82
+ general_data["risk_scale"] = context["risk_scale"]
83
+
84
+ general_data["introduction"] = context["introduction"]
85
+ fund_table = context["funds_table"]
86
+ calculated_risk_scale = context["risk_scale"]
87
+
88
+ # Register Fonts
89
+ pdfmetrics.registerFont(TTFont("customfont", ReportAsset.objects.get(key="font-default").asset))
90
+ pdfmetrics.registerFont(TTFont("customfont-bd", ReportAsset.objects.get(key="font-bd").asset))
91
+ pdfmetrics.registerFont(TTFont("customfont-it", ReportAsset.objects.get(key="font-it").asset))
92
+ registerFontFamily("customfont", normal="customfont", bold="customfont-bd", italic="customfont-it")
93
+
94
+ # Page Variables
95
+ LINEHEIGHT = 12
96
+
97
+ HEADER_HEIGHT = 2.34 * cm
98
+ FOOTER_HEIGHT = 2.34 * cm
99
+
100
+ SIDE_MARGIN = 0.96 * cm
101
+ TOP_MARGIN = 3.63 * cm
102
+ CONTENT_OFFSET = 0.34 * cm
103
+ CONTENT_MARGIN = SIDE_MARGIN + CONTENT_OFFSET
104
+
105
+ CONTENT_HEIGHT = 22.25 * cm
106
+ CONTENT_WIDTH_PAGE1_LEFT = 13.84 * cm
107
+ CONTENT_WIDTH_PAGE1_RIGHT = 4.23 * cm
108
+ CONTENT_WIDTH_PAGE2 = A4[0] - SIDE_MARGIN - CONTENT_MARGIN
109
+
110
+ CONTENT_X_PAGE1_RIGHT = A4[0] - CONTENT_MARGIN - CONTENT_WIDTH_PAGE1_RIGHT
111
+ CONTENT_Y = A4[1] - CONTENT_HEIGHT - TOP_MARGIN
112
+
113
+ TABLE_MARGIN_PAGE2 = 0.56 * cm
114
+
115
+ output = BytesIO()
116
+ doc = BaseDocTemplate(
117
+ output,
118
+ pagesize=A4,
119
+ rightMargin=0,
120
+ leftMargin=0,
121
+ topMargin=0,
122
+ bottomMargin=0,
123
+ title=general_data["title"],
124
+ author="Atonra Partners SA",
125
+ )
126
+ elements = []
127
+
128
+ s_base = ParagraphStyle(name="s_base", fontName="customfont", fontSize=9, leading=10)
129
+ s_base_small_justified = ParagraphStyle(
130
+ name="s_base_small_justified", parent=s_base, fontSize=6.5, leading=7, alignment=TA_JUSTIFY
131
+ )
132
+ s_base_small_justified_indent = ParagraphStyle(
133
+ name="s_base_small_justified_indent", parent=s_base_small_justified, leftIndent=CONTENT_OFFSET
134
+ )
135
+ s_base_indent = ParagraphStyle(name="s_description", parent=s_base, spaceBefore=8, leftIndent=CONTENT_OFFSET)
136
+ s_table_base = ParagraphStyle(
137
+ name="s_table_base",
138
+ fontName="customfont",
139
+ fontSize=6,
140
+ leading=6,
141
+ )
142
+ s_table_medium = ParagraphStyle(name="s_table_medium", parent=s_table_base, fontSize=9, leading=8)
143
+ s_table_medium_leading = ParagraphStyle(name="s_table_medium_leading", parent=s_table_medium, leading=13.9)
144
+ s_table_large = ParagraphStyle(name="s_table_large", parent=s_table_medium, fontSize=11, leading=11)
145
+ s_table_large_center = ParagraphStyle(name="s_table_large", parent=s_table_large, alignment=TA_CENTER)
146
+ s_table_large_center_padding = ParagraphStyle(
147
+ name="s_table_large", parent=s_table_large_center, spaceBefore=20, spaceAfter=20
148
+ )
149
+ s_table_center = ParagraphStyle(
150
+ name="s_table_center",
151
+ parent=s_table_base,
152
+ alignment=TA_CENTER,
153
+ )
154
+ s_table_center_high = ParagraphStyle(name="s_table_center", parent=s_table_center, leading=9, fontSize=8)
155
+ s_table_right = ParagraphStyle(name="s_table_right", parent=s_table_base, alignment=TA_RIGHT)
156
+ s_table_center = ParagraphStyle(name="s_table_center", parent=s_table_base, alignment=TA_CENTER)
157
+ s_table_headline = ParagraphStyle(
158
+ name="s_table_headline",
159
+ parent=s_table_base,
160
+ fontSize=16,
161
+ leading=16,
162
+ )
163
+ s_table_headline_2 = ParagraphStyle(
164
+ name="s_table_headline_2",
165
+ parent=s_table_base,
166
+ fontSize=10,
167
+ leading=10,
168
+ )
169
+
170
+ # Setup Colors
171
+ c_product = HexColor(general_data["colors"][0])
172
+ c_product_alpha = HexColor(f"{general_data['colors'][0]}20", hasAlpha=True)
173
+
174
+ c_table_border = HexColor(0x9EA3AC)
175
+ c_grid_color = HexColor(0xB6BAC1)
176
+ c_box_color = HexColor(0x3C4859)
177
+ c_table_background = colors.HexColor(0xE2E3E6)
178
+
179
+ # Frame and Page Layout
180
+ frame_defaults = {"showBoundary": debug, "leftPadding": 0, "rightPadding": 0, "topPadding": 0, "bottomPadding": 0}
181
+
182
+ text_frame = Frame(
183
+ x1=SIDE_MARGIN,
184
+ y1=CONTENT_Y,
185
+ width=CONTENT_WIDTH_PAGE1_LEFT,
186
+ height=CONTENT_HEIGHT,
187
+ id="text_frame",
188
+ **frame_defaults,
189
+ )
190
+
191
+ main_features_frame = Frame(
192
+ x1=CONTENT_X_PAGE1_RIGHT,
193
+ y1=CONTENT_Y,
194
+ width=CONTENT_WIDTH_PAGE1_RIGHT,
195
+ height=CONTENT_HEIGHT,
196
+ id="main_features_frame",
197
+ **frame_defaults,
198
+ )
199
+
200
+ second_page = Frame(
201
+ x1=SIDE_MARGIN,
202
+ y1=CONTENT_Y,
203
+ width=CONTENT_WIDTH_PAGE2,
204
+ height=CONTENT_HEIGHT,
205
+ id="second_page",
206
+ **frame_defaults,
207
+ )
208
+
209
+ def on_page(canv, dock):
210
+ canv.saveState()
211
+
212
+ # Header
213
+ canv.setFillColor(c_box_color)
214
+ canv.rect(0, A4[1] - HEADER_HEIGHT, A4[0], HEADER_HEIGHT, fill=True, stroke=False)
215
+
216
+ colors = [
217
+ HexColor(0xFFB166),
218
+ HexColor(0x8CD66B),
219
+ HexColor(0x05D6A1),
220
+ HexColor(0x01ABAA),
221
+ HexColor(0x70D6FF),
222
+ HexColor(0x0585FF),
223
+ HexColor(0x5724D9),
224
+ HexColor(0xA359E5),
225
+ HexColor(0xEF476F),
226
+ ]
227
+ height = 0.13 * cm
228
+ width = A4[0] / len(colors)
229
+ for index, color in enumerate(colors):
230
+ canv.setFillColor(color)
231
+ canv.rect(0 + index * width, A4[1] - HEADER_HEIGHT - height, width, height, fill=True, stroke=False)
232
+
233
+ # Footer
234
+ canv.setFillColor(c_box_color)
235
+ canv.rect(0, 0, A4[0], FOOTER_HEIGHT, fill=True, stroke=False)
236
+
237
+ for index, color in enumerate(colors):
238
+ canv.setFillColor(color)
239
+ canv.rect(0 + index * width, FOOTER_HEIGHT, width, height, fill=True, stroke=False)
240
+
241
+ drawing = svg2rlg(ReportAsset.objects.get(key="logo").asset)
242
+ width, height = drawing.width, drawing.height
243
+ height = 1.295 * cm
244
+
245
+ scaling_factor = 1.295 * cm / drawing.height
246
+ drawing.scale(scaling_factor, scaling_factor)
247
+ renderPDF.draw(
248
+ drawing, canv, ((A4[0] - width * scaling_factor) / 2), A4[1] - height - (HEADER_HEIGHT - height) / 2
249
+ )
250
+
251
+ reportlab_image = Image(ReportAsset.objects.get(key="footer-text").asset)
252
+ width, height = reportlab_image.imageWidth, reportlab_image.imageHeight
253
+ ratio = float(width) / float(height)
254
+ height = 0.436 * cm
255
+ width = height * ratio
256
+
257
+ reportlab_image.drawWidth = width
258
+ reportlab_image.drawHeight = height
259
+ reportlab_image.drawOn(canv, (A4[0] - width) / 2, (FOOTER_HEIGHT - height) / 2)
260
+
261
+ canv.restoreState()
262
+
263
+ doc.addPageTemplates(
264
+ [
265
+ PageTemplate(id="page", onPage=on_page, frames=[text_frame, main_features_frame]),
266
+ PageTemplate(id="second_page", onPage=on_page, frames=[second_page]),
267
+ ]
268
+ )
269
+
270
+ def generate_title(title):
271
+ table_data = [[Paragraph("", style=s_table_headline), Paragraph(title, style=s_table_headline)]]
272
+ title_table = Table(table_data, colWidths=[0.14 * cm, None], rowHeights=[0.41 * cm])
273
+ title_table.setStyle(
274
+ TableStyle(
275
+ [
276
+ ("BACKGROUND", (0, 0), (0, 0), c_product),
277
+ ("LEFTPADDING", (0, 0), (-1, -1), 0),
278
+ ("LEFTPADDING", (1, 0), (-1, -1), 0.2 * cm),
279
+ ("RIGHTPADDING", (0, 0), (-1, -1), 0),
280
+ ("TOPPADDING", (0, 0), (-1, -1), 0),
281
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
282
+ ]
283
+ )
284
+ )
285
+ return title_table
286
+
287
+ def impress(l):
288
+ style = s_table_headline_2
289
+ table_data = [
290
+ [
291
+ Paragraph("", style=style),
292
+ Paragraph("Important information", style=style),
293
+ ],
294
+ [
295
+ Paragraph("", style=style),
296
+ Paragraph(ReportAsset.objects.get(key="disclaimer").text, style=s_base_small_justified),
297
+ ],
298
+ ]
299
+
300
+ t = Table(table_data, colWidths=[0.14 * cm, None], rowHeights=[0.41 * cm, None])
301
+ t.setStyle(
302
+ TableStyle(
303
+ [
304
+ ("BACKGROUND", (0, 0), (0, 0), colors.white),
305
+ ("LEFTPADDING", (0, 0), (-1, -1), 0),
306
+ ("LEFTPADDING", (1, 0), (-1, -1), 0.2 * cm),
307
+ ("RIGHTPADDING", (0, 0), (-1, -1), 0),
308
+ ("TOPPADDING", (0, 0), (-1, -1), 0),
309
+ ("TOPPADDING", (0, 1), (-1, -1), 0.334 * cm),
310
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
311
+ # ("BOTTOMPADDING", (0, -1), (-1, -1), 0.7*cm),
312
+ ]
313
+ )
314
+ )
315
+ l.append(KeepTogether(TopPadder(t)))
316
+
317
+ # Description
318
+ elements.append(generate_title(general_data["title"]))
319
+ elements.append(Paragraph(general_data["introduction"], style=s_base_indent))
320
+ elements.append(Spacer(height=LINEHEIGHT * 2, width=0))
321
+
322
+ # Monthly Returns
323
+ elements.append(generate_title("Monthly Returns (%)"))
324
+ elements.append(Spacer(height=LINEHEIGHT * 1, width=0))
325
+ elements.append(
326
+ get_simple_aggregated_table(
327
+ df=monthly_returns,
328
+ width=CONTENT_WIDTH_PAGE1_LEFT,
329
+ row_height=0.389 * cm,
330
+ header_style=s_table_center,
331
+ row_style=s_table_center,
332
+ data_style=s_table_right,
333
+ grid_color=c_table_border,
334
+ offset=CONTENT_OFFSET,
335
+ debug=debug,
336
+ )
337
+ )
338
+ elements.append(Spacer(height=LINEHEIGHT * 1, width=0))
339
+ elements.append(
340
+ Paragraph(
341
+ "<i>*Performance data from February 2016 to July 7th 2020 reflect the historical performances of AtonRâ's thematic certificates. Themes at inception: Mobile Payments, Biotechnology, Artificial Intelligence & Robotics, Security & Space. Subsequent additions: Bionics (January 2017), Fintech (May 2017), Sustainable Future (October 2018).</i>",
342
+ style=s_base_small_justified,
343
+ )
344
+ )
345
+ elements.append(Spacer(height=LINEHEIGHT * 2, width=0))
346
+
347
+ # Price Timeseries Chart
348
+ elements.append(
349
+ get_timeseries_chart(
350
+ data=[list(zip(prices.index, prices.net_value))],
351
+ width=CONTENT_WIDTH_PAGE1_LEFT - CONTENT_OFFSET,
352
+ height=4.34 * cm,
353
+ color=c_product,
354
+ fill_color=c_product_alpha,
355
+ grid_color=c_grid_color,
356
+ scale=Scale.LOGARITHMIC.value,
357
+ debug=debug,
358
+ x=CONTENT_OFFSET,
359
+ )
360
+ )
361
+ elements.append(Spacer(height=LINEHEIGHT * 2, width=0))
362
+
363
+ # Top 3
364
+ elements.append(
365
+ get_simple_data_table(
366
+ headers=[
367
+ "<strong>Top 10 Holdings</strong>",
368
+ "<strong>Top 3 Contributors</strong>",
369
+ "<strong>Bottom 3 Contributors</strong>",
370
+ ],
371
+ data=list(itertools.zip_longest(top_10_holdings, top_3_contributors, bottom_3_contributors, fillvalue="")),
372
+ width=CONTENT_WIDTH_PAGE1_LEFT,
373
+ header_row_height=0.85 * cm,
374
+ data_row_height=0.45 * cm,
375
+ margin=0.16 * cm,
376
+ header_style=s_table_medium,
377
+ data_style=s_table_base,
378
+ grid_color=c_table_border,
379
+ offset=CONTENT_OFFSET,
380
+ debug=debug,
381
+ )
382
+ )
383
+
384
+ # More Information Box
385
+ elements.append(
386
+ TopPadder(
387
+ TextBoxWithImage(
388
+ width=CONTENT_WIDTH_PAGE1_LEFT,
389
+ height=1.36 * cm,
390
+ img=ReportAsset.objects.get(key="robot").asset,
391
+ img_x=0.516 * cm,
392
+ img_y=-0.8 * cm,
393
+ img_width=2.89 * cm,
394
+ img_height=2.89 * cm,
395
+ text='<strong>Click here to learn more:</strong> <a href="https://www.atonra.ch/our-research/">www.atonra.ch/our-research/</a>',
396
+ text_style=s_table_center_high,
397
+ box_color=c_table_background,
398
+ offset=CONTENT_OFFSET,
399
+ debug=debug,
400
+ )
401
+ )
402
+ )
403
+
404
+ elements.append(platypus.FrameBreak("main_features_frame"))
405
+
406
+ # Main Features Table
407
+
408
+ MAIN_FEATURES_COLOR_BAR_HEIGHT = 0.23 * cm
409
+ MAIN_FEATURES_GAP_HEIGHT = 0.17 * cm
410
+ MAIN_FEATURES_TITLE_HEIGHT = 1.4 * cm
411
+ MAIN_FEATURES_TITLE1_HEIGHT = 0.74 * cm
412
+
413
+ table_data = [
414
+ [Spacer(width=0, height=0)],
415
+ [
416
+ TextWithIcon(
417
+ width=CONTENT_WIDTH_PAGE1_RIGHT,
418
+ height=MAIN_FEATURES_TITLE_HEIGHT,
419
+ text=general_data["date"],
420
+ font="customfont-bd",
421
+ font_size=10,
422
+ icon=context["logo_file"] if "logo_file" in context else None,
423
+ )
424
+ ],
425
+ [Paragraph("<strong>MAIN FEATURES</strong>", style=s_table_center)],
426
+ [Spacer(width=0, height=0)],
427
+ ]
428
+
429
+ for label, value in main_features_dict.items():
430
+ if isinstance(value, date):
431
+ value = value.strftime("%d-%b-%y")
432
+ elif isinstance(value, (Decimal, float)):
433
+ value = "%.2f" % value
434
+ elif isinstance(value, int):
435
+ value = str(value)
436
+ elif value is None:
437
+ value = ""
438
+
439
+ if label.lower() in ["currency", "last price"]:
440
+ table_data.append(
441
+ [
442
+ Paragraph(f"<strong>{label.upper()}</strong>", style=s_table_base),
443
+ Paragraph(f"<strong>{value.upper()}</strong>", style=s_table_base),
444
+ ]
445
+ )
446
+ else:
447
+ table_data.append(
448
+ [
449
+ Paragraph(label.upper(), style=s_table_base),
450
+ Paragraph(value.upper(), style=s_table_base),
451
+ ]
452
+ )
453
+
454
+ table_data.extend([[Spacer(width=0, height=0)], [Spacer(width=0, height=0)]])
455
+
456
+ row_heights = [
457
+ MAIN_FEATURES_COLOR_BAR_HEIGHT,
458
+ MAIN_FEATURES_TITLE_HEIGHT,
459
+ MAIN_FEATURES_TITLE1_HEIGHT,
460
+ MAIN_FEATURES_GAP_HEIGHT,
461
+ ]
462
+ row_heights.extend([None] * (len(table_data) - 6))
463
+ row_heights.extend([MAIN_FEATURES_GAP_HEIGHT, MAIN_FEATURES_COLOR_BAR_HEIGHT])
464
+
465
+ t = Table(table_data, rowHeights=row_heights, colWidths=[CONTENT_WIDTH_PAGE1_RIGHT / 2] * 2)
466
+ table_styles = [
467
+ ("BACKGROUND", (0, 0), (1, 0), c_product), # Top Color Bar
468
+ ("BACKGROUND", (0, -1), (1, -1), c_product), # Bottom Color Bar
469
+ ("BACKGROUND", (0, 2), (-1, 2), c_table_background), # Title Background
470
+ ("LINEABOVE", (0, 2), (-1, 2), 0.25, colors.HexColor("#6d7683")), # Title Line Above
471
+ ("LINEBELOW", (0, 2), (-1, 2), 0.25, colors.HexColor("#6d7683")), # Title Line Below
472
+ ("LINEBEFORE", (1, 3), (1, -2), 0.25, colors.HexColor("#6d7683")), # Data Vertical Seperator
473
+ ("SPAN", (0, 1), (-1, 1)), # Col Span Title
474
+ ("SPAN", (0, 2), (-1, 2)), # Col Span Title1
475
+ ("LEFTPADDING", (0, 1), (0, -1), 0), # Leftpadding Data Labels
476
+ ("LEFTPADDING", (1, 1), (1, -1), 0.28 * cm), # Leftpadding Data Values
477
+ ("VALIGN", (0, 1), (-1, -1), "MIDDLE"),
478
+ ]
479
+
480
+ if debug:
481
+ table_styles.extend(
482
+ [
483
+ ("INNERGRID", (0, 0), (-1, -1), 0.25, colors.black),
484
+ ("BOX", (0, 0), (-1, -1), 0.25, colors.black),
485
+ ]
486
+ )
487
+
488
+ t.setStyle(TableStyle(table_styles))
489
+
490
+ elements.append(t)
491
+ elements.append(Spacer(height=LINEHEIGHT * 1, width=0))
492
+
493
+ # All time table
494
+ table_data = [
495
+ [
496
+ Paragraph("<strong>ALL TIME HIGH</strong>", style=s_table_center),
497
+ Paragraph("<strong>ALL TIME LOW</strong>", style=s_table_center),
498
+ ],
499
+ [
500
+ Paragraph("", style=s_table_center),
501
+ Paragraph("", style=s_table_center),
502
+ ],
503
+ ]
504
+
505
+ table_data.append(
506
+ [
507
+ Paragraph(f'PRICE: {all_time_dict["high"]["price"]:.1f}', style=s_table_center),
508
+ Paragraph(f'PRICE: {all_time_dict["low"]["price"]:.1f}', style=s_table_center),
509
+ ],
510
+ )
511
+ table_data.append(
512
+ [
513
+ Paragraph(f'DATE: {all_time_dict["high"]["date"]:%d-%b-%y}', style=s_table_center),
514
+ Paragraph(f'DATE: {all_time_dict["low"]["date"]:%d-%b-%y}', style=s_table_center),
515
+ ],
516
+ )
517
+ table_data.append(
518
+ [
519
+ Paragraph("", style=s_table_center),
520
+ Paragraph("", style=s_table_center),
521
+ ]
522
+ )
523
+
524
+ row_heights = [0.74 * cm, 0.17 * cm, None, None, 0.17 * cm]
525
+
526
+ t = Table(table_data, rowHeights=row_heights)
527
+ t.setStyle(
528
+ TableStyle(
529
+ [
530
+ ("BACKGROUND", (0, 0), (1, 0), c_table_background),
531
+ ("LINEABOVE", (0, 0), (1, 0), 0.25, colors.HexColor("#6d7683")),
532
+ ("LINEBELOW", (0, 0), (1, 0), 0.25, colors.HexColor("#6d7683")),
533
+ ("LINEBELOW", (0, -1), (1, -1), 0.25, colors.HexColor("#6d7683")),
534
+ ("LINEBEFORE", (1, 0), (1, -1), 0.25, colors.HexColor("#6d7683")),
535
+ ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
536
+ ("LEFTPADDING", (0, 0), (-1, -1), 0),
537
+ ("RIGHTPADDING", (0, 0), (-1, -1), 0),
538
+ ]
539
+ )
540
+ )
541
+
542
+ elements.append(t)
543
+
544
+ elements.append(
545
+ TopPadder(
546
+ TextBox(
547
+ width=CONTENT_WIDTH_PAGE1_RIGHT,
548
+ height=4.20 * cm,
549
+ text='<strong>Investment Team:</strong><br />a seasoned team of<br />portfolio managers /<br />analysts and engineers<br />supported by the full<br />resources of AtonRâ<br />Partners.<br /><br /><strong><a href="https://atonra.ch/who-we-are/our-team/">Click here to discover<br />the entire team</a></strong>',
550
+ text_style=s_table_center_high,
551
+ box_color=c_table_background,
552
+ debug=debug,
553
+ )
554
+ )
555
+ )
556
+
557
+ # Second Page
558
+
559
+ def get_bar_chart_height(df, bar_width=0.12 * cm, bar_padding=0.3 * cm, max_label_width=3.35 * cm):
560
+ number_of_items = len(df)
561
+ return number_of_items * (bar_width + 2 * bar_padding)
562
+
563
+ def get_bar_chart(df, width, height, bar_width=0.12 * cm, bar_padding=0.3 * cm, max_label_width=3.35 * cm):
564
+ font_size = 6
565
+ drawing = Drawing(width, height)
566
+ bar_chart = HorizontalBarChart()
567
+
568
+ data = list()
569
+ categories = list()
570
+ for index, row in enumerate(df.itertuples()):
571
+ data.append(row.weighting * 100)
572
+ categories.append(f"{row.aggregated_title} ({row.weighting*100:.1f}%)")
573
+
574
+ max_label = 0
575
+ _categories = list()
576
+ for category in categories:
577
+ _w = stringWidth(category, "customfont", font_size)
578
+ if _w > max_label_width:
579
+ split_list = category.split(" ")
580
+ splitter = int(len(split_list) * 2 / 3)
581
+
582
+ part_1 = " ".join(split_list[:splitter])
583
+ part_2 = " ".join(split_list[splitter:])
584
+ category = part_1 + "\n" + part_2
585
+ _w1 = stringWidth(part_1, "customfont", font_size)
586
+ _w2 = stringWidth(part_2, "customfont", font_size)
587
+ _w = max(_w1, _w2)
588
+ _categories.append(category)
589
+ max_label = max(max_label, _w - bar_chart.categoryAxis.labels.dx)
590
+
591
+ bar_chart.width = width - max_label
592
+
593
+ bar_chart.height = height
594
+ bar_chart.x = width - bar_chart.width
595
+ bar_chart.y = 0
596
+
597
+ bar_chart.data = [list(reversed(data))]
598
+ bar_chart.categoryAxis.categoryNames = list(reversed(_categories))
599
+ bar_chart.categoryAxis.labels.boxAnchor = "e"
600
+ bar_chart.categoryAxis.labels.textAnchor = "end"
601
+ bar_chart.categoryAxis.labels.fontName = "customfont"
602
+ bar_chart.categoryAxis.labels.fontSize = font_size
603
+ bar_chart.categoryAxis.labels.leading = font_size
604
+
605
+ bar_chart.barWidth = bar_width
606
+ bar_chart.bars.strokeColor = colors.transparent
607
+ bar_chart.bars[0].fillColor = c_product
608
+
609
+ # x-Axis
610
+ bar_chart.valueAxis.labelTextFormat = DecimalFormatter(0, suffix="%")
611
+ bar_chart.valueAxis.labels.fontName = "customfont"
612
+ bar_chart.valueAxis.labels.fontSize = 6
613
+
614
+ bar_chart.valueAxis.strokeWidth = -1
615
+ bar_chart.valueAxis.gridStrokeColor = c_grid_color
616
+ bar_chart.valueAxis.gridStrokeDashArray = (0.2, 0, 0.2)
617
+
618
+ bar_chart.valueAxis.visibleGrid = True
619
+ bar_chart.valueAxis.forceZero = True
620
+
621
+ bar_chart.categoryAxis.strokeWidth = 0.5
622
+ bar_chart.categoryAxis.strokeColor = HexColor(0x6D7683)
623
+ bar_chart.categoryAxis.tickLeft = 0
624
+ bar_chart.categoryAxis.labels.fontName = "customfont"
625
+ bar_chart.categoryAxis.labels.fontSize = 6
626
+ drawing.add(bar_chart)
627
+ drawing.add(String(0, -25, "", fontName="customfont", fontSize=6, fillColor=colors.black))
628
+ return drawing
629
+
630
+ NUM_CHARTS_FIRST_ROW = 2
631
+ WIDTH_CHARTS_FIRST_ROW = (
632
+ (CONTENT_WIDTH_PAGE2 - CONTENT_OFFSET) - ((NUM_CHARTS_FIRST_ROW - 1) * TABLE_MARGIN_PAGE2)
633
+ ) / NUM_CHARTS_FIRST_ROW
634
+
635
+ max_height = max(
636
+ [
637
+ get_pie_chart_horizontal_height(geographical, legend_max_cols=10),
638
+ get_pie_chart_horizontal_height(currencies, legend_max_cols=10),
639
+ ]
640
+ )
641
+ geographical_pie_chart = get_pie_chart_horizontal(
642
+ geographical,
643
+ WIDTH_CHARTS_FIRST_ROW,
644
+ max_height,
645
+ general_data["colors"],
646
+ 4.23 * cm,
647
+ legend_max_cols=10,
648
+ legend_x=3.8 * cm,
649
+ )
650
+ currencies_pie_chart = get_pie_chart_horizontal(
651
+ currencies,
652
+ WIDTH_CHARTS_FIRST_ROW,
653
+ max_height,
654
+ general_data["colors"],
655
+ 4.23 * cm,
656
+ legend_max_cols=10,
657
+ legend_x=3.8 * cm,
658
+ )
659
+
660
+ max_height2 = max(
661
+ [
662
+ get_pie_chart_horizontal_height(liquidity, legend_max_cols=10),
663
+ get_pie_chart_horizontal_height(allocation, legend_max_cols=10),
664
+ ]
665
+ )
666
+ liquidity_pie_chart = get_pie_chart_horizontal(
667
+ liquidity,
668
+ WIDTH_CHARTS_FIRST_ROW,
669
+ max_height2,
670
+ general_data["colors"],
671
+ 4.23 * cm,
672
+ legend_max_cols=10,
673
+ legend_x=3.8 * cm,
674
+ )
675
+ allocation_pie_chart = get_pie_chart_horizontal(
676
+ allocation,
677
+ WIDTH_CHARTS_FIRST_ROW,
678
+ max_height2,
679
+ general_data["colors"],
680
+ 4.23 * cm,
681
+ legend_max_cols=10,
682
+ legend_x=3.8 * cm,
683
+ )
684
+
685
+ risk = RiskScale(
686
+ general_data["risk_scale"],
687
+ para_style=s_base_small_justified,
688
+ text=f"This risk ({calculated_risk_scale:.1f}) was calculated manually by weighing the risk of all implemented strategies.",
689
+ )
690
+
691
+ max_height3 = max([get_pie_chart_horizontal_height(marketcap, legend_max_cols=10), risk.height])
692
+ marketcap_pie_chart = get_pie_chart_horizontal(
693
+ marketcap,
694
+ WIDTH_CHARTS_FIRST_ROW,
695
+ max_height3,
696
+ general_data["colors"],
697
+ 4.23 * cm,
698
+ legend_max_cols=10,
699
+ legend_x=3.8 * cm,
700
+ )
701
+
702
+ third_td = [
703
+ [
704
+ Spacer(width=0, height=0),
705
+ Paragraph("<strong>Geographical<br />Breakdown</strong>", style=s_table_medium_leading),
706
+ Spacer(width=0, height=0),
707
+ Paragraph("<strong>Currency<br />Exposure</strong>", style=s_table_medium_leading),
708
+ ],
709
+ [
710
+ Spacer(width=0, height=0),
711
+ geographical_pie_chart,
712
+ Spacer(width=0, height=0),
713
+ currencies_pie_chart,
714
+ ],
715
+ [
716
+ Spacer(width=0, height=0),
717
+ Paragraph("<strong>Liquidity</strong>", style=s_table_medium_leading),
718
+ Spacer(width=0, height=0),
719
+ Paragraph("<strong>Asset<br />Allocation</strong>", style=s_table_medium_leading),
720
+ ],
721
+ [
722
+ Spacer(width=0, height=0),
723
+ liquidity_pie_chart,
724
+ Spacer(width=0, height=0),
725
+ allocation_pie_chart,
726
+ ],
727
+ [
728
+ Spacer(width=0, height=0),
729
+ Paragraph(
730
+ f"<strong>Market Cap.<br />Distributions ({context['currency']})</strong>",
731
+ style=s_table_medium_leading,
732
+ ),
733
+ Spacer(width=0, height=0),
734
+ Paragraph("<strong>Risk Scale</strong>", style=s_table_medium_leading),
735
+ ],
736
+ [Spacer(width=0, height=0), marketcap_pie_chart, Spacer(width=0, height=0), risk],
737
+ ]
738
+
739
+ cols = [CONTENT_OFFSET]
740
+ cols.extend([WIDTH_CHARTS_FIRST_ROW, TABLE_MARGIN_PAGE2] * NUM_CHARTS_FIRST_ROW)
741
+ cols.pop(-1)
742
+
743
+ third_t = Table(
744
+ third_td,
745
+ colWidths=cols,
746
+ rowHeights=[
747
+ 1.4 * cm,
748
+ max_height,
749
+ 1.4 * cm,
750
+ max_height2,
751
+ 1.4 * cm,
752
+ max_height3,
753
+ ],
754
+ )
755
+ third_table_styles = [
756
+ ("VALIGN", (0, 0), (-1, 0), "MIDDLE"),
757
+ ("VALIGN", (0, 2), (-1, 2), "MIDDLE"),
758
+ ("VALIGN", (0, 4), (-1, 4), "MIDDLE"),
759
+ ("LEFTPADDING", (0, 0), (-1, -1), 0),
760
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
761
+ ]
762
+
763
+ third_table_styles.extend(
764
+ [
765
+ ("LINEABOVE", (1, 0), (1, 0), 0.25, c_box_color),
766
+ ("LINEBELOW", (1, 0), (1, 0), 0.25, c_box_color),
767
+ ("LINEABOVE", (3, 0), (3, 0), 0.25, c_box_color),
768
+ ("LINEBELOW", (3, 0), (3, 0), 0.25, c_box_color),
769
+ ("LINEABOVE", (1, 2), (1, 2), 0.25, c_box_color),
770
+ ("LINEBELOW", (1, 2), (1, 2), 0.25, c_box_color),
771
+ ("LINEABOVE", (3, 2), (3, 2), 0.25, c_box_color),
772
+ ("LINEBELOW", (3, 2), (3, 2), 0.25, c_box_color),
773
+ ("LINEABOVE", (1, 4), (1, 4), 0.25, c_box_color),
774
+ ("LINEBELOW", (1, 4), (1, 4), 0.25, c_box_color),
775
+ ("LINEABOVE", (3, 4), (3, 4), 0.25, c_box_color),
776
+ ("LINEBELOW", (3, 4), (3, 4), 0.25, c_box_color),
777
+ ]
778
+ )
779
+
780
+ if debug:
781
+ third_table_styles.extend(
782
+ [
783
+ ("BOX", (0, 0), (-1, -1), 0.25, c_box_color),
784
+ ("INNERGRID", (0, 0), (-1, -1), 0.25, c_box_color),
785
+ ]
786
+ )
787
+
788
+ third_t.setStyle(TableStyle(third_table_styles))
789
+
790
+ elements.append(platypus.PageBreak("second_page"))
791
+ elements.append(generate_title(general_data["title"]))
792
+ elements.append(Spacer(height=LINEHEIGHT, width=0))
793
+ elements.append(third_t)
794
+
795
+ cols = [CONTENT_OFFSET]
796
+ cols.extend([WIDTH_CHARTS_FIRST_ROW, TABLE_MARGIN_PAGE2])
797
+ cols.pop(-1)
798
+
799
+ industry_height = get_bar_chart_height(industry)
800
+ industry_chart = get_bar_chart(industry, 17 * cm, industry_height - 30)
801
+
802
+ second_charts = [
803
+ [
804
+ Spacer(width=0, height=0),
805
+ Paragraph("<strong>Industry Exposure</strong>", style=s_table_medium_leading),
806
+ ],
807
+ [Spacer(width=0, height=0), industry_chart],
808
+ ]
809
+
810
+ col_width = CONTENT_WIDTH_PAGE2 - CONTENT_OFFSET - TABLE_MARGIN_PAGE2
811
+
812
+ other_table = Table(
813
+ second_charts,
814
+ colWidths=[CONTENT_OFFSET, col_width],
815
+ rowHeights=[
816
+ 0.85 * cm,
817
+ industry_height * 0.9,
818
+ ],
819
+ )
820
+
821
+ other_table_styles = [
822
+ ("VALIGN", (1, 1), (1, 1), "TOP"),
823
+ ("LINEABOVE", (1, 0), (1, 0), 0.25, c_box_color),
824
+ ("LINEBELOW", (1, 0), (1, 0), 0.25, c_box_color),
825
+ ]
826
+ if debug:
827
+ other_table_styles.extend(
828
+ [
829
+ ("BOX", (0, 0), (-1, -1), 0.25, c_box_color),
830
+ ("INNERGRID", (0, 0), (-1, -1), 0.25, c_box_color),
831
+ ]
832
+ )
833
+
834
+ other_table.setStyle(TableStyle(other_table_styles))
835
+ elements.append(platypus.PageBreak("second_page"))
836
+ elements.append(generate_title(general_data["title"]))
837
+ elements.append(Spacer(height=LINEHEIGHT, width=0))
838
+ elements.append(other_table)
839
+
840
+ elements.append(platypus.PageBreak("second_page"))
841
+ elements.append(generate_title(general_data["title"]))
842
+ elements.append(Spacer(height=LINEHEIGHT * 1, width=0))
843
+ elements.append(
844
+ get_fund_table(
845
+ df=fund_table,
846
+ width=20.0 * cm,
847
+ row_height=0.45 * cm,
848
+ header_style=s_table_center,
849
+ row_style=s_table_center,
850
+ data_style=s_table_right,
851
+ grid_color=c_table_border,
852
+ background_color=c_table_background,
853
+ offset=CONTENT_OFFSET,
854
+ debug=debug,
855
+ )
856
+ )
857
+ elements.append(Spacer(height=LINEHEIGHT * 1, width=0))
858
+
859
+ impress(elements)
860
+
861
+ doc.build(elements)
862
+ output.seek(0)
863
+
864
+ return output