diff --git a/MARKET_SUPPORT.md b/MARKET_SUPPORT.md
index 5400bc6..db5fb83 100644
--- a/MARKET_SUPPORT.md
+++ b/MARKET_SUPPORT.md
@@ -1,153 +1,26 @@
-# ๐ Market Support & Limitations
+# Market support
-OpenStock supports stocks from multiple exchanges worldwide, but there are important limitations to be aware of based on our data providers (Finnhub & TradingView).
+What OpenStock can show depends on two free data sources. This table was tested against both on the free plans (September 2026). Paid Finnhub or TradingView plans unlock more.
-## โ Fully Supported Markets
+| Market | Charts and widgets (TradingView embeds) | Our quotes, watchlist prices, alerts (Finnhub free) |
+|---|---|---|
+| ๐บ๐ธ United States (NYSE, Nasdaq, AMEX) | Live | Yes |
+| Crypto (Binance pairs, e.g. `BINANCE:BTCUSDT`) | Live | Yes |
+| Forex (e.g. `FX:EURUSD`) | Live | No |
+| ๐จ๐ฆ Canada (TSX) | Delayed | No |
+| ๐ฆ๐บ Australia (ASX) | Delayed | No |
+| ๐ฎ๐ณ India, through BSE (`.BO`; `.NS` symbols are charted on BSE) | End of day | No |
+| ๐ฉ๐ช Germany (Xetra) | End of day | No |
+| India NSE, London (LSE), Tokyo (TSE), Hong Kong (HKEX) | Blocked in embeds | No |
+| Index tickers such as NIFTY, FTSE 100, DAX, Nikkei 225, Hang Seng | Blocked in embeds | No |
-### Americas
-- **US**: NASDAQ, NYSE, AMEX (symbols like `AAPL`, `MSFT`)
-- **Canada**: TSX (symbols like `RY.TO`)
-- **Brazil**: BMFBOVESPA (symbols like `VALE3.SA`)
-- **Mexico**: BMV (symbols like `ASUR.MX`)
-- **Argentina**: BCBA (symbols like `BMA.BA`)
+## What that means in the app
-### Europe
-- **UK**: LSE (symbols like `BARC.L`)
-- **France**: EURONEXT (symbols like `AIR.PA`)
-- **Germany**: XETRA (symbols like `SAP.DE`)
-- **Sweden**: OMXSTO (symbols like `ERIC-B.ST`)
-- **Spain**: BMAD (symbols like `REPSOL.MC`)
-- **Italy**: EURONEXT Milan (symbols like `ENI.MI`)
-- **Belgium**: EURONEXT (symbols like `GIB.BR`)
-- **Denmark**: OMXCOPENHAGEN (symbols like `ISH.CO`)
-- **Finland**: OMXHEX (symbols like `NOKIA.HE`)
-- **Greece**: ATHEX (symbols like `ETE.ATH`)
-- **Ireland**: ISE (symbols like `RY.IR`)
-- **Netherlands**: EURONEXT (symbols like `ING.AS`)
-- **Norway**: OMXOSLO (symbols like `EQNR.OL`)
-- **Poland**: WSE (symbols like `PZU.WA`)
-- **Portugal**: EURONEXT (symbols like `BCP.LI`)
-- **Switzerland**: SIX (symbols like `NESN.SW`)
+- **Dashboard:** pick a market (US, India, Germany, Canada, Australia, Crypto, Forex). US and crypto tiles use our own cached quotes; the others use TradingView quote tiles.
+- **Stock pages:** symbols Finnhub can price get the full header (live price, day range, market cap). Others get TradingView's quote panel.
+- **Alerts:** US stocks and crypto only, because the alert checker needs a quote source it can call every five minutes.
+- **Search:** Finnhub search returns listings worldwide; blocked exchanges will show TradingView's "only available on TradingView" message in charts.
-### Asia-Pacific
-- **Taiwan**: TWSE (symbols like `2330.TW`), TPEX (symbols like `6488.TWO`)
-- **Hong Kong**: HKEX (symbols like `0700.HK`)
-- **Japan**: TSE (symbols like `7203.T`)
-- **South Korea**: KRX (symbols like `005930.KS`), KOSDAQ (symbols like `010000.KQ`)
-- **Singapore**: SGX (symbols like `U11.SI`)
-- **Australia**: ASX (symbols like `CBA.AX`)
-- **New Zealand**: NZX (symbols like `FBU.NZ`)
-- **India**: NSE (symbols like `INFY.NS`), BSE (symbols like `INFY.BO`)
-- **Thailand**: SET (symbols like `ADVANC.BK`)
-- **Malaysia**: KLSE (symbols like `1023.KL`)
-- **Philippines**: PSE (symbols like `JFC.PH`)
-- **Indonesia**: IDX (symbols like `BBCA.JK`)
+## Adding a market
-### Middle East & Africa
-- **Israel**: TASE (symbols like `TEVA.TA`)
-- **South Africa**: JSE (symbols like `NPN.JO`)
-- **Saudi Arabia**: TASI (limited support)
-- **UAE**: ADX (limited support)
-
-## โ ๏ธ Known Limitations
-
-### TradingView Widget Limitations
-
-TradingView's free tier embeddable widgets have several restrictions:
-
-1. **International Markets**: Some symbols, especially from emerging markets (India NSE, Vietnam, etc.), may show:
- - "This symbol is only available on TradingView" error
- - Missing charts or company profile data
- - Empty technical analysis indicators
-
-2. **Affected Markets**:
- - India (NSE/BSE): Free tier support is limited
- - Vietnam, Philippines, Indonesia: Partial or no support
- - Emerging market stocks: Often require paid subscription
-
-3. **Why This Happens**:
- - TradingView's free widget tier has limited symbol availability
- - Some exchanges require commercial licensing
- - High-volume markets get priority in free tier
-
-### Finnhub API Limitations
-
-1. **Free Tier**:
- - Supports basic quote and company data for most exchanges
- - Real-time data delayed by 15+ minutes for non-US stocks
- - Rate limited to 60 API calls per minute
- - No access to historical minute-level bars
-
-2. **Market-Specific**:
- - India NSE/BSE: Available but with delays
- - Chinese A-shares: Not available in free tier
- - Forex: Not available
- - Cryptocurrencies: Not available
-
-## ๐ง Troubleshooting
-
-### "This symbol is only available on TradingView"
-
-**What this means**: TradingView's embedded widgets don't support this symbol.
-
-**What you can still do**:
-- โ Search for the stock using Finnhub data
-- โ View company profile from Finnhub
-- โ Add to watchlist (data updates available)
-- โ See market news from Finnhub
-- โ View interactive TradingView charts
-- โ See technical analysis indicators from TradingView
-
-**Solutions**:
-1. **For Personal Use**: Upgrade to Finnhub/TradingView paid plans
-2. **For Self-Hosted Deployments**:
- - Upgrade API keys in your `.env` file
- - Consider alternative chart libraries (e.g., Lightweight Charts, Chart.js)
-
-### Charts Don't Load
-
-**Possible causes**:
-- Symbol not supported on TradingView
-- Network connectivity issue
-- TradingView API rate limiting
-
-**Troubleshooting**:
-1. Check browser console for errors (F12)
-2. Verify symbol exists on Finnhub search
-3. Try a US stock (e.g., AAPL) to confirm basic functionality
-4. Check your internet connection
-
-## ๐ Future Improvements
-
-The OpenStock community is working on:
-
-- [ ] Fallback chart libraries for unsupported symbols
-- [ ] Market availability checker before displaying widgets
-- [ ] Alternative data sources for emerging markets
-- [ ] Forex support
-- [ ] Cryptocurrency support
-- [ ] Custom indicators and drawing tools
-- [ ] Paper trading features
-
-## ๐ก Contributing
-
-If you discover:
-- A market that should be supported
-- An exchange with incorrect symbol mapping
-- Alternative data providers we should consider
-
-Please [open an issue](https://github.com/Open-Dev-Society/OpenStock/issues) with:
-- Exchange name and country
-- Example stock symbols
-- Expected vs. actual behavior
-- Links to Finnhub/TradingView documentation
-
-## ๐ References
-
-- [Finnhub API Documentation](https://finnhub.io/docs/api)
-- [TradingView Widget Documentation](https://www.tradingview.com/pine-script-docs/)
-- [Supported Finnhub Exchanges](https://finnhub.io/docs/api/symbol-lookup)
-
----
-
-**Disclaimer**: Nothing here is financial advice. Market data availability depends on provider terms and your subscription tier. Always verify current data before making investment decisions. OpenStock is community-built and not a brokerage.
\ No newline at end of file
+Add an entry to `lib/markets.ts` with symbols from an exchange that renders in TradingView embeds (open one symbol in a quote widget first). To price and alert on more exchanges, you need a data plan that covers them.
diff --git a/__tests__/market-clock.test.ts b/__tests__/market-clock.test.ts
new file mode 100644
index 0000000..240042f
--- /dev/null
+++ b/__tests__/market-clock.test.ts
@@ -0,0 +1,54 @@
+import { describe, expect, it } from 'vitest';
+import { formatSpan, sessionOf } from '@/lib/market-session';
+
+const at = (h: number, m = 0) => h * 60 + m;
+
+describe('NYSE session', () => {
+ it('knows each weekday phase', () => {
+ expect(sessionOf(2, at(3)).phase).toBe('closed');
+ expect(sessionOf(2, at(4)).phase).toBe('pre');
+ expect(sessionOf(2, at(9, 30)).phase).toBe('open');
+ expect(sessionOf(2, at(15, 59)).phase).toBe('open');
+ expect(sessionOf(2, at(16)).phase).toBe('after');
+ expect(sessionOf(2, at(20)).phase).toBe('closed');
+ });
+
+ it('counts down to the right open', () => {
+ expect(sessionOf(2, at(9)).line).toBe('Pre-market ยท opens in 30m');
+ expect(sessionOf(2, at(14)).line).toBe('Market open ยท closes in 2h 00m');
+ // Friday night and Saturday wait for Monday 9:30
+ expect(sessionOf(5, at(21)).line).toBe('Market closed ยท opens in 2d 12h');
+ expect(sessionOf(6, at(12)).line).toBe('Market closed ยท opens in 1d 21h');
+ });
+
+ it('formats spans', () => {
+ expect(formatSpan(45)).toBe('45m');
+ expect(formatSpan(125)).toBe('2h 05m');
+ });
+});
+
+import { getMarket, hasFinnhubQuotes, isMarketOpen } from '@/lib/markets';
+
+describe('markets', () => {
+ it('knows which symbols Finnhub free can price', () => {
+ expect(hasFinnhubQuotes('AAPL')).toBe(true);
+ expect(hasFinnhubQuotes('BRK.B')).toBe(true);
+ expect(hasFinnhubQuotes('BINANCE:BTCUSDT')).toBe(true);
+ expect(hasFinnhubQuotes('RELIANCE.NS')).toBe(false);
+ expect(hasFinnhubQuotes('BARC.L')).toBe(false);
+ expect(hasFinnhubQuotes('7203.T')).toBe(false);
+ expect(hasFinnhubQuotes('FX:EURUSD')).toBe(false);
+ });
+
+ it('opens each market in its own time zone', () => {
+ // Wed 2026-09-23 10:00 in Mumbai = 04:30 UTC
+ expect(isMarketOpen(getMarket('in'), new Date('2026-09-23T04:30:00Z'))).toBe(true);
+ expect(isMarketOpen(getMarket('us'), new Date('2026-09-23T04:30:00Z'))).toBe(false);
+ // Saturday: stocks and FX closed, crypto open
+ expect(isMarketOpen(getMarket('de'), new Date('2026-09-26T10:00:00Z'))).toBe(false);
+ expect(isMarketOpen(getMarket('fx'), new Date('2026-09-26T10:00:00Z'))).toBe(false);
+ expect(isMarketOpen(getMarket('crypto'), new Date('2026-09-26T10:00:00Z'))).toBe(true);
+ // FX reopens Sunday 17:00 New York (21:00 UTC in September)
+ expect(isMarketOpen(getMarket('fx'), new Date('2026-09-27T21:30:00Z'))).toBe(true);
+ });
+});
diff --git a/__tests__/utils.test.ts b/__tests__/utils.test.ts
index 443a6c5..58ee746 100644
--- a/__tests__/utils.test.ts
+++ b/__tests__/utils.test.ts
@@ -46,8 +46,8 @@ describe('formatSymbolForTradingView', () => {
expect(formatSymbolForTradingView('005930.KS')).toBe('KRX:005930');
});
- it('maps India NSE (.NS) to NSE prefix', () => {
- expect(formatSymbolForTradingView('RELIANCE.NS')).toBe('NSE:RELIANCE');
+ it('charts India NSE (.NS) listings on BSE, since NSE is blocked in free TradingView embeds', () => {
+ expect(formatSymbolForTradingView('RELIANCE.NS')).toBe('BSE:RELIANCE');
});
it('maps India BSE (.BO) to BSE prefix', () => {
diff --git a/app/(root)/dashboard/page.tsx b/app/(root)/dashboard/page.tsx
new file mode 100644
index 0000000..4c488f5
--- /dev/null
+++ b/app/(root)/dashboard/page.tsx
@@ -0,0 +1,88 @@
+import { Suspense } from "react";
+import { cookies } from "next/headers";
+import TradingViewWidget from "@/components/TradingViewWidget";
+import Panel from "@/components/Panel";
+import IndexPulse, { PulseSkeleton } from "@/components/IndexPulse";
+import DataFreshness from "@/components/DataFreshness";
+import MarketSwitcher from "@/components/MarketSwitcher";
+import {
+ CRYPTO_HEATMAP_CONFIG,
+ STOCK_HEATMAP_CONFIG,
+ marketOverviewConfig,
+ marketQuotesConfig,
+ timelineConfig,
+} from "@/lib/constants";
+import { MARKETS, MARKET_COOKIE, getMarket, isMarketOpen, marketForCountry } from "@/lib/markets";
+import { getSession } from "@/lib/better-auth/auth";
+
+const scriptUrl = `https://s3.tradingview.com/external-embedding/embed-widget-`;
+
+// Widgets come from TradingView (live, no Finnhub quota) and render immediately; the tiles stream in.
+export default async function Dashboard({ searchParams }: { searchParams: Promise<{ market?: string }> }) {
+ const { market: requested } = await searchParams;
+ // Explicit choice, then the remembered one, then the market of the user's country
+ const saved = (await cookies()).get(MARKET_COOKIE)?.value;
+ const market = getMarket(requested ?? saved ?? marketForCountry((await getSession())?.user.country));
+ const open = isMarketOpen(market);
+ const native = market.pulse.some((p) => p.finnhub);
+ const heatmap = market.heatmap === 'stocks'
+ ? { script: 'stock-heatmap.js', config: STOCK_HEATMAP_CONFIG, title: 'S&P 500 heatmap' }
+ : market.heatmap === 'crypto'
+ ? { script: 'crypto-coins-heatmap.js', config: CRYPTO_HEATMAP_CONFIG, title: 'Crypto heatmap' }
+ : null;
+
+ return (
+ <>
+
+