Page 2 of 3

Re: Sticky-note daily loss limit that faces the screen

Posted: Thu Sep 24, 2026 10:28 am
by FTtrader
To translate the "digital sticky note" philosophy to MetaTrader, we must use an Expert Advisor (EA) rather than an indicator. Indicators cannot close trades.

By running these as EAs on a dedicated chart, they act as active background enforcers. If you hit your daily limit, the EA immediately flattens the account. More importantly, if you try to "negotiate" by opening a new ticket manually, the EA will instantly detect the breach and close the new trade within milliseconds. It acts as an aggressive circuit breaker.

Here are the basic versions for both MQL4 and MQL5. Both use a 1-second timer to ensure enforcement happens even in low-liquidity/low-tick conditions.

Re: Sticky-note daily loss limit that faces the screen

Posted: Thu Sep 24, 2026 10:29 am
by FTtrader
MQL4: Digital Sticky Note EA

Code: Select all

//+------------------------------------------------------------------+
//|                                            DigitalStickyNote.mq4 |
//+------------------------------------------------------------------+
#property copyright "Risk Enforcer"
#property strict

input double MaxDailyLoss = 500.0; // Daily Loss Limit (Local Currency)

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
    EventSetTimer(1);
    DrawUI();
    return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason) {
    EventKillTimer();
    ObjectsDeleteAll(0, "Sticky_");
}

//+------------------------------------------------------------------+
//| Timer function (Runs every second, independent of ticks)         |
//+------------------------------------------------------------------+
void OnTimer() {
    CheckRiskLimit();
}
void OnTick() {
    CheckRiskLimit();
}

//+------------------------------------------------------------------+
//| Core Logic                                                       |
//+------------------------------------------------------------------+
void CheckRiskLimit() {
    double dailyPnL = CalculateDailyPnL();
    bool limitHit = (dailyPnL <= -MaxDailyLoss);
    
    UpdateUI(dailyPnL, limitHit);
    
    if (limitHit) {
        FlattenAccount();
    }
}

double CalculateDailyPnL() {
    double pnl = 0.0;
    datetime startOfDay = iTime(Symbol(), PERIOD_D1, 0);
    
    // 1. Sum Closed Trades Today
    for(int i = OrdersHistoryTotal() - 1; i >= 0; i--) {
        if(OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) {
            if(OrderCloseTime() >= startOfDay) {
                pnl += OrderProfit() + OrderCommission() + OrderSwap();
            }
        }
    }
    
    // 2. Sum Open Trades (Floating PnL)
    for(int i = OrdersTotal() - 1; i >= 0; i--) {
        if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) {
            pnl += OrderProfit() + OrderCommission() + OrderSwap();
        }
    }
    
    return pnl;
}

void FlattenAccount() {
    for(int i = OrdersTotal() - 1; i >= 0; i--) {
        if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) {
            if(OrderType() == OP_BUY) {
                OrderClose(OrderTicket(), OrderLots(), MarketInfo(OrderSymbol(), MODE_BID), 3);
            } else if(OrderType() == OP_SELL) {
                OrderClose(OrderTicket(), OrderLots(), MarketInfo(OrderSymbol(), MODE_ASK), 3);
            } else {
                OrderDelete(OrderTicket()); // Delete pending orders
            }
        }
    }
}

//+------------------------------------------------------------------+
//| UI Rendering                                                     |
//+------------------------------------------------------------------+
void DrawUI() {
    ObjectCreate(0, "Sticky_Bg", OBJ_RECTANGLE_LABEL, 0, 0, 0);
    ObjectSetInteger(0, "Sticky_Bg", OBJPROP_CORNER, CORNER_RIGHT_LOWER);
    ObjectSetInteger(0, "Sticky_Bg", OBJPROP_XDISTANCE, 20);
    ObjectSetInteger(0, "Sticky_Bg", OBJPROP_YDISTANCE, 20);
    ObjectSetInteger(0, "Sticky_Bg", OBJPROP_XSIZE, 200);
    ObjectSetInteger(0, "Sticky_Bg", OBJPROP_YSIZE, 70);
    ObjectSetInteger(0, "Sticky_Bg", OBJPROP_BGCOLOR, clrYellow);
    
    ObjectCreate(0, "Sticky_Limit", OBJ_LABEL, 0, 0, 0);
    ObjectSetInteger(0, "Sticky_Limit", OBJPROP_CORNER, CORNER_RIGHT_LOWER);
    ObjectSetInteger(0, "Sticky_Limit", OBJPROP_XDISTANCE, 190);
    ObjectSetInteger(0, "Sticky_Limit", OBJPROP_YDISTANCE, 60);
    ObjectSetString(0, "Sticky_Limit", OBJPROP_FONT, "Arial");
    ObjectSetInteger(0, "Sticky_Limit", OBJPROP_FONTSIZE, 10);
    
    ObjectCreate(0, "Sticky_PnL", OBJ_LABEL, 0, 0, 0);
    ObjectSetInteger(0, "Sticky_PnL", OBJPROP_CORNER, CORNER_RIGHT_LOWER);
    ObjectSetInteger(0, "Sticky_PnL", OBJPROP_XDISTANCE, 190);
    ObjectSetInteger(0, "Sticky_PnL", OBJPROP_YDISTANCE, 35);
    ObjectSetString(0, "Sticky_PnL", OBJPROP_FONT, "Arial Bold");
    ObjectSetInteger(0, "Sticky_PnL", OBJPROP_FONTSIZE, 12);
}

