dcfApp

  1import threading
  2import tkinter as tk
  3from tkinter import filedialog
  4import pandas as pd
  5import ttkthemes
  6from dcfArrayCalculator import DcfArrayCalculator
  7
  8
  9class DcfApp:
 10    def __init__(self, master):
 11        """
 12        Inicializa de DcfApp.
 13
 14        Args:
 15            master: El widget principal de la aplicación.
 16        """
 17        self.master = master
 18        """Segundo ventana principal de la aplicación"""
 19        self.data = []
 20        """Lista que contiene los datos de la tabla de resultados."""
 21        self.addRange = 1
 22        """Número de campos de entrada para el ticker de la empresa."""
 23        self.entries = []
 24        """Lista que contiene los campos de entrada para el ticker de la empresa."""
 25        self.ventana = master
 26        """Ventana principal de la aplicación."""
 27        self.replayDCF = 0
 28        """Número de veces que se ha ejecutado el cálculo DCF."""
 29        self.progress = None
 30        """Barra de progreso para el cálculo DCF."""
 31        self.buttonXLS = None
 32        """Botón para exportar a Excel."""
 33
 34        self.create_widgets()
 35
 36    def create_widgets(self):
 37        """
 38        Crea los widgets para la interfaz de la aplicación.
 39        """
 40        self.ventana.title("Calculadora de DCF")
 41        self.ventana.minsize(1500, 700)
 42        self.ventana.configure(background="white")
 43
 44        self.mainFrame = tk.Frame(self.ventana)
 45        self.mainFrame.pack(fill=tk.BOTH, expand=True)
 46
 47        self.myCanvas = tk.Canvas(self.mainFrame, bg="white")
 48        self.myCanvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
 49
 50        self.myScrollbar = tk.ttk.Scrollbar(
 51            self.mainFrame, orient=tk.VERTICAL, command=self.myCanvas.yview
 52        )
 53        self.myScrollbar.pack(side=tk.RIGHT, fill=tk.Y)
 54
 55        self.myScrollbarX = tk.ttk.Scrollbar(
 56            self.mainFrame, orient=tk.HORIZONTAL, command=self.myCanvas.xview
 57        )
 58        self.myScrollbarX.pack(side=tk.BOTTOM, fill=tk.X)
 59
 60        self.myCanvas.configure(yscrollcommand=self.myScrollbar.set)
 61        self.myCanvas.configure(xscrollcommand=self.myScrollbarX.set)
 62
 63        self.secondFrame = tk.Frame(self.myCanvas, bg="white")
 64
 65        self.title = tk.Label(
 66            self.secondFrame, text="Calculadora de DCF", font=("Arial", 20), bg="white"
 67        )
 68        self.title.pack()
 69
 70        self.frame = tk.Frame(self.secondFrame, width=1000, height=300, bg="white")
 71        self.frame.pack()
 72
 73        self.myCanvas.bind("<Configure>", self.center_window)
 74
 75        self.style = ttkthemes.ThemedStyle()
 76
 77        # self.exelFrame = tk.Frame(self.secondFrame, width=500, height=70, bg="white")
 78
 79        self.status = tk.Label(
 80            self.frame, bd=0, relief=tk.SUNKEN, anchor=tk.W, fg="red", bg="white"
 81        )
 82        self.status.pack()
 83
 84        self.boton1 = tk.Button(
 85            self.frame,
 86            text="Calcular DCF",
 87            width=20,
 88            height=2,
 89            bg="DeepSkyBlue3",
 90            fg="white",
 91        )
 92        self.boton1.pack()
 93        self.boton1.place(x=300, y=240)
 94        self.boton_agregar_excel = tk.Button(
 95            self.frame,
 96            text="Agregar desde Excel",
 97            width=20,
 98            height=2,
 99            bg="DeepSkyBlue3",
100            fg="white",
101        )
102
103        self.bottonDelete = tk.Button(
104            self.frame, text="-", width=2, height=1, bg="SkyBlue1", fg="white"
105        )
106        self.bottonDelete.pack()
107        self.bottonDelete.config(command=self.delete)
108        self.bottonDelete.place(x=240, y=50)
109        self.bottonDelete.config(state="disabled")
110
111        self.label1 = tk.Label(
112            self.frame, text="Introduce el ticker de la empresa", bg="white"
113        )
114        self.label1.pack()
115        self.label1.place(x=40, y=15)
116
117        tickerMessage = "El ticker de la empresa es el símbolo que se utiliza para identificar una empresa en el mercado de valores, \ncomo AAPL para Apple Inc. o MSFT para Microsoft Corporation."
118        self.label1.bind(
119            "<Enter>", lambda event: self.enter(event, tickerMessage, self.label1)
120        )
121        self.label1.bind("<Leave>", self.leave)
122
123        self.add()
124
125        self.bottonAdd = tk.Button(
126            self.frame, text="+", width=2, height=1, bg="DeepSkyBlue3", fg="white"
127        )
128        self.bottonAdd.pack()
129        self.bottonAdd.config(command=self.add)
130        self.bottonAdd.place(x=240, y=15)
131
132        self.label2 = tk.Label(
133            self.frame, text="Introduce la tasa libre de riesgo", bg="white"
134        )
135        self.label2.pack()
136        self.label2.place(x=285, y=15)
137
138        self.rfMessage = "La tasa libre de riesgo es el rendimiento que se espera de una inversión libre de riesgo, \ncomo los bonos del Tesoro de EE. UU. a 10 años (4%)"
139        self.label2.bind(
140            "<Enter>", lambda event: self.enter(event, self.rfMessage, self.label2)
141        )
142        self.label2.bind("<Leave>", self.leave)
143
144        self.validate_cmd = self.ventana.register(self.validar_input)
145
146        self.rf = tk.Entry(
147            self.frame,
148            width=30,
149            bg="gray90",
150            validate="key",
151            validatecommand=(self.validate_cmd, "%P"),
152            textvariable=tk.StringVar(self.ventana, "0.04"),
153        )
154        self.rf.pack()
155        self.rf.place(x=285, y=40)
156
157        self.label3 = tk.Label(
158            self.frame,
159            text="Introduce el rendimiento real\ndel mercado",
160            bg="white",
161            justify="left",
162        )
163        self.label3.pack()
164        self.label3.place(x=285, y=80)
165
166        self.rmMessage = "El rendimiento real del mercado es el rendimiento que se espera de una inversión en el mercado de valores, \ncomo el S&P 500 (10%)"
167        self.label3.bind(
168            "<Enter>", lambda event: self.enter(event, self.rmMessage, self.label3)
169        )
170        self.label3.bind("<Leave>", self.leave)
171
172        self.rm = tk.Entry(
173            self.frame,
174            width=30,
175            bg="gray90",
176            validate="key",
177            validatecommand=(self.validate_cmd, "%P"),
178            textvariable=tk.StringVar(self.ventana, "0.1"),
179        )
180        self.rm.pack()
181        self.rm.place(x=285, y=120)
182
183        self.label4 = tk.Label(
184            self.frame, text="Introduce el crecimiento perpetuo", bg="white"
185        )
186        self.label4.pack()
187        self.label4.place(x=285, y=160)
188
189        self.gMessage = "El crecimiento perpetuo es el crecimiento constante que se espera de una empresa a largo plazo, \ncomo el crecimiento del PIB (3%)"
190        self.label4.bind(
191            "<Enter>", lambda event: self.enter(event, self.gMessage, self.label4)
192        )
193        self.label4.bind("<Leave>", self.leave)
194
195        self.g = tk.Entry(
196            self.frame,
197            width=30,
198            bg="gray90",
199            validate="key",
200            validatecommand=(self.validate_cmd, "%P"),
201            textvariable=tk.StringVar(self.ventana, "0.03"),
202        )
203        self.g.pack()
204        self.g.place(x=285, y=185)
205
206        self.ebitda = tk.IntVar()
207        self.check = tk.Checkbutton(
208            self.frame,
209            text="Mostrar EBITDA",
210            bg="white",
211            variable=self.ebitda,
212            onvalue=1,
213            offvalue=0,
214        )
215        self.check.pack()
216        self.check.place(x=500, y=15)
217
218        self.ebitdaMessage = "El EBITDA (Earnings Before Interest, Taxes, Depreciation and Amortization) es una medida de la rentabilidad de una empresa, \nantes de intereses, impuestos, depreciación y amortización."
219        self.check.bind(
220            "<Enter>", lambda event: self.enter(event, self.ebitdaMessage, self.check)
221        )
222        self.check.bind("<Leave>", self.leave)
223
224        self.earnings = tk.IntVar()
225        self.check2 = tk.Checkbutton(
226            self.frame,
227            text="Mostrar margen de ganacias bruto",
228            variable=self.earnings,
229            onvalue=1,
230            bg="white",
231        )
232        self.check2.pack()
233        self.check2.place(x=500, y=55)
234
235        self.earningsMessage = "El margen de ganancias bruto es la relación entre las ganancias brutas y los ingresos totales. \nUn margen de ganancias bruto más alto puede indicar que la empresa es más eficiente."
236        self.check2.bind(
237            "<Enter>",
238            lambda event: self.enter(event, self.earningsMessage, self.check2),
239        )
240        self.check2.bind("<Leave>", self.leave)
241
242        self.roe = tk.IntVar()
243        self.check3 = tk.Checkbutton(
244            self.frame, text="Mostrar ROE", variable=self.roe, onvalue=1, bg="white"
245        )
246        self.check3.pack()
247        self.check3.place(x=500, y=95)
248
249        self.roeMessage = "El ROE (Return on Equity) es la relación entre las ganancias netas y el patrimonio neto de la empresa. \nUn ROE más alto puede indicar que la empresa es más eficiente."
250        self.check3.bind(
251            "<Enter>", lambda event: self.enter(event, self.roeMessage, self.check3)
252        )
253        self.check3.bind("<Leave>", self.leave)
254
255        self.per = tk.IntVar()
256        self.check4 = tk.Checkbutton(
257            self.frame, text="Mostrar el PER", variable=self.per, onvalue=1, bg="white"
258        )
259        self.check4.pack()
260        self.check4.place(x=500, y=135)
261
262        # Asociar el tooltip con el Checkbutton
263        self.perMessage = (
264            "El PER (Price Earnings Ratio) es la relación entre el precio de una acción y \n"
265            "las ganancias por acción de la empresa. Un PER más alto puede indicar \n"
266            "que la acción está sobrevalorada, mientras que un PER más bajo puede \n"
267            "indicar que la acción está infravalorada.\n"
268            " - PER bajo: 0-10\n"
269            " - PER medio: 10-20\n"
270            " - PER alto: +20"
271        )
272        self.check4.bind(
273            "<Enter>", lambda event: self.enter(event, self.perMessage, self.check4)
274        )
275        self.check4.bind("<Leave>", self.leave)
276
277        self.seeking = tk.IntVar()
278        self.check5 = tk.Checkbutton(
279            self.frame,
280            text="Calcular con el crecimiento de Seeking Alpha",
281            variable=self.seeking,
282            onvalue=1,
283            bg="white",
284        )
285        self.check5.pack()
286        self.check5.place(x=500, y=175)
287
288        self.seekingMessage = (
289            "El crecimiento de Seeking Alpha es el crecimiento que se espera de una empresa a largo plazo, \n"
290            "según el consenso de analistas de Seeking Alpha."
291        )
292        self.check5.bind(
293            "<Enter>", lambda event: self.enter(event, self.seekingMessage, self.check5)
294        )
295        self.check5.bind("<Leave>", self.leave)
296
297        self.zacks = tk.IntVar()
298        self.check6 = tk.Checkbutton(
299            self.frame,
300            text="Calcular con el crecimiento de Zacks",
301            variable=self.zacks,
302            onvalue=1,
303            bg="white",
304        )
305        self.check6.pack()
306        self.check6.place(x=710, y=15)
307
308        self.zacksMessage = (
309            "El crecimiento de Zacks es el crecimiento que se espera de una empresa a largo plazo, \n"
310            "según el consenso de analistas de Zacks."
311        )
312        self.check6.bind(
313            "<Enter>", lambda event: self.enter(event, self.zacksMessage, self.check6)
314        )
315        self.check6.bind("<Leave>", self.leave)
316
317        self.tableFrame = tk.Frame(self.secondFrame, width=500, height=70, bg="white")
318        self.tableFrame.pack(padx=(20, 0))
319
320        self.tableFrame2 = tk.Frame(self.secondFrame, width=500, height=70, bg="white")
321        self.tableFrame2.pack(padx=(20, 0), pady=(20, 0))
322
323        self.tableFrame3 = tk.Frame(self.secondFrame, width=500, height=70, bg="white")
324        self.tableFrame3.pack(padx=(20, 0), pady=(20, 0))
325
326        # self.exelFrame.pack()
327
328        self.boton1.config(command=self.threaded_create_table)
329
330        self.boton_agregar_excel.pack()
331        self.boton_agregar_excel.place(x=65, y=240)
332        self.boton_agregar_excel.config(command=self.agregar_datos_desde_excel)
333
334    def center_window(self, event=None):
335        """
336        Centra la ventana secundaria en el lienzo cuando se redimensiona.
337
338        Args:
339            event: El evento que activa la función (opcional).
340        """
341        self.myCanvas.update_idletasks()
342
343        canvas_width = self.myCanvas.winfo_width()
344        frame_width = self.secondFrame.winfo_reqwidth()
345        x = max((canvas_width - frame_width) / 2, 0)
346
347        self.myCanvas.create_window(x, 0, window=self.secondFrame, anchor="nw")
348        self.myCanvas.configure(scrollregion=self.myCanvas.bbox("all"))
349
350        self.myScrollbar.place(relx=1, rely=0, relheight=1, anchor="ne")
351        self.myCanvas.configure(xscrollcommand=self.myScrollbarX.set)
352        self.myScrollbarX.place(
353            x=0,
354            y=self.myCanvas.winfo_height(),
355            width=self.myCanvas.winfo_width(),
356            anchor="sw",
357        )
358
359    def validar_input(self, P):
360        """
361        Valida la entrada del usuario para asegurar que sea un número.
362
363        Args:
364            P: El valor a validar.
365
366        Returns:
367            True si la entrada es válida, False en caso contrario.
368        """
369        if P == "" or P.isdigit():
370            return True
371        try:
372            float(P)
373        except ValueError:
374            return False
375        return True
376
377    def add(self, nameCompany=None):
378        """
379        Agrega un campo de entrada para el ticker de la empresa.
380
381        Args:
382            nameCompany (str): El nombre de la empresa (opcional).
383        """
384        self.addRange += 1
385        self.entry = tk.Entry(self.frame, width=30, bg="gray90")
386        self.entry.pack()
387        self.entry.place(x=40, y=1 + (self.addRange - 1) * 40)
388        self.status.config(text="")
389
390        if nameCompany is not None:
391            self.entry.insert(0, nameCompany)
392        self.entries.append(self.entry)
393
394        if self.addRange > 5:
395            self.frame.configure(
396                height=self.frame.winfo_height()
397                + (40 if nameCompany is None else (self.addRange - 5) * 40)
398            )
399            self.status.place(x=100, y=220 + (self.addRange - 5) * 40)
400            self.boton1.place(x=300, y=240 + (self.addRange - 5) * 40)
401            self.boton_agregar_excel.place(x=65, y=240 + (self.addRange - 5) * 40)
402            self.myCanvas.configure(scrollregion=self.myCanvas.bbox("all"))
403        else:
404            self.status.place(x=293, y=220)
405
406        if self.addRange > 2:
407            self.bottonDelete.config(state="active", bg="DeepSkyBlue3", fg="white")
408
409    def delete(self):
410        """
411        Elimina el último campo de entrada para el ticker de la empresa.
412        """
413        self.status.config(text="")
414        if self.addRange > 2:
415            self.addRange -= 1
416            self.entries[-1].destroy()  # Destruir el último Entry
417            self.entries.pop()  # Eliminar referencia del Entry de la lista
418            if self.addRange > 4:
419                self.frame.configure(height=self.frame.winfo_height() - 40)
420                self.boton1.place(x=300, y=240 + (self.addRange - 5) * 40)
421                self.boton_agregar_excel.place(x=40, y=240 + (self.addRange - 5) * 40)
422                self.status.place(x=100, y=220 + (self.addRange - 5) * 40)
423                self.myCanvas.configure(scrollregion=self.myCanvas.bbox("all"))
424            else:
425                self.boton1.place(x=300, y=240)
426            if self.addRange <= 2:
427                self.bottonDelete.config(state="disabled")
428                self.bottonDelete.config(bg="SkyBlue1", fg="white")
429
430    def destroy_button(self):
431        """
432        Destruye el botón de archivo Excel si existe.
433        """
434        if self.buttonXLS:
435            self.buttonXLS.destroy()
436            self.buttonXLS = None
437
438    def show_button(self):
439        """
440        Muestra el botón para exportar a Excel.
441        """
442        self.buttonXLS = tk.Button(
443            self.frame,
444            text="Exportar a Excel",
445            width=20,
446            height=2,
447            bg="DeepSkyBlue3",
448            fg="white",
449        )
450        self.buttonXLS.pack()
451        self.buttonXLS.place(x=530, y=240)
452        self.buttonXLS.config(command=self.to_excel_or_csv)
453
454    def create_tables(self):
455        """
456        Crea las tablas con los resultados del cálculo DCF para cada opción de crecimiento.
457        """
458
459        frames = [self.tableFrame, self.tableFrame2, self.tableFrame3]
460        growthOptions = ["yahoo"]
461
462        framesOpt = [self.tableFrame, self.tableFrame2, self.tableFrame3]
463        for frameOpt in framesOpt:
464            for widget in frameOpt.winfo_children():
465                widget.destroy()
466
467        if self.seeking.get():
468            growthOptions.append("seeking")
469
470        if self.zacks.get():
471            growthOptions.append("zacks")
472
473        for i, option in enumerate(growthOptions):
474            self.create_table(option, frames[i])
475        growthOptions.clear()
476
477    def create_table(self, growthOption, frame):
478        """
479        Crea una tabla con los resultados del cálculo DCF.
480        """
481        # for widget in frame.winfo_children():
482        #     widget.destroy()
483
484        menssageOption = ""
485        if growthOption == "yahoo":
486            menssageOption = "Crecimiento de Yahoo Finance"
487        elif growthOption == "seeking":
488            menssageOption = "Crecimiento de Seeking Alpha"
489        elif growthOption == "zacks":
490            menssageOption = "Crecimiento de Zacks"
491
492        labelOptions = tk.Label(
493            frame, text=menssageOption, font=("Arial", 8), bg="white", justify="left"
494        )
495        labelOptions.pack()
496
497        entry_values = [entry.get() for entry in self.entries]
498
499        if entry_values.count("") > 0:
500            self.status.config(text="Debes llenar todos los campos")
501            if self.addRange > 5:
502                self.status.place(x=293, y=(220 + (self.addRange - 5) * 40))
503            else:
504                self.status.place(x=293, y=220)
505            return
506
507        if self.rf.get() == "" or self.rm.get() == "" or self.g.get() == "":
508            self.status.config(text="Debes llenar todos los campos")
509            if self.addRange > 5:
510                self.status.place(x=293, y=(220 + (self.addRange - 5) * 40))
511            else:
512                self.status.place(x=293, y=220)
513            return
514
515        self.boton1.config(state="disabled")
516        dataDcfResult = DcfArrayCalculator()
517        self.data = dataDcfResult.arrDcf(
518            entry_values,
519            float(self.g.get()),
520            float(self.rf.get()),
521            float(self.rm.get()),
522            self.ebitda.get(),
523            self.earnings.get(),
524            self.roe.get(),
525            self.per.get(),
526            growthOption,
527        )
528        # print(self.data)
529        tickersError = []
530        for i in range(len(self.data)):
531            if self.data[i][1] == "Error":
532                tickersError.append(self.data[i][0])
533
534        height = len(self.entries) - (len(tickersError))
535        if height > 0:
536            columns = (
537                13
538                + self.ebitda.get()
539                + self.earnings.get()
540                + self.roe.get()
541                + self.per.get()
542            )
543            table = tk.ttk.Treeview(
544                frame,
545                columns=tuple(range(1, columns + 1)),
546                show="headings",
547                height=height,
548            )
549
550            for i in range(1, columns + 1):
551                table.column(i, width=110, anchor=tk.CENTER)
552                table.heading(
553                    i,
554                    command=lambda _col=i: self.treeview_sort_column(
555                        table, _col, False
556                    ),
557                )
558
559            headings = [
560                "Ticker",
561                "WACC",
562                "FCF 2023",
563                "FCF 2024",
564                "FCF 2025",
565                "FCF 2026",
566                "FCF 2027",
567                "FCF 2028",
568                "% FCF",
569                "Valor de la empresa",
570                "Precio de la acción",
571                "Valor intrínseco",
572                "Diferencia",
573            ]
574            if self.ebitda.get() == 1:
575                headings.append("EBITDA")
576            if self.earnings.get() == 1:
577                headings.append("Margen de beneficio bruto")
578            if self.roe.get() == 1:
579                headings.append("ROE")
580            if self.per.get() == 1:
581                headings.append("PER")
582
583            for i, heading in enumerate(headings, start=1):
584                table.heading(i, text=heading)
585
586            table.pack(fill="both", expand=True)
587
588            self.style.configure("LightRed.TTreeview", background="#ffcccc")
589            self.style.configure("Red.TTreeview", background="#ff6666")
590            self.style.configure("DarkRed.TTreeview", background="#b30000")
591            self.style.configure("LightGreen.TTreeview", background="#ccffcc")
592            self.style.configure("Green.TTreeview", background="#66ff66")
593            self.style.configure("DarkGreen.TTreeview", background="#00b300")
594
595            for i, row in enumerate(self.data):
596                if row[1] != "Error":
597                    second_value = float(row[12].replace("%", ""))
598                    if second_value < 25:
599                        table.insert("", "end", values=row, tags=("DarkRed",))
600                    elif second_value < 50:
601                        table.insert("", "end", values=row, tags=("Red",))
602                    elif second_value < 100:
603                        table.insert("", "end", values=row, tags=("LightRed",))
604                    elif second_value > 300:
605                        table.insert("", "end", values=row, tags=("DarkGreen",))
606                    elif second_value > 200:
607                        table.insert("", "end", values=row, tags=("Green",))
608                    elif second_value > 100:
609                        table.insert("", "end", values=row, tags=("LightGreen",))
610                    else:
611                        table.insert("", "end", values=row)
612
613            table.tag_configure("LightRed", background="#ffcccc")
614            table.tag_configure("Red", background="#ff6666")
615            table.tag_configure("DarkRed", background="#b30000", foreground="white")
616            table.tag_configure("LightGreen", background="#ccffcc")
617            table.tag_configure("Green", background="#66ff66")
618            table.tag_configure("DarkGreen", background="#00b300", foreground="white")
619
620        if len(tickersError) > 0:
621            self.status.config(
622                text=f"Los siguientes tickers no se encontraron: {tickersError}"
623            )
624            if self.addRange > 5:
625                self.status.place(x=240, y=(220 + (self.addRange - 5) * 40))
626            else:
627                # lo colocamos en el centro de la ventana
628                self.status.place(
629                    x=((self.frame.winfo_width() / 2) - 50) - (40 * len(tickersError)),
630                    y=220,
631                )
632
633        self.myCanvas.update_idletasks()
634        self.center_window()
635
636        self.boton1.config(state="normal")
637
638    def treeview_sort_column(self, tv, col, reverse):
639        """
640        Ordena las columnas de la tabla.
641        Args:
642            tv (ttk.Treeview): El widget Treeview.
643            col (int): El índice de la columna a ordenar.
644            reverse (bool): True para orden descendente, False para orden ascendente.
645        """
646        l = [(tv.set(k, col), k) for k in tv.get_children("")]
647        l.sort(reverse=reverse)
648
649        for index, (val, k) in enumerate(l):
650            tv.move(k, "", index)
651
652        # Reverse sort next time.
653        tv.heading(col, command=lambda: self.treeview_sort_column(tv, col, not reverse))
654
655    def enter(self, event, message, label):
656        """
657        Muestra un tooltip cuando el cursor entra en el área del widget.
658
659        Args:
660            event: El evento de entrada que desencadenó la función.
661            message (str): El mensaje a mostrar en el tooltip.
662            label: El widget Label asociado al tooltip.
663        """
664        x, y, _, _ = self.check4.bbox("insert")
665        x += label.winfo_rootx() + 25
666        y += label.winfo_rooty() + 20
667        self.tooltip = tk.Toplevel(self.check4)
668        self.tooltip.wm_overrideredirect(True)
669        self.tooltip.wm_geometry(f"+{x}+{y}")
670        label = tk.Label(
671            self.tooltip,
672            text=message,
673            bg="white",
674            relief="solid",
675            borderwidth=1,
676            justify=tk.LEFT,
677        )
678        label.pack()
679
680    def leave(self, event):
681        """
682        Oculta el tooltip cuando el cursor sale del área del widget.
683        """
684        if self.tooltip:
685            self.tooltip.destroy()
686
687    def threaded_create_table(self):
688        """
689        Inicia un hilo para crear la tabla de resultados de DCF.
690        """
691        self.replayDCF += 1
692        self.progressFrame = tk.Frame(
693            self.ventana, bg="white", borderwidth=2, relief="solid"
694        )
695        self.progressFrame.config(width=300, height=400)
696        self.progressFrame.pack()
697        self.progressMessage = tk.Label(
698            self.progressFrame, text="Cargando...\n Espere por favor", bg="white"
699        )
700        self.progressMessage.pack()
701        self.progressMessage.config(width=20, height=5)
702        self.progress = tk.ttk.Progressbar(
703            self.progressFrame, length=200, mode="indeterminate"
704        )
705        self.progress.pack(padx=10, pady=(0, 10))
706        if self.replayDCF > 1:
707            self.destroy_button()
708
709        self.progressFrame.place(
710            x=self.ventana.winfo_width() / 2 - 50,
711            y=self.ventana.winfo_height() / 2 - 50,
712        )
713        self.progress.start()
714        thread = threading.Thread(target=self.create_tables)
715        thread.start()
716        self.ventana.after(100, self.check_thread, thread)
717
718    def check_thread(self, thread):
719        """
720        Verifica periódicamente si el hilo de creación de tabla está vivo.
721        """
722        if thread.is_alive():
723            self.ventana.after(100, self.check_thread, thread)
724        else:
725            if self.progress is not None:
726                self.progress.stop()
727                self.progress.destroy()
728                self.progressFrame.destroy()
729                progress = None
730            if not self.data or (len(self.data) == 1 and self.data[0][1] == "Error"):
731                self.destroy_button()
732            else:
733                self.show_button()
734
735    def to_excel_or_csv(self):
736        """Función para exportar los datos a un archivo Excel o CSV"""
737        options = []
738        if self.ebitda.get() == 1:
739            options.append("EBITDA")
740        if self.earnings.get() == 1:
741            options.append("Margen de ganacia bruto")
742        if self.roe.get() == 1:
743            options.append("ROE")
744        if self.per.get() == 1:
745            options.append("PER")
746
747        df_result = pd.DataFrame(
748            self.data,
749            columns=[
750                "Ticker",
751                "WACC",
752                "FCF 2023",
753                "FCF 2024",
754                "FCF 2025",
755                "FCF 2026",
756                "FCF 2027",
757                "FCF 2028",
758                "% FCN",
759                "Valor de la empresa",
760                "Precio de la acción",
761                "Valor intrínseco de la acción",
762                "Diferencia",
763            ]
764            + options,
765        )
766
767        root = tk.Tk()
768        root.withdraw()
769
770        file_path = filedialog.asksaveasfilename(
771            defaultextension=".xlsx",
772            filetypes=[("Excel files", "*.xlsx")],
773            initialfile="dcf",
774        )
775
776        if file_path:
777            df_result.to_excel(file_path, index=False)
778
779    def agregar_datos_desde_excel(self):
780        """
781        Agrega tickers desde un archivo de Excel al formulario.
782        """
783        for i in range(self.addRange):
784            self.delete()
785
786        try:
787            ruta_archivo = filedialog.askopenfilename(
788                filetypes=[
789                    ("Archivos de Excel", "*.xlsx"),
790                    ("Todos los archivos", "*.*"),
791                ]
792            )
793            if ruta_archivo:
794                self.procesar_datos_desde_excel(ruta_archivo)
795        except Exception as e:
796            print("Ocurrió un error al abrir el archivo:", e)
797        self.myCanvas.update_idletasks()
798        self.center_window()
799
800    def procesar_datos_desde_excel(self, ruta_archivo):
801        """
802        Procesa los datos desde un archivo de Excel y los agrega al formulario.
803
804        Parameters:
805            ruta_archivo (str): La ruta del archivo de Excel.
806        """
807        try:
808            self.datos_excel = pd.read_excel(ruta_archivo)
809            self.tickers = []
810            self.tickers.append(self.datos_excel.columns[0])
811
812            for i in range(len(self.datos_excel[self.datos_excel.columns[0]])):
813                self.tickers.append(self.datos_excel[self.datos_excel.columns[0]][i])
814
815            if len(self.entries) > 1 and self.entries[-1] != "":
816                self.addRange = self.addRange - len(self.entries) + 1
817            else:
818                self.addRange = 1
819                self.entries = []
820
821            for ticker in self.tickers:
822                self.add(ticker)
823        except Exception as e:
824            print(e)
825            self.status.config(text="Ocurrió un error al procesar el archivo Excel")
826
827
828def main():
829    """
830    Función principal para iniciar la aplicación.
831    """
832    root = tk.Tk()
833    app = DcfApp(root)
834    root.mainloop()
835
836
837if __name__ == "__main__":
838    main()
class DcfApp:
 10class DcfApp:
 11    def __init__(self, master):
 12        """
 13        Inicializa de DcfApp.
 14
 15        Args:
 16            master: El widget principal de la aplicación.
 17        """
 18        self.master = master
 19        """Segundo ventana principal de la aplicación"""
 20        self.data = []
 21        """Lista que contiene los datos de la tabla de resultados."""
 22        self.addRange = 1
 23        """Número de campos de entrada para el ticker de la empresa."""
 24        self.entries = []
 25        """Lista que contiene los campos de entrada para el ticker de la empresa."""
 26        self.ventana = master
 27        """Ventana principal de la aplicación."""
 28        self.replayDCF = 0
 29        """Número de veces que se ha ejecutado el cálculo DCF."""
 30        self.progress = None
 31        """Barra de progreso para el cálculo DCF."""
 32        self.buttonXLS = None
 33        """Botón para exportar a Excel."""
 34
 35        self.create_widgets()
 36
 37    def create_widgets(self):
 38        """
 39        Crea los widgets para la interfaz de la aplicación.
 40        """
 41        self.ventana.title("Calculadora de DCF")
 42        self.ventana.minsize(1500, 700)
 43        self.ventana.configure(background="white")
 44
 45        self.mainFrame = tk.Frame(self.ventana)
 46        self.mainFrame.pack(fill=tk.BOTH, expand=True)
 47
 48        self.myCanvas = tk.Canvas(self.mainFrame, bg="white")
 49        self.myCanvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
 50
 51        self.myScrollbar = tk.ttk.Scrollbar(
 52            self.mainFrame, orient=tk.VERTICAL, command=self.myCanvas.yview
 53        )
 54        self.myScrollbar.pack(side=tk.RIGHT, fill=tk.Y)
 55
 56        self.myScrollbarX = tk.ttk.Scrollbar(
 57            self.mainFrame, orient=tk.HORIZONTAL, command=self.myCanvas.xview
 58        )
 59        self.myScrollbarX.pack(side=tk.BOTTOM, fill=tk.X)
 60
 61        self.myCanvas.configure(yscrollcommand=self.myScrollbar.set)
 62        self.myCanvas.configure(xscrollcommand=self.myScrollbarX.set)
 63
 64        self.secondFrame = tk.Frame(self.myCanvas, bg="white")
 65
 66        self.title = tk.Label(
 67            self.secondFrame, text="Calculadora de DCF", font=("Arial", 20), bg="white"
 68        )
 69        self.title.pack()
 70
 71        self.frame = tk.Frame(self.secondFrame, width=1000, height=300, bg="white")
 72        self.frame.pack()
 73
 74        self.myCanvas.bind("<Configure>", self.center_window)
 75
 76        self.style = ttkthemes.ThemedStyle()
 77
 78        # self.exelFrame = tk.Frame(self.secondFrame, width=500, height=70, bg="white")
 79
 80        self.status = tk.Label(
 81            self.frame, bd=0, relief=tk.SUNKEN, anchor=tk.W, fg="red", bg="white"
 82        )
 83        self.status.pack()
 84
 85        self.boton1 = tk.Button(
 86            self.frame,
 87            text="Calcular DCF",
 88            width=20,
 89            height=2,
 90            bg="DeepSkyBlue3",
 91            fg="white",
 92        )
 93        self.boton1.pack()
 94        self.boton1.place(x=300, y=240)
 95        self.boton_agregar_excel = tk.Button(
 96            self.frame,
 97            text="Agregar desde Excel",
 98            width=20,
 99            height=2,
100            bg="DeepSkyBlue3",
101            fg="white",
102        )
103
104        self.bottonDelete = tk.Button(
105            self.frame, text="-", width=2, height=1, bg="SkyBlue1", fg="white"
106        )
107        self.bottonDelete.pack()
108        self.bottonDelete.config(command=self.delete)
109        self.bottonDelete.place(x=240, y=50)
110        self.bottonDelete.config(state="disabled")
111
112        self.label1 = tk.Label(
113            self.frame, text="Introduce el ticker de la empresa", bg="white"
114        )
115        self.label1.pack()
116        self.label1.place(x=40, y=15)
117
118        tickerMessage = "El ticker de la empresa es el símbolo que se utiliza para identificar una empresa en el mercado de valores, \ncomo AAPL para Apple Inc. o MSFT para Microsoft Corporation."
119        self.label1.bind(
120            "<Enter>", lambda event: self.enter(event, tickerMessage, self.label1)
121        )
122        self.label1.bind("<Leave>", self.leave)
123
124        self.add()
125
126        self.bottonAdd = tk.Button(
127            self.frame, text="+", width=2, height=1, bg="DeepSkyBlue3", fg="white"
128        )
129        self.bottonAdd.pack()
130        self.bottonAdd.config(command=self.add)
131        self.bottonAdd.place(x=240, y=15)
132
133        self.label2 = tk.Label(
134            self.frame, text="Introduce la tasa libre de riesgo", bg="white"
135        )
136        self.label2.pack()
137        self.label2.place(x=285, y=15)
138
139        self.rfMessage = "La tasa libre de riesgo es el rendimiento que se espera de una inversión libre de riesgo, \ncomo los bonos del Tesoro de EE. UU. a 10 años (4%)"
140        self.label2.bind(
141            "<Enter>", lambda event: self.enter(event, self.rfMessage, self.label2)
142        )
143        self.label2.bind("<Leave>", self.leave)
144
145        self.validate_cmd = self.ventana.register(self.validar_input)
146
147        self.rf = tk.Entry(
148            self.frame,
149            width=30,
150            bg="gray90",
151            validate="key",
152            validatecommand=(self.validate_cmd, "%P"),
153            textvariable=tk.StringVar(self.ventana, "0.04"),
154        )
155        self.rf.pack()
156        self.rf.place(x=285, y=40)
157
158        self.label3 = tk.Label(
159            self.frame,
160            text="Introduce el rendimiento real\ndel mercado",
161            bg="white",
162            justify="left",
163        )
164        self.label3.pack()
165        self.label3.place(x=285, y=80)
166
167        self.rmMessage = "El rendimiento real del mercado es el rendimiento que se espera de una inversión en el mercado de valores, \ncomo el S&P 500 (10%)"
168        self.label3.bind(
169            "<Enter>", lambda event: self.enter(event, self.rmMessage, self.label3)
170        )
171        self.label3.bind("<Leave>", self.leave)
172
173        self.rm = tk.Entry(
174            self.frame,
175            width=30,
176            bg="gray90",
177            validate="key",
178            validatecommand=(self.validate_cmd, "%P"),
179            textvariable=tk.StringVar(self.ventana, "0.1"),
180        )
181        self.rm.pack()
182        self.rm.place(x=285, y=120)
183
184        self.label4 = tk.Label(
185            self.frame, text="Introduce el crecimiento perpetuo", bg="white"
186        )
187        self.label4.pack()
188        self.label4.place(x=285, y=160)
189
190        self.gMessage = "El crecimiento perpetuo es el crecimiento constante que se espera de una empresa a largo plazo, \ncomo el crecimiento del PIB (3%)"
191        self.label4.bind(
192            "<Enter>", lambda event: self.enter(event, self.gMessage, self.label4)
193        )
194        self.label4.bind("<Leave>", self.leave)
195
196        self.g = tk.Entry(
197            self.frame,
198            width=30,
199            bg="gray90",
200            validate="key",
201            validatecommand=(self.validate_cmd, "%P"),
202            textvariable=tk.StringVar(self.ventana, "0.03"),
203        )
204        self.g.pack()
205        self.g.place(x=285, y=185)
206
207        self.ebitda = tk.IntVar()
208        self.check = tk.Checkbutton(
209            self.frame,
210            text="Mostrar EBITDA",
211            bg="white",
212            variable=self.ebitda,
213            onvalue=1,
214            offvalue=0,
215        )
216        self.check.pack()
217        self.check.place(x=500, y=15)
218
219        self.ebitdaMessage = "El EBITDA (Earnings Before Interest, Taxes, Depreciation and Amortization) es una medida de la rentabilidad de una empresa, \nantes de intereses, impuestos, depreciación y amortización."
220        self.check.bind(
221            "<Enter>", lambda event: self.enter(event, self.ebitdaMessage, self.check)
222        )
223        self.check.bind("<Leave>", self.leave)
224
225        self.earnings = tk.IntVar()
226        self.check2 = tk.Checkbutton(
227            self.frame,
228            text="Mostrar margen de ganacias bruto",
229            variable=self.earnings,
230            onvalue=1,
231            bg="white",
232        )
233        self.check2.pack()
234        self.check2.place(x=500, y=55)
235
236        self.earningsMessage = "El margen de ganancias bruto es la relación entre las ganancias brutas y los ingresos totales. \nUn margen de ganancias bruto más alto puede indicar que la empresa es más eficiente."
237        self.check2.bind(
238            "<Enter>",
239            lambda event: self.enter(event, self.earningsMessage, self.check2),
240        )
241        self.check2.bind("<Leave>", self.leave)
242
243        self.roe = tk.IntVar()
244        self.check3 = tk.Checkbutton(
245            self.frame, text="Mostrar ROE", variable=self.roe, onvalue=1, bg="white"
246        )
247        self.check3.pack()
248        self.check3.place(x=500, y=95)
249
250        self.roeMessage = "El ROE (Return on Equity) es la relación entre las ganancias netas y el patrimonio neto de la empresa. \nUn ROE más alto puede indicar que la empresa es más eficiente."
251        self.check3.bind(
252            "<Enter>", lambda event: self.enter(event, self.roeMessage, self.check3)
253        )
254        self.check3.bind("<Leave>", self.leave)
255
256        self.per = tk.IntVar()
257        self.check4 = tk.Checkbutton(
258            self.frame, text="Mostrar el PER", variable=self.per, onvalue=1, bg="white"
259        )
260        self.check4.pack()
261        self.check4.place(x=500, y=135)
262
263        # Asociar el tooltip con el Checkbutton
264        self.perMessage = (
265            "El PER (Price Earnings Ratio) es la relación entre el precio de una acción y \n"
266            "las ganancias por acción de la empresa. Un PER más alto puede indicar \n"
267            "que la acción está sobrevalorada, mientras que un PER más bajo puede \n"
268            "indicar que la acción está infravalorada.\n"
269            " - PER bajo: 0-10\n"
270            " - PER medio: 10-20\n"
271            " - PER alto: +20"
272        )
273        self.check4.bind(
274            "<Enter>", lambda event: self.enter(event, self.perMessage, self.check4)
275        )
276        self.check4.bind("<Leave>", self.leave)
277
278        self.seeking = tk.IntVar()
279        self.check5 = tk.Checkbutton(
280            self.frame,
281            text="Calcular con el crecimiento de Seeking Alpha",
282            variable=self.seeking,
283            onvalue=1,
284            bg="white",
285        )
286        self.check5.pack()
287        self.check5.place(x=500, y=175)
288
289        self.seekingMessage = (
290            "El crecimiento de Seeking Alpha es el crecimiento que se espera de una empresa a largo plazo, \n"
291            "según el consenso de analistas de Seeking Alpha."
292        )
293        self.check5.bind(
294            "<Enter>", lambda event: self.enter(event, self.seekingMessage, self.check5)
295        )
296        self.check5.bind("<Leave>", self.leave)
297
298        self.zacks = tk.IntVar()
299        self.check6 = tk.Checkbutton(
300            self.frame,
301            text="Calcular con el crecimiento de Zacks",
302            variable=self.zacks,
303            onvalue=1,
304            bg="white",
305        )
306        self.check6.pack()
307        self.check6.place(x=710, y=15)
308
309        self.zacksMessage = (
310            "El crecimiento de Zacks es el crecimiento que se espera de una empresa a largo plazo, \n"
311            "según el consenso de analistas de Zacks."
312        )
313        self.check6.bind(
314            "<Enter>", lambda event: self.enter(event, self.zacksMessage, self.check6)
315        )
316        self.check6.bind("<Leave>", self.leave)
317
318        self.tableFrame = tk.Frame(self.secondFrame, width=500, height=70, bg="white")
319        self.tableFrame.pack(padx=(20, 0))
320
321        self.tableFrame2 = tk.Frame(self.secondFrame, width=500, height=70, bg="white")
322        self.tableFrame2.pack(padx=(20, 0), pady=(20, 0))
323
324        self.tableFrame3 = tk.Frame(self.secondFrame, width=500, height=70, bg="white")
325        self.tableFrame3.pack(padx=(20, 0), pady=(20, 0))
326
327        # self.exelFrame.pack()
328
329        self.boton1.config(command=self.threaded_create_table)
330
331        self.boton_agregar_excel.pack()
332        self.boton_agregar_excel.place(x=65, y=240)
333        self.boton_agregar_excel.config(command=self.agregar_datos_desde_excel)
334
335    def center_window(self, event=None):
336        """
337        Centra la ventana secundaria en el lienzo cuando se redimensiona.
338
339        Args:
340            event: El evento que activa la función (opcional).
341        """
342        self.myCanvas.update_idletasks()
343
344        canvas_width = self.myCanvas.winfo_width()
345        frame_width = self.secondFrame.winfo_reqwidth()
346        x = max((canvas_width - frame_width) / 2, 0)
347
348        self.myCanvas.create_window(x, 0, window=self.secondFrame, anchor="nw")
349        self.myCanvas.configure(scrollregion=self.myCanvas.bbox("all"))
350
351        self.myScrollbar.place(relx=1, rely=0, relheight=1, anchor="ne")
352        self.myCanvas.configure(xscrollcommand=self.myScrollbarX.set)
353        self.myScrollbarX.place(
354            x=0,
355            y=self.myCanvas.winfo_height(),
356            width=self.myCanvas.winfo_width(),
357            anchor="sw",
358        )
359
360    def validar_input(self, P):
361        """
362        Valida la entrada del usuario para asegurar que sea un número.
363
364        Args:
365            P: El valor a validar.
366
367        Returns:
368            True si la entrada es válida, False en caso contrario.
369        """
370        if P == "" or P.isdigit():
371            return True
372        try:
373            float(P)
374        except ValueError:
375            return False
376        return True
377
378    def add(self, nameCompany=None):
379        """
380        Agrega un campo de entrada para el ticker de la empresa.
381
382        Args:
383            nameCompany (str): El nombre de la empresa (opcional).
384        """
385        self.addRange += 1
386        self.entry = tk.Entry(self.frame, width=30, bg="gray90")
387        self.entry.pack()
388        self.entry.place(x=40, y=1 + (self.addRange - 1) * 40)
389        self.status.config(text="")
390
391        if nameCompany is not None:
392            self.entry.insert(0, nameCompany)
393        self.entries.append(self.entry)
394
395        if self.addRange > 5:
396            self.frame.configure(
397                height=self.frame.winfo_height()
398                + (40 if nameCompany is None else (self.addRange - 5) * 40)
399            )
400            self.status.place(x=100, y=220 + (self.addRange - 5) * 40)
401            self.boton1.place(x=300, y=240 + (self.addRange - 5) * 40)
402            self.boton_agregar_excel.place(x=65, y=240 + (self.addRange - 5) * 40)
403            self.myCanvas.configure(scrollregion=self.myCanvas.bbox("all"))
404        else:
405            self.status.place(x=293, y=220)
406
407        if self.addRange > 2:
408            self.bottonDelete.config(state="active", bg="DeepSkyBlue3", fg="white")
409
410    def delete(self):
411        """
412        Elimina el último campo de entrada para el ticker de la empresa.
413        """
414        self.status.config(text="")
415        if self.addRange > 2:
416            self.addRange -= 1
417            self.entries[-1].destroy()  # Destruir el último Entry
418            self.entries.pop()  # Eliminar referencia del Entry de la lista
419            if self.addRange > 4:
420                self.frame.configure(height=self.frame.winfo_height() - 40)
421                self.boton1.place(x=300, y=240 + (self.addRange - 5) * 40)
422                self.boton_agregar_excel.place(x=40, y=240 + (self.addRange - 5) * 40)
423                self.status.place(x=100, y=220 + (self.addRange - 5) * 40)
424                self.myCanvas.configure(scrollregion=self.myCanvas.bbox("all"))
425            else:
426                self.boton1.place(x=300, y=240)
427            if self.addRange <= 2:
428                self.bottonDelete.config(state="disabled")
429                self.bottonDelete.config(bg="SkyBlue1", fg="white")
430
431    def destroy_button(self):
432        """
433        Destruye el botón de archivo Excel si existe.
434        """
435        if self.buttonXLS:
436            self.buttonXLS.destroy()
437            self.buttonXLS = None
438
439    def show_button(self):
440        """
441        Muestra el botón para exportar a Excel.
442        """
443        self.buttonXLS = tk.Button(
444            self.frame,
445            text="Exportar a Excel",
446            width=20,
447            height=2,
448            bg="DeepSkyBlue3",
449            fg="white",
450        )
451        self.buttonXLS.pack()
452        self.buttonXLS.place(x=530, y=240)
453        self.buttonXLS.config(command=self.to_excel_or_csv)
454
455    def create_tables(self):
456        """
457        Crea las tablas con los resultados del cálculo DCF para cada opción de crecimiento.
458        """
459
460        frames = [self.tableFrame, self.tableFrame2, self.tableFrame3]
461        growthOptions = ["yahoo"]
462
463        framesOpt = [self.tableFrame, self.tableFrame2, self.tableFrame3]
464        for frameOpt in framesOpt:
465            for widget in frameOpt.winfo_children():
466                widget.destroy()
467
468        if self.seeking.get():
469            growthOptions.append("seeking")
470
471        if self.zacks.get():
472            growthOptions.append("zacks")
473
474        for i, option in enumerate(growthOptions):
475            self.create_table(option, frames[i])
476        growthOptions.clear()
477
478    def create_table(self, growthOption, frame):
479        """
480        Crea una tabla con los resultados del cálculo DCF.
481        """
482        # for widget in frame.winfo_children():
483        #     widget.destroy()
484
485        menssageOption = ""
486        if growthOption == "yahoo":
487            menssageOption = "Crecimiento de Yahoo Finance"
488        elif growthOption == "seeking":
489            menssageOption = "Crecimiento de Seeking Alpha"
490        elif growthOption == "zacks":
491            menssageOption = "Crecimiento de Zacks"
492
493        labelOptions = tk.Label(
494            frame, text=menssageOption, font=("Arial", 8), bg="white", justify="left"
495        )
496        labelOptions.pack()
497
498        entry_values = [entry.get() for entry in self.entries]
499
500        if entry_values.count("") > 0:
501            self.status.config(text="Debes llenar todos los campos")
502            if self.addRange > 5:
503                self.status.place(x=293, y=(220 + (self.addRange - 5) * 40))
504            else:
505                self.status.place(x=293, y=220)
506            return
507
508        if self.rf.get() == "" or self.rm.get() == "" or self.g.get() == "":
509            self.status.config(text="Debes llenar todos los campos")
510            if self.addRange > 5:
511                self.status.place(x=293, y=(220 + (self.addRange - 5) * 40))
512            else:
513                self.status.place(x=293, y=220)
514            return
515
516        self.boton1.config(state="disabled")
517        dataDcfResult = DcfArrayCalculator()
518        self.data = dataDcfResult.arrDcf(
519            entry_values,
520            float(self.g.get()),
521            float(self.rf.get()),
522            float(self.rm.get()),
523            self.ebitda.get(),
524            self.earnings.get(),
525            self.roe.get(),
526            self.per.get(),
527            growthOption,
528        )
529        # print(self.data)
530        tickersError = []
531        for i in range(len(self.data)):
532            if self.data[i][1] == "Error":
533                tickersError.append(self.data[i][0])
534
535        height = len(self.entries) - (len(tickersError))
536        if height > 0:
537            columns = (
538                13
539                + self.ebitda.get()
540                + self.earnings.get()
541                + self.roe.get()
542                + self.per.get()
543            )
544            table = tk.ttk.Treeview(
545                frame,
546                columns=tuple(range(1, columns + 1)),
547                show="headings",
548                height=height,
549            )
550
551            for i in range(1, columns + 1):
552                table.column(i, width=110, anchor=tk.CENTER)
553                table.heading(
554                    i,
555                    command=lambda _col=i: self.treeview_sort_column(
556                        table, _col, False
557                    ),
558                )
559
560            headings = [
561                "Ticker",
562                "WACC",
563                "FCF 2023",
564                "FCF 2024",
565                "FCF 2025",
566                "FCF 2026",
567                "FCF 2027",
568                "FCF 2028",
569                "% FCF",
570                "Valor de la empresa",
571                "Precio de la acción",
572                "Valor intrínseco",
573                "Diferencia",
574            ]
575            if self.ebitda.get() == 1:
576                headings.append("EBITDA")
577            if self.earnings.get() == 1:
578                headings.append("Margen de beneficio bruto")
579            if self.roe.get() == 1:
580                headings.append("ROE")
581            if self.per.get() == 1:
582                headings.append("PER")
583
584            for i, heading in enumerate(headings, start=1):
585                table.heading(i, text=heading)
586
587            table.pack(fill="both", expand=True)
588
589            self.style.configure("LightRed.TTreeview", background="#ffcccc")
590            self.style.configure("Red.TTreeview", background="#ff6666")
591            self.style.configure("DarkRed.TTreeview", background="#b30000")
592            self.style.configure("LightGreen.TTreeview", background="#ccffcc")
593            self.style.configure("Green.TTreeview", background="#66ff66")
594            self.style.configure("DarkGreen.TTreeview", background="#00b300")
595
596            for i, row in enumerate(self.data):
597                if row[1] != "Error":
598                    second_value = float(row[12].replace("%", ""))
599                    if second_value < 25:
600                        table.insert("", "end", values=row, tags=("DarkRed",))
601                    elif second_value < 50:
602                        table.insert("", "end", values=row, tags=("Red",))
603                    elif second_value < 100:
604                        table.insert("", "end", values=row, tags=("LightRed",))
605                    elif second_value > 300:
606                        table.insert("", "end", values=row, tags=("DarkGreen",))
607                    elif second_value > 200:
608                        table.insert("", "end", values=row, tags=("Green",))
609                    elif second_value > 100:
610                        table.insert("", "end", values=row, tags=("LightGreen",))
611                    else:
612                        table.insert("", "end", values=row)
613
614            table.tag_configure("LightRed", background="#ffcccc")
615            table.tag_configure("Red", background="#ff6666")
616            table.tag_configure("DarkRed", background="#b30000", foreground="white")
617            table.tag_configure("LightGreen", background="#ccffcc")
618            table.tag_configure("Green", background="#66ff66")
619            table.tag_configure("DarkGreen", background="#00b300", foreground="white")
620
621        if len(tickersError) > 0:
622            self.status.config(
623                text=f"Los siguientes tickers no se encontraron: {tickersError}"
624            )
625            if self.addRange > 5:
626                self.status.place(x=240, y=(220 + (self.addRange - 5) * 40))
627            else:
628                # lo colocamos en el centro de la ventana
629                self.status.place(
630                    x=((self.frame.winfo_width() / 2) - 50) - (40 * len(tickersError)),
631                    y=220,
632                )
633
634        self.myCanvas.update_idletasks()
635        self.center_window()
636
637        self.boton1.config(state="normal")
638
639    def treeview_sort_column(self, tv, col, reverse):
640        """
641        Ordena las columnas de la tabla.
642        Args:
643            tv (ttk.Treeview): El widget Treeview.
644            col (int): El índice de la columna a ordenar.
645            reverse (bool): True para orden descendente, False para orden ascendente.
646        """
647        l = [(tv.set(k, col), k) for k in tv.get_children("")]
648        l.sort(reverse=reverse)
649
650        for index, (val, k) in enumerate(l):
651            tv.move(k, "", index)
652
653        # Reverse sort next time.
654        tv.heading(col, command=lambda: self.treeview_sort_column(tv, col, not reverse))
655
656    def enter(self, event, message, label):
657        """
658        Muestra un tooltip cuando el cursor entra en el área del widget.
659
660        Args:
661            event: El evento de entrada que desencadenó la función.
662            message (str): El mensaje a mostrar en el tooltip.
663            label: El widget Label asociado al tooltip.
664        """
665        x, y, _, _ = self.check4.bbox("insert")
666        x += label.winfo_rootx() + 25
667        y += label.winfo_rooty() + 20
668        self.tooltip = tk.Toplevel(self.check4)
669        self.tooltip.wm_overrideredirect(True)
670        self.tooltip.wm_geometry(f"+{x}+{y}")
671        label = tk.Label(
672            self.tooltip,
673            text=message,
674            bg="white",
675            relief="solid",
676            borderwidth=1,
677            justify=tk.LEFT,
678        )
679        label.pack()
680
681    def leave(self, event):
682        """
683        Oculta el tooltip cuando el cursor sale del área del widget.
684        """
685        if self.tooltip:
686            self.tooltip.destroy()
687
688    def threaded_create_table(self):
689        """
690        Inicia un hilo para crear la tabla de resultados de DCF.
691        """
692        self.replayDCF += 1
693        self.progressFrame = tk.Frame(
694            self.ventana, bg="white", borderwidth=2, relief="solid"
695        )
696        self.progressFrame.config(width=300, height=400)
697        self.progressFrame.pack()
698        self.progressMessage = tk.Label(
699            self.progressFrame, text="Cargando...\n Espere por favor", bg="white"
700        )
701        self.progressMessage.pack()
702        self.progressMessage.config(width=20, height=5)
703        self.progress = tk.ttk.Progressbar(
704            self.progressFrame, length=200, mode="indeterminate"
705        )
706        self.progress.pack(padx=10, pady=(0, 10))
707        if self.replayDCF > 1:
708            self.destroy_button()
709
710        self.progressFrame.place(
711            x=self.ventana.winfo_width() / 2 - 50,
712            y=self.ventana.winfo_height() / 2 - 50,
713        )
714        self.progress.start()
715        thread = threading.Thread(target=self.create_tables)
716        thread.start()
717        self.ventana.after(100, self.check_thread, thread)
718
719    def check_thread(self, thread):
720        """
721        Verifica periódicamente si el hilo de creación de tabla está vivo.
722        """
723        if thread.is_alive():
724            self.ventana.after(100, self.check_thread, thread)
725        else:
726            if self.progress is not None:
727                self.progress.stop()
728                self.progress.destroy()
729                self.progressFrame.destroy()
730                progress = None
731            if not self.data or (len(self.data) == 1 and self.data[0][1] == "Error"):
732                self.destroy_button()
733            else:
734                self.show_button()
735
736    def to_excel_or_csv(self):
737        """Función para exportar los datos a un archivo Excel o CSV"""
738        options = []
739        if self.ebitda.get() == 1:
740            options.append("EBITDA")
741        if self.earnings.get() == 1:
742            options.append("Margen de ganacia bruto")
743        if self.roe.get() == 1:
744            options.append("ROE")
745        if self.per.get() == 1:
746            options.append("PER")
747
748        df_result = pd.DataFrame(
749            self.data,
750            columns=[
751                "Ticker",
752                "WACC",
753                "FCF 2023",
754                "FCF 2024",
755                "FCF 2025",
756                "FCF 2026",
757                "FCF 2027",
758                "FCF 2028",
759                "% FCN",
760                "Valor de la empresa",
761                "Precio de la acción",
762                "Valor intrínseco de la acción",
763                "Diferencia",
764            ]
765            + options,
766        )
767
768        root = tk.Tk()
769        root.withdraw()
770
771        file_path = filedialog.asksaveasfilename(
772            defaultextension=".xlsx",
773            filetypes=[("Excel files", "*.xlsx")],
774            initialfile="dcf",
775        )
776
777        if file_path:
778            df_result.to_excel(file_path, index=False)
779
780    def agregar_datos_desde_excel(self):
781        """
782        Agrega tickers desde un archivo de Excel al formulario.
783        """
784        for i in range(self.addRange):
785            self.delete()
786
787        try:
788            ruta_archivo = filedialog.askopenfilename(
789                filetypes=[
790                    ("Archivos de Excel", "*.xlsx"),
791                    ("Todos los archivos", "*.*"),
792                ]
793            )
794            if ruta_archivo:
795                self.procesar_datos_desde_excel(ruta_archivo)
796        except Exception as e:
797            print("Ocurrió un error al abrir el archivo:", e)
798        self.myCanvas.update_idletasks()
799        self.center_window()
800
801    def procesar_datos_desde_excel(self, ruta_archivo):
802        """
803        Procesa los datos desde un archivo de Excel y los agrega al formulario.
804
805        Parameters:
806            ruta_archivo (str): La ruta del archivo de Excel.
807        """
808        try:
809            self.datos_excel = pd.read_excel(ruta_archivo)
810            self.tickers = []
811            self.tickers.append(self.datos_excel.columns[0])
812
813            for i in range(len(self.datos_excel[self.datos_excel.columns[0]])):
814                self.tickers.append(self.datos_excel[self.datos_excel.columns[0]][i])
815
816            if len(self.entries) > 1 and self.entries[-1] != "":
817                self.addRange = self.addRange - len(self.entries) + 1
818            else:
819                self.addRange = 1
820                self.entries = []
821
822            for ticker in self.tickers:
823                self.add(ticker)
824        except Exception as e:
825            print(e)
826            self.status.config(text="Ocurrió un error al procesar el archivo Excel")
DcfApp(master)
11    def __init__(self, master):
12        """
13        Inicializa de DcfApp.
14
15        Args:
16            master: El widget principal de la aplicación.
17        """
18        self.master = master
19        """Segundo ventana principal de la aplicación"""
20        self.data = []
21        """Lista que contiene los datos de la tabla de resultados."""
22        self.addRange = 1
23        """Número de campos de entrada para el ticker de la empresa."""
24        self.entries = []
25        """Lista que contiene los campos de entrada para el ticker de la empresa."""
26        self.ventana = master
27        """Ventana principal de la aplicación."""
28        self.replayDCF = 0
29        """Número de veces que se ha ejecutado el cálculo DCF."""
30        self.progress = None
31        """Barra de progreso para el cálculo DCF."""
32        self.buttonXLS = None
33        """Botón para exportar a Excel."""
34
35        self.create_widgets()

