Skip to content

Commit c3ce06c

Browse files
authored
Merge pull request #3 from thobiast/dev
Dev
2 parents 620b51a + fba68ff commit c3ce06c

File tree

4 files changed

+86
-62
lines changed

4 files changed

+86
-62
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
requests
22
pandas
3+
lxml

src/magicformulabr/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# -*- coding: utf-8 -*-
22
"""magicformulabr module."""
33

4-
__version__ = "0.2.0"
4+
__version__ = "0.2.1"

src/magicformulabr/main.py

Lines changed: 78 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import requests
2222

2323
URL = "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
##############################################################################

tests/test_magicformula_calc_rank.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77
from magicformulabr.main import MagicFormula
88

99

10-
@pytest.fixture(scope="function")
11-
def sample_df():
10+
@pytest.fixture(name="sample_df", scope="function")
11+
def _sample_df():
1212
"""Create fake dataframe."""
1313
dic_com = {"EV/EBIT": [0.2, 8, 4, 2], "ROIC": [10, 60, 40, 90]}
1414
my_df = pd.DataFrame(data=dic_com, index=["AAAA3", "BBBB3", "CCCC3", "DDDD4"])
1515
return my_df
1616

1717

18-
def test_calc_rank_method_2(sample_df): # pylint: disable=redefined-outer-name
18+
def test_calc_rank_method_2(sample_df):
1919
"""Test calc_rank method."""
2020
expected_result = """\
2121
EV/EBIT ROIC Rank_earnings_yield Rank_return_on_capital Rank_Final
@@ -24,6 +24,6 @@ def test_calc_rank_method_2(sample_df): # pylint: disable=redefined-outer-name
2424
BBBB3 8.0 60 4.0 2.0 6.0
2525
CCCC3 4.0 40 3.0 3.0 6.0"""
2626

27-
magic_formula = MagicFormula(sample_df, magic_method="2")
28-
magic_formula.calc_rank()
29-
assert magic_formula.pd_df.to_string() == expected_result
27+
magic_formula = MagicFormula(sample_df, magic_method=2)
28+
pd_df = magic_formula._calculate_rank() # pylint: disable=protected-access
29+
assert pd_df.to_string() == expected_result

0 commit comments

Comments
 (0)