void UpdateUI(double pnl, bool limitHit) {
    color bgColor = limitHit ? clrRed : clrYellow;
    color txtColor = limitHit ? clrWhite : clrBlack;
    
    ObjectSetInteger(0, "Sticky_Bg", OBJPROP_BGCOLOR, bgColor);
    ObjectSetInteger(0, "Sticky_Limit", OBJPROP_COLOR, txtColor);
    ObjectSetInteger(0, "Sticky_PnL", OBJPROP_COLOR, txtColor);
    
    ObjectSetString(0, "Sticky_Limit", OBJPROP_TEXT, "MAX LOSS: -$" + DoubleToStr(MaxDailyLoss, 2));
    ObjectSetString(0, "Sticky_PnL", OBJPROP_TEXT, "TODAY: $" + DoubleToStr(pnl, 2));
    
    ChartRedraw();
}

Re: Sticky-note daily loss limit that faces the screen

Posted: Thu Sep 24, 2026 10:29 am
by FTtrader
MQL5: Digital Sticky Note EA

MQL5 architecture separates orders, deals, and positions. This version uses the standard #include <Trade\Trade.mqh> library for robust execution.

Code: Select all

//+------------------------------------------------------------------+
//|                                            DigitalStickyNote.mq5 |
//+------------------------------------------------------------------+
#property copyright "Risk Enforcer"
#property version   "1.00"

#include <Trade\Trade.mqh>
CTrade trade;

input double MaxDailyLoss = 500.0; // Daily Loss Limit (Local Currency)

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
    EventSetTimer(1);
    DrawUI();
    return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason) {
    EventKillTimer();
    ObjectsDeleteAll(0, "Sticky_");
}

void OnTimer() {
    CheckRiskLimit();
}
void OnTick() {
    CheckRiskLimit();
}

//+------------------------------------------------------------------+
//| Core Logic                                                       |
//+------------------------------------------------------------------+
void CheckRiskLimit() {
    double dailyPnL = CalculateDailyPnL();
    bool limitHit = (dailyPnL <= -MaxDailyLoss);
    
    UpdateUI(dailyPnL, limitHit);
    
    if (limitHit) {
        FlattenAccount();
    }
}

double CalculateDailyPnL() {
    double pnl = 0.0;
    
    // Get start of today
    MqlDateTime dt;
    TimeCurrent(dt);
    dt.hour = 0; dt.min = 0; dt.sec = 0;
    datetime startOfDay = StructToTime(dt);
    
    // 1. Sum Closed Deals Today (Realized)
    if(HistorySelect(startOfDay, TimeCurrent())) {
        int dealsTotal = HistoryDealsTotal();
        for(int i = 0; i < dealsTotal; i++) {
            ulong dealTicket = HistoryDealGetTicket(i);
            if(dealTicket > 0) {
                pnl += HistoryDealGetDouble(dealTicket, DEAL_PROFIT) 
                     + HistoryDealGetDouble(dealTicket, DEAL_COMMISSION) 
                     + HistoryDealGetDouble(dealTicket, DEAL_SWAP)
                     + HistoryDealGetDouble(dealTicket, DEAL_FEE);
            }
        }
    }
    
    // 2. Sum Open Positions (Floating)
    int posTotal = PositionsTotal();
    for(int i = 0; i < posTotal; i++) {
        ulong posTicket = PositionGetTicket(i);
        if(posTicket > 0) {
            pnl += PositionGetDouble(POSITION_PROFIT) 
                 + PositionGetDouble(POSITION_SWAP);
        }
    }
    
    return pnl;
}

void FlattenAccount() {
    // Close Open Positions
    for(int i = PositionsTotal() - 1; i >= 0; i--) {
        ulong posTicket = PositionGetTicket(i);
        if(posTicket > 0) {
            trade.PositionClose(posTicket);
        }
    }
    
    // Delete Pending Orders
    for(int i = OrdersTotal() - 1; i >= 0; i--) {
        ulong orderTicket = OrderGetTicket(i);
        if(orderTicket > 0) {
            trade.OrderDelete(orderTicket);
        }
    }
}

