Warrior_EA/Variables/ConfidenceBridge.mqh
AnimateDread 38a12a240b refactor(kiss): drop the AI sub-vote early-exit route; certified == traded
First of the AI vote layers to go. CheckClosePosition had two exit routes:
the stock blended vote, and an AI-only one reading the AI members' sub-vote
undiluted. The second existed because an AI reversal averaged in with the
classic filters could be diluted below the threshold before it could close
a position.

It is gone, and with it m_lastAiVote and the aiResult/aiWeightSum pair
Direction() carried to feed it.

This CLOSES the certified-vs-traded gap rather than widening it. The
deploy gate certifies a win rate measured on hold-to-resolution outcomes,
and CheckClosePosition already gated the blended route off whenever an AI
model's derived geometry was on the order - so the AI route was the only
vote exit an AI-certified trade could take, and the exit replay existed to
reproduce it. With it removed, an AI-certified position holds to its
barrier by construction instead of by reconstruction, so Warrior_EA.mq5
now pushes ExitPolicy(0.0, true) unconditionally. Previously it forwarded
Min_Vote_Close and relied on Disabled arriving as 1.01 to switch the
simulated exit off by arithmetic - correct at the shipped default, and one
input change away from the simulation and the live path describing
different games.

Min_Vote_Close keeps its meaning for the classic route and is now
documented as inert wherever an AI certificate governs, rather than
appearing to drive an exit it can no longer reach.

Comment debt cleared while here: a tombstone block for m_ai_exit_threshold
(a member deleted 2026-08-18) still sat in the header, and four sites still
named LiveSignedConfidence's "two consumers" - it had one, the intelligent
trailing stop, since that same date.

NOT touched, and deliberately: NMS declustering is NOT a quality layer. It
gates the live signal at Inference.mqh:226 (NmsLiveAccept), and the
undeclustered population is ~8x what the EA trades. Removing it would
multiply live position count, not simplify a scoring path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 20:27:28 -04:00

105 lines
6.4 KiB
MQL5

