AI HANDS-ON

Build an Autonomous Stock Screener with Gemma 3 & Python (Lab 4: StockAgent)

Published on 2026-09-22

Say goodbye to expensive commercial financial APIs and rigid stock screeners. Build StockAgent: a 100% local, autonomous financial market screener and stock recommender that scrapes live Yahoo Finance data, eliminates penny stocks, and solves date hallucination with Temporal Grounding.

Watch on YouTube

Watch Full Video on YouTube: https://www.youtube.com/watch?v=SR7taGM-XSo

Download notebook to follow along: https://github.com/MysteryBytes-Labs/agentic_ai/tree/main/local-agentic-ai-gemma3/


The Problem: Commercial API Costs & LLM Date Hallucination

Building an automated financial market intelligence system typically faces three formidable roadblocks:

  1. Astronomical API Subscription Costs: Commercial financial data feeds (Bloomberg, FactSet, premium REST APIs) cost hundreds to thousands of dollars per month with strict rate limits.
  2. The "Date Hallucination" Trap: Frozen LLM weights lack an internal real-time clock. When presented with market data, pretrained models frequently default to their training cutoff (such as October 2023), outputting stale or fictionalized market contexts.
  3. The Penny Stock Hazard: Public market screeners frequently flood retail investors with volatile sub-$5 micro-caps vulnerable to illiquidity, wide bid-ask spreads, and algorithmic manipulation.

The Solution: In this series finale, we build StockAgent—a 100% local, zero-subscription financial copilot powered by Google Gemma 3 (4B) and Ollama. StockAgent directly scrapes live market tables from Yahoo Finance, enforces an institutional Penny Stock Purge ($\text{Price} \ge $5.00$, $\text{Market Cap} \ge $1.0\text{B}$), and anchors every generation to the live hardware clock via Temporal Grounding.


Traditional Screeners vs. StockAgent

Feature & Safety Boundary Traditional Stock Screeners StockAgent (Local Gemma 3 + Pydantic)
Query Flexibility Hardcoded filter menus & dropdowns Natural language conversational intelligence
Risk Guardrails Manual ticker checking Automated penny stock purge ($Price \ge $5$, $Cap \ge $1\text{B}$)
Data Ingestion Paid proprietary vendor APIs Direct, autonomous web scraping from Yahoo Finance
Decision Synthesis Raw numerical data dumps Institutional investment memos with catalysts & stop-losses
Temporal Accuracy Vulnerable to stale LLM date assumptions Hardware clock anchored (Temporal Grounding)
Data Sovereignty Cloud tracking of investor watchlists 100% Local & Private analysis on your own hardware

3-Phase Closed-Loop Execution Architecture

StockAgent operates through a deterministic, three-phase autonomous cycle:

┌────────────────────────────────────────────────────────┐
│             User Financial Market Inquiry              │
│  "Scrape Yahoo Finance for today's most active stocks  │
│   and recommend the 3 most promising opportunities."   │
└──────────────────────────┬─────────────────────────────┘
                           │
                           ▼
┌────────────────────────────────────────────────────────┐
│       Phase 1: Structured Intent Analysis (Ollama)     │
│  Gemma 3 analyzes prompt & outputs StockAgentDecision  │
│  specifying market section & Pydantic filter bounds    │
└──────────────────────────┬─────────────────────────────┘
                           │
                           ▼
┌────────────────────────────────────────────────────────┐
│       Phase 2: Live Scraping & Penny Stock Purge       │
│  BeautifulSoup extracts live HTML market tables.       │
│  Applies $5 price and $1B cap filters.                 │
│  Injects authoritative hardware timestamp anchor.      │
└──────────────────────────┬─────────────────────────────┘
                           │
                           ▼
┌────────────────────────────────────────────────────────┐
│       Phase 3: Temporally Grounded Synthesis           │
│  Gemma 3 analyzes safe candidates and synthesizes     │
│  an institutional Top-3 stock recommendation memo.     │
└────────────────────────────────────────────────────────┘

Step 1: Environment Setup & Hardware Clock Verification

We initialize our local Ollama client and verify connectivity to the local daemon alongside web scraping dependencies:

pip install -q ollama pydantic requests beautifulsoup4 ipython
from pydantic import BaseModel, Field
from typing import Optional, Literal, Dict, Any, List
import json
import re
from datetime import datetime
import requests
from bs4 import BeautifulSoup
import platform
import ollama
from IPython.display import Markdown, display

