2026-07-27 10:58:29 -04:00 | | | //+------------------------------------------------------------------+
|
2026-07-13 03:23:39 -04:00 | | | //| Expert.mqh |
|
| | | //| Copyright 2000-2023, MetaQuotes Ltd. |
|
| | | //| https://www.mql5.com |
|
| | | //+------------------------------------------------------------------+
|
| | | #include <Expert\Expert.mqh>
|
| | | #include "ExpertSignalCustom.mqh"
|
2026-07-26 23:08:32 -04:00 | | | #include "..\System\TradeChecks.mqh"
|
2026-08-22 00:25:52 -04:00 | | | //--- Article 2555 #16: a soft budget for one OnTick() pass, in microseconds. Diagnostic only - it
|
| | | //--- never skips or truncates any work.
|
2026-07-26 23:08:32 -04:00 | | | #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
|
2026-07-13 03:23:39 -04:00 | | | //+------------------------------------------------------------------+
|
| | | //| Class CExpert. |
|
| | | //| Purpose: Base class expert advisor. |
|
| | | //| Derives from class CExpert. |
|
| | | //+------------------------------------------------------------------+
|
| | | class CExpertCustom : public CExpert
|
| | | {
|
 feat(vote): derive the threshold instead of configuring it
Signal_ThresholdOpen becomes a seed. The era verdict now picks the HIGHEST
sweep rung whose vote still clears the whole deploy gate - coverage floor,
exact-binomial precision bar and two-sidedness together - computes the era's
verdict AT that rung, and publishes it to the live signal's m_threshold_open
so the bar the gate certifies is the bar the EA trades.
Measured on 619 era verdicts across all six live charts:
* every era on every symbol had at least one rung clearing the full gate.
At the fixed 25% the fleet was actually running, four of six symbols had
none, ever. The threshold, not the models, was the blocker.
* walk-forward (rung derived on era N, scored on era N+1): 10.2% coverage /
31.8% precision, against an oracle re-picking on N+1 of 10.3% / 31.7%.
Near-zero shrinkage - a measurement, not a fit. It holds because the
binding constraint is COVERAGE, a near-deterministic step function of the
vote distribution, not precision.
* vs a fixed 15% (best global value): +0.6pp precision, 3.4pp less coverage.
vs a fixed 20%: deployable on all six rather than four of six.
Selection on the highest PASSING rung, never on the best-precision rung - that
is a best-of-6 on a noisy statistic and this project has crowned noise that way
four times. The multiplicity that remains is paid for: nTried in
EnsembleSurvivesSelection is now eras x rungs. Costs nothing - all six charts
clear it by 6.5-12 sigma even forming z on effective rather than raw calls.
Also fixes, in the same path: the direction-policy gate is hoisted above the
per-rung tally so every rung is scored on the population the gate certifies.
Retrain-neutral: not in BuildModelFingerprint(), no .nnw re-keyed.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:02:58 -04:00 | | | private:
|
| | | //--- Last value handed to the live signal by PublishVoteThreshold(), -1 = still on the input seed.
|
| | | int m_publishedVoteThreshold;
|
 feat(trade): two books per symbol, and delete the vote exit
Allow_Hedging (default ON, live only on a RETAIL_HEDGING account) gives the EA
an independent long book and short book on its symbol: at most one long and at
most one short, each opened on its own side's vote and each held to its own
barrier. On a netting account, or with the input off, the original
single-position path runs bit-for-bit unchanged and init says which one is live.
WHY THIS INSTEAD OF A VOTE EXIT. The deploy gate certifies
P(label agrees | vote fired) and the label runs to the barrier, so closing early
on a reversal makes the realised outcome stop being the labelled one - the
certified precision no longer describes what is traded. Opening the other side
acts on the new signal and leaves the old position's certification intact, and
costs no more than reversing: both pay the new side's spread, the difference is
only that the existing position runs on to a barrier already measured as
positive-expectancy. So Signal_ThresholdClose is DELETED rather than tuned,
along with its SIGNAL_CLOSE_PRESETS enum; the threshold is pinned to an
arithmetically unreachable 101 (the stock default of 100 is reachable by a
weighted mean of values capped at 100).
Note the two books can never both fill from one signal: CheckOpenLong and
CheckOpenShort test opposite signs of the same m_direction, so at most one clears
per tick. A hedge only forms when a LATER opposite vote fires - which is what
keeps it from being a guaranteed-loss wash pair.
The mechanism is a SelectPosition() override keyed on the active book's magic;
every inherited close/trail path then operates on that book untouched. The long
book keeps Expert_MagicNumber, so no existing position, journal row or
risk-budget state file is re-addressed. Short book is +1.
Four ownership filters had to widen from "== m_magic" to WarriorOwnsMagic(),
or the short book would have been invisible to the code that must reach it:
the scheduled close-all (positions and orders), the risk budget's emergency
flatten, and the journal's MAE/MFE walk. WarriorOwnsMagic() is deliberately NOT
gated on Allow_Hedging - turning the input off while a short-book position is
open would otherwise orphan it with nothing left to close it.
Risk sizing needed no change: CapRiskAmount already subtracts OpenRiskAtStops(),
which counts every position regardless of magic, so the second book is sized
inside what the first one left. Conservative for a hedged pair, which cannot
lose both stops - the safe direction.
Retrain-neutral: neither input is in BuildModelFingerprint() or
ComputeDbConfigFingerprint(). Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:20:35 -04:00 | | | //--- 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;
|
2026-07-13 03:23:39 -04:00 | | | protected:
|
| | | CExpertSignalCustom* GetCustomSignal()
|
| | | {
|
| | | return dynamic_cast<CExpertSignalCustom*>(m_signal);
|
| | | }
|
2026-08-24 03:09:40 -04:00 | | | //--- 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);
|
2026-07-13 03:23:39 -04:00 | | | bool CloseAndDeleteAllForSymbol(void);
|
 fix(exits): never let a declined tick leave an open position unchecked
Refresh() returning false skipped 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 already-open position got no exit check at all -
it rode. Invisible by construction: nothing logged, no order sent, and
next tick the position looks exactly as it should. The only trace is a
stop that should have moved and didn't.
ProtectOpenPosition() now runs the CLOSING half of Processing() on those
ticks. Only the closing half, on purpose: CheckReverse() and the
pending-order block both OPEN exposure, and opening on data just declared
unfit to trade on is the opposite of the point. Closing on an imperfect
quote reduces risk even when the quote is wrong; opening on it does not.
When it acts, it says so in the journal - a degraded-path exit should
never be silent.
Also pins the invariant at the Expert_EveryTick gate: that input
throttles how often the EA forms an OPINION, never how often it can act
on a position it already holds. Exits stay above the gate, and the
comment now says so to the next person editing it.
Pre-existing hole, not introduced by the EveryTick work in 6d48fdb -
that change is what made it worth reading the tick path closely.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:52:07 -04:00 | | | //--- 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);
|
2026-08-22 00:25:52 -04:00 | | | //--- 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.
|
2026-07-26 23:08:32 -04:00 | | | 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);
|
2026-08-24 02:00:10 -04:00 | | | //--- 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);
|
 perf(tester): stop agents doing chart work on deinit; enforce Expert_EveryTick
Three related changes, all aimed at work being repeated at a frequency
nobody chose.
1. OnDeinit gets a tester/optimizer fast path.
Everything in the live teardown exists to leave a CHART clean and a live
model's state on disk. An optimization agent has neither. It was still
running, on EVERY pass: a per-signal arrow-sidecar WRITE
(ShutdownChartCleanup -> PersistAndClearChartSignals) plus two full
chart-object scans plus a ChartRedraw. At optimization scale that is
hundreds of thousands of pointless file writes per agent, against a
~4,500 ms budget MetaTrader force-terminates on - the shape of thing
that stalls an agent rather than failing it.
The fast path keeps MarkShutdown() and FlushTrainRun() (so a killed pass
never leaves a half-written era) and still calls dbm.Deinit() and
Expert.Deinit() - leaking the signal tree or a handle across passes is
its own way to accumulate into a stall. The two now-unreachable
!isTesterRun guards further down are folded away.
2. All four tester handlers are present and documented by WHERE THEY RUN.
OnTesterInit/OnTesterPass/OnTesterDeinit run in the CONTROLLING TERMINAL
once per session; only OnTester runs on the agent, per pass. OnTesterPass
was missing entirely - added empty and deliberately so: it only fires for
passes that shipped FrameAdd() data, which this EA never sends, and
reading frames there would put per-pass work on the terminal's critical
path. Declared so that adding frame-sending later fails loudly instead of
silently dropping every frame.
3. Expert_EveryTick is now actually enforced.
It was passed to Expert.Init() and only ever reached StartIndex() - which
bar a signal READS. The whole pipeline still ran on every quote. It now
gates m_signal.SetDirection() in CExpertCustom::Processing(): that call
drives Direction(), which is a TRANSACTION (NN forward passes, DB rows,
chart arrows, one-shot vote state), and re-running it on every tick of a
4-hour bar repeats all of it.
Scoped deliberately. Everything after that line still runs per tick -
CheckReverse/CheckClose/CheckTrailingStop and pending-order maintenance
are risk management, and a stop that only trails at bar boundaries is a
different strategy, not a faster one. The scheduled close-all in OnTick()
matches a +-1 MINUTE window, so bar-gating it on H4 would step straight
over the thing 100% of label timeouts already resolve against.
g_riskBudget.Update() also stays at quote frequency, by design.
System/NewBar.mqh becomes CNewBar, a class. The free function it replaced
had zero callers and kept its watermark in a `static`: ONE watermark
shared by every caller, so the first caller each tick consumed the
transition and every other caller was told "no new bar" for a bar that
had just opened. Per-instance state fixes that; first observation counts
as new, so a fresh attach acts immediately instead of idling up to a full
bar.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:50:27 -04:00 | | | //--- 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;
|
2026-07-13 03:23:39 -04:00 | | | 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;
|
 feat(vote): derive the threshold instead of configuring it
