559 lines
30 KiB
Plaintext
559 lines
30 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| OLAPGUI.mqh |
|
|
//| Copyright (c) 2019-2020, Marketeer |
|
|
//| https://www.mql5.com/en/users/marketeer |
|
|
//| Online Analytical Processing of trading hypercubes |
|
|
//| https://www.mql5.com/en/articles/6602 |
|
|
//| https://www.mql5.com/en/articles/6603 |
|
|
//| https://www.mql5.com/en/articles/7656 |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "Copyright (c) 2019-2020, Marketeer"
|
|
#property link "https://www.mql5.com/en/users/marketeer"
|
|
|
|
#include <Controls\Dialog.mqh>
|
|
#include <Controls\Button.mqh>
|
|
#include <Controls\Edit.mqh>
|
|
#include <Controls\ListView.mqh>
|
|
#include <Controls\ComboBox.mqh>
|
|
#include <Layouts/Box.mqh>
|
|
#include <Layouts/ComboBoxResizable.mqh>
|
|
#include <PairPlot/Plot.mqh>
|
|
#include <Layouts/MaximizableAppDialog.mqh>
|
|
#include <OLAP/OLAPCommon.mqh>
|
|
|
|
#define BUTTON_WIDTH (100)
|
|
#define BUTTON_HEIGHT (20)
|
|
|
|
#define EDIT_HEIGHT (20)
|
|
|
|
#define GROUP_WIDTH (150)
|
|
#define LIST_HEIGHT (179)
|
|
#define RADIO_HEIGHT (56)
|
|
#define CHECK_HEIGHT (93)
|
|
|
|
#define AXES_NUMBER 3
|
|
|
|
#define ALGO_NUMBER 4
|
|
#define ALGO_AGGREGATOR 0
|
|
#define ALGO_FIELD 1
|
|
#define ALGO_SORTING 2
|
|
#define ALGO_GRAPHTYPE 3
|
|
|
|
|
|
class OLAPDialogBase: public MaximizableAppDialog
|
|
{
|
|
protected:
|
|
string selectors[/*MAX_AXES_CHOICES*/];
|
|
string settings[/*MAX_ALGO_CHOICES*/][ALGO_NUMBER];
|
|
int defaults[ALGO_NUMBER];
|
|
|
|
CBox m_main;
|
|
|
|
CBox m_row_1;
|
|
ComboBoxResizable m_axis[AXES_NUMBER];
|
|
CButton m_button_ok;
|
|
|
|
CBox m_row_2;
|
|
ComboBoxResizable m_algo[ALGO_NUMBER]; // aggregator, field, graph type, sort by
|
|
|
|
CBox m_row_plot;
|
|
CPlot m_plot;
|
|
|
|
ENUM_CURVE_TYPE curveType;
|
|
|
|
bool processing;
|
|
|
|
// 3-d dimension handling
|
|
bool browsing;
|
|
int currentZ;
|
|
int maxZ;
|
|
string titlesZ[];
|
|
void validateZ();
|
|
|
|
int customFieldCount;
|
|
string customFields[];
|
|
|
|
public:
|
|
virtual bool Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2) override;
|
|
virtual bool OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) override;
|
|
virtual int process() = 0;
|
|
virtual void setup() = 0;
|
|
|
|
virtual void setCustomFields(const DataAdapter &adapter)
|
|
{
|
|
string names[];
|
|
if(adapter.getCustomFields(names) > 0)
|
|
{
|
|
customFieldCount = ArrayCopy(customFields, names);
|
|
}
|
|
}
|
|
|
|
// data callbacks
|
|
void accept1D(const PairArray *data, const string title);
|
|
void accept2D(const double &x[], const double &y[], const string title);
|
|
void finalize();
|
|
void registerZ(const string &titles[]);
|
|
int getCurrentZ() const
|
|
{
|
|
return currentZ;
|
|
}
|
|
|
|
protected:
|
|
CWnd *CreateAxisComboBox(const int i);
|
|
CWnd *CreateAlgoComboBox(const int i);
|
|
CWnd *CreatePlot(const long chart, const string name, const int subwin);
|
|
CWnd *CreateButton(const long chart, const string name, const int subwin);
|
|
|
|
bool OnChangeComboBoxAxis(const int i);
|
|
bool OnChangeComboBoxAlgo(const int i);
|
|
void OnClickButton(void);
|
|
|
|
virtual CWnd *CreateMain(const long chart, const string name, const int subwin);
|
|
virtual CWnd *CreateAxesRow(const long chart, const string name, const int subwin);
|
|
virtual CWnd *CreateAlgoRow(const long chart, const string name, const int subwin);
|
|
virtual CWnd *CreatePlotRow(const long chart, const string name, const int subwin);
|
|
|
|
virtual void SelfAdjustment(const bool restore = false) override;
|
|
|
|
};
|
|
|
|
// since MQL5 does not support multiple inheritence we need this delegate object
|
|
class OLAPDisplay: public Display
|
|
{
|
|
private:
|
|
OLAPDialogBase *parent;
|
|
bool printout;
|
|
|
|
public:
|
|
OLAPDisplay(OLAPDialogBase *ptr, const bool printLog = true): parent(ptr), printout(printLog) {}
|
|
void setPrintLog(const bool printLog) {printout = printLog;}
|
|
virtual void display(MetaCube *metaData, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override;
|
|
};
|
|
|
|
|
|
EVENT_MAP_BEGIN(OLAPDialogBase)
|
|
ON_INDEXED_EVENT(ON_CHANGE, m_axis, OnChangeComboBoxAxis)
|
|
ON_INDEXED_EVENT(ON_CHANGE, m_algo, OnChangeComboBoxAlgo)
|
|
ON_EVENT(ON_CLICK, m_button_ok, OnClickButton)
|
|
EVENT_MAP_END(MaximizableAppDialog)
|
|
|
|
|
|
bool OLAPDialogBase::Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
|
|
{
|
|
setup();
|
|
|
|
const int maxw = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
|
|
const int maxh = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
|
|
int _x1 = x1;
|
|
int _y1 = y1;
|
|
int _x2 = x2;
|
|
int _y2 = y2;
|
|
if(x2 - x1 > maxw || x2 > maxw)
|
|
{
|
|
_x1 = 0;
|
|
_x2 = _x1 + maxw - 0;
|
|
}
|
|
if(y2 - y1 > maxh || y2 > maxh)
|
|
{
|
|
_y1 = 0;
|
|
_y2 = _y1 + maxh - 1;
|
|
}
|
|
|
|
if(MaximizableAppDialog::Create(chart, name, subwin, _x1, _y1, _x2, _y2)
|
|
&& Add(CreateMain(chart, name, subwin))
|
|
&& m_main.Pack())
|
|
{
|
|
SelfAdjustment();
|
|
m_axis[1].Disable();
|
|
m_axis[2].Disable();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
CWnd *OLAPDialogBase::CreateMain(const long chart, const string name, const int subwin)
|
|
{
|
|
m_main.LayoutStyle(LAYOUT_STYLE_VERTICAL);
|
|
|
|
if(m_main.Create(chart, name + "main", subwin, 0, 0, ClientAreaWidth(), ClientAreaHeight())
|
|
&& m_main.Add(CreateAxesRow(chart, name, subwin))
|
|
&& m_main.Add(CreateAlgoRow(chart, name, subwin))
|
|
&& m_main.Add(CreatePlotRow(chart, name, subwin)))
|
|
{
|
|
return &m_main;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
CWnd *OLAPDialogBase::CreateAxesRow(const long chart, const string name, const int subwin)
|
|
{
|
|
if(m_row_1.Create(chart, name + "axesrow", subwin, 0, 0, ClientAreaWidth(), BUTTON_HEIGHT * 1.5))
|
|
{
|
|
m_row_1.Alignment(WND_ALIGN_LEFT|WND_ALIGN_RIGHT, 2, 0, 2, 0);
|
|
for(int i = 0; i < AXES_NUMBER; i++)
|
|
{
|
|
if(!m_row_1.Add(CreateAxisComboBox(i))) return NULL;
|
|
}
|
|
if(!m_row_1.Add(CreateButton(chart, name, subwin))) return NULL;
|
|
return &m_row_1;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
CWnd *OLAPDialogBase::CreateAxisComboBox(const int x)
|
|
{
|
|
if(m_axis[x].Create(m_chart_id, m_name + "axis" + (string)x, m_subwin, 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT))
|
|
{
|
|
for(int i = 0; i < ArraySize(selectors); i++)
|
|
{
|
|
string prefix = i == 0 ? CharToString((uchar)('X' + x)) + ": " : "";
|
|
if(!m_axis[x].ItemAdd(prefix + selectors[i]))
|
|
return NULL;
|
|
}
|
|
m_axis[x].Select(0);
|
|
return &m_axis[x];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
CWnd *OLAPDialogBase::CreateButton(const long chart, const string name, const int subwin)
|
|
{
|
|
if(m_button_ok.Create(chart, name + "ok", subwin, 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT)
|
|
&& m_button_ok.Text("Process"))
|
|
{
|
|
return &m_button_ok;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
CWnd *OLAPDialogBase::CreateAlgoRow(const long chart, const string name, const int subwin)
|
|
{
|
|
if(m_row_2.Create(chart, name + "algorow", subwin, 0, 0, ClientAreaWidth(), BUTTON_HEIGHT * 1.5))
|
|
{
|
|
m_row_2.Alignment(WND_ALIGN_LEFT|WND_ALIGN_RIGHT, 2, 0, 2, 0);
|
|
for(int i = 0; i < ALGO_NUMBER; i++)
|
|
{
|
|
if(!m_row_2.Add(CreateAlgoComboBox(i))) return NULL;
|
|
}
|
|
return &m_row_2;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
CWnd *OLAPDialogBase::CreateAlgoComboBox(const int x)
|
|
{
|
|
if(m_algo[x].Create(m_chart_id, m_name + "algo" + (string)x, m_subwin, 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT))
|
|
{
|
|
for(int i = 0; i < ArrayRange(settings, 0); i++)
|
|
{
|
|
if(settings[i][x] != NULL)
|
|
{
|
|
if(!m_algo[x].ItemAdd(settings[i][x])) return NULL;
|
|
}
|
|
}
|
|
m_algo[x].Select(defaults[x]);
|
|
return &m_algo[x];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
CWnd *OLAPDialogBase::CreatePlotRow(const long chart, const string name, const int subwin)
|
|
{
|
|
m_row_plot.PaddingLeft(0);
|
|
m_row_plot.PaddingRight(0);
|
|
const int h = m_row_1.Height() + m_row_2.Height() + CONTROLS_BORDER_WIDTH * 4;
|
|
m_row_plot.Alignment(WND_ALIGN_CLIENT, CONTROLS_BORDER_WIDTH * 2, h, CONTROLS_BORDER_WIDTH * 2, CONTROLS_BORDER_WIDTH * 2);
|
|
|
|
if(m_row_plot.Create(chart, name + "plotrow", subwin, 0, 0, ClientAreaWidth(), LIST_HEIGHT)
|
|
&& m_row_plot.Add(CreatePlot(chart, name, subwin)))
|
|
{
|
|
return &m_row_plot;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
CWnd *OLAPDialogBase::CreatePlot(const long chart, const string name, const int subwin)
|
|
{
|
|
if(m_plot.Create(chart, name + "Plot", subwin, 0, 0, ClientAreaWidth(), ClientAreaHeight(), curveType))
|
|
{
|
|
return &m_plot;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
bool OLAPDialogBase::OnChangeComboBoxAxis(const int x)
|
|
{
|
|
if(browsing)
|
|
{
|
|
maxZ = 0;
|
|
validateZ();
|
|
}
|
|
|
|
int selection = (int)m_axis[x].Value();
|
|
for(int i = x + 1; i < AXES_NUMBER; i++)
|
|
{
|
|
if(selection > 0)
|
|
{
|
|
m_axis[i].Enable();
|
|
}
|
|
else
|
|
{
|
|
m_axis[i].Disable();
|
|
}
|
|
selection = (int)MathMin(selection, m_axis[i].Value());
|
|
}
|
|
|
|
/* Sorting is now enabled for ever, but sorting by _values_ for multidimensional data
|
|
is not coherent (each curve is sorted on its own, so X marks mean nothing)
|
|
int count = 0;
|
|
for(int i = 0; i < AXES_NUMBER; i++)
|
|
{
|
|
count += ((int)m_axis[i].Value() > 0 ? 1 : 0) * (i + 1);
|
|
}
|
|
if(count == 1) m_algo[2].Enable(); // sorting
|
|
else m_algo[2].Disable();
|
|
*/
|
|
return true;
|
|
}
|
|
|
|
bool OLAPDialogBase::OnChangeComboBoxAlgo(const int i)
|
|
{
|
|
if(browsing && i < 3)
|
|
{
|
|
maxZ = 0;
|
|
validateZ();
|
|
}
|
|
|
|
if(i == ALGO_GRAPHTYPE)
|
|
{
|
|
curveType = (ENUM_CURVE_TYPE)(m_algo[i].Value());
|
|
m_plot.SetDefaultCurveType(curveType);
|
|
m_plot.Refresh(true);
|
|
}
|
|
else
|
|
if(i == ALGO_AGGREGATOR)
|
|
{
|
|
AGGREGATORS a = (AGGREGATORS)(m_algo[i].Value());
|
|
|
|
if(a == AGGREGATOR_IDENTITY || a == AGGREGATOR_COUNT)
|
|
{
|
|
m_algo[1].Disable();
|
|
}
|
|
else
|
|
{
|
|
m_algo[1].Enable();
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void OLAPDialogBase::OnClickButton(void)
|
|
{
|
|
if(processing) return;
|
|
|
|
if(browsing)
|
|
{
|
|
currentZ = (currentZ + 1) % maxZ;
|
|
validateZ();
|
|
}
|
|
|
|
processing = true;
|
|
const int n = process();
|
|
if(n == 0 && processing)
|
|
{
|
|
finalize();
|
|
}
|
|
}
|
|
|
|
void OLAPDialogBase::SelfAdjustment(const bool restore = false)
|
|
{
|
|
CSize min = m_main.GetMinSize();
|
|
CSize size;
|
|
size.cx = ClientAreaWidth();
|
|
size.cy = ClientAreaHeight();
|
|
if(restore)
|
|
{
|
|
if(min.cx > size.cx) size.cx = min.cx;
|
|
if(min.cy > size.cy) size.cy = min.cy;
|
|
}
|
|
m_main.Size(size);
|
|
int w = (m_row_1.Width() - 2 * 2 * 2 * 3) / 4;
|
|
for(int i = 0; i < AXES_NUMBER; i++)
|
|
{
|
|
m_axis[i].Width(w);
|
|
}
|
|
m_button_ok.Width(w);
|
|
for(int i = 0; i < ALGO_NUMBER; i++)
|
|
{
|
|
m_algo[i].Width(w);
|
|
}
|
|
|
|
if(!restore)
|
|
{
|
|
m_plot.Size(size.cx - CONTROLS_BORDER_WIDTH * 16, size.cy - m_row_1.Height() - m_row_2.Height() - CONTROLS_BORDER_WIDTH * 16);
|
|
m_plot.Resize(0, 0, size.cx - CONTROLS_BORDER_WIDTH * 16, size.cy - m_row_1.Height() - m_row_2.Height() - CONTROLS_BORDER_WIDTH * 16);
|
|
}
|
|
|
|
m_main.Pack();
|
|
m_plot.Refresh();
|
|
}
|
|
|
|
void OLAPDisplay::display(MetaCube *metaData, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override
|
|
{
|
|
int consts[];
|
|
const int selectorCount = metaData.getDimension();
|
|
ArrayResize(consts, selectorCount);
|
|
ArrayInitialize(consts, 0);
|
|
|
|
if(selectorCount == 1)
|
|
{
|
|
PairArray *result;
|
|
if(metaData.getVector(0, consts, result, sortby))
|
|
{
|
|
string title = metaData.getDimensionTitle(0);
|
|
if(printout)
|
|
{
|
|
Print("===== " + title + " =====");
|
|
ArrayPrint(result.array);
|
|
}
|
|
parent.accept1D(result, title);
|
|
}
|
|
parent.finalize();
|
|
return;
|
|
}
|
|
|
|
int dimensions[];
|
|
ArrayResize(dimensions, selectorCount);
|
|
for(int i = 0; i < selectorCount; i++)
|
|
{
|
|
dimensions[i] = metaData.getDimensionRange(i);
|
|
}
|
|
|
|
if(selectorCount == 3)
|
|
{
|
|
PairArray *result;
|
|
consts[0] = 0;
|
|
consts[1] = 0;
|
|
string s[];
|
|
if(metaData.getVector(2, consts, result, SORT_BY_LABEL_ASCENDING))
|
|
{
|
|
result.convert(s);
|
|
parent.registerZ(s);
|
|
delete result;
|
|
}
|
|
|
|
ArrayInitialize(consts, 0);
|
|
}
|
|
|
|
if(selectorCount >= 2)
|
|
{
|
|
int z = parent.getCurrentZ();
|
|
if((dimensions[1] == 2) && identity)
|
|
{
|
|
double x[];
|
|
double y[];
|
|
PairArray *result;
|
|
consts[1] = 0;
|
|
if(selectorCount == 3) consts[2] = z; // this has no effect yet because of identity
|
|
if(metaData.getVector(0, consts, result, sortby)) // SORT_BY_NONE
|
|
{
|
|
if(printout)
|
|
{
|
|
Print("===== " + metaData.getDimensionTitle(-(1)) + " =====");
|
|
ArrayPrint(result.array);
|
|
}
|
|
result.convert(x);
|
|
delete result;
|
|
}
|
|
consts[1] = 1;
|
|
if(selectorCount == 3) consts[2] = z; // this has no effect yet because of identity
|
|
if(metaData.getVector(0, consts, result, sortby)) // SORT_BY_NONE
|
|
{
|
|
if(printout)
|
|
{
|
|
Print("===== " + metaData.getDimensionTitle(-(2)) + " =====");
|
|
ArrayPrint(result.array);
|
|
}
|
|
result.convert(y);
|
|
delete result;
|
|
}
|
|
parent.accept2D(x, y, metaData.getDimensionTitle(-(1)) + " vs " + metaData.getDimensionTitle(-(2)));
|
|
}
|
|
else
|
|
{
|
|
// NB: identity aggregator is always 2D cube: [record number][selected fields number]
|
|
// we need special handling for it
|
|
if(selectorCount == 3)
|
|
{
|
|
consts[2] = z;
|
|
}
|
|
for(int j = 0; j < dimensions[1]; j++)
|
|
{
|
|
consts[1] = j;
|
|
PairArray *result;
|
|
if(metaData.getVector(0, consts, result, sortby)) // SORT_BY_NONE - only labels sorting is valid!
|
|
{
|
|
string title = NULL;
|
|
if(identity)
|
|
{
|
|
title = metaData.getDimensionTitle(-(j + 1));
|
|
}
|
|
if(printout)
|
|
{
|
|
Print("===== " + (identity ? title : metaData.getDimensionIndexLabel(1, j)) + " =====");
|
|
ArrayPrint(result.array);
|
|
}
|
|
parent.accept1D(result, (identity ? title : metaData.getDimensionIndexLabel(1, j)));
|
|
}
|
|
}
|
|
}
|
|
parent.finalize();
|
|
}
|
|
}
|
|
|
|
|
|
void OLAPDialogBase::registerZ(const string &titles[])
|
|
{
|
|
if(!browsing)
|
|
{
|
|
maxZ = ArraySize(titles);
|
|
ArrayResize(titlesZ, maxZ); // shrink if necessary
|
|
ArrayCopy(titlesZ, titles);
|
|
currentZ = 0;
|
|
}
|
|
}
|
|
|
|
void OLAPDialogBase::validateZ()
|
|
{
|
|
if(maxZ > 0)
|
|
{
|
|
string switcher = StringFormat("%d/%d %s >>", currentZ + 1, maxZ, titlesZ[currentZ]);
|
|
Print("Current Z: ", switcher);
|
|
m_button_ok.Text(switcher);
|
|
browsing = true;
|
|
}
|
|
else
|
|
{
|
|
m_button_ok.Text("Process");
|
|
browsing = false;
|
|
}
|
|
}
|
|
|
|
void OLAPDialogBase::accept1D(const PairArray *data, const string title)
|
|
{
|
|
m_plot.CurveAdd(data, title);
|
|
}
|
|
|
|
void OLAPDialogBase::accept2D(const double &x[], const double &y[], const string title)
|
|
{
|
|
m_plot.CurveAdd(x, y, title);
|
|
}
|
|
|
|
void OLAPDialogBase::finalize()
|
|
{
|
|
m_plot.Refresh();
|
|
processing = false;
|
|
m_button_ok.Text("Process");
|
|
validateZ();
|
|
}
|