首页 脚本库 搜索
返回

OilSpreadAnalyst

MQL4 EA · 智能交易系统 v1.0
98video 👁 21 浏览 ⬇ 14 下载 📅 2026-11-09 00:30:19 更新时间: 2026-11-09 00:43:46
下载源码

详情

产品概述

OilSpreadAnalyst 是一套用于 MetaTrader 4 的 Brent / WTI 原油相对价值分析仪表盘。它不会自动下单,重点帮助交易者回答三个问题:当前价差是多少、统计上偏贵还是偏便宜、扣除交易成本后是否仍值得关注。

核心功能

  • 实时显示 UKOILm(Brent)与 USOILm(WTI)的 BID / ASK、中间价和日涨跌幅。
  • 计算 Brent-WTI 中间价差、Brent/WTI 比价、Brent 溢价和双向执行价差。
  • 基于最近 N 根 K 线计算均值、标准差、Z-Score、百分位、半衰期、动量和斜率。
  • 综合 Z-Score 与动量过滤,给出做多价差、做空价差、观望或等待拐点的读数。
  • 同时估算两个方向的回归毛利润、点差与佣金、净收益和保本收敛空间。
  • 支持在面板内切换 M15 / H1 / H4,并可手动刷新统计数据。

信号逻辑

默认可信阈值为 Z-Score ±1.5σ。价差显著高于历史均值时,面板偏向“做空价差”,即卖出 Brent、买入 WTI;价差显著低于历史均值时,偏向“做多价差”,即买入 Brent、卖出 WTI。如果极端偏离仍在快速扩散,动量过滤会提示“等待拐点”,避免过早逆势进场。

适用对象

  • 平台:MetaTrader 4。
  • 默认品种:UKOILm / USOILm,可按经纪商品种名称修改。
  • 默认周期:H1;默认统计样本:120 根 K 线。
  • 适合观察原油跨品种价差、辅助人工决策和评估交易成本。

使用提醒

本 EA 只提供统计分析与利润测算,不会自动发送订单。历史均值回归不代表未来必然回归;真实交易还需检查滑点、隔夜利息、流动性、保证金和两腿成交时间差。

源代码

MQL4
//+------------------------------------------------------------------+
//|                                            OilSpreadAnalyst.mq4 |
//|                             Brent / WTI spread analysis dashboard |
//+------------------------------------------------------------------+
#property copyright "Oil Spread Analyst"
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict
#property description "Brent/WTI relative-value dashboard with spread, z-score, percentile, half-life and after-cost P&L."

//--- Symbols and statistics
input string          BrentSymbol             = "UKOILm";
input string          WtiSymbol               = "USOILm";
input ENUM_TIMEFRAMES AnalysisTimeframe       = PERIOD_H1;
input int             LookbackBars             = 120;
input int             MomentumBars             = 12;
input double          SignalZScore             = 1.50;
input double          MomentumBlockZ           = 0.50;
input bool            AvoidWideningSpread      = true;

//--- Trade economics
input double          TradeLotsPerLeg          = 0.10;
input double          BrentCommissionPerLot   = 0.00;
input double          WtiCommissionPerLot     = 0.00;

//--- Alerts and refresh
input bool            EnableAlerts             = true;
input int             RefreshSeconds           = 1;

//--- Layout
#define PANEL_X       16
#define PANEL_Y       4
#define PANEL_W       980
#define PANEL_H       510
#define OBJECT_PREFIX "OSL_"

const color CLR_PANEL       = C'14,19,24';
const color CLR_HEADER      = C'20,28,35';
const color CLR_CELL        = C'23,31,38';
const color CLR_CELL_ALT    = C'27,36,43';
const color CLR_BORDER      = C'54,67,76';
const color CLR_TEXT        = C'226,232,236';
const color CLR_MUTED       = C'145,158,168';
const color CLR_TEAL        = C'52,184,166';
const color CLR_TEAL_DARK   = C'25,90,84';
const color CLR_ORANGE      = C'226,132,64';
const color CLR_RED         = C'224,91,88';
const color CLR_GREEN       = C'73,196,132';
const color CLR_AMBER       = C'228,185,80';
const color CLR_GAUGE       = C'34,46,55';
const color CLR_ZERO        = C'92,108,119';

struct SpreadStats
  {
   bool              valid;
   int               samples;
   double            mean;
   double            stddev;
   double            minimum;
   double            maximum;
   double            zscore;
   double            percentile;
   double            halfLife;
   double            slope;
   double            momentum;
   datetime          lastBarTime;
  };

SpreadStats g_stats;
double      g_history[];
int         g_historyCount = 0;

int         g_analysisTf = PERIOD_H1;
int         g_signal = 0;              // -1 short spread, 0 wait, +1 long spread
int         g_lastAlertSignal = 0;
datetime    g_lastAlertBar = 0;
datetime    g_lastRefresh = 0;

double      g_bidBrent = 0.0;
double      g_askBrent = 0.0;
double      g_bidWti = 0.0;
double      g_askWti = 0.0;
double      g_midBrent = 0.0;
double      g_midWti = 0.0;
double      g_midSpread = 0.0;
double      g_ratio = 0.0;
double      g_premiumPct = 0.0;
double      g_valuePerPriceBrent = 0.0;
double      g_valuePerPriceWti = 0.0;
double      g_spreadUnitValue = 0.0;
double      g_signalThreshold = 1.50;
double      g_momentumBlock = 0.50;
double      g_spreadCostMoney = 0.0;
double      g_commissionMoney = 0.0;
double      g_totalCostMoney = 0.0;
double      g_breakEvenPrice = 0.0;
double      g_longGross = 0.0;
double      g_shortGross = 0.0;
double      g_longNet = 0.0;
double      g_shortNet = 0.0;
int         g_digitsBrent = 3;
int         g_digitsWti = 3;
bool        g_quotesReady = false;
bool        g_economicsReady = false;
bool        g_previousChartForeground = true;
bool        g_chartForegroundChanged = false;

//+------------------------------------------------------------------+
//| Expert initialization                                           |
//+------------------------------------------------------------------+
int OnInit()
  {
   g_analysisTf = (int)AnalysisTimeframe;
   g_previousChartForeground = (ChartGetInteger(0, CHART_FOREGROUND) != 0);
   g_chartForegroundChanged = false;
   if(g_previousChartForeground)
      g_chartForegroundChanged = ChartSetInteger(0, CHART_FOREGROUND, false);
   ResetStats();
   BuildDashboard();
   EventSetTimer(MathMax(1, RefreshSeconds));
   UpdateDashboard(true);
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Expert deinitialization                                         |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
   DeleteDashboard();
   if(g_chartForegroundChanged)
      ChartSetInteger(0, CHART_FOREGROUND, g_previousChartForeground);
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Tick handler                                                    |
//+------------------------------------------------------------------+
void OnTick()
  {
   if(RefreshSeconds <= 0 || TimeLocal() - g_lastRefresh >= RefreshSeconds)
      UpdateDashboard(false);
  }

//+------------------------------------------------------------------+
//| Timer handler                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
   UpdateDashboard(false);
  }