//+------------------------------------------------------------------+
//| UI Rendering                                                     |
//+------------------------------------------------------------------+
void DrawUI() {
    ObjectCreate(0, "Sticky_Bg", OBJ_RECTANGLE_LABEL, 0, 0, 0);
    ObjectSetInteger(0, "Sticky_Bg", OBJPROP_CORNER, CORNER_RIGHT_LOWER);
    ObjectSetInteger(0, "Sticky_Bg", OBJPROP_XDISTANCE, 20);
    ObjectSetInteger(0, "Sticky_Bg", OBJPROP_YDISTANCE, 20);
    ObjectSetInteger(0, "Sticky_Bg", OBJPROP_XSIZE, 200);
    ObjectSetInteger(0, "Sticky_Bg", OBJPROP_YSIZE, 70);
    ObjectSetInteger(0, "Sticky_Bg", OBJPROP_BGCOLOR, clrYellow);
    
    ObjectCreate(0, "Sticky_Limit", OBJ_LABEL, 0, 0, 0);
    ObjectSetInteger(0, "Sticky_Limit", OBJPROP_CORNER, CORNER_RIGHT_LOWER);
    ObjectSetInteger(0, "Sticky_Limit", OBJPROP_XDISTANCE, 190);
    ObjectSetInteger(0, "Sticky_Limit", OBJPROP_YDISTANCE, 60);
    ObjectSetString(0, "Sticky_Limit", OBJPROP_FONT, "Arial");
    ObjectSetInteger(0, "Sticky_Limit", OBJPROP_FONTSIZE, 10);
    
    ObjectCreate(0, "Sticky_PnL", OBJ_LABEL, 0, 0, 0);
    ObjectSetInteger(0, "Sticky_PnL", OBJPROP_CORNER, CORNER_RIGHT_LOWER);
    ObjectSetInteger(0, "Sticky_PnL", OBJPROP_XDISTANCE, 190);
    ObjectSetInteger(0, "Sticky_PnL", OBJPROP_YDISTANCE, 35);
    ObjectSetString(0, "Sticky_PnL", OBJPROP_FONT, "Arial Bold");
    ObjectSetInteger(0, "Sticky_PnL", OBJPROP_FONTSIZE, 12);
}

void UpdateUI(double pnl, bool limitHit) {
    color bgColor = limitHit ? clrRed : clrYellow;
    color txtColor = limitHit ? clrWhite : clrBlack;
    
    ObjectSetInteger(0, "Sticky_Bg", OBJPROP_BGCOLOR, bgColor);
    ObjectSetInteger(0, "Sticky_Limit", OBJPROP_COLOR, txtColor);
    ObjectSetInteger(0, "Sticky_PnL", OBJPROP_COLOR, txtColor);
    
    ObjectSetString(0, "Sticky_Limit", OBJPROP_TEXT, "MAX LOSS: -$" + DoubleToString(MaxDailyLoss, 2));
    ObjectSetString(0, "Sticky_PnL", OBJPROP_TEXT, "TODAY: $" + DoubleToString(pnl, 2));
    
    ChartRedraw();
}

Re: Sticky-note daily loss limit that faces the screen

Posted: Thu Sep 24, 2026 10:30 am
by FTtrader
Operational Deployment

To run these properly alongside manual scalping:

1.) Open a blank chart (e.g., EURUSD on a daily timeframe so ticks don't distract you) and attach this EA. Keep this chart minimized or on a secondary screen.

2.) Ensure "Allow Auto Trading" / "Algo Trading" is enabled in the terminal platform settings.

3.) Because both scripts utilize OnTimer() running at 1-second intervals, they will enforce the limit during quiet market hours, weekend spikes, or illiquid periods where ticks aren't registering fast enough.

Re: Sticky-note daily loss limit that faces the screen

Posted: Thu Sep 24, 2026 10:31 am
by FTtrader
To build the "Pro" Risk Desk in MQL4 and MQL5, we have to translate Pine Script’s native tracking into custom historical array parsing. Since MT4/MT5 don't have built-in behavior tracking, the EA must manually iterate through your closed deals to calculate your daily realization, R-multiples, and consecutive losing streaks in real-time.

Here is the institutional-grade architecture for both platforms. It features a dark-mode HUD, background environment overriding (flashing the chart red/purple on a breach), and aggressive sub-second circuit breakers.

Re: Sticky-note daily loss limit that faces the screen

Posted: Thu Sep 24, 2026 10:31 am
by FTtrader
MQL4: Pro Risk Desk EA

Code: Select all

//+------------------------------------------------------------------+
//|                                            ProRiskDesk_MT4.mq4   |
//+------------------------------------------------------------------+
#property copyright "Risk Enforcer Pro"
#property strict

// --- Sunday Prep Inputs ---
input double MaxDailyLoss = 500.0; // Daily Loss Limit ($)
input double R_Value      = 100.0; // Value of 1R ($)
input int    MaxTrades    = 10;    // Max Trades Per Day
input int    MaxStreak    = 3;     // Max Consecutive Losses
input bool   FlashChart   = true;  // Override Chart Background on Halt

