AI分析功能:LLM Key 改为数据库管理,支持管理员后台配置
This commit is contained in:
134
web/backend/internal/store/ai.go
Normal file
134
web/backend/internal/store/ai.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// AnalysisContext 汇总一次 AI 分析所需的全部数据。
|
||||
type AnalysisContext struct {
|
||||
Score Score `json:"score"`
|
||||
Candles []Candle `json:"candles"`
|
||||
RecentScores []Score `json:"recent_scores"`
|
||||
}
|
||||
|
||||
// GetAnalysisContext 拉取指定合约某日的 score + 近 60 日 K 线 + 近 10 日 scores。
|
||||
func (s *FuturesStore) GetAnalysisContext(tsCode, tradeDate string) (*AnalysisContext, error) {
|
||||
ctx := &AnalysisContext{}
|
||||
|
||||
// 1) 目标日 score(含 detail_json)
|
||||
row := s.db.QueryRow(`SELECT id, ts_code, trade_date, close, oi, oi_chg, short_term, medium_term,
|
||||
long_term, composite, signal, detail_json, created_at FROM scores
|
||||
WHERE ts_code = $1 AND trade_date = $2`, tsCode, tradeDate)
|
||||
var detail sql.NullString
|
||||
if err := row.Scan(&ctx.Score.ID, &ctx.Score.TsCode, &ctx.Score.TradeDate,
|
||||
&ctx.Score.Close, &ctx.Score.OI, &ctx.Score.OIChg,
|
||||
&ctx.Score.ShortTerm, &ctx.Score.MediumTerm, &ctx.Score.LongTerm,
|
||||
&ctx.Score.Composite, &ctx.Score.Signal, &detail, &ctx.Score.CreatedAt); err != nil {
|
||||
return nil, fmt.Errorf("score not found: %w", err)
|
||||
}
|
||||
if detail.Valid {
|
||||
ctx.Score.Detail = json.RawMessage(detail.String)
|
||||
}
|
||||
|
||||
// 2) 近 60 日 K 线
|
||||
rows, err := s.db.Query(`SELECT ts_code, trade_date,
|
||||
COALESCE(NULLIF(open, 'NaN'::real), 0), COALESCE(NULLIF(high, 'NaN'::real), 0),
|
||||
COALESCE(NULLIF(low, 'NaN'::real), 0), COALESCE(NULLIF(close, 'NaN'::real), 0),
|
||||
COALESCE(NULLIF(vol, 'NaN'::real), 0), COALESCE(NULLIF(amount, 'NaN'::real), 0),
|
||||
COALESCE(NULLIF(oi, 'NaN'::real), 0), COALESCE(NULLIF(oi_chg, 'NaN'::real), 0),
|
||||
COALESCE(NULLIF(pre_close, 'NaN'::real), 0)
|
||||
FROM candles WHERE ts_code = $1 AND trade_date <= $2
|
||||
ORDER BY trade_date DESC LIMIT 60`, tsCode, tradeDate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("candles: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var c Candle
|
||||
if err := rows.Scan(&c.TsCode, &c.TradeDate, &c.Open, &c.High, &c.Low, &c.Close,
|
||||
&c.Vol, &c.Amount, &c.OI, &c.OIChg, &c.PreClose); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx.Candles = append(ctx.Candles, c)
|
||||
}
|
||||
// 反转回升序
|
||||
for i, j := 0, len(ctx.Candles)-1; i < j; i, j = i+1, j-1 {
|
||||
ctx.Candles[i], ctx.Candles[j] = ctx.Candles[j], ctx.Candles[i]
|
||||
}
|
||||
|
||||
// 3) 近 10 日 scores(不含 detail_json 以减体积)
|
||||
scoreRows, err := s.db.Query(`SELECT id, ts_code, trade_date, close, oi, oi_chg, short_term, medium_term,
|
||||
long_term, composite, signal, created_at FROM scores
|
||||
WHERE ts_code = $1 AND trade_date <= $2
|
||||
ORDER BY trade_date DESC LIMIT 10`, tsCode, tradeDate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("recent scores: %w", err)
|
||||
}
|
||||
defer scoreRows.Close()
|
||||
for scoreRows.Next() {
|
||||
var sc Score
|
||||
if err := scoreRows.Scan(&sc.ID, &sc.TsCode, &sc.TradeDate, &sc.Close, &sc.OI, &sc.OIChg,
|
||||
&sc.ShortTerm, &sc.MediumTerm, &sc.LongTerm, &sc.Composite, &sc.Signal, &sc.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx.RecentScores = append(ctx.RecentScores, sc)
|
||||
}
|
||||
// 反转回升序
|
||||
for i, j := 0, len(ctx.RecentScores)-1; i < j; i, j = i+1, j-1 {
|
||||
ctx.RecentScores[i], ctx.RecentScores[j] = ctx.RecentScores[j], ctx.RecentScores[i]
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// LLMConfig 数据库中的 LLM 配置单例。
|
||||
type LLMConfig struct {
|
||||
BaseURL string `json:"base_url"`
|
||||
APIKey string `json:"api_key"`
|
||||
Model string `json:"model"`
|
||||
}
|
||||
|
||||
// EnsureLLMConfigTable 建 llm_config 表(幂等)。
|
||||
func (s *FuturesStore) EnsureLLMConfigTable() error {
|
||||
_, err := s.db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS llm_config (
|
||||
id INTEGER PRIMARY KEY DEFAULT 1 CHECK(id = 1),
|
||||
base_url TEXT NOT NULL DEFAULT 'https://api.deepseek.com/v1',
|
||||
api_key TEXT NOT NULL DEFAULT '',
|
||||
model TEXT NOT NULL DEFAULT 'deepseek-chat',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
INSERT INTO llm_config (id) VALUES (1) ON CONFLICT DO NOTHING;
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetLLMConfig 读取 LLM 配置,无记录返回零值。
|
||||
func (s *FuturesStore) GetLLMConfig() (*LLMConfig, error) {
|
||||
cfg := &LLMConfig{}
|
||||
err := s.db.QueryRow(`SELECT base_url, api_key, model FROM llm_config WHERE id = 1`).
|
||||
Scan(&cfg.BaseURL, &cfg.APIKey, &cfg.Model)
|
||||
if err != nil {
|
||||
if err.Error() == "sql: no rows in result set" {
|
||||
return cfg, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// SaveLLMConfig 写入 LLM 配置(upsert)。
|
||||
func (s *FuturesStore) SaveLLMConfig(cfg *LLMConfig) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO llm_config (id, base_url, api_key, model, updated_at)
|
||||
VALUES (1, $1, $2, $3, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
base_url = EXCLUDED.base_url,
|
||||
api_key = EXCLUDED.api_key,
|
||||
model = EXCLUDED.model,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`, cfg.BaseURL, cfg.APIKey, cfg.Model)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user