Inicializa de DcfApp.

Args: master: El widget principal de la aplicación.

master

Segundo ventana principal de la aplicación

data

Lista que contiene los datos de la tabla de resultados.

addRange

Número de campos de entrada para el ticker de la empresa.

entries

Lista que contiene los campos de entrada para el ticker de la empresa.

ventana

Ventana principal de la aplicación.

replayDCF

Número de veces que se ha ejecutado el cálculo DCF.

progress

Barra de progreso para el cálculo DCF.

buttonXLS

Botón para exportar a Excel.

def create_widgets(self):
 37    def create_widgets(self):
 38        """
 39        Crea los widgets para la interfaz de la aplicación.
 40        """
 41        self.ventana.title("Calculadora de DCF")
 42        self.ventana.minsize(1500, 700)
 43        self.ventana.configure(background="white")
 44
 45        self.mainFrame = tk.Frame(self.ventana)
 46        self.mainFrame.pack(fill=tk.BOTH, expand=True)
 47
 48        self.myCanvas = tk.Canvas(self.mainFrame, bg="white")
 49        self.myCanvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
 50
 51        self.myScrollbar = tk.ttk.Scrollbar(
 52            self.mainFrame, orient=tk.VERTICAL, command=self.myCanvas.yview
 53        )
 54        self.myScrollbar.pack(side=tk.RIGHT, fill=tk.Y)
 55
 56        self.myScrollbarX = tk.ttk.Scrollbar(
 57            self.mainFrame, orient=tk.HORIZONTAL, command=self.myCanvas.xview
 58        )
 59        self.myScrollbarX.pack(side=tk.BOTTOM, fill=tk.X)
 60
 61        self.myCanvas.configure(yscrollcommand=self.myScrollbar.set)
 62        self.myCanvas.configure(xscrollcommand=self.myScrollbarX.set)
 63
 64        self.secondFrame = tk.Frame(self.myCanvas, bg="white")
 65
 66        self.title = tk.Label(
 67            self.secondFrame, text="Calculadora de DCF", font=("Arial", 20), bg="white"
 68        )
 69        self.title.pack()
 70
 71        self.frame = tk.Frame(self.secondFrame, width=1000, height=300, bg="white")
 72        self.frame.pack()
 73
 74        self.myCanvas.bind("<Configure>", self.center_window)
 75
 76        self.style = ttkthemes.ThemedStyle()
 77
 78        # self.exelFrame = tk.Frame(self.secondFrame, width=500, height=70, bg="white")
 79
 80        self.status = tk.Label(
 81            self.frame, bd=0, relief=tk.SUNKEN, anchor=tk.W, fg="red", bg="white"
 82        )
 83        self.status.pack()
 84
 85        self.boton1 = tk.Button(
 86            self.frame,
 87            text="Calcular DCF",
 88            width=20,
 89            height=2,
 90            bg="DeepSkyBlue3",
 91            fg="white",
 92        )
 93        self.boton1.pack()
 94        self.boton1.place(x=300, y=240)
 95        self.boton_agregar_excel = tk.Button(
 96            self.frame,
 97            text="Agregar desde Excel",
 98            width=20,
 99            height=2,
100            bg="DeepSkyBlue3",
101            fg="white",
102        )
103
104        self.bottonDelete = tk.Button(
105            self.frame, text="-", width=2, height=1, bg="SkyBlue1", fg="white"
106        )
107        self.bottonDelete.pack()
108        self.bottonDelete.config(command=self.delete)
109        self.bottonDelete.place(x=240, y=50)
110        self.bottonDelete.config(state="disabled")
111
112        self.label1 = tk.Label(
113            self.frame, text="Introduce el ticker de la empresa", bg="white"
114        )
115        self.label1.pack()
116        self.label1.place(x=40, y=15)
117
118        tickerMessage = "El ticker de la empresa es el símbolo que se utiliza para identificar una empresa en el mercado de valores, \ncomo AAPL para Apple Inc. o MSFT para Microsoft Corporation."
119        self.label1.bind(
120            "<Enter>", lambda event: self.enter(event, tickerMessage, self.label1)
121        )
122        self.label1.bind("<Leave>", self.leave)
123
124        self.add()
125
126        self.bottonAdd = tk.Button(
127            self.frame, text="+", width=2, height=1, bg="DeepSkyBlue3", fg="white"
128        )
129        self.bottonAdd.pack()
130        self.bottonAdd.config(command=self.add)
131        self.bottonAdd.place(x=240, y=15)
132
133        self.label2 = tk.Label(
134            self.frame, text="Introduce la tasa libre de riesgo", bg="white"
135        )
136        self.label2.pack()
137        self.label2.place(x=285, y=15)
138
139        self.rfMessage = "La tasa libre de riesgo es el rendimiento que se espera de una inversión libre de riesgo, \ncomo los bonos del Tesoro de EE. UU. a 10 años (4%)"
140        self.label2.bind(
141            "<Enter>", lambda event: self.enter(event, self.rfMessage, self.label2)
142        )
143        self.label2.bind("<Leave>", self.leave)
144
145        self.validate_cmd = self.ventana.register(self.validar_input)
146
147        self.rf = tk.Entry(
148            self.frame,
149            width=30,
150            bg="gray90",
151            validate="key",
152            validatecommand=(self.validate_cmd, "%P"),
153            textvariable=tk.StringVar(self.ventana, "0.04"),
154        )
155        self.rf.pack()
156        self.rf.place(x=285, y=40)
157
158        self.label3 = tk.Label(
159            self.frame,
160            text="Introduce el rendimiento real\ndel mercado",
161            bg="white",
162            justify="left",
163        )
164        self.label3.pack()
165        self.label3.place(x=285, y=80)
166
167        self.rmMessage = "El rendimiento real del mercado es el rendimiento que se espera de una inversión en el mercado de valores, \ncomo el S&P 500 (10%)"
168        self.label3.bind(
169            "<Enter>", lambda event: self.enter(event, self.rmMessage, self.label3)
170        )
171        self.label3.bind("<Leave>", self.leave)
172
173        self.rm = tk.Entry(
174            self.frame,
175            width=30,
176            bg="gray90",
177            validate="key",
178            validatecommand=(self.validate_cmd, "%P"),
179            textvariable=tk.StringVar(self.ventana, "0.1"),
180        )
181        self.rm.pack()
182        self.rm.place(x=285, y=120)
183
184        self.label4 = tk.Label(
185            self.frame, text="Introduce el crecimiento perpetuo", bg="white"
186        )
187        self.label4.pack()
188        self.label4.place(x=285, y=160)
189
190        self.gMessage = "El crecimiento perpetuo es el crecimiento constante que se espera de una empresa a largo plazo, \ncomo el crecimiento del PIB (3%)"
191        self.label4.bind(
192            "<Enter>", lambda event: self.enter(event, self.gMessage, self.label4)
193        )
194        self.label4.bind("<Leave>", self.leave)
195
196        self.g = tk.Entry(
197            self.frame,
198            width=30,
199            bg="gray90",
200            validate="key",
201            validatecommand=(self.validate_cmd, "%P"),
202            textvariable=tk.StringVar(self.ventana, "0.03"),
203        )
204        self.g.pack()
205        self.g.place(x=285, y=185)
206
207        self.ebitda = tk.IntVar()
208        self.check = tk.Checkbutton(
209            self.frame,
210            text="Mostrar EBITDA",
211            bg="white",
212            variable=self.ebitda,
213            onvalue=1,
214            offvalue=0,
215        )
216        self.check.pack()
217        self.check.place(x=500, y=15)
218
219        self.ebitdaMessage = "El EBITDA (Earnings Before Interest, Taxes, Depreciation and Amortization) es una medida de la rentabilidad de una empresa, \nantes de intereses, impuestos, depreciación y amortización."
220        self.check.bind(
221            "<Enter>", lambda event: self.enter(event, self.ebitdaMessage, self.check)
222        )
223        self.check.bind("<Leave>", self.leave)
224
225        self.earnings = tk.IntVar()
226        self.check2 = tk.Checkbutton(
227            self.frame,
228            text="Mostrar margen de ganacias bruto",
229            variable=self.earnings,
230            onvalue=1,
231            bg="white",
232        )
233        self.check2.pack()
234        self.check2.place(x=500, y=55)
235
236        self.earningsMessage = "El margen de ganancias bruto es la relación entre las ganancias brutas y los ingresos totales. \nUn margen de ganancias bruto más alto puede indicar que la empresa es más eficiente."
237        self.check2.bind(
238            "<Enter>",
239            lambda event: self.enter(event, self.earningsMessage, self.check2),
240        )
241        self.check2.bind("<Leave>", self.leave)
242
243        self.roe = tk.IntVar()
244        self.check3 = tk.Checkbutton(
245            self.frame, text="Mostrar ROE", variable=self.roe, onvalue=1, bg="white"
246        )
247        self.check3.pack()
248        self.check3.place(x=500, y=95)
249
250        self.roeMessage = "El ROE (Return on Equity) es la relación entre las ganancias netas y el patrimonio neto de la empresa. \nUn ROE más alto puede indicar que la empresa es más eficiente."
251        self.check3.bind(
252            "<Enter>", lambda event: self.enter(event, self.roeMessage, self.check3)
253        )
254        self.check3.bind("<Leave>", self.leave)
255
256        self.per = tk.IntVar()
257        self.check4 = tk.Checkbutton(
258            self.frame, text="Mostrar el PER", variable=self.per, onvalue=1, bg="white"
259        )
260        self.check4.pack()
261        self.check4.place(x=500, y=135)
262
263        # Asociar el tooltip con el Checkbutton
264        self.perMessage = (
265            "El PER (Price Earnings Ratio) es la relación entre el precio de una acción y \n"
266            "las ganancias por acción de la empresa. Un PER más alto puede indicar \n"
267            "que la acción está sobrevalorada, mientras que un PER más bajo puede \n"
268            "indicar que la acción está infravalorada.\n"
269            " - PER bajo: 0-10\n"
270            " - PER medio: 10-20\n"
271            " - PER alto: +20"
272        )
273        self.check4.bind(
274            "<Enter>", lambda event: self.enter(event, self.perMessage, self.check4)
275        )
276        self.check4.bind("<Leave>", self.leave)
277
278        self.seeking = tk.IntVar()
279        self.check5 = tk.Checkbutton(
280            self.frame,
281            text="Calcular con el crecimiento de Seeking Alpha",
282            variable=self.seeking,
283            onvalue=1,
284            bg="white",
285        )
286        self.check5.pack()
287        self.check5.place(x=500, y=175)
288
289        self.seekingMessage = (
290            "El crecimiento de Seeking Alpha es el crecimiento que se espera de una empresa a largo plazo, \n"
291            "según el consenso de analistas de Seeking Alpha."
292        )
293        self.check5.bind(
294            "<Enter>", lambda event: self.enter(event, self.seekingMessage, self.check5)
295        )
296        self.check5.bind("<Leave>", self.leave)
297
298        self.zacks = tk.IntVar()
299        self.check6 = tk.Checkbutton(
300            self.frame,
301            text="Calcular con el crecimiento de Zacks",
302            variable=self.zacks,
303            onvalue=1,
304            bg="white",
305        )
306        self.check6.pack()
307        self.check6.place(x=710, y=15)
308
309        self.zacksMessage = (
310            "El crecimiento de Zacks es el crecimiento que se espera de una empresa a largo plazo, \n"
311            "según el consenso de analistas de Zacks."
312        )
313        self.check6.bind(
314            "<Enter>", lambda event: self.enter(event, self.zacksMessage, self.check6)
315        )
316        self.check6.bind("<Leave>", self.leave)
317
318        self.tableFrame = tk.Frame(self.secondFrame, width=500, height=70, bg="white")
319        self.tableFrame.pack(padx=(20, 0))
320
321        self.tableFrame2 = tk.Frame(self.secondFrame, width=500, height=70, bg="white")
322        self.tableFrame2.pack(padx=(20, 0), pady=(20, 0))
323
324        self.tableFrame3 = tk.Frame(self.secondFrame, width=500, height=70, bg="white")
325        self.tableFrame3.pack(padx=(20, 0), pady=(20, 0))
326
327        # self.exelFrame.pack()
328
329        self.boton1.config(command=self.threaded_create_table)
330
331        self.boton_agregar_excel.pack()
332        self.boton_agregar_excel.place(x=65, y=240)
333        self.boton_agregar_excel.config(command=self.agregar_datos_desde_excel)