// --- State Variables ---
double startOfDayEquity = 0;
double peakDailyEquity = 0;
bool   isHalted = false;
bool   alertFired = false;
string haltReason = "";
color  originalBgColor;

//+------------------------------------------------------------------+
//| Initialization                                                   |
//+------------------------------------------------------------------+
int OnInit() {
    originalBgColor = (color)ChartGetInteger(0, CHART_COLOR_BACKGROUND);
    startOfDayEquity = AccountInfoDouble(ACCOUNT_EQUITY);
    peakDailyEquity = startOfDayEquity;
    
    EventSetTimer(1);
    DrawUI();
    return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason) {
    EventKillTimer();
    ObjectsDeleteAll(0, "RD_");
    ChartSetInteger(0, CHART_COLOR_BACKGROUND, originalBgColor);
}

void OnTimer() { CheckRiskLimits(); }
void OnTick()  { CheckRiskLimits(); }

//+------------------------------------------------------------------+
//| Core Logic                                                       |
//+------------------------------------------------------------------+
void CheckRiskLimits() {
    datetime startOfDay = iTime(Symbol(), PERIOD_D1, 0);
    
    // Reset state on new day
    static datetime currentDay = 0;
    if (startOfDay != currentDay) {
        startOfDayEquity = AccountInfoDouble(ACCOUNT_EQUITY);
        peakDailyEquity = startOfDayEquity;
        isHalted = false;
        alertFired = false;
        haltReason = "";
        currentDay = startOfDay;
        ChartSetInteger(0, CHART_COLOR_BACKGROUND, originalBgColor);
    }
    
    // Update Peak Equity
    double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
    if (currentEquity > peakDailyEquity) peakDailyEquity = currentEquity;
    double peakDrawdown = peakDailyEquity - currentEquity;

    // Calculate Stats
    int tradesToday = 0;
    int consecLosses = 0;
    double realizedPnL = 0;
    
    // Track History
    for(int i = 0; i < OrdersHistoryTotal(); i++) {
        if(OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) {
            if(OrderCloseTime() >= startOfDay && OrderType() <= OP_SELL) {
                tradesToday++;
                realizedPnL += OrderProfit() + OrderCommission() + OrderSwap();
            }
        }
    }
    
    // Track Consecutive Losses (Reverse loop)
    for(int i = OrdersHistoryTotal() - 1; i >= 0; i--) {
        if(OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) {
            if(OrderCloseTime() < startOfDay) break;
            if(OrderType() <= OP_SELL) {
                double prf = OrderProfit() + OrderCommission() + OrderSwap();
                if(prf < 0) consecLosses++;
                else break; // Streak broken
            }
        }
    }
    
    // Calculate Floating PnL
    double floatingPnL = 0;
    for(int i = 0; i < OrdersTotal(); i++) {
        if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) {
            floatingPnL += OrderProfit() + OrderCommission() + OrderSwap();
        }
    }
    
    double totalPnL = realizedPnL + floatingPnL;
    double safeR = R_Value > 0 ? R_Value : 1;
    double pnlInR = totalPnL / safeR;
    
    // Check Halt Conditions
    bool limitHit = (totalPnL <= -MaxDailyLoss);
    bool tiltHit = (consecLosses >= MaxStreak);
    bool overtradeHit = (tradesToday >= MaxTrades);
    
    if (limitHit || tiltHit || overtradeHit) {
        isHalted = true;
        if(limitHit) haltReason = "HARD LIMIT HIT";
        else if(tiltHit) haltReason = "TILT: MAX LOSS STREAK";
        else haltReason = "OVERTRADING CAP HIT";
        
        FlattenAccount();
        
        if (!alertFired) {
            Alert("TRADING HALTED: ", haltReason);
            alertFired = true;
            if (FlashChart) {
                color bg = limitHit ? clrMaroon : clrPurple;
                ChartSetInteger(0, CHART_COLOR_BACKGROUND, bg);
            }
        }
    }
    
    UpdateUI(totalPnL, pnlInR, peakDrawdown, tradesToday, consecLosses);
}

//+------------------------------------------------------------------+
//| Execution                                                        |
//+------------------------------------------------------------------+
void FlattenAccount() {
    for(int i = OrdersTotal() - 1; i >= 0; i--) {
        if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) {
            if(OrderType() == OP_BUY) {
                OrderClose(OrderTicket(), OrderLots(), MarketInfo(OrderSymbol(), MODE_BID), 3);
            } else if(OrderType() == OP_SELL) {
                OrderClose(OrderTicket(), OrderLots(), MarketInfo(OrderSymbol(), MODE_ASK), 3);
            } else {
                OrderDelete(OrderTicket());
            }
        }
    }
}

