mql5/Indicators/RenkoDeepSeek2.mq5
3502 lines
166 KiB
MQL5
rytikov0285
2026-11-05 13:25:46 +03:00
//+------------------------------------------------------------------+
//| 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;
}
//+------------------------------------------------------------------+