//+------------------------------------------------------------------+
//| Chart event handler                                             |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   if(id != CHARTEVENT_OBJECT_CLICK)
      return;

   if(sparam == "OSL_BTN_M15")
     {
      SetAnalysisTimeframe(PERIOD_M15);
      return;
     }
   if(sparam == "OSL_BTN_H1")
     {
      SetAnalysisTimeframe(PERIOD_H1);
      return;
     }
   if(sparam == "OSL_BTN_H4")
     {
      SetAnalysisTimeframe(PERIOD_H4);
      return;
     }
   if(sparam == "OSL_BTN_REFRESH")
     {
      ObjectSetInteger(0, sparam, OBJPROP_STATE, false);
      g_lastRefresh = 0;
      UpdateDashboard(true);
      return;
     }
  }

//+------------------------------------------------------------------+
//| Reset calculated state                                          |
//+------------------------------------------------------------------+
void ResetStats()
  {
   g_stats.valid = false;
   g_stats.samples = 0;
   g_stats.mean = 0.0;
   g_stats.stddev = 0.0;
   g_stats.minimum = 0.0;
   g_stats.maximum = 0.0;
   g_stats.zscore = 0.0;
   g_stats.percentile = 0.0;
   g_stats.halfLife = 0.0;
   g_stats.slope = 0.0;
   g_stats.momentum = 0.0;
   g_stats.lastBarTime = 0;
   ArrayResize(g_history, 0);
   g_historyCount = 0;
  }

//+------------------------------------------------------------------+
//| Change analysis timeframe                                       |
//+------------------------------------------------------------------+
void SetAnalysisTimeframe(const int timeframe)
  {
   if(g_analysisTf == timeframe)
     {
      ObjectSetInteger(0, "OSL_BTN_M15", OBJPROP_STATE, false);
      ObjectSetInteger(0, "OSL_BTN_H1", OBJPROP_STATE, false);
      ObjectSetInteger(0, "OSL_BTN_H4", OBJPROP_STATE, false);
      UpdateTimeframeButtons();
      ChartRedraw();
      return;
     }

   g_analysisTf = timeframe;
   g_lastAlertSignal = 0;
   g_lastAlertBar = 0;
   ResetStats();
   UpdateTimeframeButtons();
   g_lastRefresh = 0;
   UpdateDashboard(true);
  }

