PrimoGreedy-Agent / src /niche_hunter.py
CiscsoPonce's picture
feat: major architecture refactor with yFinance screener, scoring, and insider feeds
8ed954c
"""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()