//+------------------------------------------------------------------+ //| 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 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 #include #include template 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 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()) + TimeShift; price = StringToDouble(row[COLUMN_PRICE].get()); string t = row[COLUMN_TYPE].get(); type = t == "buy" ? +1 : (t == "sell" ? -1 : 0); t = row[COLUMN_DIRECTION].get(); direction = 0; if(StringFind(t, "in") > -1) ++direction; if(StringFind(t, "out") > -1) --direction; volume = StringToDouble(row[COLUMN_VOLUME].get()); t = row[COLUMN_PROFIT].get(); StringReplace(t, " ", ""); profit = StringToDouble(t); deal = StringToInteger(row[COLUMN_DEAL].get()); order = StringToInteger(row[COLUMN_ORDER].get()); comment = row[COLUMN_COMMENT].get(); symbol = row[COLUMN_SYMBOL].get(); commission = StringToDouble(row[COLUMN_COMISSION].get()); swap = StringToDouble(row[COLUMN_SWAP].get()); } 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 array; RubbArray queue; int size; int cursor; double balance; IndexMap *data; RubbArray *> 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(); StringTrimLeft(s); if(StringLen(s) > 0) { array << new Deal(row); } else if(row[COLUMN_TYPE].get() == "balance") { string t = row[COLUMN_PROFIT].get(); 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( 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( 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( 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 *)&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; } };