Скачать MetaTrader 5

Электронные таблицы на MQL5

23 февраля 2011, 13:30
Nikolay Demko
9
3 118

Введение

Электронными таблицами принято называть табличные процессоры (программы по хранению и обработке данных), такие как MS Excel. И хотя код, приведенный в статье, не имеет таких мощных возможностей, он может стать базовым классом для полномасштабной реализации табличного процессора. Задача создать MS Excel с помощью MQL5 не ставилась, я лишь хотел реализовать класс для оперирования разнотипными данными в двумерном массиве.

И хотя реализованный мной класс не может состязаться по скорости с двумерным массивом однотипных данных (с прямым доступом к данным), класс все же получился удобным в обращении. К тому же, данный класс можно рассматривать как реализацию класса Variant в C++ как частный случай вырожденной таблицы до одного столбца.

Для особо нетерпеливых и тех, кто не хочет разбираться в особенностях алгоритма реализации, начну описание класса CTable с описания доступных методов.

1. Описание методов класса

Для начала рассмотрим доступные методы класса с более подробной расшифровкой назначения и особенностей использования.

1.1. FirstResize

Разбивка таблицы, определение типов столбцов, TYPE[] - массив типа ENUM_DATATYPE, определяющий размер строки и типы ячеек.

void  FirstResize(const ENUM_DATATYPE &TYPE[]);

Метод фактически является дополнительным конструктором, имеющим параметр. Это удобно по двум причинам: во-первых, устраняет проблему передачи параметра внутрь конструктора, во-вторых, появляется возможность передать объект как параметр, и уже там сделать нужную разбивку массива. Именно эта особенность дает возможность использовать класс как класс Variant в C++.

К особенностям реализации можно отнести то, что хотя функция задает размер первого измерения и типы данных столбцов, она не требует указывать размер первого измерения в качестве параметра. Этот параметр берется от размера переданного массива TYPE.

1.2. SecondResize

Изменение количества строк до размера j.

void  SecondResize(int j);

Функция задает указанный размер сразу всем массивам второго измерения. Таким образом, можно сказать, что она добавляет в таблицу строки.

1.3. FirstSize

Метод возвращает размер первого измерения (длина строки).

int   FirstSize();

1.4. SecondSize

Метод возвращает размер второго измерения (длина столбца).

int   SecondSize();

1.5. PruningTable

Устанавливает новый размер первого измерения, изменения возможны в пределах стартового размера.

void   PruningTable(int count);

Функция не изменяет длину строки фактически, а лишь перезаписывает  значение переменной, отвечающей за хранение значения длины строки. В классе имеется еще одна переменная, хранящая фактически выделенную память, которая задается при начальной разбивке таблицы. В пределах значений этой переменной возможно виртуальное изменение размера первого измерения. Функция задумывалась для обрезки лишней части при копировании одной таблицы в другую.

1.6. CopyTable

Метод копирования одной таблицы в другую, на всю длину второго измерения:

void   CopyTable(CTable *sor);

Функция копирует одну таблицу в другую. Вызывает инициализацию принимающей таблицы. Может использоваться как дополнительный конструктор. Внутренняя структура вариантов сортировки не копируется. Копируется лишь размер исходной таблицы, типы столбцов и данные, содержащиеся в исходной таблице. В качестве параметра функция принимает указатель копируемого объекта типа CTable, передаваемый с помощью функции GetPointer

Копирование одной таблицы в другую, новая таблица создается по образцу sor.

void   CopyTable(CTable *sor,int sec_beg,int sec_end);

Перегрузка вышеописанной функции с дополнительными параметрами: sec_beg-начальная точка копирования исходной таблицы, sec_end-конечная точка копирования (не путайте с количеством копируемых данных). Оба параметра относятся ко второму измерению. Данные будут добавлены в начало принимающей таблицы. Размер принимающей таблицы будет задан как sec_end-sec_beg+1.

1.7. TypeTable

Возвращает значение type_table (типа ENUM_DATATYPE) столбца i.

ENUM_DATATYPE  TypeTable(int i)

1.8. Change

