From 432389cc024deec1be130526f0aebcec7442087e Mon Sep 17 00:00:00 2001 From: kocielnik Date: Mon, 16 Dec 2024 23:07:15 +0100 Subject: [PATCH] Add 10Cap valuation (closes #88) (#90) * Add 10Cap valuation (closes #88). * Delete the test that is not relevant anymore. I expect the breaking of this test to be fixed by PR #92. * Fix div id. * Make the test test the *API*, not NVDA:) Actually, the purpose of this commit is to bump the CI to run again, because it failed with an error that seems intermittent, and very rare. * Fix precision of 10Cap price. Before: ten_cap("NVDA") = 10.834 After: ten_cap("NVDA") = 10.83 --- isthisstockgood/DataFetcher.py | 6 ++++- isthisstockgood/templates/home.html | 3 ++- isthisstockgood/templates/js/search.js | 18 ++++++++++++- .../templates/json/stock_data.json | 3 ++- isthisstockgood/templates/loading.html | 2 +- isthisstockgood/templates/searchbox.html | 6 ++--- isthisstockgood/templates/ten_cap.html | 25 +++++++++++++++++++ .../templates/txt/ten_cap_tooltip.txt | 2 ++ tests/test_DataSources.py | 9 ------- tests/test_api.py | 24 ++++++++++++++++-- 10 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 isthisstockgood/templates/ten_cap.html create mode 100644 isthisstockgood/templates/txt/ten_cap_tooltip.txt diff --git a/isthisstockgood/DataFetcher.py b/isthisstockgood/DataFetcher.py index a546094..e7caa7f 100644 --- a/isthisstockgood/DataFetcher.py +++ b/isthisstockgood/DataFetcher.py @@ -31,6 +31,7 @@ def fetchDataForTickerSymbol(ticker): 'debt_equity_ratio', 'margin_of_safety_price', 'current_price' + 'ten_cap_price' """ if not ticker: return None @@ -56,7 +57,9 @@ def fetchDataForTickerSymbol(ticker): margin_of_safety_price, sticker_price = \ _calculateMarginOfSafetyPrice(msn_money.equity_growth_rates[-1], msn_money.pe_low, msn_money.pe_high, msn_money.eps[-1], five_year_growth_rate) payback_time = _calculatePaybackTime(msn_money.equity_growth_rates[-1], msn_money.last_year_net_income, msn_money.market_cap, five_year_growth_rate) - computed_free_cash_flow = round(float(msn_money.free_cash_flow[-1]) * msn_money.shares_outstanding) + free_cash_flow_per_share = float(msn_money.free_cash_flow[-1]) + computed_free_cash_flow = round(free_cash_flow_per_share * msn_money.shares_outstanding) + ten_cap_price = 10 * free_cash_flow_per_share template_values = { 'ticker' : ticker, 'name' : msn_money.name if msn_money and msn_money.name else 'null', @@ -68,6 +71,7 @@ def fetchDataForTickerSymbol(ticker): 'cash': msn_money.free_cash_flow_growth_rates if msn_money and msn_money.free_cash_flow_growth_rates else [], 'total_debt' : msn_money.total_debt, 'free_cash_flow' : computed_free_cash_flow, + 'ten_cap_price' : round(ten_cap_price, 2), 'debt_payoff_time' : round(float(msn_money.total_debt) / computed_free_cash_flow), 'debt_equity_ratio' : msn_money.debt_equity_ratio if msn_money and msn_money.debt_equity_ratio >= 0 else -1, 'margin_of_safety_price' : margin_of_safety_price if margin_of_safety_price else 'null', diff --git a/isthisstockgood/templates/home.html b/isthisstockgood/templates/home.html index b953034..2459744 100644 --- a/isthisstockgood/templates/home.html +++ b/isthisstockgood/templates/home.html @@ -7,6 +7,7 @@ {% include "management.html" %} {% include "margin_of_safety.html" %} {% include "payback_time.html" %} + {% include "ten_cap.html" %} {% include "market_cap.html" %} - {% endblock %} \ No newline at end of file + {% endblock %} diff --git a/isthisstockgood/templates/js/search.js b/isthisstockgood/templates/js/search.js index 782d918..bc780cf 100644 --- a/isthisstockgood/templates/js/search.js +++ b/isthisstockgood/templates/js/search.js @@ -122,6 +122,22 @@ $(document).ready(function() { colorCellWithBackgroundColor('#' + key, Color.red()); } + // Update 10 Cap section + let ten_cap_key = 'ten_cap_price'; + let ten_cap_field_id = '#' + ten_cap_key; + let current_price = data['current_price']; + updateHtmlWithValueForKey(data, ten_cap_key, /*commas=*/true); + if (!data[ten_cap_key]) { + colorCellWithBackgroundColor(ten_cap_field_id, Color.red()); + } + if (current_price > data[ten_cap_key]) { + colorCellWithBackgroundColor(ten_cap_field_id, Color.red()); + } + else { + colorCellWithBackgroundColor(ten_cap_field_id, Color.green()); + } + + // Update Market Cap numbers updateHtmlWithValueForKey(data, 'average_volume', /*commas=*/true); let averageVolume = data['average_volume']; @@ -212,4 +228,4 @@ function colorCellWithIDForZeroBasedRange(id, range) { } colorCellWithBackgroundColor(id, backgroundColor); -} \ No newline at end of file +} diff --git a/isthisstockgood/templates/json/stock_data.json b/isthisstockgood/templates/json/stock_data.json index 224a373..c6192e3 100644 --- a/isthisstockgood/templates/json/stock_data.json +++ b/isthisstockgood/templates/json/stock_data.json @@ -15,5 +15,6 @@ "current_price" : {{ current_price }}, "sticker_price" : {{ sticker_price }}, "payback_time" : {{ payback_time }}, + "ten_cap_price" : {{ ten_cap_price }}, "average_volume" : {{ average_volume }} -} \ No newline at end of file +} diff --git a/isthisstockgood/templates/loading.html b/isthisstockgood/templates/loading.html index 8653943..f11b437 100644 --- a/isthisstockgood/templates/loading.html +++ b/isthisstockgood/templates/loading.html @@ -90,7 +90,7 @@ this.shadowRoot.append(style, wrapper); - this.animationTimeout = None; + this.animationTimeout = null; } static get observedAttributes() { return ['data-message']; } diff --git a/isthisstockgood/templates/searchbox.html b/isthisstockgood/templates/searchbox.html index dc0e44a..69f0f90 100644 --- a/isthisstockgood/templates/searchbox.html +++ b/isthisstockgood/templates/searchbox.html @@ -4,11 +4,11 @@
+ maxlength=8/>
{% include "js/search.js" %} - \ No newline at end of file + diff --git a/isthisstockgood/templates/ten_cap.html b/isthisstockgood/templates/ten_cap.html new file mode 100644 index 0000000..2733e21 --- /dev/null +++ b/isthisstockgood/templates/ten_cap.html @@ -0,0 +1,25 @@ +
+
+ +
+ +
+
+ + + + + + + +
10 Cap Price
+ - +
+
+
diff --git a/isthisstockgood/templates/txt/ten_cap_tooltip.txt b/isthisstockgood/templates/txt/ten_cap_tooltip.txt new file mode 100644 index 0000000..c20696f --- /dev/null +++ b/isthisstockgood/templates/txt/ten_cap_tooltip.txt @@ -0,0 +1,2 @@ +Price at which the company is expected to return to you about 10% of your invested amount via Free Cash Flow (Owner's Earnings). +Discussed in the book 'Invested' by Danielle Town. diff --git a/tests/test_DataSources.py b/tests/test_DataSources.py index e07e190..1677d0a 100644 --- a/tests/test_DataSources.py +++ b/tests/test_DataSources.py @@ -32,15 +32,6 @@ def test_msn_money(): assert data.last_year_net_income > 0.0 assert data.total_debt >= 0.0 -def test_yahoo(): - test_ticker = 'MSFT' - test_name = 'Microsoft Corp' - - data = get_yahoo_data(test_ticker) - - assert data.ticker_symbol == test_ticker - assert float(data.five_year_growth_rate) > 0.0 - def get_msn_money_data(ticker): data_fetcher = DataFetcher() data_fetcher.ticker_symbol = ticker diff --git a/tests/test_api.py b/tests/test_api.py index ca53b9e..c5c6c14 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -18,7 +18,27 @@ def test_get_data(): with app.test_client() as test_client: test_client = app.test_client() + res = test_client.get('/api/ticker/nvda') - data = res.text - assert json.loads(data)['debt_payoff_time'] == 0 assert res.status_code == 200 + + assert res.json['debt_payoff_time'] >= 0 + +def test_get_ten_cap_price(): + app = create_app(fetchDataForTickerSymbol) + + with app.test_client() as test_client: + test_client = app.test_client() + res = test_client.get('/api/ticker/nvda') + assert res.json['ten_cap_price'] > 0 + +def test_ten_cap_price_has_two_places_precision(): + app = create_app(fetchDataForTickerSymbol) + + with app.test_client() as test_client: + test_client = app.test_client() + res = test_client.get('/api/ticker/nvda') + + price = res.json['ten_cap_price'] + + assert round(price, 2) == price