| name | marketingdashboard-financial-cockpit |
| description | Real-time financial market research dashboard aggregating CN/HK/US indices, commodities, treasury yields, sector flows, news, and AI token usage |
| triggers | ["create a financial market dashboard","build a stock market monitoring screen","set up real-time market data display","implement financial data visualization","create a trading cockpit interface","build a market research dashboard","set up commodity price tracking","create AI token usage dashboard"] |
marketingdashboard Financial Cockpit
Skill by ara.so — Marketing Skills collection.
A comprehensive real-time market research dashboard built with React 19, Vite, and TypeScript that aggregates A-share/Hong Kong/US stock indices, commodities, treasury yields, sector hotspots, capital flows, 7×24 news, industry chain watchlists, and AI model token usage trends on a single screen.
What It Does
This project provides a zero-dependency data service with built-in Node.js proxy that aggregates public market data APIs. It features:
- Global market indices with minute-level charts (SSE, SZSE, HSI, DJIA, NDX, SPX, VIX, USD/CNY)
- Commodities & crypto (gold, silver, copper, crude oil, BTC) with real-time prices
- US Treasury yields monitoring (10Y, 2Y, spread, yield curve history)
- Sector analysis with industry/concept board rankings and constituent stocks
- Capital flow tracking with real-time money flow charts and stock rankings
- Industry chain visualization (semiconductors, AI compute, EVs, robotics, biotech)
- AI Dashboard tracking 50+ LLM providers' token consumption via OpenRouter API
- Goods price page with futures trends and spot-futures basis tables
- 7×24 financial news aggregation with keyword highlighting
- PWA support for desktop installation
Installation
Prerequisites
- Node.js 18+
curl command available (for some proxy endpoints)
Local Development
git clone https://github.com/theBigGavin/marketingdashboard.git
cd marketingdashboard
npm install
pnpm install
npm run dev
Production Build
npm run build
echo 'OPENROUTER_API_KEY=sk-or-v1-your-key-here' > server/.env
npm start
Visit http://localhost:3000
Docker Deployment
docker build -t market-cockpit .
docker run -p 3000:3000 market-cockpit
Architecture & Data Flow
The project uses a unified quote center (src/lib/market.ts) that batch-fetches prices every 5s and distributes snapshots to all components. Server-side caching reduces API pressure:
import { quoteCenter } from '@/lib/market';
useEffect(() => {
const codes = ['sh000001', 'sz399001', 'hk.HSI'];
quoteCenter.subscribe(codes);
return () => quoteCenter.unsubscribe(codes);
}, []);
const quote = quoteCenter.getQuote('sh000001');
Key API Endpoints
All endpoints are proxied through /api/*:
Real-time Quotes
const response = await fetch('/api/quotes?codes=sh000001,sz399001,hk.HSI');
const quotes = await response.json();
Minute Charts
const response = await fetch('/api/minute?code=sh000001');
const data = await response.json();
Sector Boards
const response = await fetch('/api/boards?type=industry&dir=desc&n=20');
const boards = await response.json();
Board Constituent Stocks
const response = await fetch('/api/board-stocks?code=BK0477&n=50');
const stocks = await response.json();
Commodities & Futures
const response = await fetch('/api/futures?list=GC00,CL00,BTC');
const futures = await response.json();
const response = await fetch('/api/future-daily?code=nf_AU0&n=90');
const dailyData = await response.json();
Capital Flow
const response = await fetch('/api/moneyflow?n=30');
const flows = await response.json();
const response = await fetch('/api/board-flow?n=10');
const boardFlows = await response.json();
Stock Rankings
const response = await fetch('/api/rank?sort=amount&n=50');
const ranks = await response.json();
US Treasury Yields
const response = await fetch('/api/treasuries');
const yields = await response.json();
const response = await fetch('/api/treasury-history');
const history = await response.json();
Industry Chain & Stock Search
const response = await fetch('/api/mystery-select?query=半导体&limit=50');
const stocks = await response.json();
const response = await fetch('/api/chain-parse', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: '上游:硅片\n中游:芯片设计\n下游:封装测试' })
});
const parsed = await response.json();
const response = await fetch('/api/stock-search?q=贵州茅台');
const results = await response.json();
AI Dashboard (OpenRouter)
const response = await fetch('/api/openrouter-usage');
const usage = await response.json();
Financial News
const response = await fetch('/api/news?page=1&size=50');
const news = await response.json();
Component Examples
Using the Polling Hook
import { usePolling } from '@/hooks/usePolling';
function MarketPanel() {
const { data, loading, error } = usePolling<QuoteData[]>(
async () => {
const res = await fetch('/api/quotes?codes=sh000001,sz399001');
return res.json();
},
5000
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{data?.map(quote => (
<div key={quote.code}>
{quote.name}: {quote.price} ({quote.changePercent}%)
</div>
))}
</div>
);
}
Creating a Mini Sparkline Chart
import { Spark } from '@/components/dash/Spark';
function IndexCard() {
const [minuteData, setMinuteData] = useState<number[]>([]);
useEffect(() => {
fetch('/api/minute?code=sh000001')
.then(res => res.json())
.then(data => setMinuteData(data.prices));
}, []);
return (
<div className="p-4 bg-slate-800 rounded">
<h3>上证指数</h3>
<Spark
data={minuteData}
width={200}
height={60}
color="#10b981"
strokeWidth={1.5}
/>
</div>
);
}
Building a Sector Panel
import { usePolling } from '@/hooks/usePolling';
function SectorPanel() {
const { data: boards } = usePolling(
async () => {
const res = await fetch('/api/boards?type=industry&dir=desc&n=10');
return res.json();
},
10000
);
const [selectedBoard, setSelectedBoard] = useState<string | null>(null);
const { data: stocks } = usePolling(
async () => {
if (!selectedBoard) return [];
const res = await fetch(`/api/board-stocks?code=${selectedBoard}&n=20`);
return res.json();
},
5000,
[selectedBoard]
);
return (
<div className="grid grid-cols-2 gap-4">
<div>
<h3>行业板块</h3>
{boards?.map(board => (
<div
key={board.code}
= => setSelectedBoard(board.code)}
className="cursor-pointer hover:bg-slate-700 p-2"
>
{board.name} {board.changePercent}%
))}
成分股
{stocks?.map(stock => (
{stock.name} {stock.price} ({stock.changePercent}%)
))}
);
}
Watchlist with Local Storage
import { useState, useEffect } from 'react';
import { quoteCenter } from '@/lib/market';
function WatchlistPanel() {
const [watchlist, setWatchlist] = useState<string[]>(() => {
const saved = localStorage.getItem('watchlist');
return saved ? JSON.parse(saved) : [];
});
useEffect(() => {
localStorage.setItem('watchlist', JSON.stringify(watchlist));
quoteCenter.subscribe(watchlist);
return () => quoteCenter.unsubscribe(watchlist);
}, [watchlist]);
const addStock = (code: string) => {
setWatchlist(prev => [...new Set([...prev, code])]);
};
const removeStock = (code: string) => {
setWatchlist(prev => prev.filter( => c !== code));
};
(
);
}
Configuration
Environment Variables
Create server/.env for server-side configuration:
OPENROUTER_API_KEY=sk-or-v1-your-key-here
PORT=3000
Static Configuration Files
Key configuration files in src/config/:
export const INDICES = [
{ code: 'sh000001', name: '上证指数', color: '#ef4444' },
{ code: 'sz399001', name: '深证成指', color: '#10b981' },
];
export const COMMODITIES = [
{ code: 'GC00', name: '纽约金', unit: '美元/盎司' },
{ code: 'CL00', name: 'WTI原油', unit: '美元/桶' },
];
export const CHAINS = [
{
name: '半导体产业链',
segments: [
{ name: '上游', stocks: ['600584', '688981', ...] },
{ name: '中游', stocks: ['688396', '688256', ...] },
{ name: '下游', : [, , ...] }
]
},
];
Common Patterns
Multi-Panel Dashboard Layout
function Dashboard() {
return (
<div className="grid grid-cols-12 gap-4 p-4 bg-slate-900 min-h-screen">
{/* Left column: Indices + Commodities */}
<div className="col-span-3 space-y-4">
<IndicesPanel />
<CommoditiesPanel />
</div>
{/* Center column: Sectors + Capital Flow */}
<div className="col-span-6 space-y-4">
<SectorPanel />
<CapitalFlowPanel />
</div>
{/* Right column: News + Watchlist */}
<div className="col-span-3 space-y-4">
<NewsPanel />
<WatchlistPanel />
</div>
</div>
);
}
Shared Polling for Multiple Components
import { useSharedPolling } from '@/hooks/useSharedPolling';
function Component1() {
const { data } = useSharedPolling('sector-data',
() => fetch('/api/boards?type=industry&n=10').then(r => r.json()),
10000
);
}
function Component2() {
const { data } = useSharedPolling('sector-data',
() => fetch('/api/boards?type=industry&n=10').then(r => r.json()),
10000
);
}
Error Handling with Fallback
function RobustPanel() {
const { data, error, retry } = usePolling(
async () => {
try {
const res = await fetch('/api/quotes?codes=sh000001');
if (!res.ok) throw new Error('API error');
return res.json();
} catch (err) {
const res = await fetch('https://qt.gtimg.cn/q=sh000001');
return parseDirectResponse(res);
}
},
5000
);
if (error) {
return (
<div>
Error loading data. <button onClick={retry}>Retry</button>
</div>
);
}
}
Troubleshooting
API Rate Limiting
The server enforces rate limits:
- Public endpoints: 240 requests/minute per IP
- Private endpoints (mystery-select, openrouter): 20 requests/minute per IP
- Response: 429 Too Many Requests
Solution: Increase polling intervals or implement request batching.
CORS Issues
APIs only reflect CORS headers for same-origin requests. Cross-origin browser requests are blocked.
Solution: Always use the /api/* proxy, not direct external API URLs.
Memory Cache Growing
Server uses LRU cache with automatic cleanup. If memory issues occur:
Solution: Restart the server or reduce cache TTL in server/index.cjs:
const CACHE_TTL = {
quotes: 1500,
boards: 5000,
news: 30000,
};
Missing Historical Data
Spot price history is accumulated by server-side scheduled tasks (every 4 hours).
Solution: Run the server continuously to build history, or manually trigger:
node -e "require('./index.cjs').collectSpotPrices()"
OpenRouter API Not Working
AI Dashboard requires OPENROUTER_API_KEY environment variable.
Solution:
OPENROUTER_API_KEY=sk-or-v1-your-actual-key
npm start
PWA Not Installing
PWA requires HTTPS or localhost.
Solution: Deploy behind a reverse proxy with SSL (nginx, Cloudflare Tunnel) or use localhost for testing.
Development Proxy Not Working
Frontend dev server (Vite) should auto-proxy /api to port 3001.
Solution: Check vite.config.ts:
export default defineConfig({
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true
}
}
}
});
If port 3001 is in use, change it in server/dev.cjs:
const PORT = process.env.PROXY_PORT || 3001;
Advanced Usage
Adding Custom Industry Chains
Edit src/config/chains.ts:
export const CHAINS = [
{
name: 'My Custom Chain',
segments: [
{
name: 'Upstream',
stocks: ['600000', '600001']
},
{
name: 'Midstream',
stocks: ['600002', '600003']
},
{
name: 'Downstream',
stocks: ['600004']
}
]
}
];
Extending API Endpoints
Add new endpoints in server/index.cjs:
function handleRequest(req, res) {
const url = new URL(req.url, `http://${req.headers.host}`);
if (url.pathname === '/api/my-endpoint') {
const data = await fetchMyData();
sendJSON(res, data);
return;
}
}
Custom Market Hours
Trading hours are defined in src/lib/market.ts:
export function isInTradingHours(market: 'cn' | 'us' | 'hk'): boolean {
const now = new Date();
const hour = now.getHours();
const minute = now.getMinutes();
if (market === 'cn') {
return (hour === 9 && minute >= 30) ||
(hour >= 10 && hour < 11) ||
(hour === 11 && minute < 30) ||
(hour >= 13 && hour < 15);
}
}
This skill covers the essential usage patterns for building financial market dashboards with the marketingdashboard project.