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

72
QUICKSTART.md Normal file
View File

@@ -0,0 +1,72 @@
# 🚀 快速开始指南
## 1⃣ 安装
```bash
cd stock-tool
./install.sh
```
## 2⃣ 初始化数据
```bash
./init_data.sh
```
## 3⃣ 运行程序
```bash
./stock-tool
# 或
go run main.go
```
## 4⃣ 打开浏览器
访问: http://localhost:8080
## 📱 你会看到什么
### 主界面
- 📊 股票列表卡片
- 每张卡片显示:
- 股票代码和名称
- 当前价格
- 涨跌幅
- 关键指标市值、PE、ROE、负债率
### 点击任意股票
进入详情页面:
1. **K线图标签** - 展示100天历史走势蜡烛图
2. **技术指标标签** - RSI、MACD、均线系统
3. **基本面标签** - 收入、利润、ROE等财务数据
### 搜索功能
- 在搜索框输入: `AAPL``Apple`
- 按回车或点击搜索按钮
- 自动跳转到对应股票详情
### 刷新数据
- 点击详情页面的"🔄 刷新数据"按钮
- 重新从API获取最新数据
---
## 🎯 下一步
1. **添加更多股票** - 通过代码或API手动添加
2. **学习数据结构** - 查看数据库文件 `stock.db`
3. **集成真实API** - 修改 `main.go` 接入真实的股票数据源
4. **实现选股策略** - 添加自动筛选功能
## 💡 提示
- 默认使用模拟数据,方便快速上手
- 数据存储在本地 SQLite 数据库
- 数据库文件: `stock.db`
- 可以随时清空数据库重新开始
---
**需要帮助?** 查看 README.md 或联系 Geliebte

228
README.md Normal file
View File

@@ -0,0 +1,228 @@
# 📈 股票数据分析工具
一个基于 Go + SQLite + 免费API的股票数据分析工具内置网页界面。
## 🚀 快速开始
### 环境要求
- Go 1.21+
- SQLite3
### 安装依赖
```bash
cd stock-tool
go mod download
```
### 运行程序
```bash
go run main.go
```
### 访问网页
打开浏览器访问: http://localhost:8080
## 📊 功能特性
### 1. 股票列表展示
- 展示所有已添加的股票
- 实时价格、涨跌幅
- 基本面指标市值、PE、ROE、负债率
### 2. 股票搜索
- 支持按股票代码搜索
- 支持按公司名称搜索
### 3. 股票详情
- **K线图**展示100天历史走势
- **技术指标**RSI、MACD、均线系统
- **基本面数据**收入、利润、ROE等
### 4. 数据刷新
- 一键刷新股票数据
- 自动更新数据库
## 🗄️ 数据库结构
### stocks 表
```sql
CREATE TABLE stocks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT UNIQUE,
name TEXT,
current_price REAL,
change REAL,
change_percent REAL,
market_cap TEXT,
pe_ratio REAL,
roe REAL,
debt_ratio REAL,
update_time TEXT
);
```
### klines 表
```sql
CREATE TABLE klines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT,
date TEXT,
open REAL,
high REAL,
low REAL,
close REAL,
volume INTEGER,
FOREIGN KEY(symbol) REFERENCES stocks(symbol)
);
```
### indicators 表
```sql
CREATE TABLE indicators (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT,
date TEXT,
rsi REAL,
macd REAL,
macd_signal REAL,
ma5 REAL,
ma10 REAL,
ma20 REAL,
update_time TEXT,
FOREIGN KEY(symbol) REFERENCES stocks(symbol)
);
```
### financials 表
```sql
CREATE TABLE financials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT UNIQUE,
revenue REAL,
profit REAL,
roe REAL,
debt_to_equity REAL,
update_time TEXT
);
```
## 📡 API接口
### 获取股票列表
```
GET /api/stocks
```
### 获取股票详情
```
GET /api/stock/:symbol
```
### 获取K线数据
```
GET /api/stock/:symbol/kline
```
### 获取技术指标
```
GET /api/stock/:symbol/indicators
```
### 获取财务数据
```
GET /api/stock/:symbol/financials
```
### 搜索股票
```
GET /api/search?q=关键词
```
### 刷新股票数据
```
GET /api/refresh/:symbol
```
## 🔌 支持的API
### 1. Yahoo Finance (yfinance)
- **免费额度**: 无限制
- **数据**: 历史K线、实时报价、财务报表
- **使用**: 直接通过yfinance库获取
### 2. Alpha Vantage
- **免费额度**: 25次请求/天
- **数据**: 技术指标、实时数据
- **使用**: 补充技术分析数据
### 3. Finnhub
- **免费额度**: 60次请求/月
- **数据**: 基本面、新闻、经济指标
- **使用**: 财报数据分析
## 🎯 选股策略示例
### 技术分析选股
```python
# 筛选条件示例
1. MACD金叉短线上穿长线
2. RSI < 30超卖
3. MA5 > MA10 > MA20多头排列
4. 换手率 > 5%
```
### 基本面选股
```python
# 筛选条件示例
1. ROE > 15%
2. 负债率 < 60%
3. PE < 30
4. 利润增长率 > 10%
```
## 📝 使用建议
### 初学者
1. 先使用模拟数据体验界面
2. 学习如何读取和展示数据
3. 理解基本的API调用
### 进阶用户
1. 集成真实的yfinance API
2. 添加更多技术指标计算
3. 实现选股策略自动化
### 专业用户
1. 接入真实的历史数据源
2. 添加回测功能
3. 实现实盘交易接口
## ⚠️ 风险提示
> **股市有风险,投资需谨慎**
>
> 本工具仅用于技术学习和数据分析,不构成任何投资建议。
> 所有数据均为模拟数据实际使用时请接入真实API源。
## 🛠️ 开发计划
- [ ] 集成真实的yfinance API
- [ ] 添加更多技术指标布林带、KDJ等
- [ ] 实现选股策略自动化
- [ ] 添加回测功能
- [ ] 支持更多数据源
- [ ] 移动端适配
- [ ] 深色/浅色主题切换
## 📄 许可证
MIT License
## 🤝 贡献
欢迎提交 Issue 和 Pull Request
---
**开发者**: Geliebte
**更新时间**: 2026-02-18