//+------------------------------------------------------------------+
//| UI Engine                                                        |
//+------------------------------------------------------------------+
void DrawUI() {
    CreateRect("RD_Bg", 20, 20, 220, 140, clrBlack);
    CreateText("RD_Title", 130, 125, "RISK DESK: ACTIVE", clrWhite, 10, true);
    
    CreateText("RD_L_PnL", 220, 100, "Daily PnL:", clrSilver, 9);
    CreateText("RD_V_PnL", 40, 100, "$0.00", clrLime, 9);
    
    CreateText("RD_L_R", 220, 80, "Target (R):", clrSilver, 9);
    CreateText("RD_V_R", 40, 80, "0.00 R", clrLime, 9);
    
    CreateText("RD_L_DD", 220, 60, "Drawdown:", clrSilver, 9);
    CreateText("RD_V_DD", 40, 60, "$0.00", clrGray, 9);
    
    CreateText("RD_L_Trd", 220, 40, "Trades:", clrSilver, 9);
    CreateText("RD_V_Trd", 40, 40, "0 / 0", clrWhite, 9);
    
    CreateText("RD_L_Str", 220, 20, "Loss Streak:", clrSilver, 9);
    CreateText("RD_V_Str", 40, 20, "0 / 0", clrWhite, 9);
}

void CreateRect(string name, int x, int y, int w, int h, color bg) {
    ObjectCreate(0, name, OBJ_RECTANGLE_LABEL, 0, 0, 0);
    ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_RIGHT_LOWER);
    ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
    ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
    ObjectSetInteger(0, name, OBJPROP_XSIZE, w);
    ObjectSetInteger(0, name, OBJPROP_YSIZE, h);
    ObjectSetInteger(0, name, OBJPROP_BGCOLOR, bg);
    ObjectSetInteger(0, name, OBJPROP_BORDER_COLOR, clrDimGray);
}

void CreateText(string name, int x, int y, string text, color clr, int size, bool bold=false) {
    ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
    ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_RIGHT_LOWER);
    ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
    ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
    ObjectSetString(0, name, OBJPROP_FONT, bold ? "Courier New Bold" : "Courier New");
    ObjectSetInteger(0, name, OBJPROP_FONTSIZE, size);
    ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
    ObjectSetString(0, name, OBJPROP_TEXT, text);
}

void UpdateUI(double pnl, double r, double dd, int trd, int str) {
    ObjectSetString(0, "RD_Title", OBJPROP_TEXT, isHalted ? haltReason : "RISK DESK: ACTIVE");
    ObjectSetInteger(0, "RD_Title", OBJPROP_COLOR, isHalted ? clrWhite : clrAqua);
    ObjectSetInteger(0, "RD_Bg", OBJPROP_BGCOLOR, isHalted ? clrMaroon : clrBlack);
    
    ObjectSetString(0, "RD_V_PnL", OBJPROP_TEXT, (pnl >= 0 ? "+$" : "-$") + DoubleToStr(MathAbs(pnl), 2));
    ObjectSetInteger(0, "RD_V_PnL", OBJPROP_COLOR, pnl >= 0 ? clrLime : clrRed);
    
    ObjectSetString(0, "RD_V_R", OBJPROP_TEXT, (r >= 0 ? "+" : "") + DoubleToStr(r, 2) + " R");
    ObjectSetInteger(0, "RD_V_R", OBJPROP_COLOR, r >= 0 ? clrLime : clrRed);
    
    ObjectSetString(0, "RD_V_DD", OBJPROP_TEXT, "$" + DoubleToStr(dd, 2));
    
    ObjectSetString(0, "RD_V_Trd", OBJPROP_TEXT, IntegerToString(trd) + " / " + IntegerToString(MaxTrades));
    ObjectSetInteger(0, "RD_V_Trd", OBJPROP_COLOR, trd >= MaxTrades ? clrRed : clrWhite);
    
    ObjectSetString(0, "RD_V_Str", OBJPROP_TEXT, IntegerToString(str) + " / " + IntegerToString(MaxStreak));
    ObjectSetInteger(0, "RD_V_Str", OBJPROP_COLOR, str >= MaxStreak ? clrRed : clrWhite);
    
    ChartRedraw();
}

Re: Sticky-note daily loss limit that faces the screen

Posted: Thu Sep 24, 2026 10:32 am
by FTtrader
MQL5: Pro Risk Desk EA

In MQL5, we must filter history deals specifically by DEAL_ENTRY_OUT or DEAL_ENTRY_INOUT to accurately count round-trip executions and streaks without counting deposits/withdrawals.

Code: Select all

//+------------------------------------------------------------------+
//|                                            ProRiskDesk_MT5.mq5   |
//+------------------------------------------------------------------+
#property copyright "Risk Enforcer Pro"
#property version   "1.00"

#include <Trade\Trade.mqh>
CTrade trade;

// --- Sunday Prep Inputs ---
input double MaxDailyLoss = 500.0; 
input double R_Value      = 100.0; 
input int    MaxTrades    = 10;    
input int    MaxStreak    = 3;     
input bool   FlashChart   = true;  

