You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
206 lines
5.9 KiB
206 lines
5.9 KiB
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"market-data-service/adapter"
|
|
"market-data-service/api"
|
|
"market-data-service/internal/repository"
|
|
)
|
|
|
|
// StockServiceImpl 股票服务实现
|
|
type StockServiceImpl struct {
|
|
repo *repository.StockRepository
|
|
adapter adapter.DataSourceAdapter
|
|
}
|
|
|
|
// NewStockService 创建股票服务
|
|
func NewStockService(repo *repository.StockRepository, adapter adapter.DataSourceAdapter) StockService {
|
|
return &StockServiceImpl{
|
|
repo: repo,
|
|
adapter: adapter,
|
|
}
|
|
}
|
|
|
|
// QueryKLines 查询K线数据
|
|
// 实现回源机制:先查数据库,如数据缺失则从数据源获取并保存
|
|
func (s *StockServiceImpl) QueryKLines(ctx context.Context, req *api.KLineQueryRequest) (*api.KLineData, error) {
|
|
// 解析日期
|
|
start, err := time.Parse("20060102", req.Start)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid start date: %w", err)
|
|
}
|
|
end, err := time.Parse("20060102", req.End)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid end date: %w", err)
|
|
}
|
|
end = end.Add(24 * time.Hour).Add(-time.Second) // 包含结束日期全天
|
|
|
|
// 1. 先尝试从数据库获取数据
|
|
items, err := s.repo.GetKLines(ctx, req.Symbol, req.Freq, start, end, req.Adjust)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 2. 判断数据是否完整(简单策略:如果数据为空或数量明显不足,则触发回源)
|
|
shouldFetchFromSource := len(items) == 0 || s.isDataIncomplete(req.Start, req.End, req.Freq, len(items))
|
|
|
|
// 3. 如果数据不完整且有配置适配器,则从数据源获取
|
|
if shouldFetchFromSource && s.adapter != nil {
|
|
fetchedItems, err := s.fetchFromSourceAndSave(ctx, req, start, end)
|
|
if err == nil && len(fetchedItems) > 0 {
|
|
items = fetchedItems
|
|
}
|
|
}
|
|
|
|
// 4. 处理复权
|
|
if req.Adjust != api.AdjustNone {
|
|
items = s.applyAdjust(ctx, req.Symbol, items, req.Adjust)
|
|
}
|
|
|
|
return &api.KLineData{
|
|
Symbol: req.Symbol,
|
|
Freq: req.Freq,
|
|
Adjust: req.Adjust,
|
|
Count: len(items),
|
|
Items: items,
|
|
}, nil
|
|
}
|
|
|
|
// isDataIncomplete 判断数据是否不完整(简单启发式判断)
|
|
func (s *StockServiceImpl) isDataIncomplete(start, end string, freq api.Frequency, count int) bool {
|
|
startDate, _ := time.Parse("20060102", start)
|
|
endDate, _ := time.Parse("20060102", end)
|
|
expectedDays := int(endDate.Sub(startDate).Hours()/24) + 1
|
|
|
|
switch freq {
|
|
case api.Freq1Day:
|
|
// 日线:预期交易日数量约为自然日数量的 70%
|
|
expectedTradingDays := int(float64(expectedDays) * 0.7)
|
|
return count < expectedTradingDays
|
|
case api.Freq1Week:
|
|
expectedWeeks := expectedDays / 7
|
|
return count < expectedWeeks
|
|
case api.Freq1Month:
|
|
expectedMonths := expectedDays / 30
|
|
return count < expectedMonths
|
|
default:
|
|
// 分钟线:不判断完整性,因为数量难以预估
|
|
return false
|
|
}
|
|
}
|
|
|
|
// fetchFromSourceAndSave 从数据源获取数据并保存到数据库
|
|
func (s *StockServiceImpl) fetchFromSourceAndSave(ctx context.Context, req *api.KLineQueryRequest, start, end time.Time) ([]api.KLineItem, error) {
|
|
// 从数据源获取
|
|
adapterData, err := s.adapter.FetchKLines(req.Symbol, req.Start, req.End, string(req.Freq))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetch from source failed: %w", err)
|
|
}
|
|
|
|
if len(adapterData) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// 转换为 repository 格式并保存
|
|
saveItems := make([]api.KLineItem, len(adapterData))
|
|
resultItems := make([]api.KLineItem, len(adapterData))
|
|
for i, d := range adapterData {
|
|
item := api.KLineItem{
|
|
Time: time.Unix(d.Time, 0),
|
|
Open: d.Open,
|
|
High: d.High,
|
|
Low: d.Low,
|
|
Close: d.Close,
|
|
Volume: d.Volume,
|
|
Amount: d.Amount,
|
|
}
|
|
saveItems[i] = item
|
|
resultItems[i] = item
|
|
}
|
|
|
|
// 异步保存到数据库(不阻塞响应)
|
|
go func() {
|
|
saveCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
if err := s.repo.SaveKLines(saveCtx, req.Freq, saveItems); err != nil {
|
|
// 记录错误但不影响返回结果
|
|
fmt.Printf("save klines to db failed: %v\n", err)
|
|
}
|
|
}()
|
|
|
|
return resultItems, nil
|
|
}
|
|
|
|
// applyAdjust 应用复权
|
|
func (s *StockServiceImpl) applyAdjust(ctx context.Context, symbol string, items []api.KLineItem, adjustType api.AdjustType) []api.KLineItem {
|
|
// TODO: 实现复权计算
|
|
// 1. 从数据库获取复权系数
|
|
// 2. 根据前复权/后复权计算价格
|
|
return items
|
|
}
|
|
|
|
// ListSymbols 查询标的列表
|
|
func (s *StockServiceImpl) ListSymbols(ctx context.Context, req *api.SymbolListRequest) (*api.SymbolListData, error) {
|
|
symbols, total, err := s.repo.ListSymbols(ctx, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &api.SymbolListData{
|
|
Total: total,
|
|
Page: req.Page,
|
|
Size: req.Size,
|
|
Items: symbols,
|
|
}, nil
|
|
}
|
|
|
|
// BatchQueryKLines 批量查询K线
|
|
func (s *StockServiceImpl) BatchQueryKLines(ctx context.Context, req *api.BatchKLineRequest) (*api.BatchKLineData, error) {
|
|
results := make([]api.BatchKLineResult, len(req.Symbols))
|
|
|
|
for i, symbol := range req.Symbols {
|
|
singleReq := &api.KLineQueryRequest{
|
|
Symbol: symbol,
|
|
Start: req.Start,
|
|
End: req.End,
|
|
Freq: req.Freq,
|
|
Adjust: req.Adjust,
|
|
}
|
|
|
|
data, err := s.QueryKLines(ctx, singleReq)
|
|
results[i] = api.BatchKLineResult{
|
|
Symbol: symbol,
|
|
Success: err == nil,
|
|
}
|
|
|
|
if err != nil {
|
|
results[i].Error = err.Error()
|
|
} else {
|
|
results[i].Data = &api.KLineSubData{
|
|
Count: data.Count,
|
|
Items: data.Items,
|
|
}
|
|
}
|
|
}
|
|
|
|
return &api.BatchKLineData{Results: results}, nil
|
|
}
|
|
|
|
// GetTradingDates 获取交易日历
|
|
func (s *StockServiceImpl) GetTradingDates(ctx context.Context, req *api.TradingDatesRequest) (*api.TradingDatesData, error) {
|
|
return s.repo.GetTradingDates(ctx, req.Start, req.End)
|
|
}
|
|
|
|
// SyncSymbolsFromSource 从数据源同步标的列表
|
|
func (s *StockServiceImpl) SyncSymbolsFromSource(ctx context.Context, adapter interface{ FetchSymbols(assetType string) ([]struct {
|
|
SymbolID string
|
|
Name string
|
|
Exchange string
|
|
}, error) }) error {
|
|
// TODO: 实现从Tushare同步标的列表
|
|
return nil
|
|
}
|