35
go.mod Normal file
View File

@@ -0,0 +1,35 @@
module stock-tool
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/mattn/go-sqlite3 v1.14.19
)
require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

88
go.sum Normal file
View File

@@ -0,0 +1,88 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

74
init_data.sh Executable file
View File

@@ -0,0 +1,74 @@
#!/bin/bash
# 示例股票数据初始化脚本
cd "$(dirname "$0")"
# 创建数据库连接
DB="stock.db"
# 检查数据库是否存在
if [ ! -f "$DB" ]; then
echo "❌ 数据库不存在,请先运行程序"
exit 1
fi
# 插入示例股票数据
echo "📥 初始化示例股票数据..."
sqlite3 "$DB" <<EOF
-- Apple
INSERT INTO stocks (symbol, name, current_price, change, change_percent, market_cap, pe_ratio, roe, debt_ratio, update_time)
VALUES ('AAPL', 'Apple Inc.', 178.52, 2.34, 1.33, '2.8T', 28.5, 145.6, 0.15, datetime('now'));
-- Microsoft
INSERT INTO stocks (symbol, name, current_price, change, change_percent, market_cap, pe_ratio, roe, debt_ratio, update_time)
VALUES ('MSFT', 'Microsoft Corporation', 378.91, -1.23, -0.32, '2.8T', 32.1, 35.2, 0.25, datetime('now'));
-- Google
INSERT INTO stocks (symbol, name, current_price, 141.28, 3.42, 2.48, '1.8T', 25.3, 28.9, 0.18, datetime('now'));
-- Amazon
INSERT INTO stocks (symbol, name, current_price, change, change_percent, market_cap, pe_ratio, roe, debt_ratio, update_time)
VALUES ('AMZN', 'Amazon.com Inc.', 178.23, 1.87, 1.06, '1.9T', 42.5, 10.5, 0.45, datetime('now'));
-- NVIDIA
INSERT INTO stocks (symbol, name, current_price, change, change_percent, market_cap, pe_ratio, roe, debt_ratio, update_time)
VALUES ('NVDA', 'NVIDIA Corporation', 875.42, 15.67, 1.82, '2.2T', 65.2, 76.8, 0.08, datetime('now'));
-- Tesla
INSERT INTO stocks (symbol, name, current_price, change, change_percent, market_cap, pe_ratio, roe, debt_ratio, update_time)
VALUES ('TSLA', 'Tesla Inc.', 248.56, -5.43, -2.14, '785B', 85.3, 12.4, 0.38, datetime('now'));
-- Meta
INSERT INTO stocks (symbol, name, current_price, change, change_percent, market_cap, pe_ratio, roe, debt_ratio, update_time)
VALUES ('META', 'Meta Platforms Inc.', 505.34, 8.91, 1.80, '1.3T', 29.5, 37.2, 0.22, datetime('now'));
-- JPMorgan
INSERT INTO stocks (symbol, name, current_price, change, change_percent, market_cap, pe_ratio, roe, debt_ratio, update_time)
VALUES ('JPM', 'JPMorgan Chase & Co.', 198.45, 2.15, 1.10, '556B', 10.8, 16.5, 0.12, datetime('now'));
-- Visa
INSERT INTO stocks (symbol, name, current_price, change, change_percent, market_cap, pe_ratio, roe, debt_ratio, update_time)
VALUES ('V', 'Visa Inc.', 275.67, -0.87, -0.31, '645B', 32.4, 42.1, 0.18, datetime('now'));
-- Johnson & Johnson
INSERT INTO stocks (symbol, name, current_price, change, change_percent, market_cap, pe_ratio, roe, debt_ratio, update_time)
VALUES ('JNJ', 'Johnson & Johnson', 156.89, 1.34, 0.86, '380B', 13.5, 22.3, 0.05, datetime('now'));
EOF
echo "✅ 示例股票数据初始化完成!"
echo ""
echo "📚 已添加的股票:"
echo " - AAPL (Apple)"
echo " - MSFT (Microsoft)"
echo " - GOOGL (Google)"
echo " - AMZN (Amazon)"
echo " - NVDA (NVIDIA)"
echo " - TSLA (Tesla)"
echo " - META (Meta)"
echo " - JPM (JPMorgan)"
echo " - V (Visa)"
echo " - JNJ (Johnson & Johnson)"
echo ""
echo "💡 提示: 运行程序后访问 http://localhost:8080 查看股票列表"

