diff --git a/README.md b/README.md index 64c97a9..f1adb12 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Common formats: | `TINVEST_RETRY_COUNT` | `3` | Number of T-Invest SDK attempts. | | `TINVEST_RETRY_BACKOFF_SEC` | `2` | Initial exponential backoff in seconds. | | `TINVEST_USE_SANDBOX` | `false` | Compatibility guard; valid only with `APP_MODE=sandbox`. | -| `TINVEST_TRADING_CALENDAR_EXCHANGE` | `MOEX` | Exchange calendar used to load trading days. | +| `TINVEST_TRADING_CALENDAR_EXCHANGE` | `MOEX` | Deprecated compatibility setting; historical feature calendars are derived from loaded daily candles. | ### DB diff --git a/README.ru.md b/README.ru.md index 1de1deb..c8a5647 100644 --- a/README.ru.md +++ b/README.ru.md @@ -55,7 +55,7 @@ APP_MODE=backtest go run ./cmd/bot | `TINVEST_RETRY_COUNT` | целое число попыток | `3` | `<= 0` трактуется как одна попытка | Общее число попыток для SDK-вызовов T-Invest через exponential backoff. Больше значение повышает устойчивость к кратким сбоям, но может дольше задерживать окончательную ошибку. | | `TINVEST_RETRY_BACKOFF_SEC` | целое число секунд | `2` | рекомендуется `>= 0` | Начальный интервал exponential backoff для SDK-вызовов T-Invest. Больше значение снижает частоту повторов при сбоях, но дольше задерживает окончательную ошибку. | | `TINVEST_USE_SANDBOX` | `true` или `false` | `false` | boolean; разрешено только при `APP_MODE=sandbox` | Защитный флаг совместимости. В `live_readonly` и `live_trade` запрещён валидацией, чтобы случайно не подменить фактическую среду исполнения. | -| `TINVEST_TRADING_CALENDAR_EXCHANGE` | код биржевого календаря, например `MOEX` | `MOEX` | пустое значение заменяется на `MOEX` | Календарь торговых дней для загрузки истории и расчёта торгового цикла. | +| `TINVEST_TRADING_CALENDAR_EXCHANGE` | устаревший код биржевого календаря, например `MOEX` | `MOEX` | пустое значение заменяется на `MOEX` | Оставлен для совместимости; исторический календарь признаков строится из загруженных дневных свечей. | ### DB diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index f9ecbfc..793c30f 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -251,9 +251,9 @@ func (s *Scheduler) prepareSignals(ctx context.Context, now time.Time) error { if err := s.svc.MarketData.BackfillDaily(ctx, instrumentsList, dailyFrom, tradeDate); err != nil { return err } - tradingDays, err := s.svc.Gateway.GetTradingDays(ctx, s.cfg.TradingCalendarExchange, dailyFrom, tradeDate) + tradingDays, err := s.tradingDaysFromDailyCandles(ctx, instrumentsList, dailyFrom, tradeDate.AddDate(0, 0, -1)) if err != nil { - return fmt.Errorf("load trading calendar %s: %w", s.cfg.TradingCalendarExchange, err) + return err } s.svc.Features = s.svc.Features.WithTradingDays(tradingDays) minuteFrom := s.cfg.EntryWindowStart.On(tradeDate.AddDate(0, 0, -s.cfg.IntervalVolumeLookbackDays), s.cfg.Location) @@ -297,6 +297,33 @@ func (s *Scheduler) prepareSignals(ctx context.Context, now time.Time) error { return s.transitionTo(ctx, domain.StateWaitEntryWindow) } +func (s Scheduler) tradingDaysFromDailyCandles(ctx context.Context, instrumentsList []domain.Instrument, from, to time.Time) ([]time.Time, error) { + if to.Before(from) { + return nil, nil + } + seen := make(map[time.Time]struct{}) + for _, instrument := range instrumentsList { + if !instrument.Enabled || instrument.Quarantine { + continue + } + candles, err := s.svc.Repo.ListDailyCandles(ctx, instrument.InstrumentUID, from, to) + if err != nil { + return nil, fmt.Errorf("load daily candles for trading calendar %s: %w", instrument.Ticker, err) + } + for _, candle := range candles { + seen[tradingDate(candle.TradeDate)] = struct{}{} + } + } + days := make([]time.Time, 0, len(seen)) + for day := range seen { + days = append(days, day) + } + sort.Slice(days, func(i, j int) bool { + return days[i].Before(days[j]) + }) + return days, nil +} + func (s Scheduler) generateInstrumentSignal(ctx context.Context, tradeDate time.Time, openPositionCount int, instrument domain.Instrument) (signalCandidate, error) { book, err := s.svc.MarketData.LatestQuote(ctx, instrument.InstrumentUID, s.cfg.QuoteDepth, s.cfg.MaxQuoteAge) if err != nil {