// PlatformMt5CopyBridgeEA.mq5 // Purpose: make a broker MT5 account the sole execution and risk controller for a managed platform account. // Boundary: platform positions are ledger mirrors of actual broker MT5 deals and never execute locally. #property copyright "XT Platform" #property version "1.07" #property description "XT 平台 MT5 交易同步脚本" #property strict #include input group "接口连接" input string ApiBaseUrl = "https://xtfinance.net"; // 接口地址 input string ApiPublicKey = ""; // 公开密钥 input string ApiSecret = ""; // 私有密钥 input group "同步设置" input string GroupName = "group-1"; // 同步分组 input string SymbolMap = "GOLD:instrument-xauusd,XAUUSD:instrument-xauusd,XAUUSDm:instrument-xauusd,EURUSD:instrument-eurusd,EURUSDm:instrument-eurusd,GBPUSD:instrument-gbpusd,GBPUSDm:instrument-gbpusd,USDJPY:instrument-usdjpy,USDJPYm:instrument-usdjpy"; // 品种映射 input double CopyMultiplier = 1.00; // 复制倍率 input string CopyDirection = "同向"; // 复制方向(同向或反向) input int MaxOpenCopiedPositions = 50; // 最大复制持仓数 input double MaxTotalCopiedVolume = 10.00; // 最大复制总手数 input int SnapshotIntervalMs = 1000; // 同步间隔(毫秒) input string OpenSyncMode = "开启"; // 同步开仓(开启或关闭) input group "界面与诊断" input string ChartPanelMode = "开启"; // 显示图表面板(开启或关闭) input string SigningDebugMode = "关闭"; // 输出签名调试信息(开启或关闭) string Mt5Symbols[]; string PlatformInstrumentIds[]; bool PausedNewEntries = false; bool BridgeDisabled = false; bool ServerBridgeDisabled = false; datetime StartedAt = 0; datetime LastClosedDealScanTime = 0; datetime PendingClosedDealScanTime = 0; string LastStatus = "正在启动"; string RuntimeApiBaseUrl = ""; string RuntimeApiPublicKey = ""; string RuntimeApiSecret = ""; string RuntimeGroupName = ""; string RuntimeControllerId = ""; double RuntimeCopyMultiplier = 1.00; string RuntimeCopyDirection = "same"; int RuntimeMaxOpenCopiedPositions = 50; double RuntimeMaxTotalCopiedVolume = 10.00; int RuntimeSnapshotIntervalMs = 1000; bool RuntimeAllowOpenSync = true; int CurrentPanelMode = 0; bool PlatformManagedStateReady = false; double PlatformManagedBalance = 0.0; double PlatformManagedStopOutLevel = 50.0; long PlatformManagedStateVersion = 0; ulong PlatformManagedStateReceivedAt = 0; bool UrgentSnapshotPending = false; bool SnapshotRequestInProgress = false; long RiskClosePositionIdentifier = 0; ulong RiskCloseRequestedAt = 0; bool PendingRiskCloseAcknowledgement = false; string ManagedSourceTickets[]; string ManagedSourceSymbols[]; string ManagedTargetSides[]; double ManagedTargetVolumes[]; double ManagedOpenPrices[]; double ManagedInitialMargins[]; CTrade ManagedTrade; const string PanelPrefix = "xm_copy_panel_"; const string ButtonTabInfo = "xm_copy_panel_tab_info"; const string ButtonTabConfig = "xm_copy_panel_tab_config"; const string ButtonSaveConfig = "xm_copy_panel_save_config"; const string ButtonForceComplete = "xm_copy_panel_force_complete"; const string ButtonPause = "xm_copy_panel_pause"; const string ButtonExit = "xm_copy_panel_exit"; const string ButtonEmergencyStop = "xm_copy_panel_emergency_stop"; const string ButtonDirectionSame = "xm_copy_panel_direction_same"; const string ButtonDirectionOpposite = "xm_copy_panel_direction_opposite"; const string JsonCopyDirectionKey = "\"copyDirection\":\""; const string EditApiBaseUrl = "xm_copy_panel_edit_api_base_url"; const string EditApiPublicKey = "xm_copy_panel_edit_api_public_key"; const string EditApiSecret = "xm_copy_panel_edit_api_secret"; const string EditGroupName = "xm_copy_panel_edit_group_name"; const string EditCopyMultiplier = "xm_copy_panel_edit_copy_multiplier"; const string EditCopyDirection = "xm_copy_panel_edit_copy_direction"; const string EditMaxOpenCopiedPositions = "xm_copy_panel_edit_max_open_copied_positions"; const string EditMaxTotalCopiedVolume = "xm_copy_panel_edit_max_total_copied_volume"; const string EditSnapshotIntervalMs = "xm_copy_panel_edit_snapshot_interval_ms"; const string EditAllowOpenSync = "xm_copy_panel_edit_allow_open_sync"; const string ApiSecretMask = "********"; const int PanelModeInfo = 0; const int PanelModeConfig = 1; const int PanelX = 8; const int PanelY = 28; const int PanelWidth = 382; const int PanelHeight = 548; const int PanelPad = 14; string Trim(string value) { StringTrimLeft(value); StringTrimRight(value); return value; } string BoolToPanelText(const bool value) { return value ? "开启" : "关闭"; } bool ParsePanelBool(string value, const bool fallback) { value = Trim(value); StringToLower(value); if(value == "true" || value == "1" || value == "yes" || value == "y" || value == "on" || value == "是" || value == "开启" || value == "允许") { return true; } if(value == "false" || value == "0" || value == "no" || value == "n" || value == "off" || value == "否" || value == "关闭" || value == "停止") { return false; } return fallback; } string NormalizeCopyDirection(string value) { value = Trim(value); StringToLower(value); if(value == "opposite" || value == "reverse" || value == "inverse" || value == "反向") { return "opposite"; } return "same"; } string CopyDirectionPanelText(const string value) { return NormalizeCopyDirection(value) == "opposite" ? "反向" : "同向"; } string CopyDirectionStateKey() { return "XT.CopyDirection." + IntegerToString((int)AccountInfoInteger(ACCOUNT_LOGIN)); } string LoadCopyDirection() { string direction = NormalizeCopyDirection(CopyDirection); string stateKey = CopyDirectionStateKey(); if(GlobalVariableCheck(stateKey)) { direction = GlobalVariableGet(stateKey) >= 0.5 ? "opposite" : "same"; } return direction; } void SaveCopyDirection() { GlobalVariableSet(CopyDirectionStateKey(), RuntimeCopyDirection == "opposite" ? 1.0 : 0.0); } string RiskCloseStateKey() { return "XT.RiskClose." + IntegerToString((int)AccountInfoInteger(ACCOUNT_LOGIN)) + "." + GroupName; } void SaveRiskCloseIdentifier() { if(RiskClosePositionIdentifier > 0) { GlobalVariableSet(RiskCloseStateKey(), (double)RiskClosePositionIdentifier); } } void LoadRiskCloseIdentifier() { string stateKey = RiskCloseStateKey(); if(GlobalVariableCheck(stateKey)) { RiskClosePositionIdentifier = (long)GlobalVariableGet(stateKey); RiskCloseRequestedAt = GetTickCount64(); } } void ClearRiskCloseIdentifier() { RiskClosePositionIdentifier = 0; RiskCloseRequestedAt = 0; PendingRiskCloseAcknowledgement = false; string stateKey = RiskCloseStateKey(); if(GlobalVariableCheck(stateKey)) { GlobalVariableDel(stateKey); } } void AcknowledgeRiskCloseDeal(const string response) { if(!PendingRiskCloseAcknowledgement) { return; } double failed = 0.0; JsonNumberValue(response, "failed", failed); if(failed <= 0.0) { ClearRiskCloseIdentifier(); } } void UpdateServerBridgeStatus(const string response) { UpdateManagedAccountState(response); ApplyRejectedSourcePositions(response); if(StringFind(response, "\"status\":\"disabled\"") >= 0 || StringFind(response, "\"status\":\"emergency_stopped\"") >= 0) { ServerBridgeDisabled = true; LastStatus = "服务器已停用"; return; } if(StringFind(response, "\"status\":\"active\"") >= 0 || StringFind(response, "\"status\":\"paused_new_entries\"") >= 0) { ServerBridgeDisabled = false; } } string ShortText(const string value, const int maxChars) { if(StringLen(value) <= maxChars) { return value; } return StringSubstr(value, 0, MathMax(0, maxChars - 3)) + "..."; } string JsonEscape(string value) { StringReplace(value, "\\", "\\\\"); StringReplace(value, "\"", "\\\""); StringReplace(value, "\r", "\\r"); StringReplace(value, "\n", "\\n"); return value; } int FindMatchingJsonEnd(const string value, const int startIndex, const int openCode, const int closeCode) { int depth = 0; bool inString = false; bool escaped = false; for(int i = startIndex; i < StringLen(value); i++) { int code = StringGetCharacter(value, i); if(inString) { if(escaped) { escaped = false; } else if(code == 92) { escaped = true; } else if(code == 34) { inString = false; } continue; } if(code == 34) { inString = true; continue; } if(code == openCode) { depth++; } else if(code == closeCode) { depth--; if(depth == 0) { return i; } } } return -1; } bool JsonContainerValue( const string json, const string key, const int openCode, const int closeCode, string &output ) { string marker = "\"" + key + "\":"; int markerIndex = StringFind(json, marker); if(markerIndex < 0) { return false; } int startIndex = markerIndex + StringLen(marker); while(startIndex < StringLen(json) && StringGetCharacter(json, startIndex) != openCode) { startIndex++; } if(startIndex >= StringLen(json)) { return false; } int endIndex = FindMatchingJsonEnd(json, startIndex, openCode, closeCode); if(endIndex < startIndex) { return false; } output = StringSubstr(json, startIndex, endIndex - startIndex + 1); return true; } bool JsonStringValue(const string json, const string key, string &output) { string marker = "\"" + key + "\":\""; int startIndex = StringFind(json, marker); if(startIndex < 0) { return false; } startIndex += StringLen(marker); int endIndex = startIndex; bool escaped = false; while(endIndex < StringLen(json)) { int code = StringGetCharacter(json, endIndex); if(!escaped && code == 34) { output = StringSubstr(json, startIndex, endIndex - startIndex); return true; } if(!escaped && code == 92) { escaped = true; } else { escaped = false; } endIndex++; } return false; } bool JsonNumberValue(const string json, const string key, double &output) { string marker = "\"" + key + "\":"; int startIndex = StringFind(json, marker); if(startIndex < 0) { return false; } startIndex += StringLen(marker); while(startIndex < StringLen(json) && StringGetCharacter(json, startIndex) == 32) { startIndex++; } int endIndex = startIndex; while(endIndex < StringLen(json)) { int code = StringGetCharacter(json, endIndex); if(code == 44 || code == 125 || code == 93 || code == 32) { break; } endIndex++; } if(endIndex <= startIndex) { return false; } string numberText = StringSubstr(json, startIndex, endIndex - startIndex); if(numberText == "null") { return false; } output = StringToDouble(numberText); return true; } void ClearManagedAccountState() { ArrayResize(ManagedSourceTickets, 0); ArrayResize(ManagedSourceSymbols, 0); ArrayResize(ManagedTargetSides, 0); ArrayResize(ManagedTargetVolumes, 0); ArrayResize(ManagedOpenPrices, 0); ArrayResize(ManagedInitialMargins, 0); } bool UpdateManagedAccountState(const string response) { string managedAccountJson = ""; if(!JsonContainerValue(response, "managedAccount", 123, 125, managedAccountJson)) { return false; } double balance = 0.0; double stopOutLevel = 50.0; double stateVersion = 0.0; if(!JsonNumberValue(managedAccountJson, "balance", balance)) { return false; } JsonNumberValue(managedAccountJson, "stopOutLevel", stopOutLevel); JsonNumberValue(managedAccountJson, "stateVersion", stateVersion); ClearManagedAccountState(); string positionsJson = ""; if(JsonContainerValue(managedAccountJson, "positions", 91, 93, positionsJson)) { int cursor = 1; while(cursor < StringLen(positionsJson) - 1) { int objectStart = StringFind(positionsJson, "{", cursor); if(objectStart < 0) { break; } int objectEnd = FindMatchingJsonEnd(positionsJson, objectStart, 123, 125); if(objectEnd < objectStart) { break; } string itemJson = StringSubstr(positionsJson, objectStart, objectEnd - objectStart + 1); string sourceTicket = ""; string sourceSymbol = ""; string targetSide = ""; double targetVolume = 0.0; double openPrice = 0.0; double initialMargin = 0.0; if(JsonStringValue(itemJson, "sourceTicket", sourceTicket) && JsonStringValue(itemJson, "sourceSymbol", sourceSymbol) && JsonStringValue(itemJson, "targetSide", targetSide) && JsonNumberValue(itemJson, "targetVolume", targetVolume) && JsonNumberValue(itemJson, "openPrice", openPrice) && JsonNumberValue(itemJson, "initialMargin", initialMargin) && sourceTicket != "" && sourceSymbol != "" && targetVolume > 0.0 && openPrice > 0.0) { int nextIndex = ArraySize(ManagedSourceTickets); ArrayResize(ManagedSourceTickets, nextIndex + 1); ArrayResize(ManagedSourceSymbols, nextIndex + 1); ArrayResize(ManagedTargetSides, nextIndex + 1); ArrayResize(ManagedTargetVolumes, nextIndex + 1); ArrayResize(ManagedOpenPrices, nextIndex + 1); ArrayResize(ManagedInitialMargins, nextIndex + 1); ManagedSourceTickets[nextIndex] = sourceTicket; ManagedSourceSymbols[nextIndex] = sourceSymbol; ManagedTargetSides[nextIndex] = targetSide; ManagedTargetVolumes[nextIndex] = targetVolume; ManagedOpenPrices[nextIndex] = openPrice; ManagedInitialMargins[nextIndex] = MathMax(0.0, initialMargin); } cursor = objectEnd + 1; } } PlatformManagedBalance = balance; PlatformManagedStopOutLevel = MathMax(0.01, stopOutLevel); PlatformManagedStateVersion = (long)stateVersion; PlatformManagedStateReceivedAt = GetTickCount64(); PlatformManagedStateReady = true; return true; } string DoubleOrNull(const double value) { if(value <= 0.0) { return "null"; } return DoubleToString(value, 8); } string BytesToHex(const uchar &data[]) { string output = ""; for(int i = 0; i < ArraySize(data); i++) { output += StringFormat("%02x", data[i]); } return output; } void StringToUtf8Bytes(const string value, uchar &bytes[]) { ArrayResize(bytes, 0); StringToCharArray(value, bytes, 0, -1, CP_UTF8); int size = ArraySize(bytes); if(size > 0 && bytes[size - 1] == 0) { ArrayResize(bytes, size - 1); } } void XorBlock(uchar &block[], const int value) { for(int i = 0; i < ArraySize(block); i++) { block[i] = (uchar)(block[i] ^ value); } } void CopyBytes(const uchar &source[], uchar &target[], const int offset) { for(int i = 0; i < ArraySize(source); i++) { target[offset + i] = source[i]; } } string Sha256HexFromBytes(const uchar &data[]) { uchar key[]; uchar result[]; ArrayResize(key, 0); CryptEncode(CRYPT_HASH_SHA256, data, key, result); return BytesToHex(result); } string Sha256Hex(const string value) { uchar data[]; StringToUtf8Bytes(value, data); return Sha256HexFromBytes(data); } void Sha256Bytes(const uchar &data[], uchar &result[]) { uchar emptyKey[]; ArrayResize(emptyKey, 0); CryptEncode(CRYPT_HASH_SHA256, data, emptyKey, result); } string HMAC_SHA256_BYTES(const uchar &secretBytes[], const uchar &messageBytes[]) { uchar keyBytes[]; ArrayResize(keyBytes, ArraySize(secretBytes)); ArrayCopy(keyBytes, secretBytes); if(ArraySize(keyBytes) > 64) { uchar hashedKey[]; Sha256Bytes(keyBytes, hashedKey); ArrayResize(keyBytes, ArraySize(hashedKey)); ArrayCopy(keyBytes, hashedKey); } int keyBytesLength = ArraySize(keyBytes); ArrayResize(keyBytes, 64); for(int keyIndex = keyBytesLength; keyIndex < 64; keyIndex++) { keyBytes[keyIndex] = 0; } uchar ipad[]; uchar opad[]; ArrayResize(ipad, 64); ArrayResize(opad, 64); ArrayCopy(ipad, keyBytes); ArrayCopy(opad, keyBytes); XorBlock(ipad, 0x36); XorBlock(opad, 0x5c); uchar innerInput[]; ArrayResize(innerInput, 64 + ArraySize(messageBytes)); CopyBytes(ipad, innerInput, 0); CopyBytes(messageBytes, innerInput, 64); uchar innerHash[]; Sha256Bytes(innerInput, innerHash); uchar outerInput[]; ArrayResize(outerInput, 64 + ArraySize(innerHash)); CopyBytes(opad, outerInput, 0); CopyBytes(innerHash, outerInput, 64); uchar finalHash[]; Sha256Bytes(outerInput, finalHash); return BytesToHex(finalHash); } string HMAC_SHA256(const string secret, const string message) { uchar secretBytes[]; uchar messageBytes[]; StringToUtf8Bytes(secret, secretBytes); StringToUtf8Bytes(message, messageBytes); return HMAC_SHA256_BYTES(secretBytes, messageBytes); } string BuildNonce() { return StringFormat("%I64d-%d-%d", GetTickCount64(), MathRand(), AccountInfoInteger(ACCOUNT_LOGIN)); } string BuildControllerId() { string seed = TerminalInfoString(TERMINAL_DATA_PATH) + "|" + IntegerToString((long)AccountInfoInteger(ACCOUNT_LOGIN)); return StringSubstr(Sha256Hex(seed), 0, 32); } string BuildSignatureBytes( const string method, const string path, const uchar &bodyBytes[], const string timestamp, const string nonce, string &canonical, string &bodyHash ) { bodyHash = Sha256HexFromBytes(bodyBytes); canonical = timestamp + "." + nonce + "." + method + "." + path + "." + bodyHash; uchar secretBytes[]; uchar canonicalBytes[]; StringToUtf8Bytes(RuntimeApiSecret, secretBytes); StringToUtf8Bytes(canonical, canonicalBytes); return HMAC_SHA256_BYTES(secretBytes, canonicalBytes); } string BuildSignature(const string method, const string path, const string body, const string timestamp, const string nonce) { uchar bodyBytes[]; string canonical = ""; string bodyHash = ""; StringToUtf8Bytes(body, bodyBytes); return BuildSignatureBytes(method, path, bodyBytes, timestamp, nonce, canonical, bodyHash); } bool SignedRequest(const string method, const string path, const string body, string &responseText) { uchar payload[]; StringToUtf8Bytes(body, payload); string timestamp = IntegerToString((long)(TimeGMT() * 1000)); string nonce = BuildNonce(); string canonical = ""; string bodyHash = ""; string signature = BuildSignatureBytes(method, path, payload, timestamp, nonce, canonical, bodyHash); string headers = "content-type: application/json\r\n" + "x-xm-api-key: " + RuntimeApiPublicKey + "\r\n" + "x-xm-timestamp: " + timestamp + "\r\n" + "x-xm-nonce: " + nonce + "\r\n" + "x-xm-signature: " + signature + "\r\n"; uchar result[]; string resultHeaders = ""; string url = RuntimeApiBaseUrl + path; ResetLastError(); int status = WebRequest(method, url, headers, 10000, payload, result, resultHeaders); int mt5Error = GetLastError(); responseText = CharArrayToString(result, 0, -1, CP_UTF8); if(status < 200 || status >= 300) { if(status == -1) { LastStatus = StringFormat("网络请求被阻止 错误=%d", mt5Error); } else if(status == 401) { LastStatus = "401 请检查公开密钥和私有密钥"; } else if(status == 403) { LastStatus = "403 账户需要完成验证"; } else if(status == 400) { LastStatus = "400 请求内容或品种映射无效"; } else if(status == 409 && StringFind(responseText, "MT5_COPY_CONTROLLER_CONFLICT") >= 0) { LastStatus = "409 另一台电脑正在控制此同步账户"; } else if(status == 409) { LastStatus = "409 MT5 账户或控制器冲突"; } else { LastStatus = StringFormat("请求失败 状态=%d 错误=%d", status, mt5Error); } if(ParsePanelBool(SigningDebugMode, false)) { Print("MT5 同步签名调试:地址=", url, " 路径=", path, " 内容字节数=", ArraySize(payload), " 内容哈希=", bodyHash, " 签名原文=", canonical, " 签名=", signature); } Print("MT5 同步请求失败:地址=", url, " 路径=", path, " 状态=", status, " 错误=", mt5Error, " 响应=", responseText); if(status == -1) { Print("MT5 网络请求未到达服务器。请打开“工具 > 选项 > 智能交易系统”,允许网络请求,并准确添加:", RuntimeApiBaseUrl); } return false; } LastStatus = "在线 " + TimeToString(TimeCurrent(), TIME_SECONDS); return true; } int ParseSymbolMap() { ArrayResize(Mt5Symbols, 0); ArrayResize(PlatformInstrumentIds, 0); string entries[]; int count = StringSplit(SymbolMap, ',', entries); for(int i = 0; i < count; i++) { string entry = Trim(entries[i]); int separator = StringFind(entry, ":"); if(separator <= 0) { Print("品种映射项目无效:", entry); continue; } string mt5Symbol = Trim(StringSubstr(entry, 0, separator)); string instrumentId = Trim(StringSubstr(entry, separator + 1)); if(mt5Symbol == "" || instrumentId == "") { Print("品种映射项目无效:", entry); continue; } int nextIndex = ArraySize(Mt5Symbols); ArrayResize(Mt5Symbols, nextIndex + 1); ArrayResize(PlatformInstrumentIds, nextIndex + 1); Mt5Symbols[nextIndex] = mt5Symbol; PlatformInstrumentIds[nextIndex] = instrumentId; SymbolSelect(mt5Symbol, true); } return ArraySize(Mt5Symbols); } string ResolvePlatformInstrumentId(const string mt5Symbol) { for(int i = 0; i < ArraySize(Mt5Symbols); i++) { if(Mt5Symbols[i] == mt5Symbol) { return PlatformInstrumentIds[i]; } } return ""; } string BuildSymbolsJson() { string output = "["; bool first = true; for(int i = 0; i < ArraySize(Mt5Symbols); i++) { MqlTick tick; if(!SymbolInfoTick(Mt5Symbols[i], tick) || tick.bid <= 0 || tick.ask <= 0) { continue; } double point = SymbolInfoDouble(Mt5Symbols[i], SYMBOL_POINT); double spread = point > 0 ? (tick.ask - tick.bid) / point : 0; double contractSize = SymbolInfoDouble(Mt5Symbols[i], SYMBOL_TRADE_CONTRACT_SIZE); if(contractSize <= 0.0) { contractSize = 1.0; } if(!first) { output += ","; } first = false; output += StringFormat( "{\"mt5Symbol\":\"%s\",\"platformInstrumentId\":\"%s\",\"contractSize\":%.2f,\"bid\":%.10f,\"ask\":%.10f,\"spreadPoints\":%.2f}", JsonEscape(Mt5Symbols[i]), JsonEscape(PlatformInstrumentIds[i]), contractSize, tick.bid, tick.ask, spread ); } output += "]"; return output; } string BuildPositionsJson() { string output = "["; bool first = true; int total = PositionsTotal(); for(int i = 0; i < total; i++) { ulong ticket = PositionGetTicket(i); if(ticket == 0 || !PositionSelectByTicket(ticket)) { continue; } string symbol = PositionGetString(POSITION_SYMBOL); if(ResolvePlatformInstrumentId(symbol) == "") { continue; } long type = PositionGetInteger(POSITION_TYPE); long positionIdentifier = PositionGetInteger(POSITION_IDENTIFIER); if(positionIdentifier <= 0) { positionIdentifier = (long)ticket; } string side = type == POSITION_TYPE_BUY ? "buy" : "sell"; double volume = PositionGetDouble(POSITION_VOLUME); double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); double stopLoss = PositionGetDouble(POSITION_SL); double takeProfit = PositionGetDouble(POSITION_TP); double profit = PositionGetDouble(POSITION_PROFIT); long openedAt = (long)PositionGetInteger(POSITION_TIME) * 1000; long magic = PositionGetInteger(POSITION_MAGIC); string comment = PositionGetString(POSITION_COMMENT); if(!first) { output += ","; } first = false; output += StringFormat( "{\"ticket\":\"%I64d\",\"symbol\":\"%s\",\"side\":\"%s\",\"volume\":%.2f,\"openPrice\":%.10f,\"stopLoss\":%s,\"takeProfit\":%s,\"profit\":%.10f,\"openedAt\":%I64d,\"magic\":%I64d,\"comment\":\"%s\"}", positionIdentifier, JsonEscape(symbol), side, volume, openPrice, DoubleOrNull(stopLoss), DoubleOrNull(takeProfit), profit, openedAt, magic, JsonEscape(comment) ); } output += "]"; return output; } double FindSourceOpenPrice(const long positionId, const double fallbackPrice) { int total = HistoryDealsTotal(); for(int i = 0; i < total; i++) { ulong dealTicket = HistoryDealGetTicket(i); if(dealTicket == 0) { continue; } if((long)HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID) != positionId) { continue; } if((long)HistoryDealGetInteger(dealTicket, DEAL_ENTRY) == DEAL_ENTRY_IN) { double price = HistoryDealGetDouble(dealTicket, DEAL_PRICE); if(price > 0.0) { return price; } } } return fallbackPrice; } long FindSourceOpenedAt(const long positionId, const long fallbackTime) { int total = HistoryDealsTotal(); for(int i = 0; i < total; i++) { ulong dealTicket = HistoryDealGetTicket(i); if(dealTicket == 0) { continue; } if((long)HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID) != positionId) { continue; } if((long)HistoryDealGetInteger(dealTicket, DEAL_ENTRY) == DEAL_ENTRY_IN) { long openedAt = (long)HistoryDealGetInteger(dealTicket, DEAL_TIME) * 1000; if(openedAt > 0) { return openedAt; } } } return fallbackTime; } string DealCloseReasonText(const ulong dealTicket, const long entry, const long positionId) { if(entry == DEAL_ENTRY_OUT_BY) { return "close_by"; } long reason = (long)HistoryDealGetInteger(dealTicket, DEAL_REASON); if(reason == DEAL_REASON_SL) { return "sl"; } if(reason == DEAL_REASON_TP) { return "tp"; } if(reason == DEAL_REASON_SO) { return "stop_out"; } if(reason == DEAL_REASON_EXPERT) { return positionId == RiskClosePositionIdentifier ? "stop_out" : "expert"; } if(reason == DEAL_REASON_CLIENT || reason == DEAL_REASON_MOBILE || reason == DEAL_REASON_WEB) { return "manual"; } return "other"; } string BuildClosedPositionsJson() { string output = "["; PendingRiskCloseAcknowledgement = false; datetime fromTime = LastClosedDealScanTime > 5 ? LastClosedDealScanTime - 5 : StartedAt; datetime toTime = TimeCurrent(); PendingClosedDealScanTime = toTime; if(!HistorySelect(fromTime, toTime)) { return output + "]"; } bool first = true; int total = HistoryDealsTotal(); for(int i = 0; i < total; i++) { ulong dealTicket = HistoryDealGetTicket(i); if(dealTicket == 0) { continue; } long entry = (long)HistoryDealGetInteger(dealTicket, DEAL_ENTRY); if(entry != DEAL_ENTRY_OUT && entry != DEAL_ENTRY_OUT_BY) { continue; } string symbol = HistoryDealGetString(dealTicket, DEAL_SYMBOL); if(ResolvePlatformInstrumentId(symbol) == "") { continue; } long positionId = (long)HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID); if(positionId <= 0) { continue; } double closePrice = HistoryDealGetDouble(dealTicket, DEAL_PRICE); double volume = HistoryDealGetDouble(dealTicket, DEAL_VOLUME); if(closePrice <= 0.0 || volume <= 0.0) { continue; } long dealType = (long)HistoryDealGetInteger(dealTicket, DEAL_TYPE); string sourceSide = dealType == DEAL_TYPE_SELL ? "buy" : "sell"; long closedAt = (long)HistoryDealGetInteger(dealTicket, DEAL_TIME) * 1000; double openPrice = FindSourceOpenPrice(positionId, closePrice); long openedAt = FindSourceOpenedAt(positionId, closedAt); string closeReason = DealCloseReasonText(dealTicket, entry, positionId); if(positionId == RiskClosePositionIdentifier && closeReason == "stop_out") { PendingRiskCloseAcknowledgement = true; } if(!first) { output += ","; } first = false; output += StringFormat( "{\"ticket\":\"%I64d\",\"closeDealTicket\":\"%I64u\",\"symbol\":\"%s\",\"side\":\"%s\",\"volume\":%.2f,\"openPrice\":%.10f,\"closePrice\":%.10f,\"profit\":%.10f,\"openedAt\":%I64d,\"closedAt\":%I64d,\"closeReason\":\"%s\"}", positionId, dealTicket, JsonEscape(symbol), sourceSide, volume, openPrice, closePrice, HistoryDealGetDouble(dealTicket, DEAL_PROFIT), openedAt, closedAt, closeReason ); } output += "]"; return output; } string TradeModeText() { long mode = AccountInfoInteger(ACCOUNT_TRADE_MODE); if(mode == ACCOUNT_TRADE_MODE_DEMO) { return "demo"; } if(mode == ACCOUNT_TRADE_MODE_REAL) { return "real"; } if(mode == ACCOUNT_TRADE_MODE_CONTEST) { return "contest"; } return "unknown"; } string BuildSourceJson() { string broker = AccountInfoString(ACCOUNT_COMPANY); string server = AccountInfoString(ACCOUNT_SERVER); string currency = AccountInfoString(ACCOUNT_CURRENCY); long login = AccountInfoInteger(ACCOUNT_LOGIN); long leverage = AccountInfoInteger(ACCOUNT_LEVERAGE); string output = "{"; output += StringFormat( "\"broker\":\"%s\",\"server\":\"%s\",\"login\":\"%I64d\",\"currency\":\"%s\",\"balance\":%.2f,\"equity\":%.2f,\"margin\":%.2f,\"freeMargin\":%.2f,\"leverage\":%I64d,", JsonEscape(broker), JsonEscape(server), login, JsonEscape(currency), AccountInfoDouble(ACCOUNT_BALANCE), AccountInfoDouble(ACCOUNT_EQUITY), AccountInfoDouble(ACCOUNT_MARGIN), AccountInfoDouble(ACCOUNT_MARGIN_FREE), leverage ); output += "\"tradeAllowed\":" + (AccountInfoInteger(ACCOUNT_TRADE_ALLOWED) != 0 ? "true" : "false") + ","; output += "\"tradeMode\":\"" + TradeModeText() + "\""; output += "}"; return output; } ulong FindPositionTicketByIdentifier(const long positionIdentifier) { for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0 || !PositionSelectByTicket(ticket)) { continue; } if((long)PositionGetInteger(POSITION_IDENTIFIER) == positionIdentifier) { return ticket; } } return 0; } bool ManagedStateIsFresh() { if(!PlatformManagedStateReady || PlatformManagedStateReceivedAt == 0) { return false; } ulong maxAge = (ulong)MathMax(3000, RuntimeSnapshotIntervalMs * 4); return GetTickCount64() - PlatformManagedStateReceivedAt <= maxAge; } double ManagedPositionPnl(const int index, bool &available) { available = false; if(index < 0 || index >= ArraySize(ManagedSourceTickets)) { return 0.0; } long identifier = (long)StringToInteger(ManagedSourceTickets[index]); if(identifier <= 0 || FindPositionTicketByIdentifier(identifier) == 0) { return 0.0; } MqlTick tick; if(!SymbolInfoTick(ManagedSourceSymbols[index], tick) || tick.bid <= 0.0 || tick.ask <= 0.0) { return 0.0; } available = true; if(ManagedTargetSides[index] == "buy") { return (tick.bid - ManagedOpenPrices[index]) * ManagedTargetVolumes[index]; } return (ManagedOpenPrices[index] - tick.ask) * ManagedTargetVolumes[index]; } bool CloseManagedPositionIdentifier(const long positionIdentifier, const string reasonText) { ulong brokerTicket = FindPositionTicketByIdentifier(positionIdentifier); if(brokerTicket == 0 || !PositionSelectByTicket(brokerTicket)) { return false; } string symbol = PositionGetString(POSITION_SYMBOL); ManagedTrade.SetTypeFillingBySymbol(symbol); ResetLastError(); bool closed = ManagedTrade.PositionClose(brokerTicket); if(!closed) { Print( "MT5 托管平仓失败:标识=", positionIdentifier, " 票据=", brokerTicket, " 原因=", reasonText, " 返回码=", ManagedTrade.ResultRetcode(), " 说明=", ManagedTrade.ResultRetcodeDescription(), " 错误=", GetLastError() ); return false; } Print( "MT5 托管平仓已提交:标识=", positionIdentifier, " 票据=", brokerTicket, " 原因=", reasonText, " 返回码=", ManagedTrade.ResultRetcode(), " 说明=", ManagedTrade.ResultRetcodeDescription() ); return true; } void ScheduleUrgentSnapshot() { if(BridgeDisabled) { return; } UrgentSnapshotPending = true; EventSetMillisecondTimer(100); } void ApplyRejectedSourcePositions(const string response) { string rejectedJson = ""; if(!JsonContainerValue(response, "rejectedPositions", 91, 93, rejectedJson)) { return; } int cursor = 1; while(cursor < StringLen(rejectedJson) - 1) { int objectStart = StringFind(rejectedJson, "{", cursor); if(objectStart < 0) { break; } int objectEnd = FindMatchingJsonEnd(rejectedJson, objectStart, 123, 125); if(objectEnd < objectStart) { break; } string itemJson = StringSubstr(rejectedJson, objectStart, objectEnd - objectStart + 1); string sourceTicket = ""; string reason = "platform_rejected"; JsonStringValue(itemJson, "reason", reason); if(JsonStringValue(itemJson, "sourceTicket", sourceTicket)) { long identifier = (long)StringToInteger(sourceTicket); if(identifier > 0 && FindPositionTicketByIdentifier(identifier) > 0) { LastStatus = "平台拒绝新仓,正在回撤 " + sourceTicket; if(CloseManagedPositionIdentifier(identifier, reason)) { ScheduleUrgentSnapshot(); } } } cursor = objectEnd + 1; } } void EvaluatePlatformManagedStopOut() { if(BridgeDisabled || ServerBridgeDisabled || !ManagedStateIsFresh()) { return; } if(RiskClosePositionIdentifier > 0) { if(FindPositionTicketByIdentifier(RiskClosePositionIdentifier) == 0) { return; } else if(GetTickCount64() - RiskCloseRequestedAt < 5000) { return; } else { ClearRiskCloseIdentifier(); } } double totalPnl = 0.0; double usedMargin = 0.0; double worstPnl = DBL_MAX; int worstIndex = -1; int availableCount = 0; for(int i = 0; i < ArraySize(ManagedSourceTickets); i++) { bool available = false; double pnl = ManagedPositionPnl(i, available); if(!available) { continue; } availableCount++; totalPnl += pnl; usedMargin += MathMax(0.0, ManagedInitialMargins[i]); if(pnl < worstPnl) { worstPnl = pnl; worstIndex = i; } } if(availableCount != ArraySize(ManagedSourceTickets) || usedMargin <= 0.0 || worstIndex < 0) { return; } double equity = PlatformManagedBalance + totalPnl; double marginLevel = equity / usedMargin * 100.0; if(marginLevel > PlatformManagedStopOutLevel) { return; } long identifier = (long)StringToInteger(ManagedSourceTickets[worstIndex]); RiskClosePositionIdentifier = identifier; RiskCloseRequestedAt = GetTickCount64(); SaveRiskCloseIdentifier(); LastStatus = StringFormat("平台保证金 %.2f%%,正在强平", marginLevel); if(!CloseManagedPositionIdentifier(identifier, "platform_stop_out")) { ClearRiskCloseIdentifier(); } DrawPanel(); } int CountMappedSourcePositions() { int count = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0 || !PositionSelectByTicket(ticket)) { continue; } if(ResolvePlatformInstrumentId(PositionGetString(POSITION_SYMBOL)) != "") { count++; } } return count; } int CloseAllManagedSourcePositions(const string reasonText) { long identifiers[]; ArrayResize(identifiers, 0); for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0 || !PositionSelectByTicket(ticket)) { continue; } if(ResolvePlatformInstrumentId(PositionGetString(POSITION_SYMBOL)) == "") { continue; } long identifier = (long)PositionGetInteger(POSITION_IDENTIFIER); if(identifier <= 0) { identifier = (long)ticket; } int nextIndex = ArraySize(identifiers); ArrayResize(identifiers, nextIndex + 1); identifiers[nextIndex] = identifier; } int submitted = 0; for(int i = ArraySize(identifiers) - 1; i >= 0; i--) { long identifier = identifiers[i]; if(identifier > 0 && CloseManagedPositionIdentifier(identifier, reasonText)) { submitted++; } } return submitted; } string BuildHandshakeJson() { long sentAt = (long)TimeGMT() * 1000; string paused = PausedNewEntries || !RuntimeAllowOpenSync ? "true" : "false"; string output = "{"; output += "\"source\":" + BuildSourceJson() + ","; output += StringFormat( "\"group\":{\"name\":\"%s\",\"controllerId\":\"%s\",\"multiplier\":%.2f,\"pausedNewEntries\":%s,\"copyDirection\":\"%s\",\"maxOpenCopiedPositions\":%d,\"maxTotalCopiedVolume\":%.2f},", JsonEscape(RuntimeGroupName), JsonEscape(RuntimeControllerId), RuntimeCopyMultiplier, paused, JsonEscape(RuntimeCopyDirection), RuntimeMaxOpenCopiedPositions, RuntimeMaxTotalCopiedVolume ); output += StringFormat("\"sentAt\":%I64d", sentAt); output += "}"; return output; } bool SendHandshake() { string response = ""; bool ok = SignedRequest("POST", "/api/mt5/v1/copy/handshake", BuildHandshakeJson(), response); if(ok) { UpdateServerBridgeStatus(response); if(!ServerBridgeDisabled) { LastStatus = "连接成功 " + TimeToString(TimeCurrent(), TIME_SECONDS); } } Print("MT5 同步连接响应:", response); DrawPanel(); return ok; } string BuildSnapshotJson() { long sentAt = (long)TimeGMT() * 1000; string paused = PausedNewEntries || !RuntimeAllowOpenSync ? "true" : "false"; string output = "{"; output += "\"source\":" + BuildSourceJson() + ","; output += StringFormat( "\"group\":{\"name\":\"%s\",\"controllerId\":\"%s\",\"multiplier\":%.2f,\"pausedNewEntries\":%s,\"copyDirection\":\"%s\",\"maxOpenCopiedPositions\":%d,\"maxTotalCopiedVolume\":%.2f},", JsonEscape(RuntimeGroupName), JsonEscape(RuntimeControllerId), RuntimeCopyMultiplier, paused, JsonEscape(RuntimeCopyDirection), RuntimeMaxOpenCopiedPositions, RuntimeMaxTotalCopiedVolume ); output += "\"symbols\":" + BuildSymbolsJson() + ","; output += "\"positions\":" + BuildPositionsJson() + ","; output += "\"closedPositions\":" + BuildClosedPositionsJson() + ","; output += StringFormat("\"sentAt\":%I64d", sentAt); output += "}"; return output; } bool PostSnapshot() { if(BridgeDisabled) { LastStatus = "已停用"; DrawPanel(); return false; } if(SnapshotRequestInProgress) { return false; } SnapshotRequestInProgress = true; string response = ""; bool ok = SignedRequest("POST", "/api/mt5/v1/copy/snapshot", BuildSnapshotJson(), response); if(ok) { UpdateServerBridgeStatus(response); } if(ok && PendingClosedDealScanTime > LastClosedDealScanTime) { LastClosedDealScanTime = PendingClosedDealScanTime; } if(ok) { AcknowledgeRiskCloseDeal(response); } SnapshotRequestInProgress = false; if(ok && ServerBridgeDisabled && CountMappedSourcePositions() == 0) { BridgeDisabled = true; LastStatus = "同步已停止,所有源仓位均已平仓"; } Print("MT5 同步快照响应:", response); DrawPanel(); return ok; } bool SendAction(const string action) { long sentAt = (long)TimeGMT() * 1000; string body = StringFormat( "{\"action\":\"%s\",\"sourceLogin\":\"%I64d\",\"groupName\":\"%s\",\"controllerId\":\"%s\",\"sentAt\":%I64d}", action, AccountInfoInteger(ACCOUNT_LOGIN), JsonEscape(RuntimeGroupName), JsonEscape(RuntimeControllerId), sentAt ); string response = ""; bool ok = SignedRequest("POST", "/api/mt5/v1/copy/actions", body, response); if(ok) { UpdateServerBridgeStatus(response); } Print("MT5 同步操作响应:", response); DrawPanel(); return ok; } void DrawPanelBox(const string suffix, const int x, const int y, const int width, const int height, const color background, const color border) { string name = PanelPrefix + suffix; 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, x); ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y); ObjectSetInteger(0, name, OBJPROP_XSIZE, width); ObjectSetInteger(0, name, OBJPROP_YSIZE, height); ObjectSetInteger(0, name, OBJPROP_BGCOLOR, background); ObjectSetInteger(0, name, OBJPROP_COLOR, border); ObjectSetInteger(0, name, OBJPROP_BACK, false); ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); } void DrawPanelLabel(const string suffix, const string text, const int x, const int y, const int fontSize, const color textColor) { string name = PanelPrefix + suffix; 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, x); ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y); ObjectSetInteger(0, name, OBJPROP_COLOR, textColor); ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize); ObjectSetString(0, name, OBJPROP_FONT, "Microsoft YaHei"); ObjectSetString(0, name, OBJPROP_TEXT, text); ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); } void DrawPanelEdit(const string name, const string text, const int x, const int y, const int width, const int height) { bool created = false; if(ObjectFind(0, name) < 0) { ObjectCreate(0, name, OBJ_EDIT, 0, 0, 0); created = true; } ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x); ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y); ObjectSetInteger(0, name, OBJPROP_XSIZE, width); ObjectSetInteger(0, name, OBJPROP_YSIZE, height); ObjectSetInteger(0, name, OBJPROP_BGCOLOR, C'10,14,20'); ObjectSetInteger(0, name, OBJPROP_COLOR, C'232,238,244'); ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 8); ObjectSetString(0, name, OBJPROP_FONT, "Microsoft YaHei"); if(created) { ObjectSetString(0, name, OBJPROP_TEXT, text); } ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); } void DeletePanelObjects() { for(int i = ObjectsTotal(0) - 1; i >= 0; i--) { string name = ObjectName(0, i); if(StringFind(name, PanelPrefix) == 0) { ObjectDelete(0, name); } } } void DrawButton(const string name, const string label, const int x, const int y, const int width, const int height, const color background) { 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, x); ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y); ObjectSetInteger(0, name, OBJPROP_XSIZE, width); ObjectSetInteger(0, name, OBJPROP_YSIZE, height); ObjectSetInteger(0, name, OBJPROP_BGCOLOR, background); ObjectSetInteger(0, name, OBJPROP_COLOR, clrWhite); ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 8); ObjectSetString(0, name, OBJPROP_FONT, "Microsoft YaHei"); ObjectSetString(0, name, OBJPROP_TEXT, label); ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); } void DrawPanelTabs() { color infoColor = CurrentPanelMode == PanelModeInfo ? C'46,118,190' : C'42,50,61'; color configColor = CurrentPanelMode == PanelModeConfig ? C'46,118,190' : C'42,50,61'; DrawButton(ButtonTabInfo, "运行状态", PanelX + PanelPad, PanelY + 46, 96, 24, infoColor); DrawButton(ButtonTabConfig, "参数配置", PanelX + PanelPad + 104, PanelY + 46, 96, 24, configColor); } void DrawActionButtons() { int buttonY = PanelY + PanelHeight - 52; string pauseLabel = ServerBridgeDisabled || BridgeDisabled ? "恢复同步" : PausedNewEntries ? "恢复新仓" : "停止新仓"; color pauseColor = ServerBridgeDisabled || BridgeDisabled ? C'46,118,190' : clrSeaGreen; DrawButton(ButtonForceComplete, "强制补全", PanelX + PanelPad, buttonY, 78, 28, clrDarkViolet); DrawButton(ButtonPause, pauseLabel, PanelX + PanelPad + 86, buttonY, 78, 28, pauseColor); DrawButton(ButtonExit, "退出系统", PanelX + PanelPad + 172, buttonY, 78, 28, clrFireBrick); DrawButton(ButtonEmergencyStop, "紧急停止", PanelX + PanelPad + 258, buttonY, 78, 28, clrCrimson); } void DrawPanelShell() { DrawPanelBox("shadow", PanelX + 4, PanelY + 5, PanelWidth, PanelHeight, C'8,10,14', C'8,10,14'); DrawPanelBox("background", PanelX, PanelY, PanelWidth, PanelHeight, C'18,22,28', C'58,68,82'); DrawPanelBox("header", PanelX + 1, PanelY + 1, PanelWidth - 2, 82, C'30,38,48', C'30,38,48'); DrawPanelLabel("title", "交易同步", PanelX + PanelPad, PanelY + 10, 12, clrWhite); DrawPanelLabel("subtitle", "同步开仓与平仓", PanelX + PanelPad, PanelY + 29, 8, C'185,195,205'); DrawPanelTabs(); } void DrawInfoPanel() { int longCount = 0; double longLots = 0.0; double longProfit = 0.0; int total = PositionsTotal(); for(int i = 0; i < total; i++) { ulong ticket = PositionGetTicket(i); if(ticket == 0 || !PositionSelectByTicket(ticket)) { continue; } if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { longCount++; longLots += PositionGetDouble(POSITION_VOLUME); longProfit += PositionGetDouble(POSITION_PROFIT); } } int rowX = PanelX + PanelPad + 12; int valueX = PanelX + PanelPad + 92; int y = PanelY + 104; DrawPanelBox("section_account", PanelX + PanelPad, PanelY + 92, PanelWidth - PanelPad * 2, 118, C'24,29,37', C'54,64,76'); DrawPanelBox("section_sync", PanelX + PanelPad, PanelY + 222, PanelWidth - PanelPad * 2, 136, C'24,29,37', C'54,64,76'); DrawPanelBox("section_stats", PanelX + PanelPad, PanelY + 370, PanelWidth - PanelPad * 2, 56, C'24,29,37', C'54,64,76'); DrawPanelLabel("account_header", "本账号", rowX, y, 9, C'230,236,242'); y += 22; DrawPanelLabel("account_login_label", "账号:", rowX, y, 8, C'148,160,176'); DrawPanelLabel("account_login_value", IntegerToString((int)AccountInfoInteger(ACCOUNT_LOGIN)), valueX, y, 8, C'210,220,230'); y += 20; DrawPanelLabel("account_balance_label", "余额:", rowX, y, 8, C'148,160,176'); DrawPanelLabel("account_balance_value", DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2) + " " + AccountInfoString(ACCOUNT_CURRENCY), valueX, y, 8, C'210,220,230'); y += 20; DrawPanelLabel("account_positions_label", "单数:", rowX, y, 8, C'148,160,176'); DrawPanelLabel("account_positions_value", IntegerToString(total), valueX, y, 8, C'210,220,230'); y += 20; DrawPanelLabel("account_group_label", "分组:", rowX, y, 8, C'148,160,176'); DrawPanelLabel("account_group_value", ShortText(RuntimeGroupName, 28) + " / " + DoubleToString(RuntimeCopyMultiplier, 2), valueX, y, 8, C'210,220,230'); y = PanelY + 234; DrawPanelLabel("sync_header", "同步状态", rowX, y, 9, C'230,236,242'); y += 22; DrawPanelLabel("sync_started_label", "启动:", rowX, y, 8, C'148,160,176'); DrawPanelLabel("sync_started_value", TimeToString(StartedAt, TIME_DATE | TIME_MINUTES), valueX, y, 8, C'210,220,230'); y += 20; DrawPanelLabel("sync_status_label", "连接:", rowX, y, 8, C'148,160,176'); DrawPanelLabel("sync_status_value", ShortText(LastStatus, 34), valueX, y, 8, BridgeDisabled || ServerBridgeDisabled ? clrTomato : C'105,220,155'); y += 20; DrawPanelLabel("sync_open_label", "新仓:", rowX, y, 8, C'148,160,176'); DrawPanelLabel("sync_open_value", RuntimeAllowOpenSync && !PausedNewEntries ? "允许" : "停止", valueX, y, 8, RuntimeAllowOpenSync && !PausedNewEntries ? C'105,220,155' : clrGold); DrawPanelLabel("sync_close_label", "平仓:", PanelX + PanelPad + 208, y, 8, C'148,160,176'); DrawPanelLabel("sync_close_value", "始终开启", PanelX + PanelPad + 256, y, 8, C'105,220,155'); y += 20; DrawPanelLabel("sync_direction_label", "方向:", rowX, y, 8, C'148,160,176'); DrawPanelLabel("sync_direction_value", CopyDirectionPanelText(RuntimeCopyDirection), valueX, y, 8, RuntimeCopyDirection == "opposite" ? clrGold : C'105,220,155'); DrawPanelLabel("sync_limit_label", "限制:", PanelX + PanelPad + 208, y, 8, C'148,160,176'); DrawPanelLabel("sync_limit_value", IntegerToString(RuntimeMaxOpenCopiedPositions) + " / " + DoubleToString(RuntimeMaxTotalCopiedVolume, 2), PanelX + PanelPad + 256, y, 8, C'210,220,230'); y = PanelY + 384; DrawPanelLabel("stats_long_count", "多单: " + IntegerToString(longCount), rowX, y, 8, C'210,220,230'); DrawPanelLabel("stats_long_profit", "获利: " + DoubleToString(longProfit, 2), PanelX + PanelPad + 122, y, 8, longProfit >= 0 ? C'105,220,155' : clrTomato); DrawPanelLabel("stats_long_lots", "手数: " + DoubleToString(longLots, 2), PanelX + PanelPad + 244, y, 8, C'210,220,230'); DrawActionButtons(); } void DrawConfigField(const string suffix, const string label, const string objectName, const string value, const int y) { DrawPanelLabel("config_label_" + suffix, label, PanelX + PanelPad + 12, y + 4, 8, C'148,160,176'); DrawPanelEdit(objectName, value, PanelX + PanelPad + 118, y, PanelWidth - PanelPad * 2 - 132, 22); } void DrawDirectionSelector(const int y) { int buttonX = PanelX + PanelPad + 118; color sameColor = RuntimeCopyDirection == "same" ? C'46,118,190' : C'42,50,61'; color oppositeColor = RuntimeCopyDirection == "opposite" ? C'190,126,46' : C'42,50,61'; ObjectDelete(0, EditCopyDirection); DrawButton(ButtonDirectionSame, "同向", buttonX, y, 108, 22, sameColor); DrawButton(ButtonDirectionOpposite, "反向", buttonX + 114, y, 108, 22, oppositeColor); } void DrawConfigPanel() { DrawPanelBox("section_config", PanelX + PanelPad, PanelY + 92, PanelWidth - PanelPad * 2, 392, C'24,29,37', C'54,64,76'); DrawPanelLabel("config_title", "参数配置", PanelX + PanelPad + 12, PanelY + 104, 9, C'230,236,242'); DrawPanelLabel("config_hint", "保存后立即生效;私有密钥留空或保留星号即不修改。", PanelX + PanelPad + 12, PanelY + 124, 7, C'148,160,176'); DrawConfigField("api_base_url", "接口地址", EditApiBaseUrl, RuntimeApiBaseUrl, PanelY + 150); DrawConfigField("api_public_key", "公开密钥", EditApiPublicKey, RuntimeApiPublicKey, PanelY + 180); DrawConfigField("api_secret", "私有密钥", EditApiSecret, ApiSecretMask, PanelY + 210); DrawConfigField("group_name", "分组", EditGroupName, RuntimeGroupName, PanelY + 240); DrawConfigField("copy_multiplier", "倍率", EditCopyMultiplier, DoubleToString(RuntimeCopyMultiplier, 2), PanelY + 270); DrawConfigField("copy_direction", "方向", EditCopyDirection, CopyDirectionPanelText(RuntimeCopyDirection), PanelY + 300); DrawConfigField("max_positions", "最大笔数", EditMaxOpenCopiedPositions, IntegerToString(RuntimeMaxOpenCopiedPositions), PanelY + 330); DrawConfigField("max_total_volume", "总手数", EditMaxTotalCopiedVolume, DoubleToString(RuntimeMaxTotalCopiedVolume, 2), PanelY + 360); DrawConfigField("snapshot_interval", "间隔毫秒", EditSnapshotIntervalMs, IntegerToString(RuntimeSnapshotIntervalMs), PanelY + 390); DrawConfigField("allow_open", "开仓同步", EditAllowOpenSync, BoolToPanelText(RuntimeAllowOpenSync), PanelY + 420); DrawPanelLabel("config_label_allow_close", "平仓同步", PanelX + PanelPad + 12, PanelY + 454, 8, C'148,160,176'); DrawPanelLabel("config_value_allow_close", "始终开启(托管必需)", PanelX + PanelPad + 118, PanelY + 454, 8, C'105,220,155'); DrawButton(ButtonSaveConfig, "保存参数", PanelX + PanelPad + 232, PanelY + PanelHeight - 52, 108, 28, C'46,118,190'); } string GetPanelEditText(const string name) { if(ObjectFind(0, name) < 0) { return ""; } return ObjectGetString(0, name, OBJPROP_TEXT); } void ApplyPanelConfig() { string nextApiBaseUrl = Trim(GetPanelEditText(EditApiBaseUrl)); string nextApiPublicKey = Trim(GetPanelEditText(EditApiPublicKey)); string nextApiSecret = Trim(GetPanelEditText(EditApiSecret)); string nextGroupName = Trim(GetPanelEditText(EditGroupName)); double nextMultiplier = StringToDouble(Trim(GetPanelEditText(EditCopyMultiplier))); int nextMaxPositions = (int)StringToInteger(Trim(GetPanelEditText(EditMaxOpenCopiedPositions))); double nextMaxTotalVolume = StringToDouble(Trim(GetPanelEditText(EditMaxTotalCopiedVolume))); int nextInterval = (int)StringToInteger(Trim(GetPanelEditText(EditSnapshotIntervalMs))); if(nextApiBaseUrl != "") { RuntimeApiBaseUrl = nextApiBaseUrl; } if(nextApiPublicKey != "") { RuntimeApiPublicKey = nextApiPublicKey; } if(nextApiSecret != "" && nextApiSecret != ApiSecretMask) { RuntimeApiSecret = nextApiSecret; } if(nextGroupName != "") { RuntimeGroupName = nextGroupName; } if(nextMultiplier > 0.0) { RuntimeCopyMultiplier = nextMultiplier; } if(nextMaxPositions > 0) { RuntimeMaxOpenCopiedPositions = nextMaxPositions; } if(nextMaxTotalVolume > 0.0) { RuntimeMaxTotalCopiedVolume = nextMaxTotalVolume; } if(nextInterval >= 500) { RuntimeSnapshotIntervalMs = nextInterval; EventKillTimer(); EventSetMillisecondTimer(RuntimeSnapshotIntervalMs); } RuntimeAllowOpenSync = ParsePanelBool(GetPanelEditText(EditAllowOpenSync), RuntimeAllowOpenSync); LastStatus = "配置已保存 " + TimeToString(TimeCurrent(), TIME_SECONDS); } void DrawPanel() { if(!ParsePanelBool(ChartPanelMode, true)) { Comment(""); DeletePanelObjects(); return; } Comment(""); DrawPanelShell(); if(CurrentPanelMode == PanelModeConfig) { DrawConfigPanel(); // Keep the legacy input parameter while using an explicit runtime selector. DrawDirectionSelector(PanelY + 300); } else { DrawInfoPanel(); } ChartRedraw(); } int OnInit() { MathSrand((uint)GetTickCount()); StartedAt = TimeCurrent(); LastClosedDealScanTime = StartedAt; PendingClosedDealScanTime = StartedAt; RuntimeApiBaseUrl = ApiBaseUrl; RuntimeApiPublicKey = ApiPublicKey; RuntimeApiSecret = ApiSecret; RuntimeGroupName = GroupName; RuntimeControllerId = BuildControllerId(); RuntimeCopyMultiplier = CopyMultiplier; RuntimeCopyDirection = LoadCopyDirection(); LoadRiskCloseIdentifier(); RuntimeMaxOpenCopiedPositions = MathMax(1, MaxOpenCopiedPositions); RuntimeMaxTotalCopiedVolume = MathMax(0.01, MaxTotalCopiedVolume); RuntimeSnapshotIntervalMs = MathMax(500, SnapshotIntervalMs); RuntimeAllowOpenSync = ParsePanelBool(OpenSyncMode, true); ManagedTrade.SetAsyncMode(false); ManagedTrade.SetExpertMagicNumber(1481911605); ManagedTrade.SetDeviationInPoints(20); ManagedTrade.SetMarginMode(); if(RuntimeApiPublicKey == "" || RuntimeApiSecret == "" || RuntimeApiPublicKey == "xm_mt5_replace_me") { Print("运行 PlatformMt5CopyBridgeEA 前,请先设置接口地址、公开密钥和私有密钥。"); return INIT_FAILED; } if(ParseSymbolMap() <= 0) { Print("没有配置有效的品种映射。"); return INIT_FAILED; } if(!SendHandshake()) { Print("PlatformMt5CopyBridgeEA 连接失败,请检查接口设置和网络请求白名单。"); } EventSetMillisecondTimer(RuntimeSnapshotIntervalMs); DrawPanel(); PostSnapshot(); return INIT_SUCCEEDED; } void OnDeinit(const int reason) { EventKillTimer(); DeletePanelObjects(); Comment(""); } void OnTimer() { bool wasUrgent = UrgentSnapshotPending; UrgentSnapshotPending = false; PostSnapshot(); if(wasUrgent && !UrgentSnapshotPending && !BridgeDisabled) { EventSetMillisecondTimer(RuntimeSnapshotIntervalMs); } } void OnTick() { EvaluatePlatformManagedStopOut(); } void OnTradeTransaction( const MqlTradeTransaction &transaction, const MqlTradeRequest &request, const MqlTradeResult &result ) { if(transaction.type != TRADE_TRANSACTION_DEAL_ADD || transaction.deal == 0) { return; } if(HistoryDealSelect(transaction.deal)) { string symbol = HistoryDealGetString(transaction.deal, DEAL_SYMBOL); if(ResolvePlatformInstrumentId(symbol) == "") { return; } } ScheduleUrgentSnapshot(); } void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id != CHARTEVENT_OBJECT_CLICK) { return; } if(sparam == ButtonForceComplete) { SendAction("force_complete"); PostSnapshot(); } else if(sparam == ButtonPause) { if(ServerBridgeDisabled || BridgeDisabled) { if(SendAction("resume_new_entries")) { ServerBridgeDisabled = false; BridgeDisabled = false; PausedNewEntries = false; LastStatus = "同步已恢复 " + TimeToString(TimeCurrent(), TIME_SECONDS); SendHandshake(); PostSnapshot(); } } else { PausedNewEntries = !PausedNewEntries; SendAction(PausedNewEntries ? "pause_new_entries" : "resume_new_entries"); } DrawPanel(); } else if(sparam == ButtonExit) { if(CountMappedSourcePositions() > 0) { LastStatus = "仍有源仓位,必须先在 MT5 平仓"; DrawPanel(); return; } if(SendAction("exit_system")) { BridgeDisabled = true; } DrawPanel(); } else if(sparam == ButtonEmergencyStop) { if(SendAction("emergency_stop")) { int submitted = CloseAllManagedSourcePositions("emergency_stop"); if(submitted > 0) { BridgeDisabled = false; LastStatus = StringFormat("紧急停止:已提交 %d 个 MT5 平仓", submitted); ScheduleUrgentSnapshot(); } else { BridgeDisabled = true; LastStatus = "紧急停止完成,没有未平源仓位"; } } DrawPanel(); } else if(sparam == ButtonTabInfo) { CurrentPanelMode = PanelModeInfo; DeletePanelObjects(); DrawPanel(); } else if(sparam == ButtonTabConfig) { CurrentPanelMode = PanelModeConfig; DeletePanelObjects(); DrawPanel(); } else if(sparam == ButtonDirectionSame || sparam == ButtonDirectionOpposite) { RuntimeCopyDirection = sparam == ButtonDirectionOpposite ? "opposite" : "same"; SaveCopyDirection(); LastStatus = "方向:" + CopyDirectionPanelText(RuntimeCopyDirection); DrawPanel(); PostSnapshot(); } else if(sparam == ButtonSaveConfig) { ApplyPanelConfig(); DeletePanelObjects(); DrawPanel(); PostSnapshot(); } }