Signal_ThresholdOpen becomes a seed. The era verdict now picks the HIGHEST
sweep rung whose vote still clears the whole deploy gate - coverage floor,
exact-binomial precision bar and two-sidedness together - computes the era's
verdict AT that rung, and publishes it to the live signal's m_threshold_open
so the bar the gate certifies is the bar the EA trades.
Measured on 619 era verdicts across all six live charts:
* every era on every symbol had at least one rung clearing the full gate.
At the fixed 25% the fleet was actually running, four of six symbols had
none, ever. The threshold, not the models, was the blocker.
* walk-forward (rung derived on era N, scored on era N+1): 10.2% coverage /
31.8% precision, against an oracle re-picking on N+1 of 10.3% / 31.7%.
Near-zero shrinkage - a measurement, not a fit. It holds because the
binding constraint is COVERAGE, a near-deterministic step function of the
vote distribution, not precision.
* vs a fixed 15% (best global value): +0.6pp precision, 3.4pp less coverage.
vs a fixed 20%: deployable on all six rather than four of six.
Selection on the highest PASSING rung, never on the best-precision rung - that
is a best-of-6 on a noisy statistic and this project has crowned noise that way
four times. The multiplicity that remains is paid for: nTried in
EnsembleSurvivesSelection is now eras x rungs. Costs nothing - all six charts
clear it by 6.5-12 sigma even forming z on effective rather than raw calls.
Also fixes, in the same path: the direction-policy gate is hoisted above the
per-rung tally so every rung is scored on the population the gate certifies.
Retrain-neutral: not in BuildModelFingerprint(), no .nnw re-keyed.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:02:58 -04:00 | | | //--- 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);
|
 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 | | | //--- 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__,
|
 feat(vote): derive the threshold instead of configuring it
Signal_ThresholdOpen becomes a seed. The era verdict now picks the HIGHEST
sweep rung whose vote still clears the whole deploy gate - coverage floor,
exact-binomial precision bar and two-sidedness together - computes the era's
verdict AT that rung, and publishes it to the live signal's m_threshold_open
so the bar the gate certifies is the bar the EA trades.
Measured on 619 era verdicts across all six live charts:
* every era on every symbol had at least one rung clearing the full gate.
At the fixed 25% the fleet was actually running, four of six symbols had
none, ever. The threshold, not the models, was the blocker.
* walk-forward (rung derived on era N, scored on era N+1): 10.2% coverage /
31.8% precision, against an oracle re-picking on N+1 of 10.3% / 31.7%.
Near-zero shrinkage - a measurement, not a fit. It holds because the
binding constraint is COVERAGE, a near-deterministic step function of the
vote distribution, not precision.
* vs a fixed 15% (best global value): +0.6pp precision, 3.4pp less coverage.
vs a fixed 20%: deployable on all six rather than four of six.
Selection on the highest PASSING rung, never on the best-precision rung - that
is a best-of-6 on a noisy statistic and this project has crowned noise that way
four times. The multiplicity that remains is paid for: nTried in
EnsembleSurvivesSelection is now eras x rungs. Costs nothing - all six charts
clear it by 6.5-12 sigma even forming z on effective rather than raw calls.
Also fixes, in the same path: the direction-policy gate is hoisted above the
per-rung tally so every rung is scored on the population the gate certifies.
Retrain-neutral: not in BuildModelFingerprint(), no .nnw re-keyed.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:02:58 -04:00 | | | threshold, (m_publishedVoteThreshold < 0 ? "the Signal_ThresholdOpen seed"
|
 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 | | | : IntegerToString(m_publishedVoteThreshold) + "%"),
|
| | | (Exit_On_Reversal_Vote ? StringFormat("ARMED at the same %d%%", threshold)
|
| | | : "OFF (hold to the barrier)"));
|
 feat(vote): derive the threshold instead of configuring it
Signal_ThresholdOpen becomes a seed. The era verdict now picks the HIGHEST
sweep rung whose vote still clears the whole deploy gate - coverage floor,
exact-binomial precision bar and two-sidedness together - computes the era's
verdict AT that rung, and publishes it to the live signal's m_threshold_open
so the bar the gate certifies is the bar the EA trades.
Measured on 619 era verdicts across all six live charts:
* every era on every symbol had at least one rung clearing the full gate.
At the fixed 25% the fleet was actually running, four of six symbols had
none, ever. The threshold, not the models, was the blocker.
* walk-forward (rung derived on era N, scored on era N+1): 10.2% coverage /
31.8% precision, against an oracle re-picking on N+1 of 10.3% / 31.7%.
Near-zero shrinkage - a measurement, not a fit. It holds because the
binding constraint is COVERAGE, a near-deterministic step function of the
vote distribution, not precision.
* vs a fixed 15% (best global value): +0.6pp precision, 3.4pp less coverage.
vs a fixed 20%: deployable on all six rather than four of six.
Selection on the highest PASSING rung, never on the best-precision rung - that
is a best-of-6 on a noisy statistic and this project has crowned noise that way
four times. The multiplicity that remains is paid for: nTried in
EnsembleSurvivesSelection is now eras x rungs. Costs nothing - all six charts
clear it by 6.5-12 sigma even forming z on effective rather than raw calls.
Also fixes, in the same path: the direction-policy gate is hoisted above the
per-rung tally so every rung is scored on the population the gate certifies.
Retrain-neutral: not in BuildModelFingerprint(), no .nnw re-keyed.
Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:02:58 -04:00 | | | m_publishedVoteThreshold = threshold;
|
| | | }
|
 feat(panel): commands reach signals down the filter tree, not through a registry
The control panel drove training by looping g_aiSignals[] - a
hand-maintained, MAX_AI_SIGNALS-capped, AI-only registry that had
already dropped an ensemble member on the floor once (609be10). A model
missing from it still trains and still votes, it just cannot be paused,
stopped, deployed or reset, and every button label is computed from the
same short list, so the panel described one set of models while acting
on another. Classic signals could not respond to a panel action at all.
Commands now walk the signal tree CExpert already owns:
Expert.DispatchSignalCommand(cmd) -> root signal -> every filter,
recursively, returning how many actually acted.
CExpertSignalCustom carries the seam (OnSignalCommand / HasSignalTrait,
both no-ops by default), so a classic signal opts in by overriding two
methods and needs no registration and no cap. CExpertSignalAIBase
implements the training commands over its existing Pause/Stop/Deploy/
Reset methods - the behaviour is unchanged, only its reach reported.
Button labels ask the same tree via CountSignalTrait, with
SIGTRAIT_TRAINABLE as an explicit denominator: "all paused" is
meaningless without knowing how many could be paused. Pause/Stop resolve
their toggle direction ONCE in the EA and hand every model the same
plain command, instead of each re-deriving the direction from its own
local state - which is how a mixed set ends up half paused. The alerts
now report the count acted on rather than assuming it.
Two dispatch bugs found on the way, both from a database guard copied
onto event delivery: CExpertSignalCustom::OnTickHandler and
::OnChartEventHandler each skipped any filter whose GetFilterID() is
"NULL". That id is a DB folder name, and CSignalNewsFilter,
CSignalSessionFilter and CSignalRiskGuard never set one - so all three
were silently receiving neither ticks nor chart events. The guard stays
where it belongs, on the paths that write pattern tables.
ENUM_CP_ACTION moves to Enumerations\GlobalEnums.mqh (now include-
guarded) because the Expert bases have to name it and the panel is
included long after them.
The AI-only lifecycle loops - PollTraining, the weight autosave,
AltDataReload, OnDeinit's shutdown cascade - still use g_aiSignals[] and
are untouched here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:05:50 -04:00 | | | //--- 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);
|
| | | }
|
2026-07-13 03:23:39 -04:00 | | | //--- 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;
|
| | | }
|
 feat(vote): backfill the ensemble win-rate record from the overlay sweep