//+------------------------------------------------------------------+
//| Main dashboard refresh                                          |
//+------------------------------------------------------------------+
void UpdateDashboard(const bool force)
  {
   if(!force && RefreshSeconds > 0 && TimeLocal() - g_lastRefresh < RefreshSeconds)
      return;

   g_lastRefresh = TimeLocal();
   g_signalThreshold = MathMax(0.25, SignalZScore);
   g_momentumBlock = MathMax(0.0, MomentumBlockZ);
   g_quotesReady = ReadQuotes();

   if(!g_quotesReady)
     {
      UpdateUnavailableState();
      ChartRedraw();
      return;
     }

   g_midBrent = (g_bidBrent + g_askBrent) * 0.5;
   g_midWti = (g_bidWti + g_askWti) * 0.5;
   g_midSpread = g_midBrent - g_midWti;
   g_ratio = (g_midWti > 0.0 ? g_midBrent / g_midWti : 0.0);
   g_premiumPct = (g_ratio - 1.0) * 100.0;

   CalculateStatistics();
   CalculateEconomics();
   DetermineSignal();

   UpdateHeader();
   UpdateQuoteBlocks();
   UpdateSpreadBlock();
   UpdateStatisticsBlock();
   UpdateGauge();
   UpdateSignalBlock();
   UpdateTradeBlocks();
   UpdateFooterStatus();
   UpdateTimeframeButtons();
   CheckSignalAlert();

   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Read live quotes and symbol properties                          |
//+------------------------------------------------------------------+
bool ReadQuotes()
  {
   g_bidBrent = MarketInfo(BrentSymbol, MODE_BID);
   g_askBrent = MarketInfo(BrentSymbol, MODE_ASK);
   g_bidWti = MarketInfo(WtiSymbol, MODE_BID);
   g_askWti = MarketInfo(WtiSymbol, MODE_ASK);

   if(g_bidBrent <= 0.0 || g_askBrent <= 0.0 ||
      g_bidWti <= 0.0 || g_askWti <= 0.0)
      return false;

   if(g_askBrent < g_bidBrent || g_askWti < g_bidWti)
      return false;

   g_digitsBrent = (int)MarketInfo(BrentSymbol, MODE_DIGITS);
   g_digitsWti = (int)MarketInfo(WtiSymbol, MODE_DIGITS);

   double tickSizeBrent = MarketInfo(BrentSymbol, MODE_TICKSIZE);
   double tickSizeWti = MarketInfo(WtiSymbol, MODE_TICKSIZE);
   double pointBrent = MarketInfo(BrentSymbol, MODE_POINT);
   double pointWti = MarketInfo(WtiSymbol, MODE_POINT);
   double tickValueBrent = MarketInfo(BrentSymbol, MODE_TICKVALUE);
   double tickValueWti = MarketInfo(WtiSymbol, MODE_TICKVALUE);

   if(tickSizeBrent <= 0.0)
      tickSizeBrent = pointBrent;
   if(tickSizeWti <= 0.0)
      tickSizeWti = pointWti;

   g_valuePerPriceBrent = (tickSizeBrent > 0.0 ? tickValueBrent / tickSizeBrent : 0.0);
   g_valuePerPriceWti = (tickSizeWti > 0.0 ? tickValueWti / tickSizeWti : 0.0);
   g_spreadUnitValue = (g_valuePerPriceBrent + g_valuePerPriceWti) * 0.5;
   g_economicsReady = (g_valuePerPriceBrent > 0.0 && g_valuePerPriceWti > 0.0);
   return true;
  }

//+------------------------------------------------------------------+
//| Calculate rolling spread statistics                             |
//+------------------------------------------------------------------+
bool CalculateStatistics()
  {
   ResetStats();

   int barsBrent = iBars(BrentSymbol, g_analysisTf);
   int barsWti = iBars(WtiSymbol, g_analysisTf);
   int requested = MathMax(20, MathMin(LookbackBars, 1000));
   int available = MathMin(barsBrent - 2, barsWti - 2);
   if(available < 20)
      return false;

   int sampleCount = MathMin(requested, available);
   double values[];
   ArrayResize(values, sampleCount);
   ArrayInitialize(values, 0.0);

   int n = 0;
   int periodSeconds = PeriodSeconds(g_analysisTf);

   // Oldest completed bar first, newest completed bar last.
   for(int k = sampleCount; k >= 1; k--)
     {
      datetime brentTime = iTime(BrentSymbol, g_analysisTf, k);
      if(brentTime <= 0)
         continue;

      int wtiShift = iBarShift(WtiSymbol, g_analysisTf, brentTime, false);
      if(wtiShift < 0)
         continue;

      datetime wtiTime = iTime(WtiSymbol, g_analysisTf, wtiShift);
      if(wtiTime <= 0 || MathAbs((long)brentTime - (long)wtiTime) > periodSeconds)
         continue;

      double brentClose = iClose(BrentSymbol, g_analysisTf, k);
      double wtiClose = iClose(WtiSymbol, g_analysisTf, wtiShift);
      if(brentClose <= 0.0 || wtiClose <= 0.0)
         continue;

      values[n] = brentClose - wtiClose;
      n++;
     }

   if(n < 20)
      return false;

   ArrayResize(values, n);
   ArrayResize(g_history, n);
   g_historyCount = n;

   double sum = 0.0;
   double minimum = values[0];
   double maximum = values[0];
   for(int i = 0; i < n; i++)
     {
      g_history[i] = values[i];
      sum += values[i];
      if(values[i] < minimum)
         minimum = values[i];
      if(values[i] > maximum)
         maximum = values[i];
     }

   double mean = sum / n;
   double squared = 0.0;
   for(int j = 0; j < n; j++)
      squared += MathPow(values[j] - mean, 2.0);

   double stddev = (n > 1 ? MathSqrt(squared / (n - 1)) : 0.0);
   double zscore = (stddev > 0.0 ? (g_midSpread - mean) / stddev : 0.0);

   int belowOrEqual = 0;
   for(int p = 0; p < n; p++)
     {
      if(values[p] <= g_midSpread)
         belowOrEqual++;
     }
   double percentile = 100.0 * (double)belowOrEqual / (double)n;

   int momentumBars = MathMax(2, MathMin(MomentumBars, n - 1));
   double momentum = (stddev > 0.0 ?
                      (g_midSpread - values[n - 1 - momentumBars]) / stddev : 0.0);

   int slopeBars = MathMin(MathMax(MomentumBars, 5), n);
   double slope = CalculateSlope(values, n - slopeBars, slopeBars);
   double halfLife = CalculateHalfLife(values, n);

   g_stats.valid = true;
   g_stats.samples = n;
   g_stats.mean = mean;
   g_stats.stddev = stddev;
   g_stats.minimum = minimum;
   g_stats.maximum = maximum;
   g_stats.zscore = zscore;
   g_stats.percentile = percentile;
   g_stats.halfLife = halfLife;
   g_stats.slope = slope;
   g_stats.momentum = momentum;
   g_stats.lastBarTime = iTime(BrentSymbol, g_analysisTf, 1);
   return true;
  }

//+------------------------------------------------------------------+
//| Linear regression slope per bar                                 |
//+------------------------------------------------------------------+
double CalculateSlope(const double &values[], const int start, const int count)
  {
   if(count < 2 || start < 0 || start + count > ArraySize(values))
      return 0.0;

   double sumX = 0.0;
   double sumY = 0.0;
   double sumXY = 0.0;
   double sumXX = 0.0;

   for(int i = 0; i < count; i++)
     {
      double x = (double)i;
      double y = values[start + i];
      sumX += x;
      sumY += y;
      sumXY += x * y;
      sumXX += x * x;
     }

   double denominator = count * sumXX - sumX * sumX;
   if(MathAbs(denominator) < 0.0000001)
      return 0.0;

   return (count * sumXY - sumX * sumY) / denominator;
  }

//+------------------------------------------------------------------+
//| AR(1) half-life in bars                                         |
//+------------------------------------------------------------------+
double CalculateHalfLife(const double &values[], const int count)
  {
   if(count < 10)
      return 0.0;

   double sumX = 0.0;
   double sumY = 0.0;
   for(int i = 1; i < count; i++)
     {
      sumX += values[i - 1];
      sumY += values[i] - values[i - 1];
     }

   int observations = count - 1;
   double meanX = sumX / observations;
   double meanY = sumY / observations;
   double covariance = 0.0;
   double variance = 0.0;

   for(int j = 1; j < count; j++)
     {
      double x = values[j - 1] - meanX;
      double y = (values[j] - values[j - 1]) - meanY;
      covariance += x * y;
      variance += x * x;
     }

   if(variance <= 0.0)
      return 0.0;

   double beta = covariance / variance;
   if(beta >= 0.0 || beta <= -0.999999)
      return 0.0;

   double halfLife = -MathLog(2.0) / MathLog(1.0 + beta);
   if(halfLife <= 0.0)
      return 0.0;
   return halfLife;
  }

//+------------------------------------------------------------------+
//| Calculate after-cost economics for both spread directions       |
//+------------------------------------------------------------------+
void CalculateEconomics()
  {
   double spreadBrent = MathMax(0.0, g_askBrent - g_bidBrent);
   double spreadWti = MathMax(0.0, g_askWti - g_bidWti);
   double lots = MathMax(0.0, TradeLotsPerLeg);

   g_spreadCostMoney =
      (spreadBrent * g_valuePerPriceBrent +
       spreadWti * g_valuePerPriceWti) * lots;
   g_commissionMoney =
      (MathMax(0.0, BrentCommissionPerLot) +
       MathMax(0.0, WtiCommissionPerLot)) * lots;
   g_totalCostMoney = g_spreadCostMoney + g_commissionMoney;

   g_longGross = (g_stats.valid ?
                  (g_stats.mean - g_midSpread) * g_spreadUnitValue * lots : 0.0);
   g_shortGross = (g_stats.valid ?
                   (g_midSpread - g_stats.mean) * g_spreadUnitValue * lots : 0.0);
   g_longNet = g_longGross - g_totalCostMoney;
   g_shortNet = g_shortGross - g_totalCostMoney;

   double unitValueAtLots = g_spreadUnitValue * lots;
   g_breakEvenPrice = (unitValueAtLots > 0.0 ?
                       g_totalCostMoney / unitValueAtLots : 0.0);
  }

//+------------------------------------------------------------------+
//| Select statistical signal with a widening-spread filter         |
//+------------------------------------------------------------------+
void DetermineSignal()
  {
   g_signal = 0;
   if(!g_stats.valid)
      return;

   int rawSignal = 0;
   if(g_stats.zscore >= g_signalThreshold)
      rawSignal = -1;
   else if(g_stats.zscore <= -g_signalThreshold)
      rawSignal = 1;

   if(rawSignal == 0)
      return;

   bool blockedByMomentum = false;
   if(AvoidWideningSpread)
     {
      if(rawSignal == -1 && g_stats.momentum > g_momentumBlock)
         blockedByMomentum = true;
      if(rawSignal == 1 && g_stats.momentum < -g_momentumBlock)
         blockedByMomentum = true;
     }

   if(!blockedByMomentum)
      g_signal = rawSignal;
  }

//+------------------------------------------------------------------+
//| Send a signal-transition alert on a new bar                     |
//+------------------------------------------------------------------+
void CheckSignalAlert()
  {
   if(!EnableAlerts || !g_stats.valid)
      return;

   datetime currentBar = iTime(BrentSymbol, g_analysisTf, 0);
   if(currentBar <= 0 || currentBar == g_lastAlertBar)
      return;

   g_lastAlertBar = currentBar;
   if(g_signal == 0)
     {
      g_lastAlertSignal = 0;
      return;
     }
   if(g_signal == g_lastAlertSignal)
      return;

   string action = (g_signal > 0 ? "做多价差" : "做空价差");
   string message = StringFormat(
      "%s %s/%s | %s | 价差 %.3f | z %.2f | 分位 %.1f%% | 预计净利 %s %.2f",
      BrentSymbol, WtiSymbol, TimeframeName(g_analysisTf), action,
      g_midSpread, g_stats.zscore, g_stats.percentile,
      AccountCurrency(), (g_signal > 0 ? g_longNet : g_shortNet));
   Alert(message);
   g_lastAlertSignal = g_signal;
  }

//+------------------------------------------------------------------+
//| Build all chart objects                                         |
//+------------------------------------------------------------------+
void BuildDashboard()
  {
   DeleteDashboard();

   CreateRect("OSL_BG", 0, 0, PANEL_W, PANEL_H, CLR_PANEL, CLR_BORDER);
   CreateRect("OSL_HEADER", 0, 0, PANEL_W, 50, CLR_HEADER, CLR_BORDER);
   CreateRect("OSL_ACCENT", 0, 0, 5, 50, CLR_TEAL, CLR_TEAL);

   CreateLabel("OSL_TITLE", 22, 10, "OIL SPREAD LAB", 16, CLR_TEXT, "Segoe UI Semibold");
   CreateLabel("OSL_SUBTITLE", 22, 30, "Brent / WTI 相对价值分析台", 9, CLR_MUTED, "Microsoft YaHei");
   CreateLabel("OSL_HEADER_SYMBOLS", 560, 11, BrentSymbol + "  /  " + WtiSymbol, 9, CLR_TEXT, "Consolas");
   CreateLabel("OSL_HEADER_TIME", 560, 31, "等待行情", 8, CLR_MUTED, "Consolas");

   CreateRect("OSL_SYM1_BG", 16, 58, 468, 74, CLR_CELL, CLR_BORDER);
   CreateRect("OSL_SYM2_BG", 496, 58, 468, 74, CLR_CELL, CLR_BORDER);
   CreateLabel("OSL_SYM1_NAME", 31, 65, BrentSymbol + "  ·  BRENT", 9, CLR_TEAL, "Segoe UI Semibold");
   CreateLabel("OSL_SYM1_PRICE", 31, 82, "--", 22, CLR_TEXT, "Consolas");
   CreateLabel("OSL_SYM1_QUOTE", 31, 116, "BID --  /  ASK --", 8, CLR_MUTED, "Consolas");
   CreateLabel("OSL_SYM1_CHANGE", 468, 65, "--", 8, CLR_MUTED, "Consolas", ANCHOR_RIGHT_UPPER);

   CreateLabel("OSL_SYM2_NAME", 511, 65, WtiSymbol + "  ·  WTI", 9, CLR_ORANGE, "Segoe UI Semibold");
   CreateLabel("OSL_SYM2_PRICE", 511, 82, "--", 22, CLR_TEXT, "Consolas");
   CreateLabel("OSL_SYM2_QUOTE", 511, 116, "BID --  /  ASK --", 8, CLR_MUTED, "Consolas");
   CreateLabel("OSL_SYM2_CHANGE", 948, 65, "--", 8, CLR_MUTED, "Consolas", ANCHOR_RIGHT_UPPER);

   CreateRect("OSL_SPREAD_BG", 16, 142, 320, 168, CLR_CELL_ALT, CLR_BORDER);
   CreateLabel("OSL_SPREAD_CAPTION", 31, 150, "MID SPREAD  ·  BRENT - WTI", 8, CLR_MUTED, "Segoe UI Semibold");
   CreateLabel("OSL_SPREAD_VALUE", 31, 169, "--", 28, CLR_TEXT, "Consolas");
   CreateLabel("OSL_SPREAD_UNIT", 31, 207, "USD / bbl", 8, CLR_MUTED, "Segoe UI");
   CreateLabel("OSL_RATIO_CAPTION", 185, 151, "BRENT / WTI 比价", 8, CLR_MUTED, "Microsoft YaHei");
   CreateLabel("OSL_RATIO_VALUE", 185, 170, "--", 18, CLR_TEXT, "Consolas");
   CreateLabel("OSL_PREMIUM_CAPTION", 260, 151, "BRENT 溢价", 8, CLR_MUTED, "Microsoft YaHei");
   CreateLabel("OSL_PREMIUM_VALUE", 260, 170, "--", 18, CLR_TEXT, "Consolas");
   CreateLabel("OSL_EXEC_SPREADS", 31, 235, "执行价差  多头 --  /  空头 --", 8, CLR_MUTED, "Consolas");
   CreateLabel("OSL_SPREAD_NOTE", 31, 274,
               "多头 = 买 Brent / 卖 WTI    空头 = 卖 Brent / 买 WTI",
               7, CLR_MUTED, "Microsoft YaHei");

   CreateRect("OSL_STATS_BG", 344, 142, 620, 168, CLR_CELL, CLR_BORDER);
   CreateStatBlock("OSL_STAT_MEAN", 360, 150, "均值", "MEAN");
   CreateStatBlock("OSL_STAT_SD", 566, 150, "标准差", "STD DEV");
   CreateStatBlock("OSL_STAT_Z", 772, 150, "Z-SCORE", "偏离");
   CreateStatBlock("OSL_STAT_PCT", 360, 218, "百分位", "PERCENTILE");
   CreateStatBlock("OSL_STAT_HL", 566, 218, "半衰期", "HALF-LIFE");
   CreateStatBlock("OSL_STAT_SLOPE", 772, 218, "动量", "SLOPE");

   CreateRect("OSL_SIGNAL_BG", 16, 318, 480, 144, CLR_CELL, CLR_BORDER);
   CreateLabel("OSL_SIGNAL_CAPTION", 31, 326, "策略信号", 8, CLR_MUTED, "Microsoft YaHei");
   CreateLabel("OSL_SIGNAL_ACTION", 31, 342, "等待数据", 15, CLR_AMBER, "Microsoft YaHei");
   CreateLabel("OSL_SIGNAL_REASON", 31, 367, "--", 8, CLR_MUTED, "Microsoft YaHei");
   CreateLabel("OSL_SIGNAL_STRENGTH", 480, 326, "--", 8, CLR_MUTED, "Consolas", ANCHOR_RIGHT_UPPER);

   CreateLabel("OSL_GAUGE_CAPTION", 31, 389, "Z-SCORE 区间  ·  统计偏离强度", 8, CLR_MUTED, "Microsoft YaHei");
   CreateLabel("OSL_GAUGE_NEG2", 109, 401, "-2σ", 7, CLR_MUTED, "Segoe UI");
   CreateLabel("OSL_GAUGE_NEG1", 179, 401, "-1σ", 7, CLR_MUTED, "Segoe UI");
   CreateLabel("OSL_GAUGE_ZERO", 249, 401, "0", 7, CLR_MUTED, "Segoe UI");
   CreateLabel("OSL_GAUGE_POS1", 317, 401, "+1σ", 7, CLR_MUTED, "Segoe UI");
   CreateLabel("OSL_GAUGE_POS2", 387, 401, "+2σ", 7, CLR_MUTED, "Segoe UI");
   CreateRect("OSL_GAUGE_TRACK", 48, 419, 416, 10, CLR_GAUGE, CLR_GAUGE);
   CreateRect("OSL_GAUGE_LONG_ZONE", 48, 419, 104, 10, CLR_TEAL_DARK, CLR_TEAL_DARK);
   CreateRect("OSL_GAUGE_SHORT_ZONE", 360, 419, 104, 10, C'84,50,35', C'84,50,35');
   CreateRect("OSL_GAUGE_MID", 255, 417, 2, 14, CLR_ZERO, CLR_ZERO);
   CreateRect("OSL_GAUGE_FILL", 255, 421, 2, 6, CLR_TEAL, CLR_TEAL);
   CreateRect("OSL_GAUGE_MARKER", 253, 416, 6, 16, CLR_TEXT, CLR_TEXT);
   CreateLabel("OSL_GAUGE_LEFT", 48, 434, "做多价差区", 7, CLR_TEAL, "Microsoft YaHei");
   CreateLabel("OSL_GAUGE_RIGHT", 464, 434, "做空价差区", 7, CLR_ORANGE, "Microsoft YaHei", ANCHOR_RIGHT_UPPER);

   CreateRect("OSL_TRADE_BG", 504, 318, 460, 144, CLR_CELL, CLR_BORDER);
   CreateRect("OSL_TRADE_DIVIDER", 734, 326, 1, 112, CLR_BORDER, CLR_BORDER);
   CreateLabel("OSL_LONG_TITLE", 519, 326, "做多价差", 10, CLR_TEAL, "Microsoft YaHei");
   CreateLabel("OSL_LONG_SUB", 519, 339, "买 Brent / 卖 WTI", 8, CLR_MUTED, "Microsoft YaHei");
   CreateLabel("OSL_LONG_NET", 519, 352, "净利 @ 均值  --", 16, CLR_TEXT, "Consolas");
   CreateLabel("OSL_LONG_GROSS", 519, 379, "回归毛利 --", 8, CLR_MUTED, "Microsoft YaHei");
   CreateLabel("OSL_LONG_COST", 519, 396, "点差 + 佣金 --", 8, CLR_MUTED, "Microsoft YaHei");
   CreateLabel("OSL_LONG_BE", 519, 413, "保本收敛 --", 8, CLR_MUTED, "Microsoft YaHei");

   CreateLabel("OSL_SHORT_TITLE", 749, 326, "做空价差", 10, CLR_ORANGE, "Microsoft YaHei");
   CreateLabel("OSL_SHORT_SUB", 749, 339, "卖 Brent / 买 WTI", 8, CLR_MUTED, "Microsoft YaHei");
   CreateLabel("OSL_SHORT_NET", 749, 352, "净利 @ 均值  --", 16, CLR_TEXT, "Consolas");
   CreateLabel("OSL_SHORT_GROSS", 749, 379, "回归毛利 --", 8, CLR_MUTED, "Microsoft YaHei");
   CreateLabel("OSL_SHORT_COST", 749, 396, "点差 + 佣金 --", 8, CLR_MUTED, "Microsoft YaHei");
   CreateLabel("OSL_SHORT_BE", 749, 413, "保本收敛 --", 8, CLR_MUTED, "Microsoft YaHei");
   CreateLabel("OSL_TRADE_LOTS", 519, 441, "测算手数:每腿 -- 手", 8, CLR_MUTED, "Microsoft YaHei");

   CreateRect("OSL_FOOTER", 16, 470, 948, 36, CLR_HEADER, CLR_BORDER);
   CreateLabel("OSL_FOOTER_STATUS", 31, 475, "正在读取历史行情...", 8, CLR_MUTED, "Microsoft YaHei");
   CreateLabel("OSL_FOOTER_ASSUMPTION", 31, 492,
               "净利假设:价差回归均值、两腿等幅收敛;不含隔夜利息与滑点。", 7, CLR_MUTED,
               "Microsoft YaHei");

   CreateButton("OSL_BTN_M15", 680, 476, 50, 24, "M15");
   CreateButton("OSL_BTN_H1", 737, 476, 50, 24, "H1");
   CreateButton("OSL_BTN_H4", 794, 476, 50, 24, "H4");
   CreateButton("OSL_BTN_REFRESH", 851, 476, 62, 24, "刷新");
  }

//+------------------------------------------------------------------+
//| Create one statistic block                                      |
//+------------------------------------------------------------------+
void CreateStatBlock(const string name, const int x, const int y,
                     const string cn, const string en)
  {
   CreateLabel(name + "_CAPTION", x, y, cn + "  ·  " + en, 8, CLR_MUTED, "Microsoft YaHei");
   CreateLabel(name + "_VALUE", x, y + 16, "--", 14, CLR_TEXT, "Consolas");
   CreateLabel(name + "_SUB", x, y + 43, "--", 8, CLR_MUTED, "Consolas");
  }

//+------------------------------------------------------------------+
//| Update header                                                   |
//+------------------------------------------------------------------+
void UpdateHeader()
  {
   SetLabelText("OSL_HEADER_SYMBOLS", BrentSymbol + "  /  " + WtiSymbol);
   SetLabelText("OSL_HEADER_TIME", "服务器  " + TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS));
   SetLabelText("OSL_SYM1_NAME", BrentSymbol + "  ·  BRENT");
   SetLabelText("OSL_SYM2_NAME", WtiSymbol + "  ·  WTI");
  }

