//+------------------------------------------------------------------+
//|                 50_Multi_Symbol_Trend_Consensus_v1_00.mq5       |
//|                                        FXおもしろラボ             |
//|                           https://fx-omoshiro-lab.com/           |
//+------------------------------------------------------------------+
#property copyright "FXおもしろラボ"
#property link      "https://fx-omoshiro-lab.com/"
#property version   "1.00"
#property strict
#property description "複数銘柄の確定足EMAを同一時刻で比較するダッシュボード"
#property description "有効銘柄だけを分母にし、欠損と80%到達を分けて表示します"
#property description "表示専用です。注文・ポジション変更は行いません"

#property indicator_chart_window
#property indicator_buffers 12
#property indicator_plots   12

#define MAX_SYMBOLS 12
#define DIAGNOSTIC_BUFFER_COUNT 12
#define OBJECT_PREFIX "MQL50_"

input group "==== Symbol Settings ===="
input string InpSymbols =
   "EURUSD,GBPUSD,USDJPY,USDCHF,USDCAD,AUDUSD,NZDUSD,EURJPY,GBPJPY,EURGBP,AUDJPY,CADJPY";
input ENUM_TIMEFRAMES InpTimeframe = PERIOD_H1;

input group "==== Trend Rule ===="
input int InpFastMAPeriod = 20;
input int InpSlowMAPeriod = 50;
input ENUM_MA_METHOD InpMAMethod = MODE_EMA;
input ENUM_APPLIED_PRICE InpAppliedPrice = PRICE_CLOSE;
input double InpBullishThresholdPct = 80.0;
input int InpMinimumValidSymbols = 8;

input group "==== Refresh / Display ===="
input int InpRefreshSeconds = 2;
input bool InpShowMAValues = true;

double ReadyBuffer[];
double SymbolCountBuffer[];
double ValidCountBuffer[];
double BullishCountBuffer[];
double BearishCountBuffer[];
double NeutralCountBuffer[];
double MissingCountBuffer[];
double BullishPercentBuffer[];
double TargetMetBuffer[];
double AnchorTimeBuffer[];
double ObjectCountBuffer[];
double RefreshCountBuffer[];

string g_base_symbols[];
string g_symbols[];
int g_states[];
double g_fast_values[];
double g_slow_values[];

int g_active_fast_handle = INVALID_HANDLE;
int g_active_slow_handle = INVALID_HANDLE;
int g_scan_index = 0;
int g_scan_wait_ticks = 0;
bool g_scan_in_progress = false;
datetime g_scan_anchor_time = 0;
datetime g_last_completed_local = 0;

int g_symbol_count = 0;
int g_valid_count = 0;
int g_bullish_count = 0;
int g_bearish_count = 0;
int g_neutral_count = 0;
int g_missing_count = 0;
double g_bullish_percent = 0.0;
bool g_target_met = false;
datetime g_anchor_time = 0;
ulong g_refresh_count = 0;

enum MQL50_TREND_STATE
{
   MQL50_MISSING = -2,
   MQL50_BEARISH = -1,
   MQL50_NEUTRAL = 0,
   MQL50_BULLISH = 1
};

ENUM_TIMEFRAMES UsedTimeframe();
bool ParseSymbols();
string Trimmed(string value);
bool IsDuplicateBase(const string base_symbol, const int count);
string ResolveBrokerSymbol(const string base_symbol);
bool TrySelectSymbol(const string symbol);
string CanonicalFXSymbol(const string symbol);
bool ResolveSymbols();
void StartScan(const datetime anchor_time);
void ProcessScanStep();
bool CreateActiveHandles();
bool ReadActiveTrend(int &state,
                     double &fast_value, double &slow_value);
void AdvanceScan(const int state,
                 const double fast_value, const double slow_value);
void FinalizeScan();
void ReleaseActiveHandles();
bool IsUsableValue(const double value);
void WriteDiagnosticBuffers();
void RenderDashboard();
bool EnsureRectangle(const string name, const int x, const int y,
                     const int width, const int height,
                     const color background, const color border);
void SetLabel(const string name, const int x, const int y,
              const string text, const color text_color,
              const int font_size);
