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:
- 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.
- 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.
- 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:
- NVIDIA Corporation (NVDA): $222.27 | +1.34% | Market Cap $5,367B. Thesis: Sustained AI accelerated computing demand. Stop-Loss: $205.00.
- Intel Corporation (INTC): $108.60 | -0.18% | Market Cap $574B. Thesis: Foundry turnaround and enterprise Gaudi accelerator ramp. Stop-Loss: $98.00.
- 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
- Zero Data Costs: Combining BeautifulSoup4 with local Ollama inference gives individual developers enterprise-grade market intelligence without paying thousands in API subscription fees.
- Eliminating Date Hallucination: Injecting a hardware timestamp anchor during data ingestion completely eliminates temporal drift in local LLMs.
- 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:
- Lab 1: High-Performance Local LLMs with Gemma 3 & Token Streaming
- Lab 2: Pydantic Schemas & Constrained Tool Calling
- Lab 3: OpsAgent — Autonomous DevOps & Hardware Telemetry Copilot
- Lab 4 (This Guide): StockAgent — Autonomous Market Screener & Scraper
Download all four Jupyter Notebooks from our official GitHub repository: MysteryBytes-Labs/agentic_ai.
