Initial commit: 股票数据分析工具

- Go后端服务
- 内置Web界面
- SQLite数据库
- 股票列表展示
- K线图绘制
- 技术指标计算
- 基本面数据展示
This commit is contained in:
root
2026-02-19 03:07:02 +08:00
commit f246f06cb1
10 changed files with 2140 additions and 0 deletions

633
templates/index.html Normal file
View File

@@ -0,0 +1,633 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>📈 股票数据分析工具</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #fff;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
background: linear-gradient(90deg, #00d2ff, #3a7bd5);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* 搜索框 */
.search-box {
display: flex;
gap: 10px;
margin-bottom: 30px;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.search-box input {
flex: 1;
padding: 15px;
border-radius: 8px;
border: 2px solid #3a7bd5;
background: rgba(255,255,255,0.1);
color: #fff;
font-size: 16px;
outline: none;
}
.search-box input:focus {
border-color: #00d2ff;
}
.search-box button {
padding: 15px 30px;
border: none;
border-radius: 8px;
background: linear-gradient(90deg, #00d2ff, #3a7bd5);
color: #fff;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: transform 0.2s;
}
.search-box button:hover {
transform: scale(1.05);
}
/* 股票列表 */
.stock-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stock-card {
background: rgba(255,255,255,0.05);
border-radius: 12px;
padding: 20px;
cursor: pointer;
transition: all 0.3s;
border: 1px solid rgba(255,255,255,0.1);
}
.stock-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
border-color: #00d2ff;
}
.stock-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.stock-symbol {
font-size: 1.5em;
font-weight: bold;
color: #00d2ff;
}
.stock-name {
font-size: 0.9em;
color: #aaa;
margin-bottom: 10px;
}
.stock-price {
font-size: 1.8em;
font-weight: bold;
}
.stock-change {
font-size: 1.2em;
}
.stock-change.positive {
color: #00ff88;
}
.stock-change.negative {
color: #ff4757;
}
.stock-metrics {
margin-top: 15px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
font-size: 0.85em;
}
.metric {
background: rgba(255,255,255,0.05);
padding: 8px;
border-radius: 6px;
}
.metric-label {
color: #888;
margin-bottom: 3px;
}
.metric-value {
font-weight: bold;
}
/* 详情页面 */
.detail-view {
display: none;
}
.detail-header {
background: rgba(255,255,255,0.05);
border-radius: 12px;
padding: 30px;
margin-bottom: 20px;
}
.detail-price {
font-size: 3em;
font-weight: bold;
}
.detail-change {
font-size: 1.5em;
}
.detail-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.tab {
padding: 12px 25px;
border: none;
border-radius: 8px;
background: rgba(255,255,255,0.1);
color: #fff;
cursor: pointer;
font-size: 16px;
transition: all 0.3s;
}
.tab.active {
background: linear-gradient(90deg, #00d2ff, #3a7bd5);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.chart-container {
background: rgba(255,255,255,0.05);
border-radius: 12px;
padding: 20px;
height: 400px;
position: relative;
}
/* 简单的CSS图表 */
.simple-chart {
width: 100%;
height: 350px;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th {
background: rgba(255,255,255,0.1);
padding: 12px;
text-align: left;
position: sticky;
top: 0;
}
.data-table td {
padding: 10px;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.refresh-btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
background: #00ff88;
color: #1a1a2e;
font-weight: bold;
cursor: pointer;
margin-top: 20px;
}
.loading {
text-align: center;
padding: 50px;
font-size: 1.2em;
color: #00d2ff;
}
.loading::after {
content: '...';
animation: dots 1.5s steps(4, end) infinite;
}
@keyframes dots {
0%, 20% { content: '.'; }
40% { content: '..'; }
60%, 100% { content: '...'; }
}
@media (max-width: 768px) {
.stock-list {
grid-template-columns: 1fr;
}
h1 {
font-size: 1.8em;
}
}
</style>
</head>
<body>
<div class="container">
<h1>📈 股票数据分析工具</h1>
<!-- 搜索框 -->
<div class="search-box">
<input type="text" id="searchInput" placeholder="输入股票代码或公司名称...">
<button onclick="searchStocks()">🔍 搜索</button>
</div>
<!-- 股票列表 -->
<div id="stockList" class="stock-list"></div>
<!-- 详情页面 -->
<div id="detailView" class="detail-view">
<div class="detail-header">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<div>
<h2 id="detailSymbol" style="color: #00d2ff; margin-bottom: 5px;"></h2>
<p id="detailName" style="color: #888;"></p>
</div>
<button class="refresh-btn" onclick="refreshCurrentStock()">🔄 刷新数据</button>
</div>
<div style="display: flex; align-items: baseline; gap: 20px;">
<span id="detailPrice" class="detail-price"></span>
<span id="detailChange" class="detail-change"></span>
</div>
</div>
<div class="detail-tabs">
<button class="tab active" onclick="switchTab('kline')">📊 K线图</button>
<button class="tab" onclick="switchTab('indicators')">📈 技术指标</button>
<button class="tab" onclick="switchTab('financials')">💰 基本面</button>
</div>
<div id="klineTab" class="tab-content active">
<div class="chart-container">
<canvas id="klineChart"></canvas>
</div>
</div>
<div id="indicatorsTab" class="tab-content">
<div class="chart-container">
<canvas id="indicatorsChart"></canvas>
</div>
</div>
<div id="financialsTab" class="tab-content">
<div class="chart-container">
<table class="data-table">
<thead>
<tr>
<th>指标</th>
<th>数值</th>
<th>说明</th>
</tr>
</thead>
<tbody id="financialsTable"></tbody>
</table>
</div>
</div>
</div>
<div id="loading" class="loading"></div>
</div>
<script>
let currentSymbol = '';
// 初始化
document.addEventListener('DOMContentLoaded', () => {
loadStocks();
});
// 加载股票列表
async function loadStocks() {
try {
const response = await fetch('/api/stocks');
const stocks = await response.json();
displayStocks(stocks);
} catch (error) {
console.error('加载失败:', error);
}
}
// 显示股票列表
function displayStocks(stocks) {
const container = document.getElementById('stockList');
container.innerHTML = '';
stocks.forEach(stock => {
const card = document.createElement('div');
card.className = 'stock-card';
card.onclick = () => showDetail(stock.symbol);
const changeClass = stock.change_percent >= 0 ? 'positive' : 'negative';
const changeSymbol = stock.change_percent >= 0 ? '↑' : '↓';
card.innerHTML = `
<div class="stock-header">
<span class="stock-symbol">${stock.symbol}</span>
<span class="stock-change ${changeClass}">${changeSymbol} ${Math.abs(stock.change_percent).toFixed(2)}%</span>
</div>
<p class="stock-name">${stock.name}</p>
<p class="stock-price">$${stock.current_price.toFixed(2)}</p>
<div class="stock-metrics">
<div class="metric">
<div class="metric-label">市值</div>
<div class="metric-value">${stock.market_cap || 'N/A'}</div>
</div>
<div class="metric">
<div class="metric-label">市盈率</div>
<div class="metric-value">${stock.pe_ratio ? stock.pe_ratio.toFixed(2) : 'N/A'}</div>
</div>
<div class="metric">
<div class="metric-label">ROE</div>
<div class="metric-value">${stock.roe ? stock.roe.toFixed(2) : 'N/A'}%</div>
</div>
<div class="metric">
<div class="metric-label">负债率</div>
<div class="metric-value">${stock.debt_ratio ? stock.debt_ratio.toFixed(2) : 'N/A'}%</div>
</div>
</div>
`;
container.appendChild(card);
});
}
// 搜索股票
async function searchStocks() {
const query = document.getElementById('searchInput').value.trim();
if (!query) {
alert('请输入搜索关键词');
return;
}
showLoading();
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const results = await response.json();
if (results.length === 0) {
alert('未找到相关股票');
} else {
showDetail(results[0].symbol);
}
} catch (error) {
console.error('搜索失败:', error);
}
}
// 显示详情
async function showDetail(symbol) {
currentSymbol = symbol;
document.getElementById('stockList').style.display = 'none';
document.getElementById('detailView').style.display = 'block';
document.getElementById('loading').style.display = 'block';
try {
// 加载股票信息
const stockResponse = await fetch(`/api/stock/${symbol}`);
const stock = await stockResponse.json();
document.getElementById('detailSymbol').textContent = stock.symbol;
document.getElementById('detailName').textContent = stock.name;
document.getElementById('detailPrice').textContent = `$${stock.current_price.toFixed(2)}`;
const changeClass = stock.change_percent >= 0 ? 'positive' : 'negative';
const changeSymbol = stock.change_percent >= 0 ? '↑' : '↓';
document.getElementById('detailChange').innerHTML =
`<span class="${changeClass}">${changeSymbol} ${Math.abs(stock.change_percent).toFixed(2)}%</span>`;
// 加载K线数据
loadKLineData(symbol);
loadIndicatorData(symbol);
loadFinancials(symbol);
} catch (error) {
console.error('加载失败:', error);
}
document.getElementById('loading').style.display = 'none';
}
// 加载K线数据
async function loadKLineData(symbol) {
try {
const response = await fetch(`/api/stock/${symbol}/kline`);
const klines = await response.json();
drawKlineChart(klines);
} catch (error) {
console.error('加载K线失败:', error);
}
}
// 加载技术指标
async function loadIndicatorData(symbol) {
try {
const response = await fetch(`/api/stock/${symbol}/indicators`);
const indicators = await response.json();
drawIndicatorsChart(indicators);
} catch (error) {
console.error('加载指标失败:', error);
}
}
// 加载财务数据
async function loadFinancials(symbol) {
try {
const response = await fetch(`/api/stock/${symbol}/financials`);
const financials = await response.json();
const tbody = document.getElementById('financialsTable');
tbody.innerHTML = `
<tr><td>营业收入</td><td>${financials.revenue ? '$' + financials.revenue.toFixed(2) : 'N/A'}</td><td>年度总收入</td></tr>
<tr><td>净利润</td><td>${financials.profit ? '$' + financials.profit.toFixed(2) : 'N/A'}</td><td>年度净利润</td></tr>
<tr><td>ROE</td><td>${financials.roe ? financials.roe.toFixed(2) : 'N/A'}%</td><td>净资产收益率</td></tr>
<tr><td>负债权益比</td><td>${financials.debt_to_equity ? financials.debt_to_equity.toFixed(2) : 'N/A'}</td><td>杠杆率</td></tr>
`;
} catch (error) {
console.error('加载财务数据失败:', error);
}
}
// 绘制K线图
function drawKlineChart(klines) {
const canvas = document.getElementById('klineChart');
const ctx = canvas.getContext('2d');
canvas.width = canvas.parentElement.clientWidth - 40;
canvas.height = 350;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (klines.length < 2) return;
const data = klines.slice().reverse();
const prices = data.map(d => d.close);
const minPrice = Math.min(...prices) * 0.99;
const maxPrice = Math.max(...prices) * 1.01;
const priceRange = maxPrice - minPrice;
const candleWidth = (canvas.width - 60) / data.length;
// 绘制网格
ctx.strokeStyle = 'rgba(255,255,255,0.1)';
ctx.lineWidth = 1;
for (let i = 0; i < 5; i++) {
const y = 30 + (i * (canvas.height - 60) / 4);
ctx.beginPath();
ctx.moveTo(30, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
// 绘制蜡烛图
data.forEach((candle, i) => {
const x = 30 + i * candleWidth + candleWidth / 2;
const yOpen = 30 + (maxPrice - candle.open) / priceRange * (canvas.height - 60);
const yClose = 30 + (maxPrice - candle.close) / priceRange * (canvas.height - 60);
const yHigh = 30 + (maxPrice - candle.high) / priceRange * (canvas.height - 60);
const yLow = 30 + (maxPrice - candle.low) / priceRange * (canvas.height - 60);
const isUp = candle.close >= candle.open;
ctx.fillStyle = isUp ? '#00ff88' : '#ff4757';
ctx.strokeStyle = ctx.fillStyle;
// 绘制影线
ctx.beginPath();
ctx.moveTo(x, yHigh);
ctx.lineTo(x, yLow);
ctx.stroke();
// 绘制实体
const bodyHeight = Math.max(Math.abs(yClose - yOpen), 1);
ctx.fillRect(x - candleWidth / 2 + 2, Math.min(yOpen, yClose), candleWidth - 4, bodyHeight);
});
}
// 绘制技术指标图
function drawIndicatorsChart(indicators) {
const canvas = document.getElementById('indicatorsChart');
const ctx = canvas.getContext('2d');
canvas.width = canvas.parentElement.clientWidth - 40;
canvas.height = 350;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (indicators.length < 2) return;
const data = indicators.slice().reverse();
const values = data.map(d => d.rsi);
const min = Math.min(...values) * 0.9;
const max = Math.max(...values) * 1.1;
const valueRange = max - min;
const stepX = (canvas.width - 40) / data.length;
// 绘制RSI中线70和30
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(30, 30 + (70 - min) / valueRange * (canvas.height - 60));
ctx.lineTo(canvas.width, 30 + (70 - min) / valueRange * (canvas.height - 60));
ctx.stroke();
ctx.beginPath();
ctx.moveTo(30, 30 + (30 - min) / valueRange * (canvas.height - 60));
ctx.lineTo(canvas.width, 30 + (30 - min) / valueRange * (canvas.height - 60));
ctx.stroke();
ctx.setLineDash([]);
// 绘制RSI曲线
ctx.strokeStyle = '#00d2ff';
ctx.lineWidth = 2;
ctx.beginPath();
data.forEach((ind, i) => {
const x = 30 + i * stepX;
const y = 30 + (ind.rsi - min) / valueRange * (canvas.height - 60);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
}
// 切换标签页
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
event.target.classList.add('active');
document.getElementById(tab + 'Tab').classList.add('active');
}
// 刷新数据
async function refreshCurrentStock() {
showLoading();
try {
const response = await fetch(`/api/refresh/${currentSymbol}`);
const result = await response.json();
alert(result.message);
// 重新加载当前页面
await loadStocks();
showDetail(currentSymbol);
} catch (error) {
console.error('刷新失败:', error);
}
document.getElementById('loading').style.display = 'none';
}
function showLoading() {
document.getElementById('loading').style.display = 'block';
document.getElementById('stockList').style.display = 'none';
document.getElementById('detailView').style.display = 'none';
}
</script>
</body>
</html>