Public/Get-StockExchangeData.ps1
|
#requires -Version 7.0 function Get-StockExchangeData { <# .SYNOPSIS Retrieves stock exchange trading hours for 20 major global exchanges. .DESCRIPTION Fetches trading hours data from Wikipedia, parses it, and saves to a local JSON cache file. Includes hardcoded fallback data for 20 major exchanges so the tool works offline. Used by PSExchangeClock to determine exchange open/close times, lunch breaks, and geographic coordinates. Supported exchanges: NYSE, NASDAQ, LSE, Euronext Paris, Tokyo, Shanghai, Hong Kong, Toronto, Frankfurt, Sydney, Bombay, NSE India, Korea, Switzerland, Johannesburg, B3 Brazil, Mexico, Singapore, Taiwan, Moscow. .PARAMETER ForceRefresh Bypass the cache and re-scrape from Wikipedia. .PARAMETER OutputPath Path to save the JSON cache file. Defaults to exchange-data.json in the module Data directory. .EXAMPLE Get-StockExchangeData # Uses cached data if available, otherwise fetches from Wikipedia .EXAMPLE Get-StockExchangeData -ForceRefresh # Forces a fresh scrape from Wikipedia .NOTES Author : PowerShellYoungTeam Version : 1.0.0 Project : https://github.com/PowerShellYoungTeam/PSExchangeClock .LINK https://github.com/PowerShellYoungTeam/PSExchangeClock .LINK New-StockExchangeCountdownDashboard #> [CmdletBinding()] param( [switch]$ForceRefresh, [string]$OutputPath = (Join-Path (Split-Path $PSScriptRoot -Parent) 'Data\exchange-data.json') ) function Get-DefaultExchangeData { <# .SYNOPSIS Returns hardcoded data for 20 major stock exchanges. #> @( [PSCustomObject]@{ Name = 'New York Stock Exchange' Code = 'XNYS' Symbol = 'NYSE' Country = 'United States' City = 'New York' TimeZoneId = 'Eastern Standard Time' OpenTimeLocal = '09:30' CloseTimeLocal = '16:00' LunchBreakStart = $null LunchBreakEnd = $null Latitude = 40.7069 Longitude = -74.0113 IsDefault = $true }, [PSCustomObject]@{ Name = 'NASDAQ' Code = 'XNAS' Symbol = 'NASDAQ' Country = 'United States' City = 'New York' TimeZoneId = 'Eastern Standard Time' OpenTimeLocal = '09:30' CloseTimeLocal = '16:00' LunchBreakStart = $null LunchBreakEnd = $null Latitude = 40.7569 Longitude = -73.9897 IsDefault = $true }, [PSCustomObject]@{ Name = 'London Stock Exchange' Code = 'XLON' Symbol = 'LSE' Country = 'United Kingdom' City = 'London' TimeZoneId = 'GMT Standard Time' OpenTimeLocal = '08:00' CloseTimeLocal = '16:30' LunchBreakStart = $null LunchBreakEnd = $null Latitude = 51.5155 Longitude = -0.0922 IsDefault = $true }, [PSCustomObject]@{ Name = 'Euronext Paris' Code = 'XPAR' Symbol = 'EPA' Country = 'France' City = 'Paris' TimeZoneId = 'Romance Standard Time' OpenTimeLocal = '09:00' CloseTimeLocal = '17:30' LunchBreakStart = $null LunchBreakEnd = $null Latitude = 48.8698 Longitude = 2.3371 IsDefault = $true }, [PSCustomObject]@{ Name = 'Tokyo Stock Exchange' Code = 'XTKS' Symbol = 'TSE' Country = 'Japan' City = 'Tokyo' TimeZoneId = 'Tokyo Standard Time' OpenTimeLocal = '09:00' CloseTimeLocal = '15:30' LunchBreakStart = '11:30' LunchBreakEnd = '12:30' Latitude = 35.6814 Longitude = 139.7637 IsDefault = $true }, [PSCustomObject]@{ Name = 'Shanghai Stock Exchange' Code = 'XSHG' Symbol = 'SSE' Country = 'China' City = 'Shanghai' TimeZoneId = 'China Standard Time' OpenTimeLocal = '09:30' CloseTimeLocal = '15:00' LunchBreakStart = '11:30' LunchBreakEnd = '13:00' Latitude = 31.2320 Longitude = 121.4758 IsDefault = $true }, [PSCustomObject]@{ Name = 'Hong Kong Stock Exchange' Code = 'XHKG' Symbol = 'HKEX' Country = 'Hong Kong' City = 'Hong Kong' TimeZoneId = 'China Standard Time' OpenTimeLocal = '09:30' CloseTimeLocal = '16:00' LunchBreakStart = '12:00' LunchBreakEnd = '13:00' Latitude = 22.2860 Longitude = 114.1580 IsDefault = $true }, [PSCustomObject]@{ Name = 'Toronto Stock Exchange' Code = 'XTSE' Symbol = 'TSX' Country = 'Canada' City = 'Toronto' TimeZoneId = 'Eastern Standard Time' OpenTimeLocal = '09:30' CloseTimeLocal = '16:00' LunchBreakStart = $null LunchBreakEnd = $null Latitude = 43.6490 Longitude = -79.3832 IsDefault = $true }, [PSCustomObject]@{ Name = 'Frankfurt Stock Exchange (XETRA)' Code = 'XETR' Symbol = 'FRA' Country = 'Germany' City = 'Frankfurt' TimeZoneId = 'W. Europe Standard Time' OpenTimeLocal = '09:00' CloseTimeLocal = '17:30' LunchBreakStart = $null LunchBreakEnd = $null Latitude = 50.1109 Longitude = 8.6821 IsDefault = $true }, [PSCustomObject]@{ Name = 'Australian Securities Exchange' Code = 'XASX' Symbol = 'ASX' Country = 'Australia' City = 'Sydney' TimeZoneId = 'AUS Eastern Standard Time' OpenTimeLocal = '10:00' CloseTimeLocal = '16:00' LunchBreakStart = $null LunchBreakEnd = $null Latitude = -33.8666 Longitude = 151.2073 IsDefault = $true }, [PSCustomObject]@{ Name = 'Bombay Stock Exchange' Code = 'XBOM' Symbol = 'BSE' Country = 'India' City = 'Mumbai' TimeZoneId = 'India Standard Time' OpenTimeLocal = '09:15' CloseTimeLocal = '15:30' LunchBreakStart = $null LunchBreakEnd = $null Latitude = 18.9262 Longitude = 72.8333 IsDefault = $false }, [PSCustomObject]@{ Name = 'National Stock Exchange of India' Code = 'XNSE' Symbol = 'NSE' Country = 'India' City = 'Mumbai' TimeZoneId = 'India Standard Time' OpenTimeLocal = '09:15' CloseTimeLocal = '15:30' LunchBreakStart = $null LunchBreakEnd = $null Latitude = 19.0553 Longitude = 72.8629 IsDefault = $false }, [PSCustomObject]@{ Name = 'Korea Exchange' Code = 'XKRX' Symbol = 'KRX' Country = 'South Korea' City = 'Seoul' TimeZoneId = 'Korea Standard Time' OpenTimeLocal = '09:00' CloseTimeLocal = '15:30' LunchBreakStart = $null LunchBreakEnd = $null Latitude = 37.5242 Longitude = 127.0507 IsDefault = $false }, [PSCustomObject]@{ Name = 'SIX Swiss Exchange' Code = 'XSWX' Symbol = 'SIX' Country = 'Switzerland' City = 'Zurich' TimeZoneId = 'W. Europe Standard Time' OpenTimeLocal = '09:00' CloseTimeLocal = '17:30' LunchBreakStart = $null LunchBreakEnd = $null Latitude = 47.3769 Longitude = 8.5417 IsDefault = $false }, [PSCustomObject]@{ Name = 'Johannesburg Stock Exchange' Code = 'XJSE' Symbol = 'JSE' Country = 'South Africa' City = 'Johannesburg' TimeZoneId = 'South Africa Standard Time' OpenTimeLocal = '09:00' CloseTimeLocal = '17:00' LunchBreakStart = $null LunchBreakEnd = $null Latitude = -26.2023 Longitude = 28.0436 IsDefault = $false }, [PSCustomObject]@{ Name = 'B3 - Brasil Bolsa Balcao' Code = 'BVMF' Symbol = 'B3' Country = 'Brazil' City = 'Sao Paulo' TimeZoneId = 'E. South America Standard Time' OpenTimeLocal = '10:00' CloseTimeLocal = '17:00' LunchBreakStart = $null LunchBreakEnd = $null Latitude = -23.5481 Longitude = -46.6335 IsDefault = $false }, [PSCustomObject]@{ Name = 'Mexican Stock Exchange' Code = 'XMEX' Symbol = 'BMV' Country = 'Mexico' City = 'Mexico City' TimeZoneId = 'Central Standard Time (Mexico)' OpenTimeLocal = '08:30' CloseTimeLocal = '15:00' LunchBreakStart = $null LunchBreakEnd = $null Latitude = 19.4213 Longitude = -99.1667 IsDefault = $false }, [PSCustomObject]@{ Name = 'Singapore Exchange' Code = 'XSES' Symbol = 'SGX' Country = 'Singapore' City = 'Singapore' TimeZoneId = 'Singapore Standard Time' OpenTimeLocal = '09:00' CloseTimeLocal = '17:00' LunchBreakStart = $null LunchBreakEnd = $null Latitude = 1.2833 Longitude = 103.8494 IsDefault = $false }, [PSCustomObject]@{ Name = 'Taiwan Stock Exchange' Code = 'XTAI' Symbol = 'TWSE' Country = 'Taiwan' City = 'Taipei' TimeZoneId = 'Taipei Standard Time' OpenTimeLocal = '09:00' CloseTimeLocal = '13:30' LunchBreakStart = $null LunchBreakEnd = $null Latitude = 25.0407 Longitude = 121.5141 IsDefault = $false }, [PSCustomObject]@{ Name = 'Moscow Exchange' Code = 'XMOS' Symbol = 'MOEX' Country = 'Russia' City = 'Moscow' TimeZoneId = 'Russian Standard Time' OpenTimeLocal = '09:50' CloseTimeLocal = '18:50' LunchBreakStart = $null LunchBreakEnd = $null Latitude = 55.7558 Longitude = 37.6173 IsDefault = $false } ) } function Invoke-WikipediaScrape { <# .SYNOPSIS Attempts to scrape stock exchange trading hours from Wikipedia. #> [CmdletBinding()] param() $url = 'https://en.wikipedia.org/wiki/List_of_stock_exchange_trading_hours' Write-Verbose "Fetching $url" try { $response = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 15 -ErrorAction Stop $html = $response.Content } catch { Write-Warning "Failed to fetch Wikipedia: $($_.Exception.Message)" return $null } # Attempt to parse wikitable rows # Look for sortable wikitable $tablePattern = '(?s)<table[^>]*class="[^"]*wikitable[^"]*sortable[^"]*"[^>]*>(.*?)</table>' if ($html -notmatch $tablePattern) { # Try without sortable $tablePattern = '(?s)<table[^>]*class="[^"]*wikitable[^"]*"[^>]*>(.*?)</table>' if ($html -notmatch $tablePattern) { Write-Warning "Could not find wikitable on page." return $null } } $tableHtml = $Matches[1] $rowPattern = '(?s)<tr>(.*?)</tr>' $cellPattern = '(?s)<t[dh][^>]*>(.*?)</t[dh]>' $rows = [regex]::Matches($tableHtml, $rowPattern) if ($rows.Count -lt 2) { Write-Warning "Table has insufficient rows ($($rows.Count))." return $null } # Parse header row to identify columns $headerRow = $rows[0] $headerCells = [regex]::Matches($headerRow.Groups[1].Value, $cellPattern) $headers = @() foreach ($cell in $headerCells) { $text = ($cell.Groups[1].Value -replace '<[^>]+>', '' -replace '&[^;]+;', ' ').Trim() $headers += $text.ToLower() } Write-Verbose "Headers found: $($headers -join ', ')" # Map known timezone IDs $tzMapping = @{ 'eastern' = 'Eastern Standard Time' 'et' = 'Eastern Standard Time' 'est' = 'Eastern Standard Time' 'gmt' = 'GMT Standard Time' 'bst' = 'GMT Standard Time' 'cet' = 'W. Europe Standard Time' 'cest' = 'W. Europe Standard Time' 'jst' = 'Tokyo Standard Time' 'cst' = 'China Standard Time' 'hkt' = 'China Standard Time' 'aest' = 'AUS Eastern Standard Time' 'ist' = 'India Standard Time' 'kst' = 'Korea Standard Time' } $scraped = @() for ($i = 1; $i -lt $rows.Count; $i++) { $cells = [regex]::Matches($rows[$i].Groups[1].Value, $cellPattern) $cellTexts = @() foreach ($cell in $cells) { $text = ($cell.Groups[1].Value -replace '<[^>]+>', '' -replace '&[^;]+;', ' ' -replace '\s+', ' ').Trim() $cellTexts += $text } if ($cellTexts.Count -lt 4) { continue } # Best-effort field extraction (depends on table structure) $exchangeName = $cellTexts[0] $timezone = if ($cellTexts.Count -gt 2) { $cellTexts[2] } else { '' } $openTime = if ($cellTexts.Count -gt 3) { $cellTexts[3] } else { '' } $closeTime = if ($cellTexts.Count -gt 4) { $cellTexts[4] } else { '' } if ($exchangeName -and $closeTime) { Write-Verbose " Scraped: $exchangeName | TZ=$timezone | Open=$openTime | Close=$closeTime" $scraped += [PSCustomObject]@{ ScrapedName = $exchangeName ScrapedTZ = $timezone ScrapedOpen = $openTime ScrapedClose = $closeTime } } } Write-Verbose "Scraped $($scraped.Count) exchange rows from Wikipedia." return $scraped } # ── Main Logic ──────────────────────────────────────────────── # Check cache if (-not $ForceRefresh -and (Test-Path $OutputPath)) { try { $cached = Get-Content $OutputPath -Raw -ErrorAction Stop | ConvertFrom-Json if ($cached.LastUpdated) { $age = (Get-Date) - [datetime]$cached.LastUpdated if ($age.TotalDays -lt 30) { Write-Verbose "Cache is $([int]$age.TotalDays) days old. Using cached data." return $cached.Exchanges } Write-Verbose "Cache is $([int]$age.TotalDays) days old. Refreshing." } } catch { Write-Verbose "Cache read failed: $($_.Exception.Message)" } } # Start with hardcoded defaults $exchanges = Get-DefaultExchangeData # Attempt scrape to verify/enhance data $scraped = Invoke-WikipediaScrape if ($scraped) { Write-Verbose "Wikipedia scrape returned $($scraped.Count) entries. Hardcoded data used as primary; scrape logged for reference." # In a future version, merge scraped data with hardcoded to add new exchanges # For now, the hardcoded data is authoritative (known-good timezone IDs and coordinates) } # Save to cache $cacheObject = [PSCustomObject]@{ LastUpdated = (Get-Date).ToString('o') Source = 'Hardcoded + Wikipedia scrape attempt' Version = '1.0' Exchanges = $exchanges } try { $cacheObject | ConvertTo-Json -Depth 5 | Set-Content $OutputPath -Encoding UTF8 -ErrorAction Stop Write-Verbose "Saved exchange data to $OutputPath" } catch { Write-Warning "Failed to save cache: $($_.Exception.Message)" } return $exchanges } # end function Get-StockExchangeData |