Warrior_EA/Expert/ExpertCustom.mqh
AnimateDread ad4ae58814 feat(vote): exit-on-reversal boolean, pin the threshold, retry the atomic rename
THE EXIT KNOB. Exit_On_Reversal_Vote (default false) replaces the deleted
Signal_ThresholdClose with one boolean: false pins the close threshold to an
arithmetically unreachable 101, true pins it to the SAME threshold the entry
uses - the seed at first, then the derived value, republished together whenever
it moves. A second threshold was always redundant; "the bot now says the other
way" is one question.

It also arms CExpertSignalCustom::m_holdToBarrier, which was DEAD CODE:
HoldToBarrier(bool) had no caller anywhere in the build, so the flag had been
permanently false and the disabled close threshold was carrying the whole
hold-to-barrier policy alone. Both halves now move together.

Default stays false because the reason is statistical: the gate certifies
P(label agrees | vote fired) against a label that runs to the barrier, so an
early close trades something never measured. Turning it on is a different
strategy, not a tightening of this one.

THE PIN. The live threshold now moves only when an era's weights become the
checkpoint, and freezes once g_ensDeployApproved. Every era still derives its own
rung - that is how the best one is found - but the rung that TRADES belongs to
the checkpoint, exactly as the weights do. Two reasons, one measured and one
structural: the per-era rung moves on 6-34% of steps (the live run flapped
SP500 15 -> 10 -> 15 within a minute of starting), and without the pin a later
era's rung could end up applied to an earlier era's deployed model. A ladder
restart releases the pin, since clearing the checkpoint clears what it pinned.
The era line now prints the rung its own numbers came from, so it stays honest
when that differs from the pinned one.

THE ATOMIC RENAME retried zero times. Six charts share the TrainPool and AltData
directories, so a publish regularly lands while a peer chart holds the
destination open and FileMove returns 5004 - 27 times in one day on the live
fleet. Nothing was lost (the temp keeps the new content, the old file stays
intact) but the row did not update until the next publish. Now four attempts at
25ms, on the FAILURE PATH ONLY - a successful rename never sleeps - and skipped
in the tester, where the contention cannot happen and Sleep would distort a pass.
A rescued retry is logged, so worsening contention is visible.

Retrain-neutral. Compiled clean; NOT yet run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:53:56 -04:00

992 行
48 KiB
MQL5

//+------------------------------------------------------------------+
//| Expert.mqh |
//| Copyright 2000-2023, MetaQuotes Ltd. |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#include <Expert\Expert.mqh>
#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<CExpertSignalCustom*>(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);
}
//+------------------------------------------------------------------+