| //+------------------------------------------------------------------+
|
| //| 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 && bsl<buy);
|
| const bool sv=(sell>0.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 &&
|
| sell<bid && sd*_Point<=threshold);
|
|
|
| string status="Ожидание подхода к уровню";
|
| string detail=_Symbol+" | Bid="+bid_text;
|
| string slinfo="SL: нет активного сигнала";
|
| color status_color=clrSilver;
|
|
|
| if(g_swing_high_breakout_alerted && buy>0.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 && bid<level && dist*_Point>rearm)
|
| {
|
| 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<level && dist>=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 && sl<level);
|
| g_pre_buy_alerted=true;
|
| NotifyUser(StringFormat("RenkoDeepSeek: ПРЕДУПРЕЖДЕНИЕ ДО ПРОБОЯ; BUY; %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_buy_alerted=false;
|
| g_pre_buy_cancelled=false;
|
| }
|
|
|
| if(g_last_swing_low>0.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;
|
| }
|
| //+------------------------------------------------------------------+ |