"Vote win rate: measuring..." never resolved on a deployed chart whose
.stats predate the WST7 ensemble record: g_ensCumOosTotal is fed only by
the era-end combined-vote scorer (Training.mqh), and a deployed ensemble
runs no further eras. The replay pass rebuilt every MEMBER's ladder
(64-71% each, per the 16:12 log) but nothing ever scored the COMBINED
vote, so the aggregate line sat on "measuring" while 300+ arrows drew.
The overlay sweep already reconstructs the vote per bar with the live
threshold and direction policy - so it now also tallies, BEFORE
declustering (NMS thins arrows, not calls), each threshold-clearing bar
against the inline swing-pivot label (same resolution ScoreReplayFromCache
uses, same window-mismatch reason). On sweep completion Warrior_EA.mq5
harvests the tally through a consuming one-shot read and adopts it ONLY
when the record is empty and the models are deployed - a training-time
sweep can never pre-empt the era scorer, and a restored record always
wins. The result is persisted immediately into every member's .stats.
Also verified against the same log: the sweep does NOT ignore
DrawUnfilteredSignals - 4986 voter bars -> ~300 arrows, all gated on the
25% open threshold. The arrow increase vs the restored set (41-312 saved)
is the replay-minted ladder reading stronger (partly in-sample), plus the
reconstruction deliberately not replaying order validation/session hours
(tooltip says so); the backfilled record carries the same caveat and is
labelled so in the log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:22:12 -04:00 | | | //--- 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;
|
| | | }
|
2026-07-13 03:23:39 -04:00 | | | protected:
|
| | | //--- refreshing
|
| | | virtual bool Refresh(void) override;
|
| | | //--- processing (main method)
|
| | | virtual bool Processing(void) override;
|
 feat(trade): two books per symbol, and delete the vote exit
Allow_Hedging (default ON, live only on a RETAIL_HEDGING account) gives the EA
an independent long book and short book on its symbol: at most one long and at
most one short, each opened on its own side's vote and each held to its own
barrier. On a netting account, or with the input off, the original
single-position path runs bit-for-bit unchanged and init says which one is live.
WHY THIS INSTEAD OF A VOTE EXIT. The deploy gate certifies
P(label agrees | vote fired) and the label runs to the barrier, so closing early
on a reversal makes the realised outcome stop being the labelled one - the
certified precision no longer describes what is traded. Opening the other side
acts on the new signal and leaves the old position's certification intact, and
costs no more than reversing: both pay the new side's spread, the difference is
only that the existing position runs on to a barrier already measured as
positive-expectancy. So Signal_ThresholdClose is DELETED rather than tuned,
along with its SIGNAL_CLOSE_PRESETS enum; the threshold is pinned to an
arithmetically unreachable 101 (the stock default of 100 is reachable by a
weighted mean of values capped at 100).
Note the two books can never both fill from one signal: CheckOpenLong and
CheckOpenShort test opposite signs of the same m_direction, so at most one clears
per tick. A hedge only forms when a LATER opposite vote fires - which is what
keeps it from being a guaranteed-loss wash pair.
The mechanism is a SelectPosition() override keyed on the active book's magic;
every inherited close/trail path then operates on that book untouched. The long
book keeps Expert_MagicNumber, so no existing position, journal row or
risk-budget state file is re-addressed. Short book is +1.
Four ownership filters had to widen from "== m_magic" to WarriorOwnsMagic(),
or the short book would have been invisible to the code that must reach it:
the scheduled close-all (positions and orders), the risk budget's emergency
flatten, and the journal's MAE/MFE walk. WarriorOwnsMagic() is deliberately NOT
gated on Allow_Hedging - turning the input off while a short-book position is
open would otherwise orphan it with nothing left to close it.
Risk sizing needed no change: CapRiskAmount already subtracts OpenRiskAtStops(),
which counts every position regardless of magic, so the second book is sized
inside what the first one left. Conservative for a hedged pair, which cannot
lose both stops - the safe direction.
Retrain-neutral: neither input is in BuildModelFingerprint() or
ComputeDbConfigFingerprint(). Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:20:35 -04:00 | | | //--- 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);
|
2026-08-22 00:25:52 -04:00 | | | //--- 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.
|
2026-07-26 23:08:32 -04:00 | | | 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;
|
2026-07-13 03:23:39 -04:00 | | | };
|
| | | //+------------------------------------------------------------------+
|
| | | //| Constructor |
|
| | | //+------------------------------------------------------------------+
|
 feat(trade): two books per symbol, and delete the vote exit
