package store import ( "database/sql" "encoding/json" "errors" "fmt" "strings" _ "github.com/lib/pq" ) var ErrMissingTsCode = errors.New("ts_code 必填") type FuturesStore struct{ db *sql.DB } func OpenFutures(databaseURL string) (*FuturesStore, error) { db, err := sql.Open("postgres", databaseURL) if err != nil { return nil, fmt.Errorf("open futures db: %w", err) } db.SetMaxOpenConns(8) if err := db.Ping(); err != nil { return nil, fmt.Errorf("ping futures db: %w", err) } return &FuturesStore{db: db}, nil } func (s *FuturesStore) Close() error { return s.db.Close() } type Score struct { ID int64 `json:"id"` TsCode string `json:"ts_code"` TradeDate string `json:"trade_date"` Close float64 `json:"close"` OI float64 `json:"oi"` OIChg float64 `json:"oi_chg"` ShortTerm float64 `json:"short_term"` MediumTerm float64 `json:"medium_term"` LongTerm float64 `json:"long_term"` Composite float64 `json:"composite"` Signal string `json:"signal"` Detail json.RawMessage `json:"detail,omitempty"` CreatedAt string `json:"created_at"` } type ScoreFilter struct { TsCode string Start string End string Limit int } func (s *FuturesStore) ListScores(f ScoreFilter) ([]Score, error) { q := `SELECT id, ts_code, trade_date, close, oi, oi_chg, short_term, medium_term, long_term, composite, signal, created_at FROM scores WHERE 1=1` args := []any{} if f.TsCode != "" { q += " AND ts_code = ?" args = append(args, f.TsCode) } if f.Start != "" { q += " AND trade_date >= ?" args = append(args, f.Start) } if f.End != "" { q += " AND trade_date <= ?" args = append(args, f.End) } q += " ORDER BY trade_date DESC, id DESC" if f.Limit <= 0 || f.Limit > 500 { f.Limit = 200 } q += " LIMIT ?" args = append(args, f.Limit) rows, err := s.db.Query(q, args...) if err != nil { return nil, err } defer rows.Close() out := []Score{} for rows.Next() { var x Score if err := rows.Scan(&x.ID, &x.TsCode, &x.TradeDate, &x.Close, &x.OI, &x.OIChg, &x.ShortTerm, &x.MediumTerm, &x.LongTerm, &x.Composite, &x.Signal, &x.CreatedAt); err != nil { return nil, err } out = append(out, x) } return out, rows.Err() } func (s *FuturesStore) GetScore(id int64) (*Score, error) { 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 id = ?`, id) var x Score var detail sql.NullString if err := row.Scan(&x.ID, &x.TsCode, &x.TradeDate, &x.Close, &x.OI, &x.OIChg, &x.ShortTerm, &x.MediumTerm, &x.LongTerm, &x.Composite, &x.Signal, &detail, &x.CreatedAt); err != nil { return nil, err } if detail.Valid && strings.TrimSpace(detail.String) != "" { x.Detail = json.RawMessage(detail.String) } return &x, nil } func (s *FuturesStore) ListContracts() ([]string, error) { rows, err := s.db.Query(`SELECT DISTINCT ts_code FROM scores ORDER BY ts_code ASC`) if err != nil { return nil, err } defer rows.Close() out := []string{} for rows.Next() { var c string if err := rows.Scan(&c); err != nil { return nil, err } out = append(out, c) } return out, rows.Err() } type Candle struct { TsCode string `json:"ts_code"` TradeDate string `json:"trade_date"` Open float64 `json:"open"` High float64 `json:"high"` Low float64 `json:"low"` Close float64 `json:"close"` Vol float64 `json:"vol"` Amount float64 `json:"amount"` OI float64 `json:"oi"` OIChg float64 `json:"oi_chg"` PreClose float64 `json:"pre_close"` } func (s *FuturesStore) ListCandles(tsCode, start, end string) ([]Candle, error) { if tsCode == "" { return nil, ErrMissingTsCode } q := `SELECT ts_code, trade_date, COALESCE(open, 0), COALESCE(high, 0), COALESCE(low, 0), COALESCE(close, 0), COALESCE(vol, 0), COALESCE(amount, 0), COALESCE(oi, 0), COALESCE(oi_chg, 0), COALESCE(pre_close, 0) FROM candles WHERE ts_code = ?` args := []any{tsCode} if start != "" { q += " AND trade_date >= ?" args = append(args, start) } if end != "" { q += " AND trade_date <= ?" args = append(args, end) } q += " ORDER BY trade_date ASC LIMIT 1000" rows, err := s.db.Query(q, args...) if err != nil { return nil, err } defer rows.Close() out := []Candle{} 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 } out = append(out, c) } return out, rows.Err() }