Why do we’d like Google Apps Script?
To alternate commerce knowledge between separate MetaTrader terminals, a easy relay server is required. Google Apps Script acts as a free middleman that transfers commerce occasions from the Grasp account to the Slave account. It ensures dependable supply of occasions even after web interruptions or terminal restarts and doesn’t require a VPS or devoted server.
The right way to create and deploy Google Apps Script
Go to https://script.google.com and Click on Begin scripting

Click on New challenge

Delete the default code and paste the script offered under the instruction steps

Press Ctrl + S to save lots of the challenge (the highest menu will change into energetic)

Click on Deploy within the top-right nook and choose New deployment

Within the opened window, click on Choose sort (⚙️) and select Internet app

In the Description subject, enter Model 1 (any textual content is okay). Set Who has entry to Anybody and depart Execute as unchanged

Click on Deploy – your Apps Script URL will likely be generated. Copy and paste this URL into the EA enter settings

const API_KEY = 'I_AM_API_KEY'; const MAX_PRUNE = 200; const CONSUMER_TTL_MS = 6 * 60 * 60 * 1000; const MAX_CONSUMERS_PER_CHANNEL = 50; perform doPost(e) { const lock = LockService.getScriptLock(); let locked = false; attempt { lock.waitLock(10000); locked = true; if (!e || !e.postData || !e.parameter) return _resp({ okay:false, error:'no knowledge' }); const key = (e.parameter.key || '').toString(); if (key !== API_KEY) return _resp({ okay:false, error:'forbidden' }); const channel = (e.parameter.channel || 'default').toString(); const shopper = (e.parameter.shopper || '').toString(); const c = shopper || 'single'; const retailer = PropertiesService.getScriptProperties(); let uncooked = e.postData.contents || '{}'; uncooked = uncooked.exchange(/[u0000-u001F]+$/g, ''); let physique; attempt { physique = JSON.parse(uncooked); } catch (parseErr) { return _resp({ okay:false, error:'unhealthy json', particulars:String(parseErr) }); } _touchConsumerFast(retailer, channel, c); if (physique && physique.motion === 'ack') { const lastId = Quantity(physique.last_id || 0); if (!lastId) return _resp({ okay:false, error:'unhealthy ack' }); retailer.setProperty(_ackKey(channel, c), String(lastId)); _pruneByMinAckFast(retailer, channel); return _resp({ okay:true, ack:lastId }); } const nextId = _nextSeq(retailer, channel); physique.id = nextId; physique.server_time_ms = Date.now(); retailer.setProperty(_evKey(channel, nextId), JSON.stringify(physique)); const minKey = _minKey(channel); const curMin = Quantity(retailer.getProperty(minKey) || '0'); if (!curMin) retailer.setProperty(minKey, String(nextId)); return _resp({ okay:true, last_id: nextId }); } catch (err) { return _resp({ okay:false, error:'exception', message:String(err), stack:(err && err.stack) ? String(err.stack) : '' }); } lastly { if (locked) { attempt { lock.releaseLock(); } catch(_) {} } } } perform doGet(e) { const lock = LockService.getScriptLock(); let locked = false; attempt { lock.waitLock(10000); locked = true; if (!e || !e.parameter) return _resp({ okay:false, error:'no params' }); const key = (e.parameter.key || '').toString(); if (key !== API_KEY) return _resp({ okay:false, error:'forbidden' }); const channel = (e.parameter.channel || 'default').toString(); const shopper = (e.parameter.shopper || '').toString(); const c = shopper || 'single'; const restrict = Math.max(1, Math.min(100, Quantity(e.parameter.restrict || 20))); const retailer = PropertiesService.getScriptProperties(); _touchConsumerFast(retailer, channel, c); const minId = Quantity(retailer.getProperty(_minKey(channel)) || '0'); const seq = Quantity(retailer.getProperty(_seqKey(channel)) || '0'); const ackKey = _ackKey(channel, c); let ack = Quantity(retailer.getProperty(ackKey) || '0'); if (minId > 0) { const floorAck = Math.max(0, minId - 1); if (ack < floorAck) { ack = floorAck; retailer.setProperty(ackKey, String(ack)); } } const mode = (e.parameter.mode || '').toString(); if (mode === 'well being' || mode === 'debug') { const customers = _listActiveConsumersFast(retailer, channel); const minAck = _minAckFast(retailer, channel, customers); const out = { okay:true, channel, shopper:c, ack, seq, min_id:minId, active_consumers:customers, min_ack:minAck }; if (mode === 'debug') { const seen = {}; for (const cc of customers) '0'); out.seen = seen; } return _resp(out); } const occasions = []; let missing_id = 0; for (let id = ack + 1; id <= seq && occasions.size < restrict; id++) { const evStr = retailer.getProperty(_evKey(channel, id)); if (!evStr) { missing_id = id; break; } attempt { occasions.push(JSON.parse(evStr)); } catch (parseErr) { missing_id = id; break; } } if (missing_id && minId > 0 && missing_id < minId) { const newAck = Math.max(0, minId - 1); retailer.setProperty(ackKey, String(newAck)); const events2 = []; let missing2 = 0; for (let id = newAck + 1; id <= seq && events2.size < restrict; id++) { const evStr = retailer.getProperty(_evKey(channel, id)); if (!evStr) { missing2 = id; break; } attempt { events2.push(JSON.parse(evStr)); } catch (_) { missing2 = id; break; } } if (!missing2) { return _resp({ okay:true, ack: newAck, seq: seq, occasions: events2 }); } return _resp({ okay:false, error:'gap_detected', ack: newAck, seq: seq, missing_id: missing2 }); } if (missing_id) { return _resp({ okay:false, error:'gap_detected', ack: ack, seq: seq, missing_id: missing_id }); } return _resp({ okay:true, ack: ack, seq: seq, occasions: occasions }); } catch (err) { return _resp({ okay:false, error:'exception', message:String(err), stack:(err && err.stack) ? String(err.stack) : '' }); } lastly { if (locked) { attempt { lock.releaseLock(); } catch(_) {} } } } perform _nextSeq(retailer, channel) perform _touchConsumerFast(retailer, channel, shopper) { const now = Date.now(); retailer.setProperty(_seenKey(channel, shopper), String(now)); const listKey = _consumersKey(channel); let arr = []; attempt catch(_) { arr = []; } if (arr.indexOf(shopper) < 0) { arr.push(shopper); if (arr.size > MAX_CONSUMERS_PER_CHANNEL) arr = arr.slice(arr.size - MAX_CONSUMERS_PER_CHANNEL); retailer.setProperty(listKey, JSON.stringify(arr)); } const ackKey = _ackKey(channel, shopper); const ackStr = retailer.getProperty(ackKey); if (ackStr === null || ackStr === undefined || ackStr === '') '0'); retailer.setProperty(ackKey, String(Math.max(0, seq))); return; const minId = Quantity(retailer.getProperty(_minKey(channel)) || '0'); const ack = Quantity(ackStr || '0'); if (minId > 0) { const floorAck = Math.max(0, minId - 1); if (ack < floorAck) retailer.setProperty(ackKey, String(floorAck)); } } perform _listActiveConsumersFast(retailer, channel) { const now = Date.now(); const listKey = _consumersKey(channel); let arr = []; attempt catch(_) { arr = []; } const energetic = []; for (const c of arr) '0'); if (!seen) proceed; if (now - seen <= CONSUMER_TTL_MS) energetic.push(c); if (energetic.size === 0) energetic.push('single'); return energetic; } perform _minAckFast(retailer, channel, customers) { let min = null; for (const c of customers) '0'); if (min === null return min === null ? 0 : min; } perform _pruneByMinAckFast(retailer, channel) { const customers = _listActiveConsumersFast(retailer, channel); const minAck = _minAckFast(retailer, channel, customers); if (minAck <= 0) return; _pruneAckedUpTo(retailer, channel, minAck); } perform _pruneAckedUpTo(retailer, channel, ackId) { const minKey = _minKey(channel); let minId = Quantity(retailer.getProperty(minKey) || '0'); if (!minId) return; let eliminated = 0; whereas (minId && minId <= ackId && eliminated < MAX_PRUNE) { retailer.deleteProperty(_evKey(channel, minId)); minId++; eliminated++; } const seq = Quantity(retailer.getProperty(_seqKey(channel)) || '0'); if (minId > seq) { retailer.deleteProperty(minKey); } else { retailer.setProperty(minKey, String(minId)); } } perform _seqKey(channel) { return channel + '__seq'; } perform _minKey(channel) { return channel + '__min'; } perform _ackKey(channel, shopper) { return channel + '__ack__' + shopper; } perform _evKey(channel, id) { return channel + '__ev__' + id; } perform _seenKey(channel, shopper) { return channel + '__seen__' + shopper; } perform _consumersKey(channel) { return channel + '__consumers'; } perform _resp(obj) { return ContentService .createTextOutput(JSON.stringify(obj)) .setMimeType(ContentService.MimeType.JSON); }