# Connect to local Ollama daemon
client = ollama.Client(host='http://localhost:11434')
MODEL_NAME = 'gemma3:4b'

print(f"✅ Ollama client connected to {client._client.base_url}")
print(f"  Target Model : {MODEL_NAME}")
print(f"  Host System  : {platform.system()} {platform.machine()}")
print(f"  System Time  : {datetime.now().strftime('%B %d, %Y (%I:%M %p)')}")
print("  Web Scraper  : Requests + BeautifulSoup4 active")

Step 2: Defining Strongly Typed Financial Tool Schemas

To prevent the model from hallucinating invalid parameters or bypassing institutional risk guardrails, we define two explicit Pydantic schemas:

1. Yahoo Finance Scraper Schema (YahooScraperParams)

Enforces minimum price ($\ge $5.00$) and market capitalization ($\ge $1.0\text{B}$) at the schema boundary:

class YahooScraperParams(BaseModel):
    """Parameters for scraping and filtering Yahoo Finance market tables."""
    section: Literal["most_active", "gainers"] = Field(
        "most_active", description="Market section to scrape: 'most_active' or 'gainers'"
    )
    min_price: float = Field(
        5.0, ge=5.0, description="Strict penny stock exclusion: minimum share price in USD (must be >= $5.00)"
    )
    min_market_cap_b: float = Field(
        1.0, ge=0.3, description="Micro-cap exclusion: minimum market capitalization in Billions USD (must be >= $1.0B)"
    )
    limit: int = Field(
        10, ge=3, le=25, description="Maximum number of candidate stocks to return"
    )

2. Fundamental Screener Schema (StockFilterParams)

class StockFilterParams(BaseModel):
    """Parameters for fine-grained fundamental and technical screening."""
    strategy: Literal["momentum", "value", "growth", "balanced"] = Field(
        "balanced", description="Screening strategy: 'momentum', 'value', 'growth', or 'balanced'"
    )
    max_pe_ratio: Optional[float] = Field(
        None, ge=1.0, description="Optional maximum P/E ratio ceiling for value screening"
    )

Step 3: Compiling the Unified StockAgent Router Contract

We combine the tool parameters into a single master decision contract: StockAgentDecision. Gemma 3 evaluates user prompts and generates strictly conforming JSON:

class StockAgentDecision(BaseModel):
    """Unified routing schema for StockAgent autonomous decisions."""
    tool: Literal["scrape_market", "screen_stocks", "none"] = Field(
        ..., description="Selected tool name: 'scrape_market', 'screen_stocks', or 'none' for direct financial concepts"
    )
    reasoning: str = Field(..., description="Financial analyst rationale for selecting this action")
    scraper_params: Optional[YahooScraperParams] = Field(
        None, description="Parameters if 'scrape_market' is selected"
    )
    filter_params: Optional[StockFilterParams] = Field(
        None, description="Parameters if 'screen_stocks' is selected"
    )

stock_agent_schema = StockAgentDecision.model_json_schema()
print("✓ Unified StockAgent Decision Schema compiled for Ollama constrained decoding.")

Step 4: The Live Scraper, Penny Stock Purge & Temporal Grounding

Here we implement the web scraping engine with three crucial engineering primitives:

1. Robust Numerical Normalization

Scraped HTML tables contain messy string representations like $222.27, 188.8M, and 5.367T. We normalize market caps into numerical billions and clean price floats:

def parse_market_cap(val_str: str) -> float:
    """Convert market cap string ('5.367T', '59.79B', '450M') to numeric billions."""
    val_str = val_str.strip().upper()
    if not val_str or val_str in ('N/A', '-', '--'):
        return 0.0
    mult = 1.0
    if val_str.endswith('T'):
        mult = 1000.0
        val_str = val_str[:-1]
    elif val_str.endswith('B'):
        mult = 1.0
        val_str = val_str[:-1]
    elif val_str.endswith('M'):
        mult = 0.001
        val_str = val_str[:-1]
    try:
        return float(val_str.replace(',', '')) * mult
    except ValueError:
        return 0.0

def parse_price(price_str: str) -> float:
    """Extract numeric price from scraped cell strings."""
    match = re.search(r'([0-9]+\.[0-9]+|[0-9]+)', price_str.replace(',', ''))
    return float(match.group(1)) if match else 0.0

2. The Penny Stock Purge

Every scraped ticker must satisfy strict institutional quality thresholds:

def filter_penny_stocks(stocks: List[Dict[str, Any]], min_price: float, min_mcap_b: float):
    """Purges penny stocks (< min_price) and illiquid micro-caps (< min_mcap_b)."""
    passed = []
    purged = 0
    for s in stocks:
        if s['price'] >= min_price and s['market_cap_b'] >= min_mcap_b:
            passed.append(s)
        else:
            purged += 1
    return passed, purged

3. Temporal Grounding at Ingestion

To eliminate date hallucinations, we inject the live system hardware timestamp directly into the scraper payload:

def scrape_yahoo_finance(params: YahooScraperParams) -> Dict[str, Any]:
    """Scrapes Yahoo Finance live tables, applies penny-stock filtration, and injects temporal grounding."""
    endpoint_map = {
        "most_active": "https://finance.yahoo.com/markets/stocks/most-active/",
        "gainers": "https://finance.yahoo.com/markets/stocks/gainers/"
    }
    url = endpoint_map.get(params.section, endpoint_map["most_active"])
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    }

    # Generate Temporal Grounding Anchor for this ingestion cycle
    now_dt = datetime.now()
    live_timestamp = now_dt.strftime("%B %d, %Y (%I:%M %p %Z)")
    market_date = now_dt.strftime("%Y-%m-%d")

    raw_stocks = []
    try:
        resp = requests.get(url, headers=headers, timeout=10)
        if resp.status_code == 200:
            soup = BeautifulSoup(resp.text, 'html.parser')
            rows = soup.find_all('tr')
            for row in rows[1:]:
                cols = [td.get_text(strip=True) for td in row.find_all('td')]
                if len(cols) >= 9:
                    raw_stocks.append({
                        'symbol': cols[0],
                        'name': cols[1],
                        'price': parse_price(cols[3]),
                        'change_pct': cols[5],
                        'volume': cols[6],
                        'market_cap_b': round(parse_market_cap(cols[8]), 2),
                        'pe_ratio': cols[9] if len(cols) > 9 and cols[9] not in ('-', '--') else 'N/A'
                    })
    except Exception as e:
        print(f"⚠️ Live scraper notice: {e}")

    # Fallback to offline cache if network blocked
    if not raw_stocks:
        raw_stocks = get_fallback_snapshot()

    passed_stocks, purged_count = filter_penny_stocks(raw_stocks, params.min_price, params.min_market_cap_b)

    return {
        "section": params.section,
        "temporal_grounding": {
            "scan_timestamp": live_timestamp,
            "market_date": market_date,
            "source": "Yahoo Finance Live Telemetry"
        },
        "total_scraped": len(raw_stocks),
        "purged_penny_stocks": purged_count,
        "min_price_threshold": f"${params.min_price:.2f}",
        "min_market_cap_threshold": f"${params.min_market_cap_b:.1f}B",
        "candidates": passed_stocks[:params.limit]
    }

TOOL_REGISTRY = {
    "scrape_market": scrape_yahoo_finance
}

Step 5: Compiling the Autonomous StockAgent Loop

We assemble the complete three-phase loop into run_stock_agent():

