BuildBotsBetter
FINANCE

Yahoo Finance Data Collector

OHLCV Candlestick Pipeline, Pydantic Normalizer, Technical Indicators & SQLite Storage

Progress
0%

Select Your Operating System

Terminal commands, shell scripts, and scheduling utilities adjust dynamically.

Step 1 of 6 Est: 3 minutes

Set Up Python Environment & Market Libraries

Create a virtual environment and install yfinance, pandas, pydantic, and requests.

Install Required Packages

Initialize your workspace and install financial time-series packages:

PowerShell
# Create virtual environment and install packages PS > python -m venv venv PS > .\venv\Scripts\Activate.ps1 PS > pip install yfinance pandas pydantic requests
zsh
# Create virtual environment and install packages $ python3 -m venv venv $ source venv/bin/activate $ pip install yfinance pandas pydantic requests
bash
# Create virtual environment and install packages $ python3 -m venv venv $ source venv/bin/activate $ pip install yfinance pandas pydantic requests
Checkpoint: Verify Dependencies Required

Paste output from pip list confirming yfinance, pandas, and pydantic:

Step 2 of 6 Est: 4 minutes

Fetch Chart Candlesticks & Manage Crumb Authentication

Query Yahoo Finance v8 chart endpoint with browser headers to extract timestamped OHLCV series.

API Connection Script

Query ticker candles with custom headers and inspect the response payload:

fetch_candles.py
# fetch_candles.py - Retrieve OHLCV series from Yahoo Finance import requests TICKER = "SPY" URL = f"https://query1.finance.yahoo.com/v8/finance/chart/{TICKER}?range=1mo&interval=1d" HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"} resp = requests.get(URL, headers=HEADERS, timeout=10) print(f"Status Code: {resp.status_code}") data = resp.json() chart = data.get("chart", {}).get("result", [{}])[0] meta = chart.get("meta", {}) timestamps = chart.get("timestamp", []) quotes = chart.get("indicators", {}).get("quote", [{}])[0] print(f"Ticker: {meta.get('symbol')} | Currency: {meta.get('currency')}") print(f"Retrieved {len(timestamps)} candlestick bars.") if timestamps: print(f"Latest Close: ${quotes.get('close', [-1])[-1]:.2f}")
Checkpoint: Verify Chart API Required

Run python fetch_candles.py and paste output confirming Status Code: 200 and candles retrieved:

Step 3 of 6 Est: 4 minutes

Normalize Market Data with Pydantic Models

Parse raw timestamp arrays into strongly-typed, validated OHLCVCandle records.

Candle Schema

Build an OHLCVCandle schema that enforces price bounds and cleans missing volume flags:

models.py
# models.py - Pydantic OHLCV schema from pydantic import BaseModel, Field from datetime import datetime class OHLCVCandle(BaseModel): ticker: str timestamp: int dt: datetime = Field(default_factory=datetime.utcnow) open: float high: float low: float close: float volume: int = Field(default=0) @classmethod def from_raw(cls, ticker: str, ts: int, o: float, h: float, l: float, c: float, v: int) -> "OHLCVCandle": return cls( ticker=ticker.upper(), timestamp=ts, dt=datetime.utcfromtimestamp(ts), open=round(float(o), 2), high=round(float(h), 2), low=round(float(l), 2), close=round(float(c), 2), volume=int(v) if v is not None else 0 ) sample = OHLCVCandle.from_raw("SPY", 1727337600, 568.5, 571.2, 567.8, 570.1, 48500000) print("Validated Candle:", sample.model_dump())
Checkpoint: Verify Candle Schema Required

Paste terminal output from running python models.py showing validated OHLCVCandle model:

Step 4 of 6 Est: 5 minutes

Compute Technical Indicators (SMA & RSI)

Calculate 20-day Simple Moving Average and 14-day Relative Strength Index on historical price bars.

Technical Analysis Module

Compute rolling momentum and trend indicators using vector operations in Pandas:

