//+------------------------------------------------------------------+ //| TicketsTable.mqh | //| Copyright 2026, Niquel Mendoza. | //| https://www.mql5.com/ | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Niquel Mendoza." #property link "https://www.mql5.com/" #property strict #ifndef MQLARTICLES_RM_TICKETS_TABLE_MQH #define MQLARTICLES_RM_TICKETS_TABLE_MQH //+------------------------------------------------------------------+ //| Include | //+------------------------------------------------------------------+ #include "RM_Functions.mqh" //+------------------------------------------------------------------+ //| Defines | //+------------------------------------------------------------------+ Position EMPTY_POSITION; //--- Defines // Numero maximo de posiciones abiertas al mismo tiempo #ifndef MQLARTICLES_RM_ACCOUNT_MAX_POSITIONS #define MQLARTICLES_RM_ACCOUNT_MAX_POSITIONS 512 #endif // MQLARTICLES_RM_ACCOUNT_MAX_POSITIONS // Numero de hash en la tabla #ifndef MQLARTICLES_HASH_TABLE_SIZE #define MQLARTICLES_HASH_TABLE_SIZE 1024 #endif // MQLARTICLES_HASH_TABLE_SIZE #define MQLARTICLES_HASH_TABLE_INVALID_INDEX 65535 //--- Descomente para activar mas logs //#define MQLARTICLES_TICKETSINFO_DEBUG //--- // Notas: // 1. Hash table alamcena posiciones en m_positions //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ struct CTicketsInfo { private: //--- Data Position m_positions[MQLARTICLES_RM_ACCOUNT_MAX_POSITIONS]; //--- Hash (una sola tabla) uint16_t m_hash_table[MQLARTICLES_HASH_TABLE_SIZE]; //--- uint16_t m_count; //--- Internal functions bool InsertHash(const ulong ticket, const uint16_t pos_idx); bool RemoveHash(const ulong ticket); void UpdateHashAfterSwap(const ulong ticket, const uint16_t new_pos_idx); public: CTicketsInfo(void); // ~CTicketsInfo(void); //--- int16_t FindHash(const ulong ticket) const; //--- Data // Get bool GetByTicket(const ulong ticket, Position& pos, int16_t& out_idx) const; bool GetByTicket(const ulong ticket, Position& pos) const; inline Position GetByTicket(const ulong ticket) const; __forceinline Position GetPositionByIndex(const int16_t index) const { return m_positions[index]; } __forceinline ulong GetPositionMagicByIndex(const int16_t index) const { return m_positions[index].magic; } // Delete bool DeleteByIndex(const int16_t pos_idx); bool DeleteByTicket(const ulong ticket); bool DeleteByTicket(const ulong ticket, Position &position); // Add bool Add(const Position &p); // Update inline void UpdatePositionPartial(const double net, const double close_volume, const int16_t pos_idx); inline void UpdateTp(const double new_tp, const ulong ticket); inline void UpdateSl(const double new_sl, const ulong ticket); //--- General Getters // Index inline bool IsValidIdx(const int16_t index) const; // Total __forceinline int16_t Total() const { return (int16_t)m_count; } __forceinline uint16_t TotalU() const { return m_count; } // Capacity inline bool IsFull() const { return m_count >= MQLARTICLES_RM_ACCOUNT_MAX_POSITIONS; } inline uint16_t Capacity() const { return MQLARTICLES_RM_ACCOUNT_MAX_POSITIONS; } //--- Exist inline bool Exists(const ulong ticket) const; //--- Free void Free(); //--- Utilidades void CloseAllPositionMask(const int& index_arr[], const int size_arr, CTrade& trade, const uint flags) const; int GetPositionsMask(const int& index_arr[], const int size_arr, const uint flags) const; double GetProfitMask(const int &index_arr[], const int size_arr) const; void GetPositionsFromMagic(const ulong magig, int& arr[], int& size_arr) const; }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CTicketsInfo::CTicketsInfo(void) : m_count(0) { ArrayInitialize(m_hash_table, MQLARTICLES_HASH_TABLE_INVALID_INDEX); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ inline bool CTicketsInfo::IsValidIdx(const int16_t index) const { if(index >= 0 && index < Total()) return true; return false; } //+------------------------------------------------------------------+ int16_t CTicketsInfo::FindHash(const ulong ticket) const { //--- const uint16_t h = (uint16_t)(ticket % MQLARTICLES_HASH_TABLE_SIZE); //--- for(uint16_t i = 0; i < MQLARTICLES_HASH_TABLE_SIZE; i++) { //--- const uint16_t idx = (h + i) % MQLARTICLES_HASH_TABLE_SIZE; const uint16_t pos_idx = m_hash_table[idx]; //--- if(pos_idx == MQLARTICLES_HASH_TABLE_INVALID_INDEX) { #ifdef MQLARTICLES_TICKETSINFO_DEBUG FastLog(FUNCION_ACTUAL, CAUTION_TEXT, StringFormat("El slot del ticket %I64u esta vacio", ticket)); #endif // MQLARTICLES_TICKETSINFO_DEBUG return -1; } //--- if(pos_idx < m_count && m_positions[pos_idx].ticket == ticket) { #ifdef MQLARTICLES_TICKETSINFO_DEBUG FastLog(FUNCION_ACTUAL, INFO_TEXT, StringFormat("Ticket %I64u encontrado en idx=%u, pos_idx=%u", ticket, idx, pos_idx)); #endif // MQLARTICLES_TICKETSINFO_DEBUG return (int16_t)pos_idx; } //--- if(pos_idx >= m_count) { FastLog(FUNCION_ACTUAL, ERROR_TEXT, StringFormat("Indice inválido pos_idx=%u para ticket %I64u en idx=%u", pos_idx, ticket, idx)); return -1; } } //--- #ifdef MQLARTICLES_TICKETSINFO_DEBUG FastLog(FUNCION_ACTUAL, WARNING_TEXT, StringFormat("El ticket %I64u no se ha encontrado tras recorrer toda la tabla", ticket)); #endif // MQLARTICLES_TICKETSINFO_DEBUG return -1; } //+------------------------------------------------------------------+ bool CTicketsInfo::InsertHash(const ulong ticket, const uint16_t pos_idx) { //--- const uint16_t h = (uint16_t)(ticket % MQLARTICLES_HASH_TABLE_SIZE); for(uint16_t i = 0; i < MQLARTICLES_HASH_TABLE_SIZE; i++) { const uint16_t idx = (h + i) % MQLARTICLES_HASH_TABLE_SIZE; if(m_hash_table[idx] == MQLARTICLES_HASH_TABLE_INVALID_INDEX) { m_hash_table[idx] = pos_idx; #ifdef MQLARTICLES_TICKETSINFO_DEBUG FastLog(FUNCION_ACTUAL, INFO_TEXT, StringFormat("Ticket %I64u insertado en idx=%u, pos_idx=%u", ticket, idx, pos_idx)); #endif // MQLARTICLES_TICKETSINFO_DEBUG return true; } } //--- FastLog(FUNCION_ACTUAL, ERROR_TEXT, StringFormat("Tabla hash llena para ticket %I64u", ticket)); return false; } //+------------------------------------------------------------------+ bool CTicketsInfo::RemoveHash(const ulong ticket) { //--- const uint16_t h = (uint16_t)(ticket % MQLARTICLES_HASH_TABLE_SIZE); //--- for(uint16_t i = 0; i < MQLARTICLES_HASH_TABLE_SIZE; i++) { //--- const uint16_t idx = (h + i) % MQLARTICLES_HASH_TABLE_SIZE; const uint16_t pos_idx = m_hash_table[idx]; //--- if(pos_idx == MQLARTICLES_HASH_TABLE_INVALID_INDEX) { #ifdef MQLARTICLES_TICKETSINFO_DEBUG FastLog(FUNCION_ACTUAL, CAUTION_TEXT, StringFormat("El ticket %I64u no existe (casilla vacía en idx=%u)", ticket, idx)); #endif // MQLARTICLES_TICKETSINFO_DEBUG return false; } //--- if(pos_idx < m_count && m_positions[pos_idx].ticket == ticket) { #ifdef MQLARTICLES_TICKETSINFO_DEBUG FastLog(FUNCION_ACTUAL, INFO_TEXT, StringFormat("Se eliminó exitosamente el ticket %I64u en idx=%u, pos_idx=%u", ticket, idx, pos_idx)); #endif // MQLARTICLES_TICKETSINFO_DEBUG m_hash_table[idx] = MQLARTICLES_HASH_TABLE_INVALID_INDEX; return true; } //--- if(pos_idx >= m_count) { FastLog(FUNCION_ACTUAL, ERROR_TEXT, StringFormat("Índice inválido pos_idx=%u para ticket %I64u en idx=%u", pos_idx, ticket, idx)); return false; } } //--- #ifdef MQLARTICLES_TICKETSINFO_DEBUG FastLog(FUNCION_ACTUAL, WARNING_TEXT, StringFormat("El ticket %I64u no se ha encontrado tras recorrer toda la tabla", ticket)); #endif // MQLARTICLES_TICKETSINFO_DEBUG return false; } //+------------------------------------------------------------------+ void CTicketsInfo::UpdateHashAfterSwap(const ulong ticket, const uint16_t new_pos_idx) { //--- const uint16_t h = (uint16_t)(ticket % MQLARTICLES_HASH_TABLE_SIZE); //--- for(uint16_t i = 0; i < MQLARTICLES_HASH_TABLE_SIZE; i++) { //--- const uint16_t idx = (h + i) % MQLARTICLES_HASH_TABLE_SIZE; const uint16_t pos_idx = m_hash_table[idx]; //--- if(pos_idx == MQLARTICLES_HASH_TABLE_INVALID_INDEX) return; //--- if(pos_idx < m_count && m_positions[new_pos_idx].ticket == ticket) { m_hash_table[idx] = new_pos_idx; return; } } } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool CTicketsInfo::GetByTicket(const ulong ticket, Position &pos, int16_t &out_idx) const { out_idx = FindHash(ticket); if(out_idx >= 0) { pos = m_positions[out_idx]; return true; } else return false; } //+------------------------------------------------------------------+ bool CTicketsInfo::GetByTicket(const ulong ticket, Position &pos) const { const int16_t pos_idx = FindHash(ticket); if(pos_idx >= 0) { pos = m_positions[pos_idx]; return true; } else return false; } //+------------------------------------------------------------------+ inline Position CTicketsInfo::GetByTicket(const ulong ticket) const { const int16_t pos_idx = FindHash(ticket); return pos_idx >= 0 ? m_positions[pos_idx] : EMPTY_POSITION; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ inline void CTicketsInfo::UpdateSl(const double new_sl, const ulong ticket) { const int16_t pos_idx = FindHash(ticket); if(pos_idx >= 0) m_positions[pos_idx].sl = new_sl; } //+------------------------------------------------------------------+ inline void CTicketsInfo::UpdateTp(const double new_tp, const ulong ticket) { const int16_t pos_idx = FindHash(ticket); if(pos_idx >= 0) m_positions[pos_idx].tp = new_tp; } //+------------------------------------------------------------------+ inline void CTicketsInfo::UpdatePositionPartial(const double net, const double close_volume, const int16_t pos_idx) { if(pos_idx >= 0 && pos_idx < (int16_t)m_count) { m_positions[pos_idx].volume -= close_volume; m_positions[pos_idx].profit += net; #ifdef MQLARTICLES_TICKETSINFO_DEBUG FastLog(FUNCION_ACTUAL, INFO_TEXT, StringFormat("Volumen cerrado = %.4f (new= %.4f) y profit obtenido = %.2f, al cierra parcial de la posicion = %I64u", close_volume, m_positions[pos_idx].volume, net, m_positions[pos_idx].ticket)); #endif // MQLARTICLES_TICKETSINFO_DEBUG } } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool CTicketsInfo::Add(const Position &p) { //--- if(m_count >= MQLARTICLES_RM_ACCOUNT_MAX_POSITIONS) { FastLog(FUNCION_ACTUAL, ERROR_TEXT, StringFormat("Límite de posiciones alcanzado (%d/%d). No se puede añadir el ticket %I64u", m_count, MQLARTICLES_RM_ACCOUNT_MAX_POSITIONS, p.ticket)); return false; } //--- if(FindHash(p.ticket) >= 0) { #ifdef MQLARTICLES_TICKETSINFO_DEBUG FastLog(FUNCION_ACTUAL, CAUTION_TEXT, StringFormat("La posición %I64u ya existe", p.ticket)); #endif // MQLARTICLES_TICKETSINFO_DEBUG return false; } //--- m_positions[m_count] = p; //--- if(!InsertHash(p.ticket, m_count)) { FastLog(FUNCION_ACTUAL, ERROR_TEXT, StringFormat("No se pudo insertar el ticket %I64u en la tabla hash", p.ticket)); return false; } //--- m_count++; return true; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool CTicketsInfo::DeleteByIndex(const int16_t pos_idx) { //--- if(pos_idx < 0 || pos_idx >= (int16_t)m_count) { FastLog(FUNCION_ACTUAL, ERROR_TEXT, StringFormat("Indice invalido %d", pos_idx)); return false; } //--- const ulong ticket = m_positions[pos_idx].ticket; if(!RemoveHash(ticket)) { FastLog(FUNCION_ACTUAL, ERROR_TEXT, StringFormat("Fallo al eliminar el ticket %I64u de la tabla hash", ticket)); return false; } //--- m_count--; if(pos_idx != m_count) { m_positions[pos_idx] = m_positions[m_count]; UpdateHashAfterSwap(m_positions[pos_idx].ticket, pos_idx); } //--- return true; } //+------------------------------------------------------------------+ bool CTicketsInfo::DeleteByTicket(const ulong ticket) { //--- const int16_t pos_idx = FindHash(ticket); if(pos_idx < 0) { FastLog(FUNCION_ACTUAL, ERROR_TEXT, StringFormat("No se pudo eliminar el ticket %I64u, ya que no existe", ticket)); return false; } //--- if(!RemoveHash(ticket)) { FastLog(FUNCION_ACTUAL, ERROR_TEXT, StringFormat("Fallo al eliminar el ticket %I64u de la tabla hash", ticket)); return false; } //--- m_count--; if(pos_idx != m_count) { m_positions[pos_idx] = m_positions[m_count]; UpdateHashAfterSwap(m_positions[pos_idx].ticket, pos_idx); } //--- return true; } //+------------------------------------------------------------------+ bool CTicketsInfo::DeleteByTicket(const ulong ticket, Position &position) { //--- const int16_t pos_idx = FindHash(ticket); if(pos_idx < 0) { FastLog(FUNCION_ACTUAL, ERROR_TEXT, StringFormat("No se pudo eliminar el ticket %I64u, ya que no existe", ticket)); return false; } //--- position = m_positions[pos_idx]; //--- if(!RemoveHash(ticket)) { FastLog(FUNCION_ACTUAL, ERROR_TEXT, StringFormat("Fallo al eliminar el ticket %I64u de la tabla hash", ticket)); return false; } //--- m_count--; if(pos_idx != m_count) { m_positions[pos_idx] = m_positions[m_count]; UpdateHashAfterSwap(m_positions[pos_idx].ticket, pos_idx); } //--- return true; } //+------------------------------------------------------------------+ void CTicketsInfo::Free(void) { m_count = 0; ArrayInitialize(m_hash_table, MQLARTICLES_HASH_TABLE_INVALID_INDEX); } //+------------------------------------------------------------------+ inline bool CTicketsInfo::Exists(const ulong ticket) const { return FindHash(ticket) >= 0; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CTicketsInfo::CloseAllPositionMask(const int &index_arr[], const int size_arr, CTrade& trade, const uint flags) const { for(int i = 0; i < size_arr; i++) { const int ri = index_arr[i]; if(!PositionSelectByTicket(m_positions[ri].ticket)) continue; //--- const double profit = PositionGetDouble(POSITION_PROFIT); const ENUM_POSITION_TYPE type = this.m_positions[ri].type; //--- if((type == POSITION_TYPE_BUY && (flags & FLAG_POSITION_TYPE_BUY) == 0) || (type == POSITION_TYPE_SELL && (flags & FLAG_POSITION_TYPE_SELL) == 0)) continue; if((flags & FLAG_CLOSE_ALL_PROFIT) != 0 && profit > 0) { trade.PositionClose(this.m_positions[ri].ticket); } else if((flags & FLAG_CLOSE_ALL_LOSS) != 0 && profit < 0) { trade.PositionClose(this.m_positions[ri].ticket); } } } //+------------------------------------------------------------------+ int CTicketsInfo::GetPositionsMask(const int &index_arr[], const int size_arr, const uint flags) const { int count = 0; for(int i = 0; i < size_arr; i++) { const ENUM_POSITION_TYPE type = m_positions[index_arr[i]].type; if(type == POSITION_TYPE_BUY && (flags & FLAG_POSITION_TYPE_BUY) != 0) count++; if(type == POSITION_TYPE_SELL && (flags & FLAG_POSITION_TYPE_SELL) != 0) count++; } return count; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ double CTicketsInfo::GetProfitMask(const int &index_arr[], const int size_arr) const { double v = 0.0; for(int i = 0; i < size_arr; i++) { if(!PositionSelectByTicket(m_positions[index_arr[i]].ticket)) continue; v += PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP); } return v; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ // arr tendra que haber sido resizeado previa.. void CTicketsInfo::GetPositionsFromMagic(const ulong magic, int& arr[], int &size_arr) const { for(int i = 0; i < m_count; i++) { if(m_positions[i].magic != magic && magic != NOT_MAGIC_NUMBER) continue; arr[size_arr++] = i; } } #endif // MQLARTICLES_RM_TICKETS_TABLE_MQH //+------------------------------------------------------------------+ //+------------------------------------------------------------------+