//+------------------------------------------------------------------+ //| Expert.mqh | //| Copyright 2000-2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #include #include "ExpertSignalCustom.mqh" #include "..\System\TradeChecks.mqh" //--- Article 2555 #16: a soft budget for one OnTick() pass, in microseconds. Diagnostic only - it //--- never skips or truncates any work. #define EXPERT_TICK_BUDGET_US 100000 //--- Article 2555 #16: soft ceiling, in MB, on MQL_MEMORY_USED before a throttled warning is logged. //--- Sized well above what the largest shipped AI topology needs (3 models x live+shadow net under //--- HYBRID) so it flags a genuine leak/runaway allocation, not ordinary training memory. #define EXPERT_MEMORY_SOFT_LIMIT_MB 1024 //+------------------------------------------------------------------+ //| Class CExpert. | //| Purpose: Base class expert advisor. | //| Derives from class CExpert. | //+------------------------------------------------------------------+ class CExpertCustom : public CExpert { private: //--- Last value handed to the live signal by PublishVoteThreshold(), -1 = still on the input seed. int m_publishedVoteThreshold; //--- WHICH BOOK THE CURRENT CALL IS ACTING FOR. Read by SelectPosition() to decide which magic to //--- select on, so every inherited CExpert method that operates on "the" position - CheckClose, //--- CheckTrailingStop, CloseLong/Short - transparently operates on this book's position instead. //--- Meaningless (and always true) when WarriorHedgingActive() is false. Always restored to true //--- at the end of the call that moved it: nothing outside a book loop may observe it as false. bool m_activeBookLong; protected: CExpertSignalCustom* GetCustomSignal() { return dynamic_cast(m_signal); } //--- CloseAndDeleteAllForSymbol()'s position-close and order-delete loops shared the same //--- select/filter/freeze-check/act shape (Article 2555 #7); CLOSEALL_POSITION/CLOSEALL_ORDER //--- dispatch the one differing step-set inside a single loop rather than two copies of the loop. enum ENUM_CLOSEALL_TARGET { CLOSEALL_POSITION, CLOSEALL_ORDER }; bool CloseAllLoop(ENUM_CLOSEALL_TARGET target, string symbol, string caller); bool CloseAndDeleteAllForSymbol(void); //--- The closing half of Processing(), runnable on a tick Refresh() has declined. See its own //--- definition for why an exit may run on data an entry may not. bool ProtectOpenPosition(void); //--- Article 2555 #7 - SYMBOL_TRADE_FREEZE_LEVEL. These two report on the CURRENTLY SELECTED //--- m_position / m_order, which is the state every caller below runs in. bool SelectedPositionIsFrozen(void); bool SelectedOrderIsFrozen(void); //--- Mirrors CExpertTrade::Buy()/Sell()'s price-vs-stops-level routing, so the checks below know //--- whether a given entry price is about to become a pending order or a market fill. ENUM_ORDER_TYPE ResolveOrderType(bool isLong, double price); //--- Shared isLong-parameterized bodies for the pre-send gates below - each pair of virtual //--- overrides (OpenLong/Short, TrailingStopLong/Short, TrailingOrderLong/Short) differs only in //--- which order-type constant applies and which CExpert::Xxx base method to delegate to at the //--- end. caller is passed through as __FUNCTION__ from each wrapper so logged messages keep //--- their original per-direction function name. bool OpenPosition(bool isLong, double price, double sl, double tp, string caller); bool TrailingStopCommon(bool isLong, double sl, double tp, string caller); bool TrailingOrderCommon(bool isLong, double delta, string caller); //--- Owns the bar watermark that enforces Expert_EveryTick - see Processing(). Its own instance //--- rather than a shared/static one, so no other consumer can swallow the transition. CNewBar m_newBar; public: CExpertCustom(void); ~CExpertCustom(void); //--- event handlers virtual void OnTick(void) override; virtual void OnTimer(void) override; virtual void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) override; //--- THE DERIVED VOTE THRESHOLD, PUBLISHED TO THE LIVE SIGNAL. The ensemble's era verdict derives //--- the rung it certifies at (THE DERIVED THRESHOLD, Expert\AIBase\Training.mqh); this is the //--- one route by which that number reaches the m_threshold_open that CheckOpenLong/CheckOpenShort //--- actually test. Without it the gate would certify at one threshold while the EA traded at //--- another - the certified!=traded defect 2c443ba was written to end, and the reason the //--- threshold could not simply stay an input: MT5 stores inputs PER CHART, so a value corrected //--- in source never reaches an already-attached EA. //--- Idempotent: an int compare on the common path, and it announces only on a real change. void PublishVoteThreshold(const int threshold) { if(threshold <= 0 || threshold == m_publishedVoteThreshold) return; CExpertSignalCustom *root = GetCustomSignal(); if(root == NULL) return; root.ThresholdOpen(threshold); //--- The exit tracks the entry when it is armed at all. One derived number governs both, so the //--- close can never drift onto a threshold the gate never scored. root.ThresholdClose(Exit_On_Reversal_Vote ? threshold : VOTE_EXIT_DISABLED_THRESHOLD); PrintFormat("%s: vote threshold now %d%% (derived from the era verdict, was %s); exit on" " opposite vote %s", __FUNCTION__, threshold, (m_publishedVoteThreshold < 0 ? "the Signal_ThresholdOpen seed" : IntegerToString(m_publishedVoteThreshold) + "%"), (Exit_On_Reversal_Vote ? StringFormat("ARMED at the same %d%%", threshold) : "OFF (hold to the barrier)")); m_publishedVoteThreshold = threshold; } //--- CONTROL PANEL -> SIGNAL TREE. GetCustomSignal() is protected on purpose, so the EA asks the //--- expert rather than reaching inside it - see CExpertSignalCustom::DispatchSignalCommand. int DispatchSignalCommand(const ENUM_SIGNAL_COMMAND cmd) { CExpertSignalCustom *root = GetCustomSignal(); return (root == NULL) ? 0 : root.DispatchSignalCommand(cmd); } int CountSignalTrait(const ENUM_SIGNAL_TRAIT trait) { CExpertSignalCustom *root = GetCustomSignal(); return (root == NULL) ? 0 : root.CountSignalTrait(trait); } //--- initialization trading objects virtual bool InitSignal(CExpertSignal *signal = NULL) override; //--- methods of creating the indicator and timeseries virtual bool SetPriceSeries(CiOpen *open, CiHigh *high, CiLow *low, CiClose *close) override; virtual bool SetOtherSeries(CiSpread *spread, CiTime *time, CiTickVolume *tick_volume, CiRealVolume *real_volume) override; //--- initialization trading objects virtual bool InitTrade(ulong magic, CExpertTrade *trade = NULL) override; //--- FILTERED-VIEW historical overlay, forwarded to the aggregate signal. Exposed here rather //--- than by handing Warrior_EA.mq5 the signal pointer: GetCustomSignal() is protected on //--- purpose, and the EA has no other business reaching inside the expert to reach the root. void ArmFilteredOverlay(void) { CExpertSignalCustom *s = GetCustomSignal(); if(s != NULL) s.StartFilteredOverlay(); } bool AdvanceFilteredOverlay(const int barBudget) { CExpertSignalCustom *s = GetCustomSignal(); return (s != NULL) ? s.AdvanceFilteredOverlay(barBudget) : false; } //--- Timer-driven vote-readout repaint, forwarded to the aggregate - same access reasoning as //--- the overlay wrappers above. void RefreshVoteReadout(void) { CExpertSignalCustom *s = GetCustomSignal(); if(s != NULL) s.RefreshVoteReadout(); } bool FilteredOverlayPending(void) { CExpertSignalCustom *s = GetCustomSignal(); return (s != NULL) ? s.FilteredOverlayPending() : false; } //--- One-shot harvest of a completed sweep's combined-vote score - see //--- CExpertSignalCustom::TakeOverlayVoteScore for the consuming-read contract. bool TakeOverlayVoteScore(long &fired, long &wins) { CExpertSignalCustom *s = GetCustomSignal(); return (s != NULL) ? s.TakeOverlayVoteScore(fired, wins) : false; } protected: //--- refreshing virtual bool Refresh(void) override; //--- processing (main method) virtual bool Processing(void) override; //--- THE TWO-BOOK POSITION SELECTOR. Stock CExpert selects one position per (symbol, magic) and //--- CheckOpen() only runs when that finds nothing - which is exactly why the stock framework can //--- never hold a long and a short at once. Overriding this to select on the ACTIVE BOOK's magic //--- turns the one state machine into two independent ones without touching any of the inherited //--- open/close/trail logic that runs on top of it. virtual bool SelectPosition(void) override; //--- Processing()'s two-book path. See its definition for why there is no CheckReverse() in it. bool ProcessBooks(void); //--- True when this SIDE already has a pending order outstanding, having first given that order //--- its delete/trail maintenance. Preserves the stock invariant that a side with a pending order //--- does not stack a second one - which the stock code got for free by returning early from a //--- single shared block, and which a per-side loop has to state explicitly. bool SideHasPendingOrder(const bool longSide); //--- Article 2555 #2/#3/#4/#5/#6 - final gate immediately before the order is sent. This is the //--- last point at which a bad request can still be withheld rather than rejected by the server. virtual bool OpenLong(double price, double sl, double tp) override; virtual bool OpenShort(double price, double sl, double tp) override; //--- Article 2555 #7 - never send a close/reverse request for a frozen position. virtual bool CheckClose(void) override; virtual bool CheckReverse(void) override; //--- Article 2555 #7/#11 - never send a modification for a frozen position, and never send one //--- that changes nothing (TRADE_RETCODE_NO_CHANGES = 10025). virtual bool CheckTrailingStop(void) override; virtual bool TrailingStopLong(double sl, double tp) override; virtual bool TrailingStopShort(double sl, double tp) override; //--- Article 2555 #7/#11 - the same two rules for pending orders. virtual bool CheckDeleteOrderLong(void) override; virtual bool CheckDeleteOrderShort(void) override; virtual bool CheckTrailingOrderLong(void) override; virtual bool CheckTrailingOrderShort(void) override; virtual bool TrailingOrderLong(double delta) override; virtual bool TrailingOrderShort(double delta) override; }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CExpertCustom::CExpertCustom(void) : m_publishedVoteThreshold(-1), m_activeBookLong(true) { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CExpertCustom::~CExpertCustom(void) { } //+------------------------------------------------------------------+ //| Setting pointers of price timeseries. | //+------------------------------------------------------------------+ bool CExpertCustom::SetPriceSeries(CiOpen *open, CiHigh *high, CiLow *low, CiClose *close) override { //--- check the initialization phase if(m_init_phase != INIT_PHASE_VALIDATION) { return(false); } //--- check pointers if((IS_OPEN_SERIES_USAGE && open == NULL) || (IS_HIGH_SERIES_USAGE && high == NULL) || (IS_LOW_SERIES_USAGE && low == NULL) || (IS_CLOSE_SERIES_USAGE && close == NULL)) { Print(__FUNCTION__ + ": NULL pointer"); return(false); } m_open = open; m_high = high; m_low = low; m_close = close; //--- ok return(true); } //+------------------------------------------------------------------+ //| Setting pointers of other timeseries. | //+------------------------------------------------------------------+ bool CExpertCustom::SetOtherSeries(CiSpread *spread, CiTime *time, CiTickVolume *tick_volume, CiRealVolume *real_volume) override { //--- check the initialization phase if(m_init_phase != INIT_PHASE_VALIDATION) { return(false); } //--- check pointers if((IS_SPREAD_SERIES_USAGE && spread == NULL) || (IS_TIME_SERIES_USAGE && time == NULL) || (IS_TICK_VOLUME_SERIES_USAGE && tick_volume == NULL) || (IS_REAL_VOLUME_SERIES_USAGE && real_volume == NULL)) { Print(__FUNCTION__ + ": NULL pointer"); return(false); } m_spread = spread; m_time = time; m_tick_volume = tick_volume; m_real_volume = real_volume; //--- ok return(true); } //+------------------------------------------------------------------+ //| Initialization signal object | //+------------------------------------------------------------------+ bool CExpertCustom::InitSignal(CExpertSignal *signal) { if(m_signal != NULL) delete m_signal; //--- if(signal == NULL) { if((m_signal = new CExpertSignalCustom) == NULL) return(false); } else m_signal = signal; //--- initializing signal object if(!m_signal.Init(GetPointer(m_symbol), m_period, m_adjusted_point)) return(false); m_signal.EveryTick(m_every_tick); m_signal.Magic(m_magic); //--- ok return(true); } //+------------------------------------------------------------------+ //| Refreshing data for processing | //+------------------------------------------------------------------+ bool CExpertCustom::Refresh(void) { MqlDateTime time; //--- Article 2555 #8 - errors caused by insufficient quote history. string history_reason; if(!TCHasEnoughHistory(m_symbol.Name(), m_period, 2, history_reason)) { TCLog("refresh-history:" + m_symbol.Name(), __FUNCTION__ + ": skipping tick - " + history_reason); return(false); } //--- refresh rates if(!m_symbol.RefreshRates()) return(false); //--- check need processing TimeToStruct(m_symbol.Time(), time); if(m_period_flags != WRONG_VALUE && m_period_flags != 0) if((m_period_flags & TimeframesFlags(time)) == 0) return(false); m_last_tick_time = time; //--- refresh indicators m_indicators.Refresh(); //--- ok return(true); } //+------------------------------------------------------------------+ //| Main function | //+------------------------------------------------------------------+ bool CExpertCustom::Processing(void) { //--- EXPERT_EVERYTICK, ENFORCED. This is the one place it can be enforced without breaking //--- something: SetDirection() is what drives CExpertSignalCustom::Direction(), which runs the NN //--- forward passes, journals DB rows, draws arrows and consumes one-shot vote state - a //--- TRANSACTION, not a query, and re-running it on every quote of a 4-hour bar repeats all of it. //--- With the input off, the direction is computed once per bar and every intrabar tick below reuses //--- the value already in m_signal. //--- INVARIANT - DO NOT MOVE AN EXIT BELOW THIS GATE. Expert_EveryTick throttles how often the EA //--- forms an OPINION. It must never throttle how often the EA can act on a position it already //--- holds. Everything after this line therefore runs on every tick regardless of the input: //--- CheckReverse / CheckClose / CheckTrailingStop and the pending-order maintenance. A stop that //--- only trails at bar boundaries is a different (worse) strategy, not a faster one, and on H4 it //--- would leave a position unmanaged for four hours at a time. //--- Two other exit paths are deliberately outside this function for the same reason: the scheduled //--- close-all in CExpertCustom::OnTick() (this file, the targetHour block) runs BEFORE Refresh() and //--- matches a +-1 MINUTE window, so a bar-gated check on H4 would step straight over it; and //--- ProtectOpenPosition() covers the ticks Refresh() declines outright. if(m_every_tick || m_newBar.IsNewBar(m_symbol.Name(), m_period)) m_signal.SetDirection(); //--- TWO BOOKS. Everything below this line is the original single-position path, kept verbatim for //--- netting accounts and for Allow_Hedging=false - on a netting account a second book is not a //--- policy choice the EA gets to make, it is arithmetically impossible. if(WarriorHedgingActive()) return(ProcessBooks()); //--- check if open positions if(SelectPosition()) { //--- open position is available //--- check the possibility of reverse the position if(CheckReverse()) return(true); //--- check the possibility of closing the position/delete pending orders if(!CheckClose()) { //--- check the possibility of modifying the position if(CheckTrailingStop()) return(true); //--- return without operations return(false); } } //--- check if plased pending orders int total = OrdersTotal(); if(total != 0) { for(int i = total - 1; i >= 0; i--) { m_order.SelectByIndex(i); if(m_order.Symbol() != m_symbol.Name()) continue; if(m_order.OrderType() == ORDER_TYPE_BUY_LIMIT || m_order.OrderType() == ORDER_TYPE_BUY_STOP) { //--- check the ability to delete a pending order to buy if(CheckDeleteOrderLong()) return(true); //--- check the possibility of modifying a pending order to buy if(CheckTrailingOrderLong()) return(true); } else { //--- check the ability to delete a pending order to sell if(CheckDeleteOrderShort()) return(true); //--- check the possibility of modifying a pending order to sell if(CheckTrailingOrderShort()) return(true); } //--- return without operations return(false); } } //--- check the possibility of opening a position/setting pending order if(CheckOpen()) return(true); //--- return without operations return(false); } //+------------------------------------------------------------------+ //| THE TWO-BOOK PATH. | //| | //| One long book and one short book on this symbol, each with at | //| most one position, each opened on its own side's vote and each | //| held to its own barrier. | //| | //| THERE IS NO CheckReverse() HERE, AND THAT IS THE POINT. A reverse | //| is a vote-driven exit with an entry stapled on: it closes a | //| position the deploy gate certified TO ITS BARRIER, so the | //| realised outcome stops being the labelled one and the certified | //| precision stops describing what was traded. With two books the | //| opposite vote simply opens the other side - the new signal is | //| acted on and the old position keeps the certification it was | //| deployed under. It costs no more than reversing would: both pay | //| the new side's spread, and the difference is only that the | //| existing position runs on to a barrier already measured as | //| positive-expectancy. | //| | //| Both books are always visited. An early return on the first one | //| to act would let a busy long book starve the short book of its | //| exit checks for as long as it kept acting - the same class of bug | //| as ProtectOpenPosition()'s: an exit must never be skipped because | //| something else happened first. | //+------------------------------------------------------------------+ bool CExpertCustom::ProcessBooks(void) { bool acted = false; for(int i = 0; i < 2; i++) { m_activeBookLong = (i == 0); //--- The order this book's trades are stamped with. Set before ANY request, so a close is //--- attributed to the book that owns it and not to whichever book ran last. if(CheckPointer(m_trade) != POINTER_INVALID) m_trade.SetExpertMagicNumber(WarriorBookMagic(m_activeBookLong)); if(SelectPosition()) { //--- Same precedence as the single-book path: a close wins over a trail, and a closed //--- position has no stop left to move. if(CheckClose()) acted = true; else if(CheckTrailingStop()) acted = true; continue; } //--- No position in this book. A side that already has a pending order must not stack a //--- second one - the stock path got that from a shared early return this loop does not have. if(SideHasPendingOrder(m_activeBookLong)) { acted = true; continue; } if(m_activeBookLong ? CheckOpenLong() : CheckOpenShort()) acted = true; } //--- RESTORED. Nothing outside this loop may observe a book other than the long one: SelectPosition() //--- is called from paths that know nothing about books (the stock CExpert internals among them), and //--- leaving the short book active would silently redirect them. m_activeBookLong = true; if(CheckPointer(m_trade) != POINTER_INVALID) m_trade.SetExpertMagicNumber(WarriorBookMagic(true)); return(acted); } //+------------------------------------------------------------------+ //| Select the ACTIVE BOOK's position. Falls straight through to the | //| stock selector whenever the two-book mode is not live, so the | //| netting path is bit-for-bit what it always was. | //+------------------------------------------------------------------+ bool CExpertCustom::SelectPosition(void) { if(!WarriorHedgingActive()) return(CExpert::SelectPosition()); return(m_position.SelectByMagic(m_symbol.Name(), WarriorBookMagic(m_activeBookLong))); } //+------------------------------------------------------------------+ //| Pending-order maintenance for ONE side. | //+------------------------------------------------------------------+ bool CExpertCustom::SideHasPendingOrder(const bool longSide) { for(int i = OrdersTotal() - 1; i >= 0; i--) { if(!m_order.SelectByIndex(i)) continue; if(m_order.Symbol() != m_symbol.Name()) continue; //--- Magic-filtered, which the stock block was not: with two books live, an order belonging to //--- the other side's book must not be read as this side's outstanding order. if(!WarriorOwnsMagic((long)m_order.Magic())) continue; bool isLongOrder = (m_order.OrderType() == ORDER_TYPE_BUY_LIMIT || m_order.OrderType() == ORDER_TYPE_BUY_STOP); if(isLongOrder != longSide) continue; if(longSide) { if(CheckDeleteOrderLong() || CheckTrailingOrderLong()) return(true); } else { if(CheckDeleteOrderShort() || CheckTrailingOrderShort()) return(true); } //--- Untouched but still outstanding. Reported as present, because the caller's question is //--- "may this side open?" and the answer is no either way. return(true); } return(false); } //+------------------------------------------------------------------+ //| Article 2555 #7 - is the SELECTED position inside the broker's | //| freeze zone? A frozen position cannot be modified or closed. | //+------------------------------------------------------------------+ bool CExpertCustom::SelectedPositionIsFrozen(void) { string reason; if(TCFreezeOkForPosition(m_position.Symbol(), (ENUM_POSITION_TYPE)m_position.PositionType(), m_position.StopLoss(), m_position.TakeProfit(), reason)) return false; TCLog("frozen-position:" + m_position.Symbol(), __FUNCTION__ + ": " + reason + " - request withheld"); return true; } //+------------------------------------------------------------------+ //| Article 2555 #7 - is the SELECTED pending order inside the | //| broker's freeze zone? A frozen order cannot be modified or deleted.| //+------------------------------------------------------------------+ bool CExpertCustom::SelectedOrderIsFrozen(void) { string reason; if(TCFreezeOkForOrder(m_order.Symbol(), (ENUM_ORDER_TYPE)m_order.OrderType(), m_order.PriceOpen(), reason)) return false; TCLog("frozen-order:" + m_order.Symbol(), __FUNCTION__ + ": " + reason + " - request withheld"); return true; } //+------------------------------------------------------------------+ //| Which order type an entry price will produce - see | //| CExpertTrade::Buy()/Sell(), whose routing this reproduces. | //+------------------------------------------------------------------+ ENUM_ORDER_TYPE CExpertCustom::ResolveOrderType(bool isLong, double price) { return TCResolveOrderType(m_symbol.Name(), isLong, price, m_symbol.Ask(), m_symbol.Bid()); } //+------------------------------------------------------------------+ //| Final pre-send gate for an entry (article #4/#6/#14). Shared by | //| both OpenLong/OpenShort - see the class declaration's comment. | //+------------------------------------------------------------------+ bool CExpertCustom::OpenPosition(bool isLong, double price, double sl, double tp, string caller) { if(price == EMPTY_VALUE) return(false); string reason; //--- #14: symbol-property reads below are meaningless for an unquoted symbol if(!TCSymbolIsTradeable(m_symbol.Name(), reason)) { TCLog("open-symbol:" + m_symbol.Name(), caller + ": withheld - " + reason); return(false); } ENUM_ORDER_TYPE type = ResolveOrderType(isLong, price); ENUM_ORDER_TYPE marketType = isLong ? ORDER_TYPE_BUY : ORDER_TYPE_SELL; if(type != marketType) { //--- #4: the account's ACCOUNT_LIMIT_ORDERS cap applies to pending orders only if(!TCIsNewOrderAllowed(reason)) { TCLog("open-orderlimit", caller + ": withheld - " + reason); return(false); } //--- #6: the activation price must still clear the stops level after the price moved if(!TCCheckPendingPrice(m_symbol.Name(), type, price, reason)) { TCLog("open-pendingprice:" + m_symbol.Name(), caller + ": withheld - " + reason); return(false); } } //--- #6: SL/TP against the stops level, measured from this order type's own reference price if(!TCCheckStops(m_symbol.Name(), type, price, sl, tp, reason)) { TCLog("open-stops:" + m_symbol.Name(), caller + ": withheld - " + reason); return(false); } return isLong ? CExpert::OpenLong(price, sl, tp) : CExpert::OpenShort(price, sl, tp); } bool CExpertCustom::OpenLong(double price, double sl, double tp) { return OpenPosition(true, price, sl, tp, __FUNCTION__); } bool CExpertCustom::OpenShort(double price, double sl, double tp) { return OpenPosition(false, price, sl, tp, __FUNCTION__); } //+------------------------------------------------------------------+ //| Article #7 - do not attempt to close a frozen position. | //+------------------------------------------------------------------+ bool CExpertCustom::CheckClose(void) { if(SelectedPositionIsFrozen()) return(false); return(CExpert::CheckClose()); } //+------------------------------------------------------------------+ //| Article #7 - a reverse closes the open position first, so the | //| same freeze restriction applies to it. | //+------------------------------------------------------------------+ bool CExpertCustom::CheckReverse(void) { if(SelectedPositionIsFrozen()) return(false); //--- UNREACHABLE UNDER TWO BOOKS, and guarded anyway. ProcessBooks() never calls this: the opposite //--- vote opens the other book instead, which acts on the new signal without closing a position the //--- gate certified to its barrier. The guard is here because CheckReverse() is virtual and reachable //--- from inherited paths, and a reverse would close one book's position while stamping the other //--- book's magic on the replacement. if(WarriorHedgingActive()) return(false); //--- Hold-to-barrier: a reverse is a vote-driven exit with an entry stapled on, so it is suppressed //--- for exactly the reason CheckClosePosition suppresses the close - the certified win rate assumes //--- the barrier decides. See CExpertSignalCustom::m_holdToBarrier. CExpertSignalCustom *sig = GetCustomSignal(); if(CheckPointer(sig) != POINTER_INVALID && sig.HoldToBarrier()) return(false); return(CExpert::CheckReverse()); } //+------------------------------------------------------------------+ //| Article #7 - do not attempt to modify a frozen position. | //+------------------------------------------------------------------+ bool CExpertCustom::CheckTrailingStop(void) { if(SelectedPositionIsFrozen()) return(false); return(CExpert::CheckTrailingStop()); } //+------------------------------------------------------------------+ //| Article #11 - a PositionModify() that changes nothing is an error | //| (TRADE_RETCODE_NO_CHANGES = 10025). CExpert's own caller compares | //| the new levels for EXACT equality, which a recomputed double can | //| miss by one ulp and still be "no change" to the server; compare | //| with the article's one-point tolerance instead. Shared by both | //| TrailingStopLong/Short - see the class declaration's comment. | //+------------------------------------------------------------------+ bool CExpertCustom::TrailingStopCommon(bool isLong, double sl, double tp, string caller) { if(!TCPositionModifyIsMeaningful(m_position.Symbol(), m_position.StopLoss(), sl, m_position.TakeProfit(), tp)) return(false); //--- the new levels are themselves subject to the stops level (article #6) string reason; ENUM_ORDER_TYPE type = isLong ? ORDER_TYPE_BUY : ORDER_TYPE_SELL; if(!TCCheckStops(m_position.Symbol(), type, 0.0, sl, tp, reason)) { TCLog("trail-stops-" + (isLong ? "long" : "short") + ":" + m_position.Symbol(), caller + ": withheld - " + reason); return(false); } return isLong ? CExpert::TrailingStopLong(sl, tp) : CExpert::TrailingStopShort(sl, tp); } bool CExpertCustom::TrailingStopLong(double sl, double tp) { return TrailingStopCommon(true, sl, tp, __FUNCTION__); } bool CExpertCustom::TrailingStopShort(double sl, double tp) { return TrailingStopCommon(false, sl, tp, __FUNCTION__); } //+------------------------------------------------------------------+ //| Article #7 - a pending order inside the freeze zone cannot be | //| deleted; withhold the request instead of having it rejected. | //+------------------------------------------------------------------+ bool CExpertCustom::CheckDeleteOrderLong(void) { if(SelectedOrderIsFrozen()) return(false); return(CExpert::CheckDeleteOrderLong()); } bool CExpertCustom::CheckDeleteOrderShort(void) { if(SelectedOrderIsFrozen()) return(false); return(CExpert::CheckDeleteOrderShort()); } //+------------------------------------------------------------------+ //| Article #7 - nor can it be modified. | //+------------------------------------------------------------------+ bool CExpertCustom::CheckTrailingOrderLong(void) { if(SelectedOrderIsFrozen()) return(false); return(CExpert::CheckTrailingOrderLong()); } bool CExpertCustom::CheckTrailingOrderShort(void) { if(SelectedOrderIsFrozen()) return(false); return(CExpert::CheckTrailingOrderShort()); } //+------------------------------------------------------------------+ //| Article #6/#11 - the shifted order must still change something, | //| and its new activation price plus SL/TP must clear the stops | //| level. CExpert::TrailingOrder*() shifts price, sl and tp by the | //| same delta and sends the modification unconditionally, so a | //| delta of 0 (or one that walks the order inside the stops level) | //| produced a guaranteed-rejected request. Shared by both | //| TrailingOrderLong/Short - see the class declaration's comment. | //+------------------------------------------------------------------+ bool CExpertCustom::TrailingOrderCommon(bool isLong, double delta, string caller) { string symbol = m_order.Symbol(); ENUM_ORDER_TYPE type = (ENUM_ORDER_TYPE)m_order.OrderType(); double price = m_symbol.NormalizePrice(m_order.PriceOpen() - delta); double sl = m_symbol.NormalizePrice(m_order.StopLoss() - delta); double tp = m_symbol.NormalizePrice(m_order.TakeProfit() - delta); if(!TCOrderModifyIsMeaningful(symbol, m_order.PriceOpen(), price, m_order.StopLoss(), sl, m_order.TakeProfit(), tp)) return(false); string reason; if(!TCCheckPendingPrice(symbol, type, price, reason) || !TCCheckStops(symbol, type, price, sl, tp, reason)) { TCLog("trail-order-" + (isLong ? "long" : "short") + ":" + symbol, caller + ": withheld - " + reason); return(false); } return isLong ? CExpert::TrailingOrderLong(delta) : CExpert::TrailingOrderShort(delta); } bool CExpertCustom::TrailingOrderLong(double delta) { return TrailingOrderCommon(true, delta, __FUNCTION__); } bool CExpertCustom::TrailingOrderShort(double delta) { return TrailingOrderCommon(false, delta, __FUNCTION__); } //+------------------------------------------------------------------+ //| THE EXITS RUN ON EVERY TICK, INCLUDING TICKS WE WON'T TRADE ON. | //| | //| Refresh() returning false skips the whole of Processing(), and | //| Processing() is where CheckClose() and CheckTrailingStop() live. | //| So on any tick with unusable quote history, a failed RefreshRates | //| or a period-flag mismatch, an OPEN POSITION got no exit check at | //| all - it simply rode. That failure is invisible by construction: | //| nothing is logged, no order is sent, and next tick the position | //| looks exactly as it should. The only trace is a stop that should | //| have moved and didn't, or an exit signal that never fired. | //| | //| This runs the CLOSING half of Processing() on those ticks anyway. | //| Deliberately ONLY the closing half - CheckReverse() and the | //| pending-order block both OPEN exposure, and opening on data we | //| have just declared unfit to trade on is the opposite of the point.| //| The asymmetry is the whole idea: closing a position on an | //| imperfect quote REDUCES risk even when the quote is wrong; | //| opening one on the same quote adds risk. When in doubt, an EA | //| should be able to get out, never to get in. | //+------------------------------------------------------------------+ bool CExpertCustom::ProtectOpenPosition(void) { //--- Best effort at a current price and ATR. Both are ALLOWED to fail here - that is the situation //--- we are in. CheckClose() still has a reason to fire that depends on neither (the signal's own //--- exit), and a trailing stop only ever moves the SL in the favorable direction, so even a stale //--- ATR cannot widen risk on an existing position. m_symbol.RefreshRates(); m_indicators.Refresh(); //--- BOTH BOOKS, and both unconditionally. This function exists because an open position must never //--- ride a tick unchecked; with two books that obligation is simply owed twice, and an early return //--- on the first book to act would recreate the exact defect on the second. if(WarriorHedgingActive()) { bool acted = false; for(int i = 0; i < 2; i++) { m_activeBookLong = (i == 0); if(CheckPointer(m_trade) != POINTER_INVALID) m_trade.SetExpertMagicNumber(WarriorBookMagic(m_activeBookLong)); if(!SelectPosition()) continue; if(CheckClose()) acted = true; else if(CheckTrailingStop()) acted = true; } m_activeBookLong = true; if(CheckPointer(m_trade) != POINTER_INVALID) m_trade.SetExpertMagicNumber(WarriorBookMagic(true)); return(acted); } if(!SelectPosition()) return(false); //--- Same precedence as Processing(): a close wins over a trail, and a closed position has no stop //--- left to move. if(CheckClose()) return(true); return(CheckTrailingStop()); } //+------------------------------------------------------------------+ //| OnTick handler | //+------------------------------------------------------------------+ void CExpertCustom::OnTick(void) { //--- check process flag if(!m_on_tick_process) return; //--- Article 2555 #16 - measure the tick with GetMicrosecondCount() and report (throttled) when it //--- overruns EXPERT_TICK_BUDGET_US. Purely diagnostic; see the macro's comment. ulong tick_started_us = TCNow(); //--- close positions and orders at specified time if(targetDayOfWeek != -1 && targetMinutes != -1 && targetHour != -1) { // Buffer in minutes for checking the condition int bufferMinutes = 1; // ±1 minute buffer // Get current server time datetime currentTimeValue = TimeCurrent(); MqlDateTime currentTime; TimeToStruct(currentTimeValue, currentTime); // Convert to MqlDateTime // Extract the current hour and minute int currentHour = currentTime.hour; int currentMinute = currentTime.min; // Check if the current day matches the target day if(currentTime.day_of_week == targetDayOfWeek || targetDayOfWeek == CLOSE_EVERYDAY) { //--- Resolve the target minute-of-day. Fixed hours keep the exact old arithmetic (same +-1 //--- minute buffer). int targetMinOfDay = -1; if(targetHour == CH_MARKET_CLOSE) { int mktClose = WarriorMarketCloseSeconds(m_symbol.Name(), currentTime.day_of_week); if(mktClose > 0) targetMinOfDay = mktClose / 60 - (int)targetMinutes; } else targetMinOfDay = (int)targetHour * 60 + (int)targetMinutes; int nowMinOfDay = currentHour * 60 + currentMinute; if(targetMinOfDay >= 0 && MathAbs(nowMinOfDay - targetMinOfDay) <= bufferMinutes) { // If it matches, call CloseAndDeleteAllForSymbol if(CloseAndDeleteAllForSymbol()) { // Log or handle the successful close Print("Positions and orders closed."); } } } } CExpertSignalCustom* customSignal = GetCustomSignal(); if(customSignal != NULL) customSignal.OnTickHandler(); //--- updated quotes and indicators if(!Refresh()) { //--- NOT a plain return. Refresh() declining this tick means we will not OPEN anything on it; //--- it must never mean an already-open position goes unchecked. See ProtectOpenPosition(). if(ProtectOpenPosition()) Print(__FUNCTION__ + ": exit acted via the protective path on " + m_symbol.Name() + " - Refresh() declined this tick (stale rates / short history / period flags)"); if(VerboseMode) TCWarnIfSlow("CExpertCustom::OnTick", tick_started_us, EXPERT_TICK_BUDGET_US); return; } //--- expert processing Processing(); if(VerboseMode) TCWarnIfSlow("CExpertCustom::OnTick", tick_started_us, EXPERT_TICK_BUDGET_US); } //+------------------------------------------------------------------+ //| OnChartEvent handler | //+------------------------------------------------------------------+ void CExpertCustom::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- check process flag if(!m_on_chart_event_process) return; CExpertSignalCustom* customSignal = GetCustomSignal(); if(customSignal != NULL) customSignal.OnChartEventHandler(id, lparam, dparam, sparam); } //+------------------------------------------------------------------+ //| Shared select/filter/freeze-check/act loop for one target kind. | //+------------------------------------------------------------------+ bool CExpertCustom::CloseAllLoop(ENUM_CLOSEALL_TARGET target, string symbol, string caller) { bool result = false; // Track if any action was successfully performed string reason; //--- Iterate through all tickets of this target kind, back to front (safe against the list //--- shrinking as this loop closes/deletes entries). int total = (target == CLOSEALL_POSITION) ? PositionsTotal() : OrdersTotal(); for(int i = total - 1; i >= 0; i--) { ulong ticket = (target == CLOSEALL_POSITION) ? PositionGetTicket(i) : OrderGetTicket(i); if(ticket == 0) continue; if(target == CLOSEALL_POSITION && !PositionSelectByTicket(ticket)) continue; if(target == CLOSEALL_POSITION) { if(PositionGetString(POSITION_SYMBOL) != symbol) continue; //--- BOTH BOOKS. This is the scheduled close-all; a filter that recognised only the long //--- book would leave a short-book position open past the flat-by time with nothing left //--- to close it. WarriorOwnsMagic() is deliberately not gated on Allow_Hedging for the //--- same reason - see its definition. if(!WarriorOwnsMagic(PositionGetInteger(POSITION_MAGIC))) continue; //--- Stamp the closing request with the magic of the book that owns the position, so the //--- deal is attributed to the book that opened it rather than to whichever ran last. if(CheckPointer(m_trade) != POINTER_INVALID) m_trade.SetExpertMagicNumber((ulong)PositionGetInteger(POSITION_MAGIC)); //--- article 2555 #7: a frozen position cannot be closed; retry on the next timer/tick pass if(!TCFreezeOkForPosition(symbol, (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE), PositionGetDouble(POSITION_SL), PositionGetDouble(POSITION_TP), reason)) { TCLog("closeall-frozen-position:" + symbol, caller + ": " + reason + " - close deferred"); continue; } if(m_trade.PositionClose(ticket)) result = true; } else { if(OrderGetString(ORDER_SYMBOL) != symbol) continue; if(!WarriorOwnsMagic(OrderGetInteger(ORDER_MAGIC))) continue; if(CheckPointer(m_trade) != POINTER_INVALID) m_trade.SetExpertMagicNumber((ulong)OrderGetInteger(ORDER_MAGIC)); //--- article 2555 #7: nor can a frozen pending order be deleted if(!TCFreezeOkForOrder(symbol, (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE), OrderGetDouble(ORDER_PRICE_OPEN), reason)) { TCLog("closeall-frozen-order:" + symbol, caller + ": " + reason + " - delete deferred"); continue; } if(m_trade.OrderDelete(ticket)) result = true; } } //--- The per-ticket stamping above left m_trade on whichever book owned the last ticket. Restore it, //--- so a caller that never heard of books does not inherit the short book's magic. if(CheckPointer(m_trade) != POINTER_INVALID) m_trade.SetExpertMagicNumber(WarriorBookMagic(true)); return result; // Return true if any action was performed, false otherwise } //+------------------------------------------------------------------+ //| Close all positions and delete all pending orders for Symbol() | //+------------------------------------------------------------------+ bool CExpertCustom::CloseAndDeleteAllForSymbol() { string symbol = m_symbol.Name(); bool positionsResult = CloseAllLoop(CLOSEALL_POSITION, symbol, __FUNCTION__); bool ordersResult = CloseAllLoop(CLOSEALL_ORDER, symbol, __FUNCTION__); return positionsResult || ordersResult; } //+------------------------------------------------------------------+ //| OnTimer handler | //+------------------------------------------------------------------+ void CExpertCustom::OnTimer(void) { //--- check process flag if(!m_on_timer_process) return; //--- Article 2555 #16 - keep an eye on the memory the program holds. The AI signals allocate the //--- bulk of it (network weights, replay buffers), so the timer - not the tick - is the right place //--- to sample it. Throttled and diagnostic only; nothing is freed or truncated on the back of it. if(VerboseMode) TCWarnIfMemoryAbove(EXPERT_MEMORY_SOFT_LIMIT_MB); CExpertSignalCustom* customSignal = GetCustomSignal(); if(customSignal == NULL) return; if(!dbm.OpenDatabase()) { Print(__FUNCTION__ + ": Failed to open database, skipping this timer cycle's signal processing/weight update."); return; } customSignal.ProcessBufferedSignals(); if(!customSignal.UpdateSignalsWeights()) Print(__FUNCTION__ + ": UpdateSignalsWeights failed, DB-derived filter weights not refreshed this cycle."); // This function owns the connection it opened above - ProcessBufferedSignals() deliberately // leaves it open so UpdateSignalsWeights() can reuse the same handle. if(!IsBacktesting) dbm.CloseDatabase(); } //+------------------------------------------------------------------+ //| Initialization trade object | //+------------------------------------------------------------------+ bool CExpertCustom::InitTrade(ulong magic, CExpertTrade *trade = NULL) { if(m_trade != NULL) delete m_trade; //--- if(trade == NULL) { if((m_trade = new CExpertTrade) == NULL) return(false); } else m_trade = trade; //--- tune trade object m_trade.SetSymbol(GetPointer(m_symbol)); m_trade.SetExpertMagicNumber(magic); m_trade.SetMarginMode(); //--- SYNCHRONOUS on purpose (2026-08-11; was true). The risk-budget flatten already ran its own //--- synchronous CTrade for exactly this reason (Variables\RiskBudget.mqh); this aligns every //--- send/modify/close behind the same rule. m_trade.SetAsyncMode(false); //--- set default deviation for trading in adjusted points m_trade.SetDeviationInPoints((ulong)(3 * m_adjusted_point / m_symbol.Point())); //--- ok return(true); } //+------------------------------------------------------------------+