Skip to content

Commit 97d504b

Browse files
committed
Allow for custom growth rate in API and Web UI.
Also: Retry automatically twice on external API error.
1 parent 23eba15 commit 97d504b

7 files changed

Lines changed: 615 additions & 28 deletions

File tree

isthisstockgood/DataFetcher.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import random
22
import logging
33
import isthisstockgood.RuleOneInvestingCalculations as RuleOne
4+
from requests.adapters import HTTPAdapter, Retry
5+
from requests.exceptions import RetryError
46
from requests_futures.sessions import FuturesSession
57
from isthisstockgood.Active.MSNMoney import MSNMoney
68
from isthisstockgood.Active.YahooFinance import YahooFinanceAnalysis
@@ -10,7 +12,7 @@
1012
logger = logging.getLogger("IsThisStockGood")
1113

1214

13-
def fetchDataForTickerSymbol(ticker):
15+
def fetchDataForTickerSymbol(ticker, growth_estimate=None):
1416
"""Fetches and parses all of the financial data for the `ticker`.
1517
1618
Args:
@@ -48,16 +50,22 @@ def fetchDataForTickerSymbol(ticker):
4850

4951
# Wait for each RPC result before proceeding.
5052
for rpc in data_fetcher.rpcs:
51-
rpc.result()
53+
try:
54+
rpc.result()
55+
except RetryError as e:
56+
continue
5257

5358
msn_money = data_fetcher.msn_money
5459
yahoo_finance_analysis = data_fetcher.yahoo_finance_analysis
5560
zacks_analysis = data_fetcher.zacks_analysis
5661
# NOTE: Some stocks won't have analyst growth rates, such as newly listed stocks or some foreign stocks.
57-
five_year_growth_rate = \
58-
yahoo_finance_analysis.five_year_growth_rate if yahoo_finance_analysis \
59-
else zacks_analysis.five_year_growth_rate if zacks_analysis \
60-
else 0
62+
if (yahoo_finance_analysis
63+
and yahoo_finance_analysis.five_year_growth_rate is not None):
64+
five_year_growth_rate = yahoo_finance_analysis.five_year_growth_rate
65+
elif (zacks_analysis and zacks_analysis.five_year_growth_rate is not None):
66+
five_year_growth_rate = zacks_analysis.five_year_growth_rate
67+
else:
68+
five_year_growth_rate = growth_estimate
6169
margin_of_safety_price, sticker_price = _calculateMarginOfSafetyPrice(
6270
msn_money.equity_growth_rates[-1],
6371
msn_money.pe_low,
@@ -143,6 +151,16 @@ def _create_session(self):
143151
session.headers.update({
144152
'User-Agent' : random.choice(DataFetcher.USER_AGENT_LIST)
145153
})
154+
155+
retries = Retry(
156+
total=3,
157+
backoff_factor=0.1,
158+
status_forcelist=[301]
159+
)
160+
adapter = HTTPAdapter(max_retries=retries)
161+
session.mount('http://', adapter)
162+
session.mount('https://', adapter)
163+
146164
return session
147165

148166
def fetch_msn_money_data(self):

isthisstockgood/server.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import re
23
from datetime import date
34
from flask import Flask, request, render_template, json
45

@@ -19,9 +20,17 @@ def get_logger():
1920
def create_app(fetchDataForTickerSymbol):
2021
app = Flask(__name__)
2122

22-
@app.route('/api/ticker/nvda')
23-
def api_ticker():
24-
template_values = fetchDataForTickerSymbol("NVDA")
23+
@app.route(
24+
'/api/ticker/<unsanitized_ticker>/<unsanitized_growth_estimate>'
25+
)
26+
def api_ticker(
27+
unsanitized_ticker,
28+
unsanitized_growth_estimate
29+
):
30+
ticker = re.sub(r'\W+', '', unsanitized_ticker)
31+
growth_estimate = int(re.sub(r'\W+', '', unsanitized_growth_estimate))
32+
33+
template_values = fetchDataForTickerSymbol(ticker, growth_estimate=growth_estimate)
2534

2635
if not template_values:
2736
data = render_template('json/error.json', **{'error' : 'Invalid ticker symbol'})
@@ -60,7 +69,8 @@ def search():
6069
return '<meta http-equiv="refresh" content="0; url=http://isthisstockgood.com" />'
6170

6271
ticker = request.values.get('ticker')
63-
template_values = fetchDataForTickerSymbol(ticker)
72+
custom_growth_rate = request.values.get('custom_growth_rate')
73+
template_values = fetchDataForTickerSymbol(ticker, growth_estimate=int(custom_growth_rate))
6474
if not template_values:
6575
return render_template('json/error.json', **{'error' : 'Invalid ticker symbol'})
6676
return render_template('json/stock_data.json', **template_values)

isthisstockgood/templates/js/search.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,16 @@ $(document).ready(function() {
4040
return;
4141
}
4242

43+
let $custom_growth_rate = $('#custom-growth-rate').val();
44+
if ($custom_growth_rate.length == 0) {
45+
$custom_growth_rate = 0;
46+
}
47+
4348
// Start loading
4449
loader.show();
4550

4651
// Post the data to the path.
47-
let posting = $.post(path, { ticker: $ticker } );
52+
let posting = $.post(path, { ticker: $ticker, custom_growth_rate: $custom_growth_rate } );
4853

4954
posting.fail(function(response) {
5055
$.snackbar({

isthisstockgood/templates/searchbox.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010
placeholder="Ticker Symbol"
1111
maxlength=8/>
1212
</div>
13+
<div class="col-md-2 nopadding">
14+
<input
15+
type="text"
16+
class="form-control"
17+
id="custom-growth-rate"
18+
placeholder="Custom Growth Rate (percent)"
19+
maxlength=8/>
20+
</div>
1321
<div class="col-md-1 nopadding">
1422
<input
1523
type="submit"
@@ -30,6 +38,15 @@
3038
});
3139
</script>
3240

41+
<script>
42+
document.querySelector("#custom-growth-rate").addEventListener("keypress", (event) => {
43+
let key = event.key;
44+
if (!key.match(/[0-9,.\-]/)) {
45+
event.preventDefault();
46+
}
47+
});
48+
</script>
49+
3350
<script>
3451
{% include "js/search.js" %}
3552
</script>

pyproject.toml

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
1-
[tool.poetry]
1+
[project]
22
name = "IsThisStockGood"
33
version = "0.1.0"
44
description = ""
5-
authors = ["Mark Klara"]
5+
authors = [{ name = "Mark Klara" }]
6+
requires-python = "~=3.9"
67
readme = "README.md"
8+
dependencies = [
9+
"requests-futures>=1.0.1,<2",
10+
"wheel>=0.43.0,<0.44",
11+
"lxml>=5.2.2,<6",
12+
"flask==3.0.3",
13+
"virtualenv>=20.26.6,<21",
14+
]
715

8-
[tool.poetry.dependencies]
9-
python = "^3.8"
10-
requests-futures = "^1.0.1"
11-
wheel = "^0.43.0"
12-
lxml = "^5.2.2"
13-
flask = "3.0.3"
14-
virtualenv = "^20.26.6"
15-
16-
[tool.poetry.group.dev.dependencies]
17-
pytest = "^8.2.1"
16+
[dependency-groups]
17+
dev = [
18+
"pytest>=8.4.2,<9",
19+
]
1820

1921
[build-system]
20-
requires = ["poetry-core"]
21-
build-backend = "poetry.core.masonry.api"
22+
requires = ["hatchling"]
23+
build-backend = "hatchling.build"
2224

2325
[tool.pylint]
2426
indent-string=" "

tests/test_api.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def test_get_data():
1919
with app.test_client() as test_client:
2020
test_client = app.test_client()
2121

22-
res = test_client.get('/api/ticker/nvda')
22+
res = test_client.get('/api/ticker/lulu/1')
2323
assert res.status_code == 200
2424

2525
data = res.json
@@ -32,15 +32,15 @@ def test_get_ten_cap_price():
3232

3333
with app.test_client() as test_client:
3434
test_client = app.test_client()
35-
res = test_client.get('/api/ticker/nvda')
35+
res = test_client.get('/api/ticker/nvda/1')
3636
assert res.json['ten_cap_price'] > 0
3737

3838
def test_ten_cap_price_has_two_places_precision():
3939
app = create_app(fetchDataForTickerSymbol)
4040

4141
with app.test_client() as test_client:
4242
test_client = app.test_client()
43-
res = test_client.get('/api/ticker/nvda')
43+
res = test_client.get('/api/ticker/nvda/1')
4444

4545
price = res.json['ten_cap_price']
4646

0 commit comments

Comments
 (0)