Warrior_EA/Money/WarriorMoney.mqh

110 lines
5.4 KiB
MQL5

//+------------------------------------------------------------------+
//| 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 <Expert\ExpertMoney.mqh>
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