MQL5-SyntheticTestHarness/MQL5/Include/SyntheticTestHarness/CMockBookBuilder.mqh

467 lines
15 KiB
MQL5

//+------------------------------------------------------------------+
//| 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)
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
//+------------------------------------------------------------------+