Warrior_EA/Expert/ExpertCustom.mqh

743 lines
33 KiB
MQL5
Raw Permalink Normal View History

//+------------------------------------------------------------------+
//| 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. The article's own
//--- yardstick is that the FIRST (heaviest) calculation over 10+ years of M1 data should stay
//--- under 100 ms; a steady-state tick that regularly costs more than that is starving every other
//--- program sharing this symbol's single terminal thread, so it gets a throttled journal warning
//--- pointing at the MetaEditor profiler. 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
{
protected:
CExpertSignalCustom* GetCustomSignal()
{
return dynamic_cast<CExpertSignalCustom*>(m_signal);
}
bool CloseAndDeleteAllForSymbol(void);
//--- Article 2555 #7 - SYMBOL_TRADE_FREEZE_LEVEL. While the market sits within freeze-level
//--- points of a position's SL/TP (or of a pending order's activation price) the server refuses
//--- to modify, close or delete it, and answering that refusal with a request anyway is exactly
//--- the error the article says a Market product must never produce. 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);
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;
//--- 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;
feat(chart): reconstruct the filtered view behind the handover point Completes the filtered view from 282b535, which only reached forward of attach. On a multi-hour training run that is the entire time you are looking at the chart, so the answer to "how would the whole bot have traded" was blank exactly when it was wanted. The sweep lives on the AGGREGATE signal, which is the only object holding every filter. AI members contribute their CACHED per-bar decision from the era scan - no inference re-runs, the cache already spans the chart - and the classic ladders are replayed with EvalShift(i), the same mechanism CSignalMETA's candidate sweep uses and exact because every classic pattern condition anchors on StartIndex(). Combination is the live one: weighted mean over voting filters, abstentions out of both sums, against Min_Vote_Open. THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part that is not obvious. Live journaling reads m_active_pattern_long/short from the PREVIOUS Direction() call. Replaying hundreds of past bars between two live bars leaves those slots holding whichever bar the sweep stopped on, so the next live bar journals that pattern under the current timestamp - a corrupted row in the very table pattern win rates are computed from, which is now also where vote weights come from. Save/RestoreVoteState() brackets every replayed call. CSignalMETA gets away without it only because its sweep runs once, at the first era, before any of that state matters. TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker rejected an order - it has no stops level, ATR warm-up or swing-history sync as they were at that moment - so it is an upper bound: honest about the vote, optimistic about placement. It therefore stops dead at the handover bar, which is latched ONCE so later rebuilds cannot creep it forward and start overwriting real decisions with guesses, and its arrows say "reconstructed (vote only - order validation not replayed)" in the tooltip. Someone comparing two arrows either side of that line has to be able to tell which is a record and which is a replay, and the chart is the only place they look. Re-armed on any era boundary (summed era counters), because that is when the answer changes - RankTiersFromOos has just re-derived every tier's vote weight - and only between sweeps, so a restart cannot leave the previous pass's tail undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on every classic filter, which is real indicator work on the chart thread, and an unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen. SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should reach the same distance or the raw and filtered views are not comparable. Known gap: on a classic-only chart the reconstruction is built once and not refreshed when the hourly DB ranking moves the classic weights. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
//--- 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;
}
fix(chart): the vote readout was repainted once per bar, not once per timer tick "Still stuck at 0" after a042cb4 - and the .ex5 timestamp confirmed the new build was running, so this was not a stale binary. The readout was only ever written inside Direction(), and with Expert_EveryTick=false the stock CExpert::Refresh() gates Processing() - and therefore Direction() - to NEW-BAR ticks (verified in the terminal's own Include\Expert\Expert.mqh: Refresh() returns false unless the tick lands on a period boundary). On an H4 chart that is one repaint every four hours. The label was written exactly once at attach - before any model had produced a decision, so it read 0.0 with 4 models - and then sat frozen while the models trained underneath it. "Stuck at 0" was the label's refresh RATE, not the vote's value. The prospective fallback in a042cb4 was correct and running; it just had no way to reach the screen until the next bar open. The prospective computation is extracted into RefreshVoteReadout(), called from OnTimer through CExpertCustom every timer tick. It defers to the trade path whenever the last real Direction() had live voters (m_lastLiveVoters latch): a live vote is authoritative for its whole bar, and repainting prospective numbers over it would overwrite a tradable reading with an untradable one. Cheap by construction - a handful of filters, plain arithmetic on already-computed members, no indicator reads - so it belongs on the 500ms timer without a throttle. Expect the label to move at timer cadence now, tracking pass-1's walk through the training window (dPrevSignal holds the last trained bar's output during an era), dimmed and labelled "training, not tradable yet". NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:56:06 -04:00
//--- 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();
}
feat(chart): reconstruct the filtered view behind the handover point Completes the filtered view from 282b535, which only reached forward of attach. On a multi-hour training run that is the entire time you are looking at the chart, so the answer to "how would the whole bot have traded" was blank exactly when it was wanted. The sweep lives on the AGGREGATE signal, which is the only object holding every filter. AI members contribute their CACHED per-bar decision from the era scan - no inference re-runs, the cache already spans the chart - and the classic ladders are replayed with EvalShift(i), the same mechanism CSignalMETA's candidate sweep uses and exact because every classic pattern condition anchors on StartIndex(). Combination is the live one: weighted mean over voting filters, abstentions out of both sums, against Min_Vote_Open. THE REPLAY CORRUPTS LIVE JOURNALING IF LEFT UNGUARDED, and this is the part that is not obvious. Live journaling reads m_active_pattern_long/short from the PREVIOUS Direction() call. Replaying hundreds of past bars between two live bars leaves those slots holding whichever bar the sweep stopped on, so the next live bar journals that pattern under the current timestamp - a corrupted row in the very table pattern win rates are computed from, which is now also where vote weights come from. Save/RestoreVoteState() brackets every replayed call. CSignalMETA gets away without it only because its sweep runs once, at the first era, before any of that state matters. TWO SOURCES OF TRUTH, KEPT APART. A reconstruction cannot know the broker rejected an order - it has no stops level, ATR warm-up or swing-history sync as they were at that moment - so it is an upper bound: honest about the vote, optimistic about placement. It therefore stops dead at the handover bar, which is latched ONCE so later rebuilds cannot creep it forward and start overwriting real decisions with guesses, and its arrows say "reconstructed (vote only - order validation not replayed)" in the tooltip. Someone comparing two arrows either side of that line has to be able to tell which is a record and which is a replay, and the chart is the only place they look. Re-armed on any era boundary (summed era counters), because that is when the answer changes - RankTiersFromOos has just re-derived every tier's vote weight - and only between sweeps, so a restart cannot leave the previous pass's tail undrawn. Chunked at 150 bars per timer slice: each bar replays Direction() on every classic filter, which is real indicator work on the chart thread, and an unchunked sweep here is the 2026-07-26 arrow-restore freeze waiting to happen. SIGNAL_RESCAN_LOOKBACK_BARS moves to ExpertSignalCustom.mqh alongside SIG_ARROW_PREFIX - same include-order reason, and the two rebuilds should reach the same distance or the raw and filtered views are not comparable. Known gap: on a classic-only chart the reconstruction is built once and not refreshed when the hourly DB ranking moves the classic weights. NOT COMPILED - user compiles in MetaEditor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:13 -04:00
bool FilteredOverlayPending(void)
{
CExpertSignalCustom *s = GetCustomSignal();
return (s != NULL) ? s.FilteredOverlayPending() : false;
}
protected:
//--- refreshing
virtual bool Refresh(void) override;
//--- processing (main method)
virtual bool Processing(void) override;
//--- Article 2555 #2/#3/#4/#5/#6 - final gate immediately before the order is sent. Everything
//--- here was already validated when the setup was shaped (CExpertSignalCustom::OpenParams) and
//--- when the lot was sized (CExpertMoneyCustom::ValidateLotForTrade), but those ran earlier in
//--- the tick and the market has moved since: a price that was a legal pending distance then can
//--- be inside the stops level now. 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)
{
}
//+------------------------------------------------------------------+
//| 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. Everything below this point
//--- (RefreshRates, then every indicator in m_indicators) reads a timeseries that the terminal may
//--- still be downloading: on a symbol/timeframe just switched to, on a thin instrument, or right
//--- after reconnecting, the series reports far fewer bars than the indicators' warm-up needs and
//--- the buffers come back zero-filled rather than empty. Skipping the tick until the series is
//--- synchronised is the article's "check for the required history and request the missing data"
//--- approach - touching the series here is itself what asks the terminal to build it.
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)
{
//--- calculate signal direction once
m_signal.SetDirection();
//--- 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);
}
//+------------------------------------------------------------------+
//| 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)
{
if(price <= 0.0 || price == EMPTY_VALUE)
return(isLong ? ORDER_TYPE_BUY : ORDER_TYPE_SELL);
double stops = TCStopsLevel(m_symbol.Name());
if(isLong)
{
double ask = m_symbol.Ask();
if(price > ask + stops)
return(ORDER_TYPE_BUY_STOP);
if(price < ask - stops)
return(ORDER_TYPE_BUY_LIMIT);
return(ORDER_TYPE_BUY);
}
double bid = m_symbol.Bid();
if(price > bid + stops)
return(ORDER_TYPE_SELL_LIMIT);
if(price < bid - stops)
return(ORDER_TYPE_SELL_STOP);
return(ORDER_TYPE_SELL);
}
//+------------------------------------------------------------------+
//| Final pre-send gate for a long entry (article #4/#6/#14). |
//+------------------------------------------------------------------+
bool CExpertCustom::OpenLong(double price, double sl, double tp)
{
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(), __FUNCTION__ + ": withheld - " + reason);
return(false);
}
ENUM_ORDER_TYPE type = ResolveOrderType(true, price);
if(type != ORDER_TYPE_BUY)
{
//--- #4: the account's ACCOUNT_LIMIT_ORDERS cap applies to pending orders only
if(!TCIsNewOrderAllowed(reason))
{
TCLog("open-orderlimit", __FUNCTION__ + ": 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(), __FUNCTION__ + ": 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(), __FUNCTION__ + ": withheld - " + reason);
return(false);
}
return(CExpert::OpenLong(price, sl, tp));
}
//+------------------------------------------------------------------+
//| Final pre-send gate for a short entry (article #4/#6/#14). |
//+------------------------------------------------------------------+
bool CExpertCustom::OpenShort(double price, double sl, double tp)
{
if(price == EMPTY_VALUE)
return(false);
string reason;
if(!TCSymbolIsTradeable(m_symbol.Name(), reason))
{
TCLog("open-symbol:" + m_symbol.Name(), __FUNCTION__ + ": withheld - " + reason);
return(false);
}
ENUM_ORDER_TYPE type = ResolveOrderType(false, price);
if(type != ORDER_TYPE_SELL)
{
if(!TCIsNewOrderAllowed(reason))
{
TCLog("open-orderlimit", __FUNCTION__ + ": withheld - " + reason);
return(false);
}
if(!TCCheckPendingPrice(m_symbol.Name(), type, price, reason))
{
TCLog("open-pendingprice:" + m_symbol.Name(), __FUNCTION__ + ": withheld - " + reason);
return(false);
}
}
if(!TCCheckStops(m_symbol.Name(), type, price, sl, tp, reason))
{
TCLog("open-stops:" + m_symbol.Name(), __FUNCTION__ + ": withheld - " + reason);
return(false);
}
return(CExpert::OpenShort(price, sl, tp));
}
//+------------------------------------------------------------------+
//| 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);
//--- 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. |
//+------------------------------------------------------------------+
bool CExpertCustom::TrailingStopLong(double sl, double tp)
{
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;
if(!TCCheckStops(m_position.Symbol(), ORDER_TYPE_BUY, 0.0, sl, tp, reason))
{
TCLog("trail-stops-long:" + m_position.Symbol(), __FUNCTION__ + ": withheld - " + reason);
return(false);
}
return(CExpert::TrailingStopLong(sl, tp));
}
//+------------------------------------------------------------------+
//| See TrailingStopLong() - same two rules for a short position. |
//+------------------------------------------------------------------+
bool CExpertCustom::TrailingStopShort(double sl, double tp)
{
if(!TCPositionModifyIsMeaningful(m_position.Symbol(), m_position.StopLoss(), sl, m_position.TakeProfit(), tp))
return(false);
string reason;
if(!TCCheckStops(m_position.Symbol(), ORDER_TYPE_SELL, 0.0, sl, tp, reason))
{
TCLog("trail-stops-short:" + m_position.Symbol(), __FUNCTION__ + ": withheld - " + reason);
return(false);
}
return(CExpert::TrailingStopShort(sl, tp));
}
//+------------------------------------------------------------------+
//| 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. |
//+------------------------------------------------------------------+
bool CExpertCustom::TrailingOrderLong(double delta)
{
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-long:" + symbol, __FUNCTION__ + ": withheld - " + reason);
return(false);
}
return(CExpert::TrailingOrderLong(delta));
}
bool CExpertCustom::TrailingOrderShort(double delta)
{
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-short:" + symbol, __FUNCTION__ + ": withheld - " + reason);
return(false);
}
return(CExpert::TrailingOrderShort(delta));
}
//+------------------------------------------------------------------+
//| 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)
{
// Check if the current time is within the buffer range around the target time
if(currentHour == targetHour)
{
// Check if the current minute falls within the ±1 minute buffer of the target minute
if(currentMinute >= (targetMinutes - bufferMinutes) && currentMinute <= (targetMinutes + 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())
{
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);
}
//+------------------------------------------------------------------+
//| Close all positions and delete all pending orders for Symbol() |
//+------------------------------------------------------------------+
bool CExpertCustom::CloseAndDeleteAllForSymbol()
{
bool result = false; // Track if any action was successfully performed
string symbol = m_symbol.Name();
string reason;
// Iterate through all positions. The symbol/magic filter is read off the TICKET being closed:
// PositionSelect(_Symbol) selects "some position on the chart symbol" and says nothing about
// positionTicket, so the guard it used to stand in for let this close every position on the
// account - including other symbols' and other EAs' - the moment the chart symbol happened to
// hold one. Same for the order loop below, whose OrderSelect() succeeded for any ticket at all.
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong positionTicket = PositionGetTicket(i);
if(positionTicket == 0 || !PositionSelectByTicket(positionTicket))
continue;
if(PositionGetString(POSITION_SYMBOL) != symbol)
continue;
if(PositionGetInteger(POSITION_MAGIC) != (long)m_magic)
continue;
//--- 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, __FUNCTION__ + ": " + reason + " - close deferred");
continue;
}
if(m_trade.PositionClose(positionTicket))
result = true;
}
// Iterate through all pending orders
for(int j = OrdersTotal() - 1; j >= 0; j--)
{
ulong orderTicket = OrderGetTicket(j);
if(orderTicket == 0)
continue;
if(OrderGetString(ORDER_SYMBOL) != symbol)
continue;
if(OrderGetInteger(ORDER_MAGIC) != (long)m_magic)
continue;
//--- 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, __FUNCTION__ + ": " + reason + " - delete deferred");
continue;
}
if(m_trade.OrderDelete(orderTicket))
result = true;
}
return result; // Return true if any action was performed, false otherwise
}
//+------------------------------------------------------------------+
//| 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();
fix: four risk-layer holes a funded account would eventually find 1. The expectancy stop was stone dead at shipped defaults. Its only feed - RecordTradeResult inside CTradeJournalManager::Update() - ran solely under UseDatabaseRanking, which ships false, so the da54639 halt was armed (ExpectancyMinTrades=40) and never received a single closed trade. A risk rule must not be a side effect of an analytics toggle: the journal gains InitTrackingOnly(), Update() runs unconditionally from OnTick and skips only the DB insert when no DB was initialized. 2. Below-minimum lots were silently bumped UP to SYMBOL_VOLUME_MIN by TCNormalizeVolume - correct for a user-entered fixed lot, but in the risk-sizing path it turned a budget-capped 0.05 into 0.10 on min-0.10/ step-0.01 symbols: double the intended risk, after CapRiskAmount already clamped, exactly the routine-stop-out-breaches-the-daily-limit scenario the budget exists to close. CMoneyRiskBase now refuses the trade when the risk-derived lot is below the broker minimum. 3. All trading was async fire-and-forget (SetAsyncMode(true)) with no OnTradeTransaction handler and no retry: server retcodes were never observed. Fail-safe for entries, not for closes - a silently rejected close rode the position until the next bar (or next day for the timed close window). Now synchronous, matching the risk-budget flatten's own already-synchronous CTrade; on an H1 EA the latency is irrelevant. 4. FIXED_LOT bypassed the budget entirely (no CapRiskAmount, no OpenRiskAtStops) - pre-halt it could commit more than the remaining daily allowance. A fixed lot cannot be scaled, so the rule is binary: its loss-to-stop fits the remaining allowance whole or the trade is refused; unpriceable risk (no SL) is refused while the budget is enabled. Compile: 0 errors, 0 warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:14:26 -04:00
//--- SYNCHRONOUS on purpose (2026-08-11; was true). Async OrderSendAsync returns as soon as the
//--- request is queued: server retcodes (requote, off-quotes, invalid stops, not-enough-money) were
//--- never observed, there is no OnTradeTransaction handler anywhere in this EA, and no retry logic.
//--- For an ENTRY that failure direction is fail-safe (a missed trade); for a CLOSE it is not -
//--- signal-driven closes re-evaluate only on the next bar, and the timed daily close runs in a
//--- +-1-minute window, so a silently rejected close could ride a position for a bar (or a day).
//--- 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. On an H1
//--- EA the extra round-trip latency per order is irrelevant next to a lost close.
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);
}
//+------------------------------------------------------------------+