Crea los widgets para la interfaz de la aplicación.

def center_window(self, event=None):
335    def center_window(self, event=None):
336        """
337        Centra la ventana secundaria en el lienzo cuando se redimensiona.
338
339        Args:
340            event: El evento que activa la función (opcional).
341        """
342        self.myCanvas.update_idletasks()
343
344        canvas_width = self.myCanvas.winfo_width()
345        frame_width = self.secondFrame.winfo_reqwidth()
346        x = max((canvas_width - frame_width) / 2, 0)
347
348        self.myCanvas.create_window(x, 0, window=self.secondFrame, anchor="nw")
349        self.myCanvas.configure(scrollregion=self.myCanvas.bbox("all"))
350
351        self.myScrollbar.place(relx=1, rely=0, relheight=1, anchor="ne")
352        self.myCanvas.configure(xscrollcommand=self.myScrollbarX.set)
353        self.myScrollbarX.place(
354            x=0,
355            y=self.myCanvas.winfo_height(),
356            width=self.myCanvas.winfo_width(),
357            anchor="sw",
358        )

Centra la ventana secundaria en el lienzo cuando se redimensiona.

Args: event: El evento que activa la función (opcional).

def validar_input(self, P):
360    def validar_input(self, P):
361        """
362        Valida la entrada del usuario para asegurar que sea un número.
363
364        Args:
365            P: El valor a validar.
366
367        Returns:
368            True si la entrada es válida, False en caso contrario.
369        """
370        if P == "" or P.isdigit():
371            return True
372        try:
373            float(P)
374        except ValueError:
375            return False
376        return True