// --- State Variables ---
double startOfDayEquity = 0;
double peakDailyEquity = 0;
bool   isHalted = false;
bool   alertFired = false;
string haltReason = "";
color  originalBgColor;

//+------------------------------------------------------------------+
//| Initialization                                                   |
//+------------------------------------------------------------------+
int OnInit() {
    originalBgColor = (color)ChartGetInteger(0, CHART_COLOR_BACKGROUND);
    startOfDayEquity = AccountInfoDouble(ACCOUNT_EQUITY);
    peakDailyEquity = startOfDayEquity;
    
    EventSetTimer(1);
    DrawUI();
    return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason) {
    EventKillTimer();
    ObjectsDeleteAll(0, "RD_");
    ChartSetInteger(0, CHART_COLOR_BACKGROUND, originalBgColor);
}

void OnTimer() { CheckRiskLimits(); }
void OnTick()  { CheckRiskLimits(); }

//+------------------------------------------------------------------+
//| Core Logic                                                       |
//+------------------------------------------------------------------+
void CheckRiskLimits() {
    MqlDateTime dt;
    TimeCurrent(dt);
    dt.hour = 0; dt.min = 0; dt.sec = 0;
    datetime startOfDay = StructToTime(dt);
    
    static datetime currentDay = 0;
    if (startOfDay != currentDay) {
        startOfDayEquity = AccountInfoDouble(ACCOUNT_EQUITY);
        peakDailyEquity = startOfDayEquity;
        isHalted = false;
        alertFired = false;
        haltReason = "";
        currentDay = startOfDay;
        ChartSetInteger(0, CHART_COLOR_BACKGROUND, originalBgColor);
    }
    
    double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
    if (currentEquity > peakDailyEquity) peakDailyEquity = currentEquity;
    double peakDrawdown = peakDailyEquity - currentEquity;

    int tradesToday = 0;
    int consecLosses = 0;
    double realizedPnL = 0;
    
    if(HistorySelect(startOfDay, TimeCurrent())) {
        int dealsTotal = HistoryDealsTotal();
        
        // Count total trades & realized PnL
        for(int i = 0; i < dealsTotal; i++) {
            ulong ticket = HistoryDealGetTicket(i);
            long entryType = HistoryDealGetInteger(ticket, DEAL_ENTRY);
            
            if(entryType == DEAL_ENTRY_OUT || entryType == DEAL_ENTRY_INOUT) {
                tradesToday++;
                realizedPnL += HistoryDealGetDouble(ticket, DEAL_PROFIT) 
                             + HistoryDealGetDouble(ticket, DEAL_COMMISSION) 
                             + HistoryDealGetDouble(ticket, DEAL_SWAP)
                             + HistoryDealGetDouble(ticket, DEAL_FEE);
            }
        }
        
        // Calculate Streak (Reverse loop)
        for(int i = dealsTotal - 1; i >= 0; i--) {
            ulong ticket = HistoryDealGetTicket(i);
            long entryType = HistoryDealGetInteger(ticket, DEAL_ENTRY);
            
            if(entryType == DEAL_ENTRY_OUT || entryType == DEAL_ENTRY_INOUT) {
                double prf = HistoryDealGetDouble(ticket, DEAL_PROFIT) 
                           + HistoryDealGetDouble(ticket, DEAL_COMMISSION) 
                           + HistoryDealGetDouble(ticket, DEAL_SWAP)
                           + HistoryDealGetDouble(ticket, DEAL_FEE);
                if(prf < 0) consecLosses++;
                else break; 
            }
        }
    }
    
    double floatingPnL = 0;
    for(int i = 0; i < PositionsTotal(); i++) {
        ulong posTicket = PositionGetTicket(i);
        if(posTicket > 0) {
            floatingPnL += PositionGetDouble(POSITION_PROFIT) 
                         + PositionGetDouble(POSITION_SWAP);
        }
    }
    
    double totalPnL = realizedPnL + floatingPnL;
    double safeR = R_Value > 0 ? R_Value : 1;
    double pnlInR = totalPnL / safeR;
    
    bool limitHit = (totalPnL <= -MaxDailyLoss);
    bool tiltHit = (consecLosses >= MaxStreak);
    bool overtradeHit = (tradesToday >= MaxTrades);
    
    if (limitHit || tiltHit || overtradeHit) {
        isHalted = true;
        if(limitHit) haltReason = "HARD LIMIT HIT";
        else if(tiltHit) haltReason = "TILT: MAX LOSS STREAK";
        else haltReason = "OVERTRADING CAP HIT";
        
        FlattenAccount();
        
        if (!alertFired) {
            Alert("TRADING HALTED: ", haltReason);
            alertFired = true;
            if (FlashChart) {
                color bg = limitHit ? clrMaroon : clrPurple;
                ChartSetInteger(0, CHART_COLOR_BACKGROUND, bg);
            }
        }
    }
    
    UpdateUI(totalPnL, pnlInR, peakDrawdown, tradesToday, consecLosses);
}

