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