Allow_Hedging (default ON, live only on a RETAIL_HEDGING account) gives the EA
an independent long book and short book on its symbol: at most one long and at
most one short, each opened on its own side's vote and each held to its own
barrier. On a netting account, or with the input off, the original
single-position path runs bit-for-bit unchanged and init says which one is live.
WHY THIS INSTEAD OF A VOTE EXIT. The deploy gate certifies
P(label agrees | vote fired) and the label runs to the barrier, so closing early
on a reversal makes the realised outcome stop being the labelled one - the
certified precision no longer describes what is traded. Opening the other side
acts on the new signal and leaves the old position's certification intact, and
costs no more than reversing: both pay the new side's spread, the difference is
only that the existing position runs on to a barrier already measured as
positive-expectancy. So Signal_ThresholdClose is DELETED rather than tuned,
along with its SIGNAL_CLOSE_PRESETS enum; the threshold is pinned to an
arithmetically unreachable 101 (the stock default of 100 is reachable by a
weighted mean of values capped at 100).
Note the two books can never both fill from one signal: CheckOpenLong and
CheckOpenShort test opposite signs of the same m_direction, so at most one clears
per tick. A hedge only forms when a LATER opposite vote fires - which is what
keeps it from being a guaranteed-loss wash pair.
The mechanism is a SelectPosition() override keyed on the active book's magic;
every inherited close/trail path then operates on that book untouched. The long
book keeps Expert_MagicNumber, so no existing position, journal row or
risk-budget state file is re-addressed. Short book is +1.
Four ownership filters had to widen from "== m_magic" to WarriorOwnsMagic(),
or the short book would have been invisible to the code that must reach it:
the scheduled close-all (positions and orders), the risk budget's emergency
flatten, and the journal's MAE/MFE walk. WarriorOwnsMagic() is deliberately NOT
gated on Allow_Hedging - turning the input off while a short-book position is
open would otherwise orphan it with nothing left to close it.
Risk sizing needed no change: CapRiskAmount already subtracts OpenRiskAtStops(),
which counts every position regardless of magic, so the second book is sized
inside what the first one left. Conservative for a hedged pair, which cannot
lose both stops - the safe direction.
Retrain-neutral: neither input is in BuildModelFingerprint() or
ComputeDbConfigFingerprint(). Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:20:35 -04:00 | | | CExpertCustom::CExpertCustom(void) : m_publishedVoteThreshold(-1),
|
| | | m_activeBookLong(true)
|
2026-07-13 03:23:39 -04:00 | | | {
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| 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;
|
2026-08-22 00:25:52 -04:00 | | | //--- Article 2555 #8 - errors caused by insufficient quote history.
|
2026-07-26 23:08:32 -04:00 | | | 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);
|
| | | }
|
2026-07-13 03:23:39 -04:00 | | | //--- 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)
|
| | | {
|
 perf(tester): stop agents doing chart work on deinit; enforce Expert_EveryTick
Three related changes, all aimed at work being repeated at a frequency
nobody chose.
1. OnDeinit gets a tester/optimizer fast path.
Everything in the live teardown exists to leave a CHART clean and a live
model's state on disk. An optimization agent has neither. It was still
running, on EVERY pass: a per-signal arrow-sidecar WRITE
(ShutdownChartCleanup -> PersistAndClearChartSignals) plus two full
chart-object scans plus a ChartRedraw. At optimization scale that is
hundreds of thousands of pointless file writes per agent, against a
~4,500 ms budget MetaTrader force-terminates on - the shape of thing
that stalls an agent rather than failing it.
The fast path keeps MarkShutdown() and FlushTrainRun() (so a killed pass
never leaves a half-written era) and still calls dbm.Deinit() and
Expert.Deinit() - leaking the signal tree or a handle across passes is
its own way to accumulate into a stall. The two now-unreachable
!isTesterRun guards further down are folded away.
2. All four tester handlers are present and documented by WHERE THEY RUN.
OnTesterInit/OnTesterPass/OnTesterDeinit run in the CONTROLLING TERMINAL
once per session; only OnTester runs on the agent, per pass. OnTesterPass
was missing entirely - added empty and deliberately so: it only fires for
passes that shipped FrameAdd() data, which this EA never sends, and
reading frames there would put per-pass work on the terminal's critical
path. Declared so that adding frame-sending later fails loudly instead of
silently dropping every frame.
3. Expert_EveryTick is now actually enforced.
It was passed to Expert.Init() and only ever reached StartIndex() - which
bar a signal READS. The whole pipeline still ran on every quote. It now
gates m_signal.SetDirection() in CExpertCustom::Processing(): that call
drives Direction(), which is a TRANSACTION (NN forward passes, DB rows,
chart arrows, one-shot vote state), and re-running it on every tick of a
4-hour bar repeats all of it.
Scoped deliberately. Everything after that line still runs per tick -
CheckReverse/CheckClose/CheckTrailingStop and pending-order maintenance
are risk management, and a stop that only trails at bar boundaries is a
different strategy, not a faster one. The scheduled close-all in OnTick()
matches a +-1 MINUTE window, so bar-gating it on H4 would step straight
over the thing 100% of label timeouts already resolve against.
g_riskBudget.Update() also stays at quote frequency, by design.
System/NewBar.mqh becomes CNewBar, a class. The free function it replaced
had zero callers and kept its watermark in a `static`: ONE watermark
shared by every caller, so the first caller each tick consumed the
transition and every other caller was told "no new bar" for a bar that
had just opened. Per-instance state fixes that; first observation counts
as new, so a fresh attach acts immediately instead of idling up to a full
bar.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:50:27 -04:00 | | | //--- 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.
|
 fix(exits): never let a declined tick leave an open position unchecked
Refresh() returning false skipped 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 already-open position got no exit check at all -
it rode. Invisible by construction: nothing logged, no order sent, and
next tick the position looks exactly as it should. The only trace is a
stop that should have moved and didn't.
ProtectOpenPosition() now runs the CLOSING half of Processing() on those
ticks. Only the closing half, on purpose: CheckReverse() and the
pending-order block both OPEN exposure, and opening on data just declared
unfit to trade on is the opposite of the point. Closing on an imperfect
quote reduces risk even when the quote is wrong; opening on it does not.
When it acts, it says so in the journal - a degraded-path exit should
never be silent.
Also pins the invariant at the Expert_EveryTick gate: that input
throttles how often the EA forms an OPINION, never how often it can act
on a position it already holds. Exits stay above the gate, and the
comment now says so to the next person editing it.
Pre-existing hole, not introduced by the EveryTick work in 6d48fdb -
that change is what made it worth reading the tick path closely.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:52:07 -04:00 | | | //--- 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.
|
 perf(tester): stop agents doing chart work on deinit; enforce Expert_EveryTick
Three related changes, all aimed at work being repeated at a frequency
nobody chose.
1. OnDeinit gets a tester/optimizer fast path.
Everything in the live teardown exists to leave a CHART clean and a live
model's state on disk. An optimization agent has neither. It was still
running, on EVERY pass: a per-signal arrow-sidecar WRITE
(ShutdownChartCleanup -> PersistAndClearChartSignals) plus two full
chart-object scans plus a ChartRedraw. At optimization scale that is
hundreds of thousands of pointless file writes per agent, against a
~4,500 ms budget MetaTrader force-terminates on - the shape of thing
that stalls an agent rather than failing it.
The fast path keeps MarkShutdown() and FlushTrainRun() (so a killed pass
never leaves a half-written era) and still calls dbm.Deinit() and
Expert.Deinit() - leaking the signal tree or a handle across passes is
its own way to accumulate into a stall. The two now-unreachable
!isTesterRun guards further down are folded away.
2. All four tester handlers are present and documented by WHERE THEY RUN.
OnTesterInit/OnTesterPass/OnTesterDeinit run in the CONTROLLING TERMINAL
once per session; only OnTester runs on the agent, per pass. OnTesterPass
was missing entirely - added empty and deliberately so: it only fires for
passes that shipped FrameAdd() data, which this EA never sends, and
reading frames there would put per-pass work on the terminal's critical
path. Declared so that adding frame-sending later fails loudly instead of
silently dropping every frame.
3. Expert_EveryTick is now actually enforced.
It was passed to Expert.Init() and only ever reached StartIndex() - which
bar a signal READS. The whole pipeline still ran on every quote. It now
gates m_signal.SetDirection() in CExpertCustom::Processing(): that call
drives Direction(), which is a TRANSACTION (NN forward passes, DB rows,
chart arrows, one-shot vote state), and re-running it on every tick of a
4-hour bar repeats all of it.
Scoped deliberately. Everything after that line still runs per tick -
CheckReverse/CheckClose/CheckTrailingStop and pending-order maintenance
are risk management, and a stop that only trails at bar boundaries is a
different strategy, not a faster one. The scheduled close-all in OnTick()
matches a +-1 MINUTE window, so bar-gating it on H4 would step straight
over the thing 100% of label timeouts already resolve against.
g_riskBudget.Update() also stays at quote frequency, by design.
System/NewBar.mqh becomes CNewBar, a class. The free function it replaced
had zero callers and kept its watermark in a `static`: ONE watermark
shared by every caller, so the first caller each tick consumed the
transition and every other caller was told "no new bar" for a bar that
had just opened. Per-instance state fixes that; first observation counts
as new, so a fresh attach acts immediately instead of idling up to a full
bar.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:50:27 -04:00 | | | if(m_every_tick || m_newBar.IsNewBar(m_symbol.Name(), m_period))
|
| | | m_signal.SetDirection();
|
 feat(trade): two books per symbol, and delete the vote exit
Allow_Hedging (default ON, live only on a RETAIL_HEDGING account) gives the EA
an independent long book and short book on its symbol: at most one long and at
most one short, each opened on its own side's vote and each held to its own
barrier. On a netting account, or with the input off, the original
single-position path runs bit-for-bit unchanged and init says which one is live.
WHY THIS INSTEAD OF A VOTE EXIT. The deploy gate certifies
P(label agrees | vote fired) and the label runs to the barrier, so closing early
on a reversal makes the realised outcome stop being the labelled one - the
certified precision no longer describes what is traded. Opening the other side
acts on the new signal and leaves the old position's certification intact, and
costs no more than reversing: both pay the new side's spread, the difference is
only that the existing position runs on to a barrier already measured as
positive-expectancy. So Signal_ThresholdClose is DELETED rather than tuned,
along with its SIGNAL_CLOSE_PRESETS enum; the threshold is pinned to an
arithmetically unreachable 101 (the stock default of 100 is reachable by a
weighted mean of values capped at 100).
Note the two books can never both fill from one signal: CheckOpenLong and
CheckOpenShort test opposite signs of the same m_direction, so at most one clears
per tick. A hedge only forms when a LATER opposite vote fires - which is what
keeps it from being a guaranteed-loss wash pair.
The mechanism is a SelectPosition() override keyed on the active book's magic;
every inherited close/trail path then operates on that book untouched. The long
book keeps Expert_MagicNumber, so no existing position, journal row or
risk-budget state file is re-addressed. Short book is +1.
Four ownership filters had to widen from "== m_magic" to WarriorOwnsMagic(),
or the short book would have been invisible to the code that must reach it:
the scheduled close-all (positions and orders), the risk budget's emergency
flatten, and the journal's MAE/MFE walk. WarriorOwnsMagic() is deliberately NOT
gated on Allow_Hedging - turning the input off while a short-book position is
open would otherwise orphan it with nothing left to close it.
Risk sizing needed no change: CapRiskAmount already subtracts OpenRiskAtStops(),
which counts every position regardless of magic, so the second book is sized
inside what the first one left. Conservative for a hedged pair, which cannot
lose both stops - the safe direction.
Retrain-neutral: neither input is in BuildModelFingerprint() or
ComputeDbConfigFingerprint(). Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:20:35 -04:00 | | | //--- 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());
|
2026-07-13 03:23:39 -04:00 | | | //--- 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);
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
 feat(trade): two books per symbol, and delete the vote exit
Allow_Hedging (default ON, live only on a RETAIL_HEDGING account) gives the EA
an independent long book and short book on its symbol: at most one long and at
most one short, each opened on its own side's vote and each held to its own
barrier. On a netting account, or with the input off, the original
single-position path runs bit-for-bit unchanged and init says which one is live.
WHY THIS INSTEAD OF A VOTE EXIT. The deploy gate certifies
P(label agrees | vote fired) and the label runs to the barrier, so closing early
on a reversal makes the realised outcome stop being the labelled one - the
certified precision no longer describes what is traded. Opening the other side
acts on the new signal and leaves the old position's certification intact, and
costs no more than reversing: both pay the new side's spread, the difference is
only that the existing position runs on to a barrier already measured as
positive-expectancy. So Signal_ThresholdClose is DELETED rather than tuned,
along with its SIGNAL_CLOSE_PRESETS enum; the threshold is pinned to an
arithmetically unreachable 101 (the stock default of 100 is reachable by a
weighted mean of values capped at 100).
Note the two books can never both fill from one signal: CheckOpenLong and
CheckOpenShort test opposite signs of the same m_direction, so at most one clears
per tick. A hedge only forms when a LATER opposite vote fires - which is what
keeps it from being a guaranteed-loss wash pair.
The mechanism is a SelectPosition() override keyed on the active book's magic;
every inherited close/trail path then operates on that book untouched. The long
book keeps Expert_MagicNumber, so no existing position, journal row or
risk-budget state file is re-addressed. Short book is +1.
Four ownership filters had to widen from "== m_magic" to WarriorOwnsMagic(),
or the short book would have been invisible to the code that must reach it:
the scheduled close-all (positions and orders), the risk budget's emergency
flatten, and the journal's MAE/MFE walk. WarriorOwnsMagic() is deliberately NOT
gated on Allow_Hedging - turning the input off while a short-book position is
open would otherwise orphan it with nothing left to close it.
Risk sizing needed no change: CapRiskAmount already subtracts OpenRiskAtStops(),
which counts every position regardless of magic, so the second book is sized
inside what the first one left. Conservative for a hedged pair, which cannot
lose both stops - the safe direction.
Retrain-neutral: neither input is in BuildModelFingerprint() or
ComputeDbConfigFingerprint(). Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:20:35 -04:00 | | | //| 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);
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
2026-07-26 23:08:32 -04:00 | | | //| 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)
|
| | | {
|
2026-08-24 01:39:19 -04:00 | | | return TCResolveOrderType(m_symbol.Name(), isLong, price, m_symbol.Ask(), m_symbol.Bid());
|
2026-07-26 23:08:32 -04:00 | | | }
|
| | | //+------------------------------------------------------------------+
|
2026-08-24 02:00:10 -04:00 | | | //| Final pre-send gate for an entry (article #4/#6/#14). Shared by |
|
| | | //| both OpenLong/OpenShort - see the class declaration's comment. |
|
2026-07-26 23:08:32 -04:00 | | | //+------------------------------------------------------------------+
|
2026-08-24 02:00:10 -04:00 | | | bool CExpertCustom::OpenPosition(bool isLong, double price, double sl, double tp, string caller)
|
2026-07-26 23:08:32 -04:00 | | | {
|
| | | 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))
|
| | | {
|
2026-08-24 02:00:10 -04:00 | | | TCLog("open-symbol:" + m_symbol.Name(), caller + ": withheld - " + reason);
|
2026-07-26 23:08:32 -04:00 | | | return(false);
|
| | | }
|
2026-08-24 02:00:10 -04:00 | | | ENUM_ORDER_TYPE type = ResolveOrderType(isLong, price);
|
| | | ENUM_ORDER_TYPE marketType = isLong ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
|
| | | if(type != marketType)
|
2026-07-26 23:08:32 -04:00 | | | {
|
| | | //--- #4: the account's ACCOUNT_LIMIT_ORDERS cap applies to pending orders only
|
| | | if(!TCIsNewOrderAllowed(reason))
|
| | | {
|
2026-08-24 02:00:10 -04:00 | | | TCLog("open-orderlimit", caller + ": withheld - " + reason);
|
2026-07-26 23:08:32 -04:00 | | | return(false);
|
| | | }
|
| | | //--- #6: the activation price must still clear the stops level after the price moved
|
| | | if(!TCCheckPendingPrice(m_symbol.Name(), type, price, reason))
|
| | | {
|
2026-08-24 02:00:10 -04:00 | | | TCLog("open-pendingprice:" + m_symbol.Name(), caller + ": withheld - " + reason);
|
2026-07-26 23:08:32 -04:00 | | | 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))
|
| | | {
|
2026-08-24 02:00:10 -04:00 | | | TCLog("open-stops:" + m_symbol.Name(), caller + ": withheld - " + reason);
|
2026-07-26 23:08:32 -04:00 | | | return(false);
|
| | | }
|
2026-08-24 02:00:10 -04:00 | | | 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__);
|
2026-07-26 23:08:32 -04:00 | | | }
|
| | | bool CExpertCustom::OpenShort(double price, double sl, double tp)
|
| | | {
|
2026-08-24 02:00:10 -04:00 | | | return OpenPosition(false, price, sl, tp, __FUNCTION__);
|
2026-07-26 23:08:32 -04:00 | | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| 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);
|
 feat(trade): two books per symbol, and delete the vote exit
