#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