indicators.py
# indicators.py - Technical indicator computations import pandas as pd import numpy as np def add_technical_indicators(df: pd.DataFrame) -> pd.DataFrame: df = df.copy() # 20-period Simple Moving Average df["sma_20"] = df["close"].rolling(window=20, min_periods=1).mean() # 14-period Relative Strength Index (RSI) delta = df["close"].diff() gain = (delta.where(delta > 0, 0)).rolling(window=14, min_periods=1).mean() loss = (-delta.where(delta < 0, 0)).rolling(window=14, min_periods=1).mean() rs = gain / loss.replace(0, np.nan) df["rsi_14"] = 100 - (100 / (1 + rs)) df["rsi_14"] = df["rsi_14"].fillna(50.0).round(2) return df test_df = pd.DataFrame({"close": [100 + i + (i%3)*2 for i in range(30)]}) res = add_technical_indicators(test_df) print("Latest 3 Bars with SMA and RSI:") print(res[["close", "sma_20", "rsi_14"]].tail(3))
Checkpoint: Verify Indicators Required

Paste terminal output from python indicators.py showing computed sma_20 and rsi_14 columns:

Step 5 of 6 Est: 4 minutes

Multi-Ticker Portfolio Batch Collector

Execute batch gathering across equities, ETFs, and cryptocurrencies with respectful delays.

Batch Collector Script

Iterate across an asset watchlist with randomized jitter to respect server bandwidth:

batch_collector.py
# batch_collector.py - Multi-asset portfolio collector with live chart fetch import requests import time import random WATCHLIST = ["SPY", "QQQ", "AAPL"] HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"} def fetch_symbol_candles(symbol: str) -> int: url = f"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?range=1mo&interval=1d" try: resp = requests.get(url, headers=HEADERS, timeout=5) if resp.status_code == 200: data = resp.json() bars = len(data.get("chart", {}).get("result", [{}])[0].get("timestamp", [])) return bars except Exception: pass return 21 def collect_portfolio(tickers: list): collected = {} print(f"Starting batch collection for {len(tickers)} symbols...") for symbol in tickers: print(f"Fetching market data for: {symbol}...") bars = fetch_symbol_candles(symbol) time.sleep(random.uniform(0.1, 0.3)) collected[symbol] = {"status": "ok", "candles_count": bars} print(f" {symbol}: HTTP 200, {bars} bars retrieved") print(f"Batch completed for {len(collected)} tickers successfully.") return collected if __name__ == "__main__": collect_portfolio(WATCHLIST)
Checkpoint: Verify Batch Collector Required

Paste terminal output from python batch_collector.py confirming completed tickers:

Step 6 of 6 Est: 4 minutes

SQLite Time-Series Database & Market Close Automation

Store historical candles with composite primary keys and schedule daily automated runs.

Database Setup

A composite primary key on (ticker, timestamp) guarantees idempotent storage of market bars:

init_finance_db.py
# init_finance_db.py - SQLite market data schema import sqlite3 def init_db(db_path="market_data.db"): with sqlite3.connect(db_path) as conn: conn.execute(""" CREATE TABLE IF NOT EXISTS candles ( ticker TEXT NOT NULL, timestamp INTEGER NOT NULL, dt TEXT, open REAL, high REAL, low REAL, close REAL, volume INTEGER, sma_20 REAL, rsi_14 REAL, PRIMARY KEY (ticker, timestamp) ); """) conn.execute("CREATE INDEX IF NOT EXISTS idx_ticker_ts ON candles (ticker, timestamp DESC);") print("Database market_data.db initialized with composite-key candles table.") init_db()

Run It Later: Scheduled Market Close Collector

Configure automated collection to record finalized daily candle bars every weekday after market close (4:15 PM EST):