Allow_Hedging (default ON, live only on a RETAIL_HEDGING account) gives the EA
an independent long book and short book on its symbol: at most one long and at
most one short, each opened on its own side's vote and each held to its own
barrier. On a netting account, or with the input off, the original
single-position path runs bit-for-bit unchanged and init says which one is live.
WHY THIS INSTEAD OF A VOTE EXIT. The deploy gate certifies
P(label agrees | vote fired) and the label runs to the barrier, so closing early
on a reversal makes the realised outcome stop being the labelled one - the
certified precision no longer describes what is traded. Opening the other side
acts on the new signal and leaves the old position's certification intact, and
costs no more than reversing: both pay the new side's spread, the difference is
only that the existing position runs on to a barrier already measured as
positive-expectancy. So Signal_ThresholdClose is DELETED rather than tuned,
along with its SIGNAL_CLOSE_PRESETS enum; the threshold is pinned to an
arithmetically unreachable 101 (the stock default of 100 is reachable by a
weighted mean of values capped at 100).
Note the two books can never both fill from one signal: CheckOpenLong and
CheckOpenShort test opposite signs of the same m_direction, so at most one clears
per tick. A hedge only forms when a LATER opposite vote fires - which is what
keeps it from being a guaranteed-loss wash pair.
The mechanism is a SelectPosition() override keyed on the active book's magic;
every inherited close/trail path then operates on that book untouched. The long
book keeps Expert_MagicNumber, so no existing position, journal row or
risk-budget state file is re-addressed. Short book is +1.
Four ownership filters had to widen from "== m_magic" to WarriorOwnsMagic(),
or the short book would have been invisible to the code that must reach it:
the scheduled close-all (positions and orders), the risk budget's emergency
flatten, and the journal's MAE/MFE walk. WarriorOwnsMagic() is deliberately NOT
gated on Allow_Hedging - turning the input off while a short-book position is
open would otherwise orphan it with nothing left to close it.
Risk sizing needed no change: CapRiskAmount already subtracts OpenRiskAtStops(),
which counts every position regardless of magic, so the second book is sized
inside what the first one left. Conservative for a hedged pair, which cannot
lose both stops - the safe direction.
Retrain-neutral: neither input is in BuildModelFingerprint() or
ComputeDbConfigFingerprint(). Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:20:35 -04:00 | | | //--- 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);
|
 fix(exits): hold-to-barrier policy for fractal-target charts - live trades now match the certificate
The first-ever family-wise gate pass (SP500 D1 PAI, +10.4pp, p=0.0081)
certifies a win rate measured on HOLD-TO-RESOLUTION outcomes: entry,
then the measured SL or TP decides. Live, three vote-driven exit routes
could close earlier - the averaged-vote close, the AI early-exit route
(both in CheckClosePosition), and CheckReverse - and the fractal
target's vote flips at swing-marker cadence (~3-5 bars), far inside the
barrier's typical travel time (median 7-8 D1 bars to target). The user
observed exactly this: an opposite arrow near an entry, trade cut,
price kept going.
On a fractal-target chart with a live direction model, all three routes
are now suppressed (m_holdToBarrier, set in InitializeSignal, loudly
logged): positions run to their broker SL/TP. Risk guards and trailing
are deliberately untouched - account protection is not signal opinion.
Barrier-target models keep the vote exits: their label is the vote's
own horizon, so for them the routes are semantically consistent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 16:18:11 -04:00 | | | //--- 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);
|
2026-07-26 23:08:32 -04:00 | | | 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 |
|
2026-08-24 02:00:10 -04:00 | | | //| with the article's one-point tolerance instead. Shared by both |
|
| | | //| TrailingStopLong/Short - see the class declaration's comment. |
|
2026-07-26 23:08:32 -04:00 | | | //+------------------------------------------------------------------+
|
2026-08-24 02:00:10 -04:00 | | | bool CExpertCustom::TrailingStopCommon(bool isLong, double sl, double tp, string caller)
|
2026-07-26 23:08:32 -04:00 | | | {
|
| | | 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;
|
2026-08-24 02:00:10 -04:00 | | | ENUM_ORDER_TYPE type = isLong ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
|
| | | if(!TCCheckStops(m_position.Symbol(), type, 0.0, sl, tp, reason))
|
2026-07-26 23:08:32 -04:00 | | | {
|
2026-08-24 02:00:10 -04:00 | | | TCLog("trail-stops-" + (isLong ? "long" : "short") + ":" + m_position.Symbol(), caller + ": withheld - " + reason);
|
2026-07-26 23:08:32 -04:00 | | | return(false);
|
| | | }
|
2026-08-24 02:00:10 -04:00 | | | return isLong ? CExpert::TrailingStopLong(sl, tp) : CExpert::TrailingStopShort(sl, tp);
|
| | | }
|
| | | bool CExpertCustom::TrailingStopLong(double sl, double tp)
|
| | | {
|
| | | return TrailingStopCommon(true, sl, tp, __FUNCTION__);
|
2026-07-26 23:08:32 -04:00 | | | }
|
| | | bool CExpertCustom::TrailingStopShort(double sl, double tp)
|
| | | {
|
2026-08-24 02:00:10 -04:00 | | | return TrailingStopCommon(false, sl, tp, __FUNCTION__);
|
2026-07-26 23:08:32 -04:00 | | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| 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) |
|
2026-08-24 02:00:10 -04:00 | | | //| produced a guaranteed-rejected request. Shared by both |
|
| | | //| TrailingOrderLong/Short - see the class declaration's comment. |
|
2026-07-26 23:08:32 -04:00 | | | //+------------------------------------------------------------------+
|
2026-08-24 02:00:10 -04:00 | | | bool CExpertCustom::TrailingOrderCommon(bool isLong, double delta, string caller)
|
2026-07-26 23:08:32 -04:00 | | | {
|
| | | 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))
|
| | | {
|
2026-08-24 02:00:10 -04:00 | | | TCLog("trail-order-" + (isLong ? "long" : "short") + ":" + symbol, caller + ": withheld - " + reason);
|
2026-07-26 23:08:32 -04:00 | | | return(false);
|
| | | }
|
2026-08-24 02:00:10 -04:00 | | | return isLong ? CExpert::TrailingOrderLong(delta) : CExpert::TrailingOrderShort(delta);
|
| | | }
|
| | | bool CExpertCustom::TrailingOrderLong(double delta)
|
| | | {
|
| | | return TrailingOrderCommon(true, delta, __FUNCTION__);
|
2026-07-26 23:08:32 -04:00 | | | }
|
| | | bool CExpertCustom::TrailingOrderShort(double delta)
|
| | | {
|
2026-08-24 02:00:10 -04:00 | | | return TrailingOrderCommon(false, delta, __FUNCTION__);
|
2026-07-26 23:08:32 -04:00 | | | }
|
| | | //+------------------------------------------------------------------+
|
 fix(exits): never let a declined tick leave an open position unchecked
