416 lines
14 KiB
MQL5
416 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;
|
||
|
}
|
||
|
//+------------------------------------------------------------------+
|