mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-07 13:00:59 +08:00
feat: real-time A-share stock data via Sina Finance API
NOFXi can now fetch REAL stock prices for A-shares. Data source: Sina Finance hq.sinajs.cn (free, no API key) - Name, price, open, prev close, high, low, volume, turnover - Change % calculated from prev close - GBK → UTF-8 encoding handled Stock resolution: - Known stocks: 拓维信息→sz002261, 贵州茅台→sh600519, etc. - Auto-detect 6-digit codes in user text - Prefix detection: 6xx→sh, 0xx/3xx→sz Context injection: - Stock data auto-appended to LLM context when user mentions a stock - AI sees REAL price, volume, change % — cannot fabricate Tested: '拓维信息这只股,10万元,1个月交易计划' → Returns ¥35.57 (real price), -6.71% (real change), 53.94亿成交额 → Strategy based on actual price levels, not hallucinated
This commit is contained in:
@@ -245,6 +245,17 @@ func (a *Agent) gatherContext(text string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// A-share / stocks — try Sina Finance
|
||||
stockCode, stockName := resolveStockCode(text)
|
||||
if stockCode != "" {
|
||||
quote, err := fetchStockQuote(stockCode)
|
||||
if err == nil && quote.Price > 0 {
|
||||
parts = append(parts, fmt.Sprintf("[%s(%s) Real-time A-share Data]\n%s", quote.Name, quote.Code, formatStockQuote(quote)))
|
||||
} else if err != nil {
|
||||
a.logger.Error("fetch stock quote", "code", stockCode, "name", stockName, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Trader positions
|
||||
if a.traderManager != nil {
|
||||
for _, t := range a.traderManager.GetAllTraders() {
|
||||
|
||||
155
agent/stock.go
Normal file
155
agent/stock.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
// StockQuote holds real-time stock data from Sina Finance API.
|
||||
type StockQuote struct {
|
||||
Name string
|
||||
Code string
|
||||
Open float64
|
||||
PrevClose float64
|
||||
Price float64
|
||||
High float64
|
||||
Low float64
|
||||
Volume float64 // shares
|
||||
Turnover float64 // CNY
|
||||
Date string
|
||||
Time string
|
||||
|
||||
Change float64 // Price - PrevClose
|
||||
ChangePct float64 // Change / PrevClose * 100
|
||||
}
|
||||
|
||||
// knownStocks maps Chinese names / common terms to stock codes.
|
||||
var knownStocks = map[string]string{
|
||||
"拓维信息": "sz002261", "比亚迪": "sz002594", "宁德时代": "sz300750",
|
||||
"贵州茅台": "sh600519", "中国平安": "sh601318", "招商银行": "sh600036",
|
||||
"腾讯": "hk00700", "阿里巴巴": "hk09988", "美团": "hk03690",
|
||||
"小米": "hk01810", "京东": "hk09618",
|
||||
"苹果": "usr_aapl", "特斯拉": "usr_tsla", "英伟达": "usr_nvda",
|
||||
"微软": "usr_msft", "谷歌": "usr_googl", "亚马逊": "usr_amzn",
|
||||
}
|
||||
|
||||
// resolveStockCode tries to find a stock code from user text.
|
||||
func resolveStockCode(text string) (string, string) {
|
||||
// Direct match known stocks
|
||||
for name, code := range knownStocks {
|
||||
if strings.Contains(text, name) {
|
||||
return code, name
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract a stock code like 002261, 600519, etc.
|
||||
words := strings.Fields(text)
|
||||
for _, w := range words {
|
||||
w = strings.TrimSpace(w)
|
||||
if len(w) == 6 {
|
||||
if _, err := strconv.Atoi(w); err == nil {
|
||||
prefix := "sz"
|
||||
if w[0] == '6' {
|
||||
prefix = "sh"
|
||||
} else if w[0] == '3' {
|
||||
prefix = "sz"
|
||||
} else if w[0] == '0' {
|
||||
prefix = "sz"
|
||||
}
|
||||
return prefix + w, w
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// fetchStockQuote gets real-time A-share data from Sina Finance.
|
||||
func fetchStockQuote(code string) (*StockQuote, error) {
|
||||
if !strings.HasPrefix(code, "sh") && !strings.HasPrefix(code, "sz") {
|
||||
return nil, fmt.Errorf("unsupported stock code: %s", code)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://hq.sinajs.cn/list=%s", code)
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req.Header.Set("Referer", "https://finance.sina.com.cn")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Sina returns GBK encoding
|
||||
reader := transform.NewReader(resp.Body, simplifiedchinese.GBK.NewDecoder())
|
||||
body, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
line := string(body)
|
||||
// Parse: var hq_str_sz002261="name,open,prev_close,price,high,low,..."
|
||||
start := strings.Index(line, "\"")
|
||||
end := strings.LastIndex(line, "\"")
|
||||
if start == -1 || end <= start {
|
||||
return nil, fmt.Errorf("invalid response for %s", code)
|
||||
}
|
||||
|
||||
fields := strings.Split(line[start+1:end], ",")
|
||||
if len(fields) < 32 {
|
||||
return nil, fmt.Errorf("not enough fields for %s", code)
|
||||
}
|
||||
|
||||
q := &StockQuote{
|
||||
Name: fields[0],
|
||||
Code: code,
|
||||
Date: fields[30],
|
||||
Time: fields[31],
|
||||
}
|
||||
q.Open, _ = strconv.ParseFloat(fields[1], 64)
|
||||
q.PrevClose, _ = strconv.ParseFloat(fields[2], 64)
|
||||
q.Price, _ = strconv.ParseFloat(fields[3], 64)
|
||||
q.High, _ = strconv.ParseFloat(fields[4], 64)
|
||||
q.Low, _ = strconv.ParseFloat(fields[5], 64)
|
||||
q.Volume, _ = strconv.ParseFloat(fields[8], 64)
|
||||
q.Turnover, _ = strconv.ParseFloat(fields[9], 64)
|
||||
|
||||
if q.PrevClose > 0 {
|
||||
q.Change = q.Price - q.PrevClose
|
||||
q.ChangePct = (q.Change / q.PrevClose) * 100
|
||||
}
|
||||
|
||||
return q, nil
|
||||
}
|
||||
|
||||
// formatStockQuote formats a quote for display.
|
||||
func formatStockQuote(q *StockQuote) string {
|
||||
emoji := "🟢"
|
||||
if q.ChangePct < 0 {
|
||||
emoji = "🔴"
|
||||
}
|
||||
volStr := fmt.Sprintf("%.0f万手", q.Volume/1000000)
|
||||
turnStr := fmt.Sprintf("%.2f亿", q.Turnover/100000000)
|
||||
|
||||
return fmt.Sprintf(`%s *%s* (%s)
|
||||
|
||||
💰 现价: ¥%.2f (%+.2f%%)
|
||||
📊 今开: ¥%.2f | 昨收: ¥%.2f
|
||||
📈 最高: ¥%.2f | 最低: ¥%.2f
|
||||
📦 成交量: %s | 成交额: %s
|
||||
🕐 数据时间: %s %s`,
|
||||
emoji, q.Name, q.Code,
|
||||
q.Price, q.ChangePct,
|
||||
q.Open, q.PrevClose,
|
||||
q.High, q.Low,
|
||||
volStr, turnStr,
|
||||
q.Date, q.Time)
|
||||
}
|
||||
Reference in New Issue
Block a user