Valida la entrada del usuario para asegurar que sea un número.

Args: P: El valor a validar.

Returns: True si la entrada es válida, False en caso contrario.

def add(self, nameCompany=None):
378    def add(self, nameCompany=None):
379        """
380        Agrega un campo de entrada para el ticker de la empresa.
381
382        Args:
383            nameCompany (str): El nombre de la empresa (opcional).
384        """
385        self.addRange += 1
386        self.entry = tk.Entry(self.frame, width=30, bg="gray90")
387        self.entry.pack()
388        self.entry.place(x=40, y=1 + (self.addRange - 1) * 40)
389        self.status.config(text="")
390
391        if nameCompany is not None:
392            self.entry.insert(0, nameCompany)
393        self.entries.append(self.entry)
394
395        if self.addRange > 5:
396            self.frame.configure(
397                height=self.frame.winfo_height()
398                + (40 if nameCompany is None else (self.addRange - 5) * 40)
399            )
400            self.status.place(x=100, y=220 + (self.addRange - 5) * 40)
401            self.boton1.place(x=300, y=240 + (self.addRange - 5) * 40)
402            self.boton_agregar_excel.place(x=65, y=240 + (self.addRange - 5) * 40)
403            self.myCanvas.configure(scrollregion=self.myCanvas.bbox("all"))
404        else:
405            self.status.place(x=293, y=220)
406
407        if self.addRange > 2:
408            self.bottonDelete.config(state="active", bg="DeepSkyBlue3", fg="white")

