Skip to content

Commit 0923e14

Browse files
committed
Add scanned count to email and harden startup imports
1 parent 614e415 commit 0923e14

3 files changed

Lines changed: 43 additions & 45 deletions

File tree

main.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -244,31 +244,32 @@ def main():
244244
traceback.print_exc()
245245
return 1
246246

247-
# Gather statistics
247+
# Gather statistics from ProviderRouter (real values, no placeholders)
248248
stats = router.stats
249-
stats['total_losers'] = stats.get('fmp_requests', 0) > 0 # Approximate
250-
stats['after_percent_filter'] = len(stocks) if stocks else 0
251-
252-
# We need to capture these during the run - for now, estimate
253-
# In production, you'd track these in the ProviderRouter
254-
stats['total_losers'] = 50 # Placeholder - actual count from API
255-
stats['after_percent_filter'] = 10 # Placeholder
249+
stats['total_scanned'] = int(stats.get('total_scanned', 0) or 0)
250+
stats['total_losers'] = int(stats.get('total_losers', 0) or 0)
251+
# Step 1 already returns symbols at/under the % threshold, so this equals losers count.
252+
stats['after_percent_filter'] = stats['total_losers']
253+
stats['after_sector_mcap_filter'] = len(stocks)
256254

257255
# Print results
258256
print_results(stocks)
259257

260258
# Print stats
261259
print("\nRun Statistics:")
260+
print(f" Total scanned: {stats.get('total_scanned', 0)}")
261+
print(f" Candidates from losers feed: {stats.get('total_losers', 0)}")
262+
print(f" After sector + mcap filter: {stats.get('after_sector_mcap_filter', len(stocks))}")
262263
print(f" FMP API requests: {stats.get('fmp_requests', 'N/A')}")
263264
if 'twelve_data_credits' in stats:
264265
td_credits = stats['twelve_data_credits']
265266
print(f" Twelve Data credits used today: {td_credits.get('daily_used', 0)}")
266267
print(f" Twelve Data credits remaining: {td_credits.get('daily_remaining', 0)}")
267268
print(f" Verification used: {stats.get('verification_used', False)}")
268269

269-
# Send email (unless dry run)
270+
# Send email (unless dry run). Send even with zero final matches so daily summary is visible.
270271
email_sent = False
271-
if not args.dry_run and stocks:
272+
if not args.dry_run:
272273
print("\n[Email] Preparing to send report...")
273274

274275
gmail = GmailSender()
@@ -287,8 +288,6 @@ def main():
287288
return 1
288289
elif args.dry_run:
289290
print("\n[Dry Run] Email not sent (--dry-run mode)")
290-
elif not stocks:
291-
print("\n[Email] No stocks to report, email not sent")
292291

293292
mark_day_completed(run_timestamp, len(stocks), email_sent)
294293

src/__init__.py

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,34 @@
11
"""
2-
Tech Losers Bot - Daily US Technology Stock Losers Scanner
2+
Tech Losers Bot package exports.
33
4-
This module provides:
5-
- FMP API integration for market data
6-
- Twelve Data API integration for verification
7-
- SMTP email delivery for reports
8-
- SQLite caching for profile data
9-
- Credit tracking for rate-limited APIs
4+
Use lazy imports so importing `src.config` does not eagerly import every module.
5+
This avoids startup failures from unrelated optional modules.
106
"""
117

12-
from .config import (
13-
FMP_API_KEY,
14-
TWELVE_DATA_API_KEY,
15-
MIN_PERCENT_CHANGE,
16-
MIN_MARKET_CAP,
17-
TARGET_SECTOR
18-
)
19-
20-
from .fmp_client import FMPClient, LoserStock, CompanyProfile
21-
from .twelve_data_client import TwelveDataClient
22-
from .provider_router import ProviderRouter, FilteredStock
23-
from .gmail_sender import GmailSender
24-
from .cache import ProfileCache
25-
from .credit_tracker import CreditTracker
8+
from importlib import import_module
269

2710
__version__ = "1.0.0"
28-
__all__ = [
29-
"FMPClient",
30-
"TwelveDataClient",
31-
"ProviderRouter",
32-
"GmailSender",
33-
"ProfileCache",
34-
"CreditTracker",
35-
"LoserStock",
36-
"CompanyProfile",
37-
"FilteredStock"
38-
]
11+
12+
_EXPORTS = {
13+
"FMPClient": (".fmp_client", "FMPClient"),
14+
"TwelveDataClient": (".twelve_data_client", "TwelveDataClient"),
15+
"ProviderRouter": (".provider_router", "ProviderRouter"),
16+
"GmailSender": (".gmail_sender", "GmailSender"),
17+
"ProfileCache": (".cache", "ProfileCache"),
18+
"CreditTracker": (".credit_tracker", "CreditTracker"),
19+
"LoserStock": (".fmp_client", "LoserStock"),
20+
"CompanyProfile": (".fmp_client", "CompanyProfile"),
21+
"FilteredStock": (".provider_router", "FilteredStock"),
22+
}
23+
24+
__all__ = list(_EXPORTS.keys()) + ["__version__"]
25+
26+
27+
def __getattr__(name):
28+
if name in _EXPORTS:
29+
module_name, attr_name = _EXPORTS[name]
30+
module = import_module(module_name, __name__)
31+
value = getattr(module, attr_name)
32+
globals()[name] = value
33+
return value
34+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

src/gmail_sender.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,14 +158,16 @@ def _generate_body(
158158

159159
timestamp_str = run_timestamp.strftime("%Y-%m-%d %H:%M:%S")
160160

161+
total_scanned = stats.get('total_scanned', 0)
161162
total_losers = stats.get('total_losers', 0)
162163
after_pct_filter = stats.get('after_percent_filter', 0)
163-
after_all_filters = len(stocks)
164+
after_all_filters = stats.get('after_sector_mcap_filter', len(stocks))
164165

165166
text_lines = [
166167
f"Run Timestamp: {timestamp_str} {tz_str}",
167168
"",
168169
"Summary:",
170+
f" - Total scanned: {total_scanned}",
169171
f" - Candidates from losers feed: {total_losers}",
170172
f" - After -5% filter: {after_pct_filter}",
171173
f" - After sector + mcap filter: {after_all_filters}",
@@ -217,6 +219,7 @@ def _generate_body(
217219
f"<p><strong>Run Timestamp:</strong> {timestamp_str} {tz_str}</p>",
218220
"<h3>Summary</h3>",
219221
"<ul>",
222+
f"<li>Total scanned: {total_scanned}</li>",
220223
f"<li>Candidates from losers feed: {total_losers}</li>",
221224
f"<li>After -5% filter: {after_pct_filter}</li>",
222225
f"<li>After sector + mcap filter: {after_all_filters}</li>",

0 commit comments

Comments
 (0)