37
install.sh Executable file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
echo "📦 开始安装股票分析工具..."
# 检查 Go 是否安装
if ! command -v go &> /dev/null; then
echo "❌ 未检测到 Go请先安装 Go 1.21+"
echo "下载地址: https://go.dev/dl/"
exit 1
fi
echo "✅ Go 版本: $(go version)"
# 进入项目目录
cd "$(dirname "$0")"
# 下载依赖
echo "📥 下载 Go 依赖..."
go mod download
# 编译程序
echo "🔨 编译程序..."
go build -o stock-tool main.go
# 创建数据目录
mkdir -p data
echo "✅ 安装完成!"
echo ""
echo "📋 运行程序:"
echo " ./stock-tool"
echo ""
echo "📋 或者直接运行:"
echo " go run main.go"
echo ""
echo "🌐 访问网页:"
echo " http://localhost:8080"

475
main.go Normal file
View File

@@ -0,0 +1,475 @@
package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"time"
"github.com/gin-gonic/gin"
_ "github.com/mattn/go-sqlite3"
)
// 数据库结构
type Stock struct {
Symbol string `json:"symbol"`
Name string `json:"name"`
CurrentPrice float64 `json:"current_price"`
Change float64 `json:"change"`
ChangePercent float64 `json:"change_percent"`
MarketCap string `json:"market_cap"`
PERatio float64 `json:"pe_ratio"`
ROE float64 `json:"roe"`
DebtRatio float64 `json:"debt_ratio"`
UpdateTime time.Time `json:"update_time"`
}
type KLineData struct {
Symbol string `json:"symbol"`
Date string `json:"date"`
Open float64 `json:"open"`
High float64 `json:"high"`
Low float64 `json:"low"`
Close float64 `json:"close"`
Volume int64 `json:"volume"`
}
type TechnicalIndicator struct {
Symbol string `json:"symbol"`
Date string `json:"date"`
RSI float64 `json:"rsi"`
MACD float64 `json:"macd"`
MACDSignal float64 `json:"macd_signal"`
MA5 float64 `json:"ma5"`
MA10 float64 `json:"ma10"`
MA20 float64 `json:"ma20"`
UpdateTime time.Time `json:"update_time"`
}
type Financials struct {
Symbol string `json:"symbol"`
Revenue float64 `json:"revenue"`
Profit float64 `json:"profit"`
ROE float64 `json:"roe"`
DebtToEquity float64 `json:"debt_to_equity"`
UpdateTime time.Time `json:"update_time"`
}
// 全局变量
var db *sql.DB
func main() {
var err error
db, err = sql.Open("sqlite3", "./stock.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 初始化数据库
initDB()
// 创建路由
r := gin.Default()
// 静态文件
r.LoadHTMLGlob("templates/*")
// 页面路由
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
})
// API路由
r.GET("/api/stocks", getStocks)
r.GET("/api/stock/:symbol", getStockDetail)
r.GET("/api/stock/:symbol/kline", getKLine)
r.GET("/api/stock/:symbol/indicators", getIndicators)
r.GET("/api/stock/:symbol/financials", getFinancials)
r.GET("/api/search", searchStocks)
r.GET("/api/refresh/:symbol", refreshStockData)
// 启动服务器
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("🚀 服务器启动: http://localhost:%s", port)
r.Run(":" + port)
}
func initDB() {
query := `
CREATE TABLE IF NOT EXISTS stocks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT UNIQUE,
name TEXT,
current_price REAL,
change REAL,
change_percent REAL,
market_cap TEXT,
pe_ratio REAL,
roe REAL,
debt_ratio REAL,
update_time TEXT
);
CREATE TABLE IF NOT EXISTS klines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT,
date TEXT,
open REAL,
high REAL,
low REAL,
close REAL,
volume INTEGER,
FOREIGN KEY(symbol) REFERENCES stocks(symbol)
);
CREATE TABLE IF NOT EXISTS indicators (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT,
date TEXT,
rsi REAL,
macd REAL,
macd_signal REAL,
ma5 REAL,
ma10 REAL,
ma20 REAL,
update_time TEXT,
FOREIGN KEY(symbol) REFERENCES stocks(symbol)
);
CREATE TABLE IF NOT EXISTS financials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT UNIQUE,
revenue REAL,
profit REAL,
roe REAL,
debt_to_equity REAL,
update_time TEXT
);
`
_, err := db.Exec(query)
if err != nil {
log.Fatal(err)
}
log.Println("✅ 数据库初始化成功")
}
// 获取股票列表
func getStocks(c *gin.Context) {
rows, err := db.Query("SELECT * FROM stocks ORDER BY update_time DESC")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer rows.Close()
var stocks []Stock
for rows.Next() {
var s Stock
err := rows.Scan(&s.ID, &s.Symbol, &s.Name, &s.CurrentPrice,
&s.Change, &s.ChangePercent, &s.MarketCap,
&s.PERatio, &s.ROE, &s.DebtRatio, &s.UpdateTime)
if err != nil {
continue
}
stocks = append(stocks, s)
}
c.JSON(http.StatusOK, stocks)
}
// 获取股票详情
func getStockDetail(c *gin.Context) {
symbol := c.Param("symbol")
var stock Stock
err := db.QueryRow(`
SELECT * FROM stocks WHERE symbol = ?
`, symbol).Scan(&stock.ID, &stock.Symbol, &stock.Name,
&stock.CurrentPrice, &stock.Change, &stock.ChangePercent,
&stock.MarketCap, &stock.PERatio, &stock.ROE, &stock.DebtRatio, &stock.UpdateTime)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "股票不存在"})
return
}
c.JSON(http.StatusOK, stock)
}
// 获取K线数据
func getKLine(c *gin.Context) {
symbol := c.Param("symbol")
rows, err := db.Query(`
SELECT date, open, high, low, close, volume FROM klines
WHERE symbol = ? ORDER BY date DESC LIMIT 100
`, symbol)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer rows.Close()
var klines []KLineData
for rows.Next() {
var k KLineData
err := rows.Scan(&k.Date, &k.Open, &k.High, &k.Low, &k.Close, &k.Volume)
if err != nil {
continue
}
klines = append(klines, k)
}
c.JSON(http.StatusOK, klines)
}
// 获取技术指标
func getIndicators(c *gin.Context) {
symbol := c.Param("symbol")
rows, err := db.Query(`
SELECT date, rsi, macd, macd_signal, ma5, ma10, ma20
FROM indicators WHERE symbol = ? ORDER BY date DESC LIMIT 100
`, symbol)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer rows.Close()
var indicators []TechnicalIndicator
for rows.Next() {
var ind TechnicalIndicator
err := rows.Scan(&ind.Date, &ind.RSI, &ind.MACD, &ind.MACDSignal,
&ind.MA5, &ind.MA10, &ind.MA20)
if err != nil {
continue
}
indicators = append(indicators, ind)
}
c.JSON(http.StatusOK, indicators)
}
// 获取财务数据
func getFinancials(c *gin.Context) {
symbol := c.Param("symbol")
var f Financials
err := db.QueryRow(`
SELECT revenue, profit, roe, debt_to_equity
FROM financials WHERE symbol = ?
`, symbol).Scan(&f.Revenue, &f.Profit, &f.ROE, &f.DebtToEquity)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "财务数据不存在"})
return
}
c.JSON(http.StatusOK, f)
}
// 搜索股票
func searchStocks(c *gin.Context) {
query := c.Query("q")
if query == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请输入搜索关键词"})
return
}
rows, err := db.Query(`
SELECT symbol, name, current_price, change_percent
FROM stocks WHERE symbol LIKE ? OR name LIKE ?
ORDER BY current_price DESC
`, "%"+query+"%", "%"+query+"%")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer rows.Close()
var results []map[string]interface{}
for rows.Next() {
var symbol, name string
var currentPrice, changePercent float64
err := rows.Scan(&symbol, &name, &currentPrice, &changePercent)
if err != nil {
continue
}
results = append(results, map[string]interface{}{
"symbol": symbol,
"name": name,
"current_price": currentPrice,
"change_percent": changePercent,
})
}
c.JSON(http.StatusOK, results)
}
// 刷新股票数据从yfinance获取
func refreshStockData(c *gin.Context) {
symbol := c.Param("symbol")
// 获取yfinance数据
stock := fetchYahooFinance(symbol)
// 存储到数据库
storeStockData(stock)
storeKLineData(stock)
storeIndicatorData(stock)
c.JSON(http.StatusOK, gin.H{
"message": "数据刷新成功",
"symbol": symbol,
"price": stock.CurrentPrice,
"update_time": stock.UpdateTime.Format("2006-01-02 15:04:05"),
})
}
// yfinance API调用简化版
func fetchYahooFinance(symbol string) Stock {
// 这里使用yfinance的REST API接口
// 实际需要安装github.com/joshua-proxy/yfinance-go或类似库
// 这里用模拟数据演示,实际使用需要安装库
// 模拟数据 - 实际应调用yfinance API
return Stock{
Symbol: symbol,
Name: getCompanyName(symbol),
CurrentPrice: getRandomPrice(),
Change: getRandomChange(),
ChangePercent: getRandomChangePercent(),
MarketCap: getRandomMarketCap(),
PERatio: getRandomPERatio(),
ROE: getRandomROE(),
DebtRatio: getRandomDebtRatio(),
UpdateTime: time.Now(),
}
}
func getCompanyName(symbol string) string {
companies := map[string]string{
"AAPL": "Apple Inc.",
"MSFT": "Microsoft Corporation",
"GOOGL": "Alphabet Inc.",
"AMZN": "Amazon.com Inc.",
"NVDA": "NVIDIA Corporation",
"TSLA": "Tesla Inc.",
"META": "Meta Platforms Inc.",
"JPM": "JPMorgan Chase & Co.",
"V": "Visa Inc.",
"JNJ": "Johnson & Johnson",
}
if name, ok := companies[symbol]; ok {
return name
}
return "Unknown Company"
}
func getRandomPrice() float64 {
return 100 + (time.Now().Unix() % 500)
}
func getRandomChange() float64 {
return (rand.Float64() * 10) - 5
}
func getRandomChangePercent() float64 {
return (rand.Float64() * 10) - 5
}
func getRandomMarketCap() string {
segments := []string{"B", "T"}
return fmt.Sprintf("$%d%s", int(rand.Float64()*1000), segments[rand.Intn(2)])
}
func getRandomPERatio() float64 {
return 10 + rand.Float64()*40
}
func getRandomROE() float64 {
return 5 + rand.Float64()*30
}
func getRandomDebtRatio() float64 {
return 20 + rand.Float64()*80
}
func storeStockData(stock Stock) error {
_, err := db.Exec(`
INSERT INTO stocks (symbol, name, current_price, change, change_percent, market_cap, pe_ratio, roe, debt_ratio, update_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(symbol) DO UPDATE SET
current_price = excluded.current_price,
change = excluded.change,
change_percent = excluded.change_percent,
market_cap = excluded.market_cap,
pe_ratio = excluded.pe_ratio,
roe = excluded.roe,
debt_ratio = excluded.debt_ratio,
update_time = excluded.update_time
`, stock.Symbol, stock.Name, stock.CurrentPrice, stock.Change, stock.ChangePercent,
stock.MarketCap, stock.PERatio, stock.ROE, stock.DebtRatio, stock.UpdateTime.Format("2006-01-02 15:04:05"))
return err
}
func storeKLineData(stock Stock) error {
// 生成模拟K线数据
for i := 0; i < 100; i++ {
date := time.Now().AddDate(0, 0, -i).Format("2006-01-02")
open := stock.CurrentPrice + (rand.Float64()-0.5)*10
high := open + rand.Float64()*5
low := open - rand.Float64()*5
close := open + (rand.Float64()-0.5)*5
volume := int64(rand.Float64() * 1000000)
_, err := db.Exec(`
INSERT INTO klines (symbol, date, open, high, low, close, volume)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, stock.Symbol, date, open, high, low, close, volume)
if err != nil {
return err
}
}
return nil
}
func storeIndicatorData(stock Stock) error {
// 生成模拟技术指标
for i := 0; i < 100; i++ {
date := time.Now().AddDate(0, 0, -i).Format("2006-01-02")
rsi := 30 + rand.Float64()*40
macd := (rand.Float64() * 10) - 5
macdSignal := macd + (rand.Float64()-0.5)*2
ma5 := stock.CurrentPrice + (rand.Float64()-0.5)*5
ma10 := stock.CurrentPrice + (rand.Float64()-0.5)*8
ma20 := stock.CurrentPrice + (rand.Float64()-0.5)*12
_, err := db.Exec(`
INSERT INTO indicators (symbol, date, rsi, macd, macd_signal, ma5, ma10, ma20, update_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, stock.Symbol, date, rsi, macd, macdSignal, ma5, ma10, ma20,
time.Now().Format("2006-01-02 15:04:05"))
if err != nil {
return err
}
}
return nil
}
// 随机数生成
func randFloat64(min, max float64) float64 {
return min + rand.Float64()*(max-min)
}
// Mock rand package
var rand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))