Agrega un campo de entrada para el ticker de la empresa.

Args: nameCompany (str): El nombre de la empresa (opcional).

def delete(self):
410    def delete(self):
411        """
412        Elimina el último campo de entrada para el ticker de la empresa.
413        """
414        self.status.config(text="")
415        if self.addRange > 2:
416            self.addRange -= 1
417            self.entries[-1].destroy()  # Destruir el último Entry
418            self.entries.pop()  # Eliminar referencia del Entry de la lista
419            if self.addRange > 4:
420                self.frame.configure(height=self.frame.winfo_height() - 40)
421                self.boton1.place(x=300, y=240 + (self.addRange - 5) * 40)
422                self.boton_agregar_excel.place(x=40, y=240 + (self.addRange - 5) * 40)
423                self.status.place(x=100, y=220 + (self.addRange - 5) * 40)
424                self.myCanvas.configure(scrollregion=self.myCanvas.bbox("all"))
425            else:
426                self.boton1.place(x=300, y=240)
427            if self.addRange <= 2:
428                self.bottonDelete.config(state="disabled")
429                self.bottonDelete.config(bg="SkyBlue1", fg="white")

Elimina el último campo de entrada para el ticker de la empresa.

def destroy_button(self):
431    def destroy_button(self):
432        """
433        Destruye el botón de archivo Excel si existe.
434        """
435        if self.buttonXLS:
436            self.buttonXLS.destroy()
437            self.buttonXLS = None