//+------------------------------------------------------------------+
//| Update live quote blocks                                        |
//+------------------------------------------------------------------+
void UpdateQuoteBlocks()
  {
   SetLabelText("OSL_SYM1_PRICE", FormatPrice(g_midBrent, g_digitsBrent));
   SetLabelText("OSL_SYM2_PRICE", FormatPrice(g_midWti, g_digitsWti));
   SetLabelText("OSL_SYM1_QUOTE",
                "BID " + FormatPrice(g_bidBrent, g_digitsBrent) +
                "  /  ASK " + FormatPrice(g_askBrent, g_digitsBrent));
   SetLabelText("OSL_SYM2_QUOTE",
                "BID " + FormatPrice(g_bidWti, g_digitsWti) +
                "  /  ASK " + FormatPrice(g_askWti, g_digitsWti));

   UpdateDailyChange("OSL_SYM1_CHANGE", BrentSymbol, g_midBrent, g_digitsBrent);
   UpdateDailyChange("OSL_SYM2_CHANGE", WtiSymbol, g_midWti, g_digitsWti);
  }

//+------------------------------------------------------------------+
//| Update daily change labels                                      |
//+------------------------------------------------------------------+
void UpdateDailyChange(const string name, const string symbol,
                       const double currentMid, const int digits)
  {
   double previousClose = iClose(symbol, PERIOD_D1, 1);
   if(previousClose <= 0.0 || currentMid <= 0.0)
     {
      SetLabelText(name, "日变动  --");
      SetLabelColor(name, CLR_MUTED);
      return;
     }

   double change = currentMid - previousClose;
   double percent = change / previousClose * 100.0;
   string sign = (change > 0.0 ? "+" : "");
   string text = "日变动  " + sign + FormatPrice(change, digits) +
                 "  (" + sign + DoubleToString(percent, 2) + "%)";
   SetLabelText(name, text);
   SetLabelColor(name, (change > 0.0 ? CLR_GREEN : (change < 0.0 ? CLR_RED : CLR_MUTED)));
  }

