Files
include/test/OLAP/HTMLcube.mqh
T
2025-02-07 19:15:50 +03:30

351 lines
11 KiB
Plaintext

//+------------------------------------------------------------------+
//| HTMLcube.mqh |
//| Copyright (c) 2019, Marketeer |
//| https://www.mql5.com/en/users/marketeer |
//| Online Analytical Processing of trading hypercubes |
//| https://www.mql5.com/ru/articles/6602 |
//| https://www.mql5.com/ru/articles/6603 |
//+------------------------------------------------------------------+
#include <Marketeer/GroupSettings.mqh>
input GroupSettings Common_Settings; // G E N E R A L S E T T I N G S
input string ReportFile = ""; // · ReportFile
input string Prefix = ""; // · Prefix
input string Suffix = ""; // · Suffix
input int TimeShift = 0; // · TimeShift
#include <Marketeer/WebDataExtractor.mqh>
#include <Marketeer/RubbArray.mqh>
#include <Marketeer/HTMLcolumns.mqh>
template<typename T>
class HTMLTradeRecord: public T // TradeRecord
{
public:
HTMLTradeRecord(
const double balance,
const long ticket,
const string symbol,
const int type,
const datetime time1,
const datetime time2,
const double price1,
const double price2,
const double lot,
const double profit,
const double commission,
const double swap)
{
set(FIELD_NUMBER, counter++);
set(FIELD_TICKET, ticket);
set(FIELD_SYMBOL, symbols.add(symbol));
set(FIELD_TYPE, type);
set(FIELD_DATETIME1, time1);
set(FIELD_DATETIME2, time2);
set(FIELD_DURATION, time2 - time1);
set(FIELD_PRICE1, (float)price1);
set(FIELD_PRICE2, (float)price2);
set(FIELD_MAGIC, 0);
magics.add(0);
set(FIELD_LOT, (float)lot);
set(FIELD_PROFIT_AMOUNT, (float)profit);
set(FIELD_PROFIT_PERCENT, (float)(profit / balance));
set(FIELD_PROFIT_POINT, (float)((type == OP_BUY ? +1 : -1) * (price2 - price1) / SymbolInfoDouble(symbol, SYMBOL_POINT)));
set(FIELD_COMMISSION, (float)commission);
set(FIELD_SWAP, (float)swap);
fillCustomFields(); // calls implementation from T
}
};
template<typename T>
class HTMLReportAdapter: public DataAdapter
{
private:
class Deal // if MQL5 could respect private access specifier for classes,
{ // Trades will be unreachable from outer world, so it would be fine to have
public: // fields made public for direct access from Processor only
datetime time;
double price;
int type; // +1 - buy, -1 - sell
int direction; // +1 - in, -1 - out, 0 - in/out
double volume;
double profit;
long deal;
long order;
string comment;
string symbol;
double commission;
double swap;
public:
Deal(const IndexMap *row) // this is MT5 deal
{
time = StringToTime(row[COLUMN_TIME].get<string>()) + TimeShift;
price = StringToDouble(row[COLUMN_PRICE].get<string>());
string t = row[COLUMN_TYPE].get<string>();
type = t == "buy" ? +1 : (t == "sell" ? -1 : 0);
t = row[COLUMN_DIRECTION].get<string>();
direction = 0;
if(StringFind(t, "in") > -1) ++direction;
if(StringFind(t, "out") > -1) --direction;
volume = StringToDouble(row[COLUMN_VOLUME].get<string>());
t = row[COLUMN_PROFIT].get<string>();
StringReplace(t, " ", "");
profit = StringToDouble(t);
deal = StringToInteger(row[COLUMN_DEAL].get<string>());
order = StringToInteger(row[COLUMN_ORDER].get<string>());
comment = row[COLUMN_COMMENT].get<string>();
symbol = row[COLUMN_SYMBOL].get<string>();
commission = StringToDouble(row[COLUMN_COMISSION].get<string>());
swap = StringToDouble(row[COLUMN_SWAP].get<string>());
}
bool isIn() const
{
return direction >= 0;
}
bool isOut() const
{
return direction <= 0;
}
bool isOpposite(const Deal *t) const
{
return type * t.type < 0;
}
bool isActive() const
{
return volume > 0;
}
int op_type() const
{
if(type == +1) return OP_BUY;
else if(type == -1) return OP_SELL;
return OP_BALANCE;
}
};
RubbArray<Deal *> array;
RubbArray<Deal *> queue;
int size;
int cursor;
double balance;
IndexMap *data;
RubbArray<HTMLTradeRecord<T> *> trades;
protected:
int generate()
{
array.clear();
balance = 0;
for(int i = 0; i < data.getSize(); ++i)
{
IndexMap *row = data[i];
if(CheckPointer(row) == POINTER_INVALID || row.getSize() != COLUMNS_COUNT) return 0; // something is broken
string s = row[COLUMN_SYMBOL].get<string>();
StringTrimLeft(s);
if(StringLen(s) > 0)
{
array << new Deal(row);
}
else if(row[COLUMN_TYPE].get<string>() == "balance")
{
string t = row[COLUMN_PROFIT].get<string>();
StringReplace(t, " ", "");
balance += StringToDouble(t);
}
}
if(balance == 0) balance = 10000; // default, if missing
int count = 0;
// abstract:
// if direction <= 0
// collect all Trades from the queue which have direction >= 0 and opposite type
// if this volume is greater than collected volumes
// reduce volume in this Deal by the total volume of collected Trades
// else if collected volumes are greater than this volume
// reduce volume in matched Trades in a loop until all volume of this Deal is exhausted
// create object-lines from all affected Trades to this Deal
// 'delete' all affected Trades with zero volume from queue
// if volume == 0, 'delete' this Deal (disactivate)
// if direction >= 0 push the new Deal object to the queue
for(int i = 0; i < array.size(); ++i)
{
Deal *current = array[i];
if(!current.isActive()) continue;
string real = TradeRecord::realsymbol(current.symbol);
if(real == NULL) continue;
if(current.isOut())
{
// first try to find exact match
for(int j = 0; j < queue.size(); ++j)
{
if(queue[j].isIn() && queue[j].isOpposite(current) && queue[j].volume == current.volume && queue[j].symbol == current.symbol)
{
trades << new HTMLTradeRecord<T>(
balance,
queue[j].deal,
real, // current.symbol,
queue[j].op_type(),
queue[j].time,
current.time,
queue[j].price,
current.price,
current.volume,
current.profit,
queue[j].commission + current.commission,
current.swap);
balance += current.profit;
current.volume = 0;
queue >> j; // remove from queue
++count;
break;
}
}
if(!current.isActive()) continue;
// second try to perform partial close
for(int j = 0; j < queue.size(); ++j)
{
if(queue[j].isIn() && queue[j].isOpposite(current) && queue[j].symbol == current.symbol)
{
if(current.volume >= queue[j].volume)
{
double fraction = queue[j].volume / current.volume;
trades << new HTMLTradeRecord<T>(
balance,
queue[j].deal,
real, // current.symbol,
queue[j].op_type(),
queue[j].time,
current.time,
queue[j].price,
current.price,
queue[j].volume,
current.profit * fraction,
queue[j].commission + current.commission * fraction,
current.swap * fraction);
balance += current.profit * fraction;
current.volume -= queue[j].volume;
queue[j].volume = 0;
++count;
}
else
{
double fraction = current.volume / queue[j].volume;
trades << new HTMLTradeRecord<T>(
balance,
queue[j].deal,
real, // current.symbol,
queue[j].op_type(),
queue[j].time,
current.time,
queue[j].price,
current.price,
current.volume,
queue[j].profit * fraction, // should be 0
queue[j].commission * fraction + current.commission,
current.swap);
balance += queue[j].profit * fraction;
queue[j].volume -= current.volume;
current.volume = 0;
++count;
break;
}
}
}
// purge all inactive from queue
for(int j = queue.size() - 1; j >= 0; --j)
{
if(!queue[j].isActive())
{
queue >> j;
}
}
}
if(current.isActive()) // is _still_ active
{
if(current.isIn())
{
queue << current;
}
}
}
return count;
}
void reset()
{
cursor = 0;
balance = 0;
if(CheckPointer(data) == POINTER_DYNAMIC) delete data;
}
public:
HTMLReportAdapter()
{
reset();
TradeRecord::reset();
}
~HTMLReportAdapter()
{
if(CheckPointer(data) == POINTER_DYNAMIC) delete data;
((BaseArray<Deal *> *)&queue).clear();
}
bool load(const string file)
{
reset();
data = HTMLConverter::convertReport2Map(file, true);
if(data != NULL)
{
size = generate();
Print(data.getSize(), " deals transferred to ", size, " trades");
}
return data != NULL;
}
virtual int reservedSize() override
{
return size;
}
virtual Record *getNext() override
{
if(cursor < size)
{
return trades[cursor++];
}
return NULL;
}
};