//+------------------------------------------------------------------+
//| Warrior_EA |
//| AnimateDread |
//| |
//+------------------------------------------------------------------+
// Money management classes (CExpertMoney) are invoked by the standard library's
// CExpert with a fixed (price, sl) signature - they have no pointer back to the
// signal filter that computed those levels. CExpertSignalCustom::OpenParams()
// refreshes these globals right before Money.CheckOpenLong/Short() is called for
// the same trade, so Money classes can read a same-tick confidence value without
// requiring an intrusive change to the wizard framework's call chain.
double g_AISignedConfidence = 0.0; // -1..1, sign = direction, magnitude = AI confidence; 0 if no AI filter
double g_DBConfidence = 0.0; // 0..1, historical time-based win rate of the active pattern set
// source takes CONFIDENCE_SOURCE's underlying int values (0=CONF_AI, 1=CONF_DB, 2=CONF_BLENDED).
// Declared as int rather than the enum type so this header has no dependency on the include
// order of Enumerations\InputEnums.mqh (this file is pulled in from class headers that are
// included before Inputs.mqh in Warrior_EA.mq5).
// reward:risk ratio of the specific trade OpenParams() just sized (b in the Kelly-criterion
// formula CMoneyIntelligent::AdjustRiskAmount() uses) - refreshed on the same same-tick
// contract as the two confidence globals above; always > 0 when populated, since OpenParams()
// no longer rejects on reward:risk at all (the filter was removed 2026-08-09), so g_TradeRewardRiskRatio
// reaches Money as a SIZING input rather than as the survivor of a veto.
double g_TradeRewardRiskRatio = 0.0;
// Live per-tick signed AI confidence (-1..1, sign = predicted direction, magnitude = confidence),
// refreshed every tick/timer from the active AI signal's SignedAIConfidence() in
// CExpertSignalAIBase::ScheduleTrainingIfNeeded() - independent of the OpenParams() same-tick
// contract above, because an ALREADY-OPEN position generates no OpenParams() calls yet the
// intelligent trailing (Trailing\TrailingIntelligent.mqh) still needs a current read while holding.
// This is written by the active AI signal once per tick; 0.0 means no AI filter is active/converged yet.
double g_LiveAISignedConfidence = 0.0;
//--- THE LIVE AI VOTE BOARD, and it exists to keep one signal out of another signal's business.
//---
//--- g_LiveAISignedConfidence above is written once per tick and read by the intelligent trailing stop.
//--- With ONE AI signal that is exactly right. With several (an ensemble) every member wrote it
//--- unconditionally, last writer won, and an LSTM entry's stop was moved by whichever member's OnTick
//--- happened to run last (user-identified 2026-08-17).
//---
//--- The obvious patch - have a member average its siblings - trades a scheduling bug for a coupling bug,
//--- and this EA is deliberately built the other way: every signal runs in its own instance, minds its own
//--- state, and VOTES to the orchestrator, which is the only thing allowed to combine opinions. So the
//--- board below is a publish/aggregate pair with that split enforced by shape:
//--- - a member writes ONLY its own slot (PublishAIVote), never reads another's;
//--- - the orchestrator (CExpertSignalCustom::LiveSignedConfidence) calls AggregateAIVotes() to combine.
//--- Same contract as every other global here: one-way, same-tick, no back-pointer into the wizard chain.
//---
//--- The mean, matching the live open decision (a weighted-average vote, not unanimity) and the ensemble
//--- gate's combined-vote score. A member that abstains publishes 0 and dilutes, exactly as it does there;
//--- a member still training publishes 0, so a half-trained ensemble reads WEAKER rather than louder,
//--- which is the safe direction for something that can close a position.
#define AI_VOTE_BOARD_MAX 8
double g_AIVoteBoard[AI_VOTE_BOARD_MAX];
bool g_AIVoteBoardUsed[AI_VOTE_BOARD_MAX];
void PublishAIVote(int slot, double signedConfidence)
{
if(slot < 0 || slot >= AI_VOTE_BOARD_MAX)
return;
g_AIVoteBoard[slot] = signedConfidence;
g_AIVoteBoardUsed[slot] = true;
}
double AggregateAIVotes(void)
{
double sum = 0.0;
int n = 0;
for(int i = 0; i < AI_VOTE_BOARD_MAX; i++)
if(g_AIVoteBoardUsed[i])
{
sum += g_AIVoteBoard[i];
n++;
}
return (n > 0) ? sum / n : 0.0;
}
// MEASURED barrier geometry, in ATR multiples, published by the AI signal for the LIVE order path.
// Written from exactly two places: DeriveBarrierGeometry() when the geometry is measured at era 0, and
// the .cfg adoption in Persistence.mqh when a trained model is loaded with its pinned pair. 0.0 = not
// derived (fresh start before era 0, or no AI filter) - OpenParams() then falls back to the SL_Mode/
// TP_Mode enum multiples exactly as before.
//
// This bridge exists because of a real incident, not tidiness: the deploy gate certifies "this model's
// trades reach the MEASURED target before the MEASURED stop at a win rate beating break-even" - and
// until 2026-08-09 the live EA then placed trades with the ENUM geometry (2*ATR stop, 6*ATR target on
// the shipped SP500 config) that the certificate says nothing about. The model was graded on one game
// and paid on another. Same one-way, same-tick contract as the confidence globals above; OpenParams()
// runs on the aggregate/root signal, which has no pointer to the AI filter that measured these.
double g_DerivedSlAtrMult = 0.0;
double g_DerivedTpAtrMult = 0.0;
//+------------------------------------------------------------------+
//| Combine AI/DB confidence into a single 0..1 magnitude |
//+------------------------------------------------------------------+
double CombinedConfidence(int source)
{
double aiMag = MathIsValidNumber(g_AISignedConfidence) ? MathAbs(g_AISignedConfidence) : 0.0;
double dbMag = MathIsValidNumber(g_DBConfidence) ? g_DBConfidence : 0.0;
aiMag = MathMax(0.0, MathMin(aiMag, 1.0));
dbMag = MathMax(0.0, MathMin(dbMag, 1.0));
switch(source)
{
case 1: // CONF_DB
return dbMag;
case 2: // CONF_BLENDED
return (aiMag + dbMag) / 2.0;
default: // CONF_AI
return aiMag;
}
}
//+------------------------------------------------------------------+