//+------------------------------------------------------------------+
//| Update spread and ratio block                                   |
//+------------------------------------------------------------------+
void UpdateSpreadBlock()
  {
   SetLabelText("OSL_SPREAD_VALUE", DoubleToString(g_midSpread, 3));
   SetLabelText("OSL_RATIO_VALUE", DoubleToString(g_ratio, 4));

   string premiumSign = (g_premiumPct > 0.0 ? "+" : "");
   SetLabelText("OSL_PREMIUM_VALUE", premiumSign + DoubleToString(g_premiumPct, 2) + "%");
   SetLabelColor("OSL_PREMIUM_VALUE", (g_premiumPct >= 0.0 ? CLR_ORANGE : CLR_TEAL));

   double longExecutable = g_askBrent - g_bidWti;
   double shortExecutable = g_bidBrent - g_askWti;
   SetLabelText("OSL_EXEC_SPREADS",
                "执行价差  多头 " + DoubleToString(longExecutable, 3) +
                "  /  空头 " + DoubleToString(shortExecutable, 3));
  }

//+------------------------------------------------------------------+
//| Update statistics block                                         |
//+------------------------------------------------------------------+
void UpdateStatisticsBlock()
  {
   if(!g_stats.valid)
     {
      SetLabelText("OSL_STAT_MEAN_VALUE", "--");
      SetLabelText("OSL_STAT_SD_VALUE", "--");
      SetLabelText("OSL_STAT_Z_VALUE", "--");
      SetLabelText("OSL_STAT_PCT_VALUE", "--");
      SetLabelText("OSL_STAT_HL_VALUE", "--");
      SetLabelText("OSL_STAT_SLOPE_VALUE", "--");
      return;
     }

   SetLabelText("OSL_STAT_MEAN_VALUE", DoubleToString(g_stats.mean, 3));
   SetLabelText("OSL_STAT_MEAN_SUB",
                "范围 " + DoubleToString(g_stats.minimum, 2) +
                " ~ " + DoubleToString(g_stats.maximum, 2));

   SetLabelText("OSL_STAT_SD_VALUE", DoubleToString(g_stats.stddev, 3));
   SetLabelText("OSL_STAT_SD_SUB", "样本 " + IntegerToString(g_stats.samples));

   string zSign = (g_stats.zscore > 0.0 ? "+" : "");
   SetLabelText("OSL_STAT_Z_VALUE", zSign + DoubleToString(g_stats.zscore, 2));
   SetLabelColor("OSL_STAT_Z_VALUE",
                  (g_stats.zscore >= g_signalThreshold ? CLR_ORANGE :
                   (g_stats.zscore <= -g_signalThreshold ? CLR_TEAL : CLR_TEXT)));
   SetLabelText("OSL_STAT_Z_SUB", "阈值 ±" + DoubleToString(g_signalThreshold, 2) + "σ");

   SetLabelText("OSL_STAT_PCT_VALUE", DoubleToString(g_stats.percentile, 1) + "%");
   int rank = (int)MathRound(g_stats.percentile);
   SetLabelText("OSL_STAT_PCT_SUB", "历史排名 " + IntegerToString(rank) + "/100");

   if(g_stats.halfLife > 0.0)
      SetLabelText("OSL_STAT_HL_VALUE", DoubleToString(g_stats.halfLife, 1));
   else
      SetLabelText("OSL_STAT_HL_VALUE", "N/A");
   SetLabelText("OSL_STAT_HL_SUB", "根 K 线");

   string slopeSign = (g_stats.slope > 0.0 ? "+" : "");
   SetLabelText("OSL_STAT_SLOPE_VALUE", slopeSign + DoubleToString(g_stats.slope, 4));
   SetLabelColor("OSL_STAT_SLOPE_VALUE",
                 (g_stats.slope > 0.0 ? CLR_ORANGE :
                  (g_stats.slope < 0.0 ? CLR_TEAL : CLR_TEXT)));
   string momentumSign = (g_stats.momentum > 0.0 ? "+" : "");
   SetLabelText("OSL_STAT_SLOPE_SUB",
                "动量 " + momentumSign + DoubleToString(g_stats.momentum, 2) + "σ");

  }

