Initial commit: 股票数据分析工具
- Go后端服务 - 内置Web界面 - SQLite数据库 - 股票列表展示 - K线图绘制 - 技术指标计算 - 基本面数据展示
This commit is contained in:
72
QUICKSTART.md
Normal file
72
QUICKSTART.md
Normal 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
228
README.md
Normal 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
35
go.mod
Normal 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
88
go.sum
Normal 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
74
init_data.sh
Executable 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
37
install.sh
Executable 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
475
main.go
Normal 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, ¤tPrice, &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
498
main_simple.go
Normal 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, ¤tPrice, &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
BIN
stock-tool-simple
Executable file
Binary file not shown.
633
templates/index.html
Normal file
633
templates/index.html
Normal 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>
|
||||
Reference in New Issue
Block a user