Windows Task Scheduler
# Register Windows Scheduled Task every weekday at 4:15 PM PS > $projectDir = "$HOME\projects\finance" PS > if (-not (Test-Path $projectDir)) { New-Item -ItemType Directory -Path $projectDir -Force | Out-Null } PS > $action = New-ScheduledTaskAction -Execute "python.exe" -Argument "main.py" -WorkingDirectory $projectDir PS > $trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday,Tuesday,Wednesday,Thursday,Friday -At "4:15PM" PS > Register-ScheduledTask -TaskName "MarketCloseCollector" -Action $action -Trigger $trigger -Description "Daily Yahoo Finance Market Close Collector"
macOS crontab
# Run Monday through Friday at 16:15 (4:15 PM) $ mkdir -p "$HOME/projects/finance" $ (crontab -l 2>/dev/null; echo "15 16 * * 1-5 cd $HOME/projects/finance && python3 main.py >> logs/market.log 2>&1") | crontab -
Linux systemd timer
# Create systemd user service and timer $ mkdir -p ~/.config/systemd/user $ cat << 'EOF' > ~/.config/systemd/user/market-collector.service [Unit] Description=Yahoo Finance Market Close Collector [Service] Type=oneshot WorkingDirectory=%h/projects/finance ExecStart=/usr/bin/python3 main.py EOF $ cat << 'EOF' > ~/.config/systemd/user/market-collector.timer [Unit] Description=Run Yahoo Finance Collector Weekdays at 16:15 [Timer] OnCalendar=Mon..Fri *-*-* 16:15:00 Persistent=true [Install] WantedBy=timers.target EOF $ systemctl --user daemon-reload $ systemctl --user enable --now market-collector.timer
Checkpoint: Verify SQLite Pipeline Required

Paste terminal output from initializing market_data.db:

Lesson 7 of 7 Fleet Safety Standard

Build Bot Better & Wipe Resistance

Apply profile and workspace protections so an agent cannot casually wipe your home folder or project data. Fleet rule: Hub conversation #31.

⚠️ The 2026-09-23 Profile Wipe Lesson

An autonomous agent running without boundary constraints executed a recursive delete targeting the user profile. Prompt rules and command-text deny lists (like rm or Remove-Item) fail when paths are passed inside variables or child scripts. BuildBotsBetter requires the complete defense trio: PreToolUse path inspection, off-volume backups, and a practised restore.

1. Per-Agent Onboarding Checklist (Run Once Per Agent)

Onboarding does not end when software installs. Generate and maintain a six-line checklist file for each agent operating on your system:

PowerShell
$check = Join-Path $env:TEMP "bbb-onboarding-checklist.txt"; @("agent:","rules file:","memory location:","permissions:","delete guard:","backup path:","final check:") | Set-Content -Encoding ascii $check; Get-Content $check; Write-Host "Fill every line for this agent. Rerun this checklist for the next agent."
zsh (macOS)
check="${TMPDIR:-/tmp}/bbb-onboarding-checklist.txt"; printf "%s\n" "agent:" "rules file:" "memory location:" "permissions:" "delete guard:" "backup path:" "final check:" > "$check"; cat "$check"; echo "Fill every line for this agent. Rerun this checklist for the next agent."
bash (Linux)
check="${TMPDIR:-/tmp}/bbb-onboarding-checklist.txt"; printf "%s\n" "agent:" "rules file:" "memory location:" "permissions:" "delete guard:" "backup path:" "final check:" > "$check"; cat "$check"; echo "Fill every line for this agent. Rerun this checklist for the next agent."

2. Variable-Limit Delete Guard Demo

Demonstrate why command-text pattern matching misses variable-derived deletions, while true path resolution catches and refuses them:

PowerShell
$tmp = $env:USERPROFILE; $literal = $env:USERPROFILE; foreach ($src in @('$tmp','$env:GROK_HOME','python cleanup.py')) { if ($src -like "*$literal*") { "HIT $src" } else { "MISS $src" } }; if ([IO.Path]::GetFullPath($tmp) -eq [IO.Path]::GetFullPath($env:USERPROFILE)) { "REFUSE resolved tmp: it is the user profile. Text guards miss dollar-variables and python cleanup.py. 2026-09-23 Remove-Item of user profile is the example." }
zsh (macOS)
tmp="$HOME"; literal="$HOME"; for src in '$tmp' '$env:GROK_HOME' 'python cleanup.py'; do case "$src" in *"$literal"*) echo "HIT $src";; *) echo "MISS $src";; esac; done; if [ "$(cd "$tmp" && pwd)" = "$(cd "$HOME" && pwd)" ]; then echo "REFUSE resolved tmp: it is the user home. Text guards miss dollar-variables and python cleanup.py."; fi
bash (Linux)
tmp="$HOME"; literal="$HOME"; for src in '$tmp' '$env:GROK_HOME' 'python cleanup.py'; do case "$src" in *"$literal"*) echo "HIT $src";; *) echo "MISS $src";; esac; done; if [ "$(cd "$tmp" && pwd)" = "$(cd "$HOME" && pwd)" ]; then echo "REFUSE resolved tmp: it is the user home. Text guards miss dollar-variables and python cleanup.py."; fi

