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()
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")
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.
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.
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).
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.