string StateText(const int state);
color StateColor(const int state);
string PriceText(const string symbol, const double value);
int CountDashboardObjects();
void DeleteDashboard();

//+------------------------------------------------------------------+
//| 初期化                                                            |
//+------------------------------------------------------------------+
int OnInit()
{
   if(InpFastMAPeriod < 1 ||
      InpSlowMAPeriod <= InpFastMAPeriod ||
      InpBullishThresholdPct < 0.0 ||
      InpBullishThresholdPct > 100.0 ||
      InpMinimumValidSymbols < 1 ||
      InpMinimumValidSymbols > MAX_SYMBOLS ||
      InpRefreshSeconds < 1)
   {
      Print("Multi-symbol consensus: invalid input parameters.");
      return INIT_PARAMETERS_INCORRECT;
   }

   SetIndexBuffer(0, ReadyBuffer, INDICATOR_DATA);
   SetIndexBuffer(1, SymbolCountBuffer, INDICATOR_DATA);
   SetIndexBuffer(2, ValidCountBuffer, INDICATOR_DATA);
   SetIndexBuffer(3, BullishCountBuffer, INDICATOR_DATA);
   SetIndexBuffer(4, BearishCountBuffer, INDICATOR_DATA);
   SetIndexBuffer(5, NeutralCountBuffer, INDICATOR_DATA);
   SetIndexBuffer(6, MissingCountBuffer, INDICATOR_DATA);
   SetIndexBuffer(7, BullishPercentBuffer, INDICATOR_DATA);
   SetIndexBuffer(8, TargetMetBuffer, INDICATOR_DATA);
   SetIndexBuffer(9, AnchorTimeBuffer, INDICATOR_DATA);
   SetIndexBuffer(10, ObjectCountBuffer, INDICATOR_DATA);
   SetIndexBuffer(11, RefreshCountBuffer, INDICATOR_DATA);

   ArraySetAsSeries(ReadyBuffer, true);
   ArraySetAsSeries(SymbolCountBuffer, true);
   ArraySetAsSeries(ValidCountBuffer, true);
   ArraySetAsSeries(BullishCountBuffer, true);
   ArraySetAsSeries(BearishCountBuffer, true);
   ArraySetAsSeries(NeutralCountBuffer, true);
   ArraySetAsSeries(MissingCountBuffer, true);
   ArraySetAsSeries(BullishPercentBuffer, true);
   ArraySetAsSeries(TargetMetBuffer, true);
   ArraySetAsSeries(AnchorTimeBuffer, true);
   ArraySetAsSeries(ObjectCountBuffer, true);
   ArraySetAsSeries(RefreshCountBuffer, true);
   for(int plot = 0; plot < DIAGNOSTIC_BUFFER_COUNT; ++plot)
   {
      PlotIndexSetInteger(plot, PLOT_DRAW_TYPE, DRAW_NONE);
      PlotIndexSetDouble(plot, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   }

   if(!ParseSymbols() || !ResolveSymbols())
   {
      ReleaseActiveHandles();
      return INIT_FAILED;
   }

   IndicatorSetString(
      INDICATOR_SHORTNAME,
      StringFormat("Multi-Symbol Trend Consensus (%d,%s)",
                   g_symbol_count, EnumToString(UsedTimeframe()))
   );

   ResetLastError();
   if(!EventSetTimer(InpRefreshSeconds))
   {
      PrintFormat("Multi-symbol consensus: EventSetTimer failed error=%d",
                  GetLastError());
      ReleaseActiveHandles();
      return INIT_FAILED;
   }

   const datetime anchor_time = iTime(_Symbol, UsedTimeframe(), 1);
   if(anchor_time > 0)
   {
      StartScan(anchor_time);
      ProcessScanStep();
   }
   else
      RenderDashboard();
   return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| 終了処理                                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   EventKillTimer();
   ReleaseActiveHandles();
   DeleteDashboard();
   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 < 2)
      return 0;

   const datetime current_anchor = iTime(_Symbol, UsedTimeframe(), 1);
   if(current_anchor > 0 &&
      (!g_scan_in_progress && current_anchor != g_anchor_time))
      StartScan(current_anchor);
   ProcessScanStep();
   WriteDiagnosticBuffers();
   return rates_total;
}