//+------------------------------------------------------------------+
//| Execution                                                        |
//+------------------------------------------------------------------+
void FlattenAccount() {
    for(int i = PositionsTotal() - 1; i >= 0; i--) {
        ulong posTicket = PositionGetTicket(i);
        if(posTicket > 0) trade.PositionClose(posTicket);
    }
    for(int i = OrdersTotal() - 1; i >= 0; i--) {
        ulong orderTicket = OrderGetTicket(i);
        if(orderTicket > 0) trade.OrderDelete(orderTicket);
    }
}

//+------------------------------------------------------------------+
//| UI Engine                                                        |
//+------------------------------------------------------------------+
void DrawUI() {
    CreateRect("RD_Bg", 20, 20, 220, 140, clrBlack);
    CreateText("RD_Title", 130, 125, "RISK DESK: ACTIVE", clrWhite, 10, true);
    
    CreateText("RD_L_PnL", 220, 100, "Daily PnL:", clrSilver, 9);
    CreateText("RD_V_PnL", 40, 100, "$0.00", clrLime, 9);
    
    CreateText("RD_L_R", 220, 80, "Target (R):", clrSilver, 9);
    CreateText("RD_V_R", 40, 80, "0.00 R", clrLime, 9);
    
    CreateText("RD_L_DD", 220, 60, "Drawdown:", clrSilver, 9);
    CreateText("RD_V_DD", 40, 60, "$0.00", clrGray, 9);
    
    CreateText("RD_L_Trd", 220, 40, "Trades:", clrSilver, 9);
    CreateText("RD_V_Trd", 40, 40, "0 / 0", clrWhite, 9);
    
    CreateText("RD_L_Str", 220, 20, "Loss Streak:", clrSilver, 9);
    CreateText("RD_V_Str", 40, 20, "0 / 0", clrWhite, 9);
}

void CreateRect(string name, int x, int y, int w, int h, color bg) {
    ObjectCreate(0, name, OBJ_RECTANGLE_LABEL, 0, 0, 0);
    ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_RIGHT_LOWER);
    ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
    ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
    ObjectSetInteger(0, name, OBJPROP_XSIZE, w);
    ObjectSetInteger(0, name, OBJPROP_YSIZE, h);
    ObjectSetInteger(0, name, OBJPROP_BGCOLOR, bg);
    ObjectSetInteger(0, name, OBJPROP_BORDER_COLOR, clrDimGray);
}

void CreateText(string name, int x, int y, string text, color clr, int size, bool bold=false) {
    ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
    ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_RIGHT_LOWER);
    ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
    ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
    ObjectSetString(0, name, OBJPROP_FONT, bold ? "Courier New Bold" : "Courier New");
    ObjectSetInteger(0, name, OBJPROP_FONTSIZE, size);
    ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
    ObjectSetString(0, name, OBJPROP_TEXT, text);
}

void UpdateUI(double pnl, double r, double dd, int trd, int str) {
    ObjectSetString(0, "RD_Title", OBJPROP_TEXT, isHalted ? haltReason : "RISK DESK: ACTIVE");
    ObjectSetInteger(0, "RD_Title", OBJPROP_COLOR, isHalted ? clrWhite : clrAqua);
    ObjectSetInteger(0, "RD_Bg", OBJPROP_BGCOLOR, isHalted ? clrMaroon : clrBlack);
    
    ObjectSetString(0, "RD_V_PnL", OBJPROP_TEXT, (pnl >= 0 ? "+$" : "-$") + DoubleToString(MathAbs(pnl), 2));
    ObjectSetInteger(0, "RD_V_PnL", OBJPROP_COLOR, pnl >= 0 ? clrLime : clrRed);
    
    ObjectSetString(0, "RD_V_R", OBJPROP_TEXT, (r >= 0 ? "+" : "") + DoubleToString(r, 2) + " R");
    ObjectSetInteger(0, "RD_V_R", OBJPROP_COLOR, r >= 0 ? clrLime : clrRed);
    
    ObjectSetString(0, "RD_V_DD", OBJPROP_TEXT, "$" + DoubleToString(dd, 2));
    
    ObjectSetString(0, "RD_V_Trd", OBJPROP_TEXT, IntegerToString(trd) + " / " + IntegerToString(MaxTrades));
    ObjectSetInteger(0, "RD_V_Trd", OBJPROP_COLOR, trd >= MaxTrades ? clrRed : clrWhite);
    
    ObjectSetString(0, "RD_V_Str", OBJPROP_TEXT, IntegerToString(str) + " / " + IntegerToString(MaxStreak));
    ObjectSetInteger(0, "RD_V_Str", OBJPROP_COLOR, str >= MaxStreak ? clrRed : clrWhite);
    
    ChartRedraw();
}

Re: Sticky-note daily loss limit that faces the screen

Posted: Thu Sep 24, 2026 10:32 am
by FTtrader
Institutional Deployment

