forked from animatedread/Warrior_EA
Optimize() scaled lot size off account trade-history streaks with no Magic-number filter (picked up other EAs'/manual trades) and an unconfigurable m_factor stuck at 1.0 (Factor() was never wired from an input), so a 3-trade streak could triple lot size or send it negative. It was also entirely disconnected from what the AI model actually knows about the current setup. Replaced both AdjustRiskAmount()'s linear confidence-only scale and Optimize()'s streak multiplier with one edge-based model: p from the empirically calibrated AI/DB confidence magnitude, b from the trade's real reward:risk ratio (newly bridged from OpenParams() via g_TradeRewardRiskRatio), quarter-Kelly applied and clamped so risk% can only ever scale down from its configured ceiling, never above it.
1035 lines
48 KiB
MQL5
1035 lines
48 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"
|
|
//--- 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 TP_ATR_MULTIPLIER 0.25
|
|
#define MIN_SL_ATR_MULTIPLIER 2.0
|
|
#define MIN_RR_ATR_MULTIPLIER 3.0
|
|
// AI-confidence scaling caps for SLTP_AI_CONFIDENCE mode - confidence is a 0..1
|
|
// scalar (see CExpertSignalAIBase::AIConfidence); SL tightens by up to 30%,
|
|
// reward target widens by up to 2x, as confidence rises from 0 to 1.
|
|
#define AI_SL_TIGHTEN_FACTOR 0.3
|
|
#define AI_RR_WIDEN_FACTOR 1.0
|
|
//
|
|
class CExpertSignalCustom : public CExpertSignal
|
|
{
|
|
private:
|
|
bool FetchTradeRecords(string tableName, TradeRecord &tradeRecords[]);
|
|
bool ShouldDeleteOldestEntry(TradeRecord &tradeRecords[]);
|
|
void DeleteOldestEntry(string tableName);
|
|
bool CheckForDuplicateTrade(string pattern, string direction, double entryPrice, TradeRecord &tradeRecords[]);
|
|
int FindLastTradeIndex(string pattern, string direction, TradeRecord &tradeRecords[], bool &tradeOpen);
|
|
void UpdateTradeStatusAndExit(string tableName, string pattern, string direction, double entryPrice, double exitPrice, TradeRecord &tradeRecord, TradeRecord &tradeRecords[]);
|
|
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);
|
|
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
|
|
double m_sl_atr_multiplier; // Stop-loss distance beyond swing high/low, as a multiple of ATR
|
|
double m_min_risk_reward_ratio; // Minimum reward:risk ratio required to open a trade
|
|
bool m_use_ai_sltp; // true: scale SL/TP by EffectiveConfidence(); false: pure rule-based
|
|
int m_confidence_source; // CONFIDENCE_SOURCE underlying int (0=AI, 1=DB, 2=Blended)
|
|
bool m_use_ai_exit; // true: close early when confidence reverses against the position
|
|
double m_ai_exit_threshold; // 0..1 min. confidence required to trigger an early AI exit
|
|
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.
|
|
int m_directionCurrentSecond;
|
|
double m_directionAggregatedResult;
|
|
int m_directionCount;
|
|
double m_directionLastResult;
|
|
|
|
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 SLAtrMultiplier(double value) { m_sl_atr_multiplier = value; }
|
|
void MinRiskRewardRatio(double value) { m_min_risk_reward_ratio = value; }
|
|
void UseAISLTP(bool value) { m_use_ai_sltp = value; }
|
|
void ConfidenceSource(int value) { m_confidence_source = value; }
|
|
void UseAIExit(bool value) { m_use_ai_exit = value; }
|
|
void AIExitThreshold(double value) { m_ai_exit_threshold = value; }
|
|
// 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; }
|
|
// 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;
|
|
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_atr_multiplier(3.0),
|
|
m_min_risk_reward_ratio(2.0),
|
|
m_use_ai_sltp(false),
|
|
m_confidence_source(0),
|
|
m_use_ai_exit(false),
|
|
m_ai_exit_threshold(0.7),
|
|
m_dbConfidence(0.0),
|
|
m_directionCurrentSecond(-1),
|
|
m_directionAggregatedResult(0.0),
|
|
m_directionCount(0),
|
|
m_directionLastResult(0.0)
|
|
{
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Combine AI/DB confidence per the configured Confidence_Source |
|
|
//+------------------------------------------------------------------+
|
|
double CExpertSignalCustom::EffectiveConfidence(void)
|
|
{
|
|
g_AISignedConfidence = SignedAIConfidence();
|
|
g_DBConfidence = m_dbConfidence;
|
|
return CombinedConfidence(m_confidence_source);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Destructor |
|
|
//+------------------------------------------------------------------+
|
|
CExpertSignalCustom::~CExpertSignalCustom(void)
|
|
{
|
|
ArrayFree(signalBuffer);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| 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;
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| 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(atr <= 0.0)
|
|
return false; // ATR must be positive
|
|
if(!m_symbol.Name(_Symbol))
|
|
return false; // Symbol information must be accessible
|
|
int lookback_period = m_periods;
|
|
double base_price = (m_base_price == 0.0) ? (isLong ? m_symbol.Ask() : m_symbol.Bid()) : m_base_price;
|
|
price = base_price; // Entry price
|
|
int lowest_index = iLowest(_Symbol, PERIOD_CURRENT, MODE_LOW, lookback_period, idx);
|
|
int highest_index = iHighest(_Symbol, PERIOD_CURRENT, MODE_HIGH, lookback_period, idx);
|
|
double lowest_low = Low(lowest_index);
|
|
double highest_high = High(highest_index);
|
|
double slMultiplier = m_sl_atr_multiplier;
|
|
double minRR = m_min_risk_reward_ratio;
|
|
// Refresh the confidence bridge every time regardless of m_use_ai_sltp, so that
|
|
// Use_AI_Lot_Sizing (Money\MoneyIntelligent.mqh) sees a fresh value even when the
|
|
// SL/TP sizing itself is left on rule-based.
|
|
double confidence = EffectiveConfidence();
|
|
if(m_use_ai_sltp)
|
|
{
|
|
slMultiplier *= (1.0 - AI_SL_TIGHTEN_FACTOR * confidence);
|
|
minRR *= (1.0 + AI_RR_WIDEN_FACTOR * confidence);
|
|
}
|
|
if(isLong)
|
|
{
|
|
sl = m_symbol.NormalizePrice(lowest_low - slMultiplier * atr);
|
|
tp = m_symbol.NormalizePrice(highest_high - TP_ATR_MULTIPLIER * atr);
|
|
}
|
|
else
|
|
{
|
|
sl = m_symbol.NormalizePrice(highest_high + slMultiplier * atr);
|
|
tp = m_symbol.NormalizePrice(lowest_low + TP_ATR_MULTIPLIER * atr);
|
|
}
|
|
// Ensure SL and TP are at least MIN_SL/RR_ATR_MULTIPLIER x ATR from entry price
|
|
if(fabs(price - sl) < (MIN_SL_ATR_MULTIPLIER * atr))
|
|
{
|
|
if(isLong)
|
|
{
|
|
sl = price - (MIN_SL_ATR_MULTIPLIER * atr);
|
|
}
|
|
else
|
|
{
|
|
sl = price + (MIN_SL_ATR_MULTIPLIER * atr);
|
|
}
|
|
}
|
|
if(fabs(tp - price) < (MIN_RR_ATR_MULTIPLIER * atr))
|
|
{
|
|
if(isLong)
|
|
{
|
|
tp = price + (MIN_RR_ATR_MULTIPLIER * atr);
|
|
}
|
|
else
|
|
{
|
|
tp = price - (MIN_RR_ATR_MULTIPLIER * atr);
|
|
}
|
|
}
|
|
// Check if the risk-to-reward ratio is at least 1:minRR (Min_Risk_Reward_Ratio input,
|
|
// widened further by AI confidence when SLTP_Source == SLTP_AI_CONFIDENCE)
|
|
double risk = fabs(price - sl);
|
|
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. SignedAIConfidence() is 0.0 for non-AI signal filters, so this is a
|
|
// no-op unless both Use_AI_Exit is set and the active filter is AI-driven.
|
|
if(!result && m_use_ai_exit)
|
|
{
|
|
double signed_conf = SignedAIConfidence();
|
|
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)
|
|
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)))
|
|
result = false;
|
|
}
|
|
//--- 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
|
|
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
|
|
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;
|
|
TimeGMT(gmtTime);
|
|
// Check if the current second has changed
|
|
if(gmtTime.sec != m_directionCurrentSecond)
|
|
{
|
|
// If the second has changed, update the last result
|
|
if(m_directionCount != 0)
|
|
{
|
|
m_directionLastResult = m_directionAggregatedResult / m_directionCount;
|
|
}
|
|
else
|
|
{
|
|
m_directionLastResult = 0.0; // Default to 0 if no results were aggregated
|
|
}
|
|
// Reset variables for the new second
|
|
m_directionAggregatedResult = 0.0;
|
|
m_directionCount = 0;
|
|
m_directionCurrentSecond = gmtTime.sec; // Update the current second
|
|
}
|
|
m_prohibition_signal = false;
|
|
double result = m_weight * (LongCondition() - ShortCondition());
|
|
int number = (result == 0.0) ? 0 : 1;
|
|
int total = m_filters.Total();
|
|
PrintVerbose("Starting direction calculation with total filters: " + IntegerToString(total));
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
long mask = ((long)1) << i;
|
|
if((m_ignore & mask) != 0)
|
|
continue;
|
|
CExpertSignalCustom *filter = m_filters.At(i);
|
|
if(filter == NULL)
|
|
{
|
|
Print("Error: Filter at index " + IntegerToString(i) + " is NULL");
|
|
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();
|
|
if(filterID != "NULL" && bias != "NULL" && pattern != "NULL" && m_useDatabase)
|
|
{
|
|
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;
|
|
continue;
|
|
}
|
|
if(direction != EMPTY_VALUE)
|
|
{
|
|
// 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.");
|
|
direction = EMPTY_VALUE;
|
|
result = 0;
|
|
number = 0;
|
|
break;
|
|
}
|
|
if(direction != 0)
|
|
{
|
|
number++; // Only increment `number` if `direction` is not 0 or EMPTY_VALUE
|
|
result += ((m_invert & mask) != 0) ? -direction : direction;
|
|
}
|
|
}
|
|
}
|
|
// Aggregate the result and count
|
|
m_directionAggregatedResult += result;
|
|
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("Failed to begin database transaction.");
|
|
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("Failed to commit the transaction to the database.");
|
|
return;
|
|
}
|
|
ArrayResize(signalBuffer, 0);
|
|
PrintVerbose("Signal buffer cleared after processing.");
|
|
if(!IsBacktesting)
|
|
{
|
|
dbm.CloseDatabase();
|
|
PrintVerbose("Database closed successfully.");
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
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")
|
|
{
|
|
if(tr[i].hour == gmtTime.hour && tr[i].day == gmtTime.day && tr[i].day_of_week == gmtTime.day_of_week && tr[i].month == gmtTime.mon)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|