Метод Change() меняет столбцы местами.

bool   Change(int &sor0,int &sor1);

Как сказано выше, метод меняет местами столбцы (работает с первым измерением). Поскольку фактического перемещения данных не происходит, то скорость работы функции не зависит от размера второго измерения.

1.9. Insert

Метод Insert вставляет столбец в указанное место.

bool   Insert(int rec,int sor);

Аналогичная вышеописанной функции, но с той разницей, что она делает подтягивание или выдавливание остальных столбцов, в зависимости от того, куда нужно переместить указанный столбец. Параметр rec указывает, куда будет перемещен столбец, sor - откуда. 

1.10. Variant/VariantCopy

Далее идут три функции серии variant. В классе реализовано запоминание вариантов обработки таблицы.

Варианты напоминают записную книжку. Например, если вы сделали сортировку по третьему столбцу и не хотите, чтобы при последующей обработке данные были сброшены, то следует переключить вариант. Для доступа к прошлому варианту обработки данных следует вызвать функцию variant. Если последующая обработка должна основываться на результатах предыдущей, то следует скопировать варианты. По умолчанию устанавливается один вариант под номером 0.

Установка варианта (если такого варианта не существует, он будет создан, равно как и все недостающие варианты до ind) и получения действующего варианта. Метод variantcopy копирует вариант sor в вариант rec.

void   variant(int ind);
int    variant(); 
void   variantcopy(int rec,int sor);

Метод variant(int ind) переключает выбранный вариант. Производит автоматическое выделение памяти. Если заданный параметр меньше, чем какой-либо, установленный ранее, то распределение памяти не происходит.

Метод variantcopy позволяет скопировать вариант sor в вариант rec. Функция создана для упорядочивания вариантов. Она автоматически увеличивает количество вариантов, если варианта rec не существует, а так же переключается на вновь скопированный вариант.

1.11. SortTwoDimArray

Метод SortTwoDimArray сортирует таблицу по выбранному столбцу i.

void   SortTwoDimArray(int i,int beg,int end,bool mode=false);

Функция сортировки таблицы по указанному столбцу. Параметры: i-столбец, beg-начальная точка сортировки, end-конечная точка сортировки (включительно), mode-булева переменная, указывающая направление сортировки. Значение mode=true означает, что с возрастанием индексов возрастают значения (по умолчанию стоит false, так как возрастание индексов идет вниз таблицы).

1.12. QuickSearch

Метод осуществляет быстрый поиск положения элемента в массиве по значению, равного образцу element.

int   QuickSearch(int i,long element,int beg,int end,bool mode=false);

1.13. SearchFirst

Ищет первый элемент, равный образцу в сортированном массиве. Возвращает индекс первого значения, равного образцу element. Требуется указать тип ранее произведенной сортировки на этом участке (если такого элемента нет, возвращает -1).

int  SearchFirst(int i,long element,int beg,int end,bool mode=false);

1.14. SearchLast

Ищет последний элемент, равный образцу в сортированном массиве.

int   SearchLast(int i,long element,int beg,int end,bool mode=false);

1.15. SearchGreat

Ищет ближайший элемент, больший образца в сортированном массиве.

int   SearchGreat(int i,long element,int beg,int end,bool mode=false);

1.16. SearchLess

Ищет ближайший элемент, меньший образца в сортированном массиве.

int  SearchLess(int i,long element,int beg,int end,bool mode=false);

1.17. Set/Get

Функции Set и Get имеют тип void, и являются перегрузками по четырем типам данных, с которыми работает таблица. Функции распознают тип данных и при несоответствии параметра value типу столбца вместо присваивания выдают Print с предупреждением. Единственным исключением является тип string. Если входной параметр типа string, то он будет приведен к типу столбца. Это исключение сделано для более удобной передачи данных, когда нет возможности задать переменную, принимающую значение ячейки.

Методы установки значений (i-индекс первого измерения, j-индекс второго измерения).

void   Set(int i,int j,long     value); // установка значения i-той линии и j-того столбца
void   Set(int i,int j,double   value); // установка значения i-той линии и j-того столбца
void   Set(int i,int j,datetime value);// установка значения i-той линии и j-того столбца
void   Set(int i,int j,string   value); // установка значения i-той линии и j-того, столбца 

