Initial commit: 股票数据分析工具
- Go后端服务 - 内置Web界面 - SQLite数据库 - 股票列表展示 - K线图绘制 - 技术指标计算 - 基本面数据展示
This commit is contained in:
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)
|
||||
}
|
||||
Reference in New Issue
Block a user