Destruye el botón de archivo Excel si existe.

def show_button(self):
439    def show_button(self):
440        """
441        Muestra el botón para exportar a Excel.
442        """
443        self.buttonXLS = tk.Button(
444            self.frame,
445            text="Exportar a Excel",
446            width=20,
447            height=2,
448            bg="DeepSkyBlue3",
449            fg="white",
450        )
451        self.buttonXLS.pack()
452        self.buttonXLS.place(x=530, y=240)
453        self.buttonXLS.config(command=self.to_excel_or_csv)

Muestra el botón para exportar a Excel.

def create_tables(self):
455    def create_tables(self):
456        """
457        Crea las tablas con los resultados del cálculo DCF para cada opción de crecimiento.
458        """
459
460        frames = [self.tableFrame, self.tableFrame2, self.tableFrame3]
461        growthOptions = ["yahoo"]
462
463        framesOpt = [self.tableFrame, self.tableFrame2, self.tableFrame3]
464        for frameOpt in framesOpt:
465            for widget in frameOpt.winfo_children():
466                widget.destroy()
467
468        if self.seeking.get():
469            growthOptions.append("seeking")
470
471        if self.zacks.get():
472            growthOptions.append("zacks")
473
474        for i, option in enumerate(growthOptions):
475            self.create_table(option, frames[i])
476        growthOptions.clear()

