//+------------------------------------------------------------------+ //| WarriorMoney.mqh | //| AnimateDread | //| | //| Position sizing: the standard library's shape, with the one hole | //| in it closed. | //| | //| 🛑 THE HOLE. MQL5's own CMoneyFixedRisk ends with | //| | //| if(lot < minvol) lot = minvol; | //| | //| so when the risk budget buys LESS than one minimum lot, it trades | //| a minimum lot anyway. The requested risk is silently exceeded, | //| by an arbitrary factor that is largest exactly when the stop is | //| widest - and nothing in the log says so. On an index with a 0.10 | //| minimum that can be several times the intended risk. | //| | //| Here a trade that cannot be sized within its risk is REFUSED. A | //| skipped trade is a missing row in the results; an oversized one | //| is a wrong number in them, and only the second kind is dangerous. | //| This repo closed the same hole once before, in 77e8080. | //| | //| NO RISK BUDGET, NO LATCH, NO PERSISTED STATE. CRiskBudget writes | //| a permanent halt to disk that survives between backtests, so one | //| breaching run silently truncates every later run on that symbol. | //| Account protection belongs in a veto filter that starts fresh | //| each pass, not in the lot-size calculation. | //+------------------------------------------------------------------+ #ifndef WARRIOR_SIMPLE_MONEY_MQH #define WARRIOR_SIMPLE_MONEY_MQH #include class CWarriorMoney : public CExpertMoney { protected: bool m_fixedLot; // true = trade m_lots flat, ignore m_percent double m_lots; int m_refused; // how many entries could not be sized - reported at deinit double Size(ENUM_ORDER_TYPE type, double price, double sl); public: CWarriorMoney(void) : m_fixedLot(false), m_lots(0.01), m_refused(0) {} ~CWarriorMoney(void) {} void FixedLot(const double lots) { m_fixedLot = true; m_lots = lots; } void RiskPercent(const double p) { m_fixedLot = false; m_percent = p; } int Refused(void) const { return m_refused; } virtual double CheckOpenLong(double price, double sl) override { return Size(ORDER_TYPE_BUY, price, sl); } virtual double CheckOpenShort(double price, double sl) override { return Size(ORDER_TYPE_SELL, price, sl); } }; //+------------------------------------------------------------------+ double CWarriorMoney::Size(ENUM_ORDER_TYPE type, double price, double sl) { if(m_symbol == NULL) return 0.0; const double minvol = m_symbol.LotsMin(); const double maxvol = m_symbol.LotsMax(); const double step = m_symbol.LotsStep(); if(m_fixedLot) { double lot = m_lots; if(lot < minvol) lot = minvol; if(lot > maxvol) lot = maxvol; return lot; } //--- NO STOP MEANS NO RISK UNIT, so there is nothing to size against. The stdlib answers this //--- case with "the largest lot the margin allows", which is the opposite of a risk rule. if(sl == 0.0 || price <= 0.0) { m_refused++; return 0.0; } //--- Loss per one lot between entry and stop, asked of the broker rather than derived from the //--- point value - it is the only source that knows this symbol's contract and currency. const double lossPerLot = -m_account.OrderProfitCheck(m_symbol.Name(), type, 1.0, price, sl); if(lossPerLot <= 0.0) { //--- An inverted or zero-distance stop. Refuse: dividing by it produces either a negative lot //--- or an enormous one, and both have reached live accounts in this repo's history. m_refused++; return 0.0; } const double riskMoney = m_account.Balance() * m_percent / 100.0; double lot = (step > 0.0) ? MathFloor(riskMoney / lossPerLot / step) * step : riskMoney / lossPerLot; if(lot < minvol) { //--- THE REFUSAL. Rounding up to the minimum here is what the stdlib does and what this class //--- exists not to do. Said out loud, throttled to once per 50, because a silent skip is how //--- "the EA barely traded" becomes a mystery instead of a message. m_refused++; if(m_refused % 50 == 1) PrintFormat("CWarriorMoney: refused - %.2f%% of %.2f is %.2f, and one minimum lot (%.2f)" " would risk %.2f. Widen the risk, tighten the stop, or accept fewer trades;" " trading a minimum lot here would exceed the stated risk. (%d so far)", m_percent, m_account.Balance(), riskMoney, minvol, minvol * lossPerLot, m_refused); return 0.0; } if(lot > maxvol) lot = maxvol; return lot; } #endif // WARRIOR_SIMPLE_MONEY_MQH