3502 lines
166 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| 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;
| |||
}
| |||
//+------------------------------------------------------------------+
|