Crea las tablas con los resultados del cálculo DCF para cada opción de crecimiento.

def create_table(self, growthOption, frame):
478    def create_table(self, growthOption, frame):
479        """
480        Crea una tabla con los resultados del cálculo DCF.
481        """
482        # for widget in frame.winfo_children():
483        #     widget.destroy()
484
485        menssageOption = ""
486        if growthOption == "yahoo":
487            menssageOption = "Crecimiento de Yahoo Finance"
488        elif growthOption == "seeking":
489            menssageOption = "Crecimiento de Seeking Alpha"
490        elif growthOption == "zacks":
491            menssageOption = "Crecimiento de Zacks"
492
493        labelOptions = tk.Label(
494            frame, text=menssageOption, font=("Arial", 8), bg="white", justify="left"
495        )
496        labelOptions.pack()
497
498        entry_values = [entry.get() for entry in self.entries]
499
500        if entry_values.count("") > 0:
501            self.status.config(text="Debes llenar todos los campos")
502            if self.addRange > 5:
503                self.status.place(x=293, y=(220 + (self.addRange - 5) * 40))
504            else:
505                self.status.place(x=293, y=220)
506            return
507
508        if self.rf.get() == "" or self.rm.get() == "" or self.g.get() == "":
509            self.status.config(text="Debes llenar todos los campos")
510            if self.addRange > 5:
511                self.status.place(x=293, y=(220 + (self.addRange - 5) * 40))
512            else:
513                self.status.place(x=293, y=220)
514            return
515
516        self.boton1.config(state="disabled")
517        dataDcfResult = DcfArrayCalculator()
518        self.data = dataDcfResult.arrDcf(
519            entry_values,
520            float(self.g.get()),
521            float(self.rf.get()),
522            float(self.rm.get()),
523            self.ebitda.get(),
524            self.earnings.get(),
525            self.roe.get(),
526            self.per.get(),
527            growthOption,
528        )
529        # print(self.data)
530        tickersError = []
531        for i in range(len(self.data)):
532            if self.data[i][1] == "Error":
533                tickersError.append(self.data[i][0])
534
535        height = len(self.entries) - (len(tickersError))
536        if height > 0:
537            columns = (
538                13
539                + self.ebitda.get()
540                + self.earnings.get()
541                + self.roe.get()
542                + self.per.get()
543            )
544            table = tk.ttk.Treeview(
545                frame,
546                columns=tuple(range(1, columns + 1)),
547                show="headings",
548                height=height,
549            )
550
551            for i in range(1, columns + 1):
552                table.column(i, width=110, anchor=tk.CENTER)
553                table.heading(
554                    i,
555                    command=lambda _col=i: self.treeview_sort_column(
556                        table, _col, False
557                    ),
558                )
559
560            headings = [
561                "Ticker",
562                "WACC",
563                "FCF 2023",
564                "FCF 2024",
565                "FCF 2025",
566                "FCF 2026",
567                "FCF 2027",
568                "FCF 2028",
569                "% FCF",
570                "Valor de la empresa",
571                "Precio de la acción",
572                "Valor intrínseco",
573                "Diferencia",
574            ]
575            if self.ebitda.get() == 1:
576                headings.append("EBITDA")
577            if self.earnings.get() == 1:
578                headings.append("Margen de beneficio bruto")
579            if self.roe.get() == 1:
580                headings.append("ROE")
581            if self.per.get() == 1:
582                headings.append("PER")
583
584            for i, heading in enumerate(headings, start=1):
585                table.heading(i, text=heading)
586
587            table.pack(fill="both", expand=True)
588
589            self.style.configure("LightRed.TTreeview", background="#ffcccc")
590            self.style.configure("Red.TTreeview", background="#ff6666")
591            self.style.configure("DarkRed.TTreeview", background="#b30000")
592            self.style.configure("LightGreen.TTreeview", background="#ccffcc")
593            self.style.configure("Green.TTreeview", background="#66ff66")
594            self.style.configure("DarkGreen.TTreeview", background="#00b300")
595
596            for i, row in enumerate(self.data):
597                if row[1] != "Error":
598                    second_value = float(row[12].replace("%", ""))
599                    if second_value < 25:
600                        table.insert("", "end", values=row, tags=("DarkRed",))
601                    elif second_value < 50:
602                        table.insert("", "end", values=row, tags=("Red",))
603                    elif second_value < 100:
604                        table.insert("", "end", values=row, tags=("LightRed",))
605                    elif second_value > 300:
606                        table.insert("", "end", values=row, tags=("DarkGreen",))
607                    elif second_value > 200:
608                        table.insert("", "end", values=row, tags=("Green",))
609                    elif second_value > 100:
610                        table.insert("", "end", values=row, tags=("LightGreen",))
611                    else:
612                        table.insert("", "end", values=row)
613
614            table.tag_configure("LightRed", background="#ffcccc")
615            table.tag_configure("Red", background="#ff6666")
616            table.tag_configure("DarkRed", background="#b30000", foreground="white")
617            table.tag_configure("LightGreen", background="#ccffcc")
618            table.tag_configure("Green", background="#66ff66")
619            table.tag_configure("DarkGreen", background="#00b300", foreground="white")
620
621        if len(tickersError) > 0:
622            self.status.config(
623                text=f"Los siguientes tickers no se encontraron: {tickersError}"
624            )
625            if self.addRange > 5:
626                self.status.place(x=240, y=(220 + (self.addRange - 5) * 40))
627            else:
628                # lo colocamos en el centro de la ventana
629                self.status.place(
630                    x=((self.frame.winfo_width() / 2) - 50) - (40 * len(tickersError)),
631                    y=220,
632                )
633
634        self.myCanvas.update_idletasks()
635        self.center_window()
636
637        self.boton1.config(state="normal")