Методы получения значений (i-индекс первого измерения, j-индекс второго измерения).

   //--- получение значения
void   Get(int i,int j,long     &recipient); // получение значения i-той линии и j-того столбца
void   Get(int i,int j,double   &recipient); // получение значения i-той линии и j-того столбца
void   Get(int i,int j,datetime &recipient); // получение значения i-той линии и j-того столбца
void   Get(int i,int j,string   &recipient); // получение значения i-той линии и j-того столбца

1.19. sGet

Получить значение типа string со столбца j строки i.

string sGet(int i,int j); // возврат значения i-той линии и j-того столбца 

Единственная функция из серии Get, возвращающая значение не в параметрическую переменную, а через оператор return. Возвращает значение типа string, независимо от типа столбца.

1.20. StringDigits

При приведении типов к типу string можно использовать точность, задаваемую через функции:

void  StringDigits(int i,int digits);

для задания точности double, и

int   StringDigits(int i);

Для установки точности отображения секунд datetime передается любое значение, не равное -1. Заданное значение запоминается для столбца, и задавать его каждый раз при выводе данных не требуется. Задавать значение точности можно многократно, так как данные хранятся в оригинальных типах и преобразовываются до указанной точности лишь при выводе. Значение точности не запоминается при копировании, поэтому после копирования таблицы в новую таблицу точность столбцов в новой таблице будет соответствовать точности, задаваемой по умолчанию.


1.21. Пример использования:

#include <Table.mqh>  
ENUM_DATATYPE TYPE[7]=
  {TYPE_LONG,TYPE_LONG,TYPE_STRING,TYPE_DATETIME,TYPE_STRING,TYPE_STRING,TYPE_DOUBLE};
//     0          1          2            3              4            5            6   //7 
void OnStart()
  {
   CTable table,table1;
   table.FirstResize(TYPE);             // разбивка таблицы, определение типов столбцов   
   table.SecondResize(5);               // изменение количества строк line   

   table.Set(6,0,"321.012324568");        // присвоение данных в 6-ой столбец, 0-ю строку
   table.Insert(2,6);                   // вставить 6-ой столбец на 2-ю позицию
   table.PruningTable(3);               // обрезать таблицу до 3-х столбцов
   table.StringDigits(2,5);             // задать точность до 5-го знака после запятой
   Print("table ",table.sGet(2,0));       // печать ячейки 2-го столбца, 0-ой строки  

   table1.CopyTable(GetPointer(table));  // скопировать таблицу table в таблицу table1 целиком
   table1.StringDigits(2,8);            // задать точность до 8-го знака после запятой
   Print("table1 ",table1.sGet(2,0));     // печать ячейки 2-го столбца, 0-ой строки таблицы table1  
  }

Результат работы - вывод на печать содержания ячейки (2;0). Как можно заметить, точность скопированных данных не превышает указанной точности исходящей таблицы.

2011.02.09 14:18:37     Table Script (EURUSD,H1)        table1 321.01232000
2011.02.09 14:18:37     Table Script (EURUSD,H1)        table 321.01232

Теперь перейдем к описанию самого алгоритма.


2. Выбор модели

Существует два способа организации данных: схема связных столбцов (реализованная в данной статье), и альтернативная схема связных строк.

Благодаря способу обращения к данным через посредника (описанному в п.2), принципиальной разницы для реализации на верхнем уровне нет. Но мной была выбрана модель столбцов, так как она позволяет реализовывать методы обработки данных на низком уровне, в самих объектах хранения данных. Тогда как альтернативная схема потребует перегрузку методов оперирования данными в верхнем классе CTable. Это, в свою очередь, может затруднить доработку класса, если в этом возникнет необходимость.

При этом обе схемы имеют право на жизнь. Предложенная схема позволяет более быстро перемещать данные, а альтернативная более быстро добавлять данные (так как данные в таблицу чаще всего будут добавляться построчно) и получать строки.