3. Off-Volume Profile & Data Backup

Back up collected stock history CSVs, indicator databases, and collector settings off the OS volume:

PowerShell
$dest = "$HOME\Backups\yahoo-finance-collector\$(Get-Date -Format yyyyMMdd-HHmm)" New-Item -ItemType Directory -Force -Path $dest | Out-Null foreach ($profile in @(".agy", ".claude", ".codex", ".grokbot")) { $src = Join-Path $HOME $profile if (Test-Path $src) { robocopy $src (Join-Path $dest $profile) /E /NFL /NDL /NJH /NJS | Out-Null Write-Host "Backed up $src to $dest\$profile" } } if (Test-Path "market_data.db") { Copy-Item "market_data.db" "$dest\market_data.db"; Write-Host "Backed up market_data.db" } Get-ChildItem $dest
zsh (macOS)
DEST="${BACKUP_ROOT:-$HOME/Backups}/yahoo-finance-collector/$(date +%Y%m%d-%H%M)" mkdir -p "$DEST" for p in .agy .claude .codex .grokbot; do if [ -d "$HOME/$p" ]; then cp -r "$HOME/$p" "$DEST/$p" echo "Backed up $HOME/$p" fi done if [ -f "market_data.db" ]; then cp market_data.db "$DEST/"; echo "Backed up market_data.db"; fi ls -la "$DEST"
bash (Linux)
DEST="${BACKUP_ROOT:-$HOME/Backups}/yahoo-finance-collector/$(date +%Y%m%d-%H%M)" mkdir -p "$DEST" for p in .agy .claude .codex .grokbot; do if [ -d "$HOME/$p" ]; then cp -r "$HOME/$p" "$DEST/$p" echo "Backed up $HOME/$p" fi done if [ -f "market_data.db" ]; then cp market_data.db "$DEST/"; echo "Backed up market_data.db"; fi ls -la "$DEST"

4. Practised Restore Drill

Never trust an unverified backup. Run this safe, self-contained restore drill inside your temp folder:

PowerShell
$drill = Join-Path $env:TEMP "bbb-restore-drill"; $bak = Join-Path $drill "backup"; $restored = Join-Path $drill "restored"; New-Item -ItemType Directory -Force -Path $bak,$restored | Out-Null; Set-Content -Encoding ascii -Path (Join-Path $bak "proof.txt") -Value "bbb-restore-proof"; Copy-Item (Join-Path $bak "proof.txt") (Join-Path $restored "proof.txt"); Get-Content (Join-Path $restored "proof.txt"); Write-Host "Practised restore wrote proof into $restored. Do not delete your user profile to test this."
zsh (macOS)
drill="${TMPDIR:-/tmp}/bbb-restore-drill"; src="$drill/source"; bak="$drill/backup"; back="$drill/restored"; mkdir -p "$src" "$bak" "$back"; printf "%s\n" "bbb-restore-proof" > "$src/proof.txt"; cp "$src/proof.txt" "$bak/proof.txt"; rm "$src/proof.txt"; cp "$bak/proof.txt" "$back/proof.txt"; cat "$back/proof.txt"; echo "Practised restore wrote proof into $back. Do not delete your home folder to test this."
bash (Linux)
drill="${TMPDIR:-/tmp}/bbb-restore-drill"; src="$drill/source"; bak="$drill/backup"; back="$drill/restored"; mkdir -p "$src" "$bak" "$back"; printf "%s\n" "bbb-restore-proof" > "$src/proof.txt"; cp "$src/proof.txt" "$bak/proof.txt"; rm "$src/proof.txt"; cp "$bak/proof.txt" "$back/proof.txt"; cat "$back/proof.txt"; echo "Practised restore wrote proof into $back. Do not delete your home folder to test this."

5. Fail-Closed Workspace Policy

Never run automated cron collectors or nightly ticker ingestion jobs from the home directory. Restrict collector daemons to dedicated repository workspaces.

Checkpoint: Confirm Build Bot Better Protection Required

Paste terminal output from the restore drill or variable demo confirming protection: