2026-09-12 16:11:11 -03:00 | | | //+------------------------------------------------------------------+
|
| | | //| CMockBookBuilder.mqh |
|
| | | //| Synthetic Test Harness for MetaTrader 5 |
|
| | | //| Institutional Architecture in MQL5 - Part 3 Series |
|
| | | //+------------------------------------------------------------------+
|
| | | #ifndef SYNTHETIC_TEST_HARNESS_CMOCK_BOOK_BUILDER_MQH
|
| | | #define SYNTHETIC_TEST_HARNESS_CMOCK_BOOK_BUILDER_MQH
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| CMockBookBuilder: Deterministic Level 2 DOM Constructor. |
|
| | | //| |
|
| | | //| Single Responsibility: Build valid, reproducible MqlBookInfo[] |
|
| | | //| snapshots for TDD without broker subscriptions or historical L2 |
|
| | | //| data. Designed to plug into CSyntheticEngine via |
|
| | | //| TMarketBookGetBehavior delegate injection. |
|
| | | //+------------------------------------------------------------------+
|
| | | class CMockBookBuilder
|
| | | {
|
| | | private:
|
| | | MqlBookInfo m_book[];
|
| | | int m_count;
|
| | | int m_max_levels;
|
| | |
|
| | | double m_tick_size;
|
| | | double m_digits_factor;
|
| | |
|
| | | void PushLevel(const ENUM_BOOK_TYPE side,
|
| | | const double price,
|
| | | const long volume_real,
|
| | | const long volume = 0);
|
| | | void SortAndDeduplicate(void);
|
| | | void EnsureCapacity(void);
|
| | |
|
| | | public:
|
| | | //+---------------------------------------------------------------+
|
| | | //| Constructor & Destructor |
|
| | | //+---------------------------------------------------------------+
|
| | | CMockBookBuilder(void);
|
| | | ~CMockBookBuilder(void);
|
| | |
|
| | | void Clear(void);
|
| | | void SetTickSize(const double ts, const int digits = 5);
|
| | |
|
| | | // Raw level injection
|
| | | void AddBid(const double price, const double volume_lots);
|
| | | void AddAsk(const double price, const double volume_lots);
|
| | |
|
| | | // Named canonical scenarios (Etapa 1 do plano)
|
| | | void BuildBalancedBook(const double mid_price,
|
| | | const int levels_per_side = 10,
|
| | | const double base_volume_lots = 1.0,
|
| | | const double step_ticks = 1,
|
| | | const double volume_decay = 0.9);
|
| | |
|
| | | void BuildThinBook(const double mid_price,
|
| | | const int levels_per_side = 3,
|
| | | const double base_volume_lots = 0.1,
|
| | | const double step_ticks = 5,
|
| | | const double volume_decay = 0.8);
|
| | |
|
| | | void BuildHollowBook(const double mid_price,
|
| | | const int levels_per_side = 10,
|
| | | const double surface_volume_lots = 0.5,
|
| | | const double step_ticks = 1,
|
| | | const double hollow_factor = 0.1);
|
| | |
|
| | | void BuildWallOnBid(const double mid_price,
|
| | | const int levels_per_side = 10,
|
| | | const double base_volume_lots = 1.0,
|
| | | const int wall_level_index = 2,
|
| | | const double wall_volume_lots = 25.0,
|
| | | const double step_ticks = 1);
|
| | |
|
| | | void BuildWallOnAsk(const double mid_price,
|
| | | const int levels_per_side = 10,
|
| | | const double base_volume_lots = 1.0,
|
| | | const int wall_level_index = 2,
|
| | | const double wall_volume_lots = 25.0,
|
| | | const double step_ticks = 1);
|
| | |
|
| | | void BuildAsymmetricBook(const double mid_price,
|
| | | const int levels_per_side = 10,
|
| | | const double bid_base_volume = 2.0,
|
| | | const double ask_base_volume = 0.5,
|
| | | const double step_ticks = 1,
|
| | | const double volume_decay = 0.9);
|
| | |
|
| | | void BuildLiquidityVacuum(const double mid_price,
|
| | | const int levels_per_side = 10,
|
| | | const double surface_volume = 0.2,
|
| | | const double vacuum_start_ticks = 3,
|
| | | const int vacuum_width_levels = 5,
|
| | | const double deep_volume = 5.0,
|
| | | const double step_ticks = 1);
|
| | |
|
| | | // Exporters
|
| | | bool ToArray(MqlBookInfo &out_book[]);
|
| | | int LevelsTotal(void) const { return(m_count); }
|
| | |
|
| | | // Diagnostics / Test Helpers
|
| | | double GetBestBid(void) const;
|
| | | double GetBestAsk(void) const;
|
| | | double SumVolumeSide(const ENUM_BOOK_TYPE side) const;
|
| | | };
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Constructor |
|
| | | //+------------------------------------------------------------------+
|
| | | CMockBookBuilder::CMockBookBuilder(void) :
|
| | | m_count(0),
|
| | | m_max_levels(1024),
|
| | | m_tick_size(0.00001),
|
| | | m_digits_factor(100000.0)
|
| | | {
|
| | | ArrayResize(m_book, m_max_levels);
|
| | | }
|
| | |
|
| | | CMockBookBuilder::~CMockBookBuilder(void)
|
| | | {
|
| | | ArrayFree(m_book);
|
| | | }
|
| | |
|
| | | void CMockBookBuilder::Clear(void)
|
| | | {
|
| | | ArrayFree(m_book);
|
| | | m_max_levels = 1024;
|
| | | ArrayResize(m_book, m_max_levels);
|
| | | m_count = 0;
|
| | | }
|
| | |
|
| | | void CMockBookBuilder::SetTickSize(const double ts, const int digits)
|
| | | {
|
| | | m_tick_size = (ts > 0) ? ts : m_tick_size;
|
| | | double df = 1.0;
|
| | | for(int i = 0; i < digits; i++) df *= 10.0;
|
| | | m_digits_factor = df;
|
| | | }
|
| | |
|
| | | void CMockBookBuilder::EnsureCapacity(void)
|
| | | {
|
| | | if(m_count < m_max_levels - 1) return;
|
| | | m_max_levels *= 2;
|
| | | ArrayResize(m_book, m_max_levels);
|
| | | }
|
| | |
|
| | | void CMockBookBuilder::PushLevel(const ENUM_BOOK_TYPE side,
|
| | | const double price,
|
| | | const long volume_real,
|
| | | const long volume)
|
| | | {
|
| | | EnsureCapacity();
|
| | | ZeroMemory(m_book[m_count]);
|
| | | m_book[m_count].type = side;
|
| | | m_book[m_count].price = NormalizeDouble(price, (int)MathLog10(m_digits_factor));
|
| | | m_book[m_count].volume_real = (double)volume_real;
|
| | | m_book[m_count].volume = (volume > 0) ? volume : volume_real;
|
| | | m_count++;
|
| | | }
|
| | |
|
| | | void CMockBookBuilder::AddBid(const double price, const double volume_lots)
|
| | | {
|
| | | long vol = (long)MathMax(1, MathRound(volume_lots * m_digits_factor));
|
| | | PushLevel(BOOK_TYPE_BUY, price, vol);
|
| | | }
|
| | |
|
| | | void CMockBookBuilder::AddAsk(const double price, const double volume_lots)
|
| | | {
|
| | | long vol = (long)MathMax(1, MathRound(volume_lots * m_digits_factor));
|
| | | PushLevel(BOOK_TYPE_SELL, price, vol);
|
| | | }
|
| | |
|
| | | void CMockBookBuilder::SortAndDeduplicate(void)
|
| | | {
|
| | | if(m_count <= 1) return;
|
| | |
|
| | | int n = m_count;
|
| | | for(int i = 0; i < n - 1; i++)
|
| | | {
|
| | | for(int j = i + 1; j < n; j++)
|
| | | {
|
| | | bool need_swap = false;
|
| | | if(m_book[i].type != m_book[j].type)
|
| | | need_swap = (m_book[i].type > m_book[j].type);
|
| | | else if(m_book[i].type == BOOK_TYPE_BUY)
|
| | | need_swap = (m_book[i].price < m_book[j].price);
|
| | | else
|
| | | need_swap = (m_book[i].price > m_book[j].price);
|
| | |
|
| | | if(need_swap)
|
| | | {
|
| | | MqlBookInfo tmp = m_book[i];
|
| | | m_book[i] = m_book[j];
|
| | | m_book[j] = tmp;
|
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | int write = 0;
|
| | | for(int read = 0; read < n; read++)
|
| | | {
|
| | | if(write > 0 &&
|
| | | m_book[write - 1].type == m_book[read].type &&
|
| | | MathAbs(m_book[write - 1].price - m_book[read].price) < m_tick_size * 0.5)
|
| | | {
|
| | | m_book[write - 1].volume_real += m_book[read].volume_real;
|
| | | m_book[write - 1].volume += m_book[read].volume;
|
| | | }
|
| | | else
|
| | | {
|
| | | if(read != write) m_book[write] = m_book[read];
|
| | | write++;
|
| | | }
|
| | | }
|
| | | m_count = write;
|
| | | }
|
| | |
|
| | | void CMockBookBuilder::BuildBalancedBook(const double mid_price,
|
| | | const int levels_per_side,
|
| | | const double base_volume_lots,
|
| | | const double step_ticks,
|
| | | const double volume_decay)
|
| | | {
|
| | | Clear();
|
| | | double step = m_tick_size * step_ticks;
|
| | | double half_spread = step;
|
| | |
|
| | | double bid_price = mid_price - half_spread;
|
| | | double ask_price = mid_price + half_spread;
|
| | |
|
| | | double bid_vol = base_volume_lots;
|
| | | double ask_vol = base_volume_lots;
|
| | |
|
| | | for(int i = 0; i < levels_per_side; i++)
|
| | | {
|
| | | AddBid(bid_price, bid_vol);
|
| | | AddAsk(ask_price, ask_vol);
|
| | | bid_price -= step;
|
| | | ask_price += step;
|
| | | bid_vol *= volume_decay;
|
| | | ask_vol *= volume_decay;
|
| | | }
|
| | |
|
| | | SortAndDeduplicate();
|
| | | }
|
| | |
|
| | | void CMockBookBuilder::BuildThinBook(const double mid_price,
|
| | | const int levels_per_side,
|
| | | const double base_volume_lots,
|
| | | const double step_ticks,
|
| | | const double volume_decay)
|
| | | {
|
| | | BuildBalancedBook(mid_price, levels_per_side, base_volume_lots, step_ticks, volume_decay);
|
| | | }
|
| | |
|
| | | void CMockBookBuilder::BuildHollowBook(const double mid_price,
|
| | | const int levels_per_side,
|
| | | const double surface_volume_lots,
|
| | | const double step_ticks,
|
| | | const double hollow_factor)
|
| | | {
|
| | | Clear();
|
| | | double step = m_tick_size * step_ticks;
|
| | | double half_spread = step;
|
| | |
|
| | | double bid_price = mid_price - half_spread;
|
| | | double ask_price = mid_price + half_spread;
|
| | |
|
| | | double v;
|
| | | for(int i = 0; i < levels_per_side; i++)
|
| | | {
|
| | | if(i == 0)
|
| | | v = surface_volume_lots;
|
| | | else
|
| | | v = surface_volume_lots * hollow_factor;
|
| | |
|
| | | AddBid(bid_price, v);
|
| | | AddAsk(ask_price, v);
|
| | | bid_price -= step;
|
| | | ask_price += step;
|
| | | }
|
| | |
|
| | | SortAndDeduplicate();
|
| | | }
|
| | |
|
| | | void CMockBookBuilder::BuildWallOnBid(const double mid_price,
|
| | | const int levels_per_side,
|
| | | const double base_volume_lots,
|
| | | const int wall_level_index,
|
| | | const double wall_volume_lots,
|
| | | const double step_ticks)
|
| | | {
|
| | | Clear();
|
| | | double step = m_tick_size * step_ticks;
|
| | | double half_spread = step;
|
| | |
|
| | | double bid_price = mid_price - half_spread;
|
| | | double ask_price = mid_price + half_spread;
|
| | |
|
| | | double bid_vol = base_volume_lots;
|
| | | double ask_vol = base_volume_lots;
|
| | |
|
| | | for(int i = 0; i < levels_per_side; i++)
|
| | | {
|
| | | double bv = (i == wall_level_index) ? wall_volume_lots : bid_vol;
|
| | | AddBid(bid_price, bv);
|
| | | AddAsk(ask_price, ask_vol);
|
| | | bid_price -= step;
|
| | | ask_price += step;
|
| | | bid_vol *= 0.9;
|
| | | ask_vol *= 0.9;
|
| | | }
|
| | |
|
| | | SortAndDeduplicate();
|
| | | }
|
| | |
|
| | | void CMockBookBuilder::BuildWallOnAsk(const double mid_price,
|
| | | const int levels_per_side,
|
| | | const double base_volume_lots,
|
| | | const int wall_level_index,
|
| | | const double wall_volume_lots,
|
| | | const double step_ticks)
|
| | | {
|
| | | Clear();
|
| | | double step = m_tick_size * step_ticks;
|
| | | double half_spread = step;
|
| | |
|
| | | double bid_price = mid_price - half_spread;
|
| | | double ask_price = mid_price + half_spread;
|
| | |
|
| | | double bid_vol = base_volume_lots;
|
| | | double ask_vol = base_volume_lots;
|
| | |
|
| | | for(int i = 0; i < levels_per_side; i++)
|
| | | {
|
| | | double av = (i == wall_level_index) ? wall_volume_lots : ask_vol;
|
| | | AddBid(bid_price, bid_vol);
|
| | | AddAsk(ask_price, av);
|
| | | bid_price -= step;
|
| | | ask_price += step;
|
| | | bid_vol *= 0.9;
|
| | | ask_vol *= 0.9;
|
| | | }
|
| | |
|
| | | SortAndDeduplicate();
|
| | | }
|
| | |
|
| | | void CMockBookBuilder::BuildAsymmetricBook(const double mid_price,
|
| | | const int levels_per_side,
|
| | | const double bid_base_volume,
|
| | | const double ask_base_volume,
|
| | | const double step_ticks,
|
| | | const double volume_decay)
|
| | | {
|
| | | Clear();
|
| | | double step = m_tick_size * step_ticks;
|
| | | double half_spread = step;
|
| | |
|
| | | double bid_price = mid_price - half_spread;
|
| | | double ask_price = mid_price + half_spread;
|
| | |
|
| | | double bid_vol = bid_base_volume;
|
| | | double ask_vol = ask_base_volume;
|
| | |
|
| | | for(int i = 0; i < levels_per_side; i++)
|
| | | {
|
| | | AddBid(bid_price, bid_vol);
|
| | | AddAsk(ask_price, ask_vol);
|
| | | bid_price -= step;
|
| | | ask_price += step;
|
| | | bid_vol *= volume_decay;
|
| | | ask_vol *= volume_decay;
|
| | | }
|
| | |
|
| | | SortAndDeduplicate();
|
| | | }
|
| | |
|
| | | void CMockBookBuilder::BuildLiquidityVacuum(const double mid_price,
|
| | | const int levels_per_side,
|
| | | const double surface_volume,
|
| | | const double vacuum_start_ticks,
|
| | | const int vacuum_width_levels,
|
| | | const double deep_volume,
|
| | | const double step_ticks)
|
| | | {
|
| | | Clear();
|
| | | double step = m_tick_size * step_ticks;
|
| | | double half_spread = step;
|
| | |
|
| | | double bid_price = mid_price - half_spread;
|
| | | double ask_price = mid_price + half_spread;
|
| | |
|
| | | int vac_start = (int)MathMax(1, MathFloor(vacuum_start_ticks / MathMax(1, step_ticks)));
|
| | |
|
| | | for(int i = 0; i < levels_per_side; i++)
|
| | | {
|
| | | bool in_vacuum = (i >= vac_start && i < vac_start + vacuum_width_levels);
|
| | | double v;
|
2026-09-23 14:50:29 -03:00 | | | if(i < vac_start)
|
2026-09-12 16:11:11 -03:00 | | | v = surface_volume;
|
| | | else if(in_vacuum)
|
| | | v = surface_volume * 0.05;
|
| | | else
|
| | | v = deep_volume;
|
| | |
|
| | | AddBid(bid_price, v);
|
| | | AddAsk(ask_price, v);
|
| | | bid_price -= step;
|
| | | ask_price += step;
|
| | | }
|
| | |
|
| | | SortAndDeduplicate();
|
| | | }
|
| | |
|
| | | bool CMockBookBuilder::ToArray(MqlBookInfo &out_book[])
|
| | | {
|
| | | SortAndDeduplicate();
|
| | | ArrayFree(out_book);
|
| | | if(m_count <= 0) return(false);
|
| | |
|
| | | ArrayResize(out_book, m_count);
|
| | | for(int i = 0; i < m_count; i++)
|
| | | out_book[i] = m_book[i];
|
| | |
|
| | | return(true);
|
| | | }
|
| | |
|
| | | double CMockBookBuilder::GetBestBid(void) const
|
| | | {
|
| | | double best = 0.0;
|
| | | for(int i = 0; i < m_count; i++)
|
| | | {
|
| | | if(m_book[i].type == BOOK_TYPE_BUY)
|
| | | {
|
| | | if(best == 0.0 || m_book[i].price > best)
|
| | | best = m_book[i].price;
|
| | | }
|
| | | }
|
| | | return(best);
|
| | | }
|
| | |
|
| | | double CMockBookBuilder::GetBestAsk(void) const
|
| | | {
|
| | | double best = 0.0;
|
| | | for(int i = 0; i < m_count; i++)
|
| | | {
|
| | | if(m_book[i].type == BOOK_TYPE_SELL)
|
| | | {
|
| | | if(best == 0.0 || m_book[i].price < best)
|
| | | best = m_book[i].price;
|
| | | }
|
| | | }
|
| | | return(best);
|
| | | }
|
| | |
|
| | | double CMockBookBuilder::SumVolumeSide(const ENUM_BOOK_TYPE side) const
|
| | | {
|
| | | double sum = 0.0;
|
| | | for(int i = 0; i < m_count; i++)
|
| | | {
|
| | | if(m_book[i].type == side)
|
| | | sum += (double)m_book[i].volume_real / m_digits_factor;
|
| | | }
|
| | | return(sum);
|
| | | }
|
| | |
|
| | | #endif
|
| | | //+------------------------------------------------------------------+
|