系统管理员点击同步时同步半年历史数据

This commit is contained in:
2026-07-04 11:54:07 +08:00
parent 716761b3d9
commit 63687723ab
3 changed files with 176 additions and 2 deletions
+120 -1
View File
@@ -40,6 +40,19 @@ func NewStockSyncService(cfg *config.Config, db *gorm.DB) *StockSyncService {
// Sync 执行一次盘后同步,返回写入的记录数。
func (s *StockSyncService) Sync(ctx context.Context, triggerBy string) (int, error) {
return s.runSyncJob(ctx, triggerBy, func() (int, error) {
return s.doSync(ctx)
})
}
// SyncHistory 同步近 N 个月的历史日 K 数据,返回写入的记录数。
func (s *StockSyncService) SyncHistory(ctx context.Context, triggerBy string, months int) (int, error) {
return s.runSyncJob(ctx, triggerBy, func() (int, error) {
return s.doSyncHistory(ctx, months)
})
}
func (s *StockSyncService) runSyncJob(ctx context.Context, triggerBy string, fn func() (int, error)) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -53,7 +66,7 @@ func (s *StockSyncService) Sync(ctx context.Context, triggerBy string) (int, err
return 0, fmt.Errorf("create sync job: %w", err)
}
recordsCount, err := s.doSync(ctx)
recordsCount, err := fn()
now := time.Now().UTC()
updates := map[string]any{
@@ -143,6 +156,105 @@ func (s *StockSyncService) LatestJob(ctx context.Context) (*models.StockSyncJob,
return &job, nil
}
func (s *StockSyncService) doSyncHistory(ctx context.Context, months int) (int, error) {
// 1. 获取全 A 股代码列表
universe, err := s.client.GetUniverse("CN_Equity_A")
if err != nil {
return 0, fmt.Errorf("fetch universe: %w", err)
}
if universe == nil || len(universe.Symbols) == 0 {
return 0, fmt.Errorf("empty universe")
}
// 2. 获取核心指数列表
allSymbols := append([]string{}, universe.Symbols...)
allSymbols = append(allSymbols, coreIndexSymbols...)
// 3. 计算时间范围(近 N 个月)
endMs := time.Now().In(chinaLoc).UnixMilli()
startMs := time.Now().In(chinaLoc).AddDate(0, -months, 0).UnixMilli()
log.Printf("[stock sync] start history sync for %d months, symbols=%d, range=%d-%d",
months, len(allSymbols), startMs, endMs)
// 4. 分批拉取并写入
const batchSize = 50
totalRecords := 0
for i := 0; i < len(allSymbols); i += batchSize {
batch := allSymbols[i:min(i+batchSize, len(allSymbols))]
klinesMap, err := s.client.GetKlinesBatch(batch, "1d", startMs, endMs)
if err != nil {
log.Printf("[stock sync] klines batch %d-%d failed: %v", i+1, min(i+batchSize, len(allSymbols)), err)
continue
}
records := make([]models.StockDailyQuote, 0)
for sym, data := range klinesMap {
name := sym
records = append(records, klineDataToRecords(sym, name, data)...)
}
if len(records) == 0 {
continue
}
if err := s.db.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "symbol"}, {Name: "trade_date"}},
UpdateAll: true,
}).CreateInBatches(records, 500).Error; err != nil {
return totalRecords, fmt.Errorf("upsert history batch %d: %w", i/batchSize, err)
}
totalRecords += len(records)
log.Printf("[stock sync] history batch %d/%d done, records=%d, total=%d",
(i/batchSize)+1, (len(allSymbols)+batchSize-1)/batchSize, len(records), totalRecords)
}
return totalRecords, nil
}
func klineDataToRecords(symbol, name string, data datasource.KlineData) []models.StockDailyQuote {
n := len(data.Timestamp)
if n == 0 {
return nil
}
if len(data.Open) < n { n = len(data.Open) }
if len(data.High) < n { n = len(data.High) }
if len(data.Low) < n { n = len(data.Low) }
if len(data.Close) < n { n = len(data.Close) }
if len(data.Volume) < n { n = len(data.Volume) }
if len(data.Amount) < n { n = len(data.Amount) }
records := make([]models.StockDailyQuote, 0, n)
for i := 0; i < n; i++ {
open := round2(data.Open[i])
high := round2(data.High[i])
low := round2(data.Low[i])
close := round2(data.Close[i])
var prevClose float64
if i > 0 {
prevClose = round2(data.Close[i-1])
}
var changePct float64
if prevClose != 0 {
changePct = (close - prevClose) / prevClose
}
records = append(records, models.StockDailyQuote{
Symbol: symbol,
Name: name,
TradeDate: msToDate(data.Timestamp[i]),
Open: open,
High: high,
Low: low,
Close: close,
PrevClose: prevClose,
Volume: data.Volume[i],
Amount: data.Amount[i],
ChangePct: changePct,
})
}
return records
}
func today() time.Time {
return time.Now().In(chinaLoc).Truncate(24 * time.Hour)
}
@@ -154,3 +266,10 @@ func msToDate(ms int64) time.Time {
func round2(v float64) float64 {
return math.Round(v*100) / 100
}
func min(a, b int) int {
if a < b {
return a
}
return b
}