//+------------------------------------------------------------------+ //| RenkoDeepSeek2.mq5 | //| Renko с сигналами пробоя последних подтверждённых вершин/впадин. | //| История восстанавливается по тикам; текущие тики обрабатываются | //| без смены периода графика. Цикл начинается в 03:00 по Москве. | //| | //| ЦЕЛЕВАЯ СПЕЦИФИКАЦИЯ ПРИВЯЗОК RENKO (обязательна для реализации): | //| 1. Ценовой Anchor фиксирован: 1.00000 для всех запусков, графиков| //| и экземпляров индикатора. Сетка уровней: 1.00000 + N*Brick, | //| где N — целое число, Brick — размер кирпича в цене. | //| 2. Anchor не устанавливается по первому тику, цене открытия | //| графика, текущей цене или моменту подключения индикатора. | //| 3. Начало/конец временного окна истории не меняют ценовую сетку. | //| Расширение истории и перезапуск должны давать те же уровни | //| Renko при одинаковой последовательности исходных тиков. | //| 4. Время тиков обрабатывается по единой шкале UTC; граница | //| суточного сигнального цикла — 03:00 Europe/Moscow (UTC+3). | //| Граница цикла сбрасывает сигнальные экстремумы, но не Anchor, | //| цену последнего закрытия или направление Renko. | //| 5. Временная история определяется доступными историческими | //| тиками и заданным окном загрузки, а не временем открытия | //| графика/подключения индикатора. Историческое смещение времени | //| сервера брокера должно учитываться для каждого периода; | //| неизвестное смещение/неполная история явно помечаются. | //| 6. Если 1.00000 или шаг Brick несовместимы с tick size символа, | //| не сдвигать Anchor молча и не строить уровни вне сетки тиков: | //| явно сообщить о несовместимости и остановить построение. | //| 7. Хранится и отображается не более InpMaxHistoryBricks | //| последних кирпичей (по умолчанию 400), независимо от Brick. | //| 8. Каждый экземпляр работает в отдельном подокне. Скрытый маркер | //| сохраняет идентичность подокна при изменении его номера; | //| ChartID отделяет копии шаблона на разных графиках. Два | //| экземпляра в одном подокне не запускаются. | //| | //| ВАЖНО: комментарий фиксирует целевое поведение; он сам по себе | //| не означает, что текущая реализация уже соответствует пунктам. | //| | //| Бюджет пересбора срезовый: прерванный по времени пересбор | //| продолжается с того же чанка. Хранение ограничено количеством; | //| фактический вывод дополнительно ограничен rates_total. | //| При появлении нового кирпича новые элементы идут перед старыми. | //+------------------------------------------------------------------+ #property indicator_separate_window #property indicator_plots 1 #property indicator_buffers 5 #property indicator_type1 DRAW_COLOR_CANDLES #property indicator_color1 clrLimeGreen, clrTomato #property indicator_width1 1 #property indicator_label1 "RenkoDeepSeek — кирпичи" #property version "1.44" #property description "Renko по тикам: пробой swing-уровней, раннее предупреждение, структурный SL." #property description "Цикл с 03:00 МСК; история строится чанками по 30 минут." //+------------------------------------------------------------------+ //| КРАТКОЕ ИНЖЕНЕРНОЕ ОПИСАНИЕ ТЕКУЩЕЙ РЕАЛИЗАЦИИ | //| | //| • Источник цены — Bid. Renko строится от фиксированного Anchor | //| 1.00000; несовместимость Anchor/шага с tick size блокирует | //| расчёт, а не исправляется скрытым смещением. Разворот — 2 | //| кирпича. | //| • InpMaxHistoryBricks ограничивает кольцевой буфер последних | //| кирпичей (по умолчанию 400). Это лимит количества, не дней: | //| внутреннее окно истории расширяется, пока цель не достигнута | //| либо не исчерпана доступная история. 400 — верхняя цель, а не | //| гарантия при недостатке тиков/ценового движения. | //| • История запрашивается 30-минутными чанками. Срез ограничен | //| бюджетом времени; checkpoint сохраняет окно и позицию чанка, | //| чтобы следующий вызов продолжил пересборку, а не начал заново. | //| • Граница суточного цикла по умолчанию 03:00 МСК сбрасывает | //| swing/сигнальные состояния, но сохраняет Renko-якорь, последнее| //| закрытие и направление последовательности. | //| • Изоляция экземпляров: каждому подокну соответствует скрытый | //| пустой маркер; ключи настроек/кэша используют ChartID и ID | //| маркера, а не изменяемый номер подокна. Два экземпляра | //| RenkoDeepSeek в одном подокне запрещены. Маркер сохраняется| //| при снятии индикатора для повторного подключения. | //| • Пробой подтверждается закрытием кирпича за swing-уровнем. | //| Раннее предупреждение рассчитывается по текущему Bid; | //| структурный SL — информационный. Торговые ордера не размещаются.| //| | //| ИЗВЕСТНЫЕ ГРАНИЦЫ: нет независимой Renko-оси; вывод ограничен | //| rates_total. При смене ChartID настройки/кэш могут потребовать | //| повторного создания; старый кэш после перехода на маркерную | //| идентичность не мигрируется и строится заново. | //| Исторические серверные UTC-смещения не восстановлены по периодам;| //| используется текущее смещение. Частичная история подавляет живые | //| оповещения. Описание отражает статический код, не runtime-тест. | //+------------------------------------------------------------------+ //--- Настройки индикатора: размер кирпича, история и цвета. input int InpBrickPoints = 10; // Размер кирпича Renko в пунктах (значение по умолчанию: 600) input int InpCycleStartHour = 3; // Час начала суточного цикла по Москве, 0–23 input int InpCycleStartMinute = 0; // Минута начала суточного цикла по Москве, 0–59 input string InpInstanceId = ""; // Устаревший параметр для миграции прежних настроек; вручную задавать не требуется input int InpMaxHistoryBricks = 400; // Максимум последних кирпичей в истории; 1–200000, независимо от размера кирпича input color InpUpColor = clrLimeGreen; // Цвет восходящих кирпичей input color InpDownColor = clrTomato; // Цвет нисходящих кирпичей input color InpButtonActiveColor = clrForestGreen; // Цвет активных кнопок input color InpButtonInactiveColor = clrFireBrick; // Цвет неактивных кнопок input bool InpBreakoutAlerts = false; // Стартовый мастер-флаг: включить все категории оповещений input bool InpBreakoutHighAlerts = false; // Оповещать о пробое подтверждённых вершин input bool InpBreakoutLowAlerts = false; // Оповещать о пробое подтверждённых впадин input bool InpBrickReversalAlerts = false; // Оповещать о развороте направления кирпичей input bool InpPreBreakoutBuyAlerts = true; // Раннее предупреждение BUY у вершины input bool InpPreBreakoutSellAlerts = true; // Раннее предупреждение SELL у впадины input double InpPreBreakoutDistanceBricks = 0.5; // Порог предупреждения в долях кирпича input int InpPreBreakoutDistancePoints = 0; // Дополнительный порог в пунктах; 0 — отключён input int InpStopLossBufferPoints = 10; // Буфер структурного SL в пунктах input bool InpPreBreakoutPushNotifications = false; // Push для ранних сигналов //--- Входы добавлены в конец списка, чтобы не смещать существующие параметры в .set-файлах. input bool InpPushNotifications = true; // Push для оповещений о пробоях и разворотах input bool InpPreferSavedBrickSize = true; // Устаревший параметр совместимости; сохранённый размер всегда имеет приоритет input bool InpPopupAlerts = true; // Показывать модальное окно Alert (выключите при плотных сигналах) //--- Внутренние ограничения восстановления истории; пользователю менять их не требуется. const int INITIAL_HISTORY_WINDOW_DAYS = 1; // Начальное внутреннее окно запроса; расширяется до лимита кирпичей const int PANEL_UPDATE_INTERVAL_MS = 200; // Троттлинг панели: не чаще 5 обновлений в секунду const long MOSCOW_UTC_OFFSET_SECONDS = 3 * 3600; // Москва не использует сезонный перевод часов const double RENKO_PRICE_ANCHOR = 1.00000; // Фиксированный ценовой Anchor Renko // Почанковая загрузка тиков: один запрос на сутки стабильно упирается // в тайм-аут 4403, поэтому история читается короткими интервалами. const int HISTORY_CHUNK_SECONDS = 30 * 60; // размер чанка: 30 минут const int HISTORY_CHUNK_RETRIES = 1; // одна повторная попытка; после ошибки фиксируем разрыв и продолжаем историю const int HISTORY_PROBE_SECONDS = 15 * 60; // пробный запрос свежих тиков const int TICK_BASE_PROBE_SECONDS = 3600; // пробный запрос у самой старой границы const ulong HISTORY_BUILD_BUDGET_MS = 3000; // бюджет времени на один срез пересбора (защита UI) const int HISTORY_RESUME_DELAY_MS = 2000; // пауза между срезами продолжения пересбора const int GAP_FILL_MAX_ATTEMPTS = 5; // попыток дозагрузки пропусков const ulong GAP_FILL_INTERVAL_MS = 60000; // интервал между попытками дозагрузки // Пустой ответ CopyTicksRange нельзя считать подтверждённым пропуском: // он означает и «сделок не было», и «участок базы ещё не выгружен». const long EMPTY_RANGE_CONFIRM_MS = 3600 * 1000; // доверяем пустому ответу только для давних участков const long TICK_LIVE_ALERT_WINDOW_MS = 5000; // оповещения разрешены только по «живым» тикам const int MAX_HISTORY_BRICKS_ALLOWED = 200000; // верхний технический предел настраиваемой истории //--- Структура данных одного кирпича Renko. struct RenkoBrick { datetime time; double open; double high; double low; double close; int direction; }; //--- Состояние расчёта, параметры экземпляра и буферы отображения. int g_current_brick_points = 10; double g_brick_size = 0.0; double g_price_step = 0.0; // только подтверждённый SYMBOL_TRADE_TICK_SIZE double g_last_close = 0.0; double g_last_bid = 0.0; datetime g_cycle_start = 0; int g_direction = 0; // 1 — вверх, -1 — вниз, 0 — направление не определено long g_last_tick_msc = 0; int g_ticks_processed_at_last_msc = 0; datetime g_last_tick_copy_warning = 0; bool g_have_anchor = false; bool g_anchor_init_warned = false; bool g_price_grid_valid = false; bool g_history_initialized = false; bool g_cache_loaded = false; bool g_cache_dirty = false; // кэш требует периодического сохранения bool g_cache_timer_enabled = false; bool g_settings_initialized = false; bool g_instance_identity_ready = false; ulong g_last_history_attempt_ms = 0; int g_history_retry_count = 0; bool g_alert_enabled = false; bool g_breakout_high_enabled = false; bool g_breakout_low_enabled = false; bool g_brick_reversal_enabled = false; bool g_pre_buy_enabled = true; bool g_pre_sell_enabled = true; string g_object_prefix = ""; int g_control_y_offset = 0; string g_instance_id = ""; string g_instance_marker_id = ""; string g_instance_marker_name = ""; string g_indicator_short_name = ""; int g_instance_window = -1; string g_alert_button_name = ""; string g_breakout_high_button_name = ""; string g_breakout_low_button_name = ""; string g_brick_reversal_button_name = ""; string g_pre_buy_button_name = ""; string g_pre_sell_button_name = ""; string g_brick_edit_name = ""; string g_brick_apply_name = ""; double g_open_buffer[]; double g_high_buffer[]; double g_low_buffer[]; double g_close_buffer[]; double g_color_buffer[]; RenkoBrick g_bricks[]; int g_brick_count = 0; int g_brick_start = 0; // физический индекс самого старого кирпича в кольцевом буфере int g_last_rendered_count = 0; int g_last_rendered_rates_total = -1; bool g_series_dirty = true; // серия изменилась — нужна перерисовка буферов double g_last_swing_high = 0.0; // Последняя подтверждённая вершина Renko double g_last_swing_low = 0.0; // Последняя подтверждённая впадина Renko bool g_swing_high_breakout_alerted = false; // событие пробоя зафиксировано (для панели) bool g_swing_low_breakout_alerted = false; bool g_swing_high_alert_sent = false; // оповещение по этому событию уже отправлено bool g_swing_low_alert_sent = false; bool g_pre_buy_alerted = false, g_pre_sell_alerted = false; bool g_pre_buy_cancelled = false, g_pre_sell_cancelled = false; string g_pre_status_label_name = "", g_pre_detail_label_name = "", g_pre_sl_label_name = ""; //--- Служебное состояние. long g_server_to_utc = 0; // смещение серверного времени относительно UTC bool g_replaying = false; // признак восстановления истории: без UI и журнала ulong g_last_panel_update_ms = 0; // троттлинг обновления панели bool g_history_partial = false; // история загружена не полностью int g_gap_fill_attempts = 0; ulong g_last_gap_fill_ms = 0; bool g_gap_fill_exhausted_reported = false; // сообщение об исчерпании попыток уже выдано bool g_rebuild_running = false; // защита от вложенного пересбора истории bool g_step_warned = false; // предупреждение о сетке цены уже выдано datetime g_rebuild_resume_from = 0; // пересбор продолжается с этого чанка int g_rebuild_resume_window_days = 0; // размер окна, к которому относится продолжение bool g_history_needs_more = false; // пересбор прерван по бюджету и требует продолжения bool g_rebuild_partial_accum = false; // в текущем проходе были незагруженные чанки long g_diag_valid_bid_ticks = 0; double g_diag_min_bid = 0.0; double g_diag_max_bid = 0.0; double g_diag_first_bid = 0.0; double g_diag_last_bid = 0.0; //--- Снимок серии: неудачный пересбор не должен стирать показанные кирпичи. RenkoBrick g_snap_bricks[]; int g_snap_count = 0; datetime g_snap_cycle_start = 0; int g_snap_direction = 0; long g_snap_last_tick_msc = 0; int g_snap_ticks_at_msc = 0; bool g_snap_have_anchor = false; bool g_snap_history_initialized = false; bool g_snap_partial = false; datetime g_snap_rebuild_resume_from = 0; int g_snap_rebuild_resume_window_days = 0; bool g_snap_history_needs_more = false; bool g_snap_rebuild_partial_accum = false; double g_snap_last_close = 0.0; double g_snap_last_bid = 0.0; double g_snap_swing_high = 0.0; double g_snap_swing_low = 0.0; bool g_snap_high_alerted = false; bool g_snap_low_alerted = false; bool g_snap_high_sent = false; bool g_snap_low_sent = false; bool g_snap_pre_buy = false; bool g_snap_pre_sell = false; bool g_snap_buy_cancelled = false; bool g_snap_sell_cancelled = false; bool g_snap_valid = false; int BrickPhysicalIndex(const int logical_index) { if(InpMaxHistoryBricks <= 0) return logical_index; return (g_brick_start + logical_index) % InpMaxHistoryBricks; } // Нулевая серия может быть корректным состоянием: Anchor и курсор уже есть, // но цена ещё не прошла расстояние одного кирпича. В таком случае можно // продолжать с живых тиков, не повторяя одну и ту же полную историю. bool HasUsableRenkoState() { if(g_brick_count > 0) return true; return g_have_anchor && g_cycle_start > 0 && g_last_tick_msc > 0 && g_last_close > 0.0 && g_last_bid > 0.0 && !g_history_partial; } // Получает самую раннюю известную дату истории символа. Это граница // доступности данных, а не пользовательский лимит истории. bool GetEarliestAvailableHistoryDate(datetime &first_date) { long value = 0; ResetLastError(); if(SeriesInfoInteger(_Symbol, PERIOD_CURRENT, SERIES_SERVER_FIRSTDATE, value) && value > 0) { first_date = (datetime)value; return true; } const int server_error = GetLastError(); value = 0; ResetLastError(); if(SeriesInfoInteger(_Symbol, PERIOD_CURRENT, SERIES_TERMINAL_FIRSTDATE, value) && value > 0) { first_date = (datetime)value; return true; } PrintFormat("RenkoDeepSeek: не удалось определить самую раннюю доступную дату истории символа (ошибки SeriesInfoInteger: server=%d, terminal=%d). Расширение истории заблокировано до получения границы данных.", server_error, GetLastError()); return false; } // Максимальное окно — только техническая граница реально доступной истории. // Целевой объём задаётся исключительно InpMaxHistoryBricks, а не днями. int MaximumHistoryWindowDaysAvailable(const datetime cycle_start_server) { datetime first_date = 0; if(!GetEarliestAvailableHistoryDate(first_date)) return 0; if(first_date > cycle_start_server) return 1; const long span_seconds = (long)cycle_start_server - (long)first_date; long days = (span_seconds + 86399) / 86400 + 1; if(days < 1) days = 1; if(days > 2147483647) days = 2147483647; return (int)days; } // Пересчитывает смещение серверного времени относительно UTC. // TimeTradeServer() не зависит от наличия свежих тиков, поэтому на выходных // и на только что открытом графике смещение не «уплывает» на часы. void RefreshServerOffset() { const datetime trade_server = TimeTradeServer(); const datetime server_now = (trade_server > 0 ? trade_server : TimeCurrent()); if(server_now <= 0) return; // нет соединения: сохраняем последнее известное смещение g_server_to_utc = (long)(TimeGMT() - server_now); } // Переводит время тика сервера в московское «настенное» время (UTC+3). datetime ServerTimeToMoscow(datetime server_time) { return (datetime)((long)server_time + g_server_to_utc + MOSCOW_UTC_OFFSET_SECONDS); } // Обратное преобразование: значение из CycleStart() → метка времени сервера. datetime MoscowToServerTime(datetime moscow_time) { return (datetime)((long)moscow_time - MOSCOW_UTC_OFFSET_SECONDS - g_server_to_utc); } // Возвращает границу суточного цикла по московскому календарю. datetime CycleStart(datetime server_time) { datetime moscow_time = ServerTimeToMoscow(server_time); MqlDateTime dt; if(!TimeToStruct(moscow_time, dt)) return 0; if(dt.hour < InpCycleStartHour || (dt.hour == InpCycleStartHour && dt.min < InpCycleStartMinute)) moscow_time -= 86400; if(!TimeToStruct(moscow_time, dt)) return 0; dt.hour = InpCycleStartHour; dt.min = InpCycleStartMinute; dt.sec = 0; return StructToTime(dt); } // Убирает только двоичную погрешность уже проверенной tick grid. // Размер кирпича и Anchor эта функция не корректирует: несовместимость блокируется отдельно. double SnapToTick(const double price) { if(g_price_step <= 0.0 || !MathIsValidNumber(g_price_step)) return 0.0; return NormalizeDouble(MathRound(price / g_price_step) * g_price_step, _Digits); } // Проверяет, что фиксированный Anchor и размер кирпича принадлежат сетке тиков. bool IsRenkoGridCompatible(const int brick_points, const bool report) { if(g_price_step <= 0.0 || !MathIsValidNumber(g_price_step) || _Point <= 0.0 || brick_points < 1) { if(report) Print("RenkoDeepSeek: невозможно проверить сетку Renko: неверный tick size, _Point или размер кирпича."); return false; } const double anchor_ticks = RENKO_PRICE_ANCHOR / g_price_step; const double brick_size = (double)brick_points * _Point; const double brick_ticks = brick_size / g_price_step; const double anchor_tolerance = 1e-6; const double brick_tolerance = 1e-6; if(MathAbs(anchor_ticks - MathRound(anchor_ticks)) > anchor_tolerance) { if(report) PrintFormat("RenkoDeepSeek: фиксированный Anchor %.5f несовместим с tick size %.10f символа %s; построение остановлено.", RENKO_PRICE_ANCHOR, g_price_step, _Symbol); return false; } if(brick_ticks < 1.0 || MathAbs(brick_ticks - MathRound(brick_ticks)) > brick_tolerance) { if(report) PrintFormat("RenkoDeepSeek: Brick %.10f (%d пунктов) несовместим с tick size %.10f символа %s; выберите размер, кратный шагу тика.", brick_size, brick_points, g_price_step, _Symbol); return false; } return true; } // Первое закрытие привязывается к сетке от Anchor через floor. // Цены переводятся в целые tick units, чтобы не накапливать двоичную погрешность. bool CalculateInitialCloseFromBid(const double bid, double &initial_close) { initial_close = 0.0; if(!MathIsValidNumber(bid) || bid <= 0.0 || !MathIsValidNumber(g_price_step) || g_price_step <= 0.0 || !MathIsValidNumber(g_brick_size) || g_brick_size <= 0.0) return false; const double max_exact_ticks = 9007199254740991.0; const double bid_ticks_value = bid / g_price_step; const double anchor_ticks_value = RENKO_PRICE_ANCHOR / g_price_step; const double brick_ticks_value = g_brick_size / g_price_step; if(!MathIsValidNumber(bid_ticks_value) || !MathIsValidNumber(anchor_ticks_value) || !MathIsValidNumber(brick_ticks_value) || MathAbs(bid_ticks_value) > max_exact_ticks || MathAbs(anchor_ticks_value) > max_exact_ticks || brick_ticks_value < 1.0 || brick_ticks_value > max_exact_ticks) return false; const long bid_ticks = (long)MathRound(bid_ticks_value); const long anchor_ticks = (long)MathRound(anchor_ticks_value); const long brick_ticks = (long)MathRound(brick_ticks_value); if(brick_ticks < 1 || MathAbs(bid - (double)bid_ticks * g_price_step) > g_price_step * 1e-6) return false; const long delta_ticks = bid_ticks - anchor_ticks; const double brick_count = MathFloor((double)delta_ticks / (double)brick_ticks); const double close_ticks_value = (double)anchor_ticks + brick_count * (double)brick_ticks; if(!MathIsValidNumber(close_ticks_value) || MathAbs(close_ticks_value) > max_exact_ticks) return false; const long close_ticks = (long)close_ticks_value; initial_close = NormalizeDouble((double)close_ticks * g_price_step, _Digits); const double tolerance = g_price_step * 1e-6; if(!MathIsValidNumber(initial_close) || initial_close > bid + tolerance || bid - initial_close >= g_brick_size + tolerance) { initial_close = 0.0; return false; } return true; } // Шаг одного кирпича от текущего закрытия. bool BrickStep(const bool up, const double from_close, double &to_close) { const double raw = from_close + (up ? g_brick_size : -g_brick_size); double snapped = SnapToTick(raw); if(up ? (snapped <= from_close) : (snapped >= from_close)) return false; to_close = snapped; return true; } //--- Полный сброс серии при первичной инициализации/восстановлении. void ResetCycle(datetime cycle_start) { g_cycle_start = cycle_start; g_last_close = 0.0; g_last_bid = 0.0; g_direction = 0; g_have_anchor = false; g_anchor_init_warned = false; g_last_swing_high = 0.0; g_last_swing_low = 0.0; g_swing_high_breakout_alerted = false; g_swing_low_breakout_alerted = false; g_swing_high_alert_sent = false; g_swing_low_alert_sent = false; g_pre_buy_alerted = false; g_pre_sell_alerted = false; g_pre_buy_cancelled = false; g_pre_sell_cancelled = false; } //--- Новые торговые сутки сбрасывают сигнальные экстремумы, но не Renko-цену. // Это позволяет строить единую историческую последовательность через границы суток. void ResetCycleSignals(const datetime cycle_start) { g_cycle_start = cycle_start; g_last_swing_high = 0.0; g_last_swing_low = 0.0; g_swing_high_breakout_alerted = false; g_swing_low_breakout_alerted = false; g_swing_high_alert_sent = false; g_swing_low_alert_sent = false; g_pre_buy_alerted = false; g_pre_sell_alerted = false; g_pre_buy_cancelled = false; g_pre_sell_cancelled = false; } //--- Короткое имя индикатора с указанием, что время цикла задано по Москве. string BuildShortName() { return StringFormat("RenkoDeepSeek %d пунктов / цикл %02d:%02d МСК [%s|W%d]", g_current_brick_points, InpCycleStartHour, InpCycleStartMinute, g_instance_id, g_instance_window); } //--- Добавление построенного кирпича во внутреннюю последовательность. bool DrawBrick(datetime tick_time, double open_price, double close_price, int direction) { // Буфер заранее выделен в OnInit; кольцевая запись исключает сдвиг // до 200 000 элементов при каждом новом кирпиче. bool confirmed_high = false; bool confirmed_low = false; double swing_high = 0.0; double swing_low = 0.0; if(g_brick_count > 0) { const RenkoBrick previous = g_bricks[BrickPhysicalIndex(g_brick_count - 1)]; if(previous.direction > 0 && direction < 0) { confirmed_high = true; swing_high = previous.high; } else if(previous.direction < 0 && direction > 0) { confirmed_low = true; swing_low = previous.low; } } // Кирпич приводится к сетке цены инструмента: иначе значения в буферах, // уровнях и кэше расходились бы на ~1e-16 и сравнение цен было недетерминированным. const double brick_open = SnapToTick(open_price); const double brick_close = SnapToTick(close_price); int physical_index = 0; if(g_brick_count < InpMaxHistoryBricks) physical_index = BrickPhysicalIndex(g_brick_count); else { physical_index = g_brick_start; g_brick_start = (g_brick_start + 1) % InpMaxHistoryBricks; } g_bricks[physical_index].time = tick_time; g_bricks[physical_index].open = brick_open; g_bricks[physical_index].high = MathMax(brick_open, brick_close); g_bricks[physical_index].low = MathMin(brick_open, brick_close); g_bricks[physical_index].close = brick_close; g_bricks[physical_index].direction = direction; if(g_brick_count < InpMaxHistoryBricks) ++g_brick_count; g_series_dirty = true; g_cache_dirty = true; if(confirmed_high) { g_last_swing_high = swing_high; g_swing_high_breakout_alerted = false; g_swing_high_alert_sent = false; g_pre_buy_alerted = false; g_pre_buy_cancelled = false; } else if(confirmed_low) { g_last_swing_low = swing_low; g_swing_low_breakout_alerted = false; g_swing_low_alert_sent = false; g_pre_sell_alerted = false; g_pre_sell_cancelled = false; } return true; } //--- Перенос последовательности Renko в буферы индикатора для отображения. // force=false — перерисовка выполняется только при изменении серии или rates_total. void RenderBricks(const int rates_total, const bool force) { if(!force && !g_series_dirty && rates_total == g_last_rendered_rates_total) return; int count = g_brick_count; if(count > rates_total) count = rates_total; int clear_count = (g_last_rendered_rates_total != rates_total ? rates_total : (g_last_rendered_count > count ? g_last_rendered_count : count)); if(clear_count > rates_total) clear_count = rates_total; for(int i = 0; i < clear_count; ++i) { g_open_buffer[i] = EMPTY_VALUE; g_high_buffer[i] = EMPTY_VALUE; g_low_buffer[i] = EMPTY_VALUE; g_close_buffer[i] = EMPTY_VALUE; g_color_buffer[i] = 0.0; } // Самый новый кирпич — в буфер с индексом 0, более старые — следом. for(int i = 0; i < count; ++i) { const int source = BrickPhysicalIndex(g_brick_count - 1 - i); g_open_buffer[i] = g_bricks[source].open; g_high_buffer[i] = g_bricks[source].high; g_low_buffer[i] = g_bricks[source].low; g_close_buffer[i] = g_bricks[source].close; g_color_buffer[i] = (g_bricks[source].direction > 0 ? 0.0 : 1.0); } g_last_rendered_count = count; g_last_rendered_rates_total = rates_total; g_series_dirty = false; } //--- Вывод локального и push-оповещения одним и тем же сообщением. void NotifyUser(const string message, const bool send_push) { Print(message); if(InpPopupAlerts) Alert(message); if(!send_push) return; ResetLastError(); if(!SendNotification(message)) Print("RenkoDeepSeek: SendNotification не отправлено; код ошибки=", GetLastError(), ". Проверьте настройки уведомлений MetaTrader 5."); } //--- Подбор ширины кнопки по длине отображаемого текста. void SetButtonTextFit(const string button_name, const string button_text, const int font_size) { if(button_name == "" || ObjectFind(0, button_name) < 0) return; ObjectSetInteger(0, button_name, OBJPROP_FONTSIZE, font_size); ObjectSetString(0, button_name, OBJPROP_TEXT, button_text); const int text_length = StringLen(button_text); const int button_width = (int)MathCeil((double)text_length * font_size * 0.72) + 18; ObjectSetInteger(0, button_name, OBJPROP_XSIZE, button_width); } // Ограничивает подпись доступной шириной графика, чтобы длинные строки // не выходили за правую границу и не накладывались на другие элементы. string FitPrePanelText(const string text) { const long chart_width = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0); int max_chars = (int)((chart_width - 20) / 6); if(max_chars < 12) max_chars = 12; if(StringLen(text) <= max_chars) return text; if(max_chars <= 3) return StringSubstr(text, 0, max_chars); return StringSubstr(text, 0, max_chars - 3) + "..."; } bool CreatePrePanelLabel(const string name,const int window,const int y) { if(ObjectFind(0,name)<0 && !ObjectCreate(0,name,OBJ_LABEL,window,0,0)) return false; ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER); ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER); ObjectSetInteger(0,name,OBJPROP_XDISTANCE,8); ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y); ObjectSetInteger(0,name,OBJPROP_FONTSIZE,8); ObjectSetInteger(0,name,OBJPROP_COLOR,clrSilver); ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false); ObjectSetInteger(0,name,OBJPROP_HIDDEN,true); ObjectSetString(0,name,OBJPROP_TEXT,""); return true; } // Расстояние от уровня входа до структурного SL в пунктах. string SlDistanceText(const double level, const double sl, const bool valid) { if(!valid) return "—"; return DoubleToString(MathAbs(level - sl) / _Point, 1); } // force=true — обновить немедленно (кнопки, создание панели); // force=false — с троттлингом (Bid меняется на каждом тике). void UpdatePreBreakoutPanel(const double bid,const bool force) { if(g_pre_status_label_name=="" || ObjectFind(0,g_pre_status_label_name)<0 || g_pre_detail_label_name=="" || ObjectFind(0,g_pre_detail_label_name)<0 || g_pre_sl_label_name=="" || ObjectFind(0,g_pre_sl_label_name)<0) return; if(!force) { const ulong now_ms=GetTickCount64(); if(g_last_panel_update_ms!=0 && now_ms-g_last_panel_update_ms<(ulong)PANEL_UPDATE_INTERVAL_MS) return; g_last_panel_update_ms=now_ms; } const string bid_text=DoubleToString(bid,_Digits); const double buy=(g_last_swing_high>0.0 ? NormalizeDouble(g_last_swing_high+g_brick_size,_Digits) : 0.0); const double sell=(g_last_swing_low>0.0 ? NormalizeDouble(g_last_swing_low-g_brick_size,_Digits) : 0.0); const double bsl=(g_last_swing_low>0.0 ? NormalizeDouble(g_last_swing_low-InpStopLossBufferPoints*_Point,_Digits) : 0.0); const double ssl=(g_last_swing_high>0.0 ? NormalizeDouble(g_last_swing_high+InpStopLossBufferPoints*_Point,_Digits) : 0.0); const double bd=(buy>0.0 ? MathAbs(buy-bid)/_Point : 0.0); const double sd=(sell>0.0 ? MathAbs(bid-sell)/_Point : 0.0); const double threshold=MathMax(g_brick_size*InpPreBreakoutDistanceBricks, InpPreBreakoutDistancePoints*_Point); const bool bv=(buy>0.0 && g_last_swing_low>0.0 && bsl0.0 && g_last_swing_high>0.0 && ssl>sell); const bool bn=(g_pre_buy_enabled && !g_swing_high_breakout_alerted && g_direction>0 && buy>bid && bd*_Point<=threshold); const bool sn=(g_pre_sell_enabled && !g_swing_low_breakout_alerted && g_direction<0 && sell0.0) { status="ПРОБОЙ вершины произошёл"; status_color=clrLimeGreen; detail=StringFormat("%s | Bid=%s | уровень BUY=%s",_Symbol,bid_text, DoubleToString(buy,_Digits)); slinfo=(bv ? StringFormat("SL BUY=%s | от уровня входа %.1f пунктов", DoubleToString(bsl,_Digits),MathAbs(buy-bsl)/_Point) : "SL BUY: недостаточно swing-данных"); } else if(g_swing_low_breakout_alerted && sell>0.0) { status="ПРОБОЙ впадины произошёл"; status_color=clrLimeGreen; detail=StringFormat("%s | Bid=%s | уровень SELL=%s",_Symbol,bid_text, DoubleToString(sell,_Digits)); slinfo=(sv ? StringFormat("SL SELL=%s | от уровня входа %.1f пунктов", DoubleToString(ssl,_Digits),MathAbs(ssl-sell)/_Point) : "SL SELL: недостаточно swing-данных"); } else if(bn) { status="BUY: цена приближается к пробою вершины"; status_color=clrDarkOrange; detail=StringFormat("%s | Bid=%s | уровень=%s | до уровня %.1f пунктов", _Symbol,bid_text,DoubleToString(buy,_Digits),bd); slinfo=(bv ? StringFormat("Предлагаемый SL BUY=%s | риск %.1f пунктов", DoubleToString(bsl,_Digits),MathAbs(buy-bsl)/_Point) : "SL BUY: нет корректной подтверждённой впадины"); } else if(sn) { status="SELL: цена приближается к пробою впадины"; status_color=clrDarkOrange; detail=StringFormat("%s | Bid=%s | уровень=%s | до уровня %.1f пунктов", _Symbol,bid_text,DoubleToString(sell,_Digits),sd); slinfo=(sv ? StringFormat("Предлагаемый SL SELL=%s | риск %.1f пунктов", DoubleToString(ssl,_Digits),MathAbs(ssl-sell)/_Point) : "SL SELL: нет корректной подтверждённой вершины"); } else if(g_pre_buy_cancelled || g_pre_sell_cancelled) { status="Предварительный сигнал отменён: цена отошла"; status_color=clrTomato; } else if(buy<=0.0 && sell<=0.0) { status="Ожидание: нет подтверждённых swing-уровней"; } else { detail=StringFormat("%s | Bid=%s | BUY=%s (%.1f п.) | SELL=%s (%.1f п.)",_Symbol,bid_text, (buy>0.0 ? DoubleToString(buy,_Digits) : "нет"),bd, (sell>0.0 ? DoubleToString(sell,_Digits) : "нет"),sd); slinfo="SL: противоположный подтверждённый swing + буфер"; } ObjectSetString(0,g_pre_status_label_name,OBJPROP_TEXT,FitPrePanelText(status)); ObjectSetInteger(0,g_pre_status_label_name,OBJPROP_COLOR,status_color); ObjectSetString(0,g_pre_detail_label_name,OBJPROP_TEXT,FitPrePanelText(detail)); ObjectSetString(0,g_pre_sl_label_name,OBJPROP_TEXT,FitPrePanelText(slinfo)); } void EvaluatePreBreakoutSignals(const double bid,const bool enable_alert) { if(bid<=0.0 || g_brick_size<=0.0) return; const bool push=(InpPreBreakoutPushNotifications && InpPushNotifications); const double threshold=MathMax(g_brick_size*InpPreBreakoutDistanceBricks, InpPreBreakoutDistancePoints*_Point); const double rearm=threshold+g_brick_size*0.25; if(g_last_swing_high>0.0) { const double level=NormalizeDouble(g_last_swing_high+g_brick_size,_Digits); const double dist=(level-bid)/_Point; if(g_pre_buy_alerted && bidrearm) { g_pre_buy_alerted=false; g_pre_buy_cancelled=true; } const bool near=(g_pre_buy_enabled && !g_swing_high_breakout_alerted && g_direction>0 && bid=0.0 && dist*_Point<=threshold); if(near) { g_pre_buy_cancelled=false; if(enable_alert && g_alert_enabled && !g_pre_buy_alerted) { const double sl=(g_last_swing_low>0.0 ? NormalizeDouble(g_last_swing_low-InpStopLossBufferPoints*_Point,_Digits) : 0.0); const bool valid=(sl>0.0 && sl0.0) { const double level=NormalizeDouble(g_last_swing_low-g_brick_size,_Digits); const double dist=(bid-level)/_Point; if(g_pre_sell_alerted && bid>level && dist*_Point>rearm) { g_pre_sell_alerted=false; g_pre_sell_cancelled=true; } const bool near=(g_pre_sell_enabled && !g_swing_low_breakout_alerted && g_direction<0 && bid>level && dist>=0.0 && dist*_Point<=threshold); if(near) { g_pre_sell_cancelled=false; if(enable_alert && g_alert_enabled && !g_pre_sell_alerted) { const double sl=(g_last_swing_high>0.0 ? NormalizeDouble(g_last_swing_high+InpStopLossBufferPoints*_Point,_Digits) : 0.0); const bool valid=(sl>level); g_pre_sell_alerted=true; NotifyUser(StringFormat("RenkoDeepSeek: ПРЕДУПРЕЖДЕНИЕ ДО ПРОБОЯ; SELL; %s; Bid=%s; уровень=%s; расстояние=%.1f пунктов; SL=%s; SL от уровня входа=%s пунктов. Пробой не подтверждён, вход ручной.", _Symbol,DoubleToString(bid,_Digits), DoubleToString(level,_Digits),dist, (valid ? DoubleToString(sl,_Digits) : "НЕТ ДАННЫХ"), SlDistanceText(level,sl,valid)),push); } } } else { g_pre_sell_alerted=false; g_pre_sell_cancelled=false; } // Панель не трогаем во время восстановления истории. if(!g_replaying) UpdatePreBreakoutPanel(bid,false); } //--- Пробой подтверждается только закрытием уже созданного кирпича за Swing-уровнем. void EvaluateConfirmedBreakout(const double brick_close, const bool enable_alert) { if(!MathIsValidNumber(brick_close)) return; if(g_last_swing_high > 0.0 && !g_swing_high_breakout_alerted && brick_close > g_last_swing_high) { g_swing_high_breakout_alerted = true; if(!g_replaying) Print("RenkoDeepSeek: подтверждённый пробой Swing High; вершина=", DoubleToString(g_last_swing_high, _Digits), "; закрытие кирпича=", DoubleToString(brick_close, _Digits), "."); if(enable_alert && g_alert_enabled && g_breakout_high_enabled && !g_swing_high_alert_sent) { g_swing_high_alert_sent = true; NotifyUser(StringFormat("RenkoDeepSeek: ПОДТВЕРЖДЁННЫЙ ПРОБОЙ SWING HIGH %s; Swing High=%s; закрытие Renko-кирпича=%s; размер=%d пунктов.", _Symbol, DoubleToString(g_last_swing_high, _Digits), DoubleToString(brick_close, _Digits), g_current_brick_points), InpPushNotifications); } } if(g_last_swing_low > 0.0 && !g_swing_low_breakout_alerted && brick_close < g_last_swing_low) { g_swing_low_breakout_alerted = true; if(!g_replaying) Print("RenkoDeepSeek: подтверждённый пробой Swing Low; впадина=", DoubleToString(g_last_swing_low, _Digits), "; закрытие кирпича=", DoubleToString(brick_close, _Digits), "."); if(enable_alert && g_alert_enabled && g_breakout_low_enabled && !g_swing_low_alert_sent) { g_swing_low_alert_sent = true; NotifyUser(StringFormat("RenkoDeepSeek: ПОДТВЕРЖДЁННЫЙ ПРОБОЙ SWING LOW %s; Swing Low=%s; закрытие Renko-кирпича=%s; размер=%d пунктов.", _Symbol, DoubleToString(g_last_swing_low, _Digits), DoubleToString(brick_close, _Digits), g_current_brick_points), InpPushNotifications); } } } //--- Обработка Bid: построение кирпичей и подтверждение пробоя закрытым кирпичом. void ProcessBid(datetime tick_time, double bid, bool enable_alert) { if(bid <= 0.0 || g_brick_size <= 0.0) return; // Пропуски в исторических диапазонах искажают swing-структуру: живые // оповещения подавляются, пока полная пересборка не подтвердит структуру. if(g_history_partial) enable_alert = false; const datetime tick_cycle = CycleStart(tick_time); if(tick_cycle == 0) return; if(tick_cycle != g_cycle_start) { if(!g_replaying && g_cycle_start != 0) Print("RenkoDeepSeek: начало нового суточного цикла; граница МСК=", TimeToString(MoscowToServerTime(tick_cycle), TIME_DATE|TIME_MINUTES), " по времени сервера; сигнальные экстремумы сброшены, Renko-последовательность продолжается."); if(g_cycle_start != 0 && g_have_anchor) { // Не сбрасываем last_close/direction/anchor: иначе при крупном кирпиче // каждые сутки цена заново становится якорем и история может остаться пустой. ResetCycleSignals(tick_cycle); g_last_bid = bid; // не проверяем пробой сигнала через ночной разрыв } else ResetCycle(tick_cycle); } // Первый тик после полной инициализации задаёт якорь; на границе суток // существующий Renko-якорь сохраняется, чтобы история оставалась непрерывной. if(!g_have_anchor) { if(!CalculateInitialCloseFromBid(bid, g_last_close)) { if(!g_anchor_init_warned) { Print("RenkoDeepSeek: первый Bid не удалось корректно привязать к ценовой сетке Anchor/tick size; инициализация Renko отложена."); g_anchor_init_warned = true; } return; } g_anchor_init_warned = false; g_last_bid = bid; g_have_anchor = true; if(!g_replaying) UpdatePreBreakoutPanel(bid,false); } int safety = 0; bool changed = true; bool step_failed = false; double next_close = 0.0; double brick_open = 0.0; while(changed && safety++ < 10000) { changed = false; if(g_direction == 0) { if(bid >= g_last_close + g_brick_size) { if(!BrickStep(true, g_last_close, next_close)) { step_failed = true; break; } if(!DrawBrick(tick_time, g_last_close, next_close, 1)) return; g_last_close = next_close; g_direction = 1; EvaluateConfirmedBreakout(g_last_close, enable_alert); changed = true; } else if(bid <= g_last_close - g_brick_size) { if(!BrickStep(false, g_last_close, next_close)) { step_failed = true; break; } if(!DrawBrick(tick_time, g_last_close, next_close, -1)) return; g_last_close = next_close; g_direction = -1; EvaluateConfirmedBreakout(g_last_close, enable_alert); changed = true; } } else if(g_direction > 0) { if(bid >= g_last_close + g_brick_size) { if(!BrickStep(true, g_last_close, next_close)) { step_failed = true; break; } if(!DrawBrick(tick_time, g_last_close, next_close, 1)) return; g_last_close = next_close; EvaluateConfirmedBreakout(g_last_close, enable_alert); changed = true; } else if(bid <= g_last_close - 2.0 * g_brick_size) { if(!BrickStep(false, g_last_close, brick_open) || !BrickStep(false, brick_open, next_close)) { step_failed = true; break; } if(!DrawBrick(tick_time, brick_open, next_close, -1)) return; g_last_close = next_close; g_direction = -1; EvaluateConfirmedBreakout(g_last_close, enable_alert); if(enable_alert && g_alert_enabled && g_brick_reversal_enabled) NotifyUser(StringFormat("RenkoDeepSeek: РАЗВОРОТ КИРПИЧЕЙ ВНИЗ; размер %d пунктов; %s", g_current_brick_points, _Symbol), InpPushNotifications); changed = true; } } else { if(bid <= g_last_close - g_brick_size) { if(!BrickStep(false, g_last_close, next_close)) { step_failed = true; break; } if(!DrawBrick(tick_time, g_last_close, next_close, -1)) return; g_last_close = next_close; EvaluateConfirmedBreakout(g_last_close, enable_alert); changed = true; } else if(bid >= g_last_close + 2.0 * g_brick_size) { if(!BrickStep(true, g_last_close, brick_open) || !BrickStep(true, brick_open, next_close)) { step_failed = true; break; } if(!DrawBrick(tick_time, brick_open, next_close, 1)) return; g_last_close = next_close; g_direction = 1; EvaluateConfirmedBreakout(g_last_close, enable_alert); if(enable_alert && g_alert_enabled && g_brick_reversal_enabled) NotifyUser(StringFormat("RenkoDeepSeek: РАЗВОРОТ КИРПИЧЕЙ ВВЕРХ; размер %d пунктов; %s", g_current_brick_points, _Symbol), InpPushNotifications); changed = true; } } } if(step_failed && !g_step_warned) { g_step_warned = true; Print("RenkoDeepSeek: шаг кирпича не меняет цену на сетке инструмента (tick size=", DoubleToString(SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE), _Digits), "); построение кирпичей приостановлено. Увеличьте InpBrickPoints."); } if(changed) Print("RenkoDeepSeek: достигнут предел 10000 кирпичей за один тик; остаток движения этим тиком не обработан и будет учтён при следующем поступлении цены."); g_last_bid = bid; EvaluatePreBreakoutSignals(bid, enable_alert); } //--- Сохранение согласованного состояния серии перед пересбором. bool SnapshotSequence() { // Существующий снимок остаётся действительным, пока новый не скопирован // полностью. Отказ ArrayResize не должен уничтожать резерв для отката. const int count = g_brick_count; if(count > 0 && ArrayResize(g_snap_bricks, count, 256) != count) { Print("RenkoDeepSeek: не удалось создать новый снимок серии; предыдущий резервный снимок сохранён."); return false; } for(int i = 0; i < count; ++i) g_snap_bricks[i] = g_bricks[BrickPhysicalIndex(i)]; g_snap_count = count; g_snap_cycle_start = g_cycle_start; g_snap_direction = g_direction; g_snap_last_tick_msc = g_last_tick_msc; g_snap_ticks_at_msc = g_ticks_processed_at_last_msc; g_snap_have_anchor = g_have_anchor; g_snap_history_initialized = g_history_initialized; g_snap_partial = g_history_partial; g_snap_rebuild_resume_from = g_rebuild_resume_from; g_snap_rebuild_resume_window_days = g_rebuild_resume_window_days; g_snap_history_needs_more = g_history_needs_more; g_snap_rebuild_partial_accum = g_rebuild_partial_accum; g_snap_last_close = g_last_close; g_snap_last_bid = g_last_bid; g_snap_swing_high = g_last_swing_high; g_snap_swing_low = g_last_swing_low; g_snap_high_alerted = g_swing_high_breakout_alerted; g_snap_low_alerted = g_swing_low_breakout_alerted; g_snap_high_sent = g_swing_high_alert_sent; g_snap_low_sent = g_swing_low_alert_sent; g_snap_pre_buy = g_pre_buy_alerted; g_snap_pre_sell = g_pre_sell_alerted; g_snap_buy_cancelled = g_pre_buy_cancelled; g_snap_sell_cancelled = g_pre_sell_cancelled; g_snap_valid = true; return true; } //--- Возврат к сохранённому состоянию после неудачного пересбора. void RestoreSequence() { if(!g_snap_valid) return; const int count = g_snap_count; if(ArraySize(g_bricks) != InpMaxHistoryBricks && ArrayResize(g_bricks, InpMaxHistoryBricks, 256) != InpMaxHistoryBricks) { Print("RenkoDeepSeek: не удалось подготовить кольцевой буфер для восстановления снимка; резервный снимок сохранён для повторной попытки."); return; } g_brick_start = 0; for(int i = 0; i < count; ++i) g_bricks[i] = g_snap_bricks[i]; g_brick_count = count; g_cycle_start = g_snap_cycle_start; g_direction = g_snap_direction; g_last_tick_msc = g_snap_last_tick_msc; g_ticks_processed_at_last_msc = g_snap_ticks_at_msc; g_have_anchor = g_snap_have_anchor; g_history_initialized = g_snap_history_initialized; g_history_partial = g_snap_partial; g_rebuild_resume_from = g_snap_rebuild_resume_from; g_rebuild_resume_window_days = g_snap_rebuild_resume_window_days; g_history_needs_more = g_snap_history_needs_more; g_rebuild_partial_accum = g_snap_rebuild_partial_accum; g_last_close = g_snap_last_close; g_last_bid = g_snap_last_bid; g_last_swing_high = g_snap_swing_high; g_last_swing_low = g_snap_swing_low; g_swing_high_breakout_alerted = g_snap_high_alerted; g_swing_low_breakout_alerted = g_snap_low_alerted; g_swing_high_alert_sent = g_snap_high_sent; g_swing_low_alert_sent = g_snap_low_sent; g_pre_buy_alerted = g_snap_pre_buy; g_pre_sell_alerted = g_snap_pre_sell; g_pre_buy_cancelled = g_snap_buy_cancelled; g_pre_sell_cancelled = g_snap_sell_cancelled; g_series_dirty = true; g_snap_valid = false; Print("RenkoDeepSeek: пересбор истории не удался; восстановлена предыдущая серия кирпичей (", g_brick_count, ")."); } //--- Восстановление истории тиков короткими чанками. // Причина: один запрос на сутки упирается в тайм-аут 4403 и возвращает 0 тиков. // Пересбор идёт срезами по бюджету времени и продолжается с того же чанка. bool RebuildHistoryWindow(const int requested_window_days) { if(!IsRenkoGridCompatible(g_current_brick_points, true)) return false; RefreshServerOffset(); const datetime now = TimeCurrent(); const datetime current_cycle = CycleStart(now); if(current_cycle == 0) return false; const datetime cycle_start_server = MoscowToServerTime(current_cycle); datetime earliest_history = 0; if(!GetEarliestAvailableHistoryDate(earliest_history)) return false; int window_days = requested_window_days; if(window_days < 1) window_days = 1; const int maximum_window_days = MaximumHistoryWindowDaysAvailable(cycle_start_server); if(maximum_window_days <= 0) return false; if(window_days > maximum_window_days) window_days = maximum_window_days; datetime range_from = cycle_start_server - (datetime)((window_days - 1) * 86400); if(range_from < earliest_history) range_from = earliest_history; const datetime range_to = now + 1; if(range_from >= range_to) { PrintFormat("RenkoDeepSeek: начало доступной истории %s находится за текущей границей запроса; история пока недоступна.", TimeToString(earliest_history, TIME_DATE|TIME_MINUTES)); return false; } // Один дешёвый запрос по свежему участку отделяет «база тиков ещё не готова» // от «старая часть недоступна». Проверка идёт до сброса состояния, поэтому // неудачная попытка не трогает уже показанную серию кирпичей. { MqlTick probe[]; const datetime probe_from = (now > HISTORY_PROBE_SECONDS ? now - HISTORY_PROBE_SECONDS : now - 60); ResetLastError(); const int probe_copied = CopyTicksRange(_Symbol, probe, COPY_TICKS_INFO, (ulong)probe_from * 1000, (ulong)(now + 1) * 1000); const int probe_error = GetLastError(); if(probe_error == 4403 && probe_copied <= 0) { PrintFormat("RenkoDeepSeek: база тиков ещё не готова (код=%d; доступных тиков=%d на последних %d минутах); восстановление будет повторено позже.", probe_error, probe_copied, HISTORY_PROBE_SECONDS / 60); return false; } if(probe_error == 4403 && probe_copied > 0) PrintFormat("RenkoDeepSeek: предварительная проверка получила %d частичных тиков (4403); пересбор продолжится по чанкам.", probe_copied); else if(probe_copied < 0 && probe_error != 4401) { PrintFormat("RenkoDeepSeek: база тиков ещё не готова (код=%d на последних %d минутах); восстановление будет повторено позже.", probe_error, HISTORY_PROBE_SECONDS / 60); return false; } // 0 тиков без ошибки — нормально: ночь или выходные. Продолжаем. } // Проверка у самой старой границы носит информационный характер: // обрывать из-за неё пересбор нельзя, чанки сами сообщат об ошибках. { datetime old_probe_to = range_from + (datetime)TICK_BASE_PROBE_SECONDS; if(old_probe_to > range_to) old_probe_to = range_to; if(old_probe_to > range_from) { MqlTick old_probe[]; ResetLastError(); const int old_copied = CopyTicksRange(_Symbol, old_probe, COPY_TICKS_INFO, (ulong)range_from * 1000, (ulong)old_probe_to * 1000 - 1); const int old_error = GetLastError(); if(old_copied < 0 && old_error != 4401) PrintFormat("RenkoDeepSeek: самый старый участок истории тиков пока недоступен (код=%d); пересбор продолжается по чанкам.", old_error); } } // Продолжение прерванного по бюджету пересбора: состояние НЕ очищаем, // иначе каждый срез начинал бы окно заново и история никогда не росла бы. const bool resuming = (g_rebuild_resume_from != 0 && g_rebuild_resume_window_days == window_days); if(!resuming) { g_brick_count = 0; g_brick_start = 0; g_series_dirty = true; ResetCycle(0); g_last_tick_msc = 0; g_ticks_processed_at_last_msc = 0; g_last_bid = 0.0; g_rebuild_partial_accum = false; g_diag_valid_bid_ticks = 0; g_diag_min_bid = 0.0; g_diag_max_bid = 0.0; g_diag_first_bid = 0.0; g_diag_last_bid = 0.0; g_history_needs_more = false; g_rebuild_resume_from = range_from; g_rebuild_resume_window_days = window_days; g_cache_dirty = true; } const ulong build_started_ms = GetTickCount64(); int total_chunks = 0; int failed_chunks = 0; int total_ticks_copied = 0; bool interrupted = false; bool incomplete = false; datetime chunk_from = (resuming ? g_rebuild_resume_from : range_from); while(chunk_from < range_to) { if(IsStopped()) { interrupted = true; break; } // Бюджет времени защищает интерфейс: один срез не должен заметно // замораживать график. Остаток продолжается следующим вызовом. if(GetTickCount64() - build_started_ms > HISTORY_BUILD_BUDGET_MS) { incomplete = true; break; } datetime chunk_to = chunk_from + HISTORY_CHUNK_SECONDS; if(chunk_to > range_to) chunk_to = range_to; if(chunk_to <= chunk_from) break; MqlTick ticks[]; int copied = -1; int copy_error = 0; for(int attempt = 0; attempt <= HISTORY_CHUNK_RETRIES; ++attempt) { ResetLastError(); copied = CopyTicksRange(_Symbol, ticks, COPY_TICKS_INFO, (ulong)chunk_from * 1000, (ulong)chunk_to * 1000 - 1); copy_error = GetLastError(); // 4403 может сопровождаться частичным результатом: сохраняем его, // а повтор диапазона выполняем следующим срезом, не затирая массив. if(copy_error == 4403 || copied >= 0) break; } ++total_chunks; if(copy_error == 4403 && copied <= 0) { ++failed_chunks; incomplete = true; if(failed_chunks <= 3) PrintFormat("RenkoDeepSeek: чанк пока не загружен (4403; тиков=%d); диапазон %s — %s сохранён для повтора.", copied, TimeToString(chunk_from, TIME_DATE|TIME_MINUTES), TimeToString(chunk_to, TIME_DATE|TIME_MINUTES)); // Не перескакиваем диапазон и не сбрасываем Renko-состояние: // следующий срез повторит тот же чанк после синхронизации. break; } else if(copied < 0) { // Ошибка API не подтверждает отсутствие тиков. Не перескакиваем чанк, // не сбрасываем последовательность и повторяем тот же диапазон позже. ++failed_chunks; incomplete = true; PrintFormat("RenkoDeepSeek: CopyTicksRange код=%d; %s — %s; чанк оставлен checkpoint для повторной загрузки.", copy_error, TimeToString(chunk_from, TIME_DATE|TIME_MINUTES), TimeToString(chunk_to, TIME_DATE|TIME_MINUTES)); break; } else if(copied == 0 && copy_error != 0) { // Нулевой результат вместе с ошибкой не подтверждает пустой диапазон. // Оставляем тот же чанк в checkpoint и повторяем после синхронизации. ++failed_chunks; incomplete = true; PrintFormat("RenkoDeepSeek: CopyTicksRange вернул 0 тиков с ошибкой %d для %s — %s; диапазон оставлен checkpoint для повтора.", copy_error, TimeToString(chunk_from, TIME_DATE|TIME_MINUTES), TimeToString(chunk_to, TIME_DATE|TIME_MINUTES)); break; } else if(copied == 0 && copy_error == 0) { // Успешный пустой ответ — подтверждённый пустой интервал (например, // ночь/выходной). Закрываем чанк, иначе восстановление застрянет // навсегда на первом интервале без тиков. PrintFormat("RenkoDeepSeek: подтверждённо пустой интервал %s — %s; чанк закрыт без продвижения Renko-цены.", TimeToString(chunk_from, TIME_DATE|TIME_MINUTES), TimeToString(chunk_to, TIME_DATE|TIME_MINUTES)); } else { total_ticks_copied += copied; // Повторный запрос частично загруженного чанка возвращает его с начала. // Пропускаем уже обработанный префикс по паре (time_msc, ordinal), // иначе после 4403 одни и те же тики повторно попадут в Renko-последовательность. const long resume_tick_msc = g_last_tick_msc; const int resume_ticks_at_msc = g_ticks_processed_at_last_msc; int seen_resume_msc = 0; for(int i = 0; i < copied; ++i) { if((i & 0xFFF) == 0 && IsStopped()) { interrupted = true; break; } // Не обрабатываем повторно префикс чанка, уже прошедший до checkpoint. if(ticks[i].time_msc < resume_tick_msc) continue; if(ticks[i].time_msc == resume_tick_msc) { ++seen_resume_msc; if(seen_resume_msc <= resume_ticks_at_msc) continue; } // Курсор считает все записи тиков, включая записи без корректного Bid. if(ticks[i].time_msc > g_last_tick_msc) { g_last_tick_msc = ticks[i].time_msc; g_ticks_processed_at_last_msc = 1; } else if(ticks[i].time_msc == g_last_tick_msc) { ++g_ticks_processed_at_last_msc; } g_cache_dirty = true; if(ticks[i].bid <= 0.0 || !MathIsValidNumber(ticks[i].bid)) continue; ++g_diag_valid_bid_ticks; if(g_diag_first_bid <= 0.0) g_diag_first_bid = ticks[i].bid; g_diag_last_bid = ticks[i].bid; if(g_diag_min_bid <= 0.0 || ticks[i].bid < g_diag_min_bid) g_diag_min_bid = ticks[i].bid; if(g_diag_max_bid <= 0.0 || ticks[i].bid > g_diag_max_bid) g_diag_max_bid = ticks[i].bid; ProcessBid((datetime)ticks[i].time, ticks[i].bid, false); g_last_bid = ticks[i].bid; } if(interrupted) break; if(copy_error == 4403) { ++failed_chunks; incomplete = true; if(failed_chunks <= 3) PrintFormat("RenkoDeepSeek: обработано %d частичных тиков чанка (4403); диапазон %s — %s останется checkpoint для повтора.", copied, TimeToString(chunk_from, TIME_DATE|TIME_MINUTES), TimeToString(chunk_to, TIME_DATE|TIME_MINUTES)); // Курсор тиков уже запоминает обработанные записи, включая ordinal // внутри одинакового time_msc. Повторяем этот же чанк, не теряя хвост. break; } } // Полный ответ без ошибки закрывает ранее частично полученный чанк. // Не переносим временную неполноту в итоговый статус всей истории. g_rebuild_partial_accum = false; chunk_from = chunk_to; g_rebuild_resume_from = chunk_from; } if(failed_chunks > 0 || incomplete) g_rebuild_partial_accum = true; g_cache_dirty = true; if(interrupted) { g_brick_count = 0; g_brick_start = 0; g_series_dirty = true; ResetCycle(0); g_last_tick_msc = 0; g_ticks_processed_at_last_msc = 0; g_last_bid = 0.0; g_rebuild_resume_from = 0; g_rebuild_resume_window_days = 0; g_rebuild_partial_accum = false; g_history_needs_more = false; Print("RenkoDeepSeek: восстановление истории прервано; частичные кирпичи сброшены."); return false; } if(total_ticks_copied == 0 && g_brick_count == 0 && !resuming) { g_rebuild_resume_from = 0; g_rebuild_resume_window_days = 0; g_rebuild_partial_accum = false; Print("RenkoDeepSeek: за выбранный период история тиков не получена; восстановление будет повторено позже."); return false; } // Работа не закончена — сохраняем точку продолжения. Следующий вызов // продолжит с этого чанка, а не начнёт окно заново. if(chunk_from < range_to) { g_rebuild_resume_from = chunk_from; g_rebuild_resume_window_days = window_days; g_history_needs_more = true; g_cache_dirty = true; g_history_partial = true; g_series_dirty = true; PrintFormat("RenkoDeepSeek: пересбор истории продолжается (чанк %s): внутреннее окно=%d дней; чанков=%d; не загружено=%d; кирпичей=%d/%d.", TimeToString(chunk_from, TIME_DATE|TIME_MINUTES), window_days, total_chunks, failed_chunks, g_brick_count, InpMaxHistoryBricks); return true; } // Полный проход по окну завершён. g_rebuild_resume_from = 0; g_rebuild_resume_window_days = 0; g_history_needs_more = false; g_history_partial = g_rebuild_partial_accum; if(!g_history_partial) { g_gap_fill_attempts = 0; g_gap_fill_exhausted_reported = false; } g_series_dirty = true; PrintFormat("RenkoDeepSeek: история готова: внутреннее окно=%d дней; чанков=%d; не загружено=%d; тиков=%d; Bid-тиков=%I64d; кирпичей=%d/%d; размер=%d пунктов (brick=%.10f); _Point=%.10f; tick_size=%.10f; Bid min/max=%s/%s; диапазон=%.10f; первый/последний Bid=%s/%s; якорь=%s; граница цикла (сервер)=%s%s", window_days, total_chunks, failed_chunks, total_ticks_copied, g_diag_valid_bid_ticks, g_brick_count, InpMaxHistoryBricks, g_current_brick_points, g_brick_size, _Point, SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE), (g_diag_min_bid > 0.0 ? DoubleToString(g_diag_min_bid, _Digits) : "нет"), (g_diag_max_bid > 0.0 ? DoubleToString(g_diag_max_bid, _Digits) : "нет"), (g_diag_max_bid > 0.0 && g_diag_min_bid > 0.0 ? g_diag_max_bid-g_diag_min_bid : 0.0), (g_diag_first_bid > 0.0 ? DoubleToString(g_diag_first_bid, _Digits) : "нет"), (g_diag_last_bid > 0.0 ? DoubleToString(g_diag_last_bid, _Digits) : "нет"), (g_have_anchor ? DoubleToString(g_last_close, _Digits) : "не установлен"), TimeToString(cycle_start_server, TIME_DATE|TIME_MINUTES), (g_history_partial ? "; ЧАСТИЧНО — будет дозагрузка" : "")); if(g_brick_count == 0) { const double bid_range = (g_diag_max_bid > 0.0 && g_diag_min_bid > 0.0 ? g_diag_max_bid - g_diag_min_bid : 0.0); if(g_diag_valid_bid_ticks == 0) Print("RenkoDeepSeek: ноль кирпичей: в истории нет корректных Bid."); else if(bid_range + _Point*0.1 < g_brick_size) PrintFormat("RenkoDeepSeek: диапазон Bid %.10f меньше размера кирпича %.10f; движения недостаточно для текущего размера.", bid_range, g_brick_size); else Print("RenkoDeepSeek: диапазон Bid достаточен, но кирпичи не созданы; проверьте последовательность тиков/состояние цикла."); } return true; } //--- Расширение истории с сохранением уже показанной серии при неудаче. bool RebuildHistory() { if(g_rebuild_running) return false; g_rebuild_running = true; g_last_history_attempt_ms = GetTickCount64(); // Снимок текущего состояния обязателен: без него нельзя начинать // разрушительную перестройку, поскольку откат не будет гарантирован. if(!SnapshotSequence()) { g_rebuild_running = false; Print("RenkoDeepSeek: пересборка отменена — не удалось создать резервный снимок; текущая серия и курсор не изменены."); return false; } // На всё время восстановления отключаем обновление панели и запись в журнал. const bool previous_replaying = g_replaying; g_replaying = true; bool result = false; RefreshServerOffset(); const datetime current_cycle = CycleStart(TimeCurrent()); if(current_cycle == 0) { g_replaying = previous_replaying; g_rebuild_running = false; RestoreSequence(); return false; } const datetime cycle_start_server = MoscowToServerTime(current_cycle); const int maximum_window_days = MaximumHistoryWindowDaysAvailable(cycle_start_server); if(maximum_window_days <= 0) { g_replaying = previous_replaying; g_rebuild_running = false; RestoreSequence(); return false; } // Если предыдущий срез остановился по бюджету, продолжать нужно // то же окно, а не начинать каждый OnCalculate снова с одного дня. // Иначе checkpoint в RebuildHistoryWindow не совпадёт с window_days, // серия будет очищена, а расширение до 400 кирпичей станет циклическим. int window_days = (g_rebuild_resume_from != 0 && g_rebuild_resume_window_days > 0 ? g_rebuild_resume_window_days : INITIAL_HISTORY_WINDOW_DAYS); if(window_days > maximum_window_days) window_days = maximum_window_days; const int target_bricks = InpMaxHistoryBricks; while(true) { if(!RebuildHistoryWindow(window_days)) { if(!result) { g_replaying = previous_replaying; g_rebuild_running = false; RestoreSequence(); return false; } // Удачная короткая серия уже стёрта началом неудачного расширения: // возвращаем её из снимка, сделанного после последнего успеха. RestoreSequence(); break; } result = true; g_series_dirty = true; // Новая точка отката: удачная серия этого размера окна. Если память // не позволяет создать снимок, сохраняем уже полученный результат // и прекращаем расширение, не теряя предыдущий резерв. if(!SnapshotSequence()) { Print("RenkoDeepSeek: расширение истории остановлено — новый снимок не создан; текущая успешно построенная серия сохранена."); break; } // Прерванный по бюджету пересбор нельзя прерывать расширением окна: // сначала нужно довести текущее окно до конца. if(g_history_needs_more || g_brick_count >= target_bricks || window_days >= maximum_window_days || g_history_partial) break; const int previous_window_days = window_days; if(window_days > maximum_window_days / 2) window_days = maximum_window_days; else window_days *= 2; PrintFormat("RenkoDeepSeek: количество кирпичей (%d/%d) ещё не достигнуто; расширяется внутреннее окно запроса с %d до %d дней.", g_brick_count, target_bricks, previous_window_days, window_days); } g_replaying = previous_replaying; g_rebuild_running = false; if(!result) { RestoreSequence(); return false; } if(g_brick_count <= 0 && !g_history_needs_more) { // Завершённый проход без кирпичей — допустимая нулевая серия, // если Anchor и курсор подтверждены. Не маркируем её частичной: // HasUsableRenkoState() разрешит дальнейшую обработку живых тиков. Print("RenkoDeepSeek: проход истории завершён без кирпичей; нулевая серия сохранена как валидное состояние (размер кирпича ", g_current_brick_points, " пунктов; проверены доступные тики и Anchor)."); } else if(g_brick_count < target_bricks && !g_history_needs_more && !g_history_partial && window_days >= maximum_window_days) { datetime earliest_history = 0; GetEarliestAvailableHistoryDate(earliest_history); PrintFormat("RenkoDeepSeek: достигнута самая ранняя доступная дата истории %s; построено %d из заданного лимита %d кирпичей. Дополнительные тики за более ранний период в доступной истории не обнаружены.", TimeToString(earliest_history, TIME_DATE|TIME_MINUTES), g_brick_count, target_bricks); } return true; } //--- Дозагрузка пропущенных интервалов без потери показанной серии. void TryFillHistoryGaps(const int rates_total) { // Частичная история допустима только вместе с точным checkpoint чанка. // Такой checkpoint обрабатывается главным циклом OnCalculate; полную // пересборку из фонового gap-handler не запускаем. if(!g_history_initialized || !g_history_partial || g_rebuild_running || g_history_needs_more) return; if(!g_gap_fill_exhausted_reported) { g_gap_fill_exhausted_reported = true; Print("RenkoDeepSeek: обнаружено частичное состояние без точного checkpoint диапазона; автоматическая полная пересборка запрещена. Состояние сохранено, требуется диагностика источника истории."); } } //--- Единое оформление переключателей. void SetToggleButtonStyle(const string button_name, const bool enabled) { if(button_name == "" || ObjectFind(0, button_name) < 0) return; const color button_color = enabled ? InpButtonActiveColor : InpButtonInactiveColor; ResetLastError(); bool ok = true; if(!ObjectSetInteger(0, button_name, OBJPROP_BGCOLOR, button_color)) ok = false; if(!ObjectSetInteger(0, button_name, OBJPROP_COLOR, clrWhite)) ok = false; if(!ObjectSetInteger(0, button_name, OBJPROP_BORDER_COLOR, button_color)) ok = false; if(!ObjectSetInteger(0, button_name, OBJPROP_STATE, false)) ok = false; if(!ok) Print("RenkoDeepSeek: не удалось обновить оформление кнопки ", button_name, "; код ошибки=", GetLastError()); ChartRedraw(0); } //--- Состояние разворота отображается текстом, а не только цветом. void UpdateBrickReversalButton() { if(g_brick_reversal_button_name == "" || ObjectFind(0, g_brick_reversal_button_name) < 0) return; const string state_text = g_brick_reversal_enabled ? "ВКЛ" : "ВЫКЛ"; SetButtonTextFit(g_brick_reversal_button_name, "Разворот кирпичей: " + state_text, 8); SetToggleButtonStyle(g_brick_reversal_button_name, g_brick_reversal_enabled); } // Обновляет подписи и состояние независимых переключателей предварительных сигналов. void UpdatePreBreakoutToggleButtons() { if(g_pre_buy_button_name != "" && ObjectFind(0, g_pre_buy_button_name) >= 0) { SetButtonTextFit(g_pre_buy_button_name, g_pre_buy_enabled ? "Ранний BUY: ВКЛ" : "Ранний BUY: ВЫКЛ", 8); SetToggleButtonStyle(g_pre_buy_button_name, g_pre_buy_enabled); } if(g_pre_sell_button_name != "" && ObjectFind(0, g_pre_sell_button_name) >= 0) { SetButtonTextFit(g_pre_sell_button_name, g_pre_sell_enabled ? "Ранний SELL: ВКЛ" : "Ранний SELL: ВЫКЛ", 8); SetToggleButtonStyle(g_pre_sell_button_name, g_pre_sell_enabled); } } //--- Количество включённых категорий определяет состояние главной кнопки. int EnabledAlertCategoryCount() { int count = 0; if(g_breakout_high_enabled) ++count; if(g_breakout_low_enabled) ++count; if(g_brick_reversal_enabled) ++count; if(g_pre_buy_enabled) ++count; if(g_pre_sell_enabled) ++count; return count; } // Мастер-флаг включён, если включена хотя бы одна категория. void SyncAlertMasterState() { g_alert_enabled = (EnabledAlertCategoryCount() > 0); } //--- Три состояния главной кнопки: ВЫКЛ, ЧАСТИЧНО, ВКЛ. void UpdateAlertMasterButton() { if(g_alert_button_name == "" || ObjectFind(0, g_alert_button_name) < 0) return; const int enabled_count = EnabledAlertCategoryCount(); const string label = (enabled_count == 0 ? "ОПОВЕЩЕНИЯ: ВЫКЛ" : enabled_count == 5 ? "ОПОВЕЩЕНИЯ: ВКЛ" : "ОПОВЕЩЕНИЯ: ЧАСТИЧНО"); SetButtonTextFit(g_alert_button_name, label, 9); ObjectSetInteger(0, g_alert_button_name, OBJPROP_XSIZE, 180); if(enabled_count == 0 || enabled_count == 5) SetToggleButtonStyle(g_alert_button_name, enabled_count == 5); else { ResetLastError(); bool ok = true; if(!ObjectSetInteger(0, g_alert_button_name, OBJPROP_BGCOLOR, clrDarkOrange)) ok = false; if(!ObjectSetInteger(0, g_alert_button_name, OBJPROP_COLOR, clrWhite)) ok = false; if(!ObjectSetInteger(0, g_alert_button_name, OBJPROP_BORDER_COLOR, clrDarkOrange)) ok = false; if(!ObjectSetInteger(0, g_alert_button_name, OBJPROP_STATE, false)) ok = false; if(!ok) Print("RenkoDeepSeek: не удалось обновить оформление частичного состояния главной кнопки; код ошибки=", GetLastError()); ChartRedraw(0); } } //--- Применение входных настроек к состояниям категорий. // InpBreakoutAlerts — стартовый мастер-флаг: если он включён, включаются все // категории; если выключен, действуют индивидуальные входы. void ApplyAlertInputs() { g_breakout_high_enabled = InpBreakoutHighAlerts || InpBreakoutAlerts; g_breakout_low_enabled = InpBreakoutLowAlerts || InpBreakoutAlerts; g_brick_reversal_enabled = InpBrickReversalAlerts || InpBreakoutAlerts; g_pre_buy_enabled = InpPreBreakoutBuyAlerts || InpBreakoutAlerts; g_pre_sell_enabled = InpPreBreakoutSellAlerts || InpBreakoutAlerts; SyncAlertMasterState(); } //--- Синхронизация всех подписей и цветов с сохранёнными состояниями. void RefreshAllAlertButtons() { UpdateAlertMasterButton(); if(g_breakout_high_button_name != "" && ObjectFind(0, g_breakout_high_button_name) >= 0) { SetButtonTextFit(g_breakout_high_button_name, "Пробой вершин", 8); SetToggleButtonStyle(g_breakout_high_button_name, g_breakout_high_enabled); } if(g_breakout_low_button_name != "" && ObjectFind(0, g_breakout_low_button_name) >= 0) { SetButtonTextFit(g_breakout_low_button_name, "Пробой впадин", 8); SetToggleButtonStyle(g_breakout_low_button_name, g_breakout_low_enabled); } UpdateBrickReversalButton(); UpdatePreBreakoutToggleButtons(); } //--- Создание и проверка элементов управления текущего экземпляра. bool CreateControlObjects() { int subwindow = ChartWindowFind(); if(subwindow < 0 && g_indicator_short_name != "") subwindow = ChartWindowFind(0, g_indicator_short_name); if(subwindow < 0) { ResetLastError(); Print("RenkoDeepSeek: окно текущего экземпляра индикатора пока не найдено; элементы управления будут созданы при следующей попытке. Имя=", g_indicator_short_name, "; ошибка=", GetLastError()); return false; } g_alert_button_name = g_object_prefix + ".AlertToggle"; g_breakout_high_button_name = g_object_prefix + ".BreakoutHighToggle"; g_breakout_low_button_name = g_object_prefix + ".BreakoutLowToggle"; g_brick_reversal_button_name = g_object_prefix + ".BrickReversalToggle"; g_pre_buy_button_name = g_object_prefix + ".PreBuyToggle"; g_pre_sell_button_name = g_object_prefix + ".PreSellToggle"; g_pre_status_label_name=g_object_prefix+".PreStatus"; g_pre_detail_label_name=g_object_prefix+".PreDetails"; g_pre_sl_label_name=g_object_prefix+".PreSL"; g_brick_edit_name = g_object_prefix + ".BrickSizeEdit"; g_brick_apply_name = g_object_prefix + ".BrickSizeApply"; SyncAlertMasterState(); if(ObjectFind(0, g_alert_button_name) < 0) { ResetLastError(); if(!ObjectCreate(0, g_alert_button_name, OBJ_BUTTON, subwindow, 0, 0)) { Print("RenkoDeepSeek: не удалось создать кнопку оповещений; код ошибки=", GetLastError()); g_alert_button_name = ""; g_breakout_high_button_name = ""; g_breakout_low_button_name = ""; g_brick_edit_name = ""; g_brick_apply_name = ""; return false; } } ObjectSetInteger(0, g_alert_button_name, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, g_alert_button_name, OBJPROP_XDISTANCE, 8); ObjectSetInteger(0, g_alert_button_name, OBJPROP_YDISTANCE, 35 + g_control_y_offset); ObjectSetInteger(0, g_alert_button_name, OBJPROP_YSIZE, 24); ObjectSetInteger(0, g_alert_button_name, OBJPROP_FONTSIZE, 9); ObjectSetInteger(0, g_alert_button_name, OBJPROP_STATE, false); UpdateAlertMasterButton(); if(ObjectFind(0, g_breakout_high_button_name) < 0 && !ObjectCreate(0, g_breakout_high_button_name, OBJ_BUTTON, subwindow, 0, 0)) { Print("RenkoDeepSeek: не удалось создать кнопку пробоя вершин; код ошибки=", GetLastError()); g_breakout_high_button_name = ""; return false; } ObjectSetInteger(0, g_breakout_high_button_name, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, g_breakout_high_button_name, OBJPROP_XDISTANCE, 8); ObjectSetInteger(0, g_breakout_high_button_name, OBJPROP_YDISTANCE, 63 + g_control_y_offset); ObjectSetInteger(0, g_breakout_high_button_name, OBJPROP_YSIZE, 24); ObjectSetInteger(0, g_breakout_high_button_name, OBJPROP_FONTSIZE, 8); SetButtonTextFit(g_breakout_high_button_name, "Пробой вершин", 8); ObjectSetInteger(0, g_breakout_high_button_name, OBJPROP_STATE, false); SetToggleButtonStyle(g_breakout_high_button_name, g_breakout_high_enabled); if(ObjectFind(0, g_breakout_low_button_name) < 0 && !ObjectCreate(0, g_breakout_low_button_name, OBJ_BUTTON, subwindow, 0, 0)) { Print("RenkoDeepSeek: не удалось создать кнопку пробоя впадин; код ошибки=", GetLastError()); g_breakout_low_button_name = ""; return false; } ObjectSetInteger(0, g_breakout_low_button_name, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, g_breakout_low_button_name, OBJPROP_XDISTANCE, 8); ObjectSetInteger(0, g_breakout_low_button_name, OBJPROP_YDISTANCE, 91 + g_control_y_offset); ObjectSetInteger(0, g_breakout_low_button_name, OBJPROP_YSIZE, 24); ObjectSetInteger(0, g_breakout_low_button_name, OBJPROP_FONTSIZE, 8); SetButtonTextFit(g_breakout_low_button_name, "Пробой впадин", 8); ObjectSetInteger(0, g_breakout_low_button_name, OBJPROP_STATE, false); SetToggleButtonStyle(g_breakout_low_button_name, g_breakout_low_enabled); if(ObjectFind(0, g_brick_reversal_button_name) < 0 && !ObjectCreate(0, g_brick_reversal_button_name, OBJ_BUTTON, subwindow, 0, 0)) { Print("RenkoDeepSeek: не удалось создать кнопку разворота кирпичей; код ошибки=", GetLastError()); g_brick_reversal_button_name = ""; return false; } ObjectSetInteger(0, g_brick_reversal_button_name, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, g_brick_reversal_button_name, OBJPROP_XDISTANCE, 8); ObjectSetInteger(0, g_brick_reversal_button_name, OBJPROP_YDISTANCE, 119 + g_control_y_offset); ObjectSetInteger(0, g_brick_reversal_button_name, OBJPROP_YSIZE, 24); ObjectSetInteger(0, g_brick_reversal_button_name, OBJPROP_FONTSIZE, 8); ObjectSetInteger(0, g_brick_reversal_button_name, OBJPROP_STATE, false); UpdateBrickReversalButton(); if(ObjectFind(0, g_pre_buy_button_name) < 0 && !ObjectCreate(0, g_pre_buy_button_name, OBJ_BUTTON, subwindow, 0, 0)) { Print("RenkoDeepSeek: не удалось создать кнопку раннего BUY; код ошибки=", GetLastError()); g_pre_buy_button_name = ""; return false; } ObjectSetInteger(0, g_pre_buy_button_name, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, g_pre_buy_button_name, OBJPROP_XDISTANCE, 8); ObjectSetInteger(0, g_pre_buy_button_name, OBJPROP_YDISTANCE, 147 + g_control_y_offset); ObjectSetInteger(0, g_pre_buy_button_name, OBJPROP_XSIZE, 136); ObjectSetInteger(0, g_pre_buy_button_name, OBJPROP_YSIZE, 24); ObjectSetInteger(0, g_pre_buy_button_name, OBJPROP_FONTSIZE, 8); if(ObjectFind(0, g_pre_sell_button_name) < 0 && !ObjectCreate(0, g_pre_sell_button_name, OBJ_BUTTON, subwindow, 0, 0)) { Print("RenkoDeepSeek: не удалось создать кнопку раннего SELL; код ошибки=", GetLastError()); g_pre_sell_button_name = ""; return false; } ObjectSetInteger(0, g_pre_sell_button_name, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, g_pre_sell_button_name, OBJPROP_XDISTANCE, 150); ObjectSetInteger(0, g_pre_sell_button_name, OBJPROP_YDISTANCE, 147 + g_control_y_offset); ObjectSetInteger(0, g_pre_sell_button_name, OBJPROP_XSIZE, 136); ObjectSetInteger(0, g_pre_sell_button_name, OBJPROP_YSIZE, 24); ObjectSetInteger(0, g_pre_sell_button_name, OBJPROP_FONTSIZE, 8); UpdatePreBreakoutToggleButtons(); if(!CreatePrePanelLabel(g_pre_status_label_name,subwindow,175) || !CreatePrePanelLabel(g_pre_detail_label_name,subwindow,193) || !CreatePrePanelLabel(g_pre_sl_label_name,subwindow,211)) Print("RenkoDeepSeek: панель ранних сигналов будет повторно создана при следующем обновлении."); UpdatePreBreakoutPanel(g_last_bid,true); if(ObjectFind(0, g_brick_edit_name) < 0) { ResetLastError(); if(!ObjectCreate(0, g_brick_edit_name, OBJ_EDIT, subwindow, 0, 0)) { Print("RenkoDeepSeek: не удалось создать поле ввода размера кирпича; код ошибки=", GetLastError()); g_alert_button_name = ""; g_breakout_high_button_name = ""; g_breakout_low_button_name = ""; g_brick_reversal_button_name = ""; g_brick_edit_name = ""; g_brick_apply_name = ""; return false; } } ObjectSetInteger(0, g_brick_edit_name, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, g_brick_edit_name, OBJPROP_XDISTANCE, 192); ObjectSetInteger(0, g_brick_edit_name, OBJPROP_YDISTANCE, 35 + g_control_y_offset); ObjectSetInteger(0, g_brick_edit_name, OBJPROP_XSIZE, 72); ObjectSetInteger(0, g_brick_edit_name, OBJPROP_YSIZE, 24); ObjectSetInteger(0, g_brick_edit_name, OBJPROP_FONTSIZE, 9); ObjectSetInteger(0, g_brick_edit_name, OBJPROP_ALIGN, ALIGN_CENTER); ObjectSetInteger(0, g_brick_edit_name, OBJPROP_READONLY, false); ObjectSetInteger(0, g_brick_edit_name, OBJPROP_COLOR, clrBlack); ObjectSetInteger(0, g_brick_edit_name, OBJPROP_BGCOLOR, clrWhite); ObjectSetInteger(0, g_brick_edit_name, OBJPROP_BORDER_COLOR, clrDimGray); ObjectSetString(0, g_brick_edit_name, OBJPROP_TEXT, IntegerToString(g_current_brick_points)); if(ObjectFind(0, g_brick_apply_name) < 0) { ResetLastError(); if(!ObjectCreate(0, g_brick_apply_name, OBJ_BUTTON, subwindow, 0, 0)) { Print("RenkoDeepSeek: не удалось создать кнопку применения размера; код ошибки=", GetLastError()); g_alert_button_name = ""; g_breakout_high_button_name = ""; g_breakout_low_button_name = ""; g_brick_reversal_button_name = ""; g_brick_edit_name = ""; g_brick_apply_name = ""; return false; } } ObjectSetInteger(0, g_brick_apply_name, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, g_brick_apply_name, OBJPROP_XDISTANCE, 272); ObjectSetInteger(0, g_brick_apply_name, OBJPROP_YDISTANCE, 35 + g_control_y_offset); ObjectSetInteger(0, g_brick_apply_name, OBJPROP_YSIZE, 24); ObjectSetInteger(0, g_brick_apply_name, OBJPROP_FONTSIZE, 9); SetButtonTextFit(g_brick_apply_name, "Применить", 9); ObjectSetInteger(0, g_brick_apply_name, OBJPROP_STATE, false); ObjectSetInteger(0, g_brick_apply_name, OBJPROP_BGCOLOR, clrSteelBlue); ObjectSetInteger(0, g_brick_apply_name, OBJPROP_COLOR, clrWhite); ObjectSetInteger(0, g_brick_apply_name, OBJPROP_BORDER_COLOR, clrMidnightBlue); ChartRedraw(0); return true; } //--- Проверка введённого размера кирпича и перестроение истории. bool ApplyBrickSizeFromEdit() { if(g_brick_edit_name == "") return false; string entered = ObjectGetString(0, g_brick_edit_name, OBJPROP_TEXT); StringTrimLeft(entered); StringTrimRight(entered); const long parsed = StringToInteger(entered); if(entered == "" || parsed < 1 || parsed > 100000000) { NotifyUser("RenkoDeepSeek: укажите целое число пунктов от 1 до 100000000.", false); ObjectSetString(0, g_brick_edit_name, OBJPROP_TEXT, IntegerToString(g_current_brick_points)); ObjectSetInteger(0, g_brick_apply_name, OBJPROP_STATE, false); return false; } for(int i = 0; i < StringLen(entered); ++i) { const ushort ch = StringGetCharacter(entered, i); if(ch < '0' || ch > '9') { NotifyUser("RenkoDeepSeek: размер кирпича должен быть целым числом пунктов.", false); ObjectSetString(0, g_brick_edit_name, OBJPROP_TEXT, IntegerToString(g_current_brick_points)); ObjectSetInteger(0, g_brick_apply_name, OBJPROP_STATE, false); return false; } } const int requested_points = (int)parsed; if(requested_points == g_current_brick_points) { ObjectSetInteger(0, g_brick_apply_name, OBJPROP_STATE, false); return true; } if(!IsRenkoGridCompatible(requested_points, true)) { NotifyUser("RenkoDeepSeek: размер кирпича несовместим с SYMBOL_TRADE_TICK_SIZE/Anchor; размер не изменён.", false); ObjectSetString(0, g_brick_edit_name, OBJPROP_TEXT, IntegerToString(g_current_brick_points)); ObjectSetInteger(0, g_brick_apply_name, OBJPROP_STATE, false); return false; } const int previous_points = g_current_brick_points; const double previous_size = g_brick_size; g_current_brick_points = requested_points; g_brick_size = (double)g_current_brick_points * _Point; g_cache_loaded = false; g_history_initialized = false; g_gap_fill_attempts = 0; g_gap_fill_exhausted_reported = false; g_step_warned = false; g_rebuild_resume_from = 0; g_rebuild_resume_window_days = 0; g_history_needs_more = false; g_last_tick_msc = 0; g_ticks_processed_at_last_msc = 0; IndicatorSetString(INDICATOR_SHORTNAME, BuildShortName()); if(!RebuildHistory()) { // Откат к прежней геометрии: серия уже восстановлена из снимка, // поэтому повторная пересборка здесь не нужна и только блокирует UI. g_current_brick_points = previous_points; g_brick_size = previous_size; g_last_history_attempt_ms = 0; g_gap_fill_attempts = 0; g_gap_fill_exhausted_reported = false; g_step_warned = false; g_rebuild_resume_from = 0; g_rebuild_resume_window_days = 0; g_history_needs_more = false; IndicatorSetString(INDICATOR_SHORTNAME, BuildShortName()); g_history_initialized = HasUsableRenkoState(); ObjectSetString(0, g_brick_edit_name, OBJPROP_TEXT, IntegerToString(g_current_brick_points)); ObjectSetInteger(0, g_brick_apply_name, OBJPROP_STATE, false); RenderBricks(ArraySize(g_open_buffer), true); ChartRedraw(0); NotifyUser("RenkoDeepSeek: не удалось перестроить историю для нового размера кирпича; размер не изменён.", false); return false; } g_history_initialized = HasUsableRenkoState(); RenderBricks(ArraySize(g_open_buffer), true); ObjectSetString(0, g_brick_edit_name, OBJPROP_TEXT, IntegerToString(g_current_brick_points)); ObjectSetInteger(0, g_brick_apply_name, OBJPROP_STATE, false); ChartRedraw(0); if(!SaveBrickPoints(g_current_brick_points)) NotifyUser("RenkoDeepSeek: размер применён и история перестроена, но сохранить настройку на диск не удалось; после перезапуска будет использовано прежнее значение.", false); else Print("RenkoDeepSeek: размер кирпича изменён на ", g_current_brick_points, " пунктов; история перестроена."); return true; } //--- Устойчивая идентификация файла настроек. uint HashText(const string value) { uint hash = (uint)2166136261; for(int i = 0; i < StringLen(value); ++i) { hash ^= (uint)StringGetCharacter(value, i); hash *= 16777619; } return hash; } // Маркер привязан к подокну и сохраняет идентичность при изменении номера окна. // ChartID дополнительно изолирует одинаковые маркеры на разных графиках. const string INSTANCE_MARKER_PREFIX = "RGB.Instance."; int CountInstanceMarkersInWindow(const int window, string &marker_name, string &marker_id) { marker_name = ""; marker_id = ""; int count = 0; const int total = ObjectsTotal(0, window, -1); for(int i = 0; i < total; ++i) { const string name = ObjectName(0, i, window, -1); if(StringFind(name, INSTANCE_MARKER_PREFIX) != 0) continue; ++count; marker_name = name; marker_id = StringSubstr(name, StringLen(INSTANCE_MARKER_PREFIX)); } return count; } // Маркер — пустая метка без цвета: она не отображается и скрыта из обычного списка объектов. // Объект намеренно не удаляется в OnDeinit: он хранит идентичность подокна между перезапусками. bool CreateInstanceMarker(const int window, const string marker_id, string &marker_name) { marker_name = INSTANCE_MARKER_PREFIX + marker_id; if(StringLen(marker_name) > 63 || ObjectFind(0, marker_name) >= 0) return false; ResetLastError(); if(!ObjectCreate(0, marker_name, OBJ_LABEL, window, 0, 0)) { PrintFormat("RenkoDeepSeek: не удалось создать маркер идентичности подокна %d; код ошибки=%d.", window, GetLastError()); return false; } bool ok = true; const bool set_hidden = ObjectSetInteger(0, marker_name, OBJPROP_HIDDEN, true); const bool set_selectable = ObjectSetInteger(0, marker_name, OBJPROP_SELECTABLE, false); const bool set_selected = ObjectSetInteger(0, marker_name, OBJPROP_SELECTED, false); const bool set_color = ObjectSetInteger(0, marker_name, OBJPROP_COLOR, clrNONE); const bool set_back = ObjectSetInteger(0, marker_name, OBJPROP_BACK, true); const bool set_corner = ObjectSetInteger(0, marker_name, OBJPROP_CORNER, CORNER_LEFT_UPPER); const bool set_xdistance = ObjectSetInteger(0, marker_name, OBJPROP_XDISTANCE, 0); const bool set_ydistance = ObjectSetInteger(0, marker_name, OBJPROP_YDISTANCE, 0); const bool set_fontsize = ObjectSetInteger(0, marker_name, OBJPROP_FONTSIZE, 1); const bool set_text = ObjectSetString(0, marker_name, OBJPROP_TEXT, ""); ok = set_hidden && set_selectable && set_selected && set_color && set_back && set_corner && set_xdistance && set_ydistance && set_fontsize && set_text; const int actual_window = ObjectFind(0, marker_name); const long actual_type = ObjectGetInteger(0, marker_name, OBJPROP_TYPE); const long actual_hidden = ObjectGetInteger(0, marker_name, OBJPROP_HIDDEN); const long actual_selectable = ObjectGetInteger(0, marker_name, OBJPROP_SELECTABLE); const long actual_selected = ObjectGetInteger(0, marker_name, OBJPROP_SELECTED); const long actual_color = ObjectGetInteger(0, marker_name, OBJPROP_COLOR); const long actual_back = ObjectGetInteger(0, marker_name, OBJPROP_BACK); const string actual_text = ObjectGetString(0, marker_name, OBJPROP_TEXT); if(!ok || actual_window != window || actual_type != OBJ_LABEL || actual_hidden != true || actual_selectable != false || actual_selected != false || (uint)actual_color != (uint)clrNONE || actual_back != true || actual_text != "") { const int error = GetLastError(); PrintFormat("RenkoDeepSeek: проверка маркера подокна %d не пройдена; " "set[hidden=%d selectable=%d selected=%d color=%d back=%d corner=%d x=%d y=%d font=%d text=%d]; " "read[window=%d type=%d hidden=%I64d selectable=%I64d selected=%I64d color=%I64d back=%I64d text_len=%d]; error=%d.", window, set_hidden, set_selectable, set_selected, set_color, set_back, set_corner, set_xdistance, set_ydistance, set_fontsize, set_text, actual_window, (int)actual_type, actual_hidden, actual_selectable, actual_selected, actual_color, actual_back, StringLen(actual_text), error); ObjectDelete(0, marker_name); marker_name = ""; return false; } return true; } string MakePersistentInstanceId() { const string base_id = IntegerToString((long)GetTickCount64()) + "_" + IntegerToString((long)GetMicrosecondCount()) + "_" + IntegerToString(ObjectsTotal(0, -1, -1)); string candidate = base_id; int suffix = 1; while(ObjectFind(0, INSTANCE_MARKER_PREFIX + candidate) >= 0) { candidate = base_id + "_" + IntegerToString(suffix); ++suffix; } return candidate; } bool InitializeInstanceMarker(const int window) { string marker_name = ""; string marker_id = ""; const int marker_count = CountInstanceMarkersInWindow(window, marker_name, marker_id); if(marker_count > 1) { PrintFormat("RenkoDeepSeek: в подокне %d найдено несколько маркеров идентичности; запуск остановлен во избежание смешивания настроек.", window); return false; } if(marker_count == 1) { if(marker_id == "" || ObjectFind(0, marker_name) != window || ObjectGetInteger(0, marker_name, OBJPROP_TYPE) != OBJ_LABEL || ObjectGetInteger(0, marker_name, OBJPROP_HIDDEN) != true || ObjectGetInteger(0, marker_name, OBJPROP_SELECTABLE) != false || (uint)ObjectGetInteger(0, marker_name, OBJPROP_COLOR) != (uint)clrNONE || ObjectGetString(0, marker_name, OBJPROP_TEXT) != "") { PrintFormat("RenkoDeepSeek: маркер идентичности подокна %d повреждён или привязан неоднозначно; запуск остановлен.", window); return false; } g_instance_marker_name = marker_name; g_instance_marker_id = marker_id; return true; } marker_id = MakePersistentInstanceId(); if(!CreateInstanceMarker(window, marker_id, marker_name)) { PrintFormat("RenkoDeepSeek: не удалось создать уникальный маркер идентичности для подокна %d; запуск остановлен.", window); return false; } g_instance_marker_name = marker_name; g_instance_marker_id = marker_id; return true; } // Не допускаем два экземпляра RenkoDeepSeek в одном подокне. // Остальные типы индикаторов могут совместно работать в этом подокне. bool HasOtherRenkoInstanceInWindow(const int window) { const string own_marker = "[" + g_instance_id; const int total = ChartIndicatorsTotal(0, window); for(int i = 0; i < total; ++i) { const string short_name = ChartIndicatorName(0, window, i); if(StringFind(short_name, "RenkoDeepSeek") != 0) continue; if(StringFind(short_name, own_marker) >= 0) continue; return true; } return false; } // Идентичность файлов и кэша не зависит от изменяемого номера подокна. string InstanceIdentityKey() { if(g_instance_marker_id == "") return ""; return "CH:" + IntegerToString((long)ChartID()) + ":INSTANCE:" + g_instance_marker_id; } // Пространство настроек разделяется по серверу и номеру торгового счёта. string SettingsAccountNamespace() { string server = AccountInfoString(ACCOUNT_SERVER); StringReplace(server, "/", "_"); StringReplace(server, "\\", "_"); StringReplace(server, ":", "_"); StringReplace(server, " ", "_"); const long login = AccountInfoInteger(ACCOUNT_LOGIN); if(login <= 0 || server == "") return "UNBOUND_" + IntegerToString((long)ChartID()); return IntegerToString(login) + "_" + server; } // Один общий lock не позволяет нескольким счетам импортировать одни и те же // старые файлы, в которых ещё не было сведений о счёте. string LegacySettingsMigrationLockFile() { return StringFormat("RenkoDeepSeekLegacySettings_%u_%u.lock", HashText(_Symbol), HashText(InstanceIdentityKey())); } void CopyLegacySettingsFile(const string source_file, const string target_file, const string description) { if(FileIsExist(target_file, FILE_COMMON) || !FileIsExist(source_file, FILE_COMMON)) return; ResetLastError(); if(FileCopy(source_file, FILE_COMMON, target_file, FILE_COMMON)) Print("RenkoDeepSeek: перенесены прежние настройки (", description, ") в пространство текущего экземпляра: ", target_file); else Print("RenkoDeepSeek: не удалось перенести прежние настройки (", description, "); источник=", source_file, "; код ошибки=", GetLastError()); } // Старые файлы не содержат account identity. Первый счёт, который // захватит эксклюзивный lock, становится единственным получателем миграции. // Остальные счета не импортируют эти общие legacy-настройки. void MigrateLegacyInstanceSettings() { const string current_owner = SettingsAccountNamespace(); if(StringFind(current_owner, "UNBOUND_") == 0) { Print("RenkoDeepSeek: счёт/сервер ещё не определён; миграция общих старых настроек отложена до появления account identity."); return; } const string lock_file = LegacySettingsMigrationLockFile(); ResetLastError(); const int lock_handle = FileOpen(lock_file, FILE_READ|FILE_WRITE|FILE_TXT|FILE_COMMON); if(lock_handle == INVALID_HANDLE) { Print("RenkoDeepSeek: не удалось получить lock миграции старых настроек; импорт пропущен во избежание смешивания счетов. Код ошибки=", GetLastError()); return; } string claimed_owner = FileReadString(lock_handle); StringTrimLeft(claimed_owner); StringTrimRight(claimed_owner); if(claimed_owner != "" && claimed_owner != current_owner) { FileClose(lock_handle); Print("RenkoDeepSeek: старые настройки уже закреплены за другим счётом; импорт в текущий счёт запрещён."); return; } if(claimed_owner == "") { FileSeek(lock_handle, 0, SEEK_SET); if(FileWriteString(lock_handle, current_owner + "\n") <= 0) { FileClose(lock_handle); Print("RenkoDeepSeek: не удалось зафиксировать владельца миграции старых настроек; импорт пропущен."); return; } FileFlush(lock_handle); } string stable_instance_id = InpInstanceId; StringTrimLeft(stable_instance_id); StringTrimRight(stable_instance_id); string legacy_window_identity = "CH:" + IntegerToString((long)ChartID()) + ":W" + IntegerToString(g_instance_window); if(stable_instance_id != "") legacy_window_identity += ":ID:" + stable_instance_id; CopyLegacySettingsFile( StringFormat("RenkoDeepSeek_%u_%u.txt", HashText(_Symbol), HashText(legacy_window_identity)), BrickSizeSettingsFile(), "размер кирпича"); CopyLegacySettingsFile( StringFormat("RenkoDeepSeekButtons_%u_%u.txt", HashText(_Symbol), HashText(legacy_window_identity)), ButtonSettingsFile(), "состояния кнопок"); // Поддержка самой старой схемы, где явный ID был единственным ключом. if(stable_instance_id != "") { const string legacy_identity = "ID:" + stable_instance_id; CopyLegacySettingsFile( StringFormat("RenkoDeepSeek_%u_%u.txt", HashText(_Symbol), HashText(legacy_identity)), BrickSizeSettingsFile(), "размер кирпича по прежнему явному ID"); CopyLegacySettingsFile( StringFormat("RenkoDeepSeekButtons_%u_%u.txt", HashText(_Symbol), HashText(legacy_identity)), ButtonSettingsFile(), "состояния кнопок по прежнему явному ID"); } FileClose(lock_handle); } string BrickSizeSettingsFile() { return StringFormat("RenkoDeepSeek_%s_%u_%u.txt", SettingsAccountNamespace(), HashText(_Symbol), HashText(InstanceIdentityKey())); } // Возвращает сохранённый размер или входное значение, если сохранения ещё нет. int LoadSavedBrickPoints(const int fallback_points) { // Сохранённый размер всегда имеет приоритет; параметр InpPreferSavedBrickSize // оставлен только для совместимости со старыми наборами входных параметров. const string file_name = BrickSizeSettingsFile(); ResetLastError(); const int handle = FileOpen(file_name, FILE_READ|FILE_TXT|FILE_COMMON|FILE_SHARE_READ|FILE_SHARE_WRITE); if(handle == INVALID_HANDLE) return fallback_points; string value = FileReadString(handle); FileClose(handle); StringTrimLeft(value); StringTrimRight(value); const long parsed = StringToInteger(value); if(value == "" || parsed < 1 || parsed > 100000000) { Print("RenkoDeepSeek: сохранённый размер кирпича недействителен; используется InpBrickPoints=", fallback_points, ". Файл=", file_name); return fallback_points; } for(int i = 0; i < StringLen(value); ++i) { const ushort ch = StringGetCharacter(value, i); if(ch < '0' || ch > '9') { Print("RenkoDeepSeek: сохранённый размер кирпича содержит недопустимые символы; используется InpBrickPoints=", fallback_points, ". Файл=", file_name); return fallback_points; } } if((int)parsed != fallback_points) Print("RenkoDeepSeek: применён сохранённый размер кирпича ", (int)parsed, " пунктов вместо InpBrickPoints=", fallback_points, "; InpBrickPoints используется только при отсутствии или повреждении сохранённого значения. ID экземпляра=", g_instance_id, "."); return (int)parsed; } // Сохраняет настройку в Common\Files с проверкой повторным чтением. bool SaveBrickPoints(const int points) { const string file_name = BrickSizeSettingsFile(); ResetLastError(); const int handle = FileOpen(file_name, FILE_WRITE|FILE_TXT|FILE_COMMON|FILE_SHARE_READ|FILE_SHARE_WRITE); if(handle == INVALID_HANDLE) { Print("RenkoDeepSeek: не удалось открыть файл сохранения размера кирпича; файл=", file_name, "; код ошибки=", GetLastError()); return false; } FileWriteString(handle, IntegerToString(points)); FileFlush(handle); FileClose(handle); ResetLastError(); const int verify_handle = FileOpen(file_name, FILE_READ|FILE_TXT|FILE_COMMON|FILE_SHARE_READ|FILE_SHARE_WRITE); if(verify_handle == INVALID_HANDLE) { Print("RenkoDeepSeek: не удалось проверить сохранённый размер кирпича; файл=", file_name, "; код ошибки=", GetLastError()); return false; } string saved_value = FileReadString(verify_handle); FileClose(verify_handle); StringTrimLeft(saved_value); StringTrimRight(saved_value); if(saved_value != IntegerToString(points)) { Print("RenkoDeepSeek: проверка сохранения размера кирпича не пройдена; ожидалось=", points, "; прочитано=", saved_value, "; файл=", file_name); return false; } return true; } //--- Пропуск пустых строк в конце файла. // В FILE_TXT запись с завершающим переводом строки может оставить «пустой» // токен, поэтому строгий FileIsEnding ложно объявлял бы файл повреждённым. bool ConsumeTrailingEmptyLines(const int handle) { for(int guard = 0; guard < 64 && !FileIsEnding(handle); ++guard) { string value = FileReadString(handle); StringTrimLeft(value); StringTrimRight(value); if(value != "") return false; } return true; } //--- Файл состояний переключателей на каждый экземпляр. string ButtonSettingsFile() { return StringFormat("RenkoDeepSeekButtons_%s_%u_%u.txt", SettingsAccountNamespace(), HashText(_Symbol), HashText(InstanceIdentityKey())); } // Старые файлы с четырьмя состояниями совместимы. void LoadButtonSettings() { const string file_name = ButtonSettingsFile(); ResetLastError(); const int handle = FileOpen(file_name, FILE_READ|FILE_TXT|FILE_COMMON|FILE_SHARE_READ|FILE_SHARE_WRITE); if(handle == INVALID_HANDLE) return; string values[6]; for(int i = 0; i < 6; ++i) values[i] = ""; int read_count = 0; bool corrupted = false; for(int guard = 0; guard < 64 && read_count < 6 && !FileIsEnding(handle); ++guard) { string value = FileReadString(handle); StringTrimLeft(value); StringTrimRight(value); if(value == "") continue; // завершающий перевод строки — не ошибка формата if(value != "0" && value != "1") { corrupted = true; // лишнее содержимое: файл не наш или повреждён break; } values[read_count] = value; ++read_count; } if(!corrupted) corrupted = !ConsumeTrailingEmptyLines(handle); FileClose(handle); if(read_count < 4 || corrupted) { Print("RenkoDeepSeek: файл состояний кнопок повреждён или имеет неизвестный формат; используются входные настройки. Файл=", file_name); return; } g_breakout_high_enabled = (values[1] == "1"); g_breakout_low_enabled = (values[2] == "1"); g_brick_reversal_enabled = (values[3] == "1"); if(read_count >= 5) g_pre_buy_enabled = (values[4] == "1"); if(read_count >= 6) g_pre_sell_enabled = (values[5] == "1"); // Мастер-флаг — производное состояние. SyncAlertMasterState(); Print("RenkoDeepSeek: состояния кнопок восстановлены; ID экземпляра=", g_instance_id, "."); } // Сохраняет все переключатели одним файлом после каждого нажатия. bool SaveButtonSettings() { const string file_name = ButtonSettingsFile(); ResetLastError(); const int handle = FileOpen(file_name, FILE_WRITE|FILE_TXT|FILE_COMMON|FILE_SHARE_READ|FILE_SHARE_WRITE); if(handle == INVALID_HANDLE) { Print("RenkoDeepSeek: не удалось сохранить состояния кнопок; файл=", file_name, "; код ошибки=", GetLastError()); return false; } FileWriteString(handle, (g_alert_enabled ? "1" : "0") + "\n" + (g_breakout_high_enabled ? "1" : "0") + "\n" + (g_breakout_low_enabled ? "1" : "0") + "\n" + (g_brick_reversal_enabled ? "1" : "0") + "\n" + (g_pre_buy_enabled ? "1" : "0") + "\n" + (g_pre_sell_enabled ? "1" : "0") + "\n"); FileFlush(handle); FileClose(handle); ResetLastError(); const int verify_handle = FileOpen(file_name, FILE_READ|FILE_TXT|FILE_COMMON|FILE_SHARE_READ|FILE_SHARE_WRITE); if(verify_handle == INVALID_HANDLE) { Print("RenkoDeepSeek: не удалось проверить файл состояний кнопок; файл=", file_name, "; код ошибки=", GetLastError()); return false; } string expected[6]; expected[0] = (g_alert_enabled ? "1" : "0"); expected[1] = (g_breakout_high_enabled ? "1" : "0"); expected[2] = (g_breakout_low_enabled ? "1" : "0"); expected[3] = (g_brick_reversal_enabled ? "1" : "0"); expected[4] = (g_pre_buy_enabled ? "1" : "0"); expected[5] = (g_pre_sell_enabled ? "1" : "0"); bool verified = true; for(int i = 0; i < 6; ++i) { if(FileIsEnding(verify_handle) || FileReadString(verify_handle) != expected[i]) { verified = false; break; } } FileClose(verify_handle); if(!verified) Print("RenkoDeepSeek: проверка сохранения состояний кнопок не пройдена; файл=", file_name); return verified; } //--- Идентификатор кэша включает экземпляр, сервер и параметры Renko. string RenkoCacheKey() { // Старые кэши с Anchor от первого Bid несовместимы с новой ценовой сеткой. return StringFormat("v8|anchor=%.5f|anchor_init=floor_first_bid|%s|%I64d|%s|%s|%d|%.10f|%.10f|%d|%d|%d|%d", RENKO_PRICE_ANCHOR, AccountInfoString(ACCOUNT_SERVER), AccountInfoInteger(ACCOUNT_LOGIN), _Symbol, InstanceIdentityKey(), _Digits, _Point, g_price_step, g_current_brick_points, InpCycleStartHour, InpCycleStartMinute, InpMaxHistoryBricks); } string RenkoCacheFileName() { string server = AccountInfoString(ACCOUNT_SERVER); StringReplace(server, "/", "_"); StringReplace(server, "\\", "_"); StringReplace(server, ":", "_"); StringReplace(server, " ", "_"); string symbol = _Symbol; StringReplace(symbol, "/", "_"); StringReplace(symbol, "\\", "_"); StringReplace(symbol, ":", "_"); return StringFormat("RenkoDeepSeek_%I64d_%s_%s_%u_%d_%d_%d_%d.cache", AccountInfoInteger(ACCOUNT_LOGIN), server, symbol, HashText(InstanceIdentityKey()), g_current_brick_points, InpCycleStartHour, InpCycleStartMinute, InpMaxHistoryBricks); } bool CacheWriteLine(const int handle, const string value, string &checksum_payload) { const string line = value + "\n"; if(FileWriteString(handle, line) <= 0) return false; checksum_payload += line; return true; } string CacheReadLine(const int handle, string &checksum_payload) { const string value = FileReadString(handle); checksum_payload += value + "\n"; return value; } //--- Сохраняем согласованный снимок состояния, включая контрольную сумму. bool SaveRenkoCache() { // Пустая серия допустима только как checkpoint незавершённой загрузки. // Разрешаем сохранять его до появления первого кирпича: // иначе перезапуск во время самого начала загрузки потеряет позицию чанка. if((!g_history_initialized && g_rebuild_resume_from <= 0) || (g_brick_count <= 0 && g_rebuild_resume_from <= 0 && (!g_have_anchor || g_last_tick_msc <= 0))) return false; const string file_name = RenkoCacheFileName(); const string temp_name = file_name + ".tmp"; ResetLastError(); const int handle = FileOpen(temp_name, FILE_WRITE|FILE_TXT|FILE_COMMON|FILE_SHARE_READ|FILE_SHARE_WRITE); if(handle == INVALID_HANDLE) { Print("RenkoDeepSeek: не удалось открыть временный кэш; код=", GetLastError()); return false; } bool ok = true; string checksum_payload = ""; ok = ok && CacheWriteLine(handle, "RenkoDeepSeekCacheV7", checksum_payload); ok = ok && CacheWriteLine(handle, RenkoCacheKey(), checksum_payload); ok = ok && CacheWriteLine(handle, IntegerToString(g_brick_count), checksum_payload); ok = ok && CacheWriteLine(handle, IntegerToString((long)g_cycle_start), checksum_payload); ok = ok && CacheWriteLine(handle, IntegerToString(g_direction), checksum_payload); ok = ok && CacheWriteLine(handle, IntegerToString(g_last_tick_msc), checksum_payload); ok = ok && CacheWriteLine(handle, IntegerToString(g_ticks_processed_at_last_msc), checksum_payload); ok = ok && CacheWriteLine(handle, g_have_anchor ? "1" : "0", checksum_payload); ok = ok && CacheWriteLine(handle, g_history_partial ? "1" : "0", checksum_payload); ok = ok && CacheWriteLine(handle, IntegerToString((long)g_rebuild_resume_from), checksum_payload); ok = ok && CacheWriteLine(handle, IntegerToString(g_rebuild_resume_window_days), checksum_payload); ok = ok && CacheWriteLine(handle, g_rebuild_partial_accum ? "1" : "0", checksum_payload); ok = ok && CacheWriteLine(handle, DoubleToString(g_last_close, _Digits), checksum_payload); ok = ok && CacheWriteLine(handle, DoubleToString(g_last_bid, _Digits), checksum_payload); ok = ok && CacheWriteLine(handle, DoubleToString(g_last_swing_high, _Digits), checksum_payload); ok = ok && CacheWriteLine(handle, DoubleToString(g_last_swing_low, _Digits), checksum_payload); ok = ok && CacheWriteLine(handle, g_swing_high_breakout_alerted ? "1" : "0", checksum_payload); ok = ok && CacheWriteLine(handle, g_swing_low_breakout_alerted ? "1" : "0", checksum_payload); ok = ok && CacheWriteLine(handle, g_swing_high_alert_sent ? "1" : "0", checksum_payload); ok = ok && CacheWriteLine(handle, g_swing_low_alert_sent ? "1" : "0", checksum_payload); ok = ok && CacheWriteLine(handle, g_pre_buy_alerted ? "1" : "0", checksum_payload); ok = ok && CacheWriteLine(handle, g_pre_sell_alerted ? "1" : "0", checksum_payload); ok = ok && CacheWriteLine(handle, g_pre_buy_cancelled ? "1" : "0", checksum_payload); ok = ok && CacheWriteLine(handle, g_pre_sell_cancelled ? "1" : "0", checksum_payload); for(int i = 0; ok && i < g_brick_count; ++i) { const int physical_index = BrickPhysicalIndex(i); ok = CacheWriteLine(handle, IntegerToString((long)g_bricks[physical_index].time), checksum_payload); ok = ok && CacheWriteLine(handle, DoubleToString(g_bricks[physical_index].open, _Digits), checksum_payload); ok = ok && CacheWriteLine(handle, DoubleToString(g_bricks[physical_index].high, _Digits), checksum_payload); ok = ok && CacheWriteLine(handle, DoubleToString(g_bricks[physical_index].low, _Digits), checksum_payload); ok = ok && CacheWriteLine(handle, DoubleToString(g_bricks[physical_index].close, _Digits), checksum_payload); ok = ok && CacheWriteLine(handle, IntegerToString(g_bricks[physical_index].direction), checksum_payload); } ok = ok && FileWriteString(handle, IntegerToString((long)HashText(checksum_payload)) + "\n") > 0; FileFlush(handle); FileClose(handle); if(!ok) { FileDelete(temp_name, FILE_COMMON); Print("RenkoDeepSeek: запись кэша завершилась неполностью; исходный кэш сохранён."); return false; } ResetLastError(); if(!FileMove(temp_name, FILE_COMMON, file_name, FILE_COMMON|FILE_REWRITE)) { const int error = GetLastError(); FileDelete(temp_name, FILE_COMMON); Print("RenkoDeepSeek: не удалось заменить кэш; код=", error); return false; } g_cache_dirty = false; return true; } //--- Загружаем только полный снимок с точным совпадением конфигурации. bool LoadRenkoCache() { const string file_name = RenkoCacheFileName(); ResetLastError(); const int handle = FileOpen(file_name, FILE_READ|FILE_TXT|FILE_COMMON|FILE_SHARE_READ|FILE_SHARE_WRITE); if(handle == INVALID_HANDLE) return false; bool ok = true; string checksum_payload = ""; if(CacheReadLine(handle, checksum_payload) != "RenkoDeepSeekCacheV7") ok = false; if(ok && CacheReadLine(handle, checksum_payload) != RenkoCacheKey()) ok = false; int count = -1; if(ok) count = (int)StringToInteger(CacheReadLine(handle, checksum_payload)); // Нулевая серия допустима только для сохранённого checkpoint незавершённой загрузки; // остальные пустые снимки отклоняются проверкой состояния ниже. if(count < 0 || count > InpMaxHistoryBricks) ok = false; datetime cycle_start = 0; int direction = 0; long last_tick_msc = 0; int ticks_at_msc = 0; bool have_anchor = false; bool history_partial = false; datetime rebuild_resume_from = 0; int rebuild_resume_window_days = 0; bool rebuild_partial_accum = false; double last_close = 0.0, last_bid = 0.0, swing_high = 0.0, swing_low = 0.0; bool high_alerted = false, low_alerted = false; bool high_sent = false, low_sent = false; bool pre_buy = false, pre_sell = false, buy_cancelled = false, sell_cancelled = false; if(ok) cycle_start = (datetime)StringToInteger(CacheReadLine(handle, checksum_payload)); if(ok) direction = (int)StringToInteger(CacheReadLine(handle, checksum_payload)); if(ok) last_tick_msc = (long)StringToInteger(CacheReadLine(handle, checksum_payload)); if(ok) ticks_at_msc = (int)StringToInteger(CacheReadLine(handle, checksum_payload)); if(ok) have_anchor = (StringToInteger(CacheReadLine(handle, checksum_payload)) == 1); if(ok) history_partial = (StringToInteger(CacheReadLine(handle, checksum_payload)) == 1); if(ok) rebuild_resume_from = (datetime)StringToInteger(CacheReadLine(handle, checksum_payload)); if(ok) rebuild_resume_window_days = (int)StringToInteger(CacheReadLine(handle, checksum_payload)); if(ok) rebuild_partial_accum = (StringToInteger(CacheReadLine(handle, checksum_payload)) == 1); if(ok) last_close = StringToDouble(CacheReadLine(handle, checksum_payload)); if(ok) last_bid = StringToDouble(CacheReadLine(handle, checksum_payload)); if(ok) swing_high = StringToDouble(CacheReadLine(handle, checksum_payload)); if(ok) swing_low = StringToDouble(CacheReadLine(handle, checksum_payload)); if(ok) high_alerted = (StringToInteger(CacheReadLine(handle, checksum_payload)) == 1); if(ok) low_alerted = (StringToInteger(CacheReadLine(handle, checksum_payload)) == 1); if(ok) high_sent = (StringToInteger(CacheReadLine(handle, checksum_payload)) == 1); if(ok) low_sent = (StringToInteger(CacheReadLine(handle, checksum_payload)) == 1); if(ok) pre_buy = (StringToInteger(CacheReadLine(handle, checksum_payload)) == 1); if(ok) pre_sell = (StringToInteger(CacheReadLine(handle, checksum_payload)) == 1); if(ok) buy_cancelled = (StringToInteger(CacheReadLine(handle, checksum_payload)) == 1); if(ok) sell_cancelled = (StringToInteger(CacheReadLine(handle, checksum_payload)) == 1); const bool has_bricks = (count > 0); if(ok && (direction < -1 || direction > 1 || ticks_at_msc < 0 || last_tick_msc < 0 || ((has_bricks || have_anchor) && last_tick_msc <= 0) || (has_bricks && (cycle_start <= 0 || !have_anchor || last_close <= 0.0 || last_bid <= 0.0)) || (have_anchor && (cycle_start <= 0 || last_close <= 0.0 || last_bid <= 0.0)) || (!have_anchor && (last_close != 0.0 || last_bid != 0.0)) || !MathIsValidNumber(last_close) || !MathIsValidNumber(last_bid) || !MathIsValidNumber(swing_high) || !MathIsValidNumber(swing_low) || (swing_high > 0.0 && swing_low > 0.0 && swing_high < swing_low) || rebuild_resume_from < 0 || rebuild_resume_window_days < 0 || ((rebuild_resume_from == 0) != (rebuild_resume_window_days == 0)) || (history_partial != (rebuild_resume_from > 0)) || (history_partial != rebuild_partial_accum) || (!has_bricks && rebuild_resume_from <= 0 && !have_anchor))) ok = false; if(ok && ArraySize(g_bricks) != InpMaxHistoryBricks && ArrayResize(g_bricks, InpMaxHistoryBricks, 256) != InpMaxHistoryBricks) ok = false; for(int i = 0; ok && i < count; ++i) { g_bricks[i].time = (datetime)StringToInteger(CacheReadLine(handle, checksum_payload)); g_bricks[i].open = StringToDouble(CacheReadLine(handle, checksum_payload)); g_bricks[i].high = StringToDouble(CacheReadLine(handle, checksum_payload)); g_bricks[i].low = StringToDouble(CacheReadLine(handle, checksum_payload)); g_bricks[i].close = StringToDouble(CacheReadLine(handle, checksum_payload)); g_bricks[i].direction = (int)StringToInteger(CacheReadLine(handle, checksum_payload)); if(g_bricks[i].time <= 0 || g_bricks[i].direction == 0 || g_bricks[i].direction < -1 || g_bricks[i].direction > 1 || g_bricks[i].open <= 0.0 || g_bricks[i].high <= 0.0 || g_bricks[i].low <= 0.0 || g_bricks[i].close <= 0.0 || !MathIsValidNumber(g_bricks[i].open) || !MathIsValidNumber(g_bricks[i].high) || !MathIsValidNumber(g_bricks[i].low) || !MathIsValidNumber(g_bricks[i].close) || g_bricks[i].high < MathMax(g_bricks[i].open, g_bricks[i].close) || g_bricks[i].low > MathMin(g_bricks[i].open, g_bricks[i].close) || (i > 0 && g_bricks[i].time < g_bricks[i-1].time)) ok = false; } const string checksum_value = FileReadString(handle); if(ok && (checksum_value == "" || (long)StringToInteger(checksum_value) != (long)HashText(checksum_payload))) ok = false; if(ok && !ConsumeTrailingEmptyLines(handle)) ok = false; FileClose(handle); if(!ok) { g_brick_count = 0; g_brick_start = 0; g_series_dirty = true; Print("RenkoDeepSeek: кэш отсутствует, повреждён или не соответствует настройкам; будет восстановлена история тиков."); return false; } g_brick_count = count; g_brick_start = 0; g_cycle_start = cycle_start; g_direction = direction; g_last_tick_msc = last_tick_msc; g_ticks_processed_at_last_msc = ticks_at_msc; g_have_anchor = have_anchor; g_history_partial = history_partial; g_rebuild_resume_from = rebuild_resume_from; g_rebuild_resume_window_days = rebuild_resume_window_days; g_rebuild_partial_accum = rebuild_partial_accum; g_history_needs_more = (rebuild_resume_from > 0); g_last_close = last_close; g_last_bid = last_bid; g_last_swing_high = swing_high; g_last_swing_low = swing_low; g_swing_high_breakout_alerted = high_alerted; g_swing_low_breakout_alerted = low_alerted; g_swing_high_alert_sent = high_sent; g_swing_low_alert_sent = low_sent; g_pre_buy_alerted = pre_buy; g_pre_sell_alerted = pre_sell; g_pre_buy_cancelled = buy_cancelled; g_pre_sell_cancelled = sell_cancelled; g_cache_loaded = true; g_series_dirty = true; Print("RenkoDeepSeek: восстановлен кэш Renko; кирпичей=", g_brick_count, "; курсор тиков=", g_last_tick_msc, "."); return true; } // Проверяет, занят ли runtime-ID другим индикатором на этом графике. bool IsRuntimeInstanceIdInUse(const string candidate) { const string marker = "[" + candidate; const int windows_total = (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL); for(int window = 0; window < windows_total; ++window) { const int indicators_total = ChartIndicatorsTotal(0, window); for(int index = 0; index < indicators_total; ++index) { const string short_name = ChartIndicatorName(0, window, index); if(StringFind(short_name, marker) >= 0) return true; } } return false; } // Создаёт уникальный runtime-ID до поиска подокна текущего индикатора. string MakeUniqueRuntimeInstanceId(string requested_id) { if(requested_id == "") { requested_id = "Auto" + IntegerToString((long)GetTickCount64()) + "_" + IntegerToString((long)GetMicrosecondCount()) + "_" + IntegerToString(ObjectsTotal(0, -1, -1)); } const string base_id = requested_id; int suffix = 1; while(IsRuntimeInstanceIdInUse(requested_id)) { requested_id = base_id + "_" + IntegerToString(suffix); ++suffix; } return requested_id; } // Определяет подокно только после присвоения уникального короткого имени. int ResolveOwnIndicatorWindow() { int window = ChartWindowFind(); if(window >= 0) return window; const int windows_total = (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL); for(int candidate = 0; candidate < windows_total; ++candidate) { const int indicators_total = ChartIndicatorsTotal(0, candidate); for(int index = 0; index < indicators_total; ++index) { if(ChartIndicatorName(0, candidate, index) == g_indicator_short_name) return candidate; } } return -1; } //--- Инициализация буферов, параметров и элементов управления. int OnInit() { // Runtime-ID всегда генерируется автоматически; InpInstanceId используется // только для переноса файлов, созданных прежними версиями индикатора. string instance_id = MakeUniqueRuntimeInstanceId(""); g_instance_id = instance_id; g_indicator_short_name = "RenkoDeepSeek initializing [" + g_instance_id + "]"; IndicatorSetString(INDICATOR_SHORTNAME, g_indicator_short_name); g_instance_window = ResolveOwnIndicatorWindow(); if(g_instance_window <= 0) { Print("RenkoDeepSeek: не удалось однозначно определить отдельное подокно текущего экземпляра; запуск остановлен."); return INIT_FAILED; } if(HasOtherRenkoInstanceInWindow(g_instance_window)) { PrintFormat("RenkoDeepSeek: в подокне %d уже работает другой экземпляр RenkoDeepSeek. Второй экземпляр в том же подокне запрещён; настройки и кэш не загружаются.", g_instance_window); return INIT_FAILED; } if(!InitializeInstanceMarker(g_instance_window)) return INIT_FAILED; g_instance_identity_ready = true; if(!InpPreferSavedBrickSize) Print("RenkoDeepSeek: параметр InpPreferSavedBrickSize устарел и игнорируется; сохранённый размер кирпича всегда имеет приоритет."); if(InpBrickPoints < 1 || InpMaxHistoryBricks < 1 || InpMaxHistoryBricks > MAX_HISTORY_BRICKS_ALLOWED || InpCycleStartHour < 0 || InpCycleStartHour > 23 || InpCycleStartMinute < 0 || InpCycleStartMinute > 59 || InpPreBreakoutDistanceBricks <= 0.0 || InpPreBreakoutDistanceBricks > 5.0 || InpPreBreakoutDistancePoints < 0 || InpStopLossBufferPoints < 0) { PrintFormat("RenkoDeepSeek: проверьте размер кирпича, лимит истории (1–%d кирпичей), время цикла, порог раннего сигнала (>0 и <=5 кирпичей; пункты >=0) и буфер SL (>=0).", MAX_HISTORY_BRICKS_ALLOWED); return INIT_PARAMETERS_INCORRECT; } if(ArrayResize(g_bricks, InpMaxHistoryBricks, 256) != InpMaxHistoryBricks) { Print("RenkoDeepSeek: не удалось выделить кольцевой буфер истории на ", InpMaxHistoryBricks, " кирпичей; код ошибки=", GetLastError()); return INIT_FAILED; } g_brick_start = 0; if(!SetIndexBuffer(0, g_open_buffer, INDICATOR_DATA) || !SetIndexBuffer(1, g_high_buffer, INDICATOR_DATA) || !SetIndexBuffer(2, g_low_buffer, INDICATOR_DATA) || !SetIndexBuffer(3, g_close_buffer, INDICATOR_DATA) || !SetIndexBuffer(4, g_color_buffer, INDICATOR_COLOR_INDEX)) return INIT_FAILED; ArraySetAsSeries(g_open_buffer, true); ArraySetAsSeries(g_high_buffer, true); ArraySetAsSeries(g_low_buffer, true); ArraySetAsSeries(g_close_buffer, true); ArraySetAsSeries(g_color_buffer, true); PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE); PlotIndexSetInteger(0, PLOT_SHOW_DATA, false); PlotIndexSetInteger(0, PLOT_COLOR_INDEXES, 2); PlotIndexSetInteger(0, PLOT_LINE_COLOR, 0, InpUpColor); PlotIndexSetInteger(0, PLOT_LINE_COLOR, 1, InpDownColor); IndicatorSetInteger(INDICATOR_DIGITS, _Digits); RefreshServerOffset(); double tick_size = 0.0; ResetLastError(); if(!SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE, tick_size) || !MathIsValidNumber(tick_size) || tick_size <= 0.0) { PrintFormat("RenkoDeepSeek: не удалось получить корректный SYMBOL_TRADE_TICK_SIZE для %s; автоматическая подмена на _Point запрещена. Код ошибки=%d.", _Symbol, GetLastError()); return INIT_FAILED; } g_price_step = tick_size; MigrateLegacyInstanceSettings(); g_current_brick_points = LoadSavedBrickPoints(InpBrickPoints); if(!IsRenkoGridCompatible(g_current_brick_points, true)) { PrintFormat("RenkoDeepSeek: эффективный размер кирпича %d пунктов несовместим с Anchor/tick grid; запуск отменён. Автоматический переход на InpBrickPoints или округление запрещены.", g_current_brick_points); return INIT_PARAMETERS_INCORRECT; } g_price_grid_valid = true; // InpBreakoutAlerts — стартовый мастер-флаг; файл настроек имеет приоритет. ApplyAlertInputs(); LoadButtonSettings(); SyncAlertMasterState(); g_settings_initialized = true; g_brick_size = (double)g_current_brick_points * _Point; g_cache_loaded = LoadRenkoCache(); g_history_initialized = (g_cache_loaded && HasUsableRenkoState()); g_indicator_short_name = BuildShortName(); IndicatorSetString(INDICATOR_SHORTNAME, g_indicator_short_name); g_control_y_offset = 0; const int objects_before_create = ObjectsTotal(0, -1, -1); g_object_prefix = "RGB." + IntegerToString((long)ChartID()) + "." + IntegerToString(g_instance_window) + "." + IntegerToString((long)HashText(InstanceIdentityKey())) + "." + IntegerToString((long)GetMicrosecondCount()) + "." + IntegerToString(objects_before_create) + "."; CreateControlObjects(); ChartRedraw(0); if(EventSetTimer(5)) g_cache_timer_enabled = true; else Print("RenkoDeepSeek: не удалось включить периодическое сохранение Renko-кэша; останется сохранение при деинициализации. Код ошибки=", GetLastError()); return INIT_SUCCEEDED; } //--- Периодическое сохранение изменённого состояния Renko-кэша. void OnTimer() { if(!g_instance_identity_ready || !g_settings_initialized || !g_cache_dirty) return; if(!g_history_initialized && g_rebuild_resume_from <= 0) return; SaveRenkoCache(); } //--- Удаление элементов управления текущего экземпляра. void OnDeinit(const int reason) { if(g_cache_timer_enabled) { EventKillTimer(); g_cache_timer_enabled = false; } const int resolved_window = ChartWindowFind(); const int marker_window = (g_instance_marker_name == "" ? -1 : ObjectFind(0, g_instance_marker_name)); if(resolved_window > 0 && marker_window == resolved_window) g_instance_window = resolved_window; else if(marker_window != g_instance_window) { g_instance_identity_ready = false; Print("RenkoDeepSeek: маркер экземпляра больше не соответствует подокну; сохранение настроек и кэша пропущено во избежание записи в чужое пространство."); } if(g_instance_identity_ready && g_settings_initialized && g_instance_window > 0) { if(g_current_brick_points >= 1 && !SaveBrickPoints(g_current_brick_points)) Print("RenkoDeepSeek: не удалось повторно сохранить размер кирпича при деинициализации."); if(!SaveButtonSettings()) Print("RenkoDeepSeek: не удалось повторно сохранить состояния кнопок при деинициализации."); } else if(g_settings_initialized && !g_instance_identity_ready) Print("RenkoDeepSeek: настройки не сохранены при деинициализации — идентичность экземпляра не подтверждена."); else if(g_settings_initialized) Print("RenkoDeepSeek: настройки не сохранены при деинициализации — подокно экземпляра не удалось определить."); if(g_instance_identity_ready && (g_history_initialized || g_rebuild_resume_from > 0) && !SaveRenkoCache()) Print("RenkoDeepSeek: снимок состояния не сохранён; при следующем запуске может потребоваться восстановление истории."); if(g_object_prefix != "") { for(int i = ObjectsTotal(0, -1, -1) - 1; i >= 0; --i) { const string object_name = ObjectName(0, i, -1, -1); if(StringFind(object_name, g_object_prefix) == 0) ObjectDelete(0, object_name); } } } //--- Обработка нажатий кнопок индикатора. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id != CHARTEVENT_OBJECT_CLICK) return; const int current_window = ChartWindowFind(); if(current_window <= 0 || ObjectFind(0, g_instance_marker_name) != current_window) { g_instance_identity_ready = false; g_price_grid_valid = false; Print("RenkoDeepSeek: идентичность подокна не подтверждена; действие кнопки отклонено."); return; } if(current_window != g_instance_window && HasOtherRenkoInstanceInWindow(current_window)) { g_instance_identity_ready = false; g_price_grid_valid = false; Print("RenkoDeepSeek: обнаружен конфликт экземпляров в подокне; действие кнопки отклонено."); return; } g_instance_window = current_window; if(sparam == g_brick_apply_name) { ApplyBrickSizeFromEdit(); return; } bool handled = true; if(sparam == g_breakout_high_button_name) g_breakout_high_enabled = !g_breakout_high_enabled; else if(sparam == g_breakout_low_button_name) g_breakout_low_enabled = !g_breakout_low_enabled; else if(sparam == g_brick_reversal_button_name) g_brick_reversal_enabled = !g_brick_reversal_enabled; else if(sparam == g_pre_buy_button_name) g_pre_buy_enabled = !g_pre_buy_enabled; else if(sparam == g_pre_sell_button_name) g_pre_sell_enabled = !g_pre_sell_enabled; else if(sparam == g_alert_button_name) { // Любая активная категория означает, что нажатие выключает все; // полностью выключенное состояние переключается сразу во включённое. const bool target = !g_alert_enabled; g_breakout_high_enabled = target; g_breakout_low_enabled = target; g_brick_reversal_enabled = target; g_pre_buy_enabled = target; g_pre_sell_enabled = target; } else handled = false; if(!handled) return; SyncAlertMasterState(); RefreshAllAlertButtons(); UpdatePreBreakoutPanel(g_last_bid, true); SaveButtonSettings(); ChartRedraw(0); } //--- Обновление истории и обработка новых рыночных тиков. int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { if(rates_total <= 0 || !g_price_grid_valid || !g_instance_identity_ready) return 0; const int current_window = ChartWindowFind(); if(current_window <= 0) { g_instance_identity_ready = false; g_price_grid_valid = false; Print("RenkoDeepSeek: не удалось подтвердить отдельное подокно экземпляра; расчёт остановлен."); return 0; } if(current_window > 0 && ObjectFind(0, g_instance_marker_name) != current_window) { g_instance_identity_ready = false; g_price_grid_valid = false; Print("RenkoDeepSeek: маркер идентичности отсутствует в собственном подокне; расчёт остановлен."); return 0; } if(current_window > 0 && current_window != g_instance_window) { if(HasOtherRenkoInstanceInWindow(current_window)) { g_instance_identity_ready = false; g_price_grid_valid = false; Print("RenkoDeepSeek: после изменения структуры графика обнаружен конфликт экземпляров; расчёт остановлен."); return 0; } g_instance_window = current_window; } RefreshServerOffset(); if(g_alert_button_name == "" || ObjectFind(0, g_alert_button_name) < 0 || g_breakout_high_button_name == "" || ObjectFind(0, g_breakout_high_button_name) < 0 || g_breakout_low_button_name == "" || ObjectFind(0, g_breakout_low_button_name) < 0 || g_brick_reversal_button_name == "" || ObjectFind(0, g_brick_reversal_button_name) < 0 || g_pre_buy_button_name == "" || ObjectFind(0, g_pre_buy_button_name) < 0 || g_pre_sell_button_name == "" || ObjectFind(0, g_pre_sell_button_name) < 0 || g_pre_status_label_name == "" || ObjectFind(0,g_pre_status_label_name)<0 || g_pre_detail_label_name == "" || ObjectFind(0,g_pre_detail_label_name)<0 || g_pre_sl_label_name == "" || ObjectFind(0,g_pre_sl_label_name)<0 || g_brick_edit_name == "" || ObjectFind(0, g_brick_edit_name) < 0 || g_brick_apply_name == "" || ObjectFind(0, g_brick_apply_name) < 0) { CreateControlObjects(); ChartRedraw(0); } int retry_delay = 30; for(int retry_step = 0; retry_step < g_history_retry_count && retry_delay < 900; ++retry_step) retry_delay = (int)MathMin(retry_delay * 2, 900); const bool need_history = (!g_history_initialized || g_history_needs_more); const ulong history_wait_ms = (g_history_needs_more ? (ulong)HISTORY_RESUME_DELAY_MS : (ulong)retry_delay * 1000); if(need_history && (g_last_history_attempt_ms == 0 || GetTickCount64() - g_last_history_attempt_ms >= history_wait_ms)) { if(RebuildHistory()) { g_history_retry_count = 0; // «Тики получены» без кирпичей — не инициализация: пересборка // должна продолжаться, иначе индикатор останется пустым навсегда. g_history_initialized = HasUsableRenkoState(); } else if(g_history_retry_count < 10) ++g_history_retry_count; } // Пропущенные интервалы догружаем в фоне. TryFillHistoryGaps(rates_total); if(g_history_initialized) { MqlTick latest_tick; if(SymbolInfoTick(_Symbol, latest_tick) && latest_tick.bid > 0.0) { if(g_last_tick_msc == 0) { ProcessBid((datetime)latest_tick.time, latest_tick.bid, true); g_last_tick_msc = latest_tick.time_msc; g_ticks_processed_at_last_msc = 1; g_last_bid = latest_tick.bid; g_cache_dirty = true; } else if(latest_tick.time_msc >= g_last_tick_msc) { // Догоняем сервер короткими интервалами: один большой запрос // (например, после выходных) упирается в тайм-аут 4403. int catchup_guard = 0; while(catchup_guard++ < 240 && g_last_tick_msc < latest_tick.time_msc) { const long cursor_before = g_last_tick_msc; long request_to = cursor_before + (long)HISTORY_CHUNK_SECONDS * 1000; if(request_to > latest_tick.time_msc) request_to = latest_tick.time_msc; MqlTick new_ticks[]; ResetLastError(); const int copied = CopyTicksRange(_Symbol, new_ticks, COPY_TICKS_INFO, (ulong)cursor_before, (ulong)request_to); const int copy_error = GetLastError(); if(copied < 0 && copy_error != 4403) { Print("RenkoDeepSeek: догон тиков приостановлен: CopyTicksRange не подтвердил диапазон после курсора (код=", copy_error, "). Текущая серия и курсор сохранены; будет повтор того же диапазона без полной пересборки."); break; } if(copy_error == 4403 && copied <= 0) { if(g_last_tick_copy_warning == 0 || TimeCurrent() - g_last_tick_copy_warning >= 10) { Print("RenkoDeepSeek: догон тиков приостановлен (4403; доступных тиков=", copied, ") на курсоре ", cursor_before, "; повтор при следующем тике."); g_last_tick_copy_warning = TimeCurrent(); } break; } if(copied == 0 && copy_error == 0) { // Пустой ответ не доказывает, что участок выгружен. Курсор // сдвигаем только по давно прошедшему интервалу: иначе можно // навсегда потерять реальные тики (и сделанные по ним сигналы). if(request_to + EMPTY_RANGE_CONFIRM_MS < latest_tick.time_msc) { g_last_tick_msc = request_to; g_ticks_processed_at_last_msc = 0; g_cache_dirty = true; } else break; } if(copied > 0) { int seen_at_cursor_msc = 0; for(int i = 0; i < copied; ++i) { const long tick_msc = new_ticks[i].time_msc; if(tick_msc < g_last_tick_msc) continue; if(tick_msc == g_last_tick_msc) { ++seen_at_cursor_msc; if(seen_at_cursor_msc <= g_ticks_processed_at_last_msc) continue; } if(new_ticks[i].bid <= 0.0) { if(tick_msc > g_last_tick_msc) { g_last_tick_msc = tick_msc; g_ticks_processed_at_last_msc = 1; } else if(tick_msc == g_last_tick_msc) ++g_ticks_processed_at_last_msc; g_cache_dirty = true; continue; } // Оповещения разрешены только по «живым» тикам: догон // не должен выдавать алерты по событиям, которые уже прошли. const bool live_tick = (latest_tick.time_msc - tick_msc) <= TICK_LIVE_ALERT_WINDOW_MS; ProcessBid((datetime)new_ticks[i].time, new_ticks[i].bid, live_tick); if(tick_msc > g_last_tick_msc) { g_last_tick_msc = tick_msc; g_ticks_processed_at_last_msc = 1; } else if(tick_msc == g_last_tick_msc) ++g_ticks_processed_at_last_msc; g_last_bid = new_ticks[i].bid; g_cache_dirty = true; } } if(copy_error == 4403) { if(g_last_tick_copy_warning == 0 || TimeCurrent() - g_last_tick_copy_warning >= 10) { Print("RenkoDeepSeek: обработаны доступные тики при тайм-ауте 4403; догон продолжится при следующем тике."); g_last_tick_copy_warning = TimeCurrent(); } // Не запрашиваем следующий интервал: текущий ответ неполон. // Следующий OnCalculate повторит запрос с обновлённого курсора. break; } // Курсор не сдвинулся — дальше крутиться бессмысленно. if(g_last_tick_msc <= cursor_before) break; } } else { // Серверное время временно отстало от сохранённого курсора. // Не обнуляем пригодную серию и не запускаем полную пересборку: // ждём, пока поток тиков достигнет сохранённой временной позиции. if(g_last_tick_copy_warning == 0 || TimeCurrent() - g_last_tick_copy_warning >= 60) { Print("RenkoDeepSeek: курсор тиков (", g_last_tick_msc, " мс) впереди времени последнего тика (", latest_tick.time_msc, " мс); обработка приостановлена без сброса кэша и серии."); g_last_tick_copy_warning = TimeCurrent(); } } } } if(g_last_bid>0.0) UpdatePreBreakoutPanel(g_last_bid,false); RenderBricks(rates_total, false); return rates_total; } //+------------------------------------------------------------------+