void OnTimer()
{
   const datetime current_anchor = iTime(_Symbol, UsedTimeframe(), 1);
   if(current_anchor <= 0)
      return;

   const bool retry_missing =
      !g_scan_in_progress &&
      g_refresh_count > 0 &&
      g_missing_count > 0 &&
      (TimeLocal() - g_last_completed_local) >= 30;
   if(!g_scan_in_progress &&
      (current_anchor != g_anchor_time || retry_missing))
      StartScan(current_anchor);
   ProcessScanStep();
}

//+------------------------------------------------------------------+
//| 使用時間足                                                        |
//+------------------------------------------------------------------+
ENUM_TIMEFRAMES UsedTimeframe()
{
   return InpTimeframe == PERIOD_CURRENT ?
          (ENUM_TIMEFRAMES)_Period : InpTimeframe;
}

//+------------------------------------------------------------------+
//| CSV形式の銘柄一覧を安全に分解                                     |
//+------------------------------------------------------------------+
bool ParseSymbols()
{
   string raw[];
   const ushort separator = StringGetCharacter(",", 0);
   const int raw_count = StringSplit(InpSymbols, separator, raw);
   if(raw_count <= 0)
   {
      Print("Multi-symbol consensus: symbol list is empty.");
      return false;
   }

   ArrayResize(g_base_symbols, MAX_SYMBOLS);
   g_symbol_count = 0;
   for(int index = 0;
       index < raw_count && g_symbol_count < MAX_SYMBOLS;
       ++index)
   {
      string base_symbol = Trimmed(raw[index]);
      StringToUpper(base_symbol);
      if(base_symbol == "" ||
         IsDuplicateBase(base_symbol, g_symbol_count))
         continue;
      g_base_symbols[g_symbol_count] = base_symbol;
      ++g_symbol_count;
   }
   ArrayResize(g_base_symbols, g_symbol_count);

   if(g_symbol_count == 0)
   {
      Print("Multi-symbol consensus: no usable symbols.");
      return false;
   }
   if(InpMinimumValidSymbols > g_symbol_count)
   {
      PrintFormat(
         "Multi-symbol consensus: minimum valid symbols (%d) exceed list size (%d).",
         InpMinimumValidSymbols, g_symbol_count);
      return false;
   }
   return true;
}

string Trimmed(string value)
{
   StringTrimLeft(value);
   StringTrimRight(value);
   return value;
}