1.) Run this on a completely isolated, blank chart (like a Daily timeframe so ticks aren't distracting).

2.) The UI is built programmatically using modular CreateRect and CreateText functions to keep the architecture clean (and offload rendering overhead).

3.) The Lockout: Because the execution block (FlattenAccount) is tied directly to the OnTimer() function that fires every 1000ms, if you hit a behavioral limit (e.g., 3 consecutive losses) and attempt to bypass it by manually submitting a new order, the EA will detect the isHalted state and instantly close the trade in the next sub-second cycle.

Re: Sticky-note daily loss limit that faces the screen

Posted: Thu Sep 24, 2026 10:33 am
by FTtrader
Because cTrader is built on C# and .NET, we can build the UI using its native, WPF-style control elements and use LINQ to calculate the historical PnL cleanly.

As a cBot, this runs in the background. It uses a 1-second timer to ensure the circuit breaker functions perfectly even during illiquid market moments when ticks aren't registering fast enough.

Here is the basic "Digital Sticky Note" translated for cTrader.

Re: Sticky-note daily loss limit that faces the screen

Posted: Thu Sep 24, 2026 10:34 am
by FTtrader
cTrader: Digital Sticky Note (cBot)

Code: Select all

using System;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Models;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class DigitalStickyNote : Robot
    {
        [Parameter("Daily Loss Limit ($)", DefaultValue = 500.0, MinValue = 0, Step = 50)]
        public double MaxDailyLoss { get; set; }

        private Border _stickyBorder;
        private TextBlock _limitText;
        private TextBlock _pnlText;

        protected override void OnStart()
        {
            // 1. Initialize the UI (The Digital Sticky Note)
            DrawUI();

            // 2. Start the 1-second timer to enforce risk regardless of tick volume
            Timer.Start(TimeSpan.FromSeconds(1));
        }

        protected override void OnTick()
        {
            CheckRiskLimit();
        }

        protected override void OnTimer()
        {
            CheckRiskLimit();
        }

        private void CheckRiskLimit()
        {
            double dailyPnL = CalculateDailyPnL();
            bool limitHit = dailyPnL <= -MaxDailyLoss;

            UpdateUI(dailyPnL, limitHit);

            if (limitHit)
            {
                FlattenAccount();
            }
        }

        private double CalculateDailyPnL()
        {
            // Get the start of the current trading day
            DateTime startOfDay = Server.Time.Date;

            // Sum Realized PnL (Closed trades today). NetProfit includes commissions and swaps.
            double realizedPnL = History
                .Where(trade => trade.ClosingTime >= startOfDay)
                .Sum(trade => trade.NetProfit);

            // Sum Unrealized PnL (Open positions floating)
            double unrealizedPnL = Positions.Sum(pos => pos.NetProfit);

            return realizedPnL + unrealizedPnL;
        }

        private void FlattenAccount()
        {
            // Close all open positions asynchronously to avoid blocking the thread
            foreach (var position in Positions)
            {
                ClosePositionAsync(position);
            }

            // Cancel all pending orders
            foreach (var order in PendingOrders)
            {
                CancelPendingOrderAsync(order);
            }
        }

        private void DrawUI()
        {
            // Define the text elements
            _limitText = new TextBlock
            {
                Text = $"MAX LOSS: -${MaxDailyLoss:F2}",
                ForegroundColor = Color.Black,
                FontWeight = FontWeight.Normal,
                FontSize = 11
            };

            _pnlText = new TextBlock
            {
                Text = "TODAY: $0.00",
                ForegroundColor = Color.Black,
                FontWeight = FontWeight.ExtraBold,
                FontSize = 14,
                Margin = new Thickness(0, 5, 0, 0)
            };

            // Wrap them in a StackPanel
            var stackPanel = new StackPanel
            {
                Orientation = Orientation.Vertical,
                Margin = new Thickness(15)
            };
            stackPanel.AddChild(_limitText);
            stackPanel.AddChild(_pnlText);

            // Create the Yellow Sticky Note background
            _stickyBorder = new Border
            {
                BackgroundColor = Color.Yellow,
                BorderColor = Color.Black,
                BorderThickness = new Thickness(2),
                HorizontalAlignment = HorizontalAlignment.Right,
                VerticalAlignment = VerticalAlignment.Bottom,
                Margin = new Thickness(20),
                Child = stackPanel
            };

            // Render to the chart
            Chart.AddControl(_stickyBorder);
        }

        private void UpdateUI(double pnl, bool limitHit)
        {
            // Visual interrupt: Switch to Red/White if the limit is breached
            _stickyBorder.BackgroundColor = limitHit ? Color.Red : Color.Yellow;
            _limitText.ForegroundColor = limitHit ? Color.White : Color.Black;
            _pnlText.ForegroundColor = limitHit ? Color.White : Color.Black;

            _pnlText.Text = $"TODAY: ${(pnl > 0 ? "+" : "")}{pnl:F2}";
        }
    }
}