Refresh() returning false skipped 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 already-open position got no exit check at all -
it rode. Invisible by construction: nothing logged, no order sent, and
next tick the position looks exactly as it should. The only trace is a
stop that should have moved and didn't.
ProtectOpenPosition() now runs the CLOSING half of Processing() on those
ticks. Only the closing half, on purpose: CheckReverse() and the
pending-order block both OPEN exposure, and opening on data just declared
unfit to trade on is the opposite of the point. Closing on an imperfect
quote reduces risk even when the quote is wrong; opening on it does not.
When it acts, it says so in the journal - a degraded-path exit should
never be silent.
Also pins the invariant at the Expert_EveryTick gate: that input
throttles how often the EA forms an OPINION, never how often it can act
on a position it already holds. Exits stay above the gate, and the
comment now says so to the next person editing it.
Pre-existing hole, not introduced by the EveryTick work in 6d48fdb -
that change is what made it worth reading the tick path closely.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:52:07 -04:00 | | | //| 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();
|
 feat(trade): two books per symbol, and delete the vote exit
Allow_Hedging (default ON, live only on a RETAIL_HEDGING account) gives the EA
an independent long book and short book on its symbol: at most one long and at
most one short, each opened on its own side's vote and each held to its own
barrier. On a netting account, or with the input off, the original
single-position path runs bit-for-bit unchanged and init says which one is live.
WHY THIS INSTEAD OF A VOTE EXIT. The deploy gate certifies
P(label agrees | vote fired) and the label runs to the barrier, so closing early
on a reversal makes the realised outcome stop being the labelled one - the
certified precision no longer describes what is traded. Opening the other side
acts on the new signal and leaves the old position's certification intact, and
costs no more than reversing: both pay the new side's spread, the difference is
only that the existing position runs on to a barrier already measured as
positive-expectancy. So Signal_ThresholdClose is DELETED rather than tuned,
along with its SIGNAL_CLOSE_PRESETS enum; the threshold is pinned to an
arithmetically unreachable 101 (the stock default of 100 is reachable by a
weighted mean of values capped at 100).
Note the two books can never both fill from one signal: CheckOpenLong and
CheckOpenShort test opposite signs of the same m_direction, so at most one clears
per tick. A hedge only forms when a LATER opposite vote fires - which is what
keeps it from being a guaranteed-loss wash pair.
The mechanism is a SelectPosition() override keyed on the active book's magic;
every inherited close/trail path then operates on that book untouched. The long
book keeps Expert_MagicNumber, so no existing position, journal row or
risk-budget state file is re-addressed. Short book is +1.
Four ownership filters had to widen from "== m_magic" to WarriorOwnsMagic(),
or the short book would have been invisible to the code that must reach it:
the scheduled close-all (positions and orders), the risk budget's emergency
flatten, and the journal's MAE/MFE walk. WarriorOwnsMagic() is deliberately NOT
gated on Allow_Hedging - turning the input off while a short-book position is
open would otherwise orphan it with nothing left to close it.
Risk sizing needed no change: CapRiskAmount already subtracts OpenRiskAtStops(),
which counts every position regardless of magic, so the second book is sized
inside what the first one left. Conservative for a hedged pair, which cannot
lose both stops - the safe direction.
Retrain-neutral: neither input is in BuildModelFingerprint() or
ComputeDbConfigFingerprint(). Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:20:35 -04:00 | | | //--- 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);
|
| | | }
|
 fix(exits): never let a declined tick leave an open position unchecked
