forked from airat77786/MQL5Book
415 lines
14 KiB
MQL5
415 lines
14 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| LotMarginExposureTable.mq5 |
| |||
//| Copyright (c) 2021-2022, Marketeer |
| |||
//| https://www.mql5.com/en/users/marketeer |
| |||
//+------------------------------------------------------------------+
| |||
#property copyright "Copyright (c) 2021-2022, Marketeer"
| |||
#property link "https://www.mql5.com/en/users/marketeer"
| |||
#property version "1.0"
| |||
#property description "Display a table with lots, risks, margins and exposure estimation for selected symbols.\n"
| |||
| |||
#include "..\..\Include\LotMarginExposure.mqh"
| |||
#include "..\..\Include\Tableau.mqh"
| |||
#define TBL_COLUMNS 8
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Inputs |
| |||
//+------------------------------------------------------------------+
| |||
input ENUM_ORDER_TYPE Action = ORDER_TYPE_BUY;
| |||
input string WorkList = ""; // Symbols (comma,separated,list)
| |||
input double Money = 0; // Money (0 = free margin)
| |||
input double Lot = 0; // Lot (0 = min lot)
| |||
input double Exposure = 5.0; // Exposure (%)
| |||
input double RiskLevel = 5.0; // RiskLevel (%)
| |||
input int RiskPoints = 0; // RiskPoints/SL (0 = auto-range of RiskPeriod)
| |||
input ENUM_TIMEFRAMES RiskPeriod = PERIOD_W1;
| |||
input int UpdateFrequency = 0; // UpdateFrequency (sec, 0 - once per bar)
| |||
input bool PrintToLog = false;
| |||
input ENUM_BASE_CORNER Corner = CORNER_RIGHT_LOWER;
| |||
input int Gap = 16;
| |||
input int FontSize = 8;
| |||
input string DefaultFontName = "Consolas";
| |||
input string TitleFontName = "Arial Black";
| |||
input string MotoTypeFontsHint = "Consolas/Courier/Courier New/Lucida Console/Lucida Sans Typewriter";
| |||
input color BackgroundColor = 0x808080;
| |||
input uchar BackgroundTransparency = 0xC0; // BackgroundTransparency (255 - opaque, 0 - glassy)
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Globals |
| |||
//+------------------------------------------------------------------+
| |||
string symbols[];
| |||
datetime lastTime;
| |||
Tableau *t;
| |||
int sortByColumn;
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Columns of the table |
| |||
//+------------------------------------------------------------------+
| |||
enum LME_FIELDS // 10 data fields, 3 additional properties of symbol
| |||
{
| |||
eLot,
| |||
eAtrPointsNormalized,
| |||
eAtrValue,
| |||
eLotFromExposureRaw,
| |||
eLotFromExposure,
| |||
eLotFromRiskOfStopLossRaw,
| |||
eLotFromRiskOfStopLoss,
| |||
eExposureFromLot,
| |||
eMarginLevelFromLot,
| |||
eLotDig,
| |||
eMinLot,
| |||
eContract,
| |||
eSymbol
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Hints (shown in tooltips on mouse hover) for 8 columns' headers |
| |||
//+------------------------------------------------------------------+
| |||
string hints[] =
| |||
{
| |||
"Balance",
| |||
"Lot (Exposure,%)",
| |||
"Lot (Risk,%)",
| |||
"Exposure,% (Lot)\nLot=0 means minimal lot",
| |||
"Margin Level,% (Lot)\nLot=0 means minimal lot",
| |||
"Minimal lot",
| |||
"Contract size",
| |||
"P/L for 1 lot (range)\nfor RiskPoints or RiskPeriod"
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Expert initialization function |
| |||
//+------------------------------------------------------------------+
| |||
void OnInit()
| |||
{
| |||
Comment("Starting...");
| |||
lastTime = 0;
| |||
t = NULL;
| |||
| |||
if(GlobalVariableCheck("LEMT_SORT")) // restore sorting by column
| |||
{
| |||
sortByColumn = (int)GlobalVariableGet("LEMT_SORT");
| |||
if(sortByColumn == 0 || sortByColumn > TBL_COLUMNS || sortByColumn < -TBL_COLUMNS)
| |||
{
| |||
sortByColumn = INT_MAX;
| |||
}
| |||
}
| |||
else
| |||
{
| |||
sortByColumn = INT_MAX;
| |||
}
| |||
EventSetTimer(1);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Timer event handler |
| |||
//+------------------------------------------------------------------+
| |||
void OnTimer()
| |||
{
| |||
if(lastTime == 0) // first time calculation
| |||
{
| |||
OnTick();
| |||
Comment("Started");
| |||
}
| |||
else if(lastTime != -1)
| |||
{
| |||
if(UpdateFrequency <= 0) // if no frequency given, wait for new bar in OnTick
| |||
{
| |||
EventKillTimer(); // drop the timer, no need it anymore
| |||
}
| |||
else if(TimeCurrent() - lastTime >= UpdateFrequency)
| |||
{
| |||
lastTime = LONG_MAX; // prevent reentrancy to this if-branch
| |||
OnTick();
| |||
if(lastTime != -1) // OnTick processed without error
| |||
{
| |||
lastTime = TimeCurrent(); // update the time mark
| |||
}
| |||
}
| |||
Comment("");
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Find a single character mark for account currency |
| |||
//+------------------------------------------------------------------+
| |||
string getCurrencyMark()
| |||
{
| |||
string acc = AccountInfoString(ACCOUNT_CURRENCY);
| |||
StringToUpper(acc);
| |||
if(acc == "USD") return "$";
| |||
if(acc == "EUR") return "€";
| |||
if(acc == "GBP") return "£";
| |||
if(acc == "JPY") return "¥";
| |||
return StringSubstr(acc, 0, 1);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Short presentation of Kilo-numbers |
| |||
//+------------------------------------------------------------------+
| |||
template<typename T>
| |||
string compact(T v, string fmt)
| |||
{
| |||
if(v > 1000)
| |||
{
| |||
if(StringReplace(fmt, "%%", "K%%") <= 0)
| |||
{
| |||
fmt += "K";
| |||
}
| |||
return StringFormat(fmt, (T)(v / 1000));
| |||
}
| |||
return StringFormat(fmt, v);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Helper equivalent of a string like number, used for unified sort |
| |||
//+------------------------------------------------------------------+
| |||
double pack2double(const string s)
| |||
{
| |||
double r = 0;
| |||
for(int i = 0; i < StringLen(s); i++)
| |||
{
| |||
r = (r * 255) + (StringGetCharacter(s, i) % 255);
| |||
}
| |||
return r;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Expert tick function |
| |||
//+------------------------------------------------------------------+
| |||
void OnTick()
| |||
{
| |||
if(lastTime == -1) return; // error was already there, just exit
| |||
| |||
if(UpdateFrequency <= 0)
| |||
{
| |||
if(lastTime == iTime(NULL, 0, 0)) return; // wait for a new bar
| |||
}
| |||
else if(TimeCurrent() - lastTime < UpdateFrequency)
| |||
{
| |||
return;
| |||
}
| |||
| |||
const int ns = StringSplit((WorkList == "" ? _Symbol : WorkList), ',', symbols);
| |||
if(ns <= 0)
| |||
{
| |||
Print("Empty symbols");
| |||
lastTime = -1;
| |||
return;
| |||
}
| |||
| |||
if(Exposure > 100 || Exposure <= 0)
| |||
{
| |||
Print("Percent of Exposure is incorrect: ", Exposure);
| |||
lastTime = -1;
| |||
return;
| |||
}
| |||
| |||
if(RiskLevel > 100 || RiskLevel <= 0)
| |||
{
| |||
Print("Percent of RiskLevel is incorrect: ", RiskLevel);
| |||
lastTime = -1;
| |||
return;
| |||
}
| |||
| |||
lastTime = UpdateFrequency > 0 ? TimeCurrent() : iTime(NULL, 0, 0);
| |||
| |||
const double stopOutLevel = AccountInfoDouble(ACCOUNT_MARGIN_SO_CALL);
| |||
const int digits = (int)AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS);
| |||
const double money = Money == 0 ? AccountInfoDouble(ACCOUNT_MARGIN_FREE) : Money;
| |||
| |||
string used = "";
| |||
if(Money == 0 && AccountInfoDouble(ACCOUNT_MARGIN) > 0)
| |||
{
| |||
hints[0] = "Free\nIn Use: " + StringFormat("%.*f", digits, AccountInfoDouble(ACCOUNT_MARGIN));
| |||
}
| |||
else
| |||
{
| |||
hints[0] = "Balance";
| |||
}
| |||
| |||
hints[0] += "\nStop Out: " + (string)(float)stopOutLevel;
| |||
| |||
LEMLR::SymbolLotExposureRisk r = {};
| |||
| |||
// In total 13 fields per symbol are stored in 'LME' array (below)
| |||
// and used to render the table in UI.
| |||
// In the table 8 visible columns comprise:
| |||
// - 5 columns formed from 10 calculated values (marginal characteristics)
| |||
// - 3 columns with additional properties (symbol name, minimal lot, contact size)
| |||
//
| |||
// * symbol
| |||
// lotFromExposure/lotFromExposureRaw
| |||
// lotFromRiskOfStopLoss/lotFromRiskOfStopLossRaw
| |||
// exposureFromLot
| |||
// marginLevelFromLot
| |||
// * minlot
| |||
// * contract
| |||
// atrValue
| |||
| |||
double LME[][13];
| |||
ArrayResize(LME, ns);
| |||
ArrayInitialize(LME, 0);
| |||
| |||
for(int i = 0; i < ns; i++)
| |||
{
| |||
if(!LEMLR::Estimate(Action, symbols[i], Lot, 0,
| |||
Exposure, RiskLevel, RiskPoints, RiskPeriod, Money, r))
| |||
{
| |||
Print("Calc failed (will try on the next bar, or refresh manually)");
| |||
return;
| |||
}
| |||
| |||
LME[i][eLot] = r.lot;
| |||
LME[i][eAtrPointsNormalized] = r.atrPointsNormalized;
| |||
LME[i][eAtrValue] = r.atrValue;
| |||
LME[i][eLotFromExposureRaw] = r.lotFromExposureRaw;
| |||
LME[i][eLotFromExposure] = r.lotFromExposure;
| |||
LME[i][eLotFromRiskOfStopLossRaw] = r.lotFromRiskOfStopLossRaw;
| |||
LME[i][eLotFromRiskOfStopLoss] = r.lotFromRiskOfStopLoss;
| |||
LME[i][eExposureFromLot] = r.exposureFromLot;
| |||
LME[i][eMarginLevelFromLot] = r.marginLevelFromLot;
| |||
LME[i][eLotDig] = r.lotDigits;
| |||
LME[i][eMinLot] = SymbolInfoDouble(symbols[i], SYMBOL_VOLUME_MIN);
| |||
LME[i][eContract] = SymbolInfoDouble(symbols[i], SYMBOL_TRADE_CONTRACT_SIZE);
| |||
LME[i][eSymbol] = pack2double(symbols[i]);
| |||
}
| |||
| |||
// prepare the string array to render the table in UI
| |||
string data[];
| |||
ArrayResize(data, (ns + 1) * TBL_COLUMNS);
| |||
| |||
const string C = getCurrencyMark();
| |||
const int lotDigits = Lot <= 0.0 ? 0 : (int)MathLog10(1.0 / Lot);
| |||
| |||
// the first row in the header
| |||
data[0] = StringFormat("%s%.*f%s", C, digits, money, used);
| |||
data[1] = StringFormat("L(E=%.*f%%)", 1, Exposure);
| |||
data[2] = StringFormat("L(R=%.*f%%)", 1, RiskLevel);
| |||
data[3] = StringFormat("E%%(L=%.*f)", lotDigits, Lot);
| |||
data[4] = StringFormat("M%%(L=%.*f)", lotDigits, Lot);
| |||
data[5] = "MinL";
| |||
data[6] = "Contract";
| |||
data[7] = StringFormat("Risk%s",
| |||
(RiskPoints == 0 ?
| |||
"(" + StringSubstr(EnumToString(RiskPeriod), StringLen("PERIOD_")) + ")" :
| |||
"(" + (string)RiskPoints + (RiskPoints < 0 ? "%)" : "pt)")));
| |||
| |||
double sorter[][2];
| |||
| |||
if(sortByColumn != INT_MAX) // if some column was clicked, reorder by its values
| |||
{
| |||
ArrayResize(sorter, ns);
| |||
const int s = sortByColumn != INT_MAX ? MathAbs(sortByColumn) : 1;
| |||
| |||
// from column number to field index
| |||
static int converter[] = {eSymbol, eLotFromExposureRaw, eLotFromRiskOfStopLossRaw,
| |||
eExposureFromLot, eMarginLevelFromLot, eMinLot, eContract, eAtrValue};
| |||
| |||
// copy selected column to 0-th for sorting
| |||
for(int i = 0; i < ns; i++)
| |||
{
| |||
sorter[i][0] = LME[i][converter[s - 1]];
| |||
sorter[i][1] = i;
| |||
}
| |||
| |||
ArraySort(sorter);
| |||
if(sortByColumn < 1)
| |||
{
| |||
ArrayReverse(sorter);
| |||
}
| |||
data[MathAbs(sortByColumn) - 1] += sortByColumn > 0 ? "˅" : "˄";
| |||
}
| |||
| |||
int c = TBL_COLUMNS; // place cursor right after header strings
| |||
| |||
// fill array with actual values, stringified using appropriate formatting
| |||
for(int j = 0; j < ns; j++)
| |||
{
| |||
int i = (int)(sortByColumn != INT_MAX ? sorter[j][1] : j);
| |||
data[c + 0] = symbols[i];
| |||
data[c + 1] = StringFormat("%s", (LME[i][eLotFromExposure] == 0 ?
| |||
"(" + (string)(float)LME[i][eLotFromExposureRaw] + ")" :
| |||
StringFormat("%.*f", MathMax((int)LME[i][eLotDig], 2), LME[i][eLotFromExposure])));
| |||
data[c + 2] = StringFormat("%.*f %s", MathMax((int)LME[i][eLotDig], 2),
| |||
LME[i][eLotFromRiskOfStopLoss], (LME[i][eLotFromRiskOfStopLoss] == 0 ?
| |||
"(" + (string)(float)LME[i][eLotFromRiskOfStopLossRaw] + ")" : ""));
| |||
data[c + 3] = StringFormat("%.2f%%", LME[i][eExposureFromLot]);
| |||
data[c + 4] = StringFormat("%s%%", compact(LME[i][eMarginLevelFromLot], "%.2f"));
| |||
data[c + 5] = StringFormat("%.*f", MathMax((int)LME[i][eLotDig], 2),
| |||
SymbolInfoDouble(symbols[i], SYMBOL_VOLUME_MIN));
| |||
data[c + 6] = StringFormat("%s", compact((int)SymbolInfoDouble(symbols[i],
| |||
SYMBOL_TRADE_CONTRACT_SIZE), "%d"));
| |||
data[c + 7] = StringFormat("%s%s", (RiskPoints >= 0 ? C + compact(LME[i][eAtrValue], "%.2f") : ""),
| |||
(RiskPoints <= 0 ? "(" + compact((int)LME[i][eAtrPointsNormalized], "%d") + "pt)" : ""));
| |||
c += TBL_COLUMNS;
| |||
}
| |||
| |||
// create the table object if not exist already
| |||
if(t == NULL)
| |||
{
| |||
t = new Tableau("LEMT", ns + 1, TBL_COLUMNS, -1, 100,
| |||
Corner, Gap, FontSize, DefaultFontName, TitleFontName,
| |||
TBL_FLAG_COL_0_HEADER | TBL_FLAG_ROW_0_HEADER,
| |||
BackgroundColor, BackgroundTransparency);
| |||
}
| |||
if(PrintToLog)
| |||
{
| |||
ArrayPrint(data);
| |||
}
| |||
t.fill(data, hints); // display the data array into the table
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Chart event handler (used for sorting by columns) |
| |||
//+------------------------------------------------------------------+
| |||
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
| |||
{
| |||
if(id == CHARTEVENT_OBJECT_CLICK)
| |||
{
| |||
if(StringFind(sparam, "LEMT") == 0)
| |||
{
| |||
string parts[];
| |||
if(StringSplit(sparam, '_', parts) == 3)
| |||
{
| |||
const int s = (int)StringToInteger(parts[2]) + 1;
| |||
if(s != MathAbs(sortByColumn))
| |||
{
| |||
sortByColumn = s;
| |||
}
| |||
else
| |||
{
| |||
if(sortByColumn > 0)
| |||
{
| |||
sortByColumn = -sortByColumn; // change direction
| |||
}
| |||
else
| |||
{
| |||
sortByColumn = INT_MAX;
| |||
}
| |||
}
| |||
| |||
if(sortByColumn != INT_MAX)
| |||
{
| |||
GlobalVariableSet("LEMT_SORT", sortByColumn);
| |||
}
| |||
else
| |||
{
| |||
GlobalVariableDel("LEMT_SORT");
| |||
}
| |||
| |||
lastTime = 0;
| |||
OnTick();
| |||
}
| |||
}
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Finalization function |
| |||
//+------------------------------------------------------------------+
| |||
void OnDeinit(const int)
| |||
{
| |||
if(CheckPointer(t) == POINTER_DYNAMIC) delete t;
| |||
}
| |||
//+------------------------------------------------------------------+
|