Spaces:
Sleeping
Sleeping
| """Standalone global niche-market discovery using Brave Search + Graham filter. | |
| Uses shared core modules for search, ticker extraction, and logging. | |
| """ | |
| import os | |
| import re | |
| import yfinance as yf | |
| from dotenv import load_dotenv | |
| from src.core.logger import get_logger | |
| from src.core.search import brave_search_raw | |
| from src.core.ticker_utils import normalize_price | |
| from src.finance_tools import check_financial_health | |
| from src.email_utils import send_email_report | |
| from src.global_router import MARKET_CONFIG, normalize_ticker, get_official_filing_link | |
| load_dotenv() | |
| logger = get_logger(__name__) | |
| def extract_tickers(text: str, region: str) -> list[str]: | |
| """Extract stock ticker patterns from raw text with regional awareness.""" | |
| found_tickers: set[str] = set() | |
| # Cashtags: $AAPL | |
| cashtags = re.findall(r"\$([A-Za-z]{2,5})", text) | |
| found_tickers.update(t.upper() for t in cashtags) | |
| # Parenthesised tickers: (AAPL) — skip 2-letter to avoid (US), (UK) | |
| parentheses = re.findall(r"\(([A-Z]{3,5})\)", text) | |
| found_tickers.update(parentheses) | |
| # Regional exchange prefixes | |
| if region == "UK": | |
| uk_tags = re.findall(r"LON:\s?([A-Za-z]{2,5})", text) | |
| found_tickers.update(t.upper() for t in uk_tags) | |
| elif region == "CANADA": | |
| ca_tags = re.findall(r"TSX:\s?([A-Za-z]{2,5})", text) | |
| found_tickers.update(t.upper() for t in ca_tags) | |
| blacklist = {"IPO", "CEO", "YTD", "USD", "GBP", "EUR", "ETF", "EPS", "FYI", "AGM"} | |
| suffix = MARKET_CONFIG[region]["suffix"] | |
| normalized: list[str] = [] | |
| for t in found_tickers: | |
| clean = t.strip().upper() | |
| if clean in blacklist: | |
| continue | |
| if suffix and not clean.endswith(suffix): | |
| clean = f"{clean}{suffix}" | |
| normalized.append(clean) | |
| return list(set(normalized)) | |
| def run_global_hunt(): | |
| """Execute the global niche hunt across all configured markets.""" | |
| logger.info("Starting Agentic Global Scout (Brave Powered)...") | |
| report_html = "<h1>Daily Agentic Scout Report</h1>" | |
| report_html += "<p><i>Generated by scraping real-time market discussions via Brave Search.</i></p>" | |
| search_queries = { | |
| "USA": "undervalued microcap stocks reddit 2026 buy list", | |
| "UK": "best aim stocks to buy 2026 undervalued ukinvesting", | |
| "CANADA": "tsx venture deep value stocks mining reddit", | |
| "AUSTRALIA": "asx small cap gems hotcopper undervalued", | |
| } | |
| for region, config in MARKET_CONFIG.items(): | |
| query = search_queries.get(region) | |
| logger.info("Scout searching %s: '%s'", region, query) | |
| raw_text = brave_search_raw(query) | |
| candidates = extract_tickers(raw_text, region) | |
| logger.info("Found %d potential tickers: %s", len(candidates), candidates) | |
| region_gems = [] | |
| for ticker in candidates: | |
| try: | |
| stock = yf.Ticker(ticker) | |
| info = stock.info | |
| price = info.get("currentPrice") | |
| if not price: | |
| continue | |
| # BUG FIX: pass both ticker AND info to check_financial_health | |
| health = check_financial_health(ticker, info) | |
| if health["status"] == "PASS" or health.get("metrics", {}).get("margin_of_safety", "N/A") != "N/A": | |
| link = get_official_filing_link(ticker, region) | |
| safety = health["metrics"].get("margin_of_safety", "N/A") | |
| color = "green" if "%" in str(safety) and "-" not in str(safety) else "black" | |
| raw_price = info.get("currentPrice", 0) | |
| currency = info.get("currency", "USD") | |
| display_price = normalize_price(raw_price, ticker, currency) | |
| if region == "UK": | |
| display_price_str = f"£{display_price:.2f}" | |
| else: | |
| display_price_str = f"{display_price:.2f} {currency}" | |
| region_gems.append(f""" | |
| <li style="margin-bottom: 10px;"> | |
| <b>{ticker}</b> ({info.get('shortName', 'Unknown')})<br> | |
| Price: {display_price_str}<br> | |
| Verdict: {health['reason']}<br> | |
| Safety Margin: <span style="color:{color}; font-weight:bold;">{safety}</span><br> | |
| <a href="{link}">Verify at {config['gov_source']}</a> | |
| </li> | |
| """) | |
| except Exception as exc: | |
| logger.debug("Error processing %s: %s", ticker, exc) | |
| continue | |
| if region_gems: | |
| report_html += f"<h2>{region} Discovery</h2><ul>{''.join(region_gems)}</ul>" | |
| else: | |
| report_html += f"<h2>{region}</h2><p>Scouted {len(candidates)} tickers, but none met Graham's strict criteria.</p>" | |
| # Email dispatch | |
| users = [ | |
| {"name": "Cisco", "email": os.getenv("EMAIL_CISCO"), "key": os.getenv("RESEND_API_KEY_CISCO")}, | |
| {"name": "Raul", "email": os.getenv("EMAIL_RAUL"), "key": os.getenv("RESEND_API_KEY_RAUL")}, | |
| {"name": "David", "email": os.getenv("EMAIL_DAVID"), "key": os.getenv("RESEND_API_KEY_DAVID")}, | |
| ] | |
| logger.info("Preparing dispatch for %d agents...", len(users)) | |
| for user in users: | |
| if user["email"] and user["key"]: | |
| logger.info("Sending to %s...", user["name"]) | |
| try: | |
| send_email_report( | |
| subject="Agentic Scout Report", | |
| html_content=report_html, | |
| recipient=user["email"], | |
| api_key=user["key"], | |
| ) | |
| except Exception as exc: | |
| logger.error("Failed to send to %s: %s", user["name"], exc) | |
| else: | |
| logger.info("Skipping %s (missing credentials)", user["name"]) | |
| if __name__ == "__main__": | |
| run_global_hunt() | |