Complete guide to implementing the Bazaar Indian Stock Market Dashboard using Electron.js
- Project Overview
- Technology Stack
- Project Structure
- Prerequisites
- Initial Setup
- Implementation Details
- Building & Packaging
- Distribution
- Testing
- Deployment
- Performance Optimization
- Troubleshooting
Bazaar is a cross-platform desktop application that displays real-time Indian stock market information with a nostalgic Windows XP-style interface.
- Framework: Python + Tkinter
- Size: ~60-100 MB (executable)
- Dependencies: yfinance, pandas, requests
- Modern web technologies (HTML/CSS/JavaScript)
- Better UI/UX capabilities
- Rich ecosystem (React, Vue, Tailwind options)
- Auto-updates support
- Cross-platform consistency
- Native notification support
- Electron (v28+): Desktop application framework
- Node.js (v18+): JavaScript runtime
- HTML5/CSS3: UI structure and styling
- JavaScript (ES6+): Application logic
- Yahoo Finance API: Via
yahoo-finance2npm package - Axios: HTTP client for API requests
- Node-fetch: Alternative HTTP client
- electron-builder: Packaging and distribution
- electron-updater: Auto-update functionality
- React/Vue.js: For complex UI components
- Chart.js/D3.js: For data visualization
- Tailwind CSS: For modern styling
- TypeScript: For type safety
bazaar-electron/
│
├── package.json # Project metadata and dependencies
├── package-lock.json # Dependency lock file
│
├── main.js # Electron main process (entry point)
├── preload.js # Preload script (security bridge)
│
├── src/
│ ├── renderer/ # Renderer process (UI)
│ │ ├── index.html # Main HTML file
│ │ ├── styles/
│ │ │ ├── main.css # Main styles
│ │ │ ├── winxp.css # Windows XP theme
│ │ │ └── components.css # Component-specific styles
│ │ ├── js/
│ │ │ ├── app.js # Main app logic
│ │ │ ├── ui-components.js # UI section components
│ │ │ └── utils.js # Utility functions
│ │ └── assets/
│ │ ├── icon.png # App icon
│ │ └── logo.png # Logo
│ │
│ ├── main/ # Main process modules
│ │ ├── menu.js # Application menu
│ │ └── window.js # Window management
│ │
│ └── services/ # Backend services
│ ├── data-fetcher.js # Data fetching logic
│ └── api-client.js # API communication
│
├── assets/ # Build assets
│ ├── icon.icns # macOS icon
│ ├── icon.ico # Windows icon
│ └── icon.png # Linux icon
│
├── build/ # Electron-builder config
│ └── icons/ # Build-time icons
│
├── dist/ # Distribution files (gitignored)
│
├── scripts/ # Utility scripts
│ ├── build.js # Build script
│ └── dev.js # Development script
│
├── docs/ # Documentation
│ └── README.md
│
├── .gitignore
├── electron-builder.json # Builder configuration
└── README.md # Project README
-
Node.js (v18.0.0 or higher)
node --version # Should be v18+ -
npm (v9.0.0 or higher)
npm --version # Should be v9+ -
Git (for version control)
git --version
- Windows 7 or later
- Visual Studio Build Tools (for native modules)
npm install --global windows-build-tools
- macOS 10.12 or later
- Xcode Command Line Tools
xcode-select --install
- Ubuntu 18.04+ or equivalent
- Build essentials
sudo apt-get install build-essential
mkdir bazaar-electron
cd bazaar-electronnpm init -y# Core dependencies
npm install electron --save-dev
npm install yahoo-finance2 axios
# Build tools
npm install electron-builder --save-dev
# Optional: For better development experience
npm install electron-reload --save-dev
npm install nodemon --save-dev# Unix/Linux/macOS
mkdir -p src/renderer/styles src/renderer/js src/renderer/assets
mkdir -p src/main src/services
mkdir -p assets/icons build/icons scripts docs
# Windows (PowerShell)
New-Item -ItemType Directory -Force -Path src/renderer/styles, src/renderer/js, src/renderer/assets, src/main, src/services, assets/icons, build/icons, scripts, docsconst { app, BrowserWindow, ipcMain } = require("electron");
const path = require("path");
// Keep a global reference to prevent garbage collection
let mainWindow;
function createWindow() {
// Create the browser window
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 1000,
minHeight: 700,
backgroundColor: "#ECE9D8", // Windows XP beige
icon: path.join(__dirname, "assets/icon.png"),
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
enableRemoteModule: false,
},
show: false, // Don't show until ready
});
// Load the index.html
mainWindow.loadFile(path.join(__dirname, "src/renderer/index.html"));
// Show window when ready
mainWindow.once("ready-to-show", () => {
mainWindow.show();
});
// Open DevTools in development
if (process.env.NODE_ENV === "development") {
mainWindow.webContents.openDevTools();
}
// Emitted when the window is closed
mainWindow.on("closed", () => {
mainWindow = null;
});
}
// This method will be called when Electron has finished initialization
app.whenReady().then(() => {
createWindow();
app.on("activate", () => {
// On macOS, re-create window when dock icon is clicked
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
// Quit when all windows are closed
app.on("window-all-closed", () => {
// On macOS, apps typically stay active until Cmd+Q
if (process.platform !== "darwin") {
app.quit();
}
});
// Handle IPC messages from renderer
ipcMain.handle("get-market-data", async (event, params) => {
// This will be handled by the data fetcher
const { getMarketData } = require("./src/services/data-fetcher");
return await getMarketData(params);
});
ipcMain.handle("get-gainers-losers", async (event, params) => {
const { getGainersLosers } = require("./src/services/data-fetcher");
return await getGainersLosers(params);
});
ipcMain.handle("get-sectoral-data", async (event) => {
const { getSectoralPerformance } = require("./src/services/data-fetcher");
return await getSectoralPerformance();
});
ipcMain.handle("get-vix-data", async (event) => {
const { getVixData } = require("./src/services/data-fetcher");
return await getVixData();
});const { contextBridge, ipcRenderer } = require("electron");
// Expose protected methods that allow the renderer process to use
// ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld("api", {
// Market data methods
getMarketData: (params) => ipcRenderer.invoke("get-market-data", params),
getGainersLosers: (params) =>
ipcRenderer.invoke("get-gainers-losers", params),
getSectoralData: () => ipcRenderer.invoke("get-sectoral-data"),
getVixData: () => ipcRenderer.invoke("get-vix-data"),
// System info
platform: process.platform,
versions: {
node: process.versions.node,
chrome: process.versions.chrome,
electron: process.versions.electron,
},
});const yahooFinance = require("yahoo-finance2").default;
// Indian market index symbols
const INDICES = {
NIFTY50: "^NSEI",
BANKNIFTY: "^NSEBANK",
SENSEX: "^BSESN",
VIX: "^INDIAVIX",
};
// Sectoral indices
const SECTORS = {
IT: "^CNXIT",
Bank: "^NSEBANK",
Auto: "^CNXAUTO",
Pharma: "^CNXPHARMA",
Metal: "^CNXMETAL",
FMCG: "^CNXFMCG",
Realty: "^CNXREALTY",
Energy: "^CNXENERGY",
Infra: "^CNXINFRA",
Media: "^CNXMEDIA",
};
// Stock lists for different indices
const STOCK_LISTS = {
NIFTY50: [
"RELIANCE.NS",
"TCS.NS",
"HDFCBANK.NS",
"INFY.NS",
"HINDUNILVR.NS",
"ICICIBANK.NS",
"KOTAKBANK.NS",
"SBIN.NS",
"BHARTIARTL.NS",
"ITC.NS",
"LT.NS",
"AXISBANK.NS",
"ASIANPAINT.NS",
"MARUTI.NS",
"TITAN.NS",
"BAJFINANCE.NS",
"SUNPHARMA.NS",
"WIPRO.NS",
"ULTRACEMCO.NS",
"NTPC.NS",
"ONGC.NS",
"HCLTECH.NS",
"M&M.NS",
"POWERGRID.NS",
"TATAMOTORS.NS",
"NESTLEIND.NS",
"DIVISLAB.NS",
"JSWSTEEL.NS",
"TECHM.NS",
"HINDALCO.NS",
],
SENSEX: [
"RELIANCE.NS",
"TCS.NS",
"HDFCBANK.NS",
"INFY.NS",
"ICICIBANK.NS",
"HINDUNILVR.NS",
"ITC.NS",
"SBIN.NS",
"BHARTIARTL.NS",
"KOTAKBANK.NS",
"LT.NS",
"AXISBANK.NS",
"BAJFINANCE.NS",
"MARUTI.NS",
"ASIANPAINT.NS",
"SUNPHARMA.NS",
"TITAN.NS",
"ULTRACEMCO.NS",
"NTPC.NS",
"M&M.NS",
],
BANKNIFTY: [
"HDFCBANK.NS",
"ICICIBANK.NS",
"KOTAKBANK.NS",
"SBIN.NS",
"AXISBANK.NS",
"INDUSINDBK.NS",
"BANKBARODA.NS",
"PNB.NS",
"IDFCFIRSTB.NS",
"BANDHANBNK.NS",
],
};
/**
* Get data for a specific index
* @param {string} indexName - Name of the index (NIFTY50, BANKNIFTY, SENSEX, VIX)
* @returns {Promise<Object>} Index data
*/
async function getIndexData(indexName) {
try {
const symbol = INDICES[indexName];
if (!symbol) {
throw new Error(`Unknown index: ${indexName}`);
}
// Get quote data
const quote = await yahooFinance.quote(symbol);
// Get historical data for previous close
const now = new Date();
const history = await yahooFinance.historical(symbol, {
period1: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000), // 7 days ago
period2: now,
interval: "1d",
});
const currentPrice = quote.regularMarketPrice || 0;
const prevClose = quote.regularMarketPreviousClose || currentPrice;
const change = currentPrice - prevClose;
const changePct = (change / prevClose) * 100;
return {
name: indexName,
price: currentPrice,
change: change,
change_pct: changePct,
high: quote.regularMarketDayHigh || 0,
low: quote.regularMarketDayLow || 0,
open: quote.regularMarketOpen || 0,
volume: quote.regularMarketVolume || 0,
previousClose: prevClose,
};
} catch (error) {
console.error(`Error fetching ${indexName}:`, error.message);
return null;
}
}
/**
* Get market summary for all major indices
* @returns {Promise<Object>} Market summary
*/
async function getMarketData() {
try {
const indices = ["NIFTY50", "BANKNIFTY", "SENSEX"];
const summary = {};
// Fetch all indices data
const promises = indices.map((index) => getIndexData(index));
const results = await Promise.all(promises);
results.forEach((data, i) => {
if (data) {
summary[indices[i]] = data;
}
});
// Determine market status
const now = new Date();
const hours = now.getHours();
const minutes = now.getMinutes();
const day = now.getDay();
const isWeekday = day >= 1 && day <= 5;
const marketStart = hours > 9 || (hours === 9 && minutes >= 15);
const marketEnd = hours < 15 || (hours === 15 && minutes <= 30);
summary.marketStatus =
isWeekday && marketStart && marketEnd ? "OPEN" : "CLOSED";
return summary;
} catch (error) {
console.error("Error fetching market data:", error);
throw error;
}
}
/**
* Get top gainers and losers for a given index
* @param {Object} params - Parameters object
* @param {string} params.index - Index name (NIFTY50, SENSEX, BANKNIFTY)
* @param {string} params.timePeriod - Time period (1D, 1Week, 1Month, 6Months, 1Year)
* @param {number} params.limit - Number of stocks to return (default: 10)
* @returns {Promise<Object>} Gainers and losers data
*/
async function getGainersLosers({
index = "NIFTY50",
timePeriod = "1D",
limit = 10,
}) {
try {
const stocks = STOCK_LISTS[index] || STOCK_LISTS.NIFTY50;
// Map time period to days
const periodMap = {
"1D": 2,
"1Week": 7,
"1Month": 30,
"6Months": 180,
"1Year": 365,
};
const daysAgo = periodMap[timePeriod] || 2;
const now = new Date();
const startDate = new Date(now.getTime() - daysAgo * 24 * 60 * 60 * 1000);
const stockData = [];
// Fetch data for each stock (in batches to avoid rate limiting)
const batchSize = 10;
for (let i = 0; i < stocks.length; i += batchSize) {
const batch = stocks.slice(i, i + batchSize);
const promises = batch.map(async (symbol) => {
try {
const history = await yahooFinance.historical(symbol, {
period1: startDate,
period2: now,
interval: "1d",
});
if (history && history.length >= 2) {
const current = history[history.length - 1].close;
const previous = history[0].close;
const changePct = ((current - previous) / previous) * 100;
return {
symbol: symbol.replace(".NS", ""),
price: current,
change_pct: changePct,
};
}
} catch (error) {
console.error(`Error fetching ${symbol}:`, error.message);
return null;
}
});
const results = await Promise.all(promises);
stockData.push(...results.filter(Boolean));
// Small delay between batches to avoid rate limiting
if (i + batchSize < stocks.length) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
// Sort by change percentage
stockData.sort((a, b) => b.change_pct - a.change_pct);
return {
gainers: stockData.slice(0, limit),
losers: stockData.slice(-limit).reverse(),
};
} catch (error) {
console.error("Error fetching gainers/losers:", error);
return { gainers: [], losers: [] };
}
}
/**
* Get VIX data
* @returns {Promise<Object>} VIX data
*/
async function getVixData() {
return await getIndexData("VIX");
}
/**
* Calculate Fear/Greed index
* @returns {Promise<Object>} Fear/Greed data
*/
async function getFearGreedIndex() {
try {
const vixData = await getVixData();
const niftyData = await getIndexData("NIFTY50");
if (!vixData || !niftyData) {
return { score: 50, status: "NEUTRAL", vix: 0 };
}
// Simple calculation:
// VIX < 15: Greed, VIX > 25: Fear
// Nifty positive: +ve sentiment, negative: -ve sentiment
const vixScore = Math.max(0, Math.min(100, (25 - vixData.price) * 2 + 50));
let marketScore = 50 + niftyData.change_pct * 5;
marketScore = Math.max(0, Math.min(100, marketScore));
const finalScore = vixScore * 0.6 + marketScore * 0.4;
let status;
if (finalScore < 25) {
status = "EXTREME FEAR";
} else if (finalScore < 45) {
status = "FEAR";
} else if (finalScore < 55) {
status = "NEUTRAL";
} else if (finalScore < 75) {
status = "GREED";
} else {
status = "EXTREME GREED";
}
return {
score: Math.round(finalScore * 10) / 10,
status: status,
vix: vixData.price,
};
} catch (error) {
console.error("Error calculating fear/greed:", error);
return { score: 50, status: "NEUTRAL", vix: 0 };
}
}
/**
* Get sectoral performance data
* @returns {Promise<Array>} Sectoral data
*/
async function getSectoralPerformance() {
try {
const sectoralData = [];
for (const [sectorName, symbol] of Object.entries(SECTORS)) {
try {
const now = new Date();
const history = await yahooFinance.historical(symbol, {
period1: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000),
period2: now,
interval: "1d",
});
if (history && history.length >= 2) {
const current = history[history.length - 1].close;
const previous = history[history.length - 2].close;
const changePct = ((current - previous) / previous) * 100;
sectoralData.push({
sector: sectorName,
price: current,
change_pct: changePct,
});
}
} catch (error) {
console.error(`Error fetching ${sectorName}:`, error.message);
}
}
// Sort by performance
sectoralData.sort((a, b) => b.change_pct - a.change_pct);
return sectoralData;
} catch (error) {
console.error("Error fetching sectoral data:", error);
return [];
}
}
module.exports = {
getIndexData,
getMarketData,
getGainersLosers,
getVixData,
getFearGreedIndex,
getSectoralPerformance,
};<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'"
/>
<title>Bazaar - Indian Stock Market</title>
<link rel="stylesheet" href="styles/winxp.css" />
<link rel="stylesheet" href="styles/main.css" />
<link rel="stylesheet" href="styles/components.css" />
</head>
<body>
<!-- Loading Overlay -->
<div id="loading-overlay" class="loading-overlay hidden">
<div class="loading-container">
<div class="loading-icon">⏳</div>
<div class="loading-text">Loading market data...</div>
<div class="loading-subtext">
Please wait while we fetch the latest updates
</div>
</div>
</div>
<!-- Main Container -->
<div class="main-container">
<!-- Header -->
<header class="header">
<div class="header-left">
<h1 class="header-title">
🏛️ Bazaar - Indian Stock Market Dashboard
</h1>
</div>
<div class="header-right">
<span id="status-label" class="status-label">Loading...</span>
<button id="refresh-btn" class="refresh-btn">🔄 Refresh Now</button>
</div>
</header>
<!-- Scrollable Content Area -->
<div class="content-wrapper">
<div class="scrollable-content">
<!-- Index Tickers Section -->
<section id="tickers-section" class="section">
<div class="section-header">
<h2 class="section-title">📈 Index Tickers</h2>
</div>
<div class="section-content">
<div id="tickers-container" class="tickers-grid">
<!-- Ticker cards will be inserted here -->
</div>
</div>
</section>
<!-- Gainers & Losers Section -->
<section id="gainers-losers-section" class="section">
<div class="section-header">
<h2 class="section-title">🔥 Top Gainers & Losers</h2>
</div>
<div class="section-content">
<!-- Selector Controls -->
<div class="selector-controls">
<label class="selector-label">
<span class="label-text">Select Index:</span>
<select id="index-selector" class="selector-dropdown">
<option value="NIFTY50">NIFTY 50</option>
<option value="SENSEX">SENSEX 30</option>
<option value="BANKNIFTY">BANK NIFTY</option>
</select>
</label>
<label class="selector-label">
<span class="label-text">Time:</span>
<select id="time-selector" class="selector-dropdown">
<option value="1D">1 Day</option>
<option value="1Week">1 Week</option>
<option value="1Month">1 Month</option>
<option value="6Months">6 Months</option>
<option value="1Year">1 Year</option>
</select>
</label>
</div>
<!-- Gainers & Losers Grid -->
<div class="gainers-losers-grid">
<div class="gainers-column">
<h3 class="column-title positive">🟢 Top Gainers</h3>
<div id="gainers-list" class="stock-list">
<!-- Gainers will be inserted here -->
</div>
</div>
<div class="losers-column">
<h3 class="column-title negative">🔴 Top Losers</h3>
<div id="losers-list" class="stock-list">
<!-- Losers will be inserted here -->
</div>
</div>
</div>
</div>
</section>
<!-- Market Sentiment Section -->
<section id="sentiment-section" class="section">
<div class="section-header">
<h2 class="section-title">😰 Market Sentiment</h2>
</div>
<div class="section-content">
<div class="sentiment-grid">
<!-- VIX Card -->
<div class="sentiment-card">
<h3 class="card-title">India VIX (Volatility Index)</h3>
<div id="vix-value" class="card-value">--</div>
<div id="vix-change" class="card-change">--</div>
</div>
<!-- Fear/Greed Meter Card -->
<div class="sentiment-card">
<h3 class="card-title">Market Greed Meter</h3>
<div id="greed-score" class="card-value">--</div>
<div id="greed-status" class="card-status">--</div>
<div class="meter-legend">
0=Extreme Fear 25=Fear 50=Neutral 75=Greed 100=Extreme Greed
</div>
</div>
</div>
</div>
</section>
<!-- Sectoral Performance Section -->
<section id="sectoral-section" class="section">
<div class="section-header">
<h2 class="section-title">🏭 Sectoral Performance</h2>
</div>
<div class="section-content">
<div id="sectoral-container" class="sectoral-list">
<!-- Sectoral bars will be inserted here -->
</div>
</div>
</section>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<span id="last-update-label">Last Updated: Never</span>
</footer>
</div>
<!-- Scripts -->
<script src="js/ui-components.js"></script>
<script src="js/app.js"></script>
</body>
</html>// Main application logic
class BazaarApp {
constructor() {
this.refreshInterval = 60000; // 60 seconds
this.autoRefreshTimer = null;
this.isRefreshing = false;
this.initializeElements();
this.attachEventListeners();
this.startApp();
}
initializeElements() {
// Control elements
this.refreshBtn = document.getElementById("refresh-btn");
this.statusLabel = document.getElementById("status-label");
this.lastUpdateLabel = document.getElementById("last-update-label");
this.loadingOverlay = document.getElementById("loading-overlay");
// Selector elements
this.indexSelector = document.getElementById("index-selector");
this.timeSelector = document.getElementById("time-selector");
// Content containers
this.tickersContainer = document.getElementById("tickers-container");
this.gainersList = document.getElementById("gainers-list");
this.losersList = document.getElementById("losers-list");
this.sectoralContainer = document.getElementById("sectoral-container");
// Sentiment elements
this.vixValue = document.getElementById("vix-value");
this.vixChange = document.getElementById("vix-change");
this.greedScore = document.getElementById("greed-score");
this.greedStatus = document.getElementById("greed-status");
}
attachEventListeners() {
// Refresh button
this.refreshBtn.addEventListener("click", () => this.refreshAllData());
// Selector changes
this.indexSelector.addEventListener("change", () =>
this.refreshGainersLosers()
);
this.timeSelector.addEventListener("change", () =>
this.refreshGainersLosers()
);
}
async startApp() {
await this.refreshAllData();
this.startAutoRefresh();
}
showLoading() {
this.loadingOverlay.classList.remove("hidden");
this.refreshBtn.disabled = true;
this.statusLabel.textContent = "⏳ Refreshing data...";
}
hideLoading() {
this.loadingOverlay.classList.add("hidden");
this.refreshBtn.disabled = false;
}
async refreshAllData() {
if (this.isRefreshing) return;
this.isRefreshing = true;
this.showLoading();
try {
// Refresh all sections in parallel
await Promise.all([
this.refreshTickers(),
this.refreshGainersLosers(),
this.refreshSentiment(),
this.refreshSectoral(),
]);
// Update last refresh time
const now = new Date();
this.lastUpdateLabel.textContent = `Last Updated: ${now.toLocaleString()}`;
this.statusLabel.textContent = "✓ Data refreshed successfully";
} catch (error) {
console.error("Error refreshing data:", error);
this.statusLabel.textContent = `❌ Error: ${error.message}`;
} finally {
this.isRefreshing = false;
this.hideLoading();
}
}
async refreshTickers() {
try {
const data = await window.api.getMarketData();
this.tickersContainer.innerHTML = "";
["NIFTY50", "BANKNIFTY", "SENSEX"].forEach((index) => {
if (data[index]) {
const card = UIComponents.createTickerCard(data[index]);
this.tickersContainer.appendChild(card);
}
});
} catch (error) {
console.error("Error refreshing tickers:", error);
this.tickersContainer.innerHTML =
'<div class="error-message">Failed to load index data</div>';
}
}
async refreshGainersLosers() {
try {
const index = this.indexSelector.value;
const timePeriod = this.timeSelector.value;
// Show loading in the list containers
this.gainersList.innerHTML =
'<div class="loading-text">⏳ Loading...</div>';
this.losersList.innerHTML =
'<div class="loading-text">⏳ Loading...</div>';
const data = await window.api.getGainersLosers({ index, timePeriod });
// Clear loading
this.gainersList.innerHTML = "";
this.losersList.innerHTML = "";
// Populate gainers
if (data.gainers && data.gainers.length > 0) {
data.gainers.forEach((stock) => {
const item = UIComponents.createStockItem(stock, true);
this.gainersList.appendChild(item);
});
} else {
this.gainersList.innerHTML =
'<div class="empty-message">No data available</div>';
}
// Populate losers
if (data.losers && data.losers.length > 0) {
data.losers.forEach((stock) => {
const item = UIComponents.createStockItem(stock, false);
this.losersList.appendChild(item);
});
} else {
this.losersList.innerHTML =
'<div class="empty-message">No data available</div>';
}
} catch (error) {
console.error("Error refreshing gainers/losers:", error);
this.gainersList.innerHTML =
'<div class="error-message">Failed to load</div>';
this.losersList.innerHTML =
'<div class="error-message">Failed to load</div>';
}
}
async refreshSentiment() {
try {
const vixData = await window.api.getVixData();
if (vixData) {
this.vixValue.textContent = vixData.price.toFixed(2);
const change = vixData.change;
const changePct = vixData.change_pct;
const arrow = change >= 0 ? "▲" : "▼";
const color = change >= 0 ? "negative" : "positive"; // VIX up is bad
this.vixChange.textContent = `${arrow} ${Math.abs(change).toFixed(
2
)} (${Math.abs(changePct).toFixed(2)}%)`;
this.vixChange.className = `card-change ${color}`;
}
// Get Fear/Greed from VIX calculation
// This would need to be added to the main process
// For now, calculate it client-side
this.calculateFearGreed(vixData);
} catch (error) {
console.error("Error refreshing sentiment:", error);
this.vixValue.textContent = "--";
this.vixChange.textContent = "--";
}
}
calculateFearGreed(vixData) {
// Simple calculation based on VIX
const vixPrice = vixData?.price || 20;
let score = Math.max(0, Math.min(100, (25 - vixPrice) * 2 + 50));
score = Math.round(score * 10) / 10;
let status, color;
if (score < 25) {
status = "EXTREME FEAR";
color = "#CC0000";
} else if (score < 45) {
status = "FEAR";
color = "#FF6600";
} else if (score < 55) {
status = "NEUTRAL";
color = "#FFD700";
} else if (score < 75) {
status = "GREED";
color = "#90EE90";
} else {
status = "EXTREME GREED";
color = "#00AA00";
}
this.greedScore.textContent = `${score} / 100`;
this.greedScore.style.color = color;
this.greedStatus.textContent = status;
this.greedStatus.style.color = color;
}
async refreshSectoral() {
try {
const data = await window.api.getSectoralData();
this.sectoralContainer.innerHTML = "";
if (data && data.length > 0) {
data.forEach((sector, index) => {
const row = UIComponents.createSectorRow(sector, index);
this.sectoralContainer.appendChild(row);
});
} else {
this.sectoralContainer.innerHTML =
'<div class="empty-message">No sectoral data available</div>';
}
} catch (error) {
console.error("Error refreshing sectoral data:", error);
this.sectoralContainer.innerHTML =
'<div class="error-message">Failed to load sectoral data</div>';
}
}
startAutoRefresh() {
// Clear existing timer if any
if (this.autoRefreshTimer) {
clearInterval(this.autoRefreshTimer);
}
// Set up new timer
this.autoRefreshTimer = setInterval(() => {
this.refreshAllData();
}, this.refreshInterval);
}
}
// Initialize app when DOM is ready
document.addEventListener("DOMContentLoaded", () => {
new BazaarApp();
});// UI Component builders
const UIComponents = {
/**
* Create a ticker card element
* @param {Object} data - Index data
* @returns {HTMLElement} Ticker card element
*/
createTickerCard(data) {
const card = document.createElement("div");
card.className = "ticker-card";
const name = document.createElement("div");
name.className = "ticker-name";
name.textContent = data.name;
const price = document.createElement("div");
price.className = "ticker-price";
price.textContent = data.price.toFixed(2);
const change = document.createElement("div");
const isPositive = data.change >= 0;
change.className = `ticker-change ${isPositive ? "positive" : "negative"}`;
const arrow = isPositive ? "▲" : "▼";
change.textContent = `${arrow} ${Math.abs(data.change).toFixed(
2
)} (${Math.abs(data.change_pct).toFixed(2)}%)`;
const info = document.createElement("div");
info.className = "ticker-info";
info.textContent = `Open: ${data.open.toFixed(
2
)} | High: ${data.high.toFixed(2)} | Low: ${data.low.toFixed(2)}`;
card.appendChild(name);
card.appendChild(price);
card.appendChild(change);
card.appendChild(info);
return card;
},
/**
* Create a stock list item
* @param {Object} stock - Stock data
* @param {boolean} isGainer - Is this a gainer or loser
* @returns {HTMLElement} Stock item element
*/
createStockItem(stock, isGainer) {
const item = document.createElement("div");
item.className = "stock-item";
const symbol = document.createElement("div");
symbol.className = "stock-symbol";
symbol.textContent = stock.symbol;
const price = document.createElement("div");
price.className = "stock-price";
price.textContent = `₹${stock.price.toFixed(2)}`;
const change = document.createElement("div");
const changePct = stock.change_pct;
const isPositive = changePct >= 0;
change.className = `stock-change ${isPositive ? "positive" : "negative"}`;
const arrow = isPositive ? "▲" : "▼";
change.textContent = `${arrow} ${Math.abs(changePct).toFixed(2)}%`;
item.appendChild(symbol);
item.appendChild(price);
item.appendChild(change);
return item;
},
/**
* Create a sector performance row
* @param {Object} sector - Sector data
* @param {number} index - Row index for alternating colors
* @returns {HTMLElement} Sector row element
*/
createSectorRow(sector, index) {
const row = document.createElement("div");
row.className = `sector-row ${index % 2 === 0 ? "even" : "odd"}`;
const name = document.createElement("div");
name.className = "sector-name";
name.textContent = sector.sector;
const barContainer = document.createElement("div");
barContainer.className = "sector-bar-container";
const barWrapper = document.createElement("div");
barWrapper.className = "sector-bar-wrapper";
const changePct = sector.change_pct;
const isPositive = changePct >= 0;
const barWidth = Math.min(Math.abs(changePct) * 60, 300);
const bar = document.createElement("div");
bar.className = `sector-bar ${isPositive ? "positive" : "negative"}`;
bar.style.width = `${barWidth}px`;
bar.style[isPositive ? "marginLeft" : "marginRight"] = "150px";
barWrapper.appendChild(bar);
barContainer.appendChild(barWrapper);
const change = document.createElement("div");
change.className = `sector-change ${isPositive ? "positive" : "negative"}`;
const arrow = isPositive ? "▲" : "▼";
change.textContent = `${arrow} ${Math.abs(changePct).toFixed(2)}%`;
row.appendChild(name);
row.appendChild(barContainer);
row.appendChild(change);
return row;
},
};/* Windows XP Theme Variables */
:root {
/* Colors */
--xp-bg: #ece9d8;
--xp-fg: #000000;
--xp-button: #d4d0c8;
--xp-button-hover: #e8e4d8;
--xp-header: #0054e3;
--xp-header-text: #ffffff;
--xp-border: #7a96df;
--xp-positive: #00aa00;
--xp-negative: #cc0000;
--xp-text-bg: #ffffff;
--xp-gray: #666666;
/* Typography */
--xp-font: "Tahoma", "MS Sans Serif", Arial, sans-serif;
}
/* Global Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--xp-font);
background-color: var(--xp-bg);
color: var(--xp-fg);
font-size: 11px;
overflow: hidden;
}
/* Windows XP Button Style */
.xp-button {
background-color: var(--xp-button);
border: 2px outset var(--xp-button);
border-radius: 2px;
padding: 4px 12px;
font-family: var(--xp-font);
font-size: 11px;
cursor: pointer;
transition: background-color 0.1s;
}
.xp-button:hover {
background-color: var(--xp-button-hover);
}
.xp-button:active {
border-style: inset;
}
.xp-button:disabled {
color: #999;
cursor: not-allowed;
}
/* Windows XP Panel Style */
.xp-panel {
background-color: var(--xp-bg);
border: 2px groove var(--xp-button);
border-radius: 2px;
}
.xp-panel-sunken {
border-style: inset;
}
.xp-panel-raised {
border-style: outset;
}
/* Windows XP Header Style */
.xp-header {
background: linear-gradient(to right, var(--xp-header), #1084f7);
color: var(--xp-header-text);
padding: 8px 12px;
font-weight: bold;
border: 2px outset var(--xp-header);
}
/* Windows XP Dropdown Style */
.xp-dropdown {
background-color: var(--xp-text-bg);
border: 2px inset var(--xp-button);
padding: 3px 5px;
font-family: var(--xp-font);
font-size: 11px;
cursor: pointer;
}
.xp-dropdown:focus {
outline: 1px dotted var(--xp-fg);
outline-offset: -2px;
}/* Main Application Styles */
.main-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: var(--xp-bg);
}
/* Header */
.header {
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(to right, var(--xp-header), #1084f7);
color: var(--xp-header-text);
padding: 10px 15px;
border: 2px outset var(--xp-header);
margin: 5px;
flex-shrink: 0;
}
.header-title {
font-size: 14px;
font-weight: bold;
margin: 0;
}
.header-right {
display: flex;
align-items: center;
gap: 15px;
}
.status-label {
font-size: 11px;
}
.refresh-btn {
background-color: var(--xp-button);
border: 2px outset var(--xp-button);
padding: 5px 12px;
font-family: var(--xp-font);
font-size: 11px;
cursor: pointer;
border-radius: 2px;
}
.refresh-btn:hover {
background-color: var(--xp-button-hover);
}
.refresh-btn:active {
border-style: inset;
}
.refresh-btn:disabled {
color: #999;
cursor: not-allowed;
}
/* Content Wrapper */
.content-wrapper {
flex: 1;
overflow: hidden;
margin: 0 5px;
}
.scrollable-content {
height: 100%;
overflow-y: auto;
overflow-x: hidden;
padding: 5px;
}
/* Scrollbar Styling */
.scrollable-content::-webkit-scrollbar {
width: 16px;
}
.scrollable-content::-webkit-scrollbar-track {
background: var(--xp-bg);
border: 2px inset var(--xp-button);
}
.scrollable-content::-webkit-scrollbar-thumb {
background: var(--xp-button);
border: 2px outset var(--xp-button);
}
.scrollable-content::-webkit-scrollbar-thumb:hover {
background: var(--xp-button-hover);
}
/* Section Styles */
.section {
background-color: var(--xp-bg);
border: 2px groove var(--xp-button);
margin-bottom: 10px;
}
.section-header {
background: linear-gradient(to right, var(--xp-header), #1084f7);
color: var(--xp-header-text);
padding: 6px 10px;
border-bottom: 1px solid var(--xp-border);
}
.section-title {
font-size: 11px;
font-weight: bold;
margin: 0;
}
.section-content {
padding: 15px;
background-color: var(--xp-bg);
}
/* Footer */
.footer {
background-color: var(--xp-bg);
border: 2px inset var(--xp-button);
padding: 5px 10px;
text-align: center;
font-size: 10px;
color: var(--xp-gray);
margin: 5px;
flex-shrink: 0;
}
/* Loading Overlay */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(236, 233, 216, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-overlay.hidden {
display: none;
}
.loading-container {
background-color: var(--xp-text-bg);
border: 3px outset var(--xp-header);
padding: 30px 40px;
text-align: center;
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.3);
}
.loading-icon {
font-size: 36px;
margin-bottom: 10px;
}
.loading-text {
font-size: 13px;
font-weight: bold;
color: var(--xp-header);
margin-bottom: 5px;
}
.loading-subtext {
font-size: 10px;
color: var(--xp-gray);
}
/* Error and Empty States */
.error-message,
.empty-message {
text-align: center;
padding: 20px;
color: var(--xp-gray);
font-size: 11px;
}
.error-message {
color: var(--xp-negative);
}
/* Positive/Negative Colors */
.positive {
color: var(--xp-positive);
}
.negative {
color: var(--xp-negative);
}/* Component-Specific Styles */
/* Tickers Section */
.tickers-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
}
.ticker-card {
background-color: var(--xp-text-bg);
border: 2px inset var(--xp-button);
padding: 15px;
text-align: center;
}
.ticker-name {
font-size: 12px;
font-weight: bold;
margin-bottom: 8px;
}
.ticker-price {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
}
.ticker-change {
font-size: 11px;
font-weight: bold;
margin-bottom: 8px;
}
.ticker-info {
font-size: 9px;
color: var(--xp-gray);
}
/* Gainers & Losers Section */
.selector-controls {
display: flex;
gap: 20px;
margin-bottom: 15px;
align-items: center;
}
.selector-label {
display: flex;
align-items: center;
gap: 8px;
}
.label-text {
font-weight: bold;
font-size: 11px;
}
.selector-dropdown {
background-color: var(--xp-text-bg);
border: 2px inset var(--xp-button);
padding: 4px 6px;
font-family: var(--xp-font);
font-size: 11px;
cursor: pointer;
min-width: 120px;
}
.gainers-losers-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.column-title {
font-size: 11px;
font-weight: bold;
margin-bottom: 8px;
text-align: center;
}
.stock-list {
background-color: var(--xp-text-bg);
border: 2px inset var(--xp-button);
max-height: 320px;
overflow-y: auto;
}
.stock-item {
display: grid;
grid-template-columns: 140px 1fr 100px;
gap: 10px;
padding: 5px 10px;
align-items: center;
border-bottom: 1px solid #f0f0f0;
}
.stock-item:nth-child(even) {
background-color: #f8f8f8;
}
.stock-symbol {
font-weight: bold;
font-size: 10px;
}
.stock-price {
font-size: 10px;
text-align: right;
}
.stock-change {
font-size: 10px;
font-weight: bold;
text-align: right;
}
/* Market Sentiment Section */
.sentiment-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.sentiment-card {
background-color: var(--xp-text-bg);
border: 2px inset var(--xp-button);
padding: 20px;
text-align: center;
}
.card-title {
font-size: 11px;
font-weight: bold;
margin-bottom: 10px;
}
.card-value {
font-size: 24px;
font-weight: bold;
margin-bottom: 10px;
}
.card-change,
.card-status {
font-size: 12px;
font-weight: bold;
margin-bottom: 5px;
}
.meter-legend {
font-size: 9px;
color: var(--xp-gray);
margin-top: 10px;
}
/* Sectoral Performance Section */
.sectoral-list {
background-color: var(--xp-text-bg);
border: 2px inset var(--xp-button);
}
.sector-row {
display: grid;
grid-template-columns: 120px 1fr 100px;
gap: 15px;
padding: 8px 12px;
align-items: center;
border-bottom: 1px solid #f0f0f0;
}
.sector-row.even {
background-color: #f8f8f8;
}
.sector-name {
font-weight: bold;
font-size: 11px;
}
.sector-bar-container {
position: relative;
height: 20px;
}
.sector-bar-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.sector-bar {
height: 10px;
margin-top: 5px;
border-radius: 2px;
}
.sector-bar.positive {
background-color: var(--xp-positive);
}
.sector-bar.negative {
background-color: var(--xp-negative);
}
.sector-change {
font-size: 11px;
font-weight: bold;
text-align: right;
}
/* Responsive adjustments */
@media (max-width: 1200px) {
.tickers-grid {
grid-template-columns: 1fr;
}
.gainers-losers-grid,
.sentiment-grid {
grid-template-columns: 1fr;
}
}{
"name": "bazaar-electron",
"version": "1.0.0",
"description": "Indian Stock Market Dashboard with Windows XP Theme",
"main": "main.js",
"scripts": {
"start": "electron .",
"dev": "NODE_ENV=development electron .",
"build": "electron-builder",
"build:win": "electron-builder --win",
"build:mac": "electron-builder --mac",
"build:linux": "electron-builder --linux",
"pack": "electron-builder --dir",
"dist": "electron-builder"
},
"keywords": ["stock", "market", "indian", "nifty", "sensex", "trading"],
"author": "Your Name",
"license": "MIT",
"devDependencies": {
"electron": "^28.0.0",
"electron-builder": "^24.9.1"
},
"dependencies": {
"yahoo-finance2": "^2.11.1",
"axios": "^1.6.0"
},
"build": {
"appId": "com.bazaar.stockmarket",
"productName": "Bazaar",
"directories": {
"output": "dist",
"buildResources": "assets"
},
"files": [
"main.js",
"preload.js",
"src/**/*",
"assets/**/*",
"node_modules/**/*",
"package.json"
],
"win": {
"target": ["nsis", "portable"],
"icon": "assets/icon.ico"
},
"mac": {
"target": ["dmg", "zip"],
"icon": "assets/icon.icns",
"category": "public.app-category.finance"
},
"linux": {
"target": ["AppImage", "deb"],
"icon": "assets/icon.png",
"category": "Office;Finance"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
}
}
}# Development mode with hot reload
npm run dev
# Build for current platform
npm run build
# Build for specific platform
npm run build:win # Windows
npm run build:mac # macOS
npm run build:linux # Linux
# Create portable version (no installer)
npm run packAfter building, you'll find:
Windows:
dist/Bazaar Setup 1.0.0.exe(installer)dist/Bazaar 1.0.0.exe(portable)
macOS:
dist/Bazaar-1.0.0.dmg(disk image)dist/Bazaar-1.0.0-mac.zip(zip archive)
Linux:
dist/Bazaar-1.0.0.AppImage(portable)dist/bazaar_1.0.0_amd64.deb(Debian package)
-
Installer (NSIS)
- Professional installer experience
- Registry entries
- Start menu shortcuts
- Uninstaller
-
Portable
- Single .exe file
- No installation required
- Portable between machines
-
DMG Image
- Drag-and-drop installation
- Professional appearance
- Code signing (requires Apple Developer account)
-
App Notarization (optional)
# Requires Apple Developer account xcrun altool --notarize-app \ --primary-bundle-id "com.bazaar.stockmarket" \ --username "your@email.com" \ --password "@keychain:AC_PASSWORD" \ --file dist/Bazaar-1.0.0.dmg
-
AppImage
- Universal Linux format
- No installation needed
- Works on most distributions
-
DEB Package
- For Debian/Ubuntu systems
- Integrated with package manager
-
Snap/Flatpak (optional)
- Sandboxed distribution
- Better security
- [ ] Application launches successfully
- [ ] All indices display correctly (Nifty, Sensex, Bank Nifty)
- [ ] Gainers/Losers load for all indices
- [ ] Time period selector works (1D, 1Week, etc.)
- [ ] VIX and sentiment data displays
- [ ] Sectoral performance shows correctly
- [ ] Refresh button works
- [ ] Auto-refresh triggers after 60 seconds
- [ ] Scrolling works smoothly
- [ ] Window resizes properly (min 1000x700)
- [ ] Loading overlay appears/disappears correctly
- [ ] Error states display when network fails
- [ ] Last update timestamp updates correctlyInstall testing frameworks:
npm install --save-dev jest electron-mocha spectronCreate test file tests/app.test.js:
const { Application } = require("spectron");
const path = require("path");
describe("Application launch", function () {
this.timeout(10000);
beforeEach(function () {
this.app = new Application({
path: path.join(__dirname, "..", "node_modules", ".bin", "electron"),
args: [path.join(__dirname, "..")],
});
return this.app.start();
});
afterEach(function () {
if (this.app && this.app.isRunning()) {
return this.app.stop();
}
});
it("shows an initial window", async function () {
const count = await this.app.client.getWindowCount();
expect(count).toBe(1);
});
it("has the correct title", async function () {
const title = await this.app.client.getTitle();
expect(title).toBe("Bazaar - Indian Stock Market");
});
});-
Create GitHub Repository
git init git add . git commit -m "Initial commit" git remote add origin https://github.com/yourusername/bazaar-electron.git git push -u origin main
-
Create Release
- Tag version:
git tag v1.0.0 - Push tag:
git push origin v1.0.0 - Create release on GitHub
- Upload build artifacts from
dist/
- Tag version:
Install electron-updater:
npm install electron-updaterAdd to main.js:
const { autoUpdater } = require("electron-updater");
// Check for updates
app.whenReady().then(() => {
autoUpdater.checkForUpdatesAndNotify();
});
autoUpdater.on("update-available", () => {
mainWindow.webContents.send("update-available");
});
autoUpdater.on("update-downloaded", () => {
mainWindow.webContents.send("update-downloaded");
});Configure in package.json:
{
"build": {
"publish": {
"provider": "github",
"owner": "yourusername",
"repo": "bazaar-electron"
}
}
}Split large components:
// Load heavy components on demand
async function loadChartModule() {
const module = await import("./js/charts.js");
return module;
}Cache API responses:
const cache = new Map();
const CACHE_DURATION = 30000; // 30 seconds
async function getCachedData(key, fetchFn) {
const now = Date.now();
const cached = cache.get(key);
if (cached && now - cached.timestamp < CACHE_DURATION) {
return cached.data;
}
const data = await fetchFn();
cache.set(key, { data, timestamp: now });
return data;
}Load sections as user scrolls:
// Intersection Observer for lazy loading
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
loadSection(entry.target);
}
});
});
document.querySelectorAll(".section").forEach((section) => {
observer.observe(section);
});Minimize bundle size:
{
"build": {
"asar": true,
"compression": "maximum",
"files": [
"!node_modules/**/*",
"node_modules/yahoo-finance2/**/*",
"node_modules/axios/**/*"
]
}
}Problem: Too many requests causing failures
Solution:
// Add delay between requests
async function delayedFetch(symbol, delay = 500) {
await new Promise((resolve) => setTimeout(resolve, delay));
return yahooFinance.quote(symbol);
}Problem: Memory leaks from not clearing intervals
Solution:
// Proper cleanup
app.on("before-quit", () => {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
}
});Problem: Antivirus flags electron app
Solution:
- Code sign your application
- Use electron-builder's signing options
- Request exclusion from antivirus vendors
Problem: Gatekeeper blocks unsigned app
Solution:
# Users can bypass with:
xattr -cr /Applications/Bazaar.app
# Or properly sign the app with:
codesign --deep --force --verify --verbose --sign "Developer ID" Bazaar.appProblem: Missing dependencies on Linux
Solution:
# Install required libraries
sudo apt-get install libgconf-2-4 libgtk-3-0- Electron Forge - Alternative builder
- Electron Fiddle - Playground
- Electron DevTools - Debugging
- ✅ Set up project structure
- ✅ Implement main and renderer processes
- ✅ Create data fetcher module
- ✅ Build basic UI components
- Complete Windows XP styling
- Add loading states
- Implement error handling
- Add animations and transitions
- Add charts and visualizations
- Implement user preferences
- Add export functionality
- Create watchlist feature
- Set up electron-builder
- Create installers for all platforms
- Implement auto-updates
- Publish to GitHub Releases
This guide provides a complete roadmap for implementing Bazaar using Electron.js. The key advantages of this approach are:
✅ Modern stack with better tooling
✅ Cross-platform consistency with web technologies
✅ Rich UI capabilities with HTML/CSS/JavaScript
✅ Easy distribution with electron-builder
✅ Active ecosystem with thousands of packages
✅ Auto-update support for seamless updates
- Windows: ~80-120 MB
- macOS: ~90-130 MB
- Linux: ~80-120 MB
- Basic functionality: 1-2 weeks
- UI polish: 1 week
- Testing & debugging: 1 week
- Build & distribution: 2-3 days
Total: 3-4 weeks for complete implementation
Feel free to contribute improvements to this implementation guide or the actual application!
This implementation guide is provided as-is for educational and development purposes.
Happy Coding! 🚀📈