Существует также способ организации таблицы как массива структур. И хотя он самый простой в реализации, у него есть существенный недостаток. Структура должна быть описана программистом. А значит, теряется возможность задать атрибуты таблицы из пользовательских настроек (без изменения кода).


3. Связывание данных в динамический массив

Для того чтобы иметь возможность связать разнотипные данные в один динамический массив, нужно решить вопрос назначения ячейкам массива разнообразных типов. Подобная задача уже решена в связных списках стандартной библиотеки. И первые мои разработки были как раз на основе стандартной библиотеки классов. Но по ходу развития проекта я понял, что мне придется слишком много править в базовом классе CObject.

Поэтому я принял решение разработать собственный класс. Для тех кто еще не разбирался со стандартной библиотекой поясню, как решается вышеописанная задача. Для решения нужно воспользоваться механизмом наследования.

class CBase
  {
public:
                     CBase(){Print(__FUNCTION__);};                    
                    ~CBase(){Print(__FUNCTION__);};
   virtual void       set(int sor){};
   virtual void       set(double sor){};
   virtual int        get(int k){return(0);};
   virtual double     get(double k){return(0);};
  };
//+------------------------------------------------------------------+
class CA: public CBase
  {
private:
   int              temp;
public:
                     CA(){Print(__FUNCTION__);};  
                    ~CA(){Print(__FUNCTION__);};
   void              set(int sor){temp=sor;};
   int               get(int k){return(temp);};
  };
//+------------------------------------------------------------------+
class CB: public CBase
  {
private:
   double            temp;
public:
                    CB(){Print(__FUNCTION__);};  
                   ~CB(){Print(__FUNCTION__);};
   void             set(double sor){temp=sor;};   
   double           get(double k){return(temp);};
  };  
//+------------------------------------------------------------------+
void OnStart()
  {
   CBase *a;   CBase *b;  
   a=new CA(); b=new CB(); 
   a.set(15);  b.set(13.3);
   Print("a=",a.get(0)," b=",b.get(0.));
   delete a;
   delete b;
  }

Графически механизм наследования можно представить как расческу:

Наследование

Если объявлено создание динамичного объекта класса потомка, то это означает, что предварительно будет вызван конструктор базового класса. Именно это свойство и дает возможность создавать объект в два этапа. Перегрузив виртуальные функции в базовом классе, появляется возможность вызова из потомков функции с разными типами параметров.

Почему не ограничится простой перегрузкой? Дело в том, что исполняемые функции довольно большие по объему, и если описать их тела в базовом классе (без использования наследования), то каждому объекту в двоичном коде будут создаваться неиспользуемые функции с полным кодом тела. А при механизме наследования создаются неиспользуемые функции-пустышки, которые занимают намного меньше памяти, чем наполненные кодом функции.


4. Операции над массивами

Второй и главный краеугольный камень, заставивший меня отказаться от использования стандартных классов - это обращение к данным. Я использую не прямое обращение к ячейке массива по индексу, а через посредника в виде массива индексов. Это в свою очередь и обуславливает меньшую скорость работы, чем при прямом обращении через переменную. Все дело в том, что переменная, указывающая индекс, работает быстрее, чем ячейка массива, которую сначала нужно найти в памяти.

Давайте рассмотрим, чем принципиально отличается сортировка одномерного и многомерного массивов. Одномерный массив до сортировки имеет беспорядочное расположение данных, а после сортировки данные упорядочены. При сортировке двумерного массива требуется, чтобы упорядоченными были не все данные, а лишь данные того столбца, по которому велась сортировка. Все строки же должны сохранить свою структуру, но располагаться должны по-новому.

Сами строки тут как раз и выступают как связные структуры, имеющие в составе разнотипные данные. Для решения такой задачи требуется не только отсортировать данные в выбранном одномерном массиве, но и сохранить структуру исходных индексов. Тогда зная, в какой строке располагалась данная ячейка, можно отобразить всю строку. Таким образом, при сортировке двумерного массива стоит задача получить массив индексов отсортированного массива, не изменяя структуры данных.

Например:

