Warrior_EA/Panel/ControlPanel.mqh
AnimateDread 506626b381 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

220 lines
11 KiB
MQL5

//+------------------------------------------------------------------+
//| ControlPanel.mqh |
//| AnimateDread |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
//--- Built on the standard MQL5 Controls library (CAppDialog/CButton). Per the library's own source
//--- (Controls\Wnd.mqh CWnd::OnEvent, Controls\Dialog.mqh CAppDialog::ChartEvent), every control in
//--- this framework - including the dialog's own caption drag and close/minimize buttons - detects
//--- clicks exclusively through CHARTEVENT_MOUSE_MOVE hit-testing, which raises an internal
//--- EventChartCustom(CONTROLS_SELF_MESSAGE, ON_CLICK, control.Id(), ...) event; CAppDialog::ChartEvent()
//--- explicitly ignores CHARTEVENT_OBJECT_CLICK outright ("we are working with the CHARTEVENT_MOUSE_MOVE
//--- events"). So button clicks are wired the same way CDialog wires its own Close button - an
//--- EVENT_MAP/ON_EVENT(ON_CLICK, control, handler) entry - not by matching object names against
//--- CHARTEVENT_OBJECT_CLICK's sparam in the EA's OnChartEvent().
#include "..\Enumerations\GlobalEnums.mqh"
#include <Controls\Dialog.mqh>
#include <Controls\Button.mqh>
#define CP_PANEL_W 176
#define CP_ROW_H 24
#define CP_MARGIN 8
#define CP_BTN_H 20
#define CP_CAPTION_H 20
#define CP_NUM_BUTTONS 9
//--- total outer height needed for the dialog (caption bar + top margin + N button rows + bottom margin)
#define CP_PANEL_H (CP_CAPTION_H + CP_MARGIN + CP_NUM_BUTTONS * CP_ROW_H + CP_MARGIN)
//--- gap left between the status label's last line and the minimized panel docked under it
#define CP_DOCK_GAP 6
class CControlPanel : public CAppDialog
{
private:
CButton m_signalsBtn;
CButton m_pauseBtn;
CButton m_stopBtn;
CButton m_deployBtn;
CButton m_saveBtn;
CButton m_loadBtn;
CButton m_resetBtn;
CButton m_reportBtn;
CButton m_resetDbBtn;
ENUM_CP_ACTION m_pendingAction;
//--- Where the panel was before it was last minimized, so Maximize() can put it back instead of
//--- leaving it wherever Minimize() docked it - see Minimize()'s declaration comment.
int m_restoreX;
int m_restoreY;
bool m_restoreSaved;
bool CreateOneButton(CButton &btn, const string name, const int y, const string text);
bool CreateButtons(void);
protected:
//--- ON_CLICK handlers (see EVENT_MAP below) - just record which button fired; the EA polls
//--- ConsumeAction() once per OnChartEvent() call and performs the actual work
void OnClickSignals(void) { m_pendingAction = CP_ACTION_TOGGLE_SIGNALS; }
void OnClickPause(void) { m_pendingAction = CP_ACTION_TOGGLE_PAUSE; }
void OnClickStop(void) { m_pendingAction = CP_ACTION_TOGGLE_STOP; }
void OnClickDeploy(void) { m_pendingAction = CP_ACTION_TOGGLE_DEPLOY; }
void OnClickSave(void) { m_pendingAction = CP_ACTION_SAVE; }
void OnClickLoad(void) { m_pendingAction = CP_ACTION_LOAD; }
void OnClickReset(void) { m_pendingAction = CP_ACTION_RESET; }
void OnClickReport(void) { m_pendingAction = CP_ACTION_REPORT; }
void OnClickResetDb(void) { m_pendingAction = CP_ACTION_RESET_DB; }
public:
CControlPanel(void) : m_pendingAction(CP_ACTION_NONE), m_restoreX(0), m_restoreY(0),
m_restoreSaved(false) {}
~CControlPanel(void) {}
virtual bool Create(const long chart, const string name, const int subwin,
const int x1, const int y1, const int x2, const int y2);
//--- chart event handler (EVENT_MAP macro below generates the body)
virtual bool OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam);
//--- CAppDialog's default OnClickButtonClose() calls Destroy(), which for a PROGRAM_EXPERT calls
//--- ExpertRemove() - i.e. the caption bar's "x" would silently detach this EA from the chart and
//--- stop live trading/training. Redirect it to Minimize() so "x" behaves like the native "-" button
//--- instead of destroying the EA.
virtual void OnClickButtonClose(void) override { Minimize(); }
//--- Docks the minimized bar directly under the status label (System\StatusLabel.mqh) rather than
//--- leaving it at its maximized position, which - since the label's line count/height changes
//--- live with training progress - could otherwise sit ON TOP of the label text (user request
//--- 2026-08-16: minimized should go under the label, not overlap it). Position is remembered so
//--- Maximize() below can restore it.
//--- Return type is VOID, matching CAppDialog's own `virtual void Minimize(void)` /
//--- `virtual void Maximize(void)` (Controls\Dialog.mqh). Declaring these `bool` is a hard compile
//--- error (265 "overriding virtual function with different return type" + 404 "does not override
//--- any base class method", plus 151 on assigning the void call to a bool) - the base pair returns
//--- nothing, so there is no success flag to forward.
virtual void Minimize(void) override
{
if(!m_restoreSaved)
{
m_restoreX = Left();
m_restoreY = Top();
m_restoreSaved = true;
}
CAppDialog::Minimize();
Move(Left(), StatusLabelBottomY() + CP_DOCK_GAP);
}
//--- Restores the position Minimize() displaced it from, rather than leaving a maximized panel
//--- parked under the label where it was only ever meant to sit while minimized.
virtual void Maximize(void) override
{
CAppDialog::Maximize();
if(m_restoreSaved)
{
Move(m_restoreX, m_restoreY);
m_restoreSaved = false;
}
}
//--- keeps button labels synced with live training/signal-visibility state
void SetSignalsText(const string text) { m_signalsBtn.Text(text); }
void SetPauseText(const string text) { m_pauseBtn.Text(text); }
void SetStopText(const string text) { m_stopBtn.Text(text); }
void SetDeployText(const string text) { m_deployBtn.Text(text); }
//--- returns and clears whichever button (if any) was clicked since the last call
ENUM_CP_ACTION ConsumeAction(void) { ENUM_CP_ACTION a = m_pendingAction; m_pendingAction = CP_ACTION_NONE; return a; }
//--- Maximize() is protected in CAppDialog; expose it so the EA can defensively un-minimize on creation
void ForceMaximize(void) { Maximize(); }
};
//+------------------------------------------------------------------+
//| Event map - same mechanism CDialog uses internally for its own |
//| Close button (Controls\Dialog.mqh) |
//+------------------------------------------------------------------+
EVENT_MAP_BEGIN(CControlPanel)
ON_EVENT(ON_CLICK, m_signalsBtn, OnClickSignals)
ON_EVENT(ON_CLICK, m_pauseBtn, OnClickPause)
ON_EVENT(ON_CLICK, m_stopBtn, OnClickStop)
ON_EVENT(ON_CLICK, m_deployBtn, OnClickDeploy)
ON_EVENT(ON_CLICK, m_saveBtn, OnClickSave)
ON_EVENT(ON_CLICK, m_loadBtn, OnClickLoad)
ON_EVENT(ON_CLICK, m_resetBtn, OnClickReset)
ON_EVENT(ON_CLICK, m_reportBtn, OnClickReport)
ON_EVENT(ON_CLICK, m_resetDbBtn, OnClickResetDb)
EVENT_MAP_END(CAppDialog)
//+------------------------------------------------------------------+
bool CControlPanel::Create(const long chart, const string name, const int subwin,
const int x1, const int y1, const int x2, const int y2)
{
if(!CAppDialog::Create(chart, name, subwin, x1, y1, x2, y2))
{
Print("CControlPanel: CAppDialog::Create() failed, error " + IntegerToString(GetLastError()));
return false;
}
if(!Caption("Warrior EA Control Panel"))
{
Print("CControlPanel: Caption() failed, error " + IntegerToString(GetLastError()));
return false;
}
if(!CreateButtons())
return false;
return true;
}
//+------------------------------------------------------------------+
bool CControlPanel::CreateOneButton(CButton &btn, const string name, const int y, const string text)
{
if(!btn.Create(m_chart_id, m_name + name, m_subwin, CP_MARGIN, y, CP_PANEL_W - CP_MARGIN, y + CP_BTN_H))
{
Print("CControlPanel: button '" + name + "' Create() failed, error " + IntegerToString(GetLastError()));
return false;
}
if(!btn.Text(text))
{
Print("CControlPanel: button '" + name + "' Text() failed, error " + IntegerToString(GetLastError()));
return false;
}
if(!Add(btn))
{
Print("CControlPanel: button '" + name + "' Add() failed, error " + IntegerToString(GetLastError()));
return false;
}
return true;
}
//+------------------------------------------------------------------+
bool CControlPanel::CreateButtons(void)
{
int y = CP_MARGIN;
//--- Report first, so the two destructive (red) buttons below end up adjacent at the bottom instead
//--- of having Report sandwiched between them.
if(!CreateOneButton(m_reportBtn, "Report", y, "Export Trade Journal"))
return false;
y += CP_ROW_H;
if(!CreateOneButton(m_signalsBtn, "Signals", y, "Hide Signals"))
return false;
y += CP_ROW_H;
if(!CreateOneButton(m_pauseBtn, "Pause", y, "Pause Training"))
return false;
y += CP_ROW_H;
if(!CreateOneButton(m_stopBtn, "Stop", y, "Stop Training"))
return false;
y += CP_ROW_H;
//--- Sits directly under Stop: both end the current training run, but Deploy is the one that promotes
//--- the result to THE model (and, clicked again, sends it back to training). Green so it reads as the
//--- constructive counterpart to the red destructive pair at the bottom.
if(!CreateOneButton(m_deployBtn, "Deploy", y, "Deploy Model"))
return false;
if(!m_deployBtn.ColorBackground(clrDarkGreen) || !m_deployBtn.Color(clrWhite))
{
Print("CControlPanel: button 'Deploy' color styling failed, error " + IntegerToString(GetLastError()));
return false;
}
y += CP_ROW_H;
if(!CreateOneButton(m_saveBtn, "Save", y, "Save Weights Now"))
return false;
y += CP_ROW_H;
if(!CreateOneButton(m_loadBtn, "Load", y, "Load Weights From Disk"))
return false;
y += CP_ROW_H;
if(!CreateOneButton(m_resetBtn, "Reset", y, "Delete && Reset Weights"))
return false;
if(!m_resetBtn.ColorBackground(clrDarkRed) || !m_resetBtn.Color(clrWhite))
{
Print("CControlPanel: button 'Reset' color styling failed, error " + IntegerToString(GetLastError()));
return false;
}
y += CP_ROW_H;
if(!CreateOneButton(m_resetDbBtn, "ResetDB", y, "Delete && Reset Database"))
return false;
if(!m_resetDbBtn.ColorBackground(clrDarkRed) || !m_resetDbBtn.Color(clrWhite))
{
Print("CControlPanel: button 'ResetDB' color styling failed, error " + IntegerToString(GetLastError()));
return false;
}
return true;
}
//+------------------------------------------------------------------+