105 lines
2.8 KiB
Go
105 lines
2.8 KiB
Go
package marketdata
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"overnight-trading-bot/internal/domain"
|
|
"overnight-trading-bot/internal/repository"
|
|
"overnight-trading-bot/internal/timeutil"
|
|
"overnight-trading-bot/internal/tinvest"
|
|
)
|
|
|
|
type Loader struct {
|
|
repo repository.Repository
|
|
gateway tinvest.Gateway
|
|
clock timeutil.Clock
|
|
}
|
|
|
|
func NewLoader(repo repository.Repository, gateway tinvest.Gateway) Loader {
|
|
return Loader{repo: repo, gateway: gateway, clock: timeutil.RealClock{}}
|
|
}
|
|
|
|
func (l *Loader) SetClock(clock timeutil.Clock) {
|
|
if clock != nil {
|
|
l.clock = clock
|
|
}
|
|
}
|
|
|
|
func (l Loader) BackfillDaily(ctx context.Context, instruments []domain.Instrument, from, to time.Time) error {
|
|
eligible := 0
|
|
succeeded := 0
|
|
for _, instrument := range instruments {
|
|
if !instrument.Enabled || instrument.Quarantine {
|
|
continue
|
|
}
|
|
eligible++
|
|
candles, err := l.gateway.GetCandles(ctx, instrument.InstrumentUID, "day", from, to)
|
|
if err != nil {
|
|
return fmt.Errorf("load daily candles %s: %w", instrument.Ticker, err)
|
|
}
|
|
if err := l.repo.UpsertDailyCandles(ctx, candles); err != nil {
|
|
return fmt.Errorf("persist candles %s: %w", instrument.Ticker, err)
|
|
}
|
|
succeeded++
|
|
}
|
|
if eligible > 0 && succeeded == 0 {
|
|
return fmt.Errorf("no daily candles loaded for eligible instruments")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (l Loader) BackfillMinute(ctx context.Context, instruments []domain.Instrument, from, to time.Time) error {
|
|
eligible := 0
|
|
succeeded := 0
|
|
for _, instrument := range instruments {
|
|
if !instrument.Enabled || instrument.Quarantine {
|
|
continue
|
|
}
|
|
eligible++
|
|
candles, err := l.gateway.GetCandles(ctx, instrument.InstrumentUID, "minute", from, to)
|
|
if err != nil {
|
|
return fmt.Errorf("load minute candles %s: %w", instrument.Ticker, err)
|
|
}
|
|
if err := l.repo.UpsertMinuteCandles(ctx, candles); err != nil {
|
|
return fmt.Errorf("persist minute candles %s: %w", instrument.Ticker, err)
|
|
}
|
|
succeeded++
|
|
}
|
|
if eligible > 0 && succeeded == 0 {
|
|
return fmt.Errorf("no minute candles loaded for eligible instruments")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (l Loader) LatestQuote(ctx context.Context, instrumentUID string, depth int32, maxAge time.Duration) (domain.OrderBook, error) {
|
|
book, err := l.gateway.GetOrderBook(ctx, instrumentUID, depth)
|
|
if err != nil {
|
|
return domain.OrderBook{}, err
|
|
}
|
|
quoteTs := quoteTimestamp(book)
|
|
if quoteTs.IsZero() {
|
|
return domain.OrderBook{}, fmt.Errorf("quote timestamp is missing")
|
|
}
|
|
age := l.nowUTC().Sub(quoteTs)
|
|
if maxAge > 0 && age > maxAge {
|
|
return domain.OrderBook{}, fmt.Errorf("quote age %s exceeds %s", age, maxAge)
|
|
}
|
|
return book, nil
|
|
}
|
|
|
|
func quoteTimestamp(book domain.OrderBook) time.Time {
|
|
if !book.Time.IsZero() {
|
|
return book.Time.UTC()
|
|
}
|
|
return book.ReceivedAt.UTC()
|
|
}
|
|
|
|
func (l Loader) nowUTC() time.Time {
|
|
if l.clock == nil {
|
|
return time.Now().UTC()
|
|
}
|
|
return l.clock.Now().UTC()
|
|
}
|