до сортировки по 2-му столбцу
4 2 3
1 5 3
3 3 6
после сортировки
1 5 3
3 3 6
4 2 3
Исходный массив при этом выглядит так:
a[0][0]= 4; a[0][1]= 2; a[0][2]= 3;
a[1][0]= 1; a[1][1]= 5; a[1][2]= 3;
a[2][0]= 3; a[2][1]= 3; a[2][2]= 6;
А массив индексов сортировки по 2-му столбцу так:
r[0]=1;
r[1]=2;
r[2]=0;
Возврат отсортированных значений производится по такой схеме:
a[r[0]][0]-> 1; a[r[0]][1]-> 5; a[r[0]][2]-> 3;
a[r[1]][0]-> 3; a[r[1]][1]-> 3; a[r[1]][2]-> 6;
a[r[2]][0]-> 4; a[r[2]][1]-> 2; a[r[2]][2]-> 3;

Таким образом, появляется возможность сортировать данные по инструменту, дате открытия ордера, прибыли и т.д.

Человечеством давно изобретено множество алгоритмов сортировки. Для данной разработки наилучшим решением будет алгоритм устойчивой сортировки.

Алгоритм Быстрой сортировки, который используется в стандартных классах, относится к алгоритмам неустойчивой сортировки. Поэтому в своем классическом исполнении он не подходит. Но даже после приведения быстрой сортировки к виду устойчивой (а это дополнительное копирование данных, перед передачей на сортировку и введения синхронного сортирования массива индексов вместе с копией данных) все равно быстрая сортировка обгоняет сортировку пузырьком (один из самых быстрых алгоритмов устойчивой сортировки). Алгоритм очень быстр, но использует рекурсию.

По этой причине при работе с массивом типа string (который требует намного больше стековой памяти) у меня используется Шейкерная сортировка.

5. Организация двумерного массива

И последний вопрос, который хотелось бы осветить, это организация динамичного двумерного массива. Для такой организации достаточно одномерному массиву сделать обертку в виде класса и вызывать объект-массив через массив указателей. Другими словами, создается массив массивов.

class CarrayInt
  {
public:
                    ~CarrayInt(){};
   int               array[];
  };
//+------------------------------------------------------------------+
class CTwoarrayInt
  {
public:
                    ~CTwoarrayInt(){};
   CarrayInt         array[];
  };
//+------------------------------------------------------------------+
void OnStart()
  {
   CTwoarrayInt two;
   two.array[0].array[0];
  }

6. Структура программы

Коды класса CTable были написаны с помощью шаблонов, описанных в статье Применение псевдошаблонов, как альтернатива шаблонов С++. Именно применение шаблонов позволило быстро создать такой большой код. Поэтому я не буду детально разбирать весь код, тем более что коды алгоритмов в большей части являются доработкой кодов стандартных классов.

Приведу лишь общую структуру класса и некоторые интересные особенности отдельных функций, проясняющих важные моменты.

Блок-схема CTable

Правая часть блок-схемы в основном занята перегруженными методами, расположенными в классах-потомках CLONGArray, CDOUBLEArray, CDATETIMEArray и CSTRINGArray. В каждом из них (в приватной области) расположен массив соответствующего типа. Как раз вокруг этих массивов и идут все ухищрения по доступу к данным. Названия методов, перечисленных выше классов, идентично публичным методам.

Базовый класс CBASEArray заполнен перегрузкой виртуальных методов и нужен лишь для объявления динамичного массива объектов CBASEArray в приватной области класса CTable. Массив указателей класса CBASEArray объявляется как динамический массив динамических объектов. В функции FirstResize() происходит окончательное конструирование объектов, выбор нужного потомка. Так же это можно сделать в функции CopyTable() так как в своем теле она вызывает FirstResize().

В классе CTable также происходит согласование методов обработки данных (расположенных в потомках класса  CBASEArray) и объекта управления индексами класса Cint2D. Все это согласование оборачивается перегруженными публичными методами. 

Часто повторяющиеся участки перегрузок в классе CTable заменены определениями,чтобы не плодить длиннющие строки:

