478 righe
18 KiB
MQL5
478 righe
18 KiB
MQL5
#ifndef TRADE_EXECUTION_MQH
|
|
#define TRADE_EXECUTION_MQH
|
|
|
|
#include "AISignalConfirmation.mqh"
|
|
#include "NNDataLogger.mqh"
|
|
|
|
void UpdateLastTradeInfo(const string direction);
|
|
|
|
bool IsInCooldown()
|
|
{
|
|
datetime now = TimeCurrent();
|
|
return (now - Last_Trade_Time < Trade_Cooldown_Seconds);
|
|
}
|
|
|
|
int CountOpenTradesForSymbol()
|
|
{
|
|
int count = 0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; --i)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0 || !PositionSelectByTicket(ticket))
|
|
continue;
|
|
|
|
if(PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber)
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
int GetSLPoints(const bool continuation);
|
|
int GetTPPoints(const bool continuation);
|
|
|
|
int GetEffectiveMaxExposure()
|
|
{
|
|
int exposure = Max_Open_Trades;
|
|
if(g_symbol_policy_ready && g_symbol_policy.max_concurrent_exposure > 0)
|
|
exposure = MathMin(exposure, g_symbol_policy.max_concurrent_exposure);
|
|
return MathMax(1, exposure);
|
|
}
|
|
|
|
double GetEffectiveMinStopPoints(const bool continuation)
|
|
{
|
|
int base_points = GetSLPoints(continuation);
|
|
double broker_min = GetBrokerMinStopPoints();
|
|
if(g_symbol_policy_ready)
|
|
base_points = MathMax(base_points, g_symbol_policy.min_stop_points);
|
|
base_points = (int)MathMax((double)base_points, broker_min);
|
|
return (double)base_points;
|
|
}
|
|
|
|
// ── Base gate: cooldown + position cap ──────────────────────────────────────
|
|
// News direction checks live in OpenBuy / OpenSell so each side can be gated
|
|
// independently (e.g., news is bullish → OpenBuy passes, OpenSell blocked).
|
|
bool CanOpenNewTrade()
|
|
{
|
|
if(IsInCooldown())
|
|
return false;
|
|
|
|
if(CountOpenTradesForSymbol() >= GetEffectiveMaxExposure())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
int GetSLPoints(const bool continuation)
|
|
{
|
|
if(Symbol_Profile_Initialized)
|
|
{
|
|
if(continuation) return Symbol_SL_Points;
|
|
return MathMax(20, Symbol_SL_Points / 2);
|
|
}
|
|
return continuation ? Continuation_SL_Points : Counter_SL_Points;
|
|
}
|
|
|
|
int GetTPPoints(const bool continuation)
|
|
{
|
|
if(Symbol_Profile_Initialized)
|
|
{
|
|
if(continuation) return Symbol_TP_Points;
|
|
return MathMax(80, (int)((double)Symbol_TP_Points * 0.85));
|
|
}
|
|
return continuation ? Continuation_TP_Points : Counter_TP_Points;
|
|
}
|
|
|
|
double BuildSignalRiskMultiplier(const bool is_buy, const bool continuation)
|
|
{
|
|
double mult = 1.0;
|
|
|
|
if(Active_Praise_Signals >= 4)
|
|
mult += 0.15;
|
|
else if(Active_Praise_Signals == 3)
|
|
mult += 0.10;
|
|
|
|
if(Active_Warnings >= 3)
|
|
mult -= 0.15;
|
|
|
|
if(Use_Signal_Coordinator)
|
|
mult *= ClampDouble(Coordinator_Lot_Multiplier, 0.80, 1.25);
|
|
|
|
mult *= BuildSymbolPolicyRiskMultiplier(continuation, GW_Session_Name, GW_Session_Phase);
|
|
|
|
if(News_Bias_Direction != 0)
|
|
{
|
|
bool aligned = (is_buy && News_Bias_Direction > 0) || (!is_buy && News_Bias_Direction < 0);
|
|
double news_factor = 1.0;
|
|
|
|
if(aligned)
|
|
news_factor += MathMin(0.20, (News_Bias_Strength / 100.0) * g_symbol_policy.news_sensitivity * 0.20);
|
|
else
|
|
news_factor -= MathMin(0.35, (News_Bias_Strength / 100.0) * g_symbol_policy.news_sensitivity * 0.35);
|
|
|
|
if(News_Trade_Block_Active)
|
|
news_factor *= 0.85;
|
|
|
|
if(News_Trade_Allowed_Direction == 1 && !is_buy)
|
|
news_factor *= 0.60;
|
|
if(News_Trade_Allowed_Direction == -1 && is_buy)
|
|
news_factor *= 0.60;
|
|
|
|
mult *= news_factor;
|
|
}
|
|
else if(News_Bias_Strength >= News_Soft_Reduce_Strength)
|
|
{
|
|
double neutral_factor = 1.0 - MathMin(0.25, (News_Bias_Strength / 100.0) * g_symbol_policy.news_sensitivity * 0.25);
|
|
mult *= MathMax(News_Soft_Bias_Floor, neutral_factor);
|
|
}
|
|
|
|
if(NN_IsUsable())
|
|
{
|
|
if((NN_Bias > 0 && is_buy) || (NN_Bias < 0 && !is_buy))
|
|
mult += 0.10;
|
|
else if(NN_Bias != 0)
|
|
mult -= 0.10;
|
|
}
|
|
|
|
if(Active_Praise_Signals >= 4 && continuation)
|
|
mult += 0.08;
|
|
if(Active_Warnings >= 4)
|
|
mult -= 0.10;
|
|
|
|
return ClampDouble(mult, Signal_Risk_Down_Floor, Signal_Risk_Up_Cap);
|
|
}
|
|
|
|
double ComputeRiskBasedLots(const bool is_buy,
|
|
const bool continuation,
|
|
const double entry_price,
|
|
const double stop_price,
|
|
double &risk_amount,
|
|
double &margin_required)
|
|
{
|
|
risk_amount = 0.0;
|
|
margin_required = 0.0;
|
|
|
|
if(!g_symbol_policy_ready)
|
|
ResolveSymbolPolicy();
|
|
|
|
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
|
|
|
|
g_symbol_balance = balance;
|
|
g_symbol_equity = equity;
|
|
g_symbol_free_margin = free_margin;
|
|
|
|
double base_risk_pct = g_symbol_policy_ready ? g_symbol_policy.base_risk_percent : 0.50;
|
|
double max_risk_pct = g_symbol_policy_ready ? g_symbol_policy.max_account_risk_pct : Max_Account_Risk_Pct;
|
|
double risk_budget = equity * (MathMin(base_risk_pct, max_risk_pct) / 100.0);
|
|
|
|
double signal_mult = BuildSignalRiskMultiplier(is_buy, continuation);
|
|
risk_budget *= signal_mult;
|
|
risk_budget = MathMin(risk_budget, equity * (Max_Account_Risk_Pct / 100.0));
|
|
risk_budget = MathMin(risk_budget, equity * (g_symbol_policy.max_account_risk_pct / 100.0));
|
|
risk_amount = risk_budget;
|
|
|
|
double risk_per_lot = EstimateRiskPerLot(is_buy, entry_price, stop_price);
|
|
if(risk_per_lot <= 0.0)
|
|
return 0.0;
|
|
|
|
double lots = risk_budget / risk_per_lot;
|
|
if(g_symbol_policy_ready)
|
|
lots = ClampDouble(lots, g_symbol_policy.lot_floor, g_symbol_policy.lot_ceiling);
|
|
|
|
lots = ClampLotsToBroker(lots);
|
|
|
|
if(lots <= 0.0)
|
|
return 0.0;
|
|
|
|
double max_margin_pct = g_symbol_policy_ready ? g_symbol_policy.max_margin_pct : Max_Trade_Margin_Pct;
|
|
double min_free_margin_pct = g_symbol_policy_ready ? g_symbol_policy.min_free_margin_pct : Min_Free_Margin_Pct;
|
|
double min_free_margin = equity * (min_free_margin_pct / 100.0);
|
|
double max_margin_allowed = equity * (max_margin_pct / 100.0);
|
|
double lot_step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
|
|
double min_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
|
|
if(lot_step <= 0.0) lot_step = 0.01;
|
|
|
|
while(lots >= min_lot)
|
|
{
|
|
if(!EstimateMarginForLots(is_buy, lots, entry_price, margin_required))
|
|
return 0.0;
|
|
|
|
if(margin_required <= max_margin_allowed &&
|
|
(free_margin - margin_required) >= min_free_margin)
|
|
break;
|
|
|
|
lots = lots - lot_step;
|
|
lots = ClampLotsToBroker(lots);
|
|
if(lots < min_lot)
|
|
return 0.0;
|
|
}
|
|
|
|
risk_amount = risk_per_lot * lots;
|
|
g_symbol_margin_required = margin_required;
|
|
g_symbol_risk_amount = risk_amount;
|
|
g_symbol_intended_lots = lots;
|
|
g_symbol_final_lots = lots;
|
|
|
|
if(lots < min_lot)
|
|
return 0.0;
|
|
|
|
return lots;
|
|
}
|
|
|
|
void LogRejectedTrade(const bool is_buy,
|
|
const string setup,
|
|
const string reason,
|
|
const double entry_price,
|
|
const bool continuation,
|
|
const double lots,
|
|
const double spread_points,
|
|
const double free_margin,
|
|
const double equity,
|
|
const double balance,
|
|
const double margin_required,
|
|
const double risk_amount)
|
|
{
|
|
if(!NN_LogData)
|
|
return;
|
|
|
|
NN_LogRejectedDecision(is_buy, setup, reason, lots, entry_price,
|
|
GW_Session_Name, GW_Session_Phase, g_symbol_policy.symbol_class,
|
|
Coordinator_Cluster_Strength, Coordinator_Conflict_Score,
|
|
continuation ? 0 : 1,
|
|
spread_points, free_margin, equity, balance, margin_required, risk_amount);
|
|
}
|
|
|
|
bool OpenBuy(const string reason, double lot_multiplier = 1.0)
|
|
{
|
|
if(!CanOpenNewTrade())
|
|
return false;
|
|
|
|
if(!g_symbol_policy_ready)
|
|
ResolveSymbolPolicy();
|
|
|
|
if(g_symbol_policy.require_session_alignment &&
|
|
!SessionInPreferenceList(GW_Session_Name, g_symbol_policy.preferred_sessions))
|
|
{
|
|
g_symbol_reject_reason = "session not preferred";
|
|
LogRejectedTrade(true, reason, g_symbol_reject_reason, SymbolInfoDouble(_Symbol, SYMBOL_ASK),
|
|
(Current_Mode == MODE_TRENDING && Current_State == STATE_CONTINUATION),
|
|
0.0, GetCurrentSpreadPoints(), AccountInfoDouble(ACCOUNT_MARGIN_FREE),
|
|
AccountInfoDouble(ACCOUNT_EQUITY), AccountInfoDouble(ACCOUNT_BALANCE), 0.0, 0.0);
|
|
return false;
|
|
}
|
|
|
|
double ai_mult = 1.0;
|
|
bool continuation = (Current_Mode == MODE_TRENDING && Current_State == STATE_CONTINUATION);
|
|
|
|
double entry = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
|
double sl_pts = GetEffectiveMinStopPoints(continuation);
|
|
int tp_pts = GetTPPoints(continuation);
|
|
double sl = NormalizePrice(entry - (sl_pts * _Point));
|
|
double tp = NormalizePrice(entry + (tp_pts * _Point));
|
|
|
|
double risk_amount = 0.0;
|
|
double margin_required = 0.0;
|
|
double lots = ComputeRiskBasedLots(true, continuation, entry, sl, risk_amount, margin_required);
|
|
if(lots <= 0.0)
|
|
{
|
|
g_symbol_reject_reason = "risk sizing or margin gate";
|
|
LogRejectedTrade(true, reason, g_symbol_reject_reason, entry, continuation,
|
|
0.0, GetCurrentSpreadPoints(), g_symbol_free_margin,
|
|
g_symbol_equity, g_symbol_balance, margin_required, risk_amount);
|
|
return false;
|
|
}
|
|
|
|
if(!AIApproveTrade(true, reason, lots, GetCurrentSpreadPoints(), risk_amount, margin_required, g_symbol_free_margin, GW_Session_Phase, ai_mult))
|
|
{
|
|
g_symbol_reject_reason = "ai rejection";
|
|
LogRejectedTrade(true, reason, g_symbol_reject_reason, entry, continuation,
|
|
lots, GetCurrentSpreadPoints(), g_symbol_free_margin,
|
|
g_symbol_equity, g_symbol_balance, margin_required, risk_amount);
|
|
return false;
|
|
}
|
|
|
|
double setup_mult = ClampDouble(lot_multiplier, 0.75, 1.25);
|
|
lots = ClampLotsToBroker(lots * setup_mult * ai_mult);
|
|
if(g_symbol_policy_ready)
|
|
lots = ClampDouble(lots, g_symbol_policy.lot_floor, g_symbol_policy.lot_ceiling);
|
|
|
|
double post_margin = 0.0;
|
|
if(!EstimateMarginForLots(true, lots, entry, post_margin))
|
|
{
|
|
g_symbol_reject_reason = "margin estimate failed";
|
|
LogRejectedTrade(true, reason, g_symbol_reject_reason, entry, continuation,
|
|
lots, GetCurrentSpreadPoints(), g_symbol_free_margin,
|
|
g_symbol_equity, g_symbol_balance, margin_required, risk_amount);
|
|
return false;
|
|
}
|
|
|
|
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
|
|
if(post_margin > equity * (g_symbol_policy.max_margin_pct / 100.0) ||
|
|
(free_margin - post_margin) < (equity * (g_symbol_policy.min_free_margin_pct / 100.0)))
|
|
{
|
|
g_symbol_reject_reason = "post-ai margin gate";
|
|
LogRejectedTrade(true, reason, g_symbol_reject_reason, entry, continuation,
|
|
lots, GetCurrentSpreadPoints(), free_margin,
|
|
equity, AccountInfoDouble(ACCOUNT_BALANCE), post_margin, risk_amount);
|
|
return false;
|
|
}
|
|
|
|
margin_required = post_margin;
|
|
risk_amount = EstimateRiskPerLot(true, entry, sl) * lots;
|
|
g_symbol_margin_required = margin_required;
|
|
g_symbol_risk_amount = risk_amount;
|
|
g_symbol_final_lots = lots;
|
|
|
|
bool ok = Trade.Buy(lots, _Symbol, 0.0, sl, tp, reason);
|
|
if(ok)
|
|
{
|
|
Last_Trade_Time = TimeCurrent();
|
|
UpdateLastTradeInfo("bullish");
|
|
TodayTrades++;
|
|
BuyTrades++;
|
|
|
|
// Capture feature snapshot for NN training (labeled when trade closes via NN_LabelFromDeal)
|
|
if(NN_LogData)
|
|
{
|
|
ulong d = Trade.ResultDeal();
|
|
if(d > 0 && HistoryDealSelect(d))
|
|
{
|
|
ulong pos_id = (ulong)HistoryDealGetInteger(d, DEAL_POSITION_ID);
|
|
int cat = (Current_State == STATE_CONTINUATION) ? 0 : 1;
|
|
NN_CaptureEntry(pos_id, true, reason, entry,
|
|
GW_Session_Name, GW_Session_Phase, g_symbol_policy.symbol_class,
|
|
Coordinator_Cluster_Strength, Coordinator_Conflict_Score, cat,
|
|
GetCurrentSpreadPoints(), g_symbol_free_margin, g_symbol_equity,
|
|
g_symbol_balance, margin_required, risk_amount);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
g_symbol_reject_reason = "trade buy failed";
|
|
LogRejectedTrade(true, reason, g_symbol_reject_reason, entry, continuation,
|
|
lots, GetCurrentSpreadPoints(), g_symbol_free_margin,
|
|
g_symbol_equity, g_symbol_balance, margin_required, risk_amount);
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
bool OpenSell(const string reason, double lot_multiplier = 1.0)
|
|
{
|
|
if(!CanOpenNewTrade())
|
|
return false;
|
|
|
|
if(!g_symbol_policy_ready)
|
|
ResolveSymbolPolicy();
|
|
|
|
if(g_symbol_policy.require_session_alignment &&
|
|
!SessionInPreferenceList(GW_Session_Name, g_symbol_policy.preferred_sessions))
|
|
{
|
|
g_symbol_reject_reason = "session not preferred";
|
|
LogRejectedTrade(false, reason, g_symbol_reject_reason, SymbolInfoDouble(_Symbol, SYMBOL_BID),
|
|
(Current_Mode == MODE_TRENDING && Current_State == STATE_CONTINUATION),
|
|
0.0, GetCurrentSpreadPoints(), AccountInfoDouble(ACCOUNT_MARGIN_FREE),
|
|
AccountInfoDouble(ACCOUNT_EQUITY), AccountInfoDouble(ACCOUNT_BALANCE), 0.0, 0.0);
|
|
return false;
|
|
}
|
|
|
|
double ai_mult = 1.0;
|
|
bool continuation = (Current_Mode == MODE_TRENDING && Current_State == STATE_CONTINUATION);
|
|
|
|
double entry = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
double sl_pts = GetEffectiveMinStopPoints(continuation);
|
|
int tp_pts = GetTPPoints(continuation);
|
|
double sl = NormalizePrice(entry + (sl_pts * _Point));
|
|
double tp = NormalizePrice(entry - (tp_pts * _Point));
|
|
|
|
double risk_amount = 0.0;
|
|
double margin_required = 0.0;
|
|
double lots = ComputeRiskBasedLots(false, continuation, entry, sl, risk_amount, margin_required);
|
|
if(lots <= 0.0)
|
|
{
|
|
g_symbol_reject_reason = "risk sizing or margin gate";
|
|
LogRejectedTrade(false, reason, g_symbol_reject_reason, entry, continuation,
|
|
0.0, GetCurrentSpreadPoints(), g_symbol_free_margin,
|
|
g_symbol_equity, g_symbol_balance, margin_required, risk_amount);
|
|
return false;
|
|
}
|
|
|
|
if(!AIApproveTrade(false, reason, lots, GetCurrentSpreadPoints(), risk_amount, margin_required, g_symbol_free_margin, GW_Session_Phase, ai_mult))
|
|
{
|
|
g_symbol_reject_reason = "ai rejection";
|
|
LogRejectedTrade(false, reason, g_symbol_reject_reason, entry, continuation,
|
|
lots, GetCurrentSpreadPoints(), g_symbol_free_margin,
|
|
g_symbol_equity, g_symbol_balance, margin_required, risk_amount);
|
|
return false;
|
|
}
|
|
|
|
double setup_mult = ClampDouble(lot_multiplier, 0.75, 1.25);
|
|
lots = ClampLotsToBroker(lots * setup_mult * ai_mult);
|
|
if(g_symbol_policy_ready)
|
|
lots = ClampDouble(lots, g_symbol_policy.lot_floor, g_symbol_policy.lot_ceiling);
|
|
|
|
double post_margin = 0.0;
|
|
if(!EstimateMarginForLots(false, lots, entry, post_margin))
|
|
{
|
|
g_symbol_reject_reason = "margin estimate failed";
|
|
LogRejectedTrade(false, reason, g_symbol_reject_reason, entry, continuation,
|
|
lots, GetCurrentSpreadPoints(), g_symbol_free_margin,
|
|
g_symbol_equity, g_symbol_balance, margin_required, risk_amount);
|
|
return false;
|
|
}
|
|
|
|
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
|
|
if(post_margin > equity * (g_symbol_policy.max_margin_pct / 100.0) ||
|
|
(free_margin - post_margin) < (equity * (g_symbol_policy.min_free_margin_pct / 100.0)))
|
|
{
|
|
g_symbol_reject_reason = "post-ai margin gate";
|
|
LogRejectedTrade(false, reason, g_symbol_reject_reason, entry, continuation,
|
|
lots, GetCurrentSpreadPoints(), free_margin,
|
|
equity, AccountInfoDouble(ACCOUNT_BALANCE), post_margin, risk_amount);
|
|
return false;
|
|
}
|
|
|
|
margin_required = post_margin;
|
|
risk_amount = EstimateRiskPerLot(false, entry, sl) * lots;
|
|
g_symbol_margin_required = margin_required;
|
|
g_symbol_risk_amount = risk_amount;
|
|
g_symbol_final_lots = lots;
|
|
|
|
bool ok = Trade.Sell(lots, _Symbol, 0.0, sl, tp, reason);
|
|
if(ok)
|
|
{
|
|
Last_Trade_Time = TimeCurrent();
|
|
UpdateLastTradeInfo("bearish");
|
|
TodayTrades++;
|
|
SellTrades++;
|
|
|
|
// Capture feature snapshot for NN training (labeled when trade closes via NN_LabelFromDeal)
|
|
if(NN_LogData)
|
|
{
|
|
ulong d = Trade.ResultDeal();
|
|
if(d > 0 && HistoryDealSelect(d))
|
|
{
|
|
ulong pos_id = (ulong)HistoryDealGetInteger(d, DEAL_POSITION_ID);
|
|
int cat = (Current_State == STATE_CONTINUATION) ? 0 : 1;
|
|
NN_CaptureEntry(pos_id, false, reason, entry,
|
|
GW_Session_Name, GW_Session_Phase, g_symbol_policy.symbol_class,
|
|
Coordinator_Cluster_Strength, Coordinator_Conflict_Score, cat,
|
|
GetCurrentSpreadPoints(), g_symbol_free_margin, g_symbol_equity,
|
|
g_symbol_balance, margin_required, risk_amount);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
g_symbol_reject_reason = "trade sell failed";
|
|
LogRejectedTrade(false, reason, g_symbol_reject_reason, entry, continuation,
|
|
lots, GetCurrentSpreadPoints(), g_symbol_free_margin,
|
|
g_symbol_equity, g_symbol_balance, margin_required, risk_amount);
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
#endif
|