|
|
package monitor
|
|
|
|
|
|
import (
|
|
|
"context"
|
|
|
"fmt"
|
|
|
"log"
|
|
|
"time"
|
|
|
|
|
|
"market-data-service/api"
|
|
|
"market-data-service/internal/repository"
|
|
|
)
|
|
|
|
|
|
// Monitor 数据质量监控
|
|
|
type Monitor struct {
|
|
|
db *repository.DB
|
|
|
stockRepo *repository.StockRepository
|
|
|
futuresRepo *repository.FuturesRepository
|
|
|
sender AlertSender
|
|
|
}
|
|
|
|
|
|
// AlertSender 告警发送接口
|
|
|
type AlertSender interface {
|
|
|
SendAlert(title, content string) error
|
|
|
}
|
|
|
|
|
|
// NewMonitor 创建监控器
|
|
|
func NewMonitor(db *repository.DB, stockRepo *repository.StockRepository, futuresRepo *repository.FuturesRepository, sender AlertSender) *Monitor {
|
|
|
return &Monitor{
|
|
|
db: db,
|
|
|
stockRepo: stockRepo,
|
|
|
futuresRepo: futuresRepo,
|
|
|
sender: sender,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// CheckResult 检查结果
|
|
|
type CheckResult struct {
|
|
|
Symbol string
|
|
|
Freq string
|
|
|
CheckDate string
|
|
|
CheckType string
|
|
|
Status string // pass/fail
|
|
|
ExpectCount int
|
|
|
ActualCount int
|
|
|
Detail string
|
|
|
}
|
|
|
|
|
|
// DailyCheck 每日数据质量检查
|
|
|
func (m *Monitor) DailyCheck(ctx context.Context, checkDate string) error {
|
|
|
log.Printf("Starting daily data quality check for %s", checkDate)
|
|
|
|
|
|
// 检查股票数据
|
|
|
if err := m.checkStockData(ctx, checkDate); err != nil {
|
|
|
log.Printf("Stock data check failed: %v", err)
|
|
|
}
|
|
|
|
|
|
// 检查期货数据
|
|
|
if err := m.checkFuturesData(ctx, checkDate); err != nil {
|
|
|
log.Printf("Futures data check failed: %v", err)
|
|
|
}
|
|
|
|
|
|
log.Printf("Daily data quality check completed")
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// checkStockData 检查股票数据质量
|
|
|
func (m *Monitor) checkStockData(ctx context.Context, checkDate string) error {
|
|
|
// 1. 获取所有活跃股票
|
|
|
symbols, _, err := m.stockRepo.ListSymbols(ctx, &api.SymbolListRequest{
|
|
|
Page: 1,
|
|
|
Size: 10000,
|
|
|
})
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
// 2. 检查1分钟线完整性(240条)
|
|
|
for _, symbol := range symbols {
|
|
|
result := m.checkKLineCompleteness(ctx, "stock", symbol.SymbolID, "1m", checkDate, 240)
|
|
|
if err := m.saveCheckResult(ctx, "stock", result); err != nil {
|
|
|
log.Printf("Failed to save check result: %v", err)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// checkFuturesData 检查期货数据质量
|
|
|
func (m *Monitor) checkFuturesData(ctx context.Context, checkDate string) error {
|
|
|
// 获取所有活跃期货合约
|
|
|
symbols, _, err := m.futuresRepo.ListSymbols(ctx, &api.SymbolListRequest{
|
|
|
Page: 1,
|
|
|
Size: 10000,
|
|
|
})
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
// 检查1分钟线完整性
|
|
|
for _, symbol := range symbols {
|
|
|
result := m.checkKLineCompleteness(ctx, "futures", symbol.SymbolID, "1m", checkDate, 240)
|
|
|
if err := m.saveCheckResult(ctx, "futures", result); err != nil {
|
|
|
log.Printf("Failed to save check result: %v", err)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// checkKLineCompleteness 检查K线完整性
|
|
|
func (m *Monitor) checkKLineCompleteness(ctx context.Context, assetType, symbol, freq, checkDate string, expectCount int) CheckResult {
|
|
|
result := CheckResult{
|
|
|
Symbol: symbol,
|
|
|
Freq: freq,
|
|
|
CheckDate: checkDate,
|
|
|
CheckType: "missing",
|
|
|
Status: "pass",
|
|
|
}
|
|
|
|
|
|
// 解析日期
|
|
|
start, _ := time.Parse("20060102", checkDate)
|
|
|
end := start.Add(24 * time.Hour).Add(-time.Second)
|
|
|
|
|
|
var actualCount int
|
|
|
var err error
|
|
|
|
|
|
if assetType == "stock" {
|
|
|
items, err := m.stockRepo.GetKLines(ctx, symbol, api.Frequency(freq), start, end, api.AdjustNone)
|
|
|
if err == nil {
|
|
|
actualCount = len(items)
|
|
|
}
|
|
|
} else {
|
|
|
items, err := m.futuresRepo.GetKLines(ctx, symbol, api.Frequency(freq), start, end)
|
|
|
if err == nil {
|
|
|
actualCount = len(items)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if err != nil {
|
|
|
result.Status = "fail"
|
|
|
result.Detail = fmt.Sprintf("Error querying data: %v", err)
|
|
|
return result
|
|
|
}
|
|
|
|
|
|
result.ExpectCount = expectCount
|
|
|
result.ActualCount = actualCount
|
|
|
|
|
|
// 判断缺失情况
|
|
|
if actualCount < expectCount {
|
|
|
result.Status = "fail"
|
|
|
result.Detail = fmt.Sprintf("Data missing: expected %d, actual %d", expectCount, actualCount)
|
|
|
|
|
|
// 发送告警
|
|
|
if m.sender != nil {
|
|
|
title := fmt.Sprintf("[%s] Data Missing Alert", assetType)
|
|
|
content := fmt.Sprintf("Symbol: %s, Date: %s, Expected: %d, Actual: %d",
|
|
|
symbol, checkDate, expectCount, actualCount)
|
|
|
m.sender.SendAlert(title, content)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
}
|
|
|
|
|
|
// saveCheckResult 保存检查结果
|
|
|
func (m *Monitor) saveCheckResult(ctx context.Context, assetType string, result CheckResult) error {
|
|
|
// 使用Repository保存到data_quality_checks表
|
|
|
query := fmt.Sprintf(`
|
|
|
INSERT INTO %s.data_quality_checks
|
|
|
(check_date, symbol_id, freq, check_type, status, expect_count, actual_count, detail)
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
|
ON CONFLICT (check_date, symbol_id, freq, check_type) DO UPDATE SET
|
|
|
status = EXCLUDED.status,
|
|
|
expect_count = EXCLUDED.expect_count,
|
|
|
actual_count = EXCLUDED.actual_count,
|
|
|
detail = EXCLUDED.detail,
|
|
|
created_at = NOW()
|
|
|
`, assetType)
|
|
|
|
|
|
_, err := m.db.ExecContext(ctx, query,
|
|
|
result.CheckDate, result.Symbol, result.Freq,
|
|
|
result.CheckType, result.Status, result.ExpectCount,
|
|
|
result.ActualCount, result.Detail)
|
|
|
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
// GetQualityReport 获取数据质量报告
|
|
|
func (m *Monitor) GetQualityReport(ctx context.Context, assetType, checkDate string) (*QualityReport, error) {
|
|
|
query := fmt.Sprintf(`
|
|
|
SELECT
|
|
|
COUNT(*) as total_checks,
|
|
|
SUM(CASE WHEN status = 'pass' THEN 1 ELSE 0 END) as pass_count,
|
|
|
SUM(CASE WHEN status = 'fail' THEN 1 ELSE 0 END) as fail_count
|
|
|
FROM %s.data_quality_checks
|
|
|
WHERE check_date = $1
|
|
|
`, assetType)
|
|
|
|
|
|
var report QualityReport
|
|
|
report.CheckDate = checkDate
|
|
|
report.AssetType = assetType
|
|
|
|
|
|
row := m.db.QueryRowContext(ctx, query, checkDate)
|
|
|
err := row.Scan(&report.TotalChecks, &report.PassCount, &report.FailCount)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
if report.TotalChecks > 0 {
|
|
|
report.PassRate = float64(report.PassCount) / float64(report.TotalChecks) * 100
|
|
|
}
|
|
|
|
|
|
return &report, nil
|
|
|
}
|
|
|
|
|
|
// QualityReport 数据质量报告
|
|
|
type QualityReport struct {
|
|
|
AssetType string `json:"asset_type"`
|
|
|
CheckDate string `json:"check_date"`
|
|
|
TotalChecks int `json:"total_checks"`
|
|
|
PassCount int `json:"pass_count"`
|
|
|
FailCount int `json:"fail_count"`
|
|
|
PassRate float64 `json:"pass_rate"`
|
|
|
}
|
|
|
|
|
|
// LogAlertSender 日志告警发送器
|
|
|
type LogAlertSender struct{}
|
|
|
|
|
|
// SendAlert 发送告警到日志
|
|
|
func (s *LogAlertSender) SendAlert(title, content string) error {
|
|
|
log.Printf("[ALERT] %s: %s", title, content)
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// StartDailyCheckCron 启动每日检查定时任务
|
|
|
func (m *Monitor) StartDailyCheckCron(ctx context.Context) {
|
|
|
go func() {
|
|
|
for {
|
|
|
// 计算到下一个盘后的时间(假设15:30)
|
|
|
now := time.Now()
|
|
|
next := time.Date(now.Year(), now.Month(), now.Day(), 15, 35, 0, 0, now.Location())
|
|
|
if next.Before(now) {
|
|
|
next = next.Add(24 * time.Hour)
|
|
|
}
|
|
|
|
|
|
timer := time.NewTimer(next.Sub(now))
|
|
|
<-timer.C
|
|
|
|
|
|
// 执行检查
|
|
|
checkDate := now.Format("20060102")
|
|
|
if err := m.DailyCheck(ctx, checkDate); err != nil {
|
|
|
log.Printf("Daily check failed: %v", err)
|
|
|
}
|
|
|
}
|
|
|
}()
|
|
|
}
|