def run_stock_agent(user_query: str) -> str:
    print(f"\n💬 User Query: \"{user_query}\"")

    # Phase 1: Structured Intent Analysis & Routing
    response = client.chat(
        model=MODEL_NAME,
        messages=[
            {
                "role": "system",
                "content": (
                    "You are StockAgent, an elite financial research assistant and stock market screener. "
                    "Follow these tool selection rules strictly:\n"
                    "1. If the user asks to scrape, find, screen, or recommend stocks from Yahoo Finance, select 'scrape_market'.\n"
                    "2. Enforce strict risk boundaries: always set min_price >= 5.0 and min_market_cap_b >= 1.0 to avoid penny stocks.\n"
                    "3. Select 'none' only for purely conceptual, historical, or theoretical financial questions."
                )
            },
            {"role": "user", "content": user_query}
        ],
        format=stock_agent_schema
    )

    decision = StockAgentDecision.model_validate_json(response["message"]["content"])
    print(f"🤖 Agent Reasoning: {decision.reasoning}")
    print(f"🔧 Tool Selected  : {decision.tool}")

    if decision.tool == "none":
        return decision.reasoning

    # Phase 2: Live Tool Execution with Penny Stock Purge & Temporal Grounding
    tool_func = TOOL_REGISTRY.get(decision.tool, scrape_yahoo_finance)
    tool_params = decision.scraper_params or YahooScraperParams()
    market_data = tool_func(tool_params)

    current_time_str = market_data.get("temporal_grounding", {}).get("scan_timestamp") or datetime.now().strftime("%B %d, %Y (%I:%M %p)")
    market_data["scan_timestamp"] = current_time_str

    print(f"⚙️ Scraper Output : Scraped {market_data['total_scraped']} tickers | Purged {market_data['purged_penny_stocks']} penny/micro-caps")
    print(f"   Safe Candidates: {[s['symbol'] for s in market_data['candidates']]}")
    print(f"🕒 Temporal Anchor: {current_time_str}")

    # Phase 3: Financial Synthesis & Top 3 Recommendations
    synthesis_prompt = f"""You are StockAgent, a senior financial analyst. Based on this verified Yahoo Finance market screening data:
{json.dumps(market_data, indent=2)}

User Request: "{user_query}"
Current Verified Market Timestamp: {current_time_str}

Provide a professional, actionable stock recommendation briefing.
In the report header, explicitly output:
**Report Date:** {current_time_str} (Live Market Telemetry)
Do not assume or hallucinate a past date.

Select the Top 3 Most Promising Stocks from the candidates (strictly avoiding any penny stocks).
For each of the 3 recommended stocks include:
1. **Ticker & Company Name**
2. **Key Financials** (Price, Change %, Market Cap, Volume, P/E)
3. **Investment Thesis & Catalyst** (Why this stock is promising right now)
4. **Risk Factors & Stop-Loss Level**

Conclude with a clear Executive Summary and portfolio risk note."""

    synthesis_res = client.chat(
        model=MODEL_NAME,
        messages=[{"role": "user", "content": synthesis_prompt}]
    )

    return synthesis_res["message"]["content"]

Step 6: Live Multi-Scenario Operational Evaluation

We test StockAgent across three rigorous operational benchmarks:

Scenario 1: Most Active Market Screener & Top-3 Memo

ans1 = run_stock_agent(
    "Scrape Yahoo Finance for today's most active stocks, filter out all penny stocks under $5 and micro-caps, and recommend the 3 most promising opportunities."
)
display(Markdown(ans1))
  • Agent Action: Scrapes Yahoo Finance most_active. Scrapes 25 tickers, purges sub-$5 penny stocks.
  • Top 3 Recommended Opportunities:
    1. NVIDIA Corporation (NVDA): $222.27 | +1.34% | Market Cap $5,367B. Thesis: Sustained AI accelerated computing demand. Stop-Loss: $205.00.
    2. Intel Corporation (INTC): $108.60 | -0.18% | Market Cap $574B. Thesis: Foundry turnaround and enterprise Gaudi accelerator ramp. Stop-Loss: $98.00.
    3. AGNC Investment Corp. (AGNC): $9.87 | -0.40% | Market Cap $11.7B. Thesis: High-yield dividend vehicle benefiting from stabilizing rate spreads. Stop-Loss: $9.00.

Scenario 2: High-Momentum Gainers with Adaptive Boundaries

ans2 = run_stock_agent(
    "Scan today's top market gainers with a minimum share price of $10 and market cap over $2B, and identify the 3 best momentum plays."
)
display(Markdown(ans2))
  • Agent Action: Automatically adjusts YahooScraperParams(min_price=10.0, min_market_cap_b=2.0).
  • Outcome: Filters out volatile small-cap spikes, delivering liquid breakout candidates.

Scenario 3: Conceptual Financial Inquiry (Penny Stock Risk)

ans3 = run_stock_agent(
    "Why are penny stocks considered dangerous for retail investors, and what is pump-and-dump manipulation?"
)
display(Markdown(ans3))
  • Agent Action: Recognizes educational inquiry and routes to tool="none".
  • Outcome: Explains wide spreads, low float dynamics, and micro-cap volatility with zero web scraping overhead.

Key Takeaways

  1. Zero Data Costs: Combining BeautifulSoup4 with local Ollama inference gives individual developers enterprise-grade market intelligence without paying thousands in API subscription fees.
  2. Eliminating Date Hallucination: Injecting a hardware timestamp anchor during data ingestion completely eliminates temporal drift in local LLMs.
  3. Institutional Risk Guardrails: Mathematical filtering inside Python tools ensures high-risk penny stocks never reach the investment synthesis phase.

4-Part Masterclass Complete Curriculum

Congratulations! You have completed the entire Local Agentic AI Masterclass:

Download all four Jupyter Notebooks from our official GitHub repository: MysteryBytes-Labs/agentic_ai.