market-ranker-tanker 0.0.1__tar.gz

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,3 @@
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
@@ -0,0 +1,6 @@
1
+ <component name="InspectionProjectProfileManager">
2
+ <settings>
3
+ <option name="USE_PROJECT_PROFILE" value="false" />
4
+ <version value="1.0" />
5
+ </settings>
6
+ </component>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="PYTHON_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$">
5
+ <excludeFolder url="file://$MODULE_DIR$/.venv" />
6
+ </content>
7
+ <orderEntry type="inheritedJdk" />
8
+ <orderEntry type="sourceFolder" forTests="false" />
9
+ </component>
10
+ </module>
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="Black">
4
+ <option name="sdkName" value="Python 3.10 (market-ranker-tanker)" />
5
+ </component>
6
+ <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (market-ranker-tanker)" project-jdk-type="Python SDK" />
7
+ </project>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/market-ranker-tanker.iml" filepath="$PROJECT_DIR$/.idea/market-ranker-tanker.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
@@ -0,0 +1,46 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="AutoImportSettings">
4
+ <option name="autoReloadType" value="SELECTIVE" />
5
+ </component>
6
+ <component name="ChangeListManager">
7
+ <list default="true" id="94ff210e-906e-4098-816f-498be6ae100d" name="Changes" comment="" />
8
+ <option name="SHOW_DIALOG" value="false" />
9
+ <option name="HIGHLIGHT_CONFLICTS" value="true" />
10
+ <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
11
+ <option name="LAST_RESOLUTION" value="IGNORE" />
12
+ </component>
13
+ <component name="FileTemplateManagerImpl">
14
+ <option name="RECENT_TEMPLATES">
15
+ <list>
16
+ <option value="Python Script" />
17
+ </list>
18
+ </option>
19
+ </component>
20
+ <component name="ProjectColorInfo"><![CDATA[{
21
+ "associatedIndex": 6
22
+ }]]></component>
23
+ <component name="ProjectId" id="399utz1YsnUpssZkzuAOM83Vq8W" />
24
+ <component name="ProjectViewState">
25
+ <option name="hideEmptyMiddlePackages" value="true" />
26
+ <option name="showLibraryContents" value="true" />
27
+ </component>
28
+ <component name="SharedIndexes">
29
+ <attachedChunks>
30
+ <set>
31
+ <option value="bundled-python-sdk-0509580d9d50-746f403e7f0c-com.jetbrains.pycharm.community.sharedIndexes.bundled-PC-241.14494.241" />
32
+ </set>
33
+ </attachedChunks>
34
+ </component>
35
+ <component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
36
+ <component name="TaskManager">
37
+ <task active="true" id="Default" summary="Default task">
38
+ <changelist id="94ff210e-906e-4098-816f-498be6ae100d" name="Changes" comment="" />
39
+ <created>1770120825534</created>
40
+ <option name="number" value="Default" />
41
+ <option name="presentableId" value="Default" />
42
+ <updated>1770120825534</updated>
43
+ </task>
44
+ <servers />
45
+ </component>
46
+ </project>
@@ -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.
@@ -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,7 @@
1
+ # Market Ranker Tanker
2
+
3
+ A professional-grade stock screening library that combines **Fundamental Valuation Models** (Graham Number, Peter Lynch Fair Value) with **Technical Analysis** (RSI, MACD, Trends).
4
+
5
+ ## Installation
6
+ ```bash
7
+ pip install market-ranker-tanker
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "market-ranker-tanker"
7
+ version = "0.0.1"
8
+ authors = [
9
+ { name="MarcvanNiekerk", email="info@quickcart.store" },
10
+ ]
11
+ description = "A multi-factor stock ranking tool combining Graham/Lynch valuations with Technical Analysis."
12
+ readme = "README.md"
13
+ requires-python = ">=3.8"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ dependencies = [
20
+ "yfinance",
21
+ "pandas",
22
+ "ta",
23
+ "numpy"
24
+ ]
25
+
@@ -0,0 +1,3 @@
1
+ from .analyzer import DataFetcher, FundamentalAnalyzer, TechnicalAnalyzer, MarketRanker
2
+
3
+ __version__ = "0.0.1"
@@ -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
+