Refresh() returning false skipped 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 already-open position got no exit check at all -
it rode. Invisible by construction: nothing logged, no order sent, and
next tick the position looks exactly as it should. The only trace is a
stop that should have moved and didn't.
ProtectOpenPosition() now runs the CLOSING half of Processing() on those
ticks. Only the closing half, on purpose: CheckReverse() and the
pending-order block both OPEN exposure, and opening on data just declared
unfit to trade on is the opposite of the point. Closing on an imperfect
quote reduces risk even when the quote is wrong; opening on it does not.
When it acts, it says so in the journal - a degraded-path exit should
never be silent.
Also pins the invariant at the Expert_EveryTick gate: that input
throttles how often the EA forms an OPINION, never how often it can act
on a position it already holds. Exits stay above the gate, and the
comment now says so to the next person editing it.
Pre-existing hole, not introduced by the EveryTick work in 6d48fdb -
that change is what made it worth reading the tick path closely.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:52:07 -04:00 | | | 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());
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
2026-07-13 03:23:39 -04:00 | | | //| OnTick handler |
|
| | | //+------------------------------------------------------------------+
|
| | | void CExpertCustom::OnTick(void)
|
| | | {
|
| | | //--- check process flag
|
| | | if(!m_on_tick_process)
|
| | | return;
|
2026-07-26 23:08:32 -04:00 | | | //--- 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();
|
2026-07-13 03:23:39 -04:00 | | | //--- 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)
|
| | | {
|
2026-08-22 00:25:52 -04:00 | | | //--- Resolve the target minute-of-day. Fixed hours keep the exact old arithmetic (same +-1
|
| | | //--- minute buffer).
|
 feat(sessions): market-hours entry gate + "Market close" close-all option, both live from the symbol's session table
Two user requests, one authority: SymbolInfoSessionTrade, read fresh on
every call so DST and per-symbol schedule changes track themselves.
- WarriorMarketOpenNow(): CheckOpenPosition refuses entries outside the
symbol's trading sessions (Sunday reopen, index CFDs' daily breaks) -
a vote can no longer fire into a closed book and collect a broker
error. ENTRIES ONLY: exits, SL/TP and the scheduled close-all stay
unguarded - closing risk must never be blocked by a session boundary.
- CH_MARKET_CLOSE = 24 (appended, .set-safe): the close-all fires
"Close-all minute" minutes before that day's LAST session close.
Friday + Market close + xxH05 = flatten 5 minutes before Friday's
actual close. Resolved identically in three places: the live executor
(CExpertCustom::OnTick), the label walk's vertical barrier
(NextScheduledCloseAll - the symbol's CURRENT table stands in for
history; MT5 keeps none, and a fixed hour is wrong by more), and the
fingerprint (the |CUT: token already carries hour=24, so switching to
the dynamic mode re-keys the model exactly like any schedule change).
Training itself is deliberately NOT gated on market hours: weekend
compute is free and labels only ever exist on real bars - what the
session table gates is order placement and, via the close-all barrier,
what the labels may count as holdable.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:34:27 -04:00 | | | int targetMinOfDay = -1;
|
| | | if(targetHour == CH_MARKET_CLOSE)
|
2026-07-13 03:23:39 -04:00 | | | {
|
 feat(sessions): market-hours entry gate + "Market close" close-all option, both live from the symbol's session table
Two user requests, one authority: SymbolInfoSessionTrade, read fresh on
every call so DST and per-symbol schedule changes track themselves.
- WarriorMarketOpenNow(): CheckOpenPosition refuses entries outside the
symbol's trading sessions (Sunday reopen, index CFDs' daily breaks) -
a vote can no longer fire into a closed book and collect a broker
error. ENTRIES ONLY: exits, SL/TP and the scheduled close-all stay
unguarded - closing risk must never be blocked by a session boundary.
- CH_MARKET_CLOSE = 24 (appended, .set-safe): the close-all fires
"Close-all minute" minutes before that day's LAST session close.
Friday + Market close + xxH05 = flatten 5 minutes before Friday's
actual close. Resolved identically in three places: the live executor
(CExpertCustom::OnTick), the label walk's vertical barrier
(NextScheduledCloseAll - the symbol's CURRENT table stands in for
history; MT5 keeps none, and a fixed hour is wrong by more), and the
fingerprint (the |CUT: token already carries hour=24, so switching to
the dynamic mode re-keys the model exactly like any schedule change).
Training itself is deliberately NOT gated on market hours: weekend
compute is free and labels only ever exist on real bars - what the
session table gates is order placement and, via the close-all barrier,
what the labels may count as holdable.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:34:27 -04:00 | | | 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())
|
2026-07-13 03:23:39 -04:00 | | | {
|
 feat(sessions): market-hours entry gate + "Market close" close-all option, both live from the symbol's session table
Two user requests, one authority: SymbolInfoSessionTrade, read fresh on
every call so DST and per-symbol schedule changes track themselves.
- WarriorMarketOpenNow(): CheckOpenPosition refuses entries outside the
symbol's trading sessions (Sunday reopen, index CFDs' daily breaks) -
a vote can no longer fire into a closed book and collect a broker
error. ENTRIES ONLY: exits, SL/TP and the scheduled close-all stay
unguarded - closing risk must never be blocked by a session boundary.
- CH_MARKET_CLOSE = 24 (appended, .set-safe): the close-all fires
"Close-all minute" minutes before that day's LAST session close.
Friday + Market close + xxH05 = flatten 5 minutes before Friday's
actual close. Resolved identically in three places: the live executor
(CExpertCustom::OnTick), the label walk's vertical barrier
(NextScheduledCloseAll - the symbol's CURRENT table stands in for
history; MT5 keeps none, and a fixed hour is wrong by more), and the
fingerprint (the |CUT: token already carries hour=24, so switching to
the dynamic mode re-keys the model exactly like any schedule change).
Training itself is deliberately NOT gated on market hours: weekend
compute is free and labels only ever exist on real bars - what the
session table gates is order placement and, via the close-all barrier,
what the labels may count as holdable.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:34:27 -04:00 | | | // Log or handle the successful close
|
| | | Print("Positions and orders closed.");
|
2026-07-13 03:23:39 -04:00 | | | }
|
| | | }
|
| | | }
|
| | | }
|
| | | CExpertSignalCustom* customSignal = GetCustomSignal();
|
| | | if(customSignal != NULL)
|
| | | customSignal.OnTickHandler();
|
2026-07-27 10:58:29 -04:00 | | | //--- updated quotes and indicators
|
2026-07-13 03:23:39 -04:00 | | | if(!Refresh())
|
2026-07-26 23:08:32 -04:00 | | | {
|
 fix(exits): never let a declined tick leave an open position unchecked
Refresh() returning false skipped 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 already-open position got no exit check at all -
it rode. Invisible by construction: nothing logged, no order sent, and
next tick the position looks exactly as it should. The only trace is a
stop that should have moved and didn't.
ProtectOpenPosition() now runs the CLOSING half of Processing() on those
ticks. Only the closing half, on purpose: CheckReverse() and the
pending-order block both OPEN exposure, and opening on data just declared
unfit to trade on is the opposite of the point. Closing on an imperfect
quote reduces risk even when the quote is wrong; opening on it does not.
When it acts, it says so in the journal - a degraded-path exit should
never be silent.
Also pins the invariant at the Expert_EveryTick gate: that input
throttles how often the EA forms an OPINION, never how often it can act
on a position it already holds. Exits stay above the gate, and the
comment now says so to the next person editing it.
Pre-existing hole, not introduced by the EveryTick work in 6d48fdb -
that change is what made it worth reading the tick path closely.
Compile-verified in _claude_stage: 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:52:07 -04:00 | | | //--- 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)");
|
2026-07-27 10:58:29 -04:00 | | | if(VerboseMode)
|
| | | TCWarnIfSlow("CExpertCustom::OnTick", tick_started_us, EXPERT_TICK_BUDGET_US);
|
2026-07-13 03:23:39 -04:00 | | | return;
|
2026-07-26 23:08:32 -04:00 | | | }
|
2026-07-13 03:23:39 -04:00 | | | //--- expert processing
|
| | | Processing();
|
2026-07-27 10:58:29 -04:00 | | | if(VerboseMode)
|
| | | TCWarnIfSlow("CExpertCustom::OnTick", tick_started_us, EXPERT_TICK_BUDGET_US);
|
2026-07-13 03:23:39 -04:00 | | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| 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);
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
2026-08-24 03:09:40 -04:00 | | | //| Shared select/filter/freeze-check/act loop for one target kind. |
|
2026-07-13 03:23:39 -04:00 | | | //+------------------------------------------------------------------+
|
2026-08-24 03:09:40 -04:00 | | | bool CExpertCustom::CloseAllLoop(ENUM_CLOSEALL_TARGET target, string symbol, string caller)
|
2026-07-13 03:23:39 -04:00 | | | {
|
2026-07-26 23:08:32 -04:00 | | | bool result = false; // Track if any action was successfully performed
|
| | | string reason;
|
2026-08-24 03:09:40 -04:00 | | | //--- 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--)
|
2026-07-13 03:23:39 -04:00 | | | {
|
2026-08-24 03:09:40 -04:00 | | | ulong ticket = (target == CLOSEALL_POSITION) ? PositionGetTicket(i) : OrderGetTicket(i);
|
| | | if(ticket == 0)
|
2026-07-26 23:08:32 -04:00 | | | continue;
|
2026-08-24 03:09:40 -04:00 | | | if(target == CLOSEALL_POSITION && !PositionSelectByTicket(ticket))
|
2026-07-26 23:08:32 -04:00 | | | continue;
|
2026-08-24 03:09:40 -04:00 | | | if(target == CLOSEALL_POSITION)
|
2026-07-13 03:23:39 -04:00 | | | {
|
2026-08-24 03:09:40 -04:00 | | | if(PositionGetString(POSITION_SYMBOL) != symbol)
|
| | | continue;
|
 feat(trade): two books per symbol, and delete the vote exit
Allow_Hedging (default ON, live only on a RETAIL_HEDGING account) gives the EA
an independent long book and short book on its symbol: at most one long and at
most one short, each opened on its own side's vote and each held to its own
barrier. On a netting account, or with the input off, the original
single-position path runs bit-for-bit unchanged and init says which one is live.
WHY THIS INSTEAD OF A VOTE EXIT. The deploy gate certifies
P(label agrees | vote fired) and the label runs to the barrier, so closing early
on a reversal makes the realised outcome stop being the labelled one - the
certified precision no longer describes what is traded. Opening the other side
acts on the new signal and leaves the old position's certification intact, and
costs no more than reversing: both pay the new side's spread, the difference is
only that the existing position runs on to a barrier already measured as
positive-expectancy. So Signal_ThresholdClose is DELETED rather than tuned,
along with its SIGNAL_CLOSE_PRESETS enum; the threshold is pinned to an
arithmetically unreachable 101 (the stock default of 100 is reachable by a
weighted mean of values capped at 100).
Note the two books can never both fill from one signal: CheckOpenLong and
CheckOpenShort test opposite signs of the same m_direction, so at most one clears
per tick. A hedge only forms when a LATER opposite vote fires - which is what
keeps it from being a guaranteed-loss wash pair.
The mechanism is a SelectPosition() override keyed on the active book's magic;
every inherited close/trail path then operates on that book untouched. The long
book keeps Expert_MagicNumber, so no existing position, journal row or
risk-budget state file is re-addressed. Short book is +1.
Four ownership filters had to widen from "== m_magic" to WarriorOwnsMagic(),
or the short book would have been invisible to the code that must reach it:
the scheduled close-all (positions and orders), the risk budget's emergency
flatten, and the journal's MAE/MFE walk. WarriorOwnsMagic() is deliberately NOT
gated on Allow_Hedging - turning the input off while a short-book position is
open would otherwise orphan it with nothing left to close it.
Risk sizing needed no change: CapRiskAmount already subtracts OpenRiskAtStops(),
which counts every position regardless of magic, so the second book is sized
inside what the first one left. Conservative for a hedged pair, which cannot
lose both stops - the safe direction.
Retrain-neutral: neither input is in BuildModelFingerprint() or
ComputeDbConfigFingerprint(). Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:20:35 -04:00 | | | //--- 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)))
|
2026-08-24 03:09:40 -04:00 | | | continue;
|
 feat(trade): two books per symbol, and delete the vote exit
Allow_Hedging (default ON, live only on a RETAIL_HEDGING account) gives the EA
an independent long book and short book on its symbol: at most one long and at
most one short, each opened on its own side's vote and each held to its own
barrier. On a netting account, or with the input off, the original
single-position path runs bit-for-bit unchanged and init says which one is live.
WHY THIS INSTEAD OF A VOTE EXIT. The deploy gate certifies
P(label agrees | vote fired) and the label runs to the barrier, so closing early
on a reversal makes the realised outcome stop being the labelled one - the
certified precision no longer describes what is traded. Opening the other side
acts on the new signal and leaves the old position's certification intact, and
costs no more than reversing: both pay the new side's spread, the difference is
only that the existing position runs on to a barrier already measured as
positive-expectancy. So Signal_ThresholdClose is DELETED rather than tuned,
along with its SIGNAL_CLOSE_PRESETS enum; the threshold is pinned to an
arithmetically unreachable 101 (the stock default of 100 is reachable by a
weighted mean of values capped at 100).
Note the two books can never both fill from one signal: CheckOpenLong and
CheckOpenShort test opposite signs of the same m_direction, so at most one clears
per tick. A hedge only forms when a LATER opposite vote fires - which is what
keeps it from being a guaranteed-loss wash pair.
The mechanism is a SelectPosition() override keyed on the active book's magic;
every inherited close/trail path then operates on that book untouched. The long
book keeps Expert_MagicNumber, so no existing position, journal row or
risk-budget state file is re-addressed. Short book is +1.
Four ownership filters had to widen from "== m_magic" to WarriorOwnsMagic(),
or the short book would have been invisible to the code that must reach it:
the scheduled close-all (positions and orders), the risk budget's emergency
flatten, and the journal's MAE/MFE walk. WarriorOwnsMagic() is deliberately NOT
gated on Allow_Hedging - turning the input off while a short-book position is
open would otherwise orphan it with nothing left to close it.
Risk sizing needed no change: CapRiskAmount already subtracts OpenRiskAtStops(),
which counts every position regardless of magic, so the second book is sized
inside what the first one left. Conservative for a hedged pair, which cannot
lose both stops - the safe direction.
Retrain-neutral: neither input is in BuildModelFingerprint() or
ComputeDbConfigFingerprint(). Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:20:35 -04:00 | | | //--- 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));
|
2026-08-24 03:09:40 -04:00 | | | //--- 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;
|
2026-07-13 03:23:39 -04:00 | | | }
|
2026-08-24 03:09:40 -04:00 | | | else
|
2026-07-13 03:23:39 -04:00 | | | {
|
2026-08-24 03:09:40 -04:00 | | | if(OrderGetString(ORDER_SYMBOL) != symbol)
|
| | | continue;
|
 feat(trade): two books per symbol, and delete the vote exit
Allow_Hedging (default ON, live only on a RETAIL_HEDGING account) gives the EA
an independent long book and short book on its symbol: at most one long and at
most one short, each opened on its own side's vote and each held to its own
barrier. On a netting account, or with the input off, the original
single-position path runs bit-for-bit unchanged and init says which one is live.
WHY THIS INSTEAD OF A VOTE EXIT. The deploy gate certifies
P(label agrees | vote fired) and the label runs to the barrier, so closing early
on a reversal makes the realised outcome stop being the labelled one - the
certified precision no longer describes what is traded. Opening the other side
acts on the new signal and leaves the old position's certification intact, and
costs no more than reversing: both pay the new side's spread, the difference is
only that the existing position runs on to a barrier already measured as
positive-expectancy. So Signal_ThresholdClose is DELETED rather than tuned,
along with its SIGNAL_CLOSE_PRESETS enum; the threshold is pinned to an
arithmetically unreachable 101 (the stock default of 100 is reachable by a
weighted mean of values capped at 100).
Note the two books can never both fill from one signal: CheckOpenLong and
CheckOpenShort test opposite signs of the same m_direction, so at most one clears
per tick. A hedge only forms when a LATER opposite vote fires - which is what
keeps it from being a guaranteed-loss wash pair.
The mechanism is a SelectPosition() override keyed on the active book's magic;
every inherited close/trail path then operates on that book untouched. The long
book keeps Expert_MagicNumber, so no existing position, journal row or
risk-budget state file is re-addressed. Short book is +1.
Four ownership filters had to widen from "== m_magic" to WarriorOwnsMagic(),
or the short book would have been invisible to the code that must reach it:
the scheduled close-all (positions and orders), the risk budget's emergency
flatten, and the journal's MAE/MFE walk. WarriorOwnsMagic() is deliberately NOT
gated on Allow_Hedging - turning the input off while a short-book position is
open would otherwise orphan it with nothing left to close it.
Risk sizing needed no change: CapRiskAmount already subtracts OpenRiskAtStops(),
which counts every position regardless of magic, so the second book is sized
inside what the first one left. Conservative for a hedged pair, which cannot
lose both stops - the safe direction.
Retrain-neutral: neither input is in BuildModelFingerprint() or
ComputeDbConfigFingerprint(). Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:20:35 -04:00 | | | if(!WarriorOwnsMagic(OrderGetInteger(ORDER_MAGIC)))
|
2026-08-24 03:09:40 -04:00 | | | continue;
|
 feat(trade): two books per symbol, and delete the vote exit
Allow_Hedging (default ON, live only on a RETAIL_HEDGING account) gives the EA
an independent long book and short book on its symbol: at most one long and at
most one short, each opened on its own side's vote and each held to its own
barrier. On a netting account, or with the input off, the original
single-position path runs bit-for-bit unchanged and init says which one is live.
WHY THIS INSTEAD OF A VOTE EXIT. The deploy gate certifies
P(label agrees | vote fired) and the label runs to the barrier, so closing early
on a reversal makes the realised outcome stop being the labelled one - the
certified precision no longer describes what is traded. Opening the other side
acts on the new signal and leaves the old position's certification intact, and
costs no more than reversing: both pay the new side's spread, the difference is
only that the existing position runs on to a barrier already measured as
positive-expectancy. So Signal_ThresholdClose is DELETED rather than tuned,
along with its SIGNAL_CLOSE_PRESETS enum; the threshold is pinned to an
arithmetically unreachable 101 (the stock default of 100 is reachable by a
weighted mean of values capped at 100).
Note the two books can never both fill from one signal: CheckOpenLong and
CheckOpenShort test opposite signs of the same m_direction, so at most one clears
per tick. A hedge only forms when a LATER opposite vote fires - which is what
keeps it from being a guaranteed-loss wash pair.
The mechanism is a SelectPosition() override keyed on the active book's magic;
every inherited close/trail path then operates on that book untouched. The long
book keeps Expert_MagicNumber, so no existing position, journal row or
risk-budget state file is re-addressed. Short book is +1.
Four ownership filters had to widen from "== m_magic" to WarriorOwnsMagic(),
or the short book would have been invisible to the code that must reach it:
the scheduled close-all (positions and orders), the risk budget's emergency
flatten, and the journal's MAE/MFE walk. WarriorOwnsMagic() is deliberately NOT
gated on Allow_Hedging - turning the input off while a short-book position is
open would otherwise orphan it with nothing left to close it.
Risk sizing needed no change: CapRiskAmount already subtracts OpenRiskAtStops(),
which counts every position regardless of magic, so the second book is sized
inside what the first one left. Conservative for a hedged pair, which cannot
lose both stops - the safe direction.
Retrain-neutral: neither input is in BuildModelFingerprint() or
ComputeDbConfigFingerprint(). Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:20:35 -04:00 | | | if(CheckPointer(m_trade) != POINTER_INVALID)
|
| | | m_trade.SetExpertMagicNumber((ulong)OrderGetInteger(ORDER_MAGIC));
|
2026-08-24 03:09:40 -04:00 | | | //--- 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;
|
2026-07-13 03:23:39 -04:00 | | | }
|
| | | }
|
 feat(trade): two books per symbol, and delete the vote exit
Allow_Hedging (default ON, live only on a RETAIL_HEDGING account) gives the EA
an independent long book and short book on its symbol: at most one long and at
most one short, each opened on its own side's vote and each held to its own
barrier. On a netting account, or with the input off, the original
single-position path runs bit-for-bit unchanged and init says which one is live.
WHY THIS INSTEAD OF A VOTE EXIT. The deploy gate certifies
P(label agrees | vote fired) and the label runs to the barrier, so closing early
on a reversal makes the realised outcome stop being the labelled one - the
certified precision no longer describes what is traded. Opening the other side
acts on the new signal and leaves the old position's certification intact, and
costs no more than reversing: both pay the new side's spread, the difference is
only that the existing position runs on to a barrier already measured as
positive-expectancy. So Signal_ThresholdClose is DELETED rather than tuned,
along with its SIGNAL_CLOSE_PRESETS enum; the threshold is pinned to an
arithmetically unreachable 101 (the stock default of 100 is reachable by a
weighted mean of values capped at 100).
Note the two books can never both fill from one signal: CheckOpenLong and
CheckOpenShort test opposite signs of the same m_direction, so at most one clears
per tick. A hedge only forms when a LATER opposite vote fires - which is what
keeps it from being a guaranteed-loss wash pair.
The mechanism is a SelectPosition() override keyed on the active book's magic;
every inherited close/trail path then operates on that book untouched. The long
book keeps Expert_MagicNumber, so no existing position, journal row or
risk-budget state file is re-addressed. Short book is +1.
Four ownership filters had to widen from "== m_magic" to WarriorOwnsMagic(),
or the short book would have been invisible to the code that must reach it:
the scheduled close-all (positions and orders), the risk budget's emergency
flatten, and the journal's MAE/MFE walk. WarriorOwnsMagic() is deliberately NOT
gated on Allow_Hedging - turning the input off while a short-book position is
open would otherwise orphan it with nothing left to close it.
Risk sizing needed no change: CapRiskAmount already subtracts OpenRiskAtStops(),
which counts every position regardless of magic, so the second book is sized
inside what the first one left. Conservative for a hedged pair, which cannot
lose both stops - the safe direction.
Retrain-neutral: neither input is in BuildModelFingerprint() or
ComputeDbConfigFingerprint(). Compiled clean; NOT yet run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:20:35 -04:00 | | | //--- 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));
|
2026-07-13 03:23:39 -04:00 | | | return result; // Return true if any action was performed, false otherwise
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
2026-08-24 03:09:40 -04:00 | | | //| 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;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
2026-07-13 03:23:39 -04:00 | | | //| OnTimer handler |
|
| | | //+------------------------------------------------------------------+
|
| | | void CExpertCustom::OnTimer(void)
|
| | | {
|
| | | //--- check process flag
|
| | | if(!m_on_timer_process)
|
| | | return;
|
2026-07-26 23:08:32 -04:00 | | | //--- 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.
|
2026-07-27 10:58:29 -04:00 | | | if(VerboseMode)
|
| | | TCWarnIfMemoryAbove(EXPERT_MEMORY_SOFT_LIMIT_MB);
|
2026-07-13 03:23:39 -04:00 | | | CExpertSignalCustom* customSignal = GetCustomSignal();
|
2026-07-26 12:12:14 -04:00 | | | if(customSignal == NULL)
|
| | | return;
|
| | | if(!dbm.OpenDatabase())
|
2026-07-13 03:23:39 -04:00 | | | {
|
2026-07-26 12:12:14 -04:00 | | | Print(__FUNCTION__ + ": Failed to open database, skipping this timer cycle's signal processing/weight update.");
|
| | | return;
|
2026-07-13 03:23:39 -04:00 | | | }
|
2026-07-26 12:12:14 -04:00 | | | 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();
|
2026-07-13 03:23:39 -04:00 | | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| 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();
|
2026-08-22 00:25:52 -04:00 | | | //--- 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.
|
 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 | | | m_trade.SetAsyncMode(false);
|
2026-07-13 03:23:39 -04:00 | | | //--- set default deviation for trading in adjusted points
|
| | | m_trade.SetDeviationInPoints((ulong)(3 * m_adjusted_point / m_symbol.Point()));
|
| | | //--- ok
|
| | | return(true);
|
| | | }
|
| | | //+------------------------------------------------------------------+
|