110 lines
5.4 KiB
MQL5
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
|