bool IsDuplicateBase(const string base_symbol, const int count)
{
   for(int index = 0; index < count; ++index)
   {
      if(g_base_symbols[index] == base_symbol)
         return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| ブローカー固有の接頭辞・接尾辞を補正                              |
//+------------------------------------------------------------------+
string ResolveBrokerSymbol(const string base_symbol)
{
   if(TrySelectSymbol(base_symbol))
      return base_symbol;

   const string chart_key = CanonicalFXSymbol(_Symbol);
   const int chart_key_pos = StringFind(_Symbol, chart_key);
   if(chart_key != "" && chart_key_pos >= 0)
   {
      const string prefix = StringSubstr(_Symbol, 0, chart_key_pos);
      const int suffix_start = chart_key_pos + StringLen(chart_key);
      const string suffix = StringSubstr(_Symbol, suffix_start);
      const string candidate = prefix + base_symbol + suffix;
      if(TrySelectSymbol(candidate))
         return candidate;
   }

   string best = "";
   int best_length = 2147483647;
   const int total = SymbolsTotal(false);
   for(int index = 0; index < total; ++index)
   {
      const string candidate = SymbolName(index, false);
      string upper_candidate = candidate;
      StringToUpper(upper_candidate);
      if(StringFind(upper_candidate, base_symbol) < 0)
         continue;
      const int candidate_length = StringLen(candidate);
      if(candidate_length < best_length && TrySelectSymbol(candidate))
      {
         best = candidate;
         best_length = candidate_length;
      }
   }
   return best;
}

bool TrySelectSymbol(const string symbol)
{
   if(symbol == "")
      return false;
   ResetLastError();
   return SymbolSelect(symbol, true);
}

string CanonicalFXSymbol(const string symbol)
{
   string upper = symbol;
   StringToUpper(upper);
   string currencies[8] =
   {
      "USD", "EUR", "GBP", "JPY",
      "CHF", "CAD", "AUD", "NZD"
   };
   for(int base = 0; base < 8; ++base)
   {
      for(int quote = 0; quote < 8; ++quote)
      {
         if(base == quote)
            continue;
         const string candidate = currencies[base] + currencies[quote];
         if(StringFind(upper, candidate) >= 0)
            return candidate;
      }
   }
   return "";
}

//+------------------------------------------------------------------+
//| 銘柄名を解決し、逐次走査用の配列を準備                            |
//+------------------------------------------------------------------+
bool ResolveSymbols()
{
   ArrayResize(g_symbols, g_symbol_count);
   ArrayResize(g_states, g_symbol_count);
   ArrayResize(g_fast_values, g_symbol_count);
   ArrayResize(g_slow_values, g_symbol_count);

   int resolved_count = 0;
   for(int index = 0; index < g_symbol_count; ++index)
   {
      g_states[index] = MQL50_MISSING;
      g_fast_values[index] = EMPTY_VALUE;
      g_slow_values[index] = EMPTY_VALUE;
      g_symbols[index] = ResolveBrokerSymbol(g_base_symbols[index]);
      if(g_symbols[index] == "")
         continue;
      ++resolved_count;
   }

   if(resolved_count == 0)
   {
      Print("Multi-symbol consensus: no indicator handles were created.");
      return false;
   }
   return true;
}

//+------------------------------------------------------------------+
//| 新しい同一時刻スキャンを開始                                      |
//+------------------------------------------------------------------+
void StartScan(const datetime anchor_time)
{
   ReleaseActiveHandles();
   g_scan_anchor_time = anchor_time;
   g_scan_index = 0;
   g_scan_wait_ticks = 0;
   g_scan_in_progress = true;
   for(int index = 0; index < g_symbol_count; ++index)
   {
      g_states[index] = MQL50_MISSING;
      g_fast_values[index] = EMPTY_VALUE;
      g_slow_values[index] = EMPTY_VALUE;
   }
   RenderDashboard();
}

//+------------------------------------------------------------------+
//| 1回のイベントで1銘柄だけ処理し、常駐ハンドルを2本に抑える          |
//+------------------------------------------------------------------+
void ProcessScanStep()
{
   if(!g_scan_in_progress)
      return;
   if(g_scan_index >= g_symbol_count)
   {
      FinalizeScan();
      return;
   }

   if(g_symbols[g_scan_index] == "")
   {
      AdvanceScan(MQL50_MISSING, EMPTY_VALUE, EMPTY_VALUE);
      return;
   }
   if(g_active_fast_handle == INVALID_HANDLE ||
      g_active_slow_handle == INVALID_HANDLE)
   {
      if(!CreateActiveHandles())
      {
         AdvanceScan(MQL50_MISSING, EMPTY_VALUE, EMPTY_VALUE);
         return;
      }
   }

   int state = MQL50_MISSING;
   double fast_value = EMPTY_VALUE;
   double slow_value = EMPTY_VALUE;
   if(ReadActiveTrend(state, fast_value, slow_value))
   {
      AdvanceScan(state, fast_value, slow_value);
      return;
   }

   ++g_scan_wait_ticks;
   if(g_scan_wait_ticks >= 20)
   {
      PrintFormat(
         "Multi-symbol consensus: timed out waiting for %s at %s",
         g_symbols[g_scan_index],
         TimeToString(g_scan_anchor_time,
                      TIME_DATE | TIME_MINUTES));
      AdvanceScan(MQL50_MISSING, EMPTY_VALUE, EMPTY_VALUE);
   }
}

bool CreateActiveHandles()
{
   ReleaseActiveHandles();
   if(g_scan_index < 0 || g_scan_index >= g_symbol_count)
      return false;

   const ENUM_TIMEFRAMES timeframe = UsedTimeframe();
   ResetLastError();
   g_active_fast_handle =
      iMA(g_symbols[g_scan_index], timeframe,
          InpFastMAPeriod, 0, InpMAMethod, InpAppliedPrice);
   g_active_slow_handle =
      iMA(g_symbols[g_scan_index], timeframe,
          InpSlowMAPeriod, 0, InpMAMethod, InpAppliedPrice);
   if(g_active_fast_handle == INVALID_HANDLE ||
      g_active_slow_handle == INVALID_HANDLE)
   {
      PrintFormat(
         "Multi-symbol consensus: handle creation failed symbol=%s error=%d",
         g_symbols[g_scan_index], GetLastError());
      ReleaseActiveHandles();
      return false;
   }
   return true;
}

bool ReadActiveTrend(int &state,
                     double &fast_value, double &slow_value)
{
   state = MQL50_MISSING;
   fast_value = EMPTY_VALUE;
   slow_value = EMPTY_VALUE;
   if(g_scan_index < 0 || g_scan_index >= g_symbol_count ||
      g_active_fast_handle == INVALID_HANDLE ||
      g_active_slow_handle == INVALID_HANDLE)
      return false;

   const string symbol = g_symbols[g_scan_index];
   const ENUM_TIMEFRAMES timeframe = UsedTimeframe();
   if(!SeriesInfoInteger(symbol, timeframe,
                         SERIES_SYNCHRONIZED))
      return false;

   ResetLastError();
   const int shift =
      iBarShift(symbol, timeframe, g_scan_anchor_time, true);
   if(shift < 1)
      return false;
   if(BarsCalculated(g_active_fast_handle) <= shift ||
      BarsCalculated(g_active_slow_handle) <= shift)
      return false;

   double fast_buffer[1];
   double slow_buffer[1];
   ResetLastError();
   const int fast_copied =
      CopyBuffer(g_active_fast_handle, 0, shift, 1, fast_buffer);
   const int fast_error = GetLastError();
   ResetLastError();
   const int slow_copied =
      CopyBuffer(g_active_slow_handle, 0, shift, 1, slow_buffer);
   const int slow_error = GetLastError();
   if(fast_copied != 1 || slow_copied != 1)
   {
      if(fast_error != 0 || slow_error != 0)
      {
         PrintFormat(
            "Multi-symbol consensus: data unavailable symbol=%s fast_error=%d slow_error=%d",
            symbol, fast_error, slow_error);
      }
      return false;
   }

   fast_value = fast_buffer[0];
   slow_value = slow_buffer[0];
   if(!IsUsableValue(fast_value) || !IsUsableValue(slow_value))
      return false;

   const double point =
      SymbolInfoDouble(symbol, SYMBOL_POINT);
   const double equality_tolerance =
      point > 0.0 ? point * 0.1 : 0.0;
   if(fast_value > slow_value + equality_tolerance)
      state = MQL50_BULLISH;
   else if(fast_value < slow_value - equality_tolerance)
      state = MQL50_BEARISH;
   else
      state = MQL50_NEUTRAL;
   return true;
}

void AdvanceScan(const int state,
                 const double fast_value, const double slow_value)
{
   if(g_scan_index >= 0 && g_scan_index < g_symbol_count)
   {
      g_states[g_scan_index] = state;
      g_fast_values[g_scan_index] = fast_value;
      g_slow_values[g_scan_index] = slow_value;
   }
   ReleaseActiveHandles();
   ++g_scan_index;
   g_scan_wait_ticks = 0;
   if(g_scan_index >= g_symbol_count)
      FinalizeScan();
   else
      RenderDashboard();
}

void FinalizeScan()
{
   ReleaseActiveHandles();
   g_valid_count = 0;
   g_bullish_count = 0;
   g_bearish_count = 0;
   g_neutral_count = 0;
   g_missing_count = 0;
   for(int index = 0; index < g_symbol_count; ++index)
   {
      if(g_states[index] == MQL50_BULLISH)
      {
         ++g_valid_count;
         ++g_bullish_count;
      }
      else if(g_states[index] == MQL50_BEARISH)
      {
         ++g_valid_count;
         ++g_bearish_count;
      }
      else if(g_states[index] == MQL50_NEUTRAL)
      {
         ++g_valid_count;
         ++g_neutral_count;
      }
      else
         ++g_missing_count;
   }

   g_anchor_time = g_scan_anchor_time;
   g_bullish_percent =
      g_valid_count > 0 ?
      100.0 * (double)g_bullish_count / (double)g_valid_count : 0.0;
   g_target_met =
      g_valid_count >= InpMinimumValidSymbols &&
      g_bullish_percent >= InpBullishThresholdPct;
   ++g_refresh_count;
   g_scan_in_progress = false;
   g_last_completed_local = TimeLocal();
   RenderDashboard();
   WriteDiagnosticBuffers();
}

void ReleaseActiveHandles()
{
   if(g_active_fast_handle != INVALID_HANDLE)
   {
      IndicatorRelease(g_active_fast_handle);
      g_active_fast_handle = INVALID_HANDLE;
   }
   if(g_active_slow_handle != INVALID_HANDLE)
   {
      IndicatorRelease(g_active_slow_handle);
      g_active_slow_handle = INVALID_HANDLE;
   }
}

bool IsUsableValue(const double value)
{
   return value != EMPTY_VALUE &&
          MathIsValidNumber(value) &&
          value > 0.0;
}

//+------------------------------------------------------------------+
//| 検証用バッファ                                                    |
//+------------------------------------------------------------------+
void WriteDiagnosticBuffers()
{
   if(ArraySize(ReadyBuffer) < 1)
      return;

   ReadyBuffer[0] =
      (g_refresh_count > 0 &&
       g_anchor_time > 0 &&
       g_valid_count >= InpMinimumValidSymbols) ? 1.0 : 0.0;
   SymbolCountBuffer[0] = (double)g_symbol_count;
   ValidCountBuffer[0] = (double)g_valid_count;
   BullishCountBuffer[0] = (double)g_bullish_count;
   BearishCountBuffer[0] = (double)g_bearish_count;
   NeutralCountBuffer[0] = (double)g_neutral_count;
   MissingCountBuffer[0] = (double)g_missing_count;
   BullishPercentBuffer[0] = g_bullish_percent;
   TargetMetBuffer[0] = g_target_met ? 1.0 : 0.0;
   AnchorTimeBuffer[0] = (double)(long)g_anchor_time;
   ObjectCountBuffer[0] = (double)CountDashboardObjects();
   RefreshCountBuffer[0] = (double)g_refresh_count;
}

//+------------------------------------------------------------------+
//| ダッシュボード描画                                                |
//+------------------------------------------------------------------+
void RenderDashboard()
{
   const int panel_width = 694;
   const int panel_height = 448;
   if(!EnsureRectangle(OBJECT_PREFIX + "PANEL",
                       24, 28, panel_width, panel_height,
                       C'7,13,20', C'54,97,120'))
      return;

   SetLabel(OBJECT_PREFIX + "HEADER", 44, 46,
            "MT5  MULTI-SYMBOL TREND CONSENSUS",
            clrDeepSkyBlue, 15);
   SetLabel(
      OBJECT_PREFIX + "ANCHOR", 44, 82,
      StringFormat("ANCHOR  %s  %s  %s  |  CONFIRMED",
                   _Symbol,
                   StringSubstr(EnumToString(UsedTimeframe()), 7),
                   g_anchor_time > 0 ?
                      TimeToString(g_anchor_time,
                                   TIME_DATE | TIME_MINUTES) : "--"),
      g_anchor_time > 0 ? clrWhite : clrTomato, 11);
   SetLabel(
      OBJECT_PREFIX + "RULE", 44, 112,
      StringFormat("RULE       %s %d > %s %d  |  VALID MIN %d",
                   EnumToString(InpMAMethod), InpFastMAPeriod,
                   EnumToString(InpMAMethod), InpSlowMAPeriod,
                   InpMinimumValidSymbols),
      clrSilver, 11);
   SetLabel(
      OBJECT_PREFIX + "SUMMARY", 44, 142,
      StringFormat("BULL  %d/%d = %.1f%%  |  TARGET %.1f%%  |  %s",
                   g_bullish_count, g_valid_count,
                   g_bullish_percent, InpBullishThresholdPct,
                   g_target_met ? "TARGET MET" : "BELOW TARGET"),
      g_target_met ? clrLime : clrGold, 11);

   for(int index = 0; index < g_symbol_count; ++index)
   {
      const int column = index / 6;
      const int row = index % 6;
      const int x = column == 0 ? 44 : 376;
      const int y = 184 + row * 34;
      const string display_symbol =
         g_symbols[index] == "" ?
         g_base_symbols[index] : g_symbols[index];
      string line =
         StringFormat("%02d  %-10s  %-7s",
                      index + 1, display_symbol,
                      StateText(g_states[index]));
      if(InpShowMAValues && g_states[index] != MQL50_MISSING)
      {
         line +=
            "  " + PriceText(display_symbol, g_fast_values[index]) +
            " / " + PriceText(display_symbol, g_slow_values[index]);
      }
      SetLabel(OBJECT_PREFIX + "SYMBOL_" + IntegerToString(index),
               x, y, line, StateColor(g_states[index]), 10);
   }

   SetLabel(
      OBJECT_PREFIX + "FOOTER", 44, 408,
      StringFormat(
         "VALID %d | BULL %d BEAR %d FLAT %d MISS %d | NO ORDERS",
         g_valid_count, g_bullish_count, g_bearish_count,
         g_neutral_count, g_missing_count),
      g_missing_count == 0 ? clrLime : clrTomato, 10);
   ChartRedraw(0);
}

bool EnsureRectangle(const string name, const int x, const int y,
                     const int width, const int height,
                     const color background, const color border)
{
   if(ObjectFind(0, name) < 0)
   {
      ResetLastError();
      if(!ObjectCreate(0, name, OBJ_RECTANGLE_LABEL, 0, 0, 0))
      {
         PrintFormat(
            "Multi-symbol consensus: rectangle creation failed error=%d",
            GetLastError());
         return false;
      }
   }
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
   ObjectSetInteger(0, name, OBJPROP_XSIZE, width);
   ObjectSetInteger(0, name, OBJPROP_YSIZE, height);
   ObjectSetInteger(0, name, OBJPROP_BGCOLOR, background);
   ObjectSetInteger(0, name, OBJPROP_BORDER_COLOR, border);
   ObjectSetInteger(0, name, OBJPROP_BORDER_TYPE, BORDER_FLAT);
   ObjectSetInteger(0, name, OBJPROP_ZORDER, 1);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
   return true;
}

void SetLabel(const string name, const int x, const int y,
              const string text, const color text_color,
              const int font_size)
{
   if(ObjectFind(0, name) < 0)
   {
      ResetLastError();
      if(!ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0))
      {
         PrintFormat(
            "Multi-symbol consensus: label creation failed name=%s error=%d",
            name, GetLastError());
         return;
      }
   }
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
   ObjectSetInteger(0, name, OBJPROP_COLOR, text_color);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, font_size);
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_ZORDER, 2);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
   ObjectSetString(0, name, OBJPROP_FONT, "Consolas");
   ObjectSetString(0, name, OBJPROP_TEXT, text);
}

string StateText(const int state)
{
   if(state == MQL50_BULLISH)
      return "BULL";
   if(state == MQL50_BEARISH)
      return "BEAR";
   if(state == MQL50_NEUTRAL)
      return "FLAT";
   return "NO DATA";
}

color StateColor(const int state)
{
   if(state == MQL50_BULLISH)
      return clrLime;
   if(state == MQL50_BEARISH)
      return clrTomato;
   if(state == MQL50_NEUTRAL)
      return clrSilver;
   return clrDarkGray;
}

string PriceText(const string symbol, const double value)
{
   if(!IsUsableValue(value))
      return "--";
   int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
   if(digits < 0 || digits > 10)
      digits = 5;
   return DoubleToString(value, digits);
}

int CountDashboardObjects()
{
   int count = 0;
   const int total = ObjectsTotal(0, -1, -1);
   for(int index = 0; index < total; ++index)
   {
      const string name = ObjectName(0, index, -1, -1);
      if(StringFind(name, OBJECT_PREFIX) == 0)
         ++count;
   }
   return count;
}

void DeleteDashboard()
{
   ObjectsDeleteAll(0, OBJECT_PREFIX);
}
