SWFX到MT4集成开发文档_2
property strict
property indicator_separate_window
property indicator_buffers 0
// SWFX XAUUSD DOM (HTTP) — Polls local JSON endpoint and renders DOM table
// Contract per SWFX到MT4集成开发文档.md
input string InpServerURL = "http://127.0.0.1:8787/v1/orderbook?symbol=XAUUSD&levels=20";
input int InpLevels = 20; // 显示档数上限(<=服务端levels)
input int InpRefreshMs = 500; // 轮询间隔(ms);注意MT4 Timer最小1秒,结合OnCalculate节流
input int InpStaleTtlMs = 1500; // 新鲜度阈值(ms)
input color InpHdrColor = clrSilver;
input color InpBidColor = clrLime;
input color InpAskColor = clrTomato;
input color InpLiveColor = clrWhite;
input color InpStaleColor = clrDarkGray;
input color InpLostColor = clrRed;
input int InpFontSize = 10;
input string InpQtyUnitLabel = "api_raw"; // 尾注单位标识
string g_label = "SWFX_DOM_HTTP_LABEL";
ulong g_last_fetch_ms = 0;
int g_http_timeout = 400; // ms
string g_last_error = "";
// --------------------- Utils ---------------------
string Trim(string s){
int i=0,j=StringLen(s)-1;
while(i<=j && StringGetChar(s,i)<=32) i++;
while(j>=i && StringGetChar(s,j)<=32) j--;
if(i>j) return "";
return StringSubstr(s,i,j-i+1);
}
int IndexOf(string s, string pat, int start){
return StringFind(s, pat, start);
}
bool IsDigitChar(ushort c){ return (c>='0' && c<='9') || c=='-' || c=='+' || c=='.' || c=='e' || c=='E'; }
// Reads a JSON number starting at idx (possibly after colon). Returns value and updates idx to first char after number.
bool ReadJsonNumber(string s, int &idx, double &out){
// skip non-number chars
int n = StringLen(s);
while(idx<n){ ushort c = StringGetChar(s, idx); if(IsDigitChar(c)) break; idx++; }
if(idx>=n) return false;
int start = idx;
while(idx<n){ ushort c = StringGetChar(s, idx); if(!IsDigitChar(c)) break; idx++; }
string tok = StringSubstr(s, start, idx-start);
out = StrToDouble(tok);
return true;
}
// --------------------- JSON parsing for our contract ---------------------
bool JsonGetTopNumber(string s, string key, double &val){
string pat = "\""+key+"\":";
int p = IndexOf(s, pat, 0);
if(p<0) return false;
p += StringLen(pat);
return ReadJsonNumber(s, p, val);
}
int JsonParseSide(string s, string sideKey, double &px[], double &qty[]){
ArrayResize(px,0); ArrayResize(qty,0);
string pat = "\""+sideKey+"\":[";
int i = IndexOf(s, pat, 0);
if(i<0) return 0;
i += StringLen(pat);
int n = StringLen(s);
while(i<n){
// find next '{' or ']' end
int ob = IndexOf(s, "{", i);
int rb = IndexOf(s, "]", i);
if(rb>=0 && (ob<0 || rb<ob)) break; // end of array
if(ob<0) break;
int j = ob;
// find p
int kp = IndexOf(s, "\"p\":", j);
if(kp<0) break;
kp += 4; // len of "p":
double pval;
if(!ReadJsonNumber(s, kp, pval)) break;
// find q after p
int kq = IndexOf(s, "\"q\":", kp);
if(kq<0) break;
kq += 4; // len of "q":
double qval;
if(!ReadJsonNumber(s, kq, qval)) break;
int sz = ArraySize(px);
ArrayResize(px, sz+1); ArrayResize(qty, sz+1);
px[sz] = pval; qty[sz] = qval;
// move i to after this object
int cb = IndexOf(s, "}", kq);
if(cb<0){ i = kq; } else { i = cb+1; }
}
return ArraySize(px);
}
void EnsureLabel(){
if(ObjectFind(0, g_label) < 0){
ObjectCreate(0, g_label, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, g_label, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, g_label, OBJPROP_XDISTANCE, 6);
ObjectSetInteger(0, g_label, OBJPROP_YDISTANCE, 6);
ObjectSetString(0, g_label, OBJPROP_FONT, "Consolas");
ObjectSetInteger(0, g_label, OBJPROP_FONTSIZE, InpFontSize);
}
}
string PadLeft(string s, int w){ int len=StringLen(s); if(len>=w) return s; string t=""; for(int i=0;i<w-len;i++) t+=" "; return t+s; }
void SetText(string text, color col){ EnsureLabel(); ObjectSetString(0, g_label, OBJPROP_TEXT, text); ObjectSetInteger(0, g_label, OBJPROP_COLOR, col); }
// --------------------- Networking & Update ---------------------
bool FetchOrderbook(string url, string &body, int &status_code){
uchar post[];
ArrayResize(post,0);
string resp_headers="";
uchar result[];
int timeout = g_http_timeout;
ResetLastError();
int code = WebRequest("GET", url, "", timeout, post, result, resp_headers);
status_code = code;
if(code == -1){ g_last_error = "WebRequest failed: "+(string)GetLastError(); return false; }
string payload = CharArrayToString(result, 0, -1);
if(code==200){ body = payload; return true; }
if(code==204){ body=""; return true; }
body = payload;
g_last_error = (StringLen(payload)>0 ? payload : resp_headers);
return false;
}
void RenderState(string state, string details){
string text = "SWFX DOM (HTTP)" + "\n" + state + (StringLen(details)>0 ? " - "+details : "");
color c = (state=="Live")? InpLiveColor : ((state=="Stale")? InpStaleColor : InpLostColor);
SetText(text, c);
}
void RenderDOM(string json, bool stale){
double ts_val=0, levels_val=0; bool ok_ts = JsonGetTopNumber(json, "ts", ts_val);
bool ok_lv = JsonGetTopNumber(json, "levels", levels_val);
double spread_val=0; JsonGetTopNumber(json, "spread", spread_val);
double bids_p[], bids_q[], asks_p[], asks_q[];
int nb = JsonParseSide(json, "bids", bids_p, bids_q);
int na = JsonParseSide(json, "asks", asks_p, asks_q);
int rows = MathMin(InpLevels, MathMax(nb, na));
string hdr = " BidSz BidPx | AskPx AskSz";
string text = hdr + "\n";
int d = (int)MarketInfo(Symbol(), MODE_DIGITS);
for(int i=0;i<rows;i++){
string bs="", bp="", ap="", aq="";
if(i<nb){ bs = PadLeft(DoubleToString(bids_q[i],0),7); bp = PadLeft(DoubleToString(bids_p[i], d), 7+d); } else { bs=PadLeft("",7); bp=PadLeft("",7+d);}
if(i<na){ ap = PadLeft(DoubleToString(asks_p[i], d), 7+d); aq = PadLeft(DoubleToString(asks_q[i],0),7); } else { ap=PadLeft("",7+d); aq=PadLeft("",7);}
text += bs+" "+bp+" | "+ap+" "+aq+"\n";
}
string ts_s = ok_ts? DoubleToString(ts_val,0) : "";
string lv_s = ok_lv? DoubleToString(levels_val,0) : "";
text += "\n ts="+ts_s+" levels="+lv_s+" unit="+InpQtyUnitLabel;
SetText(text, stale ? InpStaleColor : InpLiveColor);
}
void DoUpdate(){
string body; int code=0;
if(!FetchOrderbook(InpServerURL, body, code)){
if(code==204){ RenderState("Stale", "204 No Content"); return; }
string err = "HTTP "+(string)code+" "+g_last_error;
RenderState("Lost", err);
return;
}
if(code==204){ RenderState("Stale", "204 No Content"); return; }
if(StringLen(body)==0){ RenderState("Stale", "empty body"); return; }
double ts_val=0; bool ok_ts = JsonGetTopNumber(body, "ts", ts_val);
bool isStale = false;
if(ok_ts){
// Check staleness
datetime nowt = TimeCurrent(); // seconds resolution
// Convert to ms; TimeCurrent returns server time, but acceptable for TTL check visually
// If too strict, rely on server status; here we check against InpStaleTtlMs
double nowms = (double)nowt * 1000.0;
if(nowms - ts_val > InpStaleTtlMs) isStale = true;
}
RenderDOM(body, isStale);
}
// --------------------- MQL4 Events ---------------------
int OnInit(){
IndicatorShortName("SWFX DOM (HTTP)");
EnsureLabel();
// Timer is second-based; combine with OnCalculate throttling to approximate 500ms when ticks arrive
EventSetTimer(1);
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason){
EventKillTimer();
if(ObjectFind(0, g_label) >= 0) ObjectDelete(0, g_label);
}
int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]){
// Throttle to InpRefreshMs
ulong nowms = GetTickCount();
if(nowms - g_last_fetch_ms >= (ulong)InpRefreshMs){
DoUpdate();
g_last_fetch_ms = nowms;
}
return(rates_total);
}
void OnTimer(){
// Ensure we still refresh at least once per second when ticks are sparse
ulong nowms = GetTickCount();
if(nowms - g_last_fetch_ms >= (ulong)InpRefreshMs){
DoUpdate();
g_last_fetch_ms = nowms;
}
}