//+------------------------------------------------------------------+
//| Update z-score gauge                                            |
//+------------------------------------------------------------------+
void UpdateGauge()
  {
   double z = (g_stats.valid ? g_stats.zscore : 0.0);
   double clamped = MathMax(-3.0, MathMin(3.0, z));
   int trackX = 48;
   int centerX = trackX + 208;
   int markerX = trackX + (int)MathRound((clamped + 3.0) / 6.0 * 416.0);
   markerX = MathMax(trackX + 1, MathMin(trackX + 415, markerX));

   color activeColor = CLR_ZERO;
   if(z >= g_signalThreshold)
      activeColor = CLR_ORANGE;
   else if(z <= -g_signalThreshold)
      activeColor = CLR_TEAL;

   int fillX = MathMin(centerX, markerX);
   int fillWidth = MathMax(2, MathAbs(markerX - centerX));
   SetRectGeometry("OSL_GAUGE_FILL", fillX, 421, fillWidth, 6);
   SetRectColor("OSL_GAUGE_FILL", activeColor, activeColor);
   SetRectGeometry("OSL_GAUGE_MARKER", markerX - 3, 416, 6, 16);
   SetRectColor("OSL_GAUGE_MARKER", activeColor, activeColor);
  }

//+------------------------------------------------------------------+
//| Update signal block                                             |
//+------------------------------------------------------------------+
void UpdateSignalBlock()
  {
   if(!g_stats.valid)
     {
      SetLabelText("OSL_SIGNAL_ACTION", "等待历史数据");
      SetLabelColor("OSL_SIGNAL_ACTION", CLR_AMBER);
      SetLabelText("OSL_SIGNAL_REASON", "双品种历史尚未对齐,请检查品种名称与图表历史。");
      SetLabelText("OSL_SIGNAL_STRENGTH", "--");
      return;
     }

   string zSign = (g_stats.zscore > 0.0 ? "+" : "");
   string reason = "z " + zSign + DoubleToString(g_stats.zscore, 2) +
                   "σ  ·  " + DoubleToString(g_stats.percentile, 1) + "%分位";
   if(g_stats.halfLife > 0.0)
      reason += "  ·  半衰期 " + DoubleToString(g_stats.halfLife, 1) + "根";
   else
      reason += "  ·  均值回归证据弱";

   string action = "观望  ·  未达到统计极值";
   color actionColor = CLR_AMBER;

   if(g_signal > 0)
     {
      action = "做多价差  ·  买 Brent / 卖 WTI";
      actionColor = CLR_TEAL;
     }
   else if(g_signal < 0)
     {
      action = "做空价差  ·  卖 Brent / 买 WTI";
      actionColor = CLR_ORANGE;
     }
   else if(AvoidWideningSpread &&
            ((g_stats.zscore >= g_signalThreshold && g_stats.momentum > g_momentumBlock) ||
             (g_stats.zscore <= -g_signalThreshold && g_stats.momentum < -g_momentumBlock)))
     {
      action = "等待拐点  ·  价差仍在扩散";
      actionColor = CLR_AMBER;
      reason += "  ·  动量过滤已触发";
     }

   double strength = MathMin(100.0, MathAbs(g_stats.zscore) / 3.0 * 100.0);
   string strengthText = "偏离强度 " + DoubleToString(strength, 0) + "/100";
   SetLabelText("OSL_SIGNAL_ACTION", action);
   SetLabelColor("OSL_SIGNAL_ACTION", actionColor);
   SetLabelText("OSL_SIGNAL_REASON", reason);
   SetLabelText("OSL_SIGNAL_STRENGTH", strengthText);
  }

