fix(scheduler): derive trading calendar from candles
Deploy / Test, build and deploy (push) Failing after 5m23s

This commit is contained in:
2026-06-16 02:42:42 +04:00
parent 750860ee74
commit 3f7f5ac9cb
3 changed files with 31 additions and 4 deletions
+1 -1
View File
@@ -64,7 +64,7 @@ Common formats:
| `TINVEST_RETRY_COUNT` | `3` | Number of T-Invest SDK attempts. | | `TINVEST_RETRY_COUNT` | `3` | Number of T-Invest SDK attempts. |
| `TINVEST_RETRY_BACKOFF_SEC` | `2` | Initial exponential backoff in seconds. | | `TINVEST_RETRY_BACKOFF_SEC` | `2` | Initial exponential backoff in seconds. |
| `TINVEST_USE_SANDBOX` | `false` | Compatibility guard; valid only with `APP_MODE=sandbox`. | | `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 ### DB
+1 -1
View File
@@ -55,7 +55,7 @@ APP_MODE=backtest go run ./cmd/bot
| `TINVEST_RETRY_COUNT` | целое число попыток | `3` | `<= 0` трактуется как одна попытка | Общее число попыток для SDK-вызовов T-Invest через exponential backoff. Больше значение повышает устойчивость к кратким сбоям, но может дольше задерживать окончательную ошибку. | | `TINVEST_RETRY_COUNT` | целое число попыток | `3` | `<= 0` трактуется как одна попытка | Общее число попыток для SDK-вызовов T-Invest через exponential backoff. Больше значение повышает устойчивость к кратким сбоям, но может дольше задерживать окончательную ошибку. |
| `TINVEST_RETRY_BACKOFF_SEC` | целое число секунд | `2` | рекомендуется `>= 0` | Начальный интервал exponential backoff для SDK-вызовов T-Invest. Больше значение снижает частоту повторов при сбоях, но дольше задерживает окончательную ошибку. | | `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_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 ### DB
+29 -2
View File
@@ -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 { if err := s.svc.MarketData.BackfillDaily(ctx, instrumentsList, dailyFrom, tradeDate); err != nil {
return err 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 { 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) s.svc.Features = s.svc.Features.WithTradingDays(tradingDays)
minuteFrom := s.cfg.EntryWindowStart.On(tradeDate.AddDate(0, 0, -s.cfg.IntervalVolumeLookbackDays), s.cfg.Location) 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) 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) { 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) book, err := s.svc.MarketData.LatestQuote(ctx, instrument.InstrumentUID, s.cfg.QuoteDepth, s.cfg.MaxQuoteAge)
if err != nil { if err != nil {