447 lines
17 KiB
MQL5
447 lines
17 KiB
MQL5
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| mvp.mq5 |
|
||
|
|
//| Copyright 2025, MetaQuotes Ltd. |
|
||
|
|
//| https://www.mql5.com |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
#property copyright "Copyright 2026, MAGNUM TECH"
|
||
|
|
#property link "https://www.mql5.com"
|
||
|
|
#property version "2.00"
|
||
|
|
|
||
|
|
//add input param to use a fixed sl points OR
|
||
|
|
//build a logic to adjust for as per Instrument
|
||
|
|
input bool useFixedSL; // whethe to add a buffer of symbol's mini stop level
|
||
|
|
//input double SL_POINTS;
|
||
|
|
input double lot;
|
||
|
|
|
||
|
|
#include <Trade\Trade.mqh>
|
||
|
|
|
||
|
|
|
||
|
|
CTrade trade;
|
||
|
|
|
||
|
|
double fixedSL;
|
||
|
|
|
||
|
|
struct SwingPoint {
|
||
|
|
double price;
|
||
|
|
datetime time;
|
||
|
|
};
|
||
|
|
|
||
|
|
SwingPoint lastHigh;
|
||
|
|
SwingPoint lastLow;
|
||
|
|
datetime lastBarTime = 0;
|
||
|
|
|
||
|
|
// Temporary Counters
|
||
|
|
int countD1 = 0;
|
||
|
|
int countH1 = 0;
|
||
|
|
int countM5 = 0;
|
||
|
|
int countM1 = 0;
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Expert initialization function |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int OnInit()
|
||
|
|
{
|
||
|
|
// Initialize swing points
|
||
|
|
lastHigh.price = 0;
|
||
|
|
lastHigh.time = 0;
|
||
|
|
lastLow.price = 0;
|
||
|
|
lastLow.time = 0;
|
||
|
|
|
||
|
|
//getFixedSL(fixedSL);
|
||
|
|
Print("TickSize: ",SYMBOL_TRADE_TICK_SIZE/_Point,"\nSpread: ",SYMBOL_SPREAD/_Point,"\nstopLevels: ",SYMBOL_TRADE_STOPS_LEVEL/_Point);
|
||
|
|
/*if(useFixedSL){*/Print("SLFixed choosen is ",fixedSL);//}
|
||
|
|
Alert("");
|
||
|
|
|
||
|
|
return(INIT_SUCCEEDED);
|
||
|
|
}
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Expert deinitialization function |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void OnDeinit(const int reason)
|
||
|
|
{
|
||
|
|
Print("Maturity Counts -- D1: ", countD1, " | H1: ", countH1, " | M5: ", countM5, " | M1: ", countM1);
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Candlestick Functions |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int IsEngulfing(MqlRates &r[]) {
|
||
|
|
// Check r[1] (latest closed) vs r[2] (previous)
|
||
|
|
bool bull = r[1].close > r[1].open;
|
||
|
|
bool bear = r[1].close < r[1].open;
|
||
|
|
bool prevBear = r[2].close < r[2].open;
|
||
|
|
bool prevBull = r[2].close > r[2].open;
|
||
|
|
|
||
|
|
// Bullish Engulfing: Current Bull, Prev Bear, Body engulfs Prev Body
|
||
|
|
if (bull && r[1].high > r[2].high && r[1].low < r[2].low) return 1;
|
||
|
|
|
||
|
|
// Bearish Engulfing: Current Bear, Prev Bull, Body engulfs Prev Body
|
||
|
|
if (bear && r[1].low < r[2].low && r[1].high > r[2].high) return -1;
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
int IsGreenlight(MqlRates &r[]) {
|
||
|
|
// Check r[1]
|
||
|
|
double upperWick = r[1].high - MathMax(r[1].open, r[1].close);
|
||
|
|
double lowerWick = MathMin(r[1].open, r[1].close) - r[1].low;
|
||
|
|
|
||
|
|
if (r[1].high < r[2].high) { // catching bullish move early
|
||
|
|
if (upperWick < lowerWick) return 1;
|
||
|
|
}
|
||
|
|
else if (r[1].low > r[2].low) { // catching bearish move early
|
||
|
|
if (lowerWick < upperWick) return -1;
|
||
|
|
}
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Swing Detection |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void DetectSwings(MqlRates &r[]) {
|
||
|
|
// r[2] is the candidate swing candle
|
||
|
|
// r[3] is left, r[1] is right
|
||
|
|
|
||
|
|
bool isHigh = r[2].high > r[3].high && r[2].high > r[1].high;
|
||
|
|
bool isLow = r[2].low < r[3].low && r[2].low < r[1].low;
|
||
|
|
|
||
|
|
// Exception: Outside bar engulfing both sides
|
||
|
|
if (isHigh && isLow) {
|
||
|
|
lastHigh.price = r[2].high;
|
||
|
|
lastHigh.time = r[2].time;
|
||
|
|
lastLow.price = r[2].low;
|
||
|
|
lastLow.time = r[2].time;
|
||
|
|
Print("Dual Swing Detected at ", TimeToString(r[2].time));
|
||
|
|
} else {
|
||
|
|
if (isHigh) {
|
||
|
|
lastHigh.price = r[2].high;
|
||
|
|
lastHigh.time = r[2].time;
|
||
|
|
//Print("Swing High Detected at ", TimeToString(r[2].time));
|
||
|
|
}
|
||
|
|
if (isLow) {
|
||
|
|
lastLow.price = r[2].low;
|
||
|
|
lastLow.time = r[2].time;
|
||
|
|
//Print("Swing Low Detected at ", TimeToString(r[2].time));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Model 1 Signal Logic |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int model1(MqlRates &r[]) {
|
||
|
|
int eng = IsEngulfing(r);
|
||
|
|
int gl = IsGreenlight(r);
|
||
|
|
|
||
|
|
int signal = 0;
|
||
|
|
if (eng == 1 || gl == 1) signal = 1;
|
||
|
|
if (eng == -1 || gl == -1) signal = -1;
|
||
|
|
|
||
|
|
if (signal == 1) {
|
||
|
|
// Bullish signal: Check if backed by swing low
|
||
|
|
// Swing low should exist and preferably be below current price (support)
|
||
|
|
if (lastLow.time != 0 && lastLow.price < r[1].low) {
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (signal == -1) {
|
||
|
|
// Bearish signal: Check if backed by swing high
|
||
|
|
// Swing high should exist and preferably be above current price (resistance)
|
||
|
|
if (lastHigh.time != 0 && lastHigh.price > r[1].high) {
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Trade Signal Structure & Enums |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
enum ENUM_SIGNAL_MATURITY {
|
||
|
|
MATURITY_NONE,
|
||
|
|
MATURITY_D1,
|
||
|
|
MATURITY_H1,
|
||
|
|
MATURITY_M5,
|
||
|
|
MATURITY_M1_EXECUTION,
|
||
|
|
MATURITY_INVALID
|
||
|
|
};
|
||
|
|
|
||
|
|
struct LevelParams {
|
||
|
|
double sl;
|
||
|
|
double tp;
|
||
|
|
datetime time;
|
||
|
|
bool active;
|
||
|
|
};
|
||
|
|
|
||
|
|
struct TradeSignal {
|
||
|
|
string id; // Composite: Symbol_TimeD1
|
||
|
|
string symbol;
|
||
|
|
|
||
|
|
int type; // 1: Buy, -1: Sell
|
||
|
|
double entryPrice; // Signal Price
|
||
|
|
|
||
|
|
LevelParams matchD1;
|
||
|
|
LevelParams matchH1;
|
||
|
|
LevelParams matchM5;
|
||
|
|
LevelParams matchM1;
|
||
|
|
|
||
|
|
// Current operational pointers
|
||
|
|
double currentSL;
|
||
|
|
double currentTP;
|
||
|
|
|
||
|
|
ENUM_SIGNAL_MATURITY maturity;
|
||
|
|
bool isActive;
|
||
|
|
|
||
|
|
string comment;
|
||
|
|
};
|
||
|
|
|
||
|
|
TradeSignal activeSignals[];
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Helper Functions |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
/*might as well be problem */string GetSignalID(string symbol, datetime time) {
|
||
|
|
return symbol + "_" + TimeToString(time, TIME_DATE|TIME_MINUTES);
|
||
|
|
}
|
||
|
|
|
||
|
|
int FindSignal(string id) {
|
||
|
|
for(int i=0; i<ArraySize(activeSignals); i++) {
|
||
|
|
if(activeSignals[i].id == id && activeSignals[i].isActive) return i;
|
||
|
|
}
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool PositionExists(string comment) {
|
||
|
|
for(int i=PositionsTotal()-1; i>=0; i--) {
|
||
|
|
//if(PositionSelectByTicket(PositionGetTicket(i))) {
|
||
|
|
if(PositionGetString(POSITION_COMMENT) == comment) return true;
|
||
|
|
//}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for New Bar per timeframe
|
||
|
|
bool IsNewBar(string symbol, ENUM_TIMEFRAMES tf, datetime &last_time) {
|
||
|
|
datetime current_time = iTime(symbol, tf, 0);
|
||
|
|
if(last_time == 0) {
|
||
|
|
last_time = current_time;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if(current_time != last_time) {
|
||
|
|
last_time = current_time;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Same logic as DetectSwings but returns value for arbitrary array
|
||
|
|
void FindRecentSwings(MqlRates &rates[], double &outHigh, double &outLow) {
|
||
|
|
outHigh = 0;
|
||
|
|
outLow = 0;
|
||
|
|
/*is this the right logic??*/ int limit = MathMin(ArraySize(rates)-2, 50);
|
||
|
|
for(int i=2; i<limit; i++) { // Start at 2 to allow i-1, i+1 checks
|
||
|
|
// High Logic: r[i] > r[i+1] && r[i] > r[i-1] as per DetectSwings
|
||
|
|
if (outHigh == 0) {
|
||
|
|
if (rates[i].high > rates[i+1].high && rates[i].high > rates[i-1].high) {
|
||
|
|
outHigh = rates[i].high;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Low Logic: r[i] < r[i+1] && r[i] < r[i-1]
|
||
|
|
if (outLow == 0) {
|
||
|
|
if (rates[i].low < rates[i+1].low && rates[i].low < rates[i-1].low) {
|
||
|
|
outLow = rates[i].low;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (outHigh != 0 && outLow != 0) break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void DemoteSignal(int index) {
|
||
|
|
if (index < 0 || index >= ArraySize(activeSignals)) return;
|
||
|
|
|
||
|
|
ENUM_SIGNAL_MATURITY oldLevel = activeSignals[index].maturity;
|
||
|
|
Print("Signal Demoted: ", activeSignals[index].id, " from ", EnumToString(oldLevel));
|
||
|
|
|
||
|
|
if (oldLevel == MATURITY_H1) {
|
||
|
|
activeSignals[index].maturity = MATURITY_D1;
|
||
|
|
activeSignals[index].currentSL = activeSignals[index].matchD1.sl;
|
||
|
|
activeSignals[index].currentTP = activeSignals[index].matchD1.tp;
|
||
|
|
} else if (oldLevel == MATURITY_M5) {
|
||
|
|
activeSignals[index].maturity = MATURITY_H1;
|
||
|
|
activeSignals[index].currentSL = activeSignals[index].matchH1.sl;
|
||
|
|
activeSignals[index].currentTP = activeSignals[index].matchH1.tp;
|
||
|
|
} else if (oldLevel == MATURITY_M1_EXECUTION) {
|
||
|
|
activeSignals[index].maturity = MATURITY_M5;
|
||
|
|
activeSignals[index].currentSL = activeSignals[index].matchM5.sl;
|
||
|
|
activeSignals[index].currentTP = activeSignals[index].matchM5.tp;
|
||
|
|
} else {
|
||
|
|
activeSignals[index].isActive = false; // D1 or lower invalid
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void PromoteSignal(int index, ENUM_SIGNAL_MATURITY newLevel, MqlRates &rates[]) {
|
||
|
|
if (index < 0 || index >= ArraySize(activeSignals)) return;
|
||
|
|
|
||
|
|
double swingHigh = 0, swingLow = 0;
|
||
|
|
FindRecentSwings(rates, swingHigh, swingLow);
|
||
|
|
|
||
|
|
// rates[1] is the signal candle
|
||
|
|
double signalLow = rates[1].low;
|
||
|
|
double signalHigh = rates[1].high;
|
||
|
|
|
||
|
|
LevelParams params;
|
||
|
|
params.time = rates[1].time;
|
||
|
|
params.active = true;
|
||
|
|
|
||
|
|
if (activeSignals[index].type == 1) { // BUY
|
||
|
|
params.sl = signalLow;
|
||
|
|
params.tp = swingHigh;
|
||
|
|
} else { // SELL
|
||
|
|
params.sl = signalHigh;
|
||
|
|
params.tp = swingLow;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fallback if no swing found
|
||
|
|
if (params.tp == 0) params.tp = (activeSignals[index].type == 1) ? params.sl + (params.sl*0.01) : params.sl - (params.sl*0.01);
|
||
|
|
|
||
|
|
if (newLevel == MATURITY_H1) activeSignals[index].matchH1 = params;
|
||
|
|
else if (newLevel == MATURITY_M5) activeSignals[index].matchM5 = params;
|
||
|
|
else if (newLevel == MATURITY_M1_EXECUTION) activeSignals[index].matchM1 = params;
|
||
|
|
|
||
|
|
activeSignals[index].currentSL = params.sl;
|
||
|
|
activeSignals[index].currentTP = params.tp;
|
||
|
|
activeSignals[index].maturity = newLevel;
|
||
|
|
|
||
|
|
if (newLevel == MATURITY_H1) countH1++;
|
||
|
|
else if (newLevel == MATURITY_M5) countM5++;
|
||
|
|
else if (newLevel == MATURITY_M1_EXECUTION){
|
||
|
|
if(activeSignals[index].type==1){
|
||
|
|
///*buy*/
|
||
|
|
if(!PositionExists(activeSignals[index].id)){}
|
||
|
|
//trade.Buy(lot,_Symbol,0,(activeSignals[index].currentSL-fixedSL),activeSignals[index].matchD1.tp,activeSignals[index].id);
|
||
|
|
//instead try not inluding sl in position opening
|
||
|
|
//rather have a function killing trades right when the price touch it
|
||
|
|
}
|
||
|
|
else if(activeSignals[index].type==-1){
|
||
|
|
//sell
|
||
|
|
if(!PositionExists(activeSignals[index].id)){}
|
||
|
|
//trade.Sell(lot,_Symbol,0,(activeSignals[index].currentSL+fixedSL),activeSignals[index].matchD1.tp,activeSignals[index].id);
|
||
|
|
}
|
||
|
|
countM1++;
|
||
|
|
}
|
||
|
|
Print("Signal Promoted: ", activeSignals[index].id, " to ", EnumToString(newLevel));
|
||
|
|
}
|
||
|
|
|
||
|
|
//double getFixedSL(double &fixedSL){
|
||
|
|
// if(useFixedSL){
|
||
|
|
// fixedSL = SL_POINTS / _Point;
|
||
|
|
// } else {
|
||
|
|
// fixedSL=SYMBOL_TRADE_TICK_SIZE*safetyFactor*_Point;
|
||
|
|
// }
|
||
|
|
// return fixedSL;
|
||
|
|
//}
|
||
|
|
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Expert tick function |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void OnTick()
|
||
|
|
{
|
||
|
|
static datetime lastD1=0, lastH1=0, lastM5=0, lastM1=0;
|
||
|
|
bool isNewD1 = IsNewBar(_Symbol, PERIOD_D1, lastD1);
|
||
|
|
bool isNewH1 = IsNewBar(_Symbol, PERIOD_H1, lastH1);
|
||
|
|
bool isNewM5 = IsNewBar(_Symbol, PERIOD_M5, lastM5);
|
||
|
|
bool isNewM1 = IsNewBar(_Symbol, PERIOD_M1, lastM1);
|
||
|
|
|
||
|
|
// --- D1 SCAN ---
|
||
|
|
if (isNewD1) {
|
||
|
|
printf("new D bar formed!");
|
||
|
|
MqlRates avgD1[];
|
||
|
|
ArraySetAsSeries(avgD1, true);
|
||
|
|
if(CopyRates(_Symbol, PERIOD_D1, 0, 5, avgD1) == 5) {
|
||
|
|
DetectSwings(avgD1); // Update Globals for model1
|
||
|
|
int sigD1 = model1(avgD1);
|
||
|
|
if (sigD1 != 0) { //
|
||
|
|
string sigID = GetSignalID(_Symbol, avgD1[1].time); //give ID
|
||
|
|
if (FindSignal(sigID) == -1) { //new ID found
|
||
|
|
int newIdx = ArrayResize(activeSignals, (ArraySize(activeSignals) + 1)); //make space for new signal
|
||
|
|
activeSignals[newIdx-1].id = sigID;
|
||
|
|
activeSignals[newIdx-1].symbol = _Symbol;
|
||
|
|
activeSignals[newIdx-1].type = sigD1;
|
||
|
|
activeSignals[newIdx-1].maturity = MATURITY_D1;
|
||
|
|
activeSignals[newIdx-1].isActive = true;
|
||
|
|
activeSignals[newIdx-1].matchD1.time = avgD1[1].time;
|
||
|
|
activeSignals[newIdx-1].matchD1.active = true;
|
||
|
|
|
||
|
|
// Assign Initial D1 Targets
|
||
|
|
if(sigD1 == 1) { // Buy
|
||
|
|
activeSignals[newIdx-1].matchD1.sl = avgD1[1].low;
|
||
|
|
activeSignals[newIdx-1].matchD1.tp = lastHigh.price;
|
||
|
|
// Fallback if TP is not logical (e.g. Price > lastHigh)
|
||
|
|
//if (activeSignals[newIdx-1].matchD1.tp <= avgD1[1].close)
|
||
|
|
// activeSignals[newIdx-1].matchD1.tp = avgD1[1].close + (avgD1[1].close - lastLow.price)*2;
|
||
|
|
} else { // Sell
|
||
|
|
activeSignals[newIdx-1].matchD1.sl = avgD1[1].high;
|
||
|
|
activeSignals[newIdx-1].matchD1.tp = lastLow.price;
|
||
|
|
// Fallback
|
||
|
|
//if (activeSignals[newIdx-1].matchD1.tp >= avgD1[1].close)
|
||
|
|
// activeSignals[newIdx-1].matchD1.tp = avgD1[1].close - (lastHigh.price - avgD1[1].close)*2;
|
||
|
|
}
|
||
|
|
|
||
|
|
activeSignals[newIdx-1].currentSL = activeSignals[newIdx-1].matchD1.sl;
|
||
|
|
activeSignals[newIdx-1].currentTP = activeSignals[newIdx-1].matchD1.tp;
|
||
|
|
|
||
|
|
countD1++;
|
||
|
|
Print("New D1 Signal Found: ", sigID, " SL: ", activeSignals[newIdx-1].currentSL, " TP: ", activeSignals[newIdx-1].currentTP);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- MATURITY LOOP ---
|
||
|
|
for(int i=0; i<ArraySize(activeSignals); i++) {
|
||
|
|
if(!activeSignals[i].isActive) continue;
|
||
|
|
|
||
|
|
double currentBid = SymbolInfoDouble(activeSignals[i].symbol, SYMBOL_BID);
|
||
|
|
double currentAsk = SymbolInfoDouble(activeSignals[i].symbol, SYMBOL_ASK);
|
||
|
|
|
||
|
|
// VALIDATION (Every Tick)
|
||
|
|
if (activeSignals[i].currentSL > 0 && activeSignals[i].currentTP > 0) {
|
||
|
|
bool hitSL = false, hitTP = false;
|
||
|
|
if (activeSignals[i].type == 1) { // Buy
|
||
|
|
if (currentBid <= activeSignals[i].currentSL) hitSL = true;
|
||
|
|
if (currentBid >= activeSignals[i].currentTP) hitTP = true;
|
||
|
|
} else { // Sell
|
||
|
|
if (currentAsk >= activeSignals[i].currentSL) hitSL = true;
|
||
|
|
if (currentAsk <= activeSignals[i].currentTP) hitTP = true;
|
||
|
|
}
|
||
|
|
if (hitSL || hitTP) {
|
||
|
|
DemoteSignal(i);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// PROMOTION (Check on target TF New Bar)
|
||
|
|
MqlRates tfRates[];
|
||
|
|
ArraySetAsSeries(tfRates, true);
|
||
|
|
|
||
|
|
if (activeSignals[i].maturity == MATURITY_D1 /*lookback if prev candle was a signal*/&& isNewH1) {
|
||
|
|
if(CopyRates(_Symbol, PERIOD_H1, 0, 50, tfRates) >= 5) {
|
||
|
|
if (IsGreenlight(tfRates) == activeSignals[i].type) PromoteSignal(i, MATURITY_H1, tfRates);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else if (activeSignals[i].maturity == MATURITY_H1 /*lookback if prev candle was a signal*/&& isNewM5) {
|
||
|
|
if(CopyRates(_Symbol, PERIOD_M5, 0, 50, tfRates) >= 5) {
|
||
|
|
if (IsGreenlight(tfRates) == activeSignals[i].type) PromoteSignal(i, MATURITY_M5, tfRates);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else if (activeSignals[i].maturity == MATURITY_M5 /*lookback if prev candle was a signal*/&& isNewM1) {
|
||
|
|
if(CopyRates(_Symbol, PERIOD_M1, 0, 50, tfRates) >= 5) {
|
||
|
|
if (IsGreenlight(tfRates) == activeSignals[i].type) {
|
||
|
|
PromoteSignal(i, MATURITY_M1_EXECUTION, tfRates);
|
||
|
|
Print(" >>> EXECUTION SIGNAL <<< ", activeSignals[i].id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|