//+------------------------------------------------------------------+
//| Update both strategy economics blocks                           |
//+------------------------------------------------------------------+
void UpdateTradeBlocks()
  {
   if(!g_stats.valid || !g_economicsReady)
     {
      SetLabelText("OSL_LONG_NET", "净利 @ 均值  --");
      SetLabelText("OSL_SHORT_NET", "净利 @ 均值  --");
      SetLabelText("OSL_LONG_GROSS", "回归毛利 --");
      SetLabelText("OSL_SHORT_GROSS", "回归毛利 --");
      SetLabelText("OSL_LONG_COST", "点差 + 佣金 --");
      SetLabelText("OSL_SHORT_COST", "点差 + 佣金 --");
      SetLabelText("OSL_LONG_BE", "保本收敛 --");
      SetLabelText("OSL_SHORT_BE", "保本收敛 --");
      SetLabelText("OSL_TRADE_LOTS", "测算手数:每腿 -- 手");
      return;
     }

   string currency = AccountCurrency();
   SetLabelText("OSL_LONG_NET", "净利 @ 均值  " + SignedMoney(g_longNet, currency));
   SetLabelColor("OSL_LONG_NET", (g_longNet >= 0.0 ? CLR_GREEN : CLR_RED));
   SetLabelText("OSL_SHORT_NET", "净利 @ 均值  " + SignedMoney(g_shortNet, currency));
   SetLabelColor("OSL_SHORT_NET", (g_shortNet >= 0.0 ? CLR_GREEN : CLR_RED));

   SetLabelText("OSL_LONG_GROSS", "回归毛利  " + SignedMoney(g_longGross, currency));
   SetLabelText("OSL_SHORT_GROSS", "回归毛利  " + SignedMoney(g_shortGross, currency));
   SetLabelText("OSL_LONG_COST",
                "点差 " + MoneyText(g_spreadCostMoney, currency) +
                "  +  佣金 " + MoneyText(g_commissionMoney, currency));
   SetLabelText("OSL_SHORT_COST",
                "点差 " + MoneyText(g_spreadCostMoney, currency) +
                "  +  佣金 " + MoneyText(g_commissionMoney, currency));

   string lotsText = DoubleToString(MathMax(0.0, TradeLotsPerLeg), 2);
   SetLabelText("OSL_LONG_BE",
                "保本收敛 " + DoubleToString(g_breakEvenPrice, 3) +
                " USD/bbl");
   SetLabelText("OSL_SHORT_BE",
                "保本收敛 " + DoubleToString(g_breakEvenPrice, 3) +
                " USD/bbl");
   SetLabelText("OSL_TRADE_LOTS",
                "测算手数:每腿 " + lotsText + " 手  ·  账户货币 " + currency);
  }

//+------------------------------------------------------------------+
//| Update timeframe tab buttons                                    |
//+------------------------------------------------------------------+
void UpdateTimeframeButtons()
  {
   StyleTimeframeButton("OSL_BTN_M15", g_analysisTf == PERIOD_M15);
   StyleTimeframeButton("OSL_BTN_H1", g_analysisTf == PERIOD_H1);
   StyleTimeframeButton("OSL_BTN_H4", g_analysisTf == PERIOD_H4);
  }

//+------------------------------------------------------------------+
//| Style one timeframe button                                      |
//+------------------------------------------------------------------+
void StyleTimeframeButton(const string name, const bool selected)
  {
   ObjectSetInteger(0, name, OBJPROP_STATE, false);
   ObjectSetInteger(0, name, OBJPROP_BGCOLOR, (selected ? CLR_TEAL_DARK : CLR_CELL_ALT));
   ObjectSetInteger(0, name, OBJPROP_COLOR, (selected ? CLR_TEXT : CLR_MUTED));
   ObjectSetInteger(0, name, OBJPROP_BORDER_COLOR, (selected ? CLR_TEAL : CLR_BORDER));
  }

//+------------------------------------------------------------------+
//| Update unavailable symbols/data state                           |
//+------------------------------------------------------------------+
void UpdateUnavailableState()
  {
   g_stats.valid = false;
   g_historyCount = 0;
   SetLabelText("OSL_HEADER_TIME", "行情不可用");
   SetLabelText("OSL_SYM1_PRICE", "--");
   SetLabelText("OSL_SYM2_PRICE", "--");
   SetLabelText("OSL_SYM1_QUOTE", "BID --  /  ASK --");
   SetLabelText("OSL_SYM2_QUOTE", "BID --  /  ASK --");
   SetLabelText("OSL_SYM1_CHANGE", "日变动  --");
   SetLabelText("OSL_SYM2_CHANGE", "日变动  --");
   SetLabelText("OSL_SPREAD_VALUE", "--");
   SetLabelText("OSL_RATIO_VALUE", "--");
   SetLabelText("OSL_PREMIUM_VALUE", "--");
   SetLabelText("OSL_EXEC_SPREADS", "执行价差 --");
   SetLabelText("OSL_STAT_MEAN_VALUE", "--");
   SetLabelText("OSL_STAT_MEAN_SUB", "--");
   SetLabelText("OSL_STAT_SD_VALUE", "--");
   SetLabelText("OSL_STAT_SD_SUB", "--");
   SetLabelText("OSL_STAT_Z_VALUE", "--");
   SetLabelText("OSL_STAT_Z_SUB", "--");
   SetLabelText("OSL_STAT_PCT_VALUE", "--");
   SetLabelText("OSL_STAT_PCT_SUB", "--");
   SetLabelText("OSL_STAT_HL_VALUE", "--");
   SetLabelText("OSL_STAT_HL_SUB", "--");
   SetLabelText("OSL_STAT_SLOPE_VALUE", "--");
   SetLabelText("OSL_STAT_SLOPE_SUB", "--");
   SetLabelText("OSL_SIGNAL_ACTION", "等待行情");
   SetLabelColor("OSL_SIGNAL_ACTION", CLR_AMBER);
   SetLabelText("OSL_SIGNAL_REASON",
                "请确认 " + BrentSymbol + " 与 " + WtiSymbol +
                " 已在市场报价中,并允许下载历史数据。");
   SetLabelText("OSL_SIGNAL_STRENGTH", "--");
   SetLabelText("OSL_FOOTER_STATUS",
                "品种不可用或报价不完整  ·  请检查交易品种名称");
   SetLabelColor("OSL_FOOTER_STATUS", CLR_RED);
   UpdateGauge();
   UpdateTradeBlocks();
   UpdateTimeframeButtons();
  }

