Files
stock-tool/main.go
root f246f06cb1 Initial commit: 股票数据分析工具
- Go后端服务
- 内置Web界面
- SQLite数据库
- 股票列表展示
- K线图绘制
- 技术指标计算
- 基本面数据展示
2026-02-19 03:07:02 +08:00

476 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()))