467 lines
15 KiB
MQL5
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;
|
|
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
|
|
//+------------------------------------------------------------------+
|