Skip to content

BaseEmulationConnector._currentTime некорректно обновляется если стратегия работает больше чем с одним таймфреймом #680

@dakuenjery

Description

@dakuenjery

Столкнулся со следующей ситуацией: у меня есть стратегия, работающая с двумя таймфреймами одного инструмента (1d и 5m). И заметил что в логах "залипает" время лога (которое LogMessage.Time). Устанавливается это время из receiver.CurrentTime, где receiver в большинстве случаев - это или коннектор, или стратегия, которая все равно берет время из коннектора.

Выяснил вот что: когда в BaseEmulationConnector прилетают свечи одного таймфрейма - то все хорошо. MarketEmulator для каждой свечи генерирует 3 события, и отправляет в коннектор 3 клона свечи, у которых отличается LocalTime (open, high и low). Таким образом отправляются "минимально необходимые" данные для свечи. Все ок. Свечи идут подряд и время монотонно возрастает.

Но вот что происходит когда стратегия работает с двумя таймфреймами: таймфреймы обрабатываются в порядке убывания. И в итоге MarketEmulator присылает сначала 3 дневки в коннектор, и Low или High свечи будет сильно дальше чем Open, допустим в 20:17. После дневок в коннектор начинают прилетать 5 минутки. Но так как в _currentTime уже 20:17 - то это время не изменится до тех пор, пока минутки до "догонят" это время. Таким образом мы каждый день получаем "псевдорандомное" время (а если быть точнее - то наибольшее время LowTime или HighTime дневки), которое не будет изменяться N свечей 5 минуток.

Вероятно такое поведение неверное, и наверняка не в лучшую сторону сказывается на качестве эмуляции.

Я сделал у себя фикс, и во первых хочу поделится наблюдением, и во вторых - не до конца уверен, что этот фикс полностью корректен (сделал только для свечей, т.к. работаю именно со свечами в данный момент), и думаю разработчики сделают лучше. Мне кажется это поведение стоит править на стороне MarketEmulator.

/// <summary>
/// HistoryEmulationConnector, исправляющий некорректное время логов.
///
/// Проблема: StockSharp MarketEmulator.ProcessCandles генерирует Clone-свечи
/// с State=Active и LocalTime внутри периода свечи (OpenTime/HighTime/LowTime).
/// Clone с HighTime/LowTime может иметь LocalTime больше, чем ServerTime
/// следующей реальной свечи (но другого таймфрейма).
///
/// Решение: буферизуем Clone-свечи (State != Finished), а при получении
/// следующего реального сообщения обрабатываем буфер в порядке возрастания
/// времени, затем — само сообщение. Это гарантирует строгое возрастание
/// времени и корректное поведение ProcessTime в MarketEmulator.
/// </summary>
public class FixedHistoryEmulationConnector : HistoryEmulationConnector {
    private readonly List<Message> _cloneBuffer = new();

    public FixedHistoryEmulationConnector(
        IEnumerable<Security> securities,
        IEnumerable<Portfolio> portfolios,
        IStorageRegistry storage
    ) : base(securities, portfolios, storage) {
    }

    /// <summary>
    /// Алгоритм обработки сообщений:
    /// 1. Clone-свечи (State != Finished) буферизуются в отсортированном порядке
    ///    (вставка через бинарный поиск) — их LocalTime может превышать ServerTime
    ///    следующей реальной свечи, что ломает timeSpan в эмуляторе.
    /// 2. Перед обработкой реального сообщения из буфера извлекаются все свечи
    ///    с LocalTime &lt; currentTime сообщения (уже отсортированы!) и передаются в base.
    /// 3. Свечи с LocalTime >= currentTime остаются в буфере до следующего реального сообщения.
    /// </summary>
    protected override void OnProcessMessage(Message message) {
        if (message is CandleMessage { State: not CandleStates.Finished }) {
            InsertSorted(message);
            return;
        }

        if (_cloneBuffer.Count > 0) {
            var currentTime = message.LocalTime;

            // Бинарный поиск первого элемента с LocalTime >= currentTime
            var splitIndex = BinarySearchFirstGreaterOrEqual(_cloneBuffer, currentTime);

            // Обрабатываем все элементы до splitIndex (они уже отсортированы)
            for (var i = 0; i < splitIndex; i++)
                base.OnProcessMessage(_cloneBuffer[i]);

            // Удаляем обработанные элементы
            _cloneBuffer.RemoveRange(0, splitIndex);
        }

        base.OnProcessMessage(message);
    }

    /// <summary>
    /// Вставка сообщения в буфер с сохранением сортировки по LocalTime.
    /// Использует бинарный поиск O(log n) + вставку O(n).
    /// Для типичного случая (возрастающее время) вставка происходит в конец — O(1).
    /// </summary>
    private void InsertSorted(Message message) {
        var index = _cloneBuffer.BinarySearch(message, LocalTimeComparer.Instance);
        if (index < 0)
            index = ~index;
        _cloneBuffer.Insert(index, message);
    }

    /// <summary>
    /// Бинарный поиск первого элемента с LocalTime >= target.
    /// Возвращает индекс, по которому можно разделить список:
    /// все элементы до индекса имеют LocalTime &lt; target.
    /// </summary>
    private static int BinarySearchFirstGreaterOrEqual(List<Message> list, DateTimeOffset target) {
        int left = 0, right = list.Count;
        while (left < right) {
            int mid = (left + right) / 2;
            if (list[mid].LocalTime < target)
                left = mid + 1;
            else
                right = mid;
        }
        return left;
    }

    protected override void DisposeManaged() {
        _cloneBuffer.Clear();
        base.DisposeManaged();
    }

    private sealed class LocalTimeComparer : IComparer<Message> {
        public static readonly LocalTimeComparer Instance = new();
        public int Compare(Message? x, Message? y) => x!.LocalTime.CompareTo(y!.LocalTime);
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions