返回
OilSpreadAnalyst V2
MQL4
EA · 智能交易系统
v2.0
98video
👁 16 浏览
⬇ 11 下载
📅 2026-11-09 00:31:32
更新时间: 2026-11-09 00:45:09
详情
产品概述
OilSpreadAnalyst V2 是一套面向 MetaTrader 4 的 Brent / WTI 原油价差决策面板。在保留统计分析和成本测算的基础上,V2 将界面压缩为更直观的关键区域:当前建议、当前价差、两个品种报价、关键统计、做多净收益和做空净收益。
V2 改进
- 当前建议放在最醒目的位置,直接显示做多价差、做空价差、观望或等待拐点。
- 关键统计只保留均值、Z-Score、百分位和半衰期,减少信息干扰。
- 同时展示做多价差与做空价差的毛利润、点差与佣金、净收益和保本收敛空间。
- 新增“一键做多价差”和“一键做空价差”按钮,可同时建立 Brent / WTI 两条腿。
- 默认限制同时只保留一组价差仓位;若其中一条腿成交失败,会尝试回补已成交的另一条腿。
- 保留 M15 / H1 / H4 切换、动量过滤、提醒和手动刷新。
核心功能
- 实时显示 UKOILm(Brent)与 USOILm(WTI)的中间价、BID / ASK 和日涨跌幅。
- 计算 Brent-WTI 中间价差、Brent/WTI 比价、Brent 溢价和双向执行价差。
- 基于最近 N 根 K 线计算均值、标准差、Z-Score、百分位、半衰期、动量和斜率。
- 价差极端但动量仍在扩大时,面板提示“等待拐点”,避免盲目逆势进场。
- 使用两侧实际点差和输入佣金估算净收益,帮助过滤理论空间不足的交易机会。
信号与执行
默认信号阈值为 Z-Score ±1.5σ。价差高于历史均值时偏向做空价差,即卖出 Brent、买入 WTI;价差低于历史均值时偏向做多价差,即买入 Brent、卖出 WTI;偏离不足时显示观望。
V2 的一键交易默认启用。使用前请确认 MT4 已开启自动交易权限,账户和经纪商允许相关品种交易,并检查手数、佣金、滑点、MagicNumber 和价差仓位限制。一键开仓不会改变市场风险,仍需自行确认保证金和流动性。
适用对象
- 平台:MetaTrader 4。
- 默认品种:UKOILm / USOILm,可按经纪商品种名称修改。
- 默认周期:H1;默认统计样本:120 根 K 线。
- 适合希望快速读取原油跨品种价差、并可按按钮执行双向仓位的交易者。
风险提醒
统计信号不是盈利保证。价差可能长期偏离甚至继续扩大;真实交易还会受到滑点、隔夜利息、点差变化、保证金、成交时间和两腿执行不同步的影响。请先在小手数或模拟账户中验证。
源代码
MQL4//+------------------------------------------------------------------+
//| OilSpreadAnalystV2.mq4 |
//| Brent / WTI spread analysis dashboard |
//+------------------------------------------------------------------+
#property copyright "Oil Spread Analyst"
#property link "https://www.mql5.com"
#property version "2.00"
#property strict
#property description "Simplified Brent/WTI spread dashboard: one clear recommendation, key stats, 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;
input bool EnableOneClickTrading = true;
input int MagicNumber = 20261108;
input int SlippagePoints = 20;
input bool OnlyOneSpreadPosition = true;
//--- Alerts and refresh
input bool EnableAlerts = true;
input int RefreshSeconds = 1;
//--- Layout
#define PANEL_X 16
#define PANEL_Y 4
#define PANEL_W 860
#define PANEL_H 516
#define OBJECT_PREFIX "OSV2_"
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 == "OSV2_BTN_LONG")
{
ObjectSetInteger(0, sparam, OBJPROP_STATE, false);
OpenSpreadTrade(1);
return;
}
if(sparam == "OSV2_BTN_SHORT")
{
ObjectSetInteger(0, sparam, OBJPROP_STATE, false);
OpenSpreadTrade(-1);
return;
}
if(sparam == "OSV2_BTN_M15")
{
SetAnalysisTimeframe(PERIOD_M15);
return;
}
if(sparam == "OSV2_BTN_H1")
{
SetAnalysisTimeframe(PERIOD_H1);
return;
}
if(sparam == "OSV2_BTN_H4")
{
SetAnalysisTimeframe(PERIOD_H4);
return;
}
if(sparam == "OSV2_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, "OSV2_BTN_M15", OBJPROP_STATE, false);
ObjectSetInteger(0, "OSV2_BTN_H1", OBJPROP_STATE, false);
ObjectSetInteger(0, "OSV2_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;
}
//+------------------------------------------------------------------+
//| Normalize lot size for a symbol |
//+------------------------------------------------------------------+
bool NormalizeLots(const string symbol, const double lots, double &normalizedLots)
{
double minimumLot = MarketInfo(symbol, MODE_MINLOT);
double maximumLot = MarketInfo(symbol, MODE_MAXLOT);
double lotStep = MarketInfo(symbol, MODE_LOTSTEP);
if(lotStep <= 0.0)
lotStep = 0.01;
normalizedLots = MathFloor(lots / lotStep + 0.0000001) * lotStep;
normalizedLots = MathMax(minimumLot, MathMin(maximumLot, normalizedLots));
normalizedLots = NormalizeDouble(normalizedLots, 2);
return normalizedLots > 0.0;
}
//+------------------------------------------------------------------+
//| Check whether this EA already has an open spread position |
//+------------------------------------------------------------------+
bool HasOpenSpreadPosition()
{
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
continue;
if(OrderMagicNumber() != MagicNumber)
continue;
if(OrderSymbol() != BrentSymbol && OrderSymbol() != WtiSymbol)
continue;
if(OrderType() == OP_BUY || OrderType() == OP_SELL)
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| Close one leg if the paired order fails |
//+------------------------------------------------------------------+
bool CloseOrderTicket(const int ticket)
{
if(!OrderSelect(ticket, SELECT_BY_TICKET, MODE_TRADES))
return false;
if(OrderType() != OP_BUY && OrderType() != OP_SELL)
return false;
RefreshRates();
double closePrice = (OrderType() == OP_BUY ?
MarketInfo(OrderSymbol(), MODE_BID) :
MarketInfo(OrderSymbol(), MODE_ASK));
if(closePrice <= 0.0)
return false;
return OrderClose(ticket, OrderLots(), closePrice, SlippagePoints, clrNONE);
}
//+------------------------------------------------------------------+
//| Open a two-leg spread trade |
//+------------------------------------------------------------------+
void OpenSpreadTrade(const int direction)
{
if(!EnableOneClickTrading)
{
Alert("一键交易已关闭:请在 EA 参数中开启 EnableOneClickTrading。");
return;
}
if(!IsTradeAllowed() || IsTradeContextBusy())
{
Alert("一键交易失败:请开启自动交易,或稍后重试。");
return;
}
if(!g_quotesReady)
{
Alert("一键交易失败:行情不可用。");
return;
}
if(OnlyOneSpreadPosition && HasOpenSpreadPosition())
{
Alert("一键交易失败:当前已存在本 EA 的价差仓位。");
return;
}
double lotsBrent = 0.0;
double lotsWti = 0.0;
if(!NormalizeLots(BrentSymbol, TradeLotsPerLeg, lotsBrent) ||
!NormalizeLots(WtiSymbol, TradeLotsPerLeg, lotsWti))
{
Alert("一键交易失败:手数不符合品种最小手数。");
return;
}
int brentType = (direction > 0 ? OP_BUY : OP_SELL);
int wtiType = (direction > 0 ? OP_SELL : OP_BUY);
string comment = (direction > 0 ? "OSV2 LONG SPREAD" : "OSV2 SHORT SPREAD");
ResetLastError();
double brentMargin = AccountFreeMarginCheck(BrentSymbol, brentType, lotsBrent);
double wtiMargin = AccountFreeMarginCheck(WtiSymbol, wtiType, lotsWti);
if(brentMargin <= 0.0 || wtiMargin <= 0.0 || GetLastError() == 134)
{
Alert("一键交易失败:账户保证金不足。");
return;
}
RefreshRates();
double brentPrice = (brentType == OP_BUY ? g_askBrent : g_bidBrent);
double wtiPrice = (wtiType == OP_BUY ? g_askWti : g_bidWti);
color brentColor = (brentType == OP_BUY ? CLR_TEAL : CLR_ORANGE);
color wtiColor = (wtiType == OP_BUY ? CLR_TEAL : CLR_ORANGE);
int brentTicket = OrderSend(BrentSymbol, brentType, lotsBrent, brentPrice,
SlippagePoints, 0, 0, comment,
MagicNumber, 0, brentColor);
if(brentTicket < 0)
{
Alert("一键交易失败:Brent 腿未成交,错误码 " + IntegerToString(GetLastError()));
return;
}
RefreshRates();
wtiPrice = (wtiType == OP_BUY ? MarketInfo(WtiSymbol, MODE_ASK) :
MarketInfo(WtiSymbol, MODE_BID));
int wtiTicket = OrderSend(WtiSymbol, wtiType, lotsWti, wtiPrice,
SlippagePoints, 0, 0, comment,
MagicNumber, 0, wtiColor);
if(wtiTicket < 0)
{
int closeError = GetLastError();
if(!CloseOrderTicket(brentTicket))
Alert("警告:WTI 腿未成交,Brent 腿回补失败,请立即人工处理。");
else
Alert("一键交易失败:WTI 腿未成交,Brent 腿已回补。错误码 " +
IntegerToString(closeError));
return;
}
Alert((direction > 0 ? "已一键做多价差:" : "已一键做空价差:") +
"买/卖 " + BrentSymbol + " " + DoubleToString(lotsBrent, 2) + "手," +
"卖/买 " + WtiSymbol + " " + DoubleToString(lotsWti, 2) + "手。");
g_lastRefresh = 0;
UpdateDashboard(true);
}
//+------------------------------------------------------------------+
//| 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("OSV2_BG", 0, 0, PANEL_W, PANEL_H, CLR_PANEL, CLR_BORDER);
CreateRect("OSV2_HEADER", 0, 0, PANEL_W, 46, CLR_HEADER, CLR_BORDER);
CreateRect("OSV2_ACCENT", 0, 0, 5, 46, CLR_TEAL, CLR_TEAL);
CreateLabel("OSV2_TITLE", 22, 9, "OIL SPREAD V2", 16, CLR_TEXT, "Segoe UI Semibold");
CreateLabel("OSV2_SUBTITLE", 22, 29, "Brent / WTI 价差速览", 9, CLR_MUTED, "Microsoft YaHei");
CreateLabel("OSV2_HEADER_SYMBOLS", 826, 10, BrentSymbol + " / " + WtiSymbol, 9, CLR_TEXT, "Consolas", ANCHOR_RIGHT_UPPER);
CreateLabel("OSV2_HEADER_TIME", 826, 30, "等待行情", 8, CLR_MUTED, "Consolas", ANCHOR_RIGHT_UPPER);
CreateRect("OSV2_SIGNAL_BG", 16, 54, 532, 132, CLR_CELL, CLR_BORDER);
CreateLabel("OSV2_SIGNAL_CAPTION", 31, 62, "当前建议", 8, CLR_MUTED, "Microsoft YaHei");
CreateLabel("OSV2_SIGNAL_ACTION", 31, 77, "等待数据", 24, CLR_AMBER, "Microsoft YaHei");
CreateLabel("OSV2_SIGNAL_DETAIL", 31, 115, "--", 11, CLR_TEXT, "Microsoft YaHei");
CreateLabel("OSV2_SIGNAL_REASON", 31, 139, "--", 8, CLR_MUTED, "Microsoft YaHei");
CreateLabel("OSV2_SIGNAL_STRENGTH", 531, 62, "--", 8, CLR_MUTED, "Consolas", ANCHOR_RIGHT_UPPER);
CreateRect("OSV2_GAUGE_TRACK", 31, 166, 470, 8, CLR_GAUGE, CLR_GAUGE);
CreateRect("OSV2_GAUGE_LONG_ZONE", 31, 166, 117, 8, CLR_TEAL_DARK, CLR_TEAL_DARK);
CreateRect("OSV2_GAUGE_SHORT_ZONE", 384, 166, 117, 8, C'84,50,35', C'84,50,35');
CreateRect("OSV2_GAUGE_MID", 265, 164, 2, 12, CLR_ZERO, CLR_ZERO);
CreateRect("OSV2_GAUGE_FILL", 265, 167, 2, 5, CLR_TEAL, CLR_TEAL);
CreateRect("OSV2_GAUGE_MARKER", 263, 163, 6, 14, CLR_TEXT, CLR_TEXT);
CreateLabel("OSV2_GAUGE_LEFT_VALUE", 31, 176, "左极值 --", 7, CLR_MUTED, "Microsoft YaHei");
CreateLabel("OSV2_GAUGE_RIGHT_VALUE", 501, 176, "右极值 --", 7, CLR_MUTED, "Microsoft YaHei", ANCHOR_RIGHT_UPPER);
CreateRect("OSV2_SPREAD_BG", 556, 54, 288, 132, CLR_CELL_ALT, CLR_BORDER);
CreateLabel("OSV2_SPREAD_CAPTION", 571, 62, "当前价差", 8, CLR_MUTED, "Microsoft YaHei");
CreateLabel("OSV2_SPREAD_VALUE", 571, 79, "--", 27, CLR_TEXT, "Consolas");
CreateLabel("OSV2_SPREAD_UNIT", 571, 117, "USD / bbl", 8, CLR_MUTED, "Segoe UI");
CreateLabel("OSV2_RATIO_VALUE", 571, 140, "比价 -- · 溢价 --", 9, CLR_TEXT, "Consolas");
CreateLabel("OSV2_EXEC_SPREADS", 571, 160, "执行 多 -- / 空 --", 8, CLR_MUTED, "Consolas");
CreateRect("OSV2_SYM1_BG", 16, 194, 260, 108, CLR_CELL, CLR_BORDER);
CreateLabel("OSV2_SYM1_NAME", 31, 202, BrentSymbol + " · BRENT", 9, CLR_TEAL, "Segoe UI Semibold");
CreateLabel("OSV2_SYM1_PRICE", 31, 220, "--", 20, CLR_TEXT, "Consolas");
CreateLabel("OSV2_SYM1_QUOTE", 31, 252, "BID -- / ASK --", 8, CLR_MUTED, "Consolas");
CreateLabel("OSV2_SYM1_CHANGE", 31, 272, "日变动 --", 8, CLR_MUTED, "Consolas");
CreateRect("OSV2_SYM2_BG", 284, 194, 260, 108, CLR_CELL, CLR_BORDER);
CreateLabel("OSV2_SYM2_NAME", 299, 202, WtiSymbol + " · WTI", 9, CLR_ORANGE, "Segoe UI Semibold");
CreateLabel("OSV2_SYM2_PRICE", 299, 220, "--", 20, CLR_TEXT, "Consolas");
CreateLabel("OSV2_SYM2_QUOTE", 299, 252, "BID -- / ASK --", 8, CLR_MUTED, "Consolas");
CreateLabel("OSV2_SYM2_CHANGE", 299, 272, "日变动 --", 8, CLR_MUTED, "Consolas");
CreateRect("OSV2_STATS_BG", 556, 194, 288, 108, CLR_CELL, CLR_BORDER);
CreateLabel("OSV2_STATS_CAPTION", 571, 202, "关键统计", 8, CLR_MUTED, "Microsoft YaHei");
CreateLabel("OSV2_STAT_MEAN_CAPTION", 571, 223, "均值", 9, CLR_MUTED, "Microsoft YaHei");
CreateLabel("OSV2_STAT_MEAN_VALUE", 826, 223, "--", 10, CLR_TEXT, "Consolas", ANCHOR_RIGHT_UPPER);
CreateLabel("OSV2_STAT_Z_CAPTION", 571, 244, "Z-SCORE", 9, CLR_MUTED, "Microsoft YaHei");
CreateLabel("OSV2_STAT_Z_VALUE", 826, 244, "--", 10, CLR_TEXT, "Consolas", ANCHOR_RIGHT_UPPER);
CreateLabel("OSV2_STAT_PCT_CAPTION", 571, 265, "百分位", 9, CLR_MUTED, "Microsoft YaHei");
CreateLabel("OSV2_STAT_PCT_VALUE", 826, 265, "--", 10, CLR_TEXT, "Consolas", ANCHOR_RIGHT_UPPER);
CreateLabel("OSV2_STAT_HL_CAPTION", 571, 286, "半衰期", 9, CLR_MUTED, "Microsoft YaHei");
CreateLabel("OSV2_STAT_HL_VALUE", 826, 286, "--", 10, CLR_TEXT, "Consolas", ANCHOR_RIGHT_UPPER);
CreateRect("OSV2_LONG_BG", 16, 310, 414, 120, CLR_CELL, CLR_BORDER);
CreateLabel("OSV2_LONG_TITLE", 31, 317, "做多价差净利", 10, CLR_TEAL, "Microsoft YaHei");
CreateLabel("OSV2_LONG_NET", 31, 333, "--", 24, CLR_TEXT, "Consolas");
CreateLabel("OSV2_LONG_DETAIL", 31, 368, "买 Brent / 卖 WTI", 9, CLR_MUTED, "Microsoft YaHei");
CreateLabel("OSV2_LONG_GROSS", 31, 387, "回归毛利 --", 8, CLR_MUTED, "Microsoft YaHei");
CreateLabel("OSV2_LONG_COST", 31, 403, "点差 + 佣金 --", 8, CLR_MUTED, "Microsoft YaHei");
CreateLabel("OSV2_LONG_BE", 31, 419, "保本收敛 --", 8, CLR_MUTED, "Microsoft YaHei");
CreateRect("OSV2_SHORT_BG", 438, 310, 414, 120, CLR_CELL, CLR_BORDER);
CreateLabel("OSV2_SHORT_TITLE", 453, 317, "做空价差净利", 10, CLR_ORANGE, "Microsoft YaHei");
CreateLabel("OSV2_SHORT_NET", 453, 333, "--", 24, CLR_TEXT, "Consolas");
CreateLabel("OSV2_SHORT_DETAIL", 453, 368, "卖 Brent / 买 WTI", 9, CLR_MUTED, "Microsoft YaHei");
CreateLabel("OSV2_SHORT_GROSS", 453, 387, "回归毛利 --", 8, CLR_MUTED, "Microsoft YaHei");
CreateLabel("OSV2_SHORT_COST", 453, 403, "点差 + 佣金 --", 8, CLR_MUTED, "Microsoft YaHei");
CreateLabel("OSV2_SHORT_BE", 453, 419, "保本收敛 --", 8, CLR_MUTED, "Microsoft YaHei");
CreateButton("OSV2_BTN_LONG", 16, 438, 414, 36, "一键做多价差");
CreateButton("OSV2_BTN_SHORT", 438, 438, 414, 36, "一键做空价差");
ObjectSetInteger(0, "OSV2_BTN_LONG", OBJPROP_BGCOLOR, CLR_TEAL_DARK);
ObjectSetInteger(0, "OSV2_BTN_LONG", OBJPROP_COLOR, CLR_TEXT);
ObjectSetInteger(0, "OSV2_BTN_LONG", OBJPROP_BORDER_COLOR, CLR_TEAL);
ObjectSetInteger(0, "OSV2_BTN_SHORT", OBJPROP_BGCOLOR, C'84,50,35');
ObjectSetInteger(0, "OSV2_BTN_SHORT", OBJPROP_COLOR, CLR_TEXT);
ObjectSetInteger(0, "OSV2_BTN_SHORT", OBJPROP_BORDER_COLOR, CLR_ORANGE);
CreateRect("OSV2_FOOTER", 16, 482, 828, 30, CLR_HEADER, CLR_BORDER);
CreateLabel("OSV2_FOOTER_STATUS", 31, 489, "正在读取历史行情...", 8, CLR_MUTED, "Microsoft YaHei");
CreateButton("OSV2_BTN_M15", 680, 486, 38, 22, "M15");
CreateButton("OSV2_BTN_H1", 723, 486, 38, 22, "H1");
CreateButton("OSV2_BTN_H4", 766, 486, 38, 22, "H4");
CreateButton("OSV2_BTN_REFRESH", 809, 486, 52, 22, "刷新");
}
//+------------------------------------------------------------------+
//| 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("OSV2_HEADER_SYMBOLS", BrentSymbol + " / " + WtiSymbol);
SetLabelText("OSV2_HEADER_TIME", "服务器 " + TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS));
SetLabelText("OSV2_SYM1_NAME", BrentSymbol + " · BRENT");
SetLabelText("OSV2_SYM2_NAME", WtiSymbol + " · WTI");
}
//+------------------------------------------------------------------+
//| Update live quote blocks |
//+------------------------------------------------------------------+
void UpdateQuoteBlocks()
{
SetLabelText("OSV2_SYM1_PRICE", FormatPrice(g_midBrent, g_digitsBrent));
SetLabelText("OSV2_SYM2_PRICE", FormatPrice(g_midWti, g_digitsWti));
SetLabelText("OSV2_SYM1_QUOTE",
"BID " + FormatPrice(g_bidBrent, g_digitsBrent) +
" / ASK " + FormatPrice(g_askBrent, g_digitsBrent));
SetLabelText("OSV2_SYM2_QUOTE",
"BID " + FormatPrice(g_bidWti, g_digitsWti) +
" / ASK " + FormatPrice(g_askWti, g_digitsWti));
UpdateDailyChange("OSV2_SYM1_CHANGE", BrentSymbol, g_midBrent, g_digitsBrent);
UpdateDailyChange("OSV2_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("OSV2_SPREAD_VALUE", DoubleToString(g_midSpread, 3));
string premiumSign = (g_premiumPct > 0.0 ? "+" : "");
SetLabelText("OSV2_RATIO_VALUE",
"比价 " + DoubleToString(g_ratio, 4) +
" · 溢价 " + premiumSign + DoubleToString(g_premiumPct, 2) + "%");
double longExecutable = g_askBrent - g_bidWti;
double shortExecutable = g_bidBrent - g_askWti;
SetLabelText("OSV2_EXEC_SPREADS",
"执行 多 " + DoubleToString(longExecutable, 3) +
" / 空 " + DoubleToString(shortExecutable, 3));
}
//+------------------------------------------------------------------+
//| Update statistics block |
//+------------------------------------------------------------------+
void UpdateStatisticsBlock()
{
if(!g_stats.valid)
{
SetLabelText("OSV2_STAT_MEAN_VALUE", "--");
SetLabelText("OSV2_STAT_Z_VALUE", "--");
SetLabelText("OSV2_STAT_PCT_VALUE", "--");
SetLabelText("OSV2_STAT_HL_VALUE", "--");
return;
}
SetLabelText("OSV2_STAT_MEAN_VALUE", DoubleToString(g_stats.mean, 3));
string zSign = (g_stats.zscore > 0.0 ? "+" : "");
SetLabelText("OSV2_STAT_Z_VALUE", zSign + DoubleToString(g_stats.zscore, 2) + "σ");
SetLabelColor("OSV2_STAT_Z_VALUE",
(g_stats.zscore >= g_signalThreshold ? CLR_ORANGE :
(g_stats.zscore <= -g_signalThreshold ? CLR_TEAL : CLR_TEXT)));
SetLabelText("OSV2_STAT_PCT_VALUE", DoubleToString(g_stats.percentile, 1) + "%");
if(g_stats.halfLife > 0.0)
SetLabelText("OSV2_STAT_HL_VALUE", DoubleToString(g_stats.halfLife, 1) + "根");
else
SetLabelText("OSV2_STAT_HL_VALUE", "N/A");
}
//+------------------------------------------------------------------+
//| 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 = 31;
int centerX = trackX + 235;
int markerX = trackX + (int)MathRound((clamped + 3.0) / 6.0 * 470.0);
markerX = MathMax(trackX + 1, MathMin(trackX + 469, 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("OSV2_GAUGE_FILL", fillX, 167, fillWidth, 5);
SetRectColor("OSV2_GAUGE_FILL", activeColor, activeColor);
SetRectGeometry("OSV2_GAUGE_MARKER", markerX - 3, 163, 6, 14);
SetRectColor("OSV2_GAUGE_MARKER", activeColor, activeColor);
if(g_stats.valid)
{
double leftExtreme = g_stats.mean - 3.0 * g_stats.stddev;
double rightExtreme = g_stats.mean + 3.0 * g_stats.stddev;
SetLabelText("OSV2_GAUGE_LEFT_VALUE",
"左极值 " + DoubleToString(leftExtreme, 3));
SetLabelText("OSV2_GAUGE_RIGHT_VALUE",
"右极值 " + DoubleToString(rightExtreme, 3));
}
else
{
SetLabelText("OSV2_GAUGE_LEFT_VALUE", "左极值 --");
SetLabelText("OSV2_GAUGE_RIGHT_VALUE", "右极值 --");
}
}
//+------------------------------------------------------------------+
//| Update signal block |
//+------------------------------------------------------------------+
void UpdateSignalBlock()
{
if(!g_stats.valid)
{
SetLabelText("OSV2_SIGNAL_ACTION", "等待数据");
SetLabelColor("OSV2_SIGNAL_ACTION", CLR_AMBER);
SetLabelText("OSV2_SIGNAL_DETAIL", "双品种历史尚未对齐");
SetLabelText("OSV2_SIGNAL_REASON", "请检查品种名称与图表历史");
SetLabelText("OSV2_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 = "观望";
string detail = "价差未达到统计极值";
color actionColor = CLR_AMBER;
if(g_signal > 0)
{
action = "做多价差";
detail = "买 Brent / 卖 WTI";
actionColor = CLR_TEAL;
}
else if(g_signal < 0)
{
action = "做空价差";
detail = "卖 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 = "等待拐点";
detail = "价差仍在扩散";
actionColor = CLR_AMBER;
reason += " · 动量过滤已触发";
}
double strength = MathMin(100.0, MathAbs(g_stats.zscore) / 3.0 * 100.0);
string strengthText = "偏离强度 " + DoubleToString(strength, 0) + "/100";
SetLabelText("OSV2_SIGNAL_ACTION", action);
SetLabelColor("OSV2_SIGNAL_ACTION", actionColor);
SetLabelText("OSV2_SIGNAL_DETAIL", detail);
SetLabelColor("OSV2_SIGNAL_DETAIL", actionColor);
SetLabelText("OSV2_SIGNAL_REASON", reason);
SetLabelText("OSV2_SIGNAL_STRENGTH", strengthText);
}
//+------------------------------------------------------------------+
//| Update both strategy economics blocks |
//+------------------------------------------------------------------+
void UpdateTradeBlocks()
{
if(!g_stats.valid || !g_economicsReady)
{
SetLabelText("OSV2_LONG_NET", "--");
SetLabelText("OSV2_SHORT_NET", "--");
SetLabelText("OSV2_LONG_GROSS", "回归毛利 --");
SetLabelText("OSV2_SHORT_GROSS", "回归毛利 --");
SetLabelText("OSV2_LONG_COST", "点差 + 佣金 --");
SetLabelText("OSV2_SHORT_COST", "点差 + 佣金 --");
SetLabelText("OSV2_LONG_BE", "保本收敛 --");
SetLabelText("OSV2_SHORT_BE", "保本收敛 --");
return;
}
string currency = AccountCurrency();
SetLabelText("OSV2_LONG_NET", SignedMoney(g_longNet, currency));
SetLabelColor("OSV2_LONG_NET", (g_longNet >= 0.0 ? CLR_GREEN : CLR_RED));
SetLabelText("OSV2_SHORT_NET", SignedMoney(g_shortNet, currency));
SetLabelColor("OSV2_SHORT_NET", (g_shortNet >= 0.0 ? CLR_GREEN : CLR_RED));
SetLabelText("OSV2_LONG_GROSS", "回归毛利 " + SignedMoney(g_longGross, currency));
SetLabelText("OSV2_SHORT_GROSS", "回归毛利 " + SignedMoney(g_shortGross, currency));
SetLabelText("OSV2_LONG_COST",
"点差 " + MoneyText(g_spreadCostMoney, currency) +
" + 佣金 " + MoneyText(g_commissionMoney, currency));
SetLabelText("OSV2_SHORT_COST",
"点差 " + MoneyText(g_spreadCostMoney, currency) +
" + 佣金 " + MoneyText(g_commissionMoney, currency));
string lotsText = DoubleToString(MathMax(0.0, TradeLotsPerLeg), 2);
SetLabelText("OSV2_LONG_BE",
"保本收敛 " + DoubleToString(g_breakEvenPrice, 3) +
" USD/bbl · 每腿 " + lotsText + "手");
SetLabelText("OSV2_SHORT_BE",
"保本收敛 " + DoubleToString(g_breakEvenPrice, 3) +
" USD/bbl · 每腿 " + lotsText + "手");
}
//+------------------------------------------------------------------+
//| Update timeframe tab buttons |
//+------------------------------------------------------------------+
void UpdateTimeframeButtons()
{
StyleTimeframeButton("OSV2_BTN_M15", g_analysisTf == PERIOD_M15);
StyleTimeframeButton("OSV2_BTN_H1", g_analysisTf == PERIOD_H1);
StyleTimeframeButton("OSV2_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("OSV2_HEADER_TIME", "行情不可用");
SetLabelText("OSV2_SYM1_PRICE", "--");
SetLabelText("OSV2_SYM2_PRICE", "--");
SetLabelText("OSV2_SYM1_QUOTE", "BID -- / ASK --");
SetLabelText("OSV2_SYM2_QUOTE", "BID -- / ASK --");
SetLabelText("OSV2_SYM1_CHANGE", "日变动 --");
SetLabelText("OSV2_SYM2_CHANGE", "日变动 --");
SetLabelText("OSV2_SPREAD_VALUE", "--");
SetLabelText("OSV2_RATIO_VALUE", "比价 -- · 溢价 --");
SetLabelText("OSV2_EXEC_SPREADS", "执行 --");
SetLabelText("OSV2_STAT_MEAN_VALUE", "--");
SetLabelText("OSV2_STAT_Z_VALUE", "--");
SetLabelText("OSV2_STAT_PCT_VALUE", "--");
SetLabelText("OSV2_STAT_HL_VALUE", "--");
SetLabelText("OSV2_GAUGE_LEFT_VALUE", "左极值 --");
SetLabelText("OSV2_GAUGE_RIGHT_VALUE", "右极值 --");
SetLabelText("OSV2_SIGNAL_ACTION", "等待行情");
SetLabelColor("OSV2_SIGNAL_ACTION", CLR_AMBER);
SetLabelText("OSV2_SIGNAL_DETAIL", "报价不完整");
SetLabelColor("OSV2_SIGNAL_DETAIL", CLR_AMBER);
SetLabelText("OSV2_SIGNAL_REASON", "请检查品种名称与市场报价");
SetLabelText("OSV2_SIGNAL_STRENGTH", "--");
SetLabelText("OSV2_FOOTER_STATUS", "品种不可用或报价不完整");
SetLabelColor("OSV2_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("OSV2_FOOTER_STATUS", status);
SetLabelColor("OSV2_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);
}
}
//+------------------------------------------------------------------+