Crea una tabla con los resultados del cálculo DCF.

def treeview_sort_column(self, tv, col, reverse):
639    def treeview_sort_column(self, tv, col, reverse):
640        """
641        Ordena las columnas de la tabla.
642        Args:
643            tv (ttk.Treeview): El widget Treeview.
644            col (int): El índice de la columna a ordenar.
645            reverse (bool): True para orden descendente, False para orden ascendente.
646        """
647        l = [(tv.set(k, col), k) for k in tv.get_children("")]
648        l.sort(reverse=reverse)
649
650        for index, (val, k) in enumerate(l):
651            tv.move(k, "", index)
652
653        # Reverse sort next time.
654        tv.heading(col, command=lambda: self.treeview_sort_column(tv, col, not reverse))

Ordena las columnas de la tabla. Args: tv (ttk.Treeview): El widget Treeview. col (int): El índice de la columna a ordenar. reverse (bool): True para orden descendente, False para orden ascendente.

def enter(self, event, message, label):
656    def enter(self, event, message, label):
657        """
658        Muestra un tooltip cuando el cursor entra en el área del widget.
659
660        Args:
661            event: El evento de entrada que desencadenó la función.
662            message (str): El mensaje a mostrar en el tooltip.
663            label: El widget Label asociado al tooltip.
664        """
665        x, y, _, _ = self.check4.bbox("insert")
666        x += label.winfo_rootx() + 25
667        y += label.winfo_rooty() + 20
668        self.tooltip = tk.Toplevel(self.check4)
669        self.tooltip.wm_overrideredirect(True)
670        self.tooltip.wm_geometry(f"+{x}+{y}")
671        label = tk.Label(
672            self.tooltip,
673            text=message,
674            bg="white",
675            relief="solid",
676            borderwidth=1,
677            justify=tk.LEFT,
678        )
679        label.pack()

Muestra un tooltip cuando el cursor entra en el área del widget.

Args: event: El evento de entrada que desencadenó la función. message (str): El mensaje a mostrar en el tooltip. label: El widget Label asociado al tooltip.

def leave(self, event):
681    def leave(self, event):
682        """
683        Oculta el tooltip cuando el cursor sale del área del widget.
684        """
685        if self.tooltip:
686            self.tooltip.destroy()

Oculta el tooltip cuando el cursor sale del área del widget.

def threaded_create_table(self):
688    def threaded_create_table(self):
689        """
690        Inicia un hilo para crear la tabla de resultados de DCF.
691        """
692        self.replayDCF += 1
693        self.progressFrame = tk.Frame(
694            self.ventana, bg="white", borderwidth=2, relief="solid"
695        )
696        self.progressFrame.config(width=300, height=400)
697        self.progressFrame.pack()
698        self.progressMessage = tk.Label(
699            self.progressFrame, text="Cargando...\n Espere por favor", bg="white"
700        )
701        self.progressMessage.pack()
702        self.progressMessage.config(width=20, height=5)
703        self.progress = tk.ttk.Progressbar(
704            self.progressFrame, length=200, mode="indeterminate"
705        )
706        self.progress.pack(padx=10, pady=(0, 10))
707        if self.replayDCF > 1:
708            self.destroy_button()
709
710        self.progressFrame.place(
711            x=self.ventana.winfo_width() / 2 - 50,
712            y=self.ventana.winfo_height() / 2 - 50,
713        )
714        self.progress.start()
715        thread = threading.Thread(target=self.create_tables)
716        thread.start()
717        self.ventana.after(100, self.check_thread, thread)

Inicia un hilo para crear la tabla de resultados de DCF.

def check_thread(self, thread):
719    def check_thread(self, thread):
720        """
721        Verifica periódicamente si el hilo de creación de tabla está vivo.
722        """
723        if thread.is_alive():
724            self.ventana.after(100, self.check_thread, thread)
725        else:
726            if self.progress is not None:
727                self.progress.stop()
728                self.progress.destroy()
729                self.progressFrame.destroy()
730                progress = None
731            if not self.data or (len(self.data) == 1 and self.data[0][1] == "Error"):
732                self.destroy_button()
733            else:
734                self.show_button()

Verifica periódicamente si el hilo de creación de tabla está vivo.

def to_excel_or_csv(self):
736    def to_excel_or_csv(self):
737        """Función para exportar los datos a un archivo Excel o CSV"""
738        options = []
739        if self.ebitda.get() == 1:
740            options.append("EBITDA")
741        if self.earnings.get() == 1:
742            options.append("Margen de ganacia bruto")
743        if self.roe.get() == 1:
744            options.append("ROE")
745        if self.per.get() == 1:
746            options.append("PER")
747
748        df_result = pd.DataFrame(
749            self.data,
750            columns=[
751                "Ticker",
752                "WACC",
753                "FCF 2023",
754                "FCF 2024",
755                "FCF 2025",
756                "FCF 2026",
757                "FCF 2027",
758                "FCF 2028",
759                "% FCN",
760                "Valor de la empresa",
761                "Precio de la acción",
762                "Valor intrínseco de la acción",
763                "Diferencia",
764            ]
765            + options,
766        )
767
768        root = tk.Tk()
769        root.withdraw()
770
771        file_path = filedialog.asksaveasfilename(
772            defaultextension=".xlsx",
773            filetypes=[("Excel files", "*.xlsx")],
774            initialfile="dcf",
775        )
776
777        if file_path:
778            df_result.to_excel(file_path, index=False)

Función para exportar los datos a un archivo Excel o CSV

def agregar_datos_desde_excel(self):
780    def agregar_datos_desde_excel(self):
781        """
782        Agrega tickers desde un archivo de Excel al formulario.
783        """
784        for i in range(self.addRange):
785            self.delete()
786
787        try:
788            ruta_archivo = filedialog.askopenfilename(
789                filetypes=[
790                    ("Archivos de Excel", "*.xlsx"),
791                    ("Todos los archivos", "*.*"),
792                ]
793            )
794            if ruta_archivo:
795                self.procesar_datos_desde_excel(ruta_archivo)
796        except Exception as e:
797            print("Ocurrió un error al abrir el archivo:", e)
798        self.myCanvas.update_idletasks()
799        self.center_window()

Agrega tickers desde un archivo de Excel al formulario.

def procesar_datos_desde_excel(self, ruta_archivo):
801    def procesar_datos_desde_excel(self, ruta_archivo):
802        """
803        Procesa los datos desde un archivo de Excel y los agrega al formulario.
804
805        Parameters:
806            ruta_archivo (str): La ruta del archivo de Excel.
807        """
808        try:
809            self.datos_excel = pd.read_excel(ruta_archivo)
810            self.tickers = []
811            self.tickers.append(self.datos_excel.columns[0])
812
813            for i in range(len(self.datos_excel[self.datos_excel.columns[0]])):
814                self.tickers.append(self.datos_excel[self.datos_excel.columns[0]][i])
815
816            if len(self.entries) > 1 and self.entries[-1] != "":
817                self.addRange = self.addRange - len(self.entries) + 1
818            else:
819                self.addRange = 1
820                self.entries = []
821
822            for ticker in self.tickers:
823                self.add(ticker)
824        except Exception as e:
825            print(e)
826            self.status.config(text="Ocurrió un error al procesar el archivo Excel")

Procesa los datos desde un archivo de Excel y los agrega al formulario.

Parameters: ruta_archivo (str): La ruta del archivo de Excel.

def main():
829def main():
830    """
831    Función principal para iniciar la aplicación.
832    """
833    root = tk.Tk()
834    app = DcfApp(root)
835    root.mainloop()

Función principal para iniciar la aplicación.