Real-Time Stock Market Monitor with Zapserp and React
Stay ahead of market movements by building your own real-time stock market monitor. This tutorial shows you how to create a live dashboard that tracks stock news, market sentiment, and breaking financial developments using Zapserp.
What We're Building
A real-time market monitor featuring:
- Live stock news and analysis
- Market sentiment tracking
- Breaking financial news alerts
- Company-specific news feeds
- Responsive React dashboard
Project Setup
npx create-react-app stock-monitor
cd stock-monitor
npm install zapserp axios socket.io-client recharts
Backend Market Data Service
Create backend/market-service.js
:
const { Zapserp } = require('zapserp')
class MarketMonitor {
constructor(apiKey) {
this.zapserp = new Zapserp({ apiKey })
this.watchlist = new Set()
this.lastUpdates = new Map()
}
async getStockNews(symbol, limit = 10) {
const queries = [
`${symbol} stock news today`,
`${symbol} earnings report`,
`${symbol} market analysis`
]
const allNews = []
for (const query of queries) {
try {
const searchResults = await this.zapserp.search({
query,
engines: ['google', 'bing'],
limit: 8,
language: 'en'
})
// Filter for financial news sources
const financialNews = this.filterFinancialSources(searchResults.results)
if (financialNews.length > 0) {
const urls = financialNews.slice(0, 3).map(news => news.url)
const contentResults = await this.zapserp.readerBatch({ urls })
contentResults.results.forEach(content => {
if (content && content.content) {
allNews.push({
title: content.title,
summary: this.extractSummary(content.content),
url: content.url,
source: this.extractSource(content.url),
sentiment: this.analyzeSentiment(content.content),
publishedTime: content.metadata?.publishedTime,
symbol,
relevanceScore: this.calculateRelevance(content.content, symbol)
})
}
})
}
} catch (error) {
console.error(`Failed to fetch news for ${symbol}:`, error)
}
}
return this.rankNewsByRelevance(allNews).slice(0, limit)
}
filterFinancialSources(results) {
const financialSources = [
'bloomberg.com', 'reuters.com', 'wsj.com', 'marketwatch.com',
'cnbc.com', 'forbes.com', 'investopedia.com', 'fool.com',
'seekingalpha.com', 'finance.yahoo.com', 'benzinga.com'
]
return results.filter(result =>
financialSources.some(source => result.url.includes(source))
)
}
analyzeSentiment(content) {
const bullishWords = [
'surge', 'rally', 'gains', 'bullish', 'optimistic', 'upgrade',
'positive', 'strong', 'beat', 'exceeds', 'growth', 'buy'
]
const bearishWords = [
'decline', 'drop', 'bearish', 'pessimistic', 'downgrade',
'negative', 'weak', 'miss', 'below', 'loss', 'sell', 'warning'
]
const text = content.toLowerCase()
const bullishCount = bullishWords.filter(word => text.includes(word)).length
const bearishCount = bearishWords.filter(word => text.includes(word)).length
if (bullishCount > bearishCount + 1) return 'bullish'
if (bearishCount > bullishCount + 1) return 'bearish'
return 'neutral'
}
calculateRelevance(content, symbol) {
const text = content.toLowerCase()
const symbolMentions = (text.match(new RegExp(symbol.toLowerCase(), 'g')) || []).length
const companyMentions = text.includes('earnings') || text.includes('revenue') ? 2 : 0
return symbolMentions + companyMentions
}
rankNewsByRelevance(news) {
return news.sort((a, b) => {
// Primary sort by relevance score
if (b.relevanceScore !== a.relevanceScore) {
return b.relevanceScore - a.relevanceScore
}
// Secondary sort by publication time
const timeA = new Date(a.publishedTime || 0)
const timeB = new Date(b.publishedTime || 0)
return timeB - timeA
})
}
extractSummary(content) {
const sentences = content.split('.').filter(s => s.trim().length > 30)
return sentences[0] ? sentences[0].trim() + '.' : content.substring(0, 150) + '...'
}
extractSource(url) {
try {
return new URL(url).hostname.replace('www.', '')
} catch {
return 'Unknown'
}
}
async getMarketSentiment(symbols) {
const sentimentData = {}
for (const symbol of symbols) {
const news = await this.getStockNews(symbol, 5)
const sentiments = news.map(n => n.sentiment)
const bullishCount = sentiments.filter(s => s === 'bullish').length
const bearishCount = sentiments.filter(s => s === 'bearish').length
const neutralCount = sentiments.filter(s => s === 'neutral').length
sentimentData[symbol] = {
bullish: bullishCount,
bearish: bearishCount,
neutral: neutralCount,
overall: bullishCount > bearishCount ? 'bullish' :
bearishCount > bullishCount ? 'bearish' : 'neutral',
newsCount: news.length
}
}
return sentimentData
}
async getBreakingMarketNews() {
const breakingQueries = [
'stock market breaking news today',
'market crash today',
'market rally today',
'fed announcement today'
]
const breakingNews = []
for (const query of breakingQueries) {
try {
const results = await this.zapserp.search({
query,
engines: ['google'],
limit: 5
})
const financialNews = this.filterFinancialSources(results.results)
breakingNews.push(...financialNews.map(news => ({
title: news.title,
url: news.url,
source: this.extractSource(news.url),
snippet: news.snippet,
query
})))
} catch (error) {
console.error(`Breaking news search failed for: ${query}`, error)
}
}
return breakingNews.slice(0, 10)
}
}
module.exports = MarketMonitor
React Dashboard Components
Create src/components/StockDashboard.js
:
import React, { useState, useEffect } from 'react'
import axios from 'axios'
import './StockDashboard.css'
const StockDashboard = () => {
const [watchlist] = useState(['AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN'])
const [stockNews, setStockNews] = useState({})
const [marketSentiment, setMarketSentiment] = useState({})
const [breakingNews, setBreakingNews] = useState([])
const [loading, setLoading] = useState(true)
const [selectedStock, setSelectedStock] = useState('AAPL')
useEffect(() => {
loadMarketData()
const interval = setInterval(loadMarketData, 5 * 60 * 1000) // 5 minutes
return () => clearInterval(interval)
}, [])
const loadMarketData = async () => {
try {
// Load data for all watchlist stocks
const newsPromises = watchlist.map(symbol =>
axios.get(`/api/stock/${symbol}/news`)
)
const [newsResponses, sentimentResponse, breakingResponse] = await Promise.all([
Promise.all(newsPromises),
axios.get('/api/market/sentiment', { params: { symbols: watchlist.join(',') }}),
axios.get('/api/market/breaking')
])
// Process stock news
const newsData = {}
newsResponses.forEach((response, index) => {
newsData[watchlist[index]] = response.data.news
})
setStockNews(newsData)
setMarketSentiment(sentimentResponse.data)
setBreakingNews(breakingResponse.data)
setLoading(false)
} catch (error) {
console.error('Failed to load market data:', error)
setLoading(false)
}
}
const getSentimentColor = (sentiment) => {
switch (sentiment) {
case 'bullish': return '#10b981'
case 'bearish': return '#ef4444'
default: return '#6b7280'
}
}
const getSentimentIcon = (sentiment) => {
switch (sentiment) {
case 'bullish': return '📈'
case 'bearish': return '📉'
default: return '➡️'
}
}
if (loading) {
return <div className="loading">Loading market data...</div>
}
return (
<div className="stock-dashboard">
<header className="dashboard-header">
<h1>📊 Stock Market Monitor</h1>
<div className="last-update">
Last updated: {new Date().toLocaleTimeString()}
</div>
</header>
<div className="dashboard-grid">
{/* Watchlist Sidebar */}
<div className="watchlist-panel">
<h3>Watchlist</h3>
{watchlist.map(symbol => {
const sentiment = marketSentiment[symbol]
return (
<div
key={symbol}
className={`stock-item ${selectedStock === symbol ? 'active' : ''}`}
onClick={() => setSelectedStock(symbol)}
>
<div className="stock-symbol">{symbol}</div>
{sentiment && (
<div className="sentiment-indicator">
<span style={{ color: getSentimentColor(sentiment.overall) }}>
{getSentimentIcon(sentiment.overall)}
</span>
<small>{sentiment.newsCount} news</small>
</div>
)}
</div>
)
})}
</div>
{/* Main Content */}
<div className="main-content">
{/* Selected Stock News */}
<div className="stock-news-panel">
<h3>{selectedStock} Latest News</h3>
<div className="news-list">
{stockNews[selectedStock]?.map((news, index) => (
<div key={index} className="news-item">
<div className="news-header">
<a href={news.url} target="_blank" rel="noopener noreferrer">
{news.title}
</a>
<span
className="sentiment-badge"
style={{ backgroundColor: getSentimentColor(news.sentiment) }}
>
{news.sentiment}
</span>
</div>
<div className="news-meta">
{news.source} • {news.publishedTime || 'Recently'}
</div>
<div className="news-summary">{news.summary}</div>
</div>
)) || <div>No news available</div>}
</div>
</div>
{/* Market Sentiment Overview */}
<div className="sentiment-panel">
<h3>Market Sentiment</h3>
<div className="sentiment-grid">
{Object.entries(marketSentiment).map(([symbol, data]) => (
<div key={symbol} className="sentiment-card">
<div className="sentiment-symbol">{symbol}</div>
<div className="sentiment-bars">
<div className="sentiment-bar bullish" style={{ width: `${(data.bullish / data.newsCount) * 100}%` }}>
{data.bullish}
</div>
<div className="sentiment-bar bearish" style={{ width: `${(data.bearish / data.newsCount) * 100}%` }}>
{data.bearish}
</div>
<div className="sentiment-bar neutral" style={{ width: `${(data.neutral / data.newsCount) * 100}%` }}>
{data.neutral}
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Breaking News Sidebar */}
<div className="breaking-news-panel">
<h3>🚨 Breaking Market News</h3>
<div className="breaking-news-list">
{breakingNews.map((news, index) => (
<div key={index} className="breaking-news-item">
<a href={news.url} target="_blank" rel="noopener noreferrer">
{news.title}
</a>
<div className="breaking-source">{news.source}</div>
</div>
))}
</div>
</div>
</div>
</div>
)
}
export default StockDashboard
Express API Server
Create a simple server (backend/server.js
):
const express = require('express')
const cors = require('cors')
const MarketMonitor = require('./market-service')
const app = express()
const marketMonitor = new MarketMonitor(process.env.ZAPSERP_API_KEY)
app.use(cors())
app.use(express.json())
app.get('/api/stock/:symbol/news', async (req, res) => {
try {
const { symbol } = req.params
const news = await marketMonitor.getStockNews(symbol.toUpperCase())
res.json({ symbol, news })
} catch (error) {
res.status(500).json({ error: 'Failed to fetch stock news' })
}
})
app.get('/api/market/sentiment', async (req, res) => {
try {
const symbols = req.query.symbols.split(',')
const sentiment = await marketMonitor.getMarketSentiment(symbols)
res.json(sentiment)
} catch (error) {
res.status(500).json({ error: 'Failed to fetch market sentiment' })
}
})
app.get('/api/market/breaking', async (req, res) => {
try {
const breaking = await marketMonitor.getBreakingMarketNews()
res.json(breaking)
} catch (error) {
res.status(500).json({ error: 'Failed to fetch breaking news' })
}
})
const PORT = process.env.PORT || 5000
app.listen(PORT, () => {
console.log(`📈 Market Monitor API running on port ${PORT}`)
})
Quick Styling (src/components/StockDashboard.css
)
.stock-dashboard {
min-height: 100vh;
background: #f8fafc;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.dashboard-header {
background: white;
padding: 1rem 2rem;
border-bottom: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.dashboard-grid {
display: grid;
grid-template-columns: 250px 1fr 300px;
gap: 1rem;
padding: 1rem;
height: calc(100vh - 80px);
}
.watchlist-panel, .breaking-news-panel {
background: white;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.stock-item {
padding: 0.75rem;
margin: 0.5rem 0;
border-radius: 6px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.stock-item:hover { background: #f1f5f9; }
.stock-item.active { background: #dbeafe; border-left: 3px solid #3b82f6; }
.sentiment-badge {
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
text-transform: uppercase;
}
.news-item {
padding: 1rem;
border-bottom: 1px solid #e2e8f0;
}
.news-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.5rem;
}
.sentiment-bars {
display: flex;
height: 4px;
border-radius: 2px;
overflow: hidden;
margin-top: 4px;
}
.sentiment-bar.bullish { background: #10b981; }
.sentiment-bar.bearish { background: #ef4444; }
.sentiment-bar.neutral { background: #6b7280; }
Running the Application
- Start the backend:
node backend/server.js
- Start React:
npm start
- Visit
http://localhost:3000
Key Features
- Real-Time Updates: Refreshes market data every 5 minutes
- Sentiment Analysis: Analyzes news sentiment for each stock
- Breaking News: Displays urgent market developments
- Interactive Dashboard: Click stocks to view detailed news
- Quality Sources: Focuses on reputable financial news outlets
Enhancement Ideas
- Add price charts integration
- Implement push notifications for breaking news
- Add technical analysis indicators
- Create customizable alerts
- Add portfolio tracking features
Conclusion
You now have a powerful real-time stock market monitor that leverages Zapserp's search capabilities to provide comprehensive market intelligence. The dashboard updates automatically and provides valuable insights into market sentiment and breaking developments.
Want to add more financial features? Consider integrating with stock price APIs or adding technical analysis tools for a complete trading dashboard.