期货数据实时展示前端实现方案
下面我将为您提供一个完整的期货数据展示前端解决方案,基于StockTV API实现实时行情监控和K线图表展示。
设计思路
本方案采用模块化设计,包含以下核心功能:
- 期货品种列表展示
- 实时价格监控
- K线图表可视化
- WebSocket实时数据推送
- 响应式布局设计
完整HTML实现
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>期货实时行情监控系统 - StockTV数据对接</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root {
--primary-color: #1e88e5;
--positive-color: #4caf50;
--negative-color: #f44336;
--bg-color: #f5f5f5;
--card-bg: #ffffff;
--text-primary: #212121;
--text-secondary: #757575;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background-color: var(--bg-color);
color: var(--text-primary);
line-height: 1.6;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: var(--card-bg);
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
h1 {
color: var(--primary-color);
margin-bottom: 10px;
}
.subtitle {
color: var(--text-secondary);
font-size: 1.1rem;
}
.grid-container {
display: grid;
grid-template-columns: 300px 1fr;
gap: 20px;
}
@media (max-width: 968px) {
.grid-container {
grid-template-columns: 1fr;
}
}
.card {
background: var(--card-bg);
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
margin-bottom: 20px;
}
.card-title {
font-size: 1.2rem;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.futures-list {
max-height: 600px;
overflow-y: auto;
}
.futures-item {
padding: 12px 15px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background 0.2s;
}
.futures-item:hover {
background: #f9f9f9;
}
.futures-item.active {
background: #e3f2fd;
border-left: 3px solid var(--primary-color);
}
.symbol {
font-weight: bold;
display: flex;
justify-content: space-between;
}
.name {
font-size: 0.9rem;
color: var(--text-secondary);
margin-top: 5px;
}
.price-up {
color: var(--positive-color);
}
.price-down {
color: var(--negative-color);
}
.price-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.price-card {
padding: 15px;
border-radius: 8px;
text-align: center;
background: #f8f9fa;
}
.price-value {
font-size: 1.8rem;
font-weight: bold;
margin: 10px 0;
}
.price-change {
font-size: 1rem;
}
.chart-container {
height: 400px;
position: relative;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.interval-btn {
padding: 5px 10px;
background: #e0e0e0;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
}
.interval-btn.active {
background: var(--primary-color);
color: white;
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background: #e8f5e9;
border-radius: 5px;
margin-top: 20px;
font-size: 0.9rem;
}
.connection-status {
display: flex;
align-items: center;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
}
.connected {
background: var(--positive-color);
}
.disconnected {
background: var(--negative-color);
}
.last-update {
color: var(--text-secondary);
}
.error-message {
background: #ffebee;
color: var(--negative-color);
padding: 10px 15px;
border-radius: 5px;
margin: 10px 0;
display: none;
}
.loading {
text-align: center;
padding: 20px;
color: var(--text-secondary);
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>期货实时行情监控系统</h1>
<p class="subtitle">基于StockTV API的实时期货数据展示</p>
</header>
<div class="error-message" id="errorMessage"></div>
<div class="grid-container">
<!-- 左侧期货列表 -->
<div class="futures-list card">
<div class="card-title">
<span>期货品种</span>
<span id="listCount">0个品种</span>
</div>
<div id="futuresList">
<div class="loading">加载中...</div>
</div>
</div>
<!-- 右侧主要内容 -->
<div>
<!-- 价格卡片 -->
<div class="price-container" id="priceContainer">
<div class="price-card">
<div>最新价</div>
<div class="price-value" id="lastPrice">--</div>
<div class="price-change" id="priceChange">--</div>
</div>
<div class="price-card">
<div>涨跌幅</div>
<div class="price-value" id="changePercent">--</div>
<div>较前收盘</div>
</div>
<div class="price-card">
<div>最高价</div>
<div class="price-value" id="highPrice">--</div>
<div>最低价 <span id="lowPrice">--</span></div>
</div>
<div class="price-card">
<div>成交量</div>
<div class="price-value" id="volume">--</div>
<div>开盘价 <span id="openPrice">--</span></div>
</div>
</div>
<!-- K线图 -->
<div class="card">
<div class="card-title">
<span id="chartTitle">K线图</span>
<div class="controls">
<button class="interval-btn active" data-interval="1">1分</button>
<button class="interval-btn" data-interval="5">5分</button>
<button class="interval-btn" data-interval="15">15分</button>
<button class="interval-btn" data-interval="60">1小时</button>
<button class="interval-btn" data-interval="1d">日线</button>
</div>
</div>
<div class="chart-container">
<canvas id="klineChart"></canvas>
</div>
</div>
</div>
</div>
<div class="status-bar">
<div class="connection-status">
<div class="status-dot disconnected" id="statusDot"></div>
<span id="connectionStatus">未连接</span>
</div>
<div class="last-update">
最后更新: <span id="lastUpdateTime">--</span>
</div>
</div>
</div>
<script>
// 配置信息
const CONFIG = {
API_BASE_URL: 'https://api.stocktv.top',
WS_URL: 'wss://ws-api.stocktv.top/connect',
API_KEY: 'YOUR_API_KEY', // 请替换为您的实际API密钥
HEARTBEAT_INTERVAL: 30000, // 心跳间隔30秒
RECONNECT_DELAY: 5000, // 重连延迟5秒
KLINE_LIMIT: 100 // K线数据条数
};
// 状态变量
let state = {
currentSymbol: null,
futuresData: [],
klineData: [],
wsConnection: null,
chart: null,
isConnected: false,
reconnectAttempts: 0,
maxReconnectAttempts: 5
};
// 主要期货品种(根据StockTV API支持的品种)
const MAJOR_FUTURES = [
{ symbol: 'CL', name: 'WTI原油期货', exchange: 'NYMEX' },
{ symbol: 'GC', name: 'COMEX黄金', exchange: 'COMEX' },
{ symbol: 'SI', name: 'COMEX白银', exchange: 'COMEX' },
{ symbol: 'HG', name: 'COMEX铜', exchange: 'COMEX' },
{ symbol: 'NG', name: '天然气', exchange: 'NYMEX' },
{ symbol: 'ZS', name: '芝加哥大豆', exchange: 'CBOT' },
{ symbol: 'ZC', name: '芝加哥玉米', exchange: 'CBOT' },
{ symbol: 'FEF', name: '新加坡铁矿石', exchange: 'SGX' },
{ symbol: 'FCPO', name: '马棕油', exchange: 'BMD' }
];
// DOM加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initEventListeners();
loadFuturesList();
});
// 初始化事件监听器
function initEventListeners() {
// K线周期按钮事件
document.querySelectorAll('.interval-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.interval-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
const interval = this.getAttribute('data-interval');
if (state.currentSymbol) {
loadKlineData(state.currentSymbol, interval);
}
});
});
}
// 加载期货列表
async function loadFuturesList() {
showError('');
try {
// 这里使用模拟数据,实际应用中应调用StockTV API
// const response = await fetch(`${CONFIG.API_BASE_URL}/futures/list?key=${CONFIG.API_KEY}`);
// const data = await response.json();
// 模拟API响应
setTimeout(() => {
const mockData = {
code: 200,
data: MAJOR_FUTURES.map(future => ({
symbol: future.symbol,
name: future.name,
exchange: future.exchange,
lastPrice: (Math.random() * 100 + 50).toFixed(2),
change: (Math.random() * 2 - 1).toFixed(2),
changePercent: (Math.random() * 4 - 2).toFixed(2),
volume: Math.floor(Math.random() * 10000)
}))
};
displayFuturesList(mockData.data);
state.futuresData = mockData.data;
// 默认选择第一个品种
if (mockData.data.length > 0) {
selectFuture(mockData.data[0].symbol);
}
}, 500);
} catch (error) {
showError('获取期货列表失败: ' + error.message);
console.error('Error loading futures list:', error);
}
}
// 显示期货列表
function displayFuturesList(futures) {
const listContainer = document.getElementById('futuresList');
document.getElementById('listCount').textContent = `${futures.length}个品种`;
if (futures.length === 0) {
listContainer.innerHTML = '<div class="loading">暂无数据</div>';
return;
}
listContainer.innerHTML = futures.map(future => `
<div class="futures-item" data-symbol="${future.symbol}">
<div class="symbol">
<span>${future.symbol}</span>
<span class="${future.changePercent >= 0 ? 'price-up' : 'price-down'}">
${future.lastPrice}
</span>
</div>
<div class="name">${future.name} · ${future.exchange}</div>
<div class="${future.changePercent >= 0 ? 'price-up' : 'price-down'}">
${future.changePercent >= 0 ? '+' : ''}${future.changePercent}%
</div>
</div>
`).join('');
// 添加点击事件
listContainer.querySelectorAll('.futures-item').forEach(item => {
item.addEventListener('click', function() {
const symbol = this.getAttribute('data-symbol');
selectFuture(symbol);
});
});
}
// 选择期货品种
function selectFuture(symbol) {
// 更新UI状态
document.querySelectorAll('.futures-item').forEach(item => {
item.classList.remove('active');
});
document.querySelector(`.futures-item[data-symbol="${symbol}"]`).classList.add('active');
state.currentSymbol = symbol;
// 更新标题
const future = state.futuresData.find(f => f.symbol === symbol) ||
MAJOR_FUTURES.find(f => f.symbol === symbol);
document.getElementById('chartTitle').textContent = `${future.name} (${future.symbol}) K线图`;
// 加载详细行情和K线数据
loadFutureDetail(symbol);
const activeInterval = document.querySelector('.interval-btn.active').getAttribute('data-interval');
loadKlineData(symbol, activeInterval);
// 通过WebSocket订阅实时数据
subscribeRealtimeData(symbol);
}
// 加载期货详情
async function loadFutureDetail(symbol) {
try {
// 这里使用模拟数据,实际应用中应调用StockTV API
// const response = await fetch(`${CONFIG.API_BASE_URL}/futures/querySymbol?key=${CONFIG.API_KEY}&symbol=${symbol}`);
// const data = await response.json();
// 模拟API响应
setTimeout(() => {
const future = MAJOR_FUTURES.find(f => f.symbol === symbol);
const mockData = {
code: 200,
data: {
symbol: symbol,
name: future.name,
lastPrice: (Math.random() * 100 + 50).toFixed(2),
change: (Math.random() * 2 - 1).toFixed(2),
changePercent: (Math.random() * 4 - 2).toFixed(2),
high: (Math.random() * 5 + 100).toFixed(2),
low: (Math.random() * 5 + 95).toFixed(2),
open: (Math.random() * 5 + 98).toFixed(2),
volume: Math.floor(Math.random() * 10000),
time: new Date().toLocaleString()
}
};
updatePriceDisplay(mockData.data);
}, 300);
} catch (error) {
console.error('Error loading future detail:', error);
}
}
// 更新价格显示
function updatePriceDisplay(data) {
const isPositive = parseFloat(data.changePercent) >= 0;
const changeClass = isPositive ? 'price-up' : 'price-down';
const changeSign = isPositive ? '+' : '';
document.getElementById('lastPrice').textContent = data.lastPrice;
document.getElementById('lastPrice').className = `price-value ${changeClass}`;
document.getElementById('changePercent').textContent = `${changeSign}${data.changePercent}%`;
document.getElementById('changePercent').className = `price-value ${changeClass}`;
document.getElementById('priceChange').innerHTML =
`${changeSign}${data.change} <span class="${changeClass}">${changeSign}${data.changePercent}%</span>`;
document.getElementById('highPrice').textContent = data.high;
document.getElementById('lowPrice').textContent = data.low;
document.getElementById('openPrice').textContent = data.open;
document.getElementById('volume').textContent = data.volume.toLocaleString();
document.getElementById('lastUpdateTime').textContent = new Date().toLocaleTimeString();
}
// 加载K线数据
async function loadKlineData(symbol, interval) {
showError('');
try {
// 这里使用模拟数据,实际应用中应调用StockTV API
// const response = await fetch(
// `${CONFIG.API_BASE_URL}/futures/kline?key=${CONFIG.API_KEY}&symbol=${symbol}&interval=${interval}&limit=${CONFIG.KLINE_LIMIT}`
// );
// const data = await response.json();
// 模拟K线数据生成
const mockKlineData = generateMockKlineData(50);
state.klineData = mockKlineData;
renderKlineChart(mockKlineData);
} catch (error) {
showError('获取K线数据失败: ' + error.message);
console.error('Error loading K-line data:', error);
}
}
// 生成模拟K线数据
function generateMockKlineData(count) {
const data = [];
let basePrice = 100;
let timestamp = Date.now() - count * 60000; // 从当前时间往前推
for (let i = 0; i < count; i++) {
const open = basePrice;
const change = (Math.random() - 0.5) * 4;
const close = open + change;
const high = Math.max(open, close) + Math.random() * 2;
const low = Math.min(open, close) - Math.random() * 2;
const volume = Math.floor(Math.random() * 1000) + 100;
data.push({
timestamp: timestamp + i * 60000,
open: open,
high: high,
low: low,
close: close,
volume: volume
});
basePrice = close;
}
return data;
}
// 渲染K线图
function renderKlineChart(klineData) {
const ctx = document.getElementById('klineChart').getContext('2d');
// 销毁现有图表
if (state.chart) {
state.chart.destroy();
}
// 准备图表数据
const labels = klineData.map((_, i) => {
const date = new Date(klineData[i].timestamp);
return date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
});
const ohlcData = klineData.map(d => ({
x: new Date(d.timestamp),
o: d.open,
h: d.high,
l: d.low,
c: d.close
}));
// 创建图表
state.chart = new Chart(ctx, {
type: 'candlestick',
data: {
labels: labels,
datasets: [{
label: '价格',
data: ohlcData,
borderColor: '#1e88e5',
borderWidth: 1,
color: {
up: '#4caf50',
down: '#f44336',
unchanged: '#999'
}
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: 'time',
time: {
unit: 'minute'
},
ticks: {
maxTicksLimit: 10
}
},
y: {
ticks: {
callback: function(value) {
return value.toFixed(2);
}
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
const point = context.raw;
return [
`开盘: ${point.o}`,
`最高: ${point.h}`,
`最低: ${point.l}`,
`收盘: ${point.c}`
];
}
}
}
}
}
});
}
// WebSocket连接与实时数据订阅
function connectWebSocket() {
try {
state.wsConnection = new WebSocket(`${CONFIG.WS_URL}?key=${CONFIG.API_KEY}`);
state.wsConnection.onopen = function() {
console.log('WebSocket连接已建立');
state.isConnected = true;
state.reconnectAttempts = 0;
updateConnectionStatus(true);
// 启动心跳机制
startHeartbeat();
};
state.wsConnection.onmessage = function(event) {
const data = JSON.parse(event.data);
// 处理心跳响应
if (data.action === 'pong') {
return;
}
// 处理实时行情数据
handleRealtimeData(data);
};
state.wsConnection.onclose = function() {
console.log('WebSocket连接已关闭');
state.isConnected = false;
updateConnectionStatus(false);
// 尝试重连
if (state.reconnectAttempts < state.maxReconnectAttempts) {
setTimeout(() => {
state.reconnectAttempts++;
console.log(`尝试重连 (${state.reconnectAttempts}/${state.maxReconnectAttempts})`);
connectWebSocket();
}, CONFIG.RECONNECT_DELAY);
}
};
state.wsConnection.onerror = function(error) {
console.error('WebSocket错误:', error);
showError('WebSocket连接错误');
};
} catch (error) {
console.error('创建WebSocket连接失败:', error);
showError('创建WebSocket连接失败: ' + error.message);
}
}
// 启动心跳机制
function startHeartbeat() {
setInterval(() => {
if (state.isConnected && state.wsConnection.readyState === WebSocket.OPEN) {
state.wsConnection.send(JSON.stringify({ action: 'ping' }));
}
}, CONFIG.HEARTBEAT_INTERVAL);
}
// 订阅实时数据
function subscribeRealtimeData(symbol) {
if (state.isConnected && state.wsConnection.readyState === WebSocket.OPEN) {
const subscribeMsg = {
action: 'subscribe',
symbol: symbol
};
state.wsConnection.send(JSON.stringify(subscribeMsg));
}
}
// 处理实时数据
function handleRealtimeData(data) {
// 更新价格显示
if (data.last_numeric) {
// 查找当前品种在列表中的位置
const futureIndex = state.futuresData.findIndex(f => f.symbol === state.currentSymbol);
if (futureIndex !== -1) {
// 更新数据
const future = state.futuresData[futureIndex];
const lastPrice = parseFloat(data.last_numeric);
const change = lastPrice - parseFloat(future.lastPrice);
const changePercent = (change / parseFloat(future.lastPrice)) * 100;
future.lastPrice = lastPrice.toFixed(2);
future.change = change.toFixed(2);
future.changePercent = changePercent.toFixed(2);
// 更新UI
updatePriceDisplay({
lastPrice: future.lastPrice,
change: future.change,
changePercent: future.changePercent,
high: data.high || future.high,
low: data.low || future.low,
open: data.open || future.open,
volume: data.volume || future.volume,
time: new Date().toLocaleString()
});
// 更新列表中的显示
const listItem = document.querySelector(`.futures-item[data-symbol="${state.currentSymbol}"]`);
if (listItem) {
const priceElement = listItem.querySelector('.symbol span:last-child');
const changeElement = listItem.querySelector('.price-up, .price-down');
priceElement.textContent = future.lastPrice;
priceElement.className = change >= 0 ? 'price-up' : 'price-down';
changeElement.textContent = `${change >= 0 ? '+' : ''}${future.changePercent}%`;
changeElement.className = change >= 0 ? 'price-up' : 'price-down';
}
}
}
}
// 更新连接状态显示
function updateConnectionStatus(connected) {
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('connectionStatus');
if (connected) {
statusDot.className = 'status-dot connected';
statusText.textContent = '已连接';
} else {
statusDot.className = 'status-dot disconnected';
statusText.textContent = '未连接';
}
}
// 显示错误信息
function showError(message) {
const errorElement = document.getElementById('errorMessage');
if (message) {
errorElement.textContent = message;
errorElement.style.display = 'block';
} else {
errorElement.style.display = 'none';
}
}
// 初始化WebSocket连接
connectWebSocket();
</script>
</body>
</html>
核心功能说明
1. 数据获取与展示
- 期货列表:通过
/futures/list接口获取可用期货品种 - 实时行情:通过
/futures/querySymbol接口获取特定品种行情 - K线数据:通过
/futures/kline接口获取历史K线数据
2. 实时数据更新
- 使用WebSocket建立长连接,接收实时行情推送
- 实现心跳机制保持连接活跃
- 自动重连机制确保连接稳定性
3. 数据可视化
- 使用Chart.js绘制K线图
- 响应式设计适应不同设备
- 多时间周期切换(1分/5分/15分/1小时/日线)
使用说明
- 将代码中的
YOUR_API_KEY替换为您的实际StockTV API密钥 - 根据需要调整期货品种配置(修改
MAJOR_FUTURES数组) - 可根据实际API响应结构调整数据解析逻辑
注意事项
- 本示例使用了模拟数据,实际应用中需对接真实StockTV API
- 生产环境需添加更完善的错误处理和加载状态管理
- 注意API调用频率限制,合理使用缓存策略
- 期货合约存在到期日,需要注意主力合约切换
此实现提供了一个完整的期货数据展示前端解决方案,您可以根据实际需求进一步扩展功能或调整界面设计。
515

被折叠的 条评论
为什么被折叠?



