market-ranker-tanker 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import yfinance as yf
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
import ta # pip install ta
|
|
5
|
+
from ta.volatility import BollingerBands, AverageTrueRange
|
|
6
|
+
from ta.trend import MACD, EMAIndicator, SMAIndicator, ADXIndicator
|
|
7
|
+
from ta.momentum import RSIIndicator, StochasticOscillator
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
# Configure logging
|
|
11
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# Ensure pandas prints all rows/columns
|
|
15
|
+
pd.set_option('display.max_rows', None)
|
|
16
|
+
pd.set_option('display.max_columns', None)
|
|
17
|
+
pd.set_option('display.width', 1000)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DataFetcher:
|
|
21
|
+
"""
|
|
22
|
+
Fetches raw market data.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, tickers):
|
|
26
|
+
self.tickers = tickers
|
|
27
|
+
|
|
28
|
+
def fetch_data(self, period="2y", interval="1d"):
|
|
29
|
+
price_data = {}
|
|
30
|
+
fundamental_data = {}
|
|
31
|
+
|
|
32
|
+
logger.info(f"Fetching data for {len(self.tickers)} tickers...")
|
|
33
|
+
|
|
34
|
+
for symbol in self.tickers:
|
|
35
|
+
try:
|
|
36
|
+
ticker_obj = yf.Ticker(symbol)
|
|
37
|
+
|
|
38
|
+
# 1. Get Historical Price Data
|
|
39
|
+
hist = ticker_obj.history(period=period, interval=interval)
|
|
40
|
+
if not hist.empty:
|
|
41
|
+
price_data[symbol] = hist
|
|
42
|
+
|
|
43
|
+
# 2. Get Fundamental Info
|
|
44
|
+
info = ticker_obj.info
|
|
45
|
+
if info and 'regularMarketPrice' in info:
|
|
46
|
+
fundamental_data[symbol] = info
|
|
47
|
+
else:
|
|
48
|
+
logger.warning(f"Incomplete fundamental data for {symbol}")
|
|
49
|
+
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.error(f"Failed to fetch {symbol}: {e}")
|
|
52
|
+
|
|
53
|
+
return price_data, fundamental_data
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FundamentalAnalyzer:
|
|
57
|
+
"""
|
|
58
|
+
Calculates advanced valuation and health metrics (Graham, Lynch, Altman).
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, fundamental_data):
|
|
62
|
+
self.data = fundamental_data
|
|
63
|
+
|
|
64
|
+
def analyze(self):
|
|
65
|
+
results = []
|
|
66
|
+
for ticker, info in self.data.items():
|
|
67
|
+
metrics = self._extract_metrics(ticker, info)
|
|
68
|
+
results.append(metrics)
|
|
69
|
+
|
|
70
|
+
df = pd.DataFrame(results)
|
|
71
|
+
if not df.empty:
|
|
72
|
+
df.set_index('Ticker', inplace=True)
|
|
73
|
+
return df
|
|
74
|
+
|
|
75
|
+
def _extract_metrics(self, ticker, info):
|
|
76
|
+
"""Extracts and Calculates Advanced Metrics."""
|
|
77
|
+
|
|
78
|
+
# --- Basic Data Points ---
|
|
79
|
+
price = info.get('currentPrice', info.get('regularMarketPrice', 0))
|
|
80
|
+
eps = info.get('trailingEps', 0)
|
|
81
|
+
book_val = info.get('bookValue', 0)
|
|
82
|
+
pe = info.get('trailingPE', float('nan'))
|
|
83
|
+
growth_rate = info.get('earningsGrowth', 0) # This is usually a decimal (e.g. 0.15 for 15%)
|
|
84
|
+
div_yield = (info.get('dividendYield', 0) or 0) * 100
|
|
85
|
+
|
|
86
|
+
# --- 1. Benjamin Graham Number ---
|
|
87
|
+
# Formula: Sqrt(22.5 * EPS * Book Value)
|
|
88
|
+
# It represents the theoretical maximum price for a defensive investor.
|
|
89
|
+
graham_num = 0
|
|
90
|
+
if eps > 0 and book_val > 0:
|
|
91
|
+
graham_num = np.sqrt(22.5 * eps * book_val)
|
|
92
|
+
|
|
93
|
+
# Graham Upside: How much room to grow until it hits fair value?
|
|
94
|
+
graham_upside = 0
|
|
95
|
+
if graham_num > 0 and price > 0:
|
|
96
|
+
graham_upside = (graham_num - price) / price
|
|
97
|
+
|
|
98
|
+
# --- 2. Peter Lynch Fair Value ---
|
|
99
|
+
# Rule of Thumb: A fair P/E equals the Growth Rate.
|
|
100
|
+
# Fair Value = EPS * (Earnings Growth * 100)
|
|
101
|
+
lynch_fair_value = 0
|
|
102
|
+
if eps > 0 and growth_rate > 0:
|
|
103
|
+
# We use growth * 100 because Lynch used the whole number (e.g. 15 for 15%)
|
|
104
|
+
lynch_fair_value = eps * (growth_rate * 100)
|
|
105
|
+
# Cap extreme growth assumptions to keep it realistic (max 25% growth)
|
|
106
|
+
if (growth_rate * 100) > 25:
|
|
107
|
+
lynch_fair_value = eps * 25
|
|
108
|
+
|
|
109
|
+
lynch_upside = 0
|
|
110
|
+
if lynch_fair_value > 0 and price > 0:
|
|
111
|
+
lynch_upside = (lynch_fair_value - price) / price
|
|
112
|
+
|
|
113
|
+
# --- 3. Altman Z-Score (Simplified Proxy) ---
|
|
114
|
+
# A full Z-score requires balance sheet items often not in 'info' (Working Capital, Retained Earnings).
|
|
115
|
+
# We use a proxy based on available data:
|
|
116
|
+
# High Current Ratio + Low Debt + Positive Earnings = High Health Score
|
|
117
|
+
current_ratio = info.get('currentRatio', 0)
|
|
118
|
+
debt_to_equity = info.get('debtToEquity', 0)
|
|
119
|
+
|
|
120
|
+
# Create a customized "Health Score" (Higher is better)
|
|
121
|
+
# Logic: Reward liquidity, Penalize leverage
|
|
122
|
+
# Note: We divide Debt by 100 because Yahoo returns it as a percentage (e.g. 150 for 150%)
|
|
123
|
+
health_score = 0
|
|
124
|
+
if pd.notna(current_ratio) and pd.notna(debt_to_equity):
|
|
125
|
+
# Simple logic: (Liquidity * 3) - (Leverage ratio)
|
|
126
|
+
# A score > 1 is generally decent in this proxy model
|
|
127
|
+
health_score = (current_ratio * 3) - (debt_to_equity / 100)
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
'Ticker': ticker,
|
|
131
|
+
'Price': price,
|
|
132
|
+
'PE_Ratio': pe,
|
|
133
|
+
'PEG_Ratio': info.get('pegRatio', float('nan')),
|
|
134
|
+
'ROE': info.get('returnOnEquity', 0),
|
|
135
|
+
'FCF_Yield': (info.get('freeCashflow', 0) / info.get('marketCap', 1)) if info.get('marketCap') else 0,
|
|
136
|
+
|
|
137
|
+
# New Advanced Metrics
|
|
138
|
+
'Graham_Number': graham_num,
|
|
139
|
+
'Graham_Upside': graham_upside, # Rank High -> Better
|
|
140
|
+
'Lynch_Fair_Value': lynch_fair_value,
|
|
141
|
+
'Lynch_Upside': lynch_upside, # Rank High -> Better
|
|
142
|
+
'Fin_Health_Score': health_score # Rank High -> Better
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class TechnicalAnalyzer:
|
|
147
|
+
"""
|
|
148
|
+
Calculates technical indicators.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def __init__(self, price_data):
|
|
152
|
+
self.price_data = price_data
|
|
153
|
+
|
|
154
|
+
def analyze(self):
|
|
155
|
+
results = []
|
|
156
|
+
for ticker, df in self.price_data.items():
|
|
157
|
+
if len(df) < 200:
|
|
158
|
+
continue
|
|
159
|
+
df = df.copy()
|
|
160
|
+
|
|
161
|
+
# Indicators
|
|
162
|
+
df['RSI'] = RSIIndicator(close=df['Close'], window=14).rsi()
|
|
163
|
+
macd = MACD(close=df['Close'])
|
|
164
|
+
df['MACD_Diff'] = macd.macd_diff()
|
|
165
|
+
adx = ADXIndicator(high=df['High'], low=df['Low'], close=df['Close'], window=14)
|
|
166
|
+
df['ADX'] = adx.adx()
|
|
167
|
+
df['SMA_200'] = SMAIndicator(close=df['Close'], window=200).sma_indicator()
|
|
168
|
+
df['Dist_SMA200'] = (df['Close'] - df['SMA_200']) / df['SMA_200']
|
|
169
|
+
|
|
170
|
+
latest = df.iloc[-1]
|
|
171
|
+
results.append({
|
|
172
|
+
'Ticker': ticker,
|
|
173
|
+
'Close_Price': latest['Close'],
|
|
174
|
+
'RSI': latest['RSI'],
|
|
175
|
+
'MACD_Diff': latest['MACD_Diff'],
|
|
176
|
+
'ADX': latest['ADX'],
|
|
177
|
+
'Dist_SMA200': latest['Dist_SMA200']
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
df = pd.DataFrame(results)
|
|
181
|
+
if not df.empty:
|
|
182
|
+
df.set_index('Ticker', inplace=True)
|
|
183
|
+
return df
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class MarketRanker:
|
|
187
|
+
"""
|
|
188
|
+
Calculates Percentile Ranks.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
def __init__(self, fund_df, tech_df):
|
|
192
|
+
self.fund_df = fund_df
|
|
193
|
+
self.tech_df = tech_df
|
|
194
|
+
|
|
195
|
+
def _calculate_percentile_rank(self, df, higher_is_better_cols, lower_is_better_cols):
|
|
196
|
+
if df.empty: return df
|
|
197
|
+
rank_df = pd.DataFrame(index=df.index)
|
|
198
|
+
|
|
199
|
+
for col in higher_is_better_cols:
|
|
200
|
+
if col in df.columns:
|
|
201
|
+
rank_df[col] = df[col].rank(pct=True, ascending=True)
|
|
202
|
+
|
|
203
|
+
for col in lower_is_better_cols:
|
|
204
|
+
if col in df.columns:
|
|
205
|
+
rank_df[col] = df[col].rank(pct=True, ascending=False)
|
|
206
|
+
|
|
207
|
+
df['Avg_Rank_Percentile'] = rank_df.mean(axis=1) * 100
|
|
208
|
+
return df.sort_values(by='Avg_Rank_Percentile', ascending=False)
|
|
209
|
+
|
|
210
|
+
def get_fundamental_rank(self):
|
|
211
|
+
# Updated Logic: Includes Graham & Lynch Upside + Health Score
|
|
212
|
+
high_better = ['ROE', 'FCF_Yield', 'Graham_Upside', 'Lynch_Upside', 'Fin_Health_Score']
|
|
213
|
+
low_better = ['PE_Ratio', 'PEG_Ratio']
|
|
214
|
+
|
|
215
|
+
return self._calculate_percentile_rank(self.fund_df.copy(), high_better, low_better)
|
|
216
|
+
|
|
217
|
+
def get_technical_rank(self):
|
|
218
|
+
high_better = ['RSI', 'MACD_Diff', 'Dist_SMA200', 'ADX']
|
|
219
|
+
low_better = []
|
|
220
|
+
return self._calculate_percentile_rank(self.tech_df.copy(), high_better, low_better)
|
|
221
|
+
|
|
222
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: market-ranker-tanker
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A multi-factor stock ranking tool combining Graham/Lynch valuations with Technical Analysis.
|
|
5
|
+
Author-email: MarcvanNiekerk <info@quickcart.store>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Requires-Dist: pandas
|
|
13
|
+
Requires-Dist: ta
|
|
14
|
+
Requires-Dist: yfinance
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# Market Ranker Tanker
|
|
18
|
+
|
|
19
|
+
A professional-grade stock screening library that combines **Fundamental Valuation Models** (Graham Number, Peter Lynch Fair Value) with **Technical Analysis** (RSI, MACD, Trends).
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
```bash
|
|
23
|
+
pip install market-ranker-tanker
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
market_ranker_tanker/__init__.py,sha256=Sep8B1NBScPrgw7ABh_CI4Xr51q9dVAWiNlYHmfk17E,110
|
|
2
|
+
market_ranker_tanker/analyzer.py,sha256=kFu5uCx9h2AGac7PH-s5W9DXX_ygxquaJYc1fBAcSNw,8053
|
|
3
|
+
market_ranker_tanker-0.0.1.dist-info/METADATA,sha256=lClBUpMqEj1QFbsyTqUkRIvOQ0SlExKV5YrcPwLXs2U,797
|
|
4
|
+
market_ranker_tanker-0.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
5
|
+
market_ranker_tanker-0.0.1.dist-info/licenses/LICENSE,sha256=wOgISSk1XeKJGraWXReum1kmz30tyeQ6yG7HjPuub00,1070
|
|
6
|
+
market_ranker_tanker-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MarcvanNiekerk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|