2121import requests
2222
2323URL = "http://fundamentus.com.br/resultado.php"
24+ CACHE_FILE = "data_cache.json"
25+ CACHE_DURATION_SECONDS = 86400 # 24 hours
2426
2527
2628##############################################################################
@@ -40,6 +42,7 @@ def parse_parameters():
4042 %(prog)s -v
4143 %(prog)s -vv
4244 %(prog)s -m 3 -vv
45+ %(prog)s -m 3 --top 30 --force-update
4346 """
4447 parser = argparse .ArgumentParser (
4548 description = "Gera rank de acoes usando a magic formula" ,
@@ -101,8 +104,8 @@ class DataSourceHandler:
101104 def __init__ (
102105 self ,
103106 url ,
104- cache_file = "data_cache.json" ,
105- cache_duration = 86400 ,
107+ cache_file = CACHE_FILE ,
108+ cache_duration = CACHE_DURATION_SECONDS ,
106109 force_update = False ,
107110 ):
108111 """
@@ -226,15 +229,15 @@ class MagicFormula:
226229 # MAGIC_METHOD_FIELD is a dictionary mapping method identifiers to their respective
227230 # financial metrics used in the Magic Formula calculation. Each method identifier
228231 # corresponds to a different set of financial metrics:
229- # - "1" : Uses 'P/L' for earnings yield and 'ROE' for return on capital.
230- # - "2" : Uses 'EV/EBIT' for earnings yield and 'ROIC' for return on capital.
231- # - "3" : Uses 'EV/EBITDA' for earnings yield and 'ROIC' for return on capital.
232+ # - 1 : Uses 'P/L' for earnings yield and 'ROE' for return on capital.
233+ # - 2 : Uses 'EV/EBIT' for earnings yield and 'ROIC' for return on capital.
234+ # - 3 : Uses 'EV/EBITDA' for earnings yield and 'ROIC' for return on capital.
232235 # These mappings allow for flexibility in defining which financial metrics are used in
233236 # the calculationof the Magic Formula, accommodating different variations of the formula.
234237 MAGIC_METHOD_FIELD = {
235- "1" : {"earnings yield" : "P/L" , "return on capital" : "ROE" },
236- "2" : {"earnings yield" : "EV/EBIT" , "return on capital" : "ROIC" },
237- "3" : {"earnings yield" : "EV/EBITDA" , "return on capital" : "ROIC" },
238+ 1 : {"earnings yield" : "P/L" , "return on capital" : "ROE" },
239+ 2 : {"earnings yield" : "EV/EBIT" , "return on capital" : "ROIC" },
240+ 3 : {"earnings yield" : "EV/EBITDA" , "return on capital" : "ROIC" },
238241 }
239242
240243 def __init__ (self , pd_df , magic_method ):
@@ -244,7 +247,7 @@ def __init__(self, pd_df, magic_method):
244247
245248 Parameters:
246249 pd_df (pandas.DataFrame): The financial data of companies.
247- magic_method (str ): Specifies the method used for calculating the Magic Formula ranking.
250+ magic_method (int ): Specifies the method used for calculating the Magic Formula ranking.
248251 It determines which financial metrics are used for earnings yield
249252 and return on capital calculations.
250253 """
@@ -261,7 +264,7 @@ def pct_to_float(number):
261264 """
262265 return float (number .strip ("%" ).replace ("." , "" ).replace ("," , "." ))
263266
264- def apply_converters (self ):
267+ def _apply_converters (self ):
265268 """
266269 Converts specific columns in `pd_df` to the necessary data type or format.
267270 """
@@ -284,7 +287,7 @@ def _remove_rows(self, col_name, min_value):
284287 logging .debug (self .pd_df .loc [tickers ])
285288 self .pd_df .drop (tickers , inplace = True )
286289
287- def filter_data (self ):
290+ def _filter_data (self ):
288291 """
289292 Removes rows in `pd_df` with negative or undesirable values for key financial metrics.
290293
@@ -294,42 +297,16 @@ def filter_data(self):
294297 self ._remove_rows (self .ret_on_capital , 0 )
295298 self ._remove_rows ("Liq.2meses" , 0 )
296299
297- def drop_unneeded_columns (self , level ):
298- """
299- Removes unnecessary columns from `pd_df` based on the specified verbosity level.
300-
301- Parameters:
302- level (int): The verbosity level that determines which columns are retained.
303- Higher levels retain more columns.
304- """
305- df_columns = self .pd_df .columns .tolist ()
306- if level == 0 :
307- keep_cols = [self .earnings_yield , self .ret_on_capital ]
308- elif level == 1 :
309- keep_cols = [
310- "Cotação" ,
311- "Div.Yield" ,
312- "ROIC" ,
313- "ROE" ,
314- "P/L" ,
315- "EV/EBIT" ,
316- "EV/EBITDA" ,
317- self .earnings_yield ,
318- self .ret_on_capital ,
319- ]
320- else :
321- return
322-
323- remove_cols = [x for x in df_columns if x not in keep_cols ]
324- self .pd_df .drop (remove_cols , axis = "columns" , inplace = True )
325-
326- def calc_rank (self ):
300+ def _calculate_rank (self ):
327301 """
328302 Calculates the Magic Formula ranking for companies in `pd_df`.
329303
330304 Adds ranking columns to `pd_df` based on the earnings yield and return on capital,
331305 then calculates a final rank.
332306 """
307+ if self .pd_df .empty :
308+ return self .pd_df
309+
333310 self .pd_df ["Rank_earnings_yield" ] = self .pd_df [self .earnings_yield ].rank (
334311 ascending = True , method = "min"
335312 )
@@ -341,16 +318,62 @@ def calc_rank(self):
341318 )
342319 self .pd_df .sort_values (by = "Rank_Final" , ascending = True , inplace = True )
343320
344- def show_rank (self , top ):
345- """
346- Displays the top-ranked companies.
321+ return self .pd_df
347322
348- Parameters:
349- top (int): The number of top-ranked companies to display.
323+ def process (self ):
324+ """
325+ Executes the full Magic Formula process: conversion, filtering, and ranking.
350326 """
351- self .pd_df .reset_index (inplace = True )
352- self .pd_df .index = self .pd_df .index + 1
353- print (self .pd_df .head (top ).to_string ())
327+ self ._apply_converters ()
328+ self ._filter_data ()
329+ df_ranked = self ._calculate_rank ()
330+ return df_ranked
331+
332+
333+ def display_results (df , args ):
334+ """
335+ Displays the top-ranked companies according to verbosity.
336+
337+ Parameters:
338+ df (pd.DataFrame):
339+ The ranked DataFrame produced by `MagicFormula.calc_rank()`.
340+ args (argparse.Namespace):
341+ Parsed command-line arguments containing:
342+ - method (int): Magic Formula method (1, 2, or 3) to determine key columns.
343+ - verbose (int): Verbosity level (0, 1, or >=2) to control displayed columns.
344+ - top (int): Number of companies to display.
345+ """
346+ df_display = df .copy ()
347+
348+ base_cols = ["Rank_earnings_yield" , "Rank_return_on_capital" , "Rank_Final" ]
349+
350+ magic_method_cols = MagicFormula .MAGIC_METHOD_FIELD [args .method ]
351+ earnings_yield_col = magic_method_cols ["earnings yield" ]
352+ return_on_capital_col = magic_method_cols ["return on capital" ]
353+
354+ if args .verbose == 0 :
355+ keep_cols = [earnings_yield_col , return_on_capital_col ] + base_cols
356+ elif args .verbose == 1 :
357+ keep_cols = [
358+ "Cotação" ,
359+ "Div.Yield" ,
360+ "ROIC" ,
361+ "ROE" ,
362+ "P/L" ,
363+ "EV/EBIT" ,
364+ "EV/EBITDA" ,
365+ earnings_yield_col ,
366+ return_on_capital_col ,
367+ ] + base_cols
368+ else :
369+ keep_cols = df_display .columns .tolist ()
370+
371+ # Remove duplicates preserving order
372+ keep_cols = list (dict .fromkeys (keep_cols ))
373+
374+ df_display .reset_index (inplace = True , names = "Ticker" )
375+ df_display .index = df_display .index + 1
376+ print (df_display .head (args .top ).to_string (columns = ["Ticker" ] + keep_cols ))
354377
355378
356379##############################################################################
@@ -370,12 +393,12 @@ def main():
370393 data_handler = DataSourceHandler (URL , force_update = args .force_update )
371394 pd_df = data_handler .get_data ()
372395
373- magicformula = MagicFormula (pd_df , str ( args .method ) )
374- magicformula .apply_converters ()
375- magicformula . filter_data ()
376- magicformula . drop_unneeded_columns ( level = args . verbose )
377- magicformula . calc_rank ()
378- magicformula . show_rank ( args . top )
396+ magicformula = MagicFormula (pd_df , args .method )
397+ ranked_df = magicformula .process ()
398+ if ranked_df . empty :
399+ print ( "No companies passed the filtering criteria. The ranking is empty." )
400+ else :
401+ display_results ( ranked_df , args )
379402
380403
381404##############################################################################
0 commit comments