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

499 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"
"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)
}