#define _CHECK0_ Print(__FUNCTION__+"("+(string)i+","+(string)j+")");return;
#define _CHECK_ Print(__FUNCTION__+"("+(string)i+")");return(-1);
#define _FIRST_ first_data[aic[i]]
#define _PARAM0_ array_index.Ind(j),value
#define _PARAM1_ array_index.Ind(j),recipient
#define _PARAM2_ element,beg,end,array_index,mode

Таким образом, участок, имеющий более компактную форму:

int QuickSearch(int i,long element,int beg,int end,bool mode=false){if(!check_type(i,TYPE_LONG)){_CHECK_}return(_FIRST_.QuickSearch(_PARAM2_));};

будет заменен препроцессором на следующую строку:

int QuickSearch(int i,long element,int beg,int end,bool mode=false){if(!check_type(i,TYPE_LONG)){Print(__FUNCTION__+"("+(string)i+")");return(-1);} return(first_data[aic[i]].QuickSearch(element,beg,end,array_index,mode));};

В приведенном примере хорошо видно, как происходит обращение к методам обработки данных (участок внутри return).

Я уже говорил выше, что класс CTable не делает физического перемещения данных при обработке, а лишь изменяет значения в индексном объекте. Для того чтобы методы ведущие обработку имели возможность влиять на индексный объект, он передается во все функции обработки как параметр array_index.

Объект array_index хранит взаимное расположение элементов 2-го измерения. За индексацию 1-го измерения отвечает динамический массив aic[], объявленный в приватной зоне класса CTable. Это и дает возможность перемещать расположение столбцов (конечно же, не физически, а индексно).

Например, при операции Change() местами меняются лишь две ячейки памяти, отвечающие за хранение индекса столбцов. Хотя визуально, это выглядит как перемещение двух столбцов. Функции класса CTable довольно подробно документированы (в некоторых местах построчно).

Теперь перейдем к функциям классов потомков CBASEArray. Алгоритмы этих классов, по сути, являются алгоритмами, взятыми из стандартных классов.  Чтобы можно было иметь представление о них, названия я взял стандартные. Доработка заключается лишь в том, что стандартные алгоритмы возвращают прямое значение, а доработанные - опосредованное через массив индексов.

В первую очередь, доработка коснулась Быстрой сортировки. Поскольку алгоритм относится к категории неустойчивых сортировок, то перед началом сортировки делается копия данных, которая и передается алгоритму. Также было добавлено синхронное изменение объекта индексов по образцу изменения данных.

void CLONGArray::QuickSort(long &m_data[],Cint2D &index,int beg,int end,bool mode=0)

Вот этот участок кода сортировки:

...
            if(i<=j)
              {
               t=m_data[i];            it=index.Ind(i);
               m_data[i++]=m_data[j];  index.Ind(i-1,index.Ind(j));
               m_data[j]=t;            index.Ind(j,it);
               if(j==0) break;
               else     j--;
              }
...

В оригинальном алгоритме нет никакого объекта класса Cint2D. В остальных стандартных алгоритмах внесены похожие изменения. Шаблоны всех кодов я приводить не буду. Если у кого-то есть желание доработать код, он всегда может сделать шаблон, из реального кода просто заменив реальные типы шаблоном.

Для написания шаблона я использовал коды класса, работающего с типом long. В таких экономных алгоритмах разработчики стараются без необходимости лишний раз не пользоваться длинными целыми числами, если есть возможность применить int. Поэтому переменная типа long, скорее всего, является перегружаемым параметром. Вот их-то и стоит заменить на templat при использовании шаблонов.


Заключение

Надеюсь, данная статья будет хорошим подспорьем для начинающих программистов как в освоении объектного подхода, так и облегчит работу с данными. Класс CTable может стать базовым для многих еще более сложных программ. Приемы, освещенные в статье, могут лечь в основу разработки очень большого класса задач, так как являются общими подходами в работе с данными.

К тому же статья еще раз доказывает, что нападки на язык MQL5 безосновательны. Хотели тип Variant, вот он реализован средствами MQL5. При этом не потребовалось вводить изменения в стандарты и расшатывать безопасность языка. Удачи.