//+------------------------------------------------------------------+
//| Refresh footer data-quality status                              |
//+------------------------------------------------------------------+
void UpdateFooterStatus()
  {
   string barTime = (g_stats.valid && g_stats.lastBarTime > 0 ?
                     TimeToString(g_stats.lastBarTime, TIME_MINUTES) : "--:--");
   string status = "样本 " + IntegerToString(g_stats.samples) +
                   " / " + IntegerToString(MathMax(20, MathMin(LookbackBars, 1000))) +
                   "  ·  " + TimeframeName(g_analysisTf) +
                   "  ·  K线至 " + barTime +
                   "  ·  更新 " + TimeToString(TimeCurrent(), TIME_MINUTES);
   SetLabelText("OSL_FOOTER_STATUS", status);
   SetLabelColor("OSL_FOOTER_STATUS", CLR_MUTED);
  }

//+------------------------------------------------------------------+
//| Create rectangle label object                                   |
//+------------------------------------------------------------------+
void CreateRect(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)
      ObjectCreate(0, name, OBJ_RECTANGLE_LABEL, 0, 0, 0);

   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, PANEL_X + x);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, PANEL_Y + y);
   ObjectSetInteger(0, name, OBJPROP_XSIZE, width);
   ObjectSetInteger(0, name, OBJPROP_YSIZE, height);
   ObjectSetInteger(0, name, OBJPROP_BGCOLOR, background);
   ObjectSetInteger(0, name, OBJPROP_BORDER_TYPE, BORDER_FLAT);
   ObjectSetInteger(0, name, OBJPROP_COLOR, border);
   ObjectSetInteger(0, name, OBJPROP_BACK, false);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_SELECTED, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
  }

//+------------------------------------------------------------------+
//| Create text label                                               |
//+------------------------------------------------------------------+
void CreateLabel(const string name, const int x, const int y,
                 const string text, const int fontSize,
                 const color textColor, const string font,
                 const int anchor = ANCHOR_LEFT_UPPER)
  {
   if(ObjectFind(0, name) < 0)
      ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);

   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, PANEL_X + x);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, PANEL_Y + y);
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, anchor);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize);
   ObjectSetInteger(0, name, OBJPROP_COLOR, textColor);
   ObjectSetString(0, name, OBJPROP_FONT, font);
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_BACK, false);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_SELECTED, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
  }

//+------------------------------------------------------------------+
//| Create button object                                            |
//+------------------------------------------------------------------+
void CreateButton(const string name, const int x, const int y,
                  const int width, const int height, const string text)
  {
   if(ObjectFind(0, name) < 0)
      ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0);

   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, PANEL_X + x);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, PANEL_Y + y);
   ObjectSetInteger(0, name, OBJPROP_XSIZE, width);
   ObjectSetInteger(0, name, OBJPROP_YSIZE, height);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 9);
   ObjectSetString(0, name, OBJPROP_FONT, "Microsoft YaHei");
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_BGCOLOR, CLR_CELL_ALT);
   ObjectSetInteger(0, name, OBJPROP_COLOR, CLR_MUTED);
   ObjectSetInteger(0, name, OBJPROP_BORDER_COLOR, CLR_BORDER);
   ObjectSetInteger(0, name, OBJPROP_STATE, false);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_SELECTED, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
  }

//+------------------------------------------------------------------+
//| Update label text                                               |
//+------------------------------------------------------------------+
void SetLabelText(const string name, const string text)
  {
   ObjectSetString(0, name, OBJPROP_TEXT, text);
  }

//+------------------------------------------------------------------+
//| Update label color                                              |
//+------------------------------------------------------------------+
void SetLabelColor(const string name, const color textColor)
  {
   ObjectSetInteger(0, name, OBJPROP_COLOR, textColor);
  }

//+------------------------------------------------------------------+
//| Update rectangle geometry                                       |
//+------------------------------------------------------------------+
void SetRectGeometry(const string name, const int x, const int y,
                     const int width, const int height)
  {
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, PANEL_X + x);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, PANEL_Y + y);
   ObjectSetInteger(0, name, OBJPROP_XSIZE, MathMax(1, width));
   ObjectSetInteger(0, name, OBJPROP_YSIZE, MathMax(1, height));
  }

//+------------------------------------------------------------------+
//| Update rectangle colors                                         |
//+------------------------------------------------------------------+
void SetRectColor(const string name, const color background, const color border)
  {
   ObjectSetInteger(0, name, OBJPROP_BGCOLOR, background);
   ObjectSetInteger(0, name, OBJPROP_COLOR, border);
  }

//+------------------------------------------------------------------+
//| Delete only this EA's chart objects                             |
//+------------------------------------------------------------------+
void DeleteDashboard()
  {
   for(int i = ObjectsTotal(0, -1, -1) - 1; i >= 0; i--)
     {
      string name = ObjectName(0, i, -1, -1);
      if(StringFind(name, OBJECT_PREFIX) == 0)
         ObjectDelete(0, name);
     }
  }

//+------------------------------------------------------------------+
//| Format price with symbol digits                                 |
//+------------------------------------------------------------------+
string FormatPrice(const double price, const int digits)
  {
   int useDigits = MathMax(0, MathMin(8, digits));
   return DoubleToString(price, useDigits);
  }

//+------------------------------------------------------------------+
//| Signed account-currency amount                                  |
//+------------------------------------------------------------------+
string SignedMoney(const double value, const string currency)
  {
   string sign = (value > 0.0 ? "+" : "");
   return sign + DoubleToString(value, 2) + " " + currency;
  }

//+------------------------------------------------------------------+
//| Unsigned account-currency amount                                |
//+------------------------------------------------------------------+
string MoneyText(const double value, const string currency)
  {
   return DoubleToString(value, 2) + " " + currency;
  }

//+------------------------------------------------------------------+
//| Timeframe display name                                          |
//+------------------------------------------------------------------+
string TimeframeName(const int timeframe)
  {
   switch(timeframe)
     {
      case PERIOD_M1:  return "M1";
      case PERIOD_M5:  return "M5";
      case PERIOD_M15: return "M15";
      case PERIOD_M30: return "M30";
      case PERIOD_H1:  return "H1";
      case PERIOD_H4:  return "H4";
      case PERIOD_D1:  return "D1";
      case PERIOD_W1:  return "W1";
      case PERIOD_MN1: return "MN1";
      default:         return "TF" + IntegerToString(timeframe);
     }
  }
//+------------------------------------------------------------------+