forked from animatedread/Warrior_EA
DRY - topology construction
---------------------------
CSignalCONV and CSignalHYBRID each built the Conv+Pool front-end from scratch;
CSignalLSTM and CSignalHYBRID each built the LSTM stage from scratch. The
duplicates had already drifted: HYBRID guarded the LSTM step with
MathMax(1, historyBars/2), CSignalLSTM divided unguarded, so a historyBars of 1
gave two different steps for what is documented as the same layer.
Extracted AddConvPoolStage() and AddLstmStage() onto CExpertSignalAIBase. The
three overrides are now compositions:
CONV = AddConvPoolStage
LSTM = AddLstmStage
HYBRID = AddConvPoolStage && AddLstmStage
HYBRID's "matches the standalone CONV front-end exactly, then adds LSTM" is
enforced by construction instead of by comment. Took the guarded step for both.
Also fixed a descriptor leak the duplicates shared: on a failed topology.Add()
the CLayerDescription was neither owned by the array nor deleted.
Dead code
---------
- CNet::SaveCheckpoint / CNet::LoadCheckpoint (123 lines). Superseded by the
in-memory CaptureWeights/RestoreWeights pair; Network.mqh:1312 already said so
("This replaces the file-based SaveCheckpoint/LoadCheckpoint"). Zero call
sites - every remaining mention was a comment. The five comments that
referenced them have been reworded rather than left dangling.
- CExpertSignalCustom::CheckForDuplicateTrade / FindLastTradeIndex /
UpdateTradeStatusAndExit: declared, never defined anywhere, never called.
They only made it look as though duplicate-trade detection existed.
Compiles 0 errors, 0 warnings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1399 lines
75 KiB
MQL5
1399 lines
75 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| Warrior_EA |
|
|
//| AnimateDread |
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
#include <Expert\ExpertSignal.mqh>
|
|
#include "..\System\NewBar.mqh"
|
|
#include "..\Structures\tradeRecordStructure.mqh"
|
|
#include "..\Structures\signalInfoStructure.mqh"
|
|
#include "..\Variables\ConfidenceBridge.mqh"
|
|
#include "..\System\TradeChecks.mqh"
|
|
//--- Enumerations
|
|
#include "..\Enumerations\GlobalEnums.mqh"
|
|
//
|
|
#define MAX_TABLE_ROWS 1000 // row cap before the oldest entry is pruned
|
|
#define MIN_TRADES_FOR_WIN_RATE 100 // minimum sample size before a pattern's win rate is trusted
|
|
#define NO_DATA_WIN_RATE -1 // sentinel: not enough trades to compute a win rate
|
|
#define MIN_SL_ATR_MULTIPLIER 2.0 // hard floor on SL distance from entry (broker stop-level / sanity)
|
|
//--- Underlying-int sentinel for the "Intelligent" SL/TP modes (STOP_LOSS_MODE::SL_INTELLIGENT /
|
|
//--- TAKE_PROFIT_MODE::TP_INTELLIGENT, both -1 in Enumerations\InputEnums.mqh). Kept as a local macro
|
|
//--- rather than referencing the enum name so this header stays independent of InputEnums.mqh's include
|
|
//--- order, exactly like m_confidence_source being an int (see Variables\ConfidenceBridge.mqh).
|
|
#define SL_INTELLIGENT_MODE (-1)
|
|
#define TP_INTELLIGENT_MODE (-1)
|
|
//--- SL/TP "previous swing" sentinels (STOP_LOSS_MODE::SL_PREV_SWING / TAKE_PROFIT_MODE::TP_PREV_SWING,
|
|
//--- both -101). SL sits exactly at the swing; TP targets the opposite swing. Same local-macro rationale.
|
|
#define SL_PREV_SWING_MODE (-101)
|
|
#define TP_PREV_SWING_MODE (-101)
|
|
//--- Intelligent (AI-confidence) SL/TP shaping, driven by EffectiveConfidence() (a 0..1 magnitude, see
|
|
//--- CExpertSignalAIBase::AIConfidence/DBConfidence per Confidence_Source):
|
|
//--- - SL starts SL_INTELLIGENT_BASE_MULT beyond the swing and TIGHTENS by up to AI_SL_TIGHTEN_FACTOR
|
|
//--- (30%) as confidence -> 1: a high-conviction setup gets a tighter stop, a marginal one keeps the
|
|
//--- full ATR cushion. Still floored at MIN_SL_ATR_MULTIPLIER above.
|
|
//--- - TP is a multiple of THIS TRADE'S OWN RISK (the final entry-to-stop distance), not of ATR: it
|
|
//--- starts at TP_INTELLIGENT_BASE_RR and WIDENS by up to AI_TP_WIDEN_FACTOR (+100%, i.e. 2x) as
|
|
//--- confidence -> 1, so RR runs 2.5 (zero confidence) to 5.0 (full conviction).
|
|
//--- WHY risk-relative and not ATR-relative: SL is swing-anchored PLUS padding, so its distance
|
|
//--- grows with the swing gap, while an ATR-from-entry TP does not. Those two were decoupled when
|
|
//--- TP moved off the opposite-swing anchor (commit 0f09588), and nothing re-checked the result
|
|
//--- against Min_Risk_Reward_Ratio: with confidence pinned at 0 (i.e. AI disabled - the shipped
|
|
//--- default) the old TP_INTELLIGENT_BASE_MULT of 3.0 produced reward = 3*ATR against a risk that
|
|
//--- MIN_SL_ATR_MULTIPLIER alone floors at 2*ATR, so `reward < 2.0*risk` was ALWAYS true and
|
|
//--- OpenParams() rejected 100% of setups on every symbol and timeframe - the EA could not place a
|
|
//--- single trade. Deriving TP from the realised risk restores the coupling the swing-anchored TP
|
|
//--- used to provide, and makes the default 1:2 rejection filter satisfiable by construction.
|
|
//--- Min_Risk_Reward_Ratio stays a pure REJECTION filter - it is never used to size TP here; it
|
|
//--- simply stops firing against this mode unless the user raises it above the base RR.
|
|
#define SL_INTELLIGENT_BASE_MULT 3.0
|
|
#define TP_INTELLIGENT_BASE_RR 2.5
|
|
#define AI_SL_TIGHTEN_FACTOR 0.3
|
|
#define AI_TP_WIDEN_FACTOR 1.0
|
|
//--- ENTRY_MULTIPLIER "Intelligent"/"Prev swing" sentinels (ENTRY_INTELLIGENT/ENTRY_PREV_SWING in
|
|
//--- Enumerations\InputEnums.mqh, -100/-101), kept as local macros for the same include-order
|
|
//--- independence as the SL/TP sentinels above. ENTRY_INTELLIGENT_BASE_MULT is the DEEPEST limit
|
|
//--- pullback (in ATRs, at zero confidence); it shrinks linearly to 0 (market fill) as confidence -> 1.
|
|
#define ENTRY_INTELLIGENT_MODE (-100)
|
|
#define ENTRY_PREV_SWING_MODE (-101)
|
|
#define ENTRY_INTELLIGENT_BASE_MULT 2.0
|
|
//
|
|
class CExpertSignalCustom : public CExpertSignal
|
|
{
|
|
private:
|
|
bool FetchTradeRecords(string tableName, TradeRecord &tradeRecords[]);
|
|
bool ShouldDeleteOldestEntry(TradeRecord &tradeRecords[]);
|
|
void DeleteOldestEntry(string tableName);
|
|
//--- (CheckForDuplicateTrade / FindLastTradeIndex / UpdateTradeStatusAndExit were declared here but
|
|
//--- never defined anywhere and never called - removed. Nothing linked against them; they only made
|
|
//--- it look as though duplicate-trade detection existed on this class.)
|
|
void UpdateTradeRecordInDatabase(string tableName, TradeRecord &tradeRecord);
|
|
void RegisterSignal(int year, int month, int day, int DOW, int hour, int minutes, string tableName, string pattern, string direction, double entryPrice, double exitPrice, string result);
|
|
void ProcessSignal(SignalInfo &signal);
|
|
void BufferSignal(SignalInfo &signal);
|
|
bool CheckClosePosition(bool isLong, double &price);
|
|
bool CheckOpenPosition(bool isLong, double &price, double &sl, double &tp, datetime &expiration);
|
|
bool ShouldTraceTradeRejections(void) const;
|
|
//--- Mirrors CExpertTrade::Buy()/Sell()'s own price-vs-stops-level decision so OpenParams() can
|
|
//--- validate the stops against the order type the trade layer is actually going to send.
|
|
ENUM_ORDER_TYPE ResolveOrderType(bool isLong, double price);
|
|
void BufferNewTickSignal(string filterID, string pattern, string bias, const MqlDateTime& gmtTime, double entryPrice);
|
|
string PatternTableName(string filterID, string pattern, string direction);
|
|
string PatternName(int patternIndex) { return "Pattern_" + IntegerToString(patternIndex); }
|
|
SignalInfo signalBuffer[];
|
|
|
|
protected:
|
|
bool m_prohibition_signal;
|
|
bool m_useDatabase;
|
|
CiATR m_ATR; // ATR indicator
|
|
string m_id;
|
|
string m_active_pattern;
|
|
string m_active_direction;
|
|
int m_pattern_count;
|
|
double m_entry_multiplier; // Configurable multiple for ATR entry adjustment
|
|
int m_periods; // ATR periods
|
|
int m_sl_mode; // STOP_LOSS_MODE int: >0 = fixed ATR multiple beyond swing; SL_INTELLIGENT(-1) = AI-confidence scaled
|
|
int m_tp_mode; // TAKE_PROFIT_MODE int: >0 = fixed ATR multiple from entry; TP_INTELLIGENT(-1) = AI-confidence scaled
|
|
double m_min_risk_reward_ratio; // Minimum reward:risk to open a trade - REJECTION FILTER ONLY, never shapes TP
|
|
int m_confidence_source; // CONFIDENCE_SOURCE underlying int (0=AI, 1=DB, 2=Blended)
|
|
//--- 0..1 min. AI confidence, reversed against the position, required to trigger an early exit. Set
|
|
//--- from the SAME Min_Vote_Close input that drives m_threshold_close, just rescaled - see that
|
|
//--- input's declaration comment (Variables\Inputs.mqh) for why one number governs both exit routes.
|
|
//--- There is deliberately no companion on/off flag: Min_Vote_Close = Disabled resolves to 1.01 here,
|
|
//--- which no softmax confidence can reach, so the route switches itself off.
|
|
double m_ai_exit_threshold;
|
|
double m_dbConfidence; // last average normalized DB win-rate across active filters
|
|
//--- Direction()'s per-second aggregation state. MUST be per-instance, not function-local statics -
|
|
//--- Direction() is inherited as-is (not overridden) by every CExpertSignalCustom subclass that
|
|
//--- doesn't provide its own (the root "signal" object AND CExpertSignalAIBase, so PAI/CONV/LSTM),
|
|
//--- meaning they'd all share one compiled function body. Function-local statics there would be a
|
|
//--- single instance shared across the root signal and every AI filter, each stomping on the
|
|
//--- others' in-progress per-second average instead of keeping their own.
|
|
//--- The window key is a full GMT timestamp, NOT MqlDateTime.sec. Keying on the 0-59 seconds FIELD
|
|
//--- alone made two calls a minute (or an hour, or a day) apart look like the same window: with
|
|
//--- Expert_EveryTick=false every call lands on a bar open, where sec is always 0, so the window
|
|
//--- never rolled over and every bar's vote accumulated into one ever-growing average that decayed
|
|
//--- toward 0 as the run went on. A full timestamp rolls the window over on every new second, which
|
|
//--- is what "average the votes cast within one second" was always meant to mean.
|
|
datetime m_directionCurrentSecond;
|
|
double m_directionAggregatedResult;
|
|
int m_directionCount;
|
|
double m_directionLastResult;
|
|
int m_lastFiredDirection; // +1 Buy / -1 Sell / 0 none - THIS filter's own latest vote,
|
|
// set in Direction() before children are added in. Unlike
|
|
// GetActiveDirection(), never consumed/reset by a read - a
|
|
// pure peek, safe for a parent to poll every tick.
|
|
|
|
public:
|
|
CExpertSignalCustom(void);
|
|
~CExpertSignalCustom(void);
|
|
virtual bool AddFilter(CExpertSignal *filter);
|
|
virtual bool CheckOpenLong(double &price, double &sl, double &tp, datetime &expiration) override;
|
|
virtual bool CheckOpenShort(double &price, double &sl, double &tp, datetime &expiration) override;
|
|
virtual bool CheckCloseLong(double &price) override;
|
|
virtual bool CheckCloseShort(double &price) override;
|
|
bool OpenParams(bool isLong, double &price, double &sl, double &tp, datetime &expiration); // Added for generalized parameter calculation
|
|
virtual bool OpenLongParams(double &price, double &sl, double &tp, datetime &expiration) override;
|
|
virtual bool OpenShortParams(double &price, double &sl, double &tp, datetime &expiration) override;
|
|
virtual bool ValidationSettings(void) override;
|
|
virtual bool InitIndicators(CIndicators *indicators) override;
|
|
void Entry_Multiplier(double entry_multiplier) { m_entry_multiplier = entry_multiplier; }
|
|
void Periods(int periods) { m_periods = periods; }
|
|
void SLMode(int value) { m_sl_mode = value; }
|
|
void TPMode(int value) { m_tp_mode = value; }
|
|
void MinRiskRewardRatio(double value) { m_min_risk_reward_ratio = value; }
|
|
void ConfidenceSource(int value) { m_confidence_source = value; }
|
|
void AIExitThreshold(double value) { m_ai_exit_threshold = value; }
|
|
int LastFiredDirection(void) { return m_lastFiredDirection; }
|
|
// 0.0 = no AI confidence available (pure rule-based); overridden in
|
|
// CExpertSignalAIBase to return the live signal's confidence in [0,1].
|
|
virtual double AIConfidence(void) { return 0.0; }
|
|
// Signed version of AIConfidence: sign gives direction (+ buy, - sell), used for
|
|
// AI-driven early exit. 0.0 = no AI filter (base rule-based class never exits early).
|
|
virtual double SignedAIConfidence(void) { return 0.0; }
|
|
// Returns this instance's own SignedAIConfidence() when it IS an AI signal, otherwise the live
|
|
// value the AI signal publishes each tick (g_LiveAISignedConfidence, see
|
|
// CExpertSignalAIBase::ScheduleTrainingIfNeeded). This is what lets the non-AI aggregate/root
|
|
// signal - the object CExpert actually calls to size, scale, and manage every trade - see REAL AI
|
|
// confidence instead of the constant 0 its own SignedAIConfidence() returns. Without it,
|
|
// Intelligent MM, AI SL/TP scaling, and AI-exit were all running with their AI component pinned to 0.
|
|
double LiveSignedConfidence(void);
|
|
// Combines AIConfidence()/DBConfidence() per m_confidence_source into a single 0..1
|
|
// magnitude, used to scale SL/TP and (Intelligent MM) lot size.
|
|
double EffectiveConfidence(void);
|
|
double DBConfidence(void) { return m_dbConfidence; }
|
|
virtual void ApplyPatternWeight(int patternNumber, int weight) {};
|
|
void ID(string id) { m_id = id; }
|
|
virtual string GetFilterID(void) { return m_id; };
|
|
virtual string GetActivePattern(void);
|
|
virtual string GetActiveDirection(void);
|
|
virtual int GetPatternCount(void) { return m_pattern_count; };
|
|
virtual double Direction(void) override;
|
|
//--- Vote lifecycle hooks, for filters whose LongCondition()/ShortCondition() consume one-shot state
|
|
//--- when they fire (today: CExpertSignalAIBase's m_lastNonNeutralSignal alternation gate). Direction()
|
|
//--- calls BeginVote() on itself before polling its own conditions, and RevokeVote() on any CHILD whose
|
|
//--- vote it then throws away. Without this, a vote that Hybrid's quorum suppressed still burned the
|
|
//--- child's gate: PAI flipping Buy alone on bar 10 consumed its Buy gate, so when CONV flipped Buy on
|
|
//--- bar 12 PAI was already gated to 0 and the count was STILL 1 of the 2 required - in practice all
|
|
//--- three models had to flip on the very same bar, and every near-miss cost a model that direction
|
|
//--- until the opposite signal arrived. Deliberately NOT revoked on the prohibition path: a vetoed tick
|
|
//--- still blocks only OPENING (see CheckOpenPosition), and the vote does reach m_direction where
|
|
//--- CheckClosePosition can act on it, so that vote was used, not discarded. Base = no-op.
|
|
virtual void BeginVote(void) {}
|
|
virtual void RevokeVote(void) {}
|
|
bool UpdateSignalsWeights(void);
|
|
int CalculatePatternWinRate(string pattern, TradeRecord &tr[]);
|
|
int NormalizeWinRate(double winRate);
|
|
void ProcessBufferedSignals(void);
|
|
bool InRange(double value, double min, double max); // Helper function for range checking
|
|
void UseDatabase(bool value) { m_useDatabase = value; };
|
|
//--- event handler
|
|
virtual void OnTickHandler(void);
|
|
virtual void OnChartEventHandler(const int id,
|
|
const long &lparam,
|
|
const double &dparam,
|
|
const string &sparam);
|
|
};
|
|
//+------------------------------------------------------------------+
|
|
//| Constructor |
|
|
//+------------------------------------------------------------------+
|
|
CExpertSignalCustom::CExpertSignalCustom(void) :
|
|
m_id("NULL"),
|
|
m_active_pattern("NULL"),
|
|
m_active_direction("NULL"),
|
|
m_pattern_count(0),
|
|
m_entry_multiplier(0),
|
|
m_prohibition_signal(false),
|
|
m_periods(14),
|
|
m_useDatabase(false),
|
|
m_sl_mode(3), // SL_ATR_x3
|
|
m_tp_mode(6), // TP_ATR_x6
|
|
m_min_risk_reward_ratio(2.0),
|
|
m_confidence_source(0),
|
|
//--- seeded unreachable (>1.0), so an instance whose AIExitThreshold() was never set from
|
|
//--- Min_Vote_Close cannot early-exit on a stale default rather than on the trader's setting
|
|
m_ai_exit_threshold(1.01),
|
|
m_dbConfidence(0.0),
|
|
m_directionCurrentSecond(0),
|
|
m_directionAggregatedResult(0.0),
|
|
m_directionCount(0),
|
|
m_directionLastResult(0.0),
|
|
m_lastFiredDirection(0)
|
|
{
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Combine AI/DB confidence per the configured Confidence_Source |
|
|
//+------------------------------------------------------------------+
|
|
double CExpertSignalCustom::LiveSignedConfidence(void)
|
|
{
|
|
double own = SignedAIConfidence();
|
|
return (own != 0.0) ? own : g_LiveAISignedConfidence;
|
|
}
|
|
double CExpertSignalCustom::EffectiveConfidence(void)
|
|
{
|
|
g_AISignedConfidence = LiveSignedConfidence();
|
|
g_DBConfidence = m_dbConfidence;
|
|
return CombinedConfidence(m_confidence_source);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Destructor |
|
|
//+------------------------------------------------------------------+
|
|
CExpertSignalCustom::~CExpertSignalCustom(void)
|
|
{
|
|
ArrayFree(signalBuffer);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Tester-only trade rejection tracing |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalCustom::ShouldTraceTradeRejections(void) const
|
|
{
|
|
return VerboseMode;
|
|
}
|
|
|
|
void TraceSignalRejection(const string key, const string message)
|
|
{
|
|
if(!VerboseMode)
|
|
return;
|
|
TCLog("signal-reject:" + key, message);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Single source of truth for the per-pattern/direction table name |
|
|
//+------------------------------------------------------------------+
|
|
string CExpertSignalCustom::PatternTableName(string filterID, string pattern, string direction)
|
|
{
|
|
return filterID + "_" + pattern + "_" + direction;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Helper function to check value ranges |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalCustom::InRange(double value, double min, double max)
|
|
{
|
|
return value >= min && value <= max;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Validation settings protected data |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalCustom::ValidationSettings(void)
|
|
{
|
|
if(!CExpertSignal::ValidationSettings())
|
|
return false;
|
|
// Simplified checks using the InRange helper
|
|
if(!InRange(m_periods, 0, 200))
|
|
{
|
|
printf(__FUNCTION__ ": ATR Periods must be 0-200");
|
|
return false;
|
|
}
|
|
if(!InRange(StartIndex(), 0, 200))
|
|
{
|
|
printf(__FUNCTION__ ": ATR shift must be 0-200");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Create indicators |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalCustom::InitIndicators(CIndicators *indicators)
|
|
{
|
|
//--- check pointer
|
|
if(indicators == NULL)
|
|
return(false);
|
|
//---
|
|
CExpertSignal *filter;
|
|
int total = m_filters.Total();
|
|
//--- gather information about using of timeseries
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
filter = m_filters.At(i);
|
|
m_used_series |= filter.UsedSeries();
|
|
}
|
|
//--- create required timeseries
|
|
if(!CExpertBase::InitIndicators(indicators))
|
|
return(false);
|
|
//--- initialization of indicators and timeseries in the additional filters
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
filter = m_filters.At(i);
|
|
filter.SetPriceSeries(m_open, m_high, m_low, m_close);
|
|
filter.SetOtherSeries(m_spread, m_time, m_tick_volume, m_real_volume);
|
|
if(!filter.InitIndicators(indicators))
|
|
return(false);
|
|
}
|
|
if(!indicators.Add(GetPointer(m_ATR)) || !m_ATR.Create(m_symbol.Name(), m_period, m_periods) || !CExpertSignal::InitIndicators(indicators))
|
|
{
|
|
printf(__FUNCTION__ ": error initializing indicators");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Setting an additional filter |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalCustom::AddFilter(CExpertSignal *filter)
|
|
{
|
|
if(filter == NULL)
|
|
return false;
|
|
if(!filter.Init(m_symbol, m_period, m_adjusted_point))
|
|
return false;
|
|
if(!m_filters.Add(filter))
|
|
return false;
|
|
filter.EveryTick(m_every_tick);
|
|
filter.Magic(m_magic);
|
|
CExpertSignalCustom *customFilter = dynamic_cast<CExpertSignalCustom*>(filter);
|
|
if(customFilter != NULL)
|
|
{
|
|
string filterID = customFilter.GetFilterID();
|
|
if(filterID != "NULL" && m_useDatabase)
|
|
{
|
|
int patternCount = customFilter.GetPatternCount();
|
|
for(int i = 0; i < patternCount; i++)
|
|
{
|
|
string tableNameBuy = PatternTableName(filterID, PatternName(i), "Buy");
|
|
string tableNameSell = PatternTableName(filterID, PatternName(i), "Sell");
|
|
dbm.CreateTable(tableNameBuy, tableschema); // Create table for Buy direction
|
|
dbm.CreateTable(tableNameSell, tableschema); // Create table for Sell direction
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Which order type a given entry price will actually produce. |
|
|
//| CExpertTrade::Buy()/Sell() route on price vs ask/bid +- the |
|
|
//| SYMBOL_TRADE_STOPS_LEVEL: further out than that in the pending |
|
|
//| direction becomes a stop/limit order, anything nearer becomes a |
|
|
//| market fill. Reproducing that decision here (rather than assuming |
|
|
//| "Entry_Multiplier != MARKET means pending") is what lets |
|
|
//| OpenParams() validate the SL/TP against the right reference |
|
|
//| price - the article measures a market order's stops from the |
|
|
//| OPPOSITE side of the spread and a pending order's from its own |
|
|
//| activation price, and those are different numbers. |
|
|
//+------------------------------------------------------------------+
|
|
ENUM_ORDER_TYPE CExpertSignalCustom::ResolveOrderType(bool isLong, double price)
|
|
{
|
|
if(price <= 0.0)
|
|
return(isLong ? ORDER_TYPE_BUY : ORDER_TYPE_SELL);
|
|
double stops = TCStopsLevel(m_symbol.Name());
|
|
if(isLong)
|
|
{
|
|
double ask = m_symbol.Ask();
|
|
if(price > ask + stops)
|
|
return(ORDER_TYPE_BUY_STOP);
|
|
if(price < ask - stops)
|
|
return(ORDER_TYPE_BUY_LIMIT);
|
|
return(ORDER_TYPE_BUY);
|
|
}
|
|
double bid = m_symbol.Bid();
|
|
if(price > bid + stops)
|
|
return(ORDER_TYPE_SELL_LIMIT);
|
|
if(price < bid - stops)
|
|
return(ORDER_TYPE_SELL_STOP);
|
|
return(ORDER_TYPE_SELL);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Wrapper functions for buying and selling parameters |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalCustom::OpenParams(bool isLong, double &price, double &sl, double &tp, datetime &expiration)
|
|
{
|
|
int idx = StartIndex();
|
|
double atr = m_ATR.Main(idx);
|
|
if(!MathIsValidNumber(atr) || atr <= 0.0)
|
|
return false; // ATR must be positive
|
|
if(!m_symbol.Name(_Symbol))
|
|
return false; // Symbol information must be accessible
|
|
//--- Article 2555 #14: every symbol-property read below (stops level, point, digits) silently
|
|
//--- returns 0 for a symbol that is not selected/quoted, which would turn each of the checks
|
|
//--- further down into an unconditional pass. Verify the symbol is real and quoted first.
|
|
string tc_reason;
|
|
if(!TCSymbolIsTradeable(m_symbol.Name(), tc_reason))
|
|
{
|
|
TraceSignalRejection("openparams-symbol:" + m_symbol.Name(),
|
|
__FUNCTION__ + ": rejected - " + tc_reason);
|
|
return false;
|
|
}
|
|
int lookback_period = m_periods;
|
|
//--- Article 2555 #8: iLowest/iHighest below scan `lookback_period` bars starting at `idx`, and
|
|
//--- the ATR read above needs its own warm-up. Rather than discovering the shortfall as a -1
|
|
//--- index (handled below) or as a silently truncated scan, check the series depth up front and
|
|
//--- let the terminal build the missing history - the next tick finds it ready.
|
|
if(!TCHasEnoughHistory(m_symbol.Name(), m_period, lookback_period + idx + m_periods, tc_reason))
|
|
{
|
|
TraceSignalRejection("openparams-history:" + m_symbol.Name(),
|
|
__FUNCTION__ + ": rejected - " + tc_reason);
|
|
return false;
|
|
}
|
|
double base_price = (m_base_price == 0.0) ? (isLong ? m_symbol.Ask() : m_symbol.Bid()) : m_base_price;
|
|
if(!MathIsValidNumber(base_price) || base_price <= 0.0)
|
|
return false; // Price feed must be valid
|
|
// Keep swing sourcing strictly bound to this signal's symbol/timeframe. Mixing chart globals
|
|
// here can yield index/value mismatches in tester runs and diverge from classic behavior.
|
|
int lowest_index = iLowest(m_symbol.Name(), m_period, MODE_LOW, lookback_period, idx);
|
|
int highest_index = iHighest(m_symbol.Name(), m_period, MODE_HIGH, lookback_period, idx);
|
|
if(lowest_index < 0 || highest_index < 0)
|
|
{
|
|
// iLowest/iHighest return -1 when the requested history isn't synced yet (thin symbol history,
|
|
// timeframe just changed, broker feed gap). Indexing Low()/High() with -1 would otherwise feed
|
|
// a bogus swing price into SL/TP below - reject the setup instead.
|
|
if(ShouldTraceTradeRejections())
|
|
TraceSignalRejection("openparams-swing-index:" + m_symbol.Name(),
|
|
__FUNCTION__ + ": rejected - iLowest/iHighest returned an invalid index (lowest=" + IntegerToString(lowest_index) +
|
|
", highest=" + IntegerToString(highest_index) + ") for " + m_symbol.Name() + ", insufficient history synced.");
|
|
return false;
|
|
}
|
|
double lowest_low = iLow(m_symbol.Name(), m_period, lowest_index);
|
|
double highest_high = iHigh(m_symbol.Name(), m_period, highest_index);
|
|
if(lowest_low >= DBL_MAX * 0.5 || highest_high >= DBL_MAX * 0.5)
|
|
{
|
|
if(ShouldTraceTradeRejections())
|
|
TraceSignalRejection("openparams-swing-sentinel:" + m_symbol.Name(),
|
|
StringFormat("%s: rejected - swing prices are sentinel-like (lowest_low=%g, highest_high=%g, symbol=%s, period=%d, low_idx=%d, high_idx=%d).",
|
|
__FUNCTION__, lowest_low, highest_high, m_symbol.Name(), m_period, lowest_index, highest_index));
|
|
return false;
|
|
}
|
|
if(!MathIsValidNumber(lowest_low) || !MathIsValidNumber(highest_high))
|
|
{
|
|
if(ShouldTraceTradeRejections())
|
|
TraceSignalRejection("openparams-swing-nonfinite:" + m_symbol.Name(),
|
|
StringFormat("%s: rejected - swing prices are not finite (lowest_low=%g, highest_high=%g, symbol=%s, period=%d).",
|
|
__FUNCTION__, lowest_low, highest_high, m_symbol.Name(), m_period));
|
|
return false;
|
|
}
|
|
if(lowest_low <= 0.0 || highest_high <= 0.0)
|
|
{
|
|
if(ShouldTraceTradeRejections())
|
|
TraceSignalRejection("openparams-swing-nonpositive:" + m_symbol.Name(),
|
|
StringFormat("%s: rejected - swing prices are non-positive (lowest_low=%g, highest_high=%g, symbol=%s, period=%d).",
|
|
__FUNCTION__, lowest_low, highest_high, m_symbol.Name(), m_period));
|
|
return false;
|
|
}
|
|
double minRR = m_min_risk_reward_ratio; // REJECTION threshold only - never used to size TP
|
|
// Refresh the confidence bridge every tick regardless of SL/TP mode, so Intelligent MM
|
|
// (Money\MoneyIntelligent.mqh), the intelligent trailing (Trailing\TrailingIntelligent.mqh), and
|
|
// intelligent entry below all see a fresh value even when SL/TP are left on fixed-ATR presets.
|
|
double confidence = EffectiveConfidence();
|
|
if(!MathIsValidNumber(confidence))
|
|
confidence = 0.0;
|
|
// --- Entry price. Offsets are measured from the CURRENT price (base_price = bid/ask), except
|
|
// ENTRY_PREV_SWING which anchors to the recent swing. The resulting price is what
|
|
// CExpertTrade::Buy/Sell routes into a market / limit / stop order (it compares price to
|
|
// ask/bid +- the broker stop-level itself), so a near-market price simply fills at market.
|
|
int entryMode = (int)m_entry_multiplier;
|
|
if(entryMode == ENTRY_PREV_SWING_MODE)
|
|
price = m_symbol.NormalizePrice(isLong ? lowest_low : highest_high);
|
|
else if(entryMode == ENTRY_INTELLIGENT_MODE)
|
|
{
|
|
// Deep limit pullback when unsure, shrinking to a market fill as confidence -> 1.
|
|
double pull = ENTRY_INTELLIGENT_BASE_MULT * (1.0 - confidence) * atr;
|
|
price = m_symbol.NormalizePrice(isLong ? (base_price - pull) : (base_price + pull));
|
|
}
|
|
else
|
|
// Fixed ATR presets: buy => base + mult*ATR (limit below / stop above for -/+ mult);
|
|
// sell => base - mult*ATR (limit above / stop below). MARKET (0) leaves price at bid/ask.
|
|
price = m_symbol.NormalizePrice(isLong ? (base_price + entryMode * atr) : (base_price - entryMode * atr));
|
|
// --- Stop loss: always swing-anchored. SL_ATR_* pad the swing by that many ATR; SL_INTELLIGENT
|
|
// tightens the pad as confidence rises; SL_PREV_SWING sits EXACTLY at the swing (zero pad).
|
|
double slMultiplier;
|
|
if(m_sl_mode == SL_INTELLIGENT_MODE)
|
|
slMultiplier = SL_INTELLIGENT_BASE_MULT * (1.0 - AI_SL_TIGHTEN_FACTOR * confidence);
|
|
else if(m_sl_mode == SL_PREV_SWING_MODE)
|
|
slMultiplier = 0.0;
|
|
else
|
|
slMultiplier = (double)m_sl_mode;
|
|
sl = isLong ? m_symbol.NormalizePrice(lowest_low - slMultiplier * atr)
|
|
: m_symbol.NormalizePrice(highest_high + slMultiplier * atr);
|
|
// Enforce a hard minimum SL distance from entry (broker stop-level / sanity floor). Deliberately
|
|
// applied BEFORE take profit below: TP_INTELLIGENT sizes itself off the FINAL entry-to-stop distance,
|
|
// so a floor that widened the stop afterwards would silently shrink the realised reward:risk below the
|
|
// ratio that mode is meant to guarantee - and, at the shipped defaults, straight back under the Min RR
|
|
// rejection threshold.
|
|
if(fabs(price - sl) < (MIN_SL_ATR_MULTIPLIER * atr))
|
|
sl = isLong ? (price - MIN_SL_ATR_MULTIPLIER * atr) : (price + MIN_SL_ATR_MULTIPLIER * atr);
|
|
double risk = fabs(price - sl);
|
|
// --- Take profit: TP_ATR_* are an ATR multiple FROM THE ENTRY PRICE; TP_INTELLIGENT is a multiple of
|
|
// THIS TRADE'S OWN RISK, widening with confidence (see TP_INTELLIGENT_BASE_RR's comment for why
|
|
// it is risk-relative rather than ATR-relative); TP_PREV_SWING instead targets the opposite recent
|
|
// swing (buy: swing high / sell: swing low). Min RR (below) only rejects, never reshapes any of these.
|
|
if(m_tp_mode == TP_PREV_SWING_MODE)
|
|
tp = isLong ? m_symbol.NormalizePrice(highest_high) : m_symbol.NormalizePrice(lowest_low);
|
|
else
|
|
if(m_tp_mode == TP_INTELLIGENT_MODE)
|
|
{
|
|
double targetRR = TP_INTELLIGENT_BASE_RR * (1.0 + AI_TP_WIDEN_FACTOR * confidence);
|
|
tp = isLong ? m_symbol.NormalizePrice(price + targetRR * risk)
|
|
: m_symbol.NormalizePrice(price - targetRR * risk);
|
|
}
|
|
else
|
|
{
|
|
double tpMultiplier = (double)m_tp_mode;
|
|
tp = isLong ? m_symbol.NormalizePrice(price + tpMultiplier * atr)
|
|
: m_symbol.NormalizePrice(price - tpMultiplier * atr);
|
|
}
|
|
// Guard rail: when both AI and classic share this path, any non-finite or negative level here is an
|
|
// upstream data/state issue, not a mode-specific feature. Reject early with full context.
|
|
if(!MathIsValidNumber(price) || price < 0.0 ||
|
|
!MathIsValidNumber(sl) || sl < 0.0 ||
|
|
!MathIsValidNumber(tp) || tp < 0.0)
|
|
{
|
|
if(ShouldTraceTradeRejections())
|
|
TraceSignalRejection("openparams-invalid-levels:" + m_symbol.Name(),
|
|
StringFormat("%s: rejected - invalid computed levels (isLong=%s, entryMode=%d, slMode=%d, tpMode=%d, atr=%g, base=%g, low=%g, high=%g, price=%g, sl=%g, tp=%g).",
|
|
__FUNCTION__, isLong ? "true" : "false", entryMode, m_sl_mode, m_tp_mode,
|
|
atr, base_price, lowest_low, highest_high, price, sl, tp));
|
|
return false;
|
|
}
|
|
// --- Article 2555 #6: SL and TP must clear SYMBOL_TRADE_STOPS_LEVEL, measured against the price of
|
|
// the OPPOSITE operation for a market order (a long closes at Bid, a short at Ask) or against
|
|
// the activation price for a pending one. Nothing upstream enforced this: SL is anchored to a
|
|
// recent swing and TP to an ATR/RR multiple, both of which can land inside the broker's minimum
|
|
// distance on a quiet bar or a wide-spread symbol - the trade was then built, sized by Money,
|
|
// and rejected server-side with "Invalid stops" (10016) with nothing in the log explaining why.
|
|
// Which order type this becomes is decided by CExpertTrade::Buy()/Sell() purely from `price` vs
|
|
// ask/bid +- the stops level, so the same comparison is reproduced here to pick the type the
|
|
// stops will actually be validated against.
|
|
ENUM_ORDER_TYPE order_type = ResolveOrderType(isLong, price);
|
|
string stops_note;
|
|
if(!TCAdjustStops(m_symbol.Name(), order_type, price, sl, tp, stops_note))
|
|
{
|
|
TraceSignalRejection("openparams-stops:" + m_symbol.Name(), __FUNCTION__ + ": rejected - " + stops_note);
|
|
return false;
|
|
}
|
|
if(stops_note != "")
|
|
TraceSignalRejection("openparams-stops-adj:" + m_symbol.Name(), __FUNCTION__ + ": " + stops_note);
|
|
// A widened stop changes this trade's real risk, so recompute it before the reward:risk filter
|
|
// below - otherwise the RR the trade is accepted on is not the RR it is actually taken at.
|
|
risk = fabs(price - sl);
|
|
// Re-verify rather than trust the correction: TCAdjustStops() widens levels, and a caller that
|
|
// hands it a nonsensical pair (SL on the wrong side of the entry) can still come back illegal.
|
|
if(!TCCheckStops(m_symbol.Name(), order_type, price, sl, tp, stops_note))
|
|
{
|
|
TraceSignalRejection("openparams-stops-final:" + m_symbol.Name(), __FUNCTION__ + ": rejected - " + stops_note);
|
|
return false;
|
|
}
|
|
// A pending order's own activation price is subject to the same minimum distance. If `price`
|
|
// drifted inside it between the entry calculation above and now, CExpertTrade would quietly
|
|
// downgrade the order to a market fill at a price the setup never asked for - reject instead.
|
|
if(order_type != ORDER_TYPE_BUY && order_type != ORDER_TYPE_SELL &&
|
|
!TCCheckPendingPrice(m_symbol.Name(), order_type, price, stops_note))
|
|
{
|
|
TraceSignalRejection("openparams-pending:" + m_symbol.Name(), __FUNCTION__ + ": rejected - " + stops_note);
|
|
return false;
|
|
}
|
|
// Article 2555 #4: a pending order also has to fit inside ACCOUNT_LIMIT_ORDERS. Checked here,
|
|
// before the setup is handed to Money for sizing, so a full order book costs nothing downstream.
|
|
if(order_type != ORDER_TYPE_BUY && order_type != ORDER_TYPE_SELL &&
|
|
!TCIsNewOrderAllowed(stops_note))
|
|
{
|
|
TraceSignalRejection("openparams-orderlimit", __FUNCTION__ + ": rejected - " + stops_note);
|
|
return false;
|
|
}
|
|
// Min reward:risk is now ONLY a rejection filter (Min_Risk_Reward_Ratio) - it never reshapes TP.
|
|
double reward = fabs(tp - price);
|
|
// Bridged to Money\MoneyIntelligent.mqh's Kelly-criterion sizing the same way as
|
|
// EffectiveConfidence() above - refreshed regardless of outcome below, since a rejected
|
|
// setup here never reaches Money.CheckOpenLong/Short() this tick anyway.
|
|
g_TradeRewardRiskRatio = (risk > 0.0) ? reward / risk : 0.0;
|
|
if(reward < minRR * risk)
|
|
return false;
|
|
// Adjust expiration time
|
|
expiration += m_expiration * PeriodSeconds(m_period);
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Detecting the levels for buying |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalCustom::OpenLongParams(double &price, double &sl, double &tp, datetime &expiration)
|
|
{
|
|
return OpenParams(true, price, sl, tp, expiration);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Detecting the levels for selling |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalCustom::OpenShortParams(double &price, double &sl, double &tp, datetime &expiration)
|
|
{
|
|
return OpenParams(false, price, sl, tp, expiration);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Common function for closing positions |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalCustom::CheckClosePosition(bool isLong, double &price)
|
|
{
|
|
bool result = false;
|
|
//--- check of exceeding the threshold value, adjusted for long/short
|
|
double directionMultiplier = isLong ? -1 : 1;
|
|
// Allowing position closing without checking the prohibition signal.
|
|
if(directionMultiplier * m_direction >= m_threshold_close)
|
|
result = true;
|
|
// AI-driven early exit: close regardless of the rule-based threshold above if the AI signal has flipped
|
|
// against the open position with at least m_ai_exit_threshold confidence. LiveSignedConfidence()
|
|
// supplies the AI signal's live value even on the non-AI aggregate/root signal this runs on, so this is
|
|
// a no-op when no AI signal is active/converged yet (it returns 0.0) or when Min_Vote_Close is Disabled
|
|
// (m_ai_exit_threshold resolves to 1.01, which no confidence magnitude can reach).
|
|
//
|
|
// This is NOT redundant with the averaged vote above, which is why it exists as a second route rather
|
|
// than being folded into it. The AI's ordinary vote is ONE-SHOT - LongCondition()/ShortCondition()
|
|
// consume the m_lastNonNeutralSignal alternation gate the moment they fire - and it is then AVERAGED
|
|
// with every other filter's. So an AI reversal that lands on a bar where the average stays under
|
|
// m_threshold_close has already burned its gate and will never be re-offered, leaving the position open
|
|
// for as long as the AI holds that (now un-votable) view. Reading the LIVE signed confidence here,
|
|
// undiluted and every bar, is what closes that hole.
|
|
if(!result)
|
|
{
|
|
double signed_conf = LiveSignedConfidence();
|
|
bool reversedAgainstLong = isLong && signed_conf < 0.0 && MathAbs(signed_conf) >= m_ai_exit_threshold;
|
|
bool reversedAgainstShort = !isLong && signed_conf > 0.0 && MathAbs(signed_conf) >= m_ai_exit_threshold;
|
|
if(reversedAgainstLong || reversedAgainstShort)
|
|
result = true;
|
|
}
|
|
if(result)
|
|
{
|
|
//--- try to get the level of closing, differentiating based on isLong
|
|
if(!(isLong ? CloseLongParams(price) : CloseShortParams(price)))
|
|
result = false;
|
|
}
|
|
//--- zeroize the base price
|
|
m_base_price = 0.0;
|
|
//--- return the result
|
|
return result;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Generating a signal for closing of a long position |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalCustom::CheckCloseLong(double &price)
|
|
{
|
|
return CheckClosePosition(true, price);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Generating a signal for closing a short position |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalCustom::CheckCloseShort(double &price)
|
|
{
|
|
return CheckClosePosition(false, price);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Common function for opening positions |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalCustom::CheckOpenPosition(bool isLong, double &price, double &sl, double &tp, datetime &expiration)
|
|
{
|
|
bool result = false;
|
|
//--- the "prohibition" signal
|
|
if(m_prohibition_signal == true)
|
|
{
|
|
if(ShouldTraceTradeRejections())
|
|
TraceSignalRejection("open-prohibition",
|
|
StringFormat("%s: open %s rejected - a child filter vetoed the tick (prohibition signal).",
|
|
__FUNCTION__, isLong ? "long" : "short"));
|
|
return false;
|
|
}
|
|
//--- check of exceeding the threshold value, adjusted for long/short
|
|
double directionMultiplier = isLong ? 1 : -1;
|
|
if(directionMultiplier * m_direction >= m_threshold_open)
|
|
{
|
|
//--- there's a signal
|
|
result = true;
|
|
//--- try to get the levels of opening, differentiating based on isLong
|
|
if(!(isLong ? OpenLongParams(price, sl, tp, expiration) : OpenShortParams(price, sl, tp, expiration)))
|
|
{
|
|
// The vote reached the threshold but entry-shaping failed (invalid SL/TP, broker constraints,
|
|
// missing history). Roll back one-shot child vote state so the same directional signal can
|
|
// be re-offered on the next bar instead of being permanently consumed by this failed attempt.
|
|
int total = m_filters.Total();
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
CExpertSignalCustom *filter = m_filters.At(i);
|
|
if(filter != NULL)
|
|
filter.RevokeVote();
|
|
}
|
|
RevokeVote();
|
|
if(ShouldTraceTradeRejections())
|
|
TraceSignalRejection("open-params-failed",
|
|
StringFormat("%s: open %s rejected after direction passed threshold - order parameters failed validation (vote state restored for retry).",
|
|
__FUNCTION__, isLong ? "long" : "short"));
|
|
result = false;
|
|
}
|
|
}
|
|
else if(ShouldTraceTradeRejections())
|
|
{
|
|
TraceSignalRejection("open-threshold",
|
|
StringFormat("%s: open %s rejected - direction %.2f did not reach threshold %.2f.",
|
|
__FUNCTION__, isLong ? "long" : "short", directionMultiplier * m_direction, m_threshold_open));
|
|
}
|
|
//--- zeroize the base price
|
|
m_base_price = 0.0;
|
|
//--- return the result
|
|
return result;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Generating a buy signal |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalCustom::CheckOpenLong(double &price, double &sl, double &tp, datetime &expiration)
|
|
{
|
|
// Check if the trading strategy allows opening long positions
|
|
if(tradingdirection == LONG_ONLY || tradingdirection == BOTH)
|
|
{
|
|
return CheckOpenPosition(true, price, sl, tp, expiration);
|
|
}
|
|
// If the strategy is SHORT_ONLY, prevent opening a long position
|
|
if(ShouldTraceTradeRejections())
|
|
TraceSignalRejection("open-long-direction-block",
|
|
StringFormat("%s: open long rejected - strategy direction blocks long entries.", __FUNCTION__));
|
|
return false;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Generating a sell signal |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalCustom::CheckOpenShort(double &price, double &sl, double &tp, datetime &expiration)
|
|
{
|
|
// Check if the trading strategy allows opening short positions
|
|
if(tradingdirection == SHORT_ONLY || tradingdirection == BOTH)
|
|
{
|
|
return CheckOpenPosition(false, price, sl, tp, expiration);
|
|
}
|
|
// If the strategy is LONG_ONLY, prevent opening a short position
|
|
if(ShouldTraceTradeRejections())
|
|
TraceSignalRejection("open-short-direction-block",
|
|
StringFormat("%s: open short rejected - strategy direction blocks short entries.", __FUNCTION__));
|
|
return false;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Return the currently active pattern |
|
|
//+------------------------------------------------------------------+
|
|
string CExpertSignalCustom::GetActivePattern(void)
|
|
{
|
|
string ret = m_active_pattern;
|
|
m_active_pattern = "NULL";
|
|
return ret;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Return the currently active direction |
|
|
//+------------------------------------------------------------------+
|
|
string CExpertSignalCustom::GetActiveDirection(void)
|
|
{
|
|
string ret = m_active_direction;
|
|
m_active_direction = "NULL";
|
|
return ret;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Detecting the "weighted" direction |
|
|
//+------------------------------------------------------------------+
|
|
double CExpertSignalCustom::Direction(void)
|
|
{
|
|
MqlDateTime gmtTime;
|
|
datetime nowGMT = TimeGMT(gmtTime); // full timestamp AND broken-down form - both are used below
|
|
//--- Open a fresh intra-second averaging window whenever the second changes. This block may ONLY
|
|
//--- reset the window - it must never be the thing that publishes m_directionLastResult. It used to
|
|
//--- close the previous window here and return that value, which meant the value handed to
|
|
//--- CExpert(Custom)::SetDirection() -> m_direction (the field CheckOpenPosition/CheckClosePosition
|
|
//--- actually threshold against) was always the PREVIOUS second's average, never this call's own
|
|
//--- vote. With Expert_EveryTick=false, Direction() runs exactly once per bar at the bar open, so
|
|
//--- TimeGMT().sec is 0 on every single call: after the very first call the branch below never fired
|
|
//--- again, m_directionLastResult stayed pinned at its 0.0 seed forever, and m_direction was 0 on
|
|
//--- every bar - no signal could ever reach m_threshold_open and the EA could not open a single
|
|
//--- trade, in Classic, AI-only or Hybrid alike (they all inherit this one Direction() body). It also
|
|
//--- silently ate the AI vote entirely: CExpertSignalAIBase::LongCondition/ShortCondition consume the
|
|
//--- one-shot alternation gate (m_lastNonNeutralSignal) when they fire, so the discarded vote was
|
|
//--- never re-offered on a later bar. The window average is now computed at the end of this function
|
|
//--- with this call's own result folded in, so what is returned always includes the current tick.
|
|
if(nowGMT != m_directionCurrentSecond)
|
|
{
|
|
m_directionAggregatedResult = 0.0;
|
|
m_directionCount = 0;
|
|
m_directionCurrentSecond = nowGMT; // Update the current second
|
|
}
|
|
m_prohibition_signal = false;
|
|
BeginVote(); // snapshot any one-shot vote state, so a discarded vote can be rolled back - see BeginVote()
|
|
double result = m_weight * (LongCondition() - ShortCondition());
|
|
//--- Non-consuming quorum peek - see m_lastFiredDirection's declaration comment. Snapshotted from
|
|
//--- this filter's OWN vote, before the loop below adds any children's contributions in.
|
|
m_lastFiredDirection = (result > 0.0) ? 1 : ((result < 0.0) ? -1 : 0);
|
|
int number = (result == 0.0) ? 0 : 1;
|
|
int total = m_filters.Total();
|
|
PrintVerbose("Starting direction calculation with total filters: " + IntegerToString(total));
|
|
//--- Pass 1: refresh every filter's own Direction() - required regardless of quorum, since this is
|
|
//--- what drives each filter's own training/DB-buffering/m_lastFiredDirection side effects - caching
|
|
//--- the returned magnitude for pass 2 below instead of summing it immediately. Quorum suppression
|
|
//--- (pass 2) needs every quorum-flagged filter's m_lastFiredDirection already fresh for THIS tick;
|
|
//--- checking mid-loop, as a single pass used to, would compare against filters not yet visited this
|
|
//--- iteration (stale, still holding last tick's value).
|
|
double directions[];
|
|
ArrayResize(directions, total);
|
|
bool aborted = false;
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
long mask = ((long)1) << i;
|
|
if((m_ignore & mask) != 0)
|
|
{
|
|
directions[i] = EMPTY_VALUE;
|
|
continue;
|
|
}
|
|
CExpertSignalCustom *filter = m_filters.At(i);
|
|
if(filter == NULL)
|
|
{
|
|
Print("Error: Filter at index " + IntegerToString(i) + " is NULL");
|
|
directions[i] = EMPTY_VALUE;
|
|
continue;
|
|
}
|
|
double price = 0.0, sl = 0.0, tp = 0.0;
|
|
datetime expiration = 0;
|
|
string bias = filter.GetActiveDirection();
|
|
string filterID = filter.GetFilterID();
|
|
string pattern = filter.GetActivePattern();
|
|
//--- Only journal a pattern when the label AGREES with the net vote the filter actually cast.
|
|
//--- m_active_direction/m_active_pattern are last-writer-wins across LongCondition() then
|
|
//--- ShortCondition(), and both sides can fire on the same bar - e.g. CSignalMA with close below
|
|
//--- its MA returns Pattern_1 long AND Pattern_0 short, netting to a vote of 0 while the labels
|
|
//--- read "Sell"/"Pattern_0". Buffering off the labels alone therefore recorded a directional
|
|
//--- pattern for a bar the filter voted FLAT on, poisoning the very win-rate table
|
|
//--- UpdateSignalsWeights() feeds back into that pattern's weight. LastFiredDirection() is the
|
|
//--- signed net vote, set in this filter's own Direction(); like the labels it is read here one
|
|
//--- tick after being written, so the two are compared as of the same tick.
|
|
int filterVote = filter.LastFiredDirection();
|
|
bool labelMatchesVote = (bias == "Buy" && filterVote > 0) || (bias == "Sell" && filterVote < 0);
|
|
if(filterID != "NULL" && bias != "NULL" && pattern != "NULL" && m_useDatabase && labelMatchesVote)
|
|
{
|
|
PrintVerbose("Processing filter: " + filterID + ", Bias: " + bias + ", Pattern: " + pattern);
|
|
double newPrice = 0;
|
|
bool signalBuffered = false; // Flag to track if signal was buffered
|
|
if(bias == "Buy")
|
|
{
|
|
if(OpenLongParams(price, sl, tp, expiration))
|
|
{
|
|
newPrice = m_symbol.Ask(); // Adjust price to current ask price
|
|
signalBuffered = true; // Set flag to true as signal will be buffered
|
|
}
|
|
}
|
|
else
|
|
if(bias == "Sell")
|
|
{
|
|
if(OpenShortParams(price, sl, tp, expiration))
|
|
{
|
|
newPrice = m_symbol.Bid(); // Adjust price to current bid price
|
|
signalBuffered = true; // Set flag to true as signal will be buffered
|
|
}
|
|
}
|
|
if(signalBuffered)
|
|
{
|
|
BufferNewTickSignal(filterID, pattern, bias, gmtTime, newPrice);
|
|
}
|
|
}
|
|
double direction = filter.Direction();
|
|
if(direction == EMPTY_VALUE)
|
|
{
|
|
m_prohibition_signal = true;
|
|
directions[i] = EMPTY_VALUE;
|
|
continue;
|
|
}
|
|
// Validate the result to be within the range of -100 to 100
|
|
if(direction < -100 || direction > 100)
|
|
{
|
|
PrintVerbose("A filter's direction is invalid. Skipping tick.");
|
|
result = 0;
|
|
number = 0;
|
|
aborted = true;
|
|
break;
|
|
}
|
|
directions[i] = direction;
|
|
}
|
|
//--- The tick was discarded, so NO filter's vote was used - roll every one of them back, for the same
|
|
//--- reason a quorum-suppressed vote is rolled back in pass 2 below (see BeginVote()/RevokeVote()).
|
|
if(aborted)
|
|
{
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
CExpertSignalCustom *filter = m_filters.At(i);
|
|
if(filter != NULL)
|
|
filter.RevokeVote();
|
|
}
|
|
}
|
|
//--- Pass 2: sum each filter's cached contribution. Standard weighted voting only - no quorum gate.
|
|
if(!aborted)
|
|
{
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
double direction = directions[i];
|
|
if(direction == EMPTY_VALUE || direction == 0)
|
|
continue;
|
|
CExpertSignalCustom *filter = m_filters.At(i);
|
|
number++; // Only increment `number` if `direction` is not 0 or EMPTY_VALUE and not suppressed
|
|
long mask = ((long)1) << i;
|
|
result += ((m_invert & mask) != 0) ? -direction : direction;
|
|
}
|
|
}
|
|
//--- Normalization, as CExpertSignal::Direction() does it: the weighted votes are AVERAGED over the
|
|
//--- filters that actually voted, not summed. `number` was being counted here and then never used,
|
|
//--- which left result as a raw sum - two ordinary agreeing votes (e.g. MA's 60 + RSI's 100) could
|
|
//--- exceed the +-100 valid band and get zeroed by the range check below, throwing away exactly the
|
|
//--- strongest, most agreed-upon setups. Only non-zero, non-suppressed contributions increment
|
|
//--- `number` (see pass 2), so a lone filter voting 10 still normalizes to 10 and can clear a
|
|
//--- ThresholdOpen(10) on its own - averaging does not raise the bar for a single-voter signal.
|
|
if(!aborted && number != 0)
|
|
result /= number;
|
|
//--- Fold this call's result into the current second's window and publish the window average - see
|
|
//--- the window-reset block at the top of this function for why this must happen here.
|
|
m_directionAggregatedResult += result;
|
|
m_directionCount++;
|
|
m_directionLastResult = m_directionAggregatedResult / m_directionCount;
|
|
// Validate the aggregated result to be within the range of -100 to 100
|
|
if(m_directionLastResult < -100 || m_directionLastResult > 100)
|
|
{
|
|
m_directionLastResult = 0.0; // Set result to 0 if it's outside the range
|
|
Print("Directional result is out of range. Setting to 0.");
|
|
}
|
|
PrintVerbose("Final directional result: " + DoubleToString(m_directionLastResult));
|
|
return m_directionLastResult;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| handles the new bar signal buffering |
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalCustom::BufferNewTickSignal(string filterID, string pattern, string bias, const MqlDateTime& gmtTime, double entryPrice)
|
|
{
|
|
if(filterID == "NULL" || pattern == "NULL" || bias == "NULL")
|
|
{
|
|
Print("Error buffering new tick signal: Invalid filter parameters - filterID: '" + filterID +
|
|
"', pattern: '" + pattern + "', bias: '" + bias + "'.");
|
|
return;
|
|
}
|
|
string tableName = PatternTableName(filterID, pattern, bias);
|
|
SignalInfo signal = {gmtTime.year, gmtTime.mon, gmtTime.day, gmtTime.day_of_week, gmtTime.hour, gmtTime.min, tableName, pattern, bias, entryPrice};
|
|
BufferSignal(signal);
|
|
PrintVerbose("New tick signal buffered: " + tableName + ", Pattern: " + pattern + ", Bias: " + bias + ", Entry Price: " + DoubleToString(entryPrice));
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalCustom::BufferSignal(SignalInfo &signal)
|
|
{
|
|
// Check for duplicate signals in the buffer
|
|
for(int i = 0; i < ArraySize(signalBuffer); i++)
|
|
{
|
|
if(signalBuffer[i].tableName == signal.tableName &&
|
|
signalBuffer[i].pattern == signal.pattern &&
|
|
signalBuffer[i].direction == signal.direction)
|
|
{
|
|
PrintVerbose("Duplicate signal detected, not adding to buffer: " + signal.tableName + ", Pattern: " + signal.pattern + ", Direction: " + signal.direction);
|
|
return; // Skip buffering if a duplicate is found
|
|
}
|
|
}
|
|
// Resize the buffer and add the new signal
|
|
ArrayResize(signalBuffer, ArraySize(signalBuffer) + 1);
|
|
signalBuffer[ArraySize(signalBuffer) - 1] = signal;
|
|
PrintVerbose("Signal buffered for: " + signal.tableName + ", Pattern: " + signal.pattern + ", Direction: " + signal.direction);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Process the signal and update trades |
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalCustom::ProcessSignal(SignalInfo &signal)
|
|
{
|
|
string currentTableName = signal.tableName;
|
|
string oppositeTableName = currentTableName; // Start with a copy of the current table name
|
|
PrintVerbose("Processing signal for table: " + currentTableName);
|
|
// Swap the direction in the table name to get the opposite table name
|
|
if(signal.direction == "Buy")
|
|
{
|
|
StringReplace(oppositeTableName, "Buy", "Sell");
|
|
PrintVerbose("Swapped to opposite table: " + oppositeTableName + " from Buy to Sell");
|
|
}
|
|
else
|
|
{
|
|
StringReplace(oppositeTableName, "Sell", "Buy");
|
|
PrintVerbose("Swapped to opposite table: " + oppositeTableName + " from Sell to Buy");
|
|
}
|
|
// Fetch trade records for both directions
|
|
TradeRecord tradeRecordsCurrent[], tradeRecordsOpposite[];
|
|
if(!FetchTradeRecords(currentTableName, tradeRecordsCurrent))
|
|
{
|
|
Print("Failed to fetch current direction trades from: " + currentTableName);
|
|
return; // Fail to fetch current direction trades
|
|
}
|
|
if(!FetchTradeRecords(oppositeTableName, tradeRecordsOpposite))
|
|
{
|
|
Print("Failed to fetch opposite direction trades from: " + oppositeTableName);
|
|
return; // Fail to fetch opposite direction trades
|
|
}
|
|
if(ShouldDeleteOldestEntry(tradeRecordsCurrent))
|
|
DeleteOldestEntry(currentTableName);
|
|
if(ShouldDeleteOldestEntry(tradeRecordsOpposite))
|
|
DeleteOldestEntry(oppositeTableName);
|
|
// Process trades in the opposite direction to close them
|
|
bool isTradeOpen = false;
|
|
for(int i = 0; i < ArraySize(tradeRecordsOpposite); i++)
|
|
{
|
|
if(tradeRecordsOpposite[i].pattern == signal.pattern && tradeRecordsOpposite[i].result == "NA")
|
|
{
|
|
// Close the opposite trade
|
|
tradeRecordsOpposite[i].exitPrice = signal.entryPrice;
|
|
double profitLoss = (tradeRecordsOpposite[i].direction == "Buy") ?
|
|
(signal.entryPrice - tradeRecordsOpposite[i].entryPrice) :
|
|
(tradeRecordsOpposite[i].entryPrice - signal.entryPrice);
|
|
tradeRecordsOpposite[i].result = profitLoss >= 0 ? "Profit" : "Loss";
|
|
UpdateTradeRecordInDatabase(oppositeTableName, tradeRecordsOpposite[i]);
|
|
PrintVerbose("Closed opposite trade: " + oppositeTableName + ", Profit/Loss: " + DoubleToString(profitLoss));
|
|
isTradeOpen = true; // Signal that a trade was handled
|
|
break; // Since it's a stop and reverse, handle only one trade at a time
|
|
}
|
|
}
|
|
// Check for open trades or duplicate entries in the current direction
|
|
for(int i = 0; i < ArraySize(tradeRecordsCurrent); i++)
|
|
{
|
|
// Check for exact duplicates first
|
|
if(tradeRecordsCurrent[i].pattern == signal.pattern &&
|
|
tradeRecordsCurrent[i].year == signal.year &&
|
|
tradeRecordsCurrent[i].month == signal.month &&
|
|
tradeRecordsCurrent[i].day == signal.day &&
|
|
tradeRecordsCurrent[i].hour == signal.hour &&
|
|
tradeRecordsCurrent[i].minutes == signal.minutes)
|
|
{
|
|
PrintVerbose("Duplicate trade found, not registering new trade. Table: " + currentTableName);
|
|
return; // Duplicate trade found, exit processing
|
|
}
|
|
// Check for outdated or same time trades
|
|
if((tradeRecordsCurrent[i].year > signal.year) ||
|
|
(tradeRecordsCurrent[i].year == signal.year && tradeRecordsCurrent[i].month > signal.month) ||
|
|
(tradeRecordsCurrent[i].year == signal.year && tradeRecordsCurrent[i].month == signal.month && tradeRecordsCurrent[i].day > signal.day) ||
|
|
(tradeRecordsCurrent[i].year == signal.year && tradeRecordsCurrent[i].month == signal.month && tradeRecordsCurrent[i].day == signal.day && tradeRecordsCurrent[i].hour > signal.hour) ||
|
|
(tradeRecordsCurrent[i].year == signal.year && tradeRecordsCurrent[i].month == signal.month && tradeRecordsCurrent[i].day == signal.day && tradeRecordsCurrent[i].hour == signal.hour && tradeRecordsCurrent[i].minutes >= signal.minutes))
|
|
{
|
|
PrintVerbose("Outdated or same time trade found, not registering new trade. Table: " + currentTableName);
|
|
return; // Outdated or same time trade found, exit processing
|
|
}
|
|
// Check if there's an open trade with the same pattern
|
|
if(tradeRecordsCurrent[i].result == "NA" && tradeRecordsCurrent[i].pattern == signal.pattern)
|
|
{
|
|
PrintVerbose("Open trade found, not registering new trade. Table: " + currentTableName + ", Pattern: " + signal.pattern);
|
|
return; // Open trade found, exit processing
|
|
}
|
|
}
|
|
// Register a new trade if no duplicates, outdated, or open trades are found
|
|
if(!isTradeOpen)
|
|
{
|
|
RegisterSignal(signal.year, signal.month, signal.day, signal.DOW, signal.hour, signal.minutes,
|
|
currentTableName, signal.pattern, signal.direction, signal.entryPrice, 0.0, "NA");
|
|
PrintVerbose("Registered new trade in table: " + currentTableName + ", Pattern: " + signal.pattern + ", Direction: " + signal.direction);
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Helper function to compare two datetime values |
|
|
//+------------------------------------------------------------------+
|
|
bool IsEarlier(const SignalInfo& a, const SignalInfo& b)
|
|
{
|
|
datetime dtA = MakeDateTime(a);
|
|
datetime dtB = MakeDateTime(b);
|
|
return dtA < dtB;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Selection sort for sorting SignalInfo array by datetime |
|
|
//+------------------------------------------------------------------+
|
|
void SelectionSort(SignalInfo &signals[], int size)
|
|
{
|
|
for(int i = 0; i < size - 1; i++)
|
|
{
|
|
int min_idx = i;
|
|
for(int j = i + 1; j < size; j++)
|
|
{
|
|
if(IsEarlier(signals[j], signals[min_idx]))
|
|
{
|
|
min_idx = j;
|
|
}
|
|
}
|
|
if(min_idx != i)
|
|
{
|
|
// Swapping the elements
|
|
SignalInfo temp = signals[i];
|
|
signals[i] = signals[min_idx];
|
|
signals[min_idx] = temp;
|
|
}
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Helper function to create a sortable datetime value |
|
|
//+------------------------------------------------------------------+
|
|
datetime MakeDateTime(const SignalInfo &signal)
|
|
{
|
|
MqlDateTime t;
|
|
t.year = signal.year;
|
|
t.mon = signal.month;
|
|
t.day = signal.day;
|
|
t.hour = signal.hour;
|
|
t.min = signal.minutes;
|
|
t.sec = 0;
|
|
return StructToTime(t);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Process the signal and update trades |
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalCustom::ProcessBufferedSignals()
|
|
{
|
|
// Sort the signals array by datetime before processing
|
|
SelectionSort(signalBuffer, ArraySize(signalBuffer));
|
|
if(!dbm.OpenDatabase())
|
|
{
|
|
Print("Failed to open database.");
|
|
return;
|
|
}
|
|
if(!dbm.BeginTransaction())
|
|
{
|
|
Print(__FUNCTION__ + ": Failed to begin database transaction, " + IntegerToString(ArraySize(signalBuffer)) + " buffered signal(s) left pending for retry next cycle.");
|
|
return;
|
|
}
|
|
for(int i = 0; i < ArraySize(signalBuffer); i++)
|
|
{
|
|
PrintVerbose("Processing signal " + IntegerToString(i + 1) + " of " + IntegerToString(ArraySize(signalBuffer)));
|
|
ProcessSignal(signalBuffer[i]);
|
|
}
|
|
if(!dbm.CommitTransaction())
|
|
{
|
|
Print(__FUNCTION__ + ": Failed to commit the transaction to the database, rolling back. " + IntegerToString(ArraySize(signalBuffer)) + " buffered signal(s) left pending for retry next cycle.");
|
|
dbm.RollbackTransaction();
|
|
return;
|
|
}
|
|
ArrayResize(signalBuffer, 0);
|
|
PrintVerbose("Signal buffer cleared after processing.");
|
|
// NOTE: does NOT close dbm here - the caller (CExpertCustom::OnTimer) opens the shared
|
|
// connection once and also calls UpdateSignalsWeights() right after this returns; closing it
|
|
// here made UpdateSignalsWeights() silently fail (BeginTransaction on a closed handle) in every
|
|
// live/demo run (IsBacktesting only skipped this close in the tester, masking the bug there).
|
|
// The opener (OnTimer) now owns closing it.
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalCustom::FetchTradeRecords(string tableName, TradeRecord &tradeRecords[])
|
|
{
|
|
TradeRecord tradeRecordStruct;
|
|
if(!dbm.FetchTradeRecords(tableName, tradeRecordStruct, tradeRecords))
|
|
{
|
|
Print(__FUNCTION__ + " Failed to fetch trade records from " + tableName);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalCustom::ShouldDeleteOldestEntry(TradeRecord &tradeRecords[])
|
|
{
|
|
return ArraySize(tradeRecords) >= MAX_TABLE_ROWS;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalCustom::DeleteOldestEntry(string tableName)
|
|
{
|
|
dbm.DeleteOldestEntry(tableName); // failure is already logged by the DB layer
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Register a signal in the database |
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalCustom::RegisterSignal(int year, int month, int day, int DOW, int hour, int minutes, string tableName, string pattern, string direction, double entryPrice, double exitPrice, string result)
|
|
{
|
|
string Columns[] = {"year", "month", "day", "dayOfWeek", "hour", "minutes", "pattern", "direction", "entryPrice", "exitPrice", "result"};
|
|
string valArr[] = {IntegerToString(year), IntegerToString(month), IntegerToString(day), IntegerToString(DOW), IntegerToString(hour), IntegerToString(minutes), pattern, direction, DoubleToString(entryPrice, Digits()), DoubleToString(exitPrice, Digits()), result};
|
|
if(dbm.InsertTradeRecord(tableName, Columns, valArr))
|
|
{
|
|
PrintVerbose("Successfully registered signal in table: " + tableName);
|
|
}
|
|
else
|
|
{
|
|
Print("Failed to register signal in table: " + tableName);
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Update a trade record in the database |
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalCustom::UpdateTradeRecordInDatabase(string tableName, TradeRecord &tradeRecord)
|
|
{
|
|
string columns[] = { "exitPrice", "result" };
|
|
string values[] = { DoubleToString(tradeRecord.exitPrice, Digits()), tradeRecord.result };
|
|
if(dbm.UpdateTradeRecord(tableName, columns, values, tradeRecord.pattern, tradeRecord.direction))
|
|
{
|
|
PrintVerbose("Successfully updated trade record in table: " + tableName);
|
|
}
|
|
else
|
|
{
|
|
Print("Failed to update trade record in table: " + tableName + " for pattern " + tradeRecord.pattern + " and direction " + tradeRecord.direction);
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
bool CExpertSignalCustom::UpdateSignalsWeights(void)
|
|
{
|
|
if(!dbm.BeginTransaction())
|
|
return(false);
|
|
TradeRecord tradeRecordStruct;
|
|
int total = m_filters.Total();
|
|
double sumModuleWeight = 0.0;
|
|
int weightedFilterCount = 0;
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
CExpertSignalCustom *filter = m_filters.At(i);
|
|
//--- check pointer
|
|
if(filter == NULL)
|
|
continue;
|
|
string filterID = filter.GetFilterID();
|
|
if(filterID == "NULL")
|
|
continue;
|
|
int patternCount = filter.GetPatternCount();
|
|
if(patternCount <= 0 || patternCount == NULL)
|
|
continue;
|
|
int totalWinRate = 0;
|
|
int validPatternCount = 0;
|
|
for(int j = 0; j < patternCount; j++)
|
|
{
|
|
// Fetch trade records for the specified table
|
|
string pattern = PatternName(j);
|
|
string tableNameBuy = PatternTableName(filterID, pattern, "Buy");
|
|
string tableNameSell = PatternTableName(filterID, pattern, "Sell");
|
|
TradeRecord tradeRecordsBuy[], tradeRecordsSell[];
|
|
// Fetch Buy side trade records
|
|
if(!dbm.FetchTradeRecords(tableNameBuy, tradeRecordStruct, tradeRecordsBuy))
|
|
{
|
|
Print(__FUNCTION__ + " Failed to fetch trade records from " + tableNameBuy);
|
|
continue;
|
|
}
|
|
// Fetch Sell side trade records
|
|
if(!dbm.FetchTradeRecords(tableNameSell, tradeRecordStruct, tradeRecordsSell))
|
|
{
|
|
Print(__FUNCTION__ + " Failed to fetch trade records from " + tableNameSell);
|
|
continue;
|
|
}
|
|
int winRateBuy = CalculatePatternWinRate(pattern, tradeRecordsBuy);
|
|
int winRateSell = CalculatePatternWinRate(pattern, tradeRecordsSell);
|
|
// Skip sides with insufficient samples instead of averaging in the sentinel
|
|
if(winRateBuy == NO_DATA_WIN_RATE && winRateSell == NO_DATA_WIN_RATE)
|
|
continue;
|
|
int combinedWinRate = (winRateBuy == NO_DATA_WIN_RATE) ? winRateSell :
|
|
(winRateSell == NO_DATA_WIN_RATE) ? winRateBuy :
|
|
(winRateBuy + winRateSell) / 2;
|
|
if(combinedWinRate >= 0 && combinedWinRate <= 100)
|
|
{
|
|
filter.ApplyPatternWeight(j, combinedWinRate);
|
|
totalWinRate += combinedWinRate;
|
|
validPatternCount++;
|
|
PrintVerbose("Applied " + filterID + " " + pattern + " Weight " + IntegerToString(combinedWinRate));
|
|
}
|
|
}
|
|
// Calculate the average win rate for valid patterns
|
|
double averageWinRate = validPatternCount > 0 ? (totalWinRate) / validPatternCount : 0.0;
|
|
// Normalize the average win rate to the range 0 to 1
|
|
double normalizedWinRate = averageWinRate / 100.0;
|
|
// Round the normalized win rate to the nearest 0.05
|
|
normalizedWinRate = MathRound(normalizedWinRate * 10) / 10.0;
|
|
// Ensure the rounded value is within 0 to 1
|
|
normalizedWinRate = MathMax(0, MathMin(normalizedWinRate, 1));
|
|
// Apply the main weight based on the normalized and rounded win rate
|
|
double moduleWeight = normalizedWinRate;
|
|
if(moduleWeight > 0 && moduleWeight <= 1)
|
|
{
|
|
filter.Weight(moduleWeight);
|
|
PrintVerbose("Applied " + filterID + " Main Weight " + DoubleToString(moduleWeight, 2));
|
|
}
|
|
if(validPatternCount > 0)
|
|
{
|
|
sumModuleWeight += normalizedWinRate;
|
|
weightedFilterCount++;
|
|
}
|
|
}
|
|
// Track the overall DB win-rate confidence across all filters, so it can be
|
|
// combined with (or used instead of) AI confidence via Confidence_Source.
|
|
m_dbConfidence = weightedFilterCount > 0 ? sumModuleWeight / weightedFilterCount : 0.0;
|
|
if(dbm.CommitTransaction())
|
|
return true;
|
|
else
|
|
return(false);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate the time based win rate for specified pattern |
|
|
//+------------------------------------------------------------------+
|
|
int CExpertSignalCustom::CalculatePatternWinRate(string pattern, TradeRecord & tr[])
|
|
{
|
|
int totalTrades = 0;
|
|
int profitableTrades = 0;
|
|
MqlDateTime gmtTime;
|
|
TimeGMT(gmtTime);
|
|
if(IsBacktesting)
|
|
{
|
|
datetime nowGmt = StructToTime(gmtTime);
|
|
for(int i = ArraySize(tr) - 1; i >= 0; i--)
|
|
{
|
|
MqlDateTime recordTime;
|
|
recordTime.year = tr[i].year;
|
|
recordTime.mon = tr[i].month;
|
|
recordTime.day = tr[i].day;
|
|
recordTime.hour = tr[i].hour;
|
|
recordTime.min = tr[i].minutes;
|
|
recordTime.sec = 0;
|
|
if(StructToTime(recordTime) >= nowGmt)
|
|
ArrayResize(tr, ArraySize(tr) - 1);
|
|
else
|
|
break;
|
|
}
|
|
}
|
|
// Loop through trade records
|
|
for(int i = 0; i < ArraySize(tr); i++)
|
|
{
|
|
if(tr[i].pattern == pattern && tr[i].result != "NA")
|
|
{
|
|
//--- was previously also requiring hour/day/day_of_week/month to all match the CURRENT moment
|
|
//--- (gmtTime) simultaneously - a coincidence real trade history essentially never satisfies,
|
|
//--- which made this always return NO_DATA_WIN_RATE regardless of actual history. Win rate is
|
|
//--- per-pattern, not per-exact-timestamp, so the pattern/result match above is the only filter.
|
|
totalTrades++; // Increment total trades
|
|
if(tr[i].result == "Profit")
|
|
profitableTrades++;
|
|
}
|
|
}
|
|
// Check if total trades meet the minimum requirement
|
|
if(totalTrades < MIN_TRADES_FOR_WIN_RATE)
|
|
return NO_DATA_WIN_RATE;
|
|
// Calculate win rate based on the selected trading style
|
|
double winRate = 0.0;
|
|
winRate = (totalTrades > 2) ? (double)profitableTrades / totalTrades * 100.0 : 0.0;
|
|
// Normalize and return win rate
|
|
return NormalizeWinRate(winRate);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
int CExpertSignalCustom::NormalizeWinRate(double winRate)
|
|
{
|
|
return (int)MathRound(winRate / 10) * 10; // Round to the nearest 10
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalCustom::OnTickHandler(void)
|
|
{
|
|
int total = m_filters.Total();
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
CExpertSignalCustom *filter = m_filters.At(i);
|
|
//--- check pointer
|
|
if(filter == NULL)
|
|
continue;
|
|
string filterID = filter.GetFilterID();
|
|
if(filterID == "NULL")
|
|
continue;
|
|
filter.OnTickHandler();
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
void CExpertSignalCustom::OnChartEventHandler(const int id,
|
|
const long &lparam,
|
|
const double &dparam,
|
|
const string &sparam)
|
|
{
|
|
int total = m_filters.Total();
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
CExpertSignalCustom *filter = m_filters.At(i);
|
|
//--- check pointer
|
|
if(filter == NULL)
|
|
continue;
|
|
string filterID = filter.GetFilterID();
|
|
if(filterID == "NULL")
|
|
continue;
|
|
filter.OnChartEventHandler(id, lparam, dparam, sparam);
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|