685 lines
25 KiB
Plaintext
685 lines
25 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| Ichimoku H4-M1 Alignment EA (PO3) |
|
|
//| Entry: H4→M1 price+chikou all above/below tenkan,kijun,cloud, |
|
|
//| location-filtered by PO3 dealing ranges (room + bias) |
|
|
//| Exit: tiered TPs on PO3 levels for half the batch; remainder |
|
|
//| closes on M15 kijun cross or ATR-based protective stop |
|
|
//| 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 group "PO3 Dealing Ranges"
|
|
input bool InpUsePO3 = true; // Use PO3 dealing-range levels for TPs and entry filters
|
|
input double InpPO3Unit = 1.0; // Price per PO3 unit (1.0 = whole dollars on gold)
|
|
input int InpPO3BasePower = 4; // Base rung: 3^4 = 81 units
|
|
input int InpPO3StrongPower = 5; // Strong level: 3^5 = 243 units
|
|
input double InpPO3MinRR = 1.5; // Min reward:risk for a level to qualify as a TP
|
|
input double InpPO3MaxRR = 8.0; // Levels beyond this R are ignored (runner instead)
|
|
input double InpPO3BufferATR = 0.25; // Front-run TP buffer = ATR(M15) * this
|
|
input bool InpPO3RoomFilter = true; // Skip entries without MinRR room to next strong level
|
|
input bool InpPO3BiasFilter = true; // Block entries against a recent major-level rejection
|
|
input int InpPO3BiasPower = 6; // Major level for the bias filter: 3^6 = 729 units
|
|
input int InpPO3BiasBars = 180; // H4 bars scanned for a major-level rejection
|
|
input double InpPO3BiasTolFrac = 0.4; // Rejection tag tolerance, fraction of the base rung
|
|
|
|
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 6
|
|
#define IDX_M15 3 // index of M15 in tfs[] — used for exit check
|
|
|
|
ENUM_TIMEFRAMES tfs[TF_COUNT] = {
|
|
PERIOD_H4, PERIOD_H1, PERIOD_M30, PERIOD_M15, PERIOD_M5, PERIOD_M1
|
|
};
|
|
|
|
int ich[MAX_SYMS][TF_COUNT];
|
|
int atr[MAX_SYMS];
|
|
string syms[MAX_SYMS];
|
|
int symsCount = 0;
|
|
datetime lastM1bar[MAX_SYMS];
|
|
datetime lastH4bar = 0;
|
|
int state[MAX_SYMS]; // 0=no position, 1=long, -1=short
|
|
string lastBlock[MAX_SYMS]; // last PO3 block reason printed, per symbol (anti-spam)
|
|
|
|
int MAGIC = 20260502; // distinct from the pre-PO3 H4-M1 EA so both can run on one account
|
|
|
|
#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;
|
|
lastBlock[s] = "";
|
|
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);
|
|
}
|
|
}
|
|
|
|
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]);
|
|
}
|
|
}
|
|
|
|
//==============================================================
|
|
// 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.
|
|
for(int s = 0; s < symsCount; s++) state[s] = 0;
|
|
|
|
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; break; }
|
|
}
|
|
}
|
|
}
|
|
|
|
//==============================================================
|
|
// 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];
|
|
|
|
MqlRates rt[];
|
|
if(CopyRates(syms[s], tf, 0, 120, rt) <= 0) return 0;
|
|
ArraySetAsSeries(rt, true);
|
|
|
|
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
|
|
|
|
if(ArraySize(rt) <= chCloud) return 0;
|
|
|
|
double tenkan[1], kijun[1], senA[1], senB[1];
|
|
double tenkan_ch[1], kijun_ch[1], senA_ch[1], senB_ch[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;
|
|
// tenkan, kijun, cloud at chikou's chart position
|
|
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;
|
|
|
|
double closeP = rt[sh].close;
|
|
// 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 cHi = MathMax(senA[0], senB[0]);
|
|
double cLo = MathMin(senA[0], senB[0]);
|
|
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(closeP > tenkan[0] && closeP > kijun[0] && closeP > cHi &&
|
|
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(closeP < tenkan[0] && closeP < kijun[0] && closeP < cLo &&
|
|
chik < rt[chShift].low &&
|
|
chik < tenkan_ch[0] && chik < kijun_ch[0] && chik < cLoC) return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
//==============================================================
|
|
// Entry Check: all timeframes (H4→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: M15 price closed on wrong side of M15 kijun
|
|
//==============================================================
|
|
|
|
bool CheckM15Exit(int s, int dir)
|
|
{
|
|
double kij[1];
|
|
if(CopyBuffer(ich[s][IDX_M15], 1, 1, 1, kij) <= 0) return false;
|
|
|
|
MqlRates rt[];
|
|
if(CopyRates(syms[s], PERIOD_M15, 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;
|
|
}
|
|
|
|
//==============================================================
|
|
// 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); }
|
|
}
|
|
|
|
//==============================================================
|
|
// PO3 Dealing Ranges (Hopiplaka)
|
|
// Levels are fixed multiples of 3^n price units — on gold the 81
|
|
// grid is ...3888, 3969, 4050... with every level whose multiple
|
|
// carries a higher power of 3 outranking its neighbours (3888 =
|
|
// 16*3^5 is a 243-grade level, 4374 = 2*3^7 a 2187-grade one).
|
|
// floor(price/step)*step bounds the current dealing range; its
|
|
// midpoint is equilibrium, the lower half discount, the upper
|
|
// half premium. Ichimoku alignment decides WHEN to trade; PO3
|
|
// decides WHERE it's worth trading and HOW FAR to hold.
|
|
//==============================================================
|
|
|
|
double PO3Step(int power)
|
|
{
|
|
return MathPow(3.0, power) * InpPO3Unit;
|
|
}
|
|
|
|
// First multiple of `step` strictly beyond `price` in direction `dir`
|
|
double PO3NextLevel(double price, int dir, double step)
|
|
{
|
|
double k = MathFloor(price / step + 1e-9);
|
|
if(dir > 0) return (k + 1.0) * step;
|
|
return (k * step < price - 1e-9) ? k * step : (k - 1.0) * step;
|
|
}
|
|
|
|
// Walk levels of size 3^sigPower beyond `from` in direction `dir` and
|
|
// return the first whose buffered reward lands in [MinRR, MaxRR] * the
|
|
// stop distance — past `beyond`, when given, so TP2 clears TP1's level.
|
|
// Returns 0 when nothing qualifies: that order stays a kijun-trailed runner.
|
|
double PO3PickTarget(double from, int dir, int sigPower, double stopDist,
|
|
double buffer, double beyond)
|
|
{
|
|
double step = PO3Step(sigPower);
|
|
double lvl = PO3NextLevel(from, dir, step);
|
|
for(int i = 0; i < 64; i++, lvl += dir * step)
|
|
{
|
|
if(beyond > 0 && ((dir > 0) ? (lvl <= beyond + 1e-9)
|
|
: (lvl >= beyond - 1e-9))) continue;
|
|
double reward = MathAbs(lvl - from) - buffer;
|
|
if(reward > InpPO3MaxRR * stopDist) return 0.0;
|
|
if(reward < InpPO3MinRR * stopDist) continue;
|
|
return lvl;
|
|
}
|
|
return 0.0;
|
|
}
|
|
|
|
// Premium/discount position inside the strong-grade dealing range
|
|
string PO3RangeInfo(double price)
|
|
{
|
|
double step = PO3Step(InpPO3StrongPower);
|
|
double lo = MathFloor(price / step + 1e-9) * step;
|
|
double pct = (price - lo) / step * 100.0;
|
|
string zone = (pct > 55.0) ? "premium" : ((pct < 45.0) ? "discount" : "equilibrium");
|
|
return StringFormat("PO3 %g[%g-%g] %.0f%% %s", step, lo, lo + step, pct, zone);
|
|
}
|
|
|
|
// The first strong-grade level ahead is where the move is most likely to
|
|
// stall — entering with less than MinRR of room to it is a poor-geometry
|
|
// trade regardless of how clean the Ichimoku alignment looks.
|
|
bool PO3RoomOK(double entry, int dir, double stopDist, double buffer)
|
|
{
|
|
double lvl = PO3NextLevel(entry, dir, PO3Step(InpPO3StrongPower));
|
|
double room = MathAbs(lvl - entry) - buffer;
|
|
return room >= InpPO3MinRR * stopDist;
|
|
}
|
|
|
|
// Detects price "acting off" a major PO3 level: when the H4 lookback
|
|
// extreme tagged (or raided) a 3^BiasPower-grade level and price has since
|
|
// been rejected back away from it, only trades away from that level are
|
|
// allowed. Returns -1 (shorts only), +1 (longs only), or 0 (no bias).
|
|
// A rejection stops binding as soon as price reclaims the level, and a
|
|
// newer opposite-side tag supersedes an older one.
|
|
int PO3Bias(int s)
|
|
{
|
|
double step = PO3Step(InpPO3BiasPower);
|
|
double tol = InpPO3BiasTolFrac * PO3Step(InpPO3BasePower);
|
|
|
|
MqlRates rt[];
|
|
int n = CopyRates(syms[s], PERIOD_H4, 1, InpPO3BiasBars, rt);
|
|
if(n <= 0) return 0;
|
|
ArraySetAsSeries(rt, true);
|
|
|
|
int hhI = 0, llI = 0;
|
|
for(int i = 1; i < n; i++)
|
|
{
|
|
if(rt[i].high > rt[hhI].high) hhI = i;
|
|
if(rt[i].low < rt[llI].low) llI = i;
|
|
}
|
|
double close0 = rt[0].close;
|
|
|
|
double hLvl = MathRound(rt[hhI].high / step) * step;
|
|
bool hTag = (MathAbs(rt[hhI].high - hLvl) <= tol) && (close0 < hLvl - tol);
|
|
|
|
double lLvl = MathRound(rt[llI].low / step) * step;
|
|
bool lTag = (MathAbs(rt[llI].low - lLvl) <= tol) && (close0 > lLvl + tol);
|
|
|
|
if(hTag && lTag) return (hhI < llI) ? -1 : 1; // series order: lower index = newer
|
|
if(hTag) return -1;
|
|
if(lTag) return 1;
|
|
return 0;
|
|
}
|
|
|
|
// Print a block reason once per streak instead of once per M1 bar
|
|
void BlockNote(int s, string reason)
|
|
{
|
|
string msg = syms[s] + ": entry blocked (" + reason + ")";
|
|
if(msg == lastBlock[s]) return;
|
|
lastBlock[s] = msg;
|
|
Print(msg);
|
|
}
|
|
|
|
//==============================================================
|
|
// Trading Functions
|
|
//==============================================================
|
|
|
|
bool SpreadOK(string sym)
|
|
{
|
|
if(InpMaxSpreadPoints <= 0) return true;
|
|
return SymbolInfoInteger(sym, SYMBOL_SPREAD) <= InpMaxSpreadPoints;
|
|
}
|
|
|
|
bool GetATRValue(int s, double &val)
|
|
{
|
|
val = 0.0;
|
|
if(atr[s] == INVALID_HANDLE) return false;
|
|
double a[1];
|
|
if(CopyBuffer(atr[s], 0, 1, 1, a) <= 0 || a[0] <= 0) return false;
|
|
val = a[0];
|
|
return true;
|
|
}
|
|
|
|
// 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 = 0.0;
|
|
if(!GetATRValue(s, a)) return false;
|
|
|
|
dist = a * 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);
|
|
}
|
|
|
|
// Drop a TP that sits on the wrong side of price or inside the broker's
|
|
// minimum stop distance — such a level failed the RR gate anyway.
|
|
double NormalizeTP(int s, bool isBuy, double price, double tp)
|
|
{
|
|
if(tp <= 0) return 0.0;
|
|
if((isBuy && tp <= price) || (!isBuy && tp >= price)) return 0.0;
|
|
|
|
double point = SymbolInfoDouble(syms[s], SYMBOL_POINT);
|
|
double minDist = SymbolInfoInteger(syms[s], SYMBOL_TRADE_STOPS_LEVEL) * point;
|
|
if(MathAbs(tp - price) < minDist) return 0.0;
|
|
|
|
int digits = (int)SymbolInfoInteger(syms[s], SYMBOL_DIGITS);
|
|
return NormalizeDouble(tp, digits);
|
|
}
|
|
|
|
// Alternate the batch between the two PO3 target tiers: even-indexed
|
|
// orders bank at TP1 (nearest qualifying base rung), odd-indexed at TP2
|
|
// (nearest qualifying strong level beyond it). A zero TP leaves that
|
|
// order as a runner managed by the M15 kijun exit.
|
|
int OpenPositions(int s, bool isBuy, double dist, int count, double lots,
|
|
double tp1, double tp2)
|
|
{
|
|
string sym = syms[s];
|
|
|
|
trade.SetExpertMagicNumber(MAGIC);
|
|
|
|
int filled = 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;
|
|
double tp = NormalizeTP(s, isBuy, price, (i % 2 == 0) ? tp1 : tp2);
|
|
|
|
bool ok = isBuy ? trade.Buy(lots, sym, price, sl, tp, "Buy H4-M1")
|
|
: trade.Sell(lots, sym, price, sl, tp, "Sell H4-M1");
|
|
if(!ok) break; // out of margin or rejected — don't hammer the server
|
|
filled++;
|
|
}
|
|
return filled;
|
|
}
|
|
|
|
// Ichimoku said "go" — PO3 now decides whether the location is worth it
|
|
// (bias + room filters) and supplies the tiered targets before the batch
|
|
// is opened.
|
|
void TryEnter(int s, int st)
|
|
{
|
|
bool isBuy = (st == 1);
|
|
string sym = syms[s];
|
|
|
|
double dist = 0.0;
|
|
if(InpUseStopLoss && !GetStopDistance(s, dist)) return; // ATR unavailable — skip entry
|
|
|
|
// PO3 RR gating needs a stop distance to measure reward against; without
|
|
// one (InpUseStopLoss off) only the bias filter stays active.
|
|
double atrVal = 0.0, buffer = 0.0;
|
|
bool po3 = InpUsePO3 && dist > 0 && GetATRValue(s, atrVal);
|
|
if(po3) buffer = atrVal * InpPO3BufferATR;
|
|
|
|
double entry = isBuy ? SymbolInfoDouble(sym, SYMBOL_ASK)
|
|
: SymbolInfoDouble(sym, SYMBOL_BID);
|
|
|
|
if(InpUsePO3 && InpPO3BiasFilter)
|
|
{
|
|
int bias = PO3Bias(s);
|
|
if(bias != 0 && bias != st)
|
|
{
|
|
BlockNote(s, "against major PO3 level rejection");
|
|
return;
|
|
}
|
|
}
|
|
if(po3 && InpPO3RoomFilter && !PO3RoomOK(entry, st, dist, buffer))
|
|
{
|
|
BlockNote(s, "no room to next strong PO3 level");
|
|
return;
|
|
}
|
|
lastBlock[s] = "";
|
|
|
|
double tp1 = 0.0, tp2 = 0.0;
|
|
if(po3)
|
|
{
|
|
double lvl1 = PO3PickTarget(entry, st, InpPO3BasePower, dist, buffer, 0.0);
|
|
double lvl2 = PO3PickTarget(entry, st, InpPO3StrongPower, dist, buffer, lvl1);
|
|
if(lvl1 > 0) tp1 = lvl1 - st * buffer; // front-run the level
|
|
if(lvl2 > 0) tp2 = lvl2 - st * buffer;
|
|
}
|
|
|
|
int count; double lots;
|
|
GetEquityRisk(sym, dist, count, lots);
|
|
|
|
string action = isBuy ? "Buy" : "Sell";
|
|
string msg = PCTime() + " | " + action + " " + sym +
|
|
" x" + IntegerToString(count) +
|
|
" @ " + DoubleToString(lots, 2) + " (H4-M1)";
|
|
if(InpUsePO3)
|
|
{
|
|
msg += " | " + PO3RangeInfo(entry) +
|
|
" | TP1 " + (tp1 > 0 ? DoubleToString(tp1, 2) : "runner") +
|
|
" TP2 " + (tp2 > 0 ? DoubleToString(tp2, 2) : "runner");
|
|
}
|
|
Print(msg); Alert(msg); SendNotification(msg);
|
|
|
|
// Track state if any order filled — keeps exit logic and re-entry
|
|
// guard correct even when only some of the orders go through.
|
|
if(OpenPositions(s, isBuy, dist, count, lots, tp1, tp2) > 0)
|
|
state[s] = st;
|
|
}
|
|
|
|
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 H4 bar (CheckEquityAlert self-guards on day-of-week)
|
|
MqlRates h4[];
|
|
if(symsCount > 0 && CopyRates(syms[0], PERIOD_H4, 0, 1, h4) > 0 && h4[0].time != lastH4bar)
|
|
{
|
|
lastH4bar = h4[0].time;
|
|
CheckEquityAlert();
|
|
}
|
|
|
|
SyncStateFromPositions();
|
|
|
|
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;
|
|
|
|
// Exit check: close all when M15 closes against direction across M15 kijun
|
|
if(state[s] != 0 && CheckM15Exit(s, state[s]))
|
|
{
|
|
string side = (state[s] == 1) ? "Long" : "Short";
|
|
string msg = PCTime() + " | Close " + syms[s] + " " + side + " (M15 kijun crossed)";
|
|
Print(msg); Alert(msg); SendNotification(msg);
|
|
|
|
ClosePositions(syms[s]);
|
|
state[s] = 0;
|
|
}
|
|
|
|
// Entry check: all timeframes H4→M1 must align, spread must be sane,
|
|
// and the PO3 location filters must approve before orders go out.
|
|
if(state[s] == 0 && SpreadOK(syms[s]))
|
|
{
|
|
int st = CheckAllAlign(s);
|
|
if(st != 0) TryEnter(s, st);
|
|
}
|
|
}
|
|
}
|
|
//This work is my worship unto GOD
|