498
main_simple.go Normal file
View File

@@ -0,0 +1,498 @@
package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"math/rand"
"net/http"
"os"
"time"
)
var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
// 数据库结构
type Stock struct {
ID int `json:"id"`
Symbol string `json:"symbol"`
Name string `json:"name"`
CurrentPrice float64 `json:"current_price"`
Change float64 `json:"change"`
ChangePercent float64 `json:"change_percent"`
MarketCap string `json:"market_cap"`
PERatio float64 `json:"pe_ratio"`
ROE float64 `json:"roe"`
DebtRatio float64 `json:"debt_ratio"`
UpdateTime string `json:"update_time"`
}
type KLineData struct {
Symbol string `json:"symbol"`
Date string `json:"date"`
Open float64 `json:"open"`
High float64 `json:"high"`
Low float64 `json:"low"`
Close float64 `json:"close"`
Volume int64 `json:"volume"`
}
type TechnicalIndicator struct {
ID int `json:"id"`
Symbol string `json:"symbol"`
Date string `json:"date"`
RSI float64 `json:"rsi"`
MACD float64 `json:"macd"`
MACDSignal float64 `json:"macd_signal"`
MA5 float64 `json:"ma5"`
MA10 float64 `json:"ma10"`
MA20 float64 `json:"ma20"`
UpdateTime string `json:"update_time"`
}
type Financials struct {
ID int `json:"id"`
Symbol string `json:"symbol"`
Revenue float64 `json:"revenue"`
Profit float64 `json:"profit"`
ROE float64 `json:"roe"`
DebtToEquity float64 `json:"debt_to_equity"`
UpdateTime string `json:"update_time"`
}
// 全局变量
var db *sql.DB
func main() {
var err error
db, err = sql.Open("sqlite3", "./stock.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 初始化数据库
initDB()
// 注册路由
http.HandleFunc("/", handleIndex)
http.HandleFunc("/api/stocks", handleGetStocks)
http.HandleFunc("/api/stock/", handleStockDetail)
http.HandleFunc("/api/stock/", handleKLine)
http.HandleFunc("/api/stock/", handleIndicators)
http.HandleFunc("/api/stock/", handleFinancials)
http.HandleFunc("/api/search", handleSearch)
http.HandleFunc("/api/refresh/", handleRefresh)
// 启动服务器
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("🚀 服务器启动: http://localhost:%s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
func initDB() {
query := `
CREATE TABLE IF NOT EXISTS stocks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT UNIQUE,
name TEXT,
current_price REAL,
change REAL,
change_percent REAL,
market_cap TEXT,
pe_ratio REAL,
roe REAL,
debt_ratio REAL,
update_time TEXT
);
CREATE TABLE IF NOT EXISTS klines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT,
date TEXT,
open REAL,
high REAL,
low REAL,
close REAL,
volume INTEGER,
FOREIGN KEY(symbol) REFERENCES stocks(symbol)
);
CREATE TABLE IF NOT EXISTS indicators (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT,
date TEXT,
rsi REAL,
macd REAL,
macd_signal REAL,
ma5 REAL,
ma10 REAL,
ma20 REAL,
update_time TEXT,
FOREIGN KEY(symbol) REFERENCES stocks(symbol)
);
CREATE TABLE IF NOT EXISTS financials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT UNIQUE,
revenue REAL,
profit REAL,
roe REAL,
debt_to_equity REAL,
update_time TEXT
);
`
_, err := db.Exec(query)
if err != nil {
log.Fatal(err)
}
log.Println("✅ 数据库初始化成功")
}
// 主页面
func handleIndex(w http.ResponseWriter, r *http.Request) {
// 读取HTML模板
content, err := os.ReadFile("templates/index.html")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(content)
}
// 获取股票列表
func handleGetStocks(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query("SELECT * FROM stocks ORDER BY update_time DESC")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var stocks []Stock
for rows.Next() {
var s Stock
err := rows.Scan(&s.ID, &s.Symbol, &s.Name, &s.CurrentPrice,
&s.Change, &s.ChangePercent, &s.MarketCap,
&s.PERatio, &s.ROE, &s.DebtRatio, &s.UpdateTime)
if err != nil {
continue
}
stocks = append(stocks, s)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(stocks)
}
// 获取股票详情
func handleStockDetail(w http.ResponseWriter, r *http.Request) {
symbol := r.URL.Path[len("/api/stock/"):]
if symbol == "" {
http.Error(w, "股票代码不能为空", http.StatusBadRequest)
return
}
var stock Stock
err := db.QueryRow(`
SELECT * FROM stocks WHERE symbol = ?
`, symbol).Scan(&stock.ID, &stock.Symbol, &stock.Name,
&stock.CurrentPrice, &stock.Change, &stock.ChangePercent,
&stock.MarketCap, &stock.PERatio, &stock.ROE, &stock.DebtRatio, &stock.UpdateTime)
if err != nil {
http.Error(w, "股票不存在", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(stock)
}
// 获取K线数据
func handleKLine(w http.ResponseWriter, r *http.Request) {
symbol := r.URL.Path[len("/api/stock/"):]
if symbol == "" {
http.Error(w, "股票代码不能为空", http.StatusBadRequest)
return
}
rows, err := db.Query(`
SELECT date, open, high, low, close, volume FROM klines
WHERE symbol = ? ORDER BY date DESC LIMIT 100
`, symbol)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var klines []KLineData
for rows.Next() {
var k KLineData
err := rows.Scan(&k.Date, &k.Open, &k.High, &k.Low, &k.Close, &k.Volume)
if err != nil {
continue
}
klines = append(klines, k)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(klines)
}
// 获取技术指标
func handleIndicators(w http.ResponseWriter, r *http.Request) {
symbol := r.URL.Path[len("/api/stock/"):]
if symbol == "" {
http.Error(w, "股票代码不能为空", http.StatusBadRequest)
return
}
rows, err := db.Query(`
SELECT date, rsi, macd, macd_signal, ma5, ma10, ma20
FROM indicators WHERE symbol = ? ORDER BY date DESC LIMIT 100
`, symbol)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var indicators []TechnicalIndicator
for rows.Next() {
var ind TechnicalIndicator
err := rows.Scan(&ind.Date, &ind.RSI, &ind.MACD, &ind.MACDSignal,
&ind.MA5, &ind.MA10, &ind.MA20)
if err != nil {
continue
}
indicators = append(indicators, ind)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(indicators)
}
// 获取财务数据
func handleFinancials(w http.ResponseWriter, r *http.Request) {
symbol := r.URL.Path[len("/api/stock/"):]
if symbol == "" {
http.Error(w, "股票代码不能为空", http.StatusBadRequest)
return
}
var f Financials
err := db.QueryRow(`
SELECT revenue, profit, roe, debt_to_equity
FROM financials WHERE symbol = ?
`, symbol).Scan(&f.Revenue, &f.Profit, &f.ROE, &f.DebtToEquity)
if err != nil {
http.Error(w, "财务数据不存在", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(f)
}
// 搜索股票
func handleSearch(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if query == "" {
http.Error(w, "请输入搜索关键词", http.StatusBadRequest)
return
}
rows, err := db.Query(`
SELECT symbol, name, current_price, change_percent
FROM stocks WHERE symbol LIKE ? OR name LIKE ?
ORDER BY current_price DESC
`, "%"+query+"%", "%"+query+"%")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var results []map[string]interface{}
for rows.Next() {
var symbol, name string
var currentPrice, changePercent float64
err := rows.Scan(&symbol, &name, &currentPrice, &changePercent)
if err != nil {
continue
}
results = append(results, map[string]interface{}{
"symbol": symbol,
"name": name,
"current_price": currentPrice,
"change_percent": changePercent,
})
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(results)
}
// 刷新股票数据
func handleRefresh(w http.ResponseWriter, r *http.Request) {
symbol := r.URL.Path[len("/api/refresh/"):]
if symbol == "" {
http.Error(w, "股票代码不能为空", http.StatusBadRequest)
return
}
// 获取yfinance数据
stock := fetchYahooFinance(symbol)
// 存储到数据库
storeStockData(stock)
storeKLineData(stock)
storeIndicatorData(stock)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"message": "数据刷新成功",
"symbol": symbol,
"price": stock.CurrentPrice,
"update_time": time.Now().Format("2006-01-02 15:04:05"),
})
}
// yfinance API调用简化版
func fetchYahooFinance(symbol string) Stock {
// 模拟数据
return Stock{
Symbol: symbol,
Name: getCompanyName(symbol),
CurrentPrice: getRandomPrice(),
Change: getRandomChange(),
ChangePercent: getRandomChangePercent(),
MarketCap: getRandomMarketCap(),
PERatio: getRandomPERatio(),
ROE: getRandomROE(),
DebtRatio: getRandomDebtRatio(),
UpdateTime: time.Now().Format("2006-01-02 15:04:05"),
}
}
func getCompanyName(symbol string) string {
companies := map[string]string{
"AAPL": "Apple Inc.",
"MSFT": "Microsoft Corporation",
"GOOGL": "Alphabet Inc.",
"AMZN": "Amazon.com Inc.",
"NVDA": "NVIDIA Corporation",
"TSLA": "Tesla Inc.",
"META": "Meta Platforms Inc.",
"JPM": "JPMorgan Chase & Co.",
"V": "Visa Inc.",
"JNJ": "Johnson & Johnson",
}
if name, ok := companies[symbol]; ok {
return name
}
return "Unknown Company"
}
func getRandomPrice() float64 {
return 100 + float64(time.Now().Unix() % 500)
}
func getRandomChange() float64 {
return (randFloat64(0,1) * 10) - 5
}
func getRandomChangePercent() float64 {
return (randFloat64(0,1) * 10) - 5
}
func getRandomMarketCap() string {
segments := []string{"B", "T"}
return fmt.Sprintf("$%d%s", int(randFloat64(0,1)*1000), segments[rand.Intn(2)])
}
func getRandomPERatio() float64 {
return 10 + randFloat64(0,1)*40
}
func getRandomROE() float64 {
return 5 + randFloat64(0,1)*30
}
func getRandomDebtRatio() float64 {
return 20 + randFloat64(0,1)*80
}
func storeStockData(stock Stock) error {
_, err := db.Exec(`
INSERT INTO stocks (symbol, name, current_price, change, change_percent, market_cap, pe_ratio, roe, debt_ratio, update_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(symbol) DO UPDATE SET
current_price = excluded.current_price,
change = excluded.change,
change_percent = excluded.change_percent,
market_cap = excluded.market_cap,
pe_ratio = excluded.pe_ratio,
roe = excluded.roe,
debt_ratio = excluded.debt_ratio,
update_time = excluded.update_time
`, stock.Symbol, stock.Name, stock.CurrentPrice, stock.Change, stock.ChangePercent,
stock.MarketCap, stock.PERatio, stock.ROE, stock.DebtRatio, stock.UpdateTime)
return err
}
func storeKLineData(stock Stock) error {
for i := 0; i < 100; i++ {
date := time.Now().AddDate(0, 0, -i).Format("2006-01-02")
open := stock.CurrentPrice + (randFloat64(0,1)-0.5)*10
high := open + randFloat64(0,1)*5
low := open - randFloat64(0,1)*5
close := open + (randFloat64(0,1)-0.5)*5
volume := int64(randFloat64(0,1) * 1000000)
_, err := db.Exec(`
INSERT INTO klines (symbol, date, open, high, low, close, volume)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, stock.Symbol, date, open, high, low, close, volume)
if err != nil {
return err
}
}
return nil
}
func storeIndicatorData(stock Stock) error {
for i := 0; i < 100; i++ {
date := time.Now().AddDate(0, 0, -i).Format("2006-01-02")
rsi := 30 + randFloat64(0,1)*40
macd := (randFloat64(0,1) * 10) - 5
macdSignal := macd + (randFloat64(0,1)-0.5)*2
ma5 := stock.CurrentPrice + (randFloat64(0,1)-0.5)*5
ma10 := stock.CurrentPrice + (randFloat64(0,1)-0.5)*8
ma20 := stock.CurrentPrice + (randFloat64(0,1)-0.5)*12
_, err := db.Exec(`
INSERT INTO indicators (symbol, date, rsi, macd, macd_signal, ma5, ma10, ma20, update_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, stock.Symbol, date, rsi, macd, macdSignal, ma5, ma10, ma20,
time.Now().Format("2006-01-02 15:04:05"))
if err != nil {
return err
}
}
return nil
}
func randFloat64(min, max float64) float64 {
return min + seededRand.Float64()*(max-min)
}

BIN
stock-tool-simple Executable file

Binary file not shown.

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>