Прикрепленные файлы |
table.mqh (61.04 KB)
Evgeniy Trofimov
Evgeniy Trofimov | 22 мар 2012 в 11:20
Вот спасибо! Ни за что бы сам не догадался.
Dennis Kirichenko
Dennis Kirichenko | 22 окт 2012 в 11:16

Николай,есть вопрос.

Можно ли так сделать, чтобы из МТ5 записалась таблица в Excel в таком виде?

Интересны мне те 2 макроназвания столбцов ("Абсолютные данные" и "Относительные данные"), которые выделил цветом. Они объединяют 3 ячейки каждое.

Может много хочу от MQL5 в плане форматирования excel'евских ячеек. А вдруг  :-))

Nikolay Demko
Nikolay Demko | 22 окт 2012 в 13:47
denkir:

Николай,есть вопрос.

Можно ли так сделать, чтобы из МТ5 записалась таблица в Excel в таком виде?

Интересны мне те 2 макроназвания столбцов ("Абсолютные данные" и "Относительные данные"), которые выделил цветом. Они объединяют 3 ячейки каждое.

Может много хочу от MQL5 в плане форматирования excel'евских ячеек. А вдруг  :-))

Какой физический смысл вы вкладываете в слова "объединяют 3 ячейки" ?

В принципе можно но без верхних 2-х строк, типы данных обосабливаются по столбцам, так что в один столбец впихнуть стринг и дубль не выйдет, но это можно сделать при печати. Или доработать класс так чтоб он содержал заглавные строки форматируемые отдельно от таблицы.

Самое просто это создать две таблицы и объеденить их.

Dennis Kirichenko
Dennis Kirichenko | 22 окт 2012 в 14:01
Urain:

Какой физический смысл вы вкладываете в слова "объединяют 3 ячейки" ?


Вот что сейчас:



Что хотелось бы иметь уже привёл...

Andriy Sydoruk
Andriy Sydoruk | 12 июл 2014 в 21:42
Привіт, дуже виручила бібліотека з статті "Электронные таблицы на MQL5", вдячний.
Применение псевдошаблонов как альтернатива шаблонов С++ Применение псевдошаблонов как альтернатива шаблонов С++

В статье описывается прием программирования, позволяющий обойтись без механизма шаблонов, при этом сохранив стиль программирования, присущий им. Рассмотрены особенности реализации шаблонов пользовательскими методами, прилагается готовый к эксплуатации код скрипта, создающий код на основе указанных шаблонов.

Как открыть мир C# из MQL5 путем экспорта неуправляемого кода Как открыть мир C# из MQL5 путем экспорта неуправляемого кода

В данной статье я представил различные методы взаимодействия между кодом, написанным на MQL5, и управляемым кодом на C#. Также я подготовил несколько примеров маршалинга структур MQL5 для C# и примеров вызова экспортированных функций DLL в скриптах на MQL5. Приведенные примеры могут служить основой для дальнейших исследований аспектов написания DLL в управляемом коде. Эта статья также открывает двери для использования в MetaTrader 5 множества библиотек, уже реализованных на C#.

Фильтрация сигналов на основе статистических данных о корреляции цен Фильтрация сигналов на основе статистических данных о корреляции цен

Есть ли связь между поведением цены в прошлом и будущими трендами? Почему сегодня цена повторяет характер движения вчерашнего дня? Возможно ли использовать статистические данные как метод прогнозирования динамики цен? Ответ есть, и он положительный. Если вы ещё сомневаетесь, эта статья для вас. Я расскажу о том, как создать рабочий фильтр для торговой системы на MQL5, обнаружив интересную закономерность в изменении цен.

Реализация автоматического анализа волн Эллиотта на MQL5 Реализация автоматического анализа волн Эллиотта на MQL5

Одним из самых популярных методов анализа рынка является волновой анализ. Однако данный процесс является достаточно сложным, что приводит к использованию дополнительных инструментов. Одним из таких инструментов является автоматический разметчик. В данной статье рассматривается создание автоматического анализатора волн Эллиотта на языке MQL5.