Files
ichimoku-h4-m1-align/ichimoku-h1-m1-ea.mq5

649 lines
23 KiB
Plaintext

//+------------------------------------------------------------------+
//| Ichimoku H1-M1 Alignment EA |
//| Entry: H1→M1 price+chikou all above/below tenkan,kijun,cloud |
//| Exit: ATR chandelier trailing stop once profitable (locks in |
//| the peak), M5 close crosses M5 kijun as final fallback, |
//| or ATR-based protective stop loss |
//| Author: Neo Malesa |
//+------------------------------------------------------------------+
#property strict
#include <Trade/Trade.mqh>
//--- Input Parameters ---
input string Symbols = "GOLDm#";
input int Tenkan = 9;
input int Kijun = 26;
input int SenkouB = 52;
input int Slippage = 30;
input group "Risk Protection"
input bool InpUseStopLoss = true; // Attach ATR-based stop loss to every entry
input int InpATRPeriod = 14; // ATR period (M15)
input double InpATRMultiplier = 3.0; // SL distance = ATR * multiplier
input int InpMaxSpreadPoints = 60; // Max spread in points to allow entry (0 = no limit)
input double InpHighEquityRiskPct = 1.0; // % of equity risked per trade once equity > $8000
input int InpReentryCooldownSec = 0; // Min seconds after an exit before re-entering same symbol (0 = none)
//--- Trail mode: off, always on, or choppy-only (ADX regime filter)
enum ENUM_TRAIL_MODE { TRAIL_OFF = 0, TRAIL_ALWAYS = 1, TRAIL_CHOPPY = 2 };
input group "Exit Management"
input ENUM_TRAIL_MODE InpTrailMode = TRAIL_CHOPPY; // 0=off, 1=always, 2=choppy-only (ADX)
input double InpTrailATR = 2.0; // Trail distance = ATR(M5) * multiplier
input double InpTrailActivateATR = 1.0; // Arm the trail once profit >= ATR(M5) * multiplier
input int InpADXPeriod = 14; // ADX period for choppy-market detection (M5)
input double InpChopADXLevel = 22.0; // ADX below this = choppy -> trail on in auto mode
input group "Equity Alert Settings"
input double InpMinProfitTrigger = 5.0; // Min Profit over Baseline to trigger alert
input double InpWithdrawProfitPct = 50.0; // Percentage of the PROFIT to withdraw
input ENUM_DAY_OF_WEEK InpCheckDay = FRIDAY; // Day of the week to check
input bool InpResetBaseline = false; // Set to true to reset baseline to current equity
input bool InpSendPush = true; // Send push notification
//--- Constants and Global Variables ---
#define MAX_SYMS 60
#define TF_COUNT 5
#define IDX_M5 3 // index of M5 in tfs[] — used for exit check
ENUM_TIMEFRAMES tfs[TF_COUNT] = {
PERIOD_H1, PERIOD_M30, PERIOD_M15, PERIOD_M5, PERIOD_M1
};
int ich[MAX_SYMS][TF_COUNT];
int atr[MAX_SYMS];
int atrExit[MAX_SYMS]; // ATR(M5) handle for the chandelier trail
int adx[MAX_SYMS]; // ADX(M5) handle for choppy-market regime detection
string syms[MAX_SYMS];
int symsCount = 0;
datetime lastM1bar[MAX_SYMS];
datetime noReentryUntil[MAX_SYMS];
datetime lastH1bar = 0;
int state[MAX_SYMS]; // 0=no position, 1=long, -1=short
double entryPrice[MAX_SYMS]; // reference entry price per symbol (trail arming)
double trailHigh[MAX_SYMS]; // highest high since entry (long chandelier reference)
double trailLow[MAX_SYMS]; // lowest low since entry (short chandelier reference)
int MAGIC = 20260502;
#define GV_BASE_EQUITY "EA_EquityAlert_Base_" + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN))
#define GV_LAST_ALERT_DAY "EA_EquityAlert_Day_" + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN))
CTrade trade;
//==============================================================
// Initialization and Deinitialization
//==============================================================
int ParseSymbols(string list)
{
string parts[];
int n = StringSplit(list, ',', parts);
int cnt = 0;
for(int i = 0; i < n && cnt < MAX_SYMS; i++)
{
string sym = parts[i];
StringTrimLeft(sym);
StringTrimRight(sym);
if(SymbolSelect(sym, true)) syms[cnt++] = sym;
}
return cnt;
}
int OnInit()
{
symsCount = ParseSymbols(Symbols);
if(symsCount <= 0) return(INIT_FAILED);
for(int s = 0; s < symsCount; s++)
{
state[s] = 0;
lastM1bar[s] = 0;
noReentryUntil[s] = 0;
for(int t = 0; t < TF_COUNT; t++)
{
ich[s][t] = iIchimoku(syms[s], tfs[t], Tenkan, Kijun, SenkouB);
if(ich[s][t] == INVALID_HANDLE) return(INIT_FAILED);
}
atr[s] = INVALID_HANDLE;
if(InpUseStopLoss)
{
atr[s] = iATR(syms[s], PERIOD_M15, InpATRPeriod);
if(atr[s] == INVALID_HANDLE) return(INIT_FAILED);
}
atrExit[s] = INVALID_HANDLE;
if(InpTrailMode != TRAIL_OFF)
{
atrExit[s] = iATR(syms[s], PERIOD_M5, InpATRPeriod);
if(atrExit[s] == INVALID_HANDLE) return(INIT_FAILED);
}
adx[s] = INVALID_HANDLE;
if(InpTrailMode == TRAIL_CHOPPY)
{
adx[s] = iADX(syms[s], PERIOD_M5, InpADXPeriod);
if(adx[s] == INVALID_HANDLE) return(INIT_FAILED);
}
}
trade.SetDeviationInPoints(Slippage);
SyncStateFromPositions();
InitEquityAlert();
return(INIT_SUCCEEDED);
}
//==============================================================
// Equity Alert: weekly profit-withdrawal reminder
//==============================================================
void InitEquityAlert()
{
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
if(!GlobalVariableCheck(GV_BASE_EQUITY) || InpResetBaseline)
{
GlobalVariableSet(GV_BASE_EQUITY, currentEquity);
}
if(!GlobalVariableCheck(GV_LAST_ALERT_DAY))
{
GlobalVariableSet(GV_LAST_ALERT_DAY, 0);
}
}
void CheckEquityAlert()
{
MqlDateTime dt;
TimeCurrent(dt);
if(dt.day_of_week != InpCheckDay) return;
if((int)GlobalVariableGet(GV_LAST_ALERT_DAY) == dt.day) return;
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
double baseEquity = GlobalVariableGet(GV_BASE_EQUITY);
double profit = currentEquity - baseEquity;
if(profit >= InpMinProfitTrigger)
{
double withdrawAmount = profit * (InpWithdrawProfitPct / 100.0);
string msg = StringFormat("Profit: %.2f. Suggest withdrawing: %.2f", profit, withdrawAmount);
Alert(msg);
if(InpSendPush) SendNotification(msg);
GlobalVariableSet(GV_LAST_ALERT_DAY, (double)dt.day);
GlobalVariablesFlush();
}
}
void OnDeinit(const int reason)
{
for(int s = 0; s < symsCount; s++)
{
for(int t = 0; t < TF_COUNT; t++)
IndicatorRelease(ich[s][t]);
if(atr[s] != INVALID_HANDLE) IndicatorRelease(atr[s]);
if(atrExit[s] != INVALID_HANDLE) IndicatorRelease(atrExit[s]);
if(adx[s] != INVALID_HANDLE) IndicatorRelease(adx[s]);
}
}
//==============================================================
// Position State Sync (recover after restart)
//==============================================================
void SyncStateFromPositions()
{
// Rebuild from scratch so positions closed by SL or manually free the
// symbol for re-entry instead of leaving stale state behind.
int prevState[MAX_SYMS];
bool hasPos[MAX_SYMS];
for(int s = 0; s < symsCount; s++) { prevState[s] = state[s]; state[s] = 0; hasPos[s] = false; }
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(!PositionSelectByTicket(ticket)) continue;
string sym = PositionGetString(POSITION_SYMBOL);
int magic = (int)PositionGetInteger(POSITION_MAGIC);
int type = (int)PositionGetInteger(POSITION_TYPE);
int dir = (type == POSITION_TYPE_BUY) ? 1 : -1;
if(magic != MAGIC) continue;
for(int s = 0; s < symsCount; s++)
{
if(syms[s] == sym)
{
state[s] = dir;
hasPos[s] = true;
// EA (re)started mid-trade — rebuild the trail from the open price
// and let it accumulate fresh extremes from here.
if(entryPrice[s] == 0.0)
{
entryPrice[s] = PositionGetDouble(POSITION_PRICE_OPEN);
trailHigh[s] = entryPrice[s];
trailLow[s] = entryPrice[s];
}
break;
}
}
}
// Symbols with no open position get their trail memory cleared
for(int s = 0; s < symsCount; s++)
{
if(!hasPos[s])
{
entryPrice[s] = 0.0;
trailHigh[s] = 0.0;
trailLow[s] = 0.0;
}
}
// A position that vanished since the last sync (SL hit, manual close) frees
// the symbol; arm the optional re-entry cooldown so it can't flip instantly.
for(int s = 0; s < symsCount; s++)
{
if(prevState[s] != 0 && state[s] == 0)
noReentryUntil[s] = TimeCurrent() + InpReentryCooldownSec;
}
}
//==============================================================
// Alignment Check: price and chikou both above/below tenkan,
// kijun, and cloud. Returns 1 (bullish), -1 (bearish), 0 (none)
//==============================================================
int CheckAlign(int s, int tfIdx)
{
ENUM_TIMEFRAMES tf = tfs[tfIdx];
int sh = 1; // last closed bar
int chShift = sh + Kijun; // chikou's chart position for bar sh (Kijun bars back)
int chCloud = chShift + Kijun;// senkou buffer offset for the cloud at chikou's position
MqlRates rt[];
if(CopyRates(syms[s], tf, 0, chCloud + 1, rt) <= 0) return 0;
ArraySetAsSeries(rt, true);
if(ArraySize(rt) <= chCloud) return 0;
double tenkan[1], kijun[1], senA[1], senB[1];
// price bar sh: tenkan, kijun, cloud
if(CopyBuffer(ich[s][tfIdx], 0, sh, 1, tenkan) <= 0) return 0;
if(CopyBuffer(ich[s][tfIdx], 1, sh, 1, kijun) <= 0) return 0;
if(CopyBuffer(ich[s][tfIdx], 2, sh + Kijun, 1, senA) <= 0) return 0;
if(CopyBuffer(ich[s][tfIdx], 3, sh + Kijun, 1, senB) <= 0) return 0;
double closeP = rt[sh].close;
double cHi = MathMax(senA[0], senB[0]);
double cLo = MathMin(senA[0], senB[0]);
// Short-circuit: most bars aren't aligned, so skip the chikou-side buffers
// unless the close is already clearly on one side of tenkan/kijun/cloud.
bool above = closeP > tenkan[0] && closeP > kijun[0] && closeP > cHi;
bool below = closeP < tenkan[0] && closeP < kijun[0] && closeP < cLo;
if(!above && !below) return 0;
// tenkan, kijun, cloud at chikou's chart position
double tenkan_ch[1], kijun_ch[1], senA_ch[1], senB_ch[1];
if(CopyBuffer(ich[s][tfIdx], 0, chShift, 1, tenkan_ch) <= 0) return 0;
if(CopyBuffer(ich[s][tfIdx], 1, chShift, 1, kijun_ch) <= 0) return 0;
if(CopyBuffer(ich[s][tfIdx], 2, chCloud, 1, senA_ch) <= 0) return 0;
if(CopyBuffer(ich[s][tfIdx], 3, chCloud, 1, senB_ch) <= 0) return 0;
// The chikou span for bar sh IS its close, plotted Kijun bars back.
// Compare it against the candle and Ichimoku levels at that chart position.
double chik = closeP;
double cHiC = MathMax(senA_ch[0], senB_ch[0]);
double cLoC = MathMin(senA_ch[0], senB_ch[0]);
// bullish: price above tenkan, kijun, and cloud; chikou clear above price,
// tenkan, kijun, and cloud at its plotted position
if(above && chik > rt[chShift].high &&
chik > tenkan_ch[0] && chik > kijun_ch[0] && chik > cHiC) return 1;
// bearish: price below tenkan, kijun, and cloud; chikou clear below price,
// tenkan, kijun, and cloud at its plotted position
if(below && chik < rt[chShift].low &&
chik < tenkan_ch[0] && chik < kijun_ch[0] && chik < cLoC) return -1;
return 0;
}
//==============================================================
// Entry Check: all timeframes (H1→M1) aligned same direction
//==============================================================
int CheckAllAlign(int s)
{
int dir = CheckAlign(s, 0);
if(dir == 0) return 0;
for(int t = 1; t < TF_COUNT; t++)
{
if(CheckAlign(s, t) != dir) return 0;
}
return dir;
}
//==============================================================
// Exit Check: M5 price closed on wrong side of M5 kijun
//==============================================================
bool CheckM5Exit(int s, int dir)
{
double kij[1];
if(CopyBuffer(ich[s][IDX_M5], 1, 1, 1, kij) <= 0) return false;
MqlRates rt[];
if(CopyRates(syms[s], PERIOD_M5, 1, 1, rt) <= 0) return false;
double closeP = rt[0].close;
if(dir == 1 && closeP < kij[0]) return true;
if(dir == -1 && closeP > kij[0]) return true;
return false;
}
//==============================================================
// Choppy-market detection: ADX(M5) below InpChopADXLevel means no
// trend, so the trail is allowed. An unready/unknown ADX value is
// treated as choppy (trail on) to match the always-on default.
//==============================================================
bool IsChoppy(int s)
{
double d[1];
if(CopyBuffer(adx[s], 0, 1, 1, d) <= 0 || d[0] <= 0) return true;
return d[0] < InpChopADXLevel;
}
//==============================================================
// Exit Trail: ATR chandelier stop once the trade is in profit.
// The reference point is the extreme (high/low) of the M5 bar
// that is still forming, so a peak is locked in before it
// retraces. Re-evaluated on every new M1 bar; only ever
// tightens and never sits inside the broker minimum stop.
//==============================================================
void ManageTrail(int s)
{
if(state[s] == 0 || InpTrailMode == TRAIL_OFF || !InpUseStopLoss) return;
if(InpTrailMode == TRAIL_CHOPPY && !IsChoppy(s)) return;
double atrVal;
if(atrExit[s] == INVALID_HANDLE) return;
double a[1];
if(CopyBuffer(atrExit[s], 0, 1, 1, a) <= 0 || a[0] <= 0) return;
atrVal = a[0];
MqlRates m5[];
if(CopyRates(syms[s], PERIOD_M5, 0, 1, m5) <= 0) return;
ArraySetAsSeries(m5, true);
bool isLong = (state[s] == 1);
if(isLong)
{
if(m5[0].high > trailHigh[s]) trailHigh[s] = m5[0].high;
}
else
{
if(m5[0].low < trailLow[s]) trailLow[s] = m5[0].low;
}
double point = SymbolInfoDouble(syms[s], SYMBOL_POINT);
double minDist = SymbolInfoInteger(syms[s], SYMBOL_TRADE_STOPS_LEVEL) * point;
int digits = (int)SymbolInfoInteger(syms[s], SYMBOL_DIGITS);
// Not profitable enough yet to arm the trail
if(isLong && SymbolInfoDouble(syms[s], SYMBOL_BID) < entryPrice[s] + InpTrailActivateATR * atrVal) return;
if(!isLong && SymbolInfoDouble(syms[s], SYMBOL_ASK) > entryPrice[s] - InpTrailActivateATR * atrVal) return;
double price = isLong ? SymbolInfoDouble(syms[s], SYMBOL_BID)
: SymbolInfoDouble(syms[s], SYMBOL_ASK);
double slNew = isLong ? trailHigh[s] - InpTrailATR * atrVal
: trailLow[s] + InpTrailATR * atrVal;
slNew = NormalizeDouble(slNew, digits);
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(!PositionSelectByTicket(ticket)) continue;
if(PositionGetString(POSITION_SYMBOL) != syms[s]) continue;
if((int)PositionGetInteger(POSITION_MAGIC) != MAGIC) continue;
double slCur = PositionGetDouble(POSITION_SL);
// Only ever tighten, and keep out of the broker's minimum stop distance
if(isLong && slNew > slCur + point && slNew < price - minDist)
trade.PositionModify(ticket, slNew, 0);
if(!isLong && slNew < slCur - point && slNew > price + minDist)
trade.PositionModify(ticket, slNew, 0);
}
}
//==============================================================
// Utility Functions
//==============================================================
string PCTime()
{
MqlDateTime dt;
TimeToStruct(TimeLocal(), dt);
int h = dt.hour;
string ampm = (h >= 12) ? "PM" : "AM";
if(h == 0) h = 12;
else if(h > 12) h -= 12;
return IntegerToString(h) + ":" + StringFormat("%02d", dt.min) + " " + ampm;
}
//==============================================================
// Risk Management
//==============================================================
// Lot size that risks InpHighEquityRiskPct% of equity, split evenly across
// 'count' concurrent orders, if the ATR stop loss is hit on all of them.
// Falls back to a conservative fixed lot when the stop distance or the
// symbol's tick value/size aren't available (e.g. InpUseStopLoss = false).
double RiskBasedLots(string sym, double eq, double stopDist, int count)
{
double fallback = 0.10;
if(stopDist <= 0 || count <= 0) return fallback;
double tickValue = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE);
if(tickValue <= 0 || tickSize <= 0) return fallback;
double moneyPerLot = (stopDist / tickSize) * tickValue;
if(moneyPerLot <= 0) return fallback;
double riskMoney = eq * (InpHighEquityRiskPct / 100.0) / count;
double lots = riskMoney / moneyPerLot;
double lotStep = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
double lotMin = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
double lotMax = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
if(lotStep > 0) lots = MathFloor(lots / lotStep) * lotStep;
lots = MathMax(lotMin, MathMin(lotMax, lots));
return (lots > 0) ? lots : fallback;
}
void GetEquityRisk(string sym, double stopDist, int &count, double &lots)
{
double eq = AccountInfoDouble(ACCOUNT_EQUITY);
if(eq <= 30) { count = 2; lots = 0.10; }
else if(eq <= 50) { count = 2; lots = 0.10; }
else if(eq <= 70) { count = 4; lots = 0.10; }
else if(eq <= 100) { count = 4; lots = 0.10; }
else if(eq <= 130) { count = 6; lots = 0.10; }
else if(eq <= 150) { count = 8; lots = 0.10; }
else if(eq <= 170) { count = 10; lots = 0.10; }
else if(eq <= 200) { count = 6; lots = 0.20; }
else if(eq <= 300) { count = 4; lots = 0.30; }
else if(eq <= 400) { count = 6; lots = 0.30; }
else if(eq <= 500) { count = 6; lots = 0.30; }
else if(eq <= 600) { count = 8; lots = 0.30; }
else if(eq <= 1000) { count = 4; lots = 0.50; }
else if(eq <= 3000) { count = 4; lots = 0.30; }
else if(eq <= 5000) { count = 4; lots = 0.20; }
else if(eq <= 8000) { count = 4; lots = 0.10; }
else { count = 2; lots = RiskBasedLots(sym, eq, stopDist, count); }
}
//==============================================================
// Trading Functions
//==============================================================
bool SpreadOK(string sym)
{
if(InpMaxSpreadPoints <= 0) return true;
return SymbolInfoInteger(sym, SYMBOL_SPREAD) <= InpMaxSpreadPoints;
}
// ATR(M15) * multiplier, widened to the broker's minimum stop distance if
// needed. Returns false when the ATR value is unavailable so the caller
// skips the entry instead of trading unprotected.
bool GetStopDistance(int s, double &dist)
{
dist = 0.0;
double a[1];
if(CopyBuffer(atr[s], 0, 1, 1, a) <= 0 || a[0] <= 0) return false;
dist = a[0] * InpATRMultiplier;
double point = SymbolInfoDouble(syms[s], SYMBOL_POINT);
double minDist = SymbolInfoInteger(syms[s], SYMBOL_TRADE_STOPS_LEVEL) * point;
if(dist < minDist) dist = minDist;
return true;
}
double BuildStopLoss(int s, bool isBuy, double price, double dist)
{
int digits = (int)SymbolInfoInteger(syms[s], SYMBOL_DIGITS);
return NormalizeDouble(isBuy ? price - dist : price + dist, digits);
}
int OpenPositions(int s, bool isBuy, double dist, int count, double lots, double &firstFill)
{
string sym = syms[s];
trade.SetExpertMagicNumber(MAGIC);
int filled = 0;
firstFill = 0.0;
for(int i = 0; i < count; i++)
{
double price = isBuy ? SymbolInfoDouble(sym, SYMBOL_ASK)
: SymbolInfoDouble(sym, SYMBOL_BID);
double sl = InpUseStopLoss ? BuildStopLoss(s, isBuy, price, dist) : 0.0;
bool ok = isBuy ? trade.Buy(lots, sym, price, sl, 0, "Buy H1-M1")
: trade.Sell(lots, sym, price, sl, 0, "Sell H1-M1");
if(!ok) break; // out of margin or rejected — don't hammer the server
if(firstFill == 0.0) firstFill = price;
filled++;
}
return filled;
}
void ClosePositions(string sym)
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(!PositionSelectByTicket(ticket)) continue;
if(PositionGetString(POSITION_SYMBOL) == sym &&
(int)PositionGetInteger(POSITION_MAGIC) == MAGIC)
{
trade.PositionClose(ticket);
}
}
}
//==============================================================
// Main Loop
//==============================================================
void OnTick()
{
// Equity alert: fire only on a new H1 bar (CheckEquityAlert self-guards on day-of-week)
MqlRates h1[];
if(symsCount > 0 && CopyRates(syms[0], PERIOD_H1, 0, 1, h1) > 0 && h1[0].time != lastH1bar)
{
lastH1bar = h1[0].time;
CheckEquityAlert();
}
bool synced = false;
for(int s = 0; s < symsCount; s++)
{
// Per-symbol M1 bar gating — only act on a new closed M1 bar for this symbol
MqlRates m1[];
if(CopyRates(syms[s], PERIOD_M1, 0, 2, m1) < 2) continue;
ArraySetAsSeries(m1, true);
if(m1[1].time == lastM1bar[s]) continue;
lastM1bar[s] = m1[1].time;
// Sync position state once per tick on the first new M1 bar instead of
// rebuilding it on every single tick.
if(!synced) { SyncStateFromPositions(); synced = true; }
// Exit check: close all when M5 closes against direction across M5 kijun
if(state[s] != 0 && CheckM5Exit(s, state[s]))
{
string side = (state[s] == 1) ? "Long" : "Short";
string msg = PCTime() + " | Close " + syms[s] + " " + side + " (M5 kijun crossed)";
Print(msg); Alert(msg); SendNotification(msg);
ClosePositions(syms[s]);
state[s] = 0;
noReentryUntil[s] = TimeCurrent() + InpReentryCooldownSec;
}
// Chandelier trail: tighten stops behind the peak on each new M1 bar
if(state[s] != 0) ManageTrail(s);
// Entry check: all timeframes H1→M1 must align, spread must be sane
if(state[s] == 0 && TimeCurrent() >= noReentryUntil[s] && SpreadOK(syms[s]))
{
int st = CheckAllAlign(s);
if(st != 0)
{
bool isBuy = (st == 1);
double dist = 0.0;
if(InpUseStopLoss && !GetStopDistance(s, dist)) continue; // ATR unavailable — skip entry
int count; double lots;
GetEquityRisk(syms[s], dist, count, lots);
// Track state if any order filled — keeps exit logic and re-entry
// guard correct even when only some of the orders go through.
// Alert reports the actual fill count, not the requested count.
double firstFill = 0.0;
int filled = OpenPositions(s, isBuy, dist, count, lots, firstFill);
if(filled > 0)
{
state[s] = st;
entryPrice[s] = firstFill; // chandelier trail reference
trailHigh[s] = firstFill;
trailLow[s] = firstFill;
string action = isBuy ? "Buy" : "Sell";
string msg = PCTime() + " | " + action + " " + syms[s] +
" x" + IntegerToString(filled) +
" @ " + DoubleToString(lots, 2) + " (H1-M1)";
Print(msg); Alert(msg); SendNotification(msg);
}
else
Print(PCTime() + " | " + syms[s] + " entry signal but no order filled");
}
}
}
}
//This work is my worship unto GOD