Столкнулся со следующей ситуацией: у меня есть стратегия, работающая с двумя таймфреймами одного инструмента (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 < 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 < 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);
}
}
Столкнулся со следующей ситуацией: у меня есть стратегия, работающая с двумя таймфреймами одного инструмента (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.