Files
edge_lab/core/include/tick_generator.hpp
T

244 lines
8.9 KiB
C++

/**
* EdgeEngine - Open-Source MT5 Strategy Tester Replica
* tick_generator.hpp
*
* Reproduces the MetaTrader 5 Strategy Tester tick generation engine:
*
* TickMode::OPEN_PRICES - One synthetic tick per bar at the Open price.
* Classic "Open Prices Only" mode. Fastest.
*
* TickMode::EVERY_TICK - MT5's proprietary 12-point synthetic OHLC wave.
* Reconstructs intrabar price movement deterministically.
* Tick sequence per bar follows the canonical wave:
* Open -> [pre-trend midpoints] -> H or L first
* -> L or H -> Close (then next Open)
* Direction (H-first vs L-first) is heuristically
* inferred from Open/Close relationship.
*
* TickMode::REAL_TICKS - Pass-through: accepts externally supplied MqlTick[]
* arrays. Intended for broker recorded tick replays.
*/
#pragma once
#include <vector>
#include <cmath>
#include <cstdint>
#include <stdexcept>
#include "mql_types.hpp"
#include "types.hpp"
namespace edge {
// MT5 tick flags (mirrors MQL5 TICK_FLAG_*)
namespace TickFlag {
constexpr uint32_t BID = 0x02;
constexpr uint32_t ASK = 0x04;
constexpr uint32_t LAST = 0x08;
constexpr uint32_t VOLUME = 0x10;
constexpr uint32_t BUY = 0x20;
constexpr uint32_t SELL = 0x40;
}
class TickGenerator {
public:
/**
* Generate synthetic ticks for a single OHLCV bar.
* Mirrors MT5 Strategy Tester "Every Tick Based on Real Ticks" behaviour
* when real ticks are unavailable: deterministic 12-point OHLC wave.
*
* @param bar The OHLCV bar to decompose
* @param spread Spread in price units (applied to all generated ticks)
* @param bar_time_ms Bar open time in milliseconds
* @param bar_duration_ms Duration of bar in ms (e.g. 300000 for M5)
* @return Vector of ticks for the bar
*/
static std::vector<MqlTick> generate_bar_ticks(
const MqlRates& bar,
double spread,
int64_t bar_time_ms,
int64_t bar_duration_ms = 300000 // M5 default
) {
std::vector<MqlTick> ticks;
ticks.reserve(12);
double O = bar.open;
double H = bar.high;
double L = bar.low;
double C = bar.close;
// MT5's canonical 12-point OHLC wave.
// The heuristic for H/L order is:
// - If Close >= Open (bullish bar): Low comes first (dip then rally)
// - If Close < Open (bearish bar): High comes first (spike then sell-off)
// This matches MT5 Strategy Tester documentation and reverse-engineered behavior.
bool bull_bar = (C >= O);
bool h_first = !bull_bar; // true = High before Low in intrabar sequence
// 12-point Chebyshev-distributed price sequence
// Open -> intermediate points -> first extreme -> second extreme -> Close
std::vector<double> wave;
wave.reserve(12);
// Point 1: Open
wave.push_back(O);
// Points 2-4: Move toward first extreme (H or L)
double first_extreme = h_first ? H : L;
double second_extreme = h_first ? L : H;
// Intermediate steps (3 points) toward first extreme
double step1 = O + (first_extreme - O) * 0.33;
double step2 = O + (first_extreme - O) * 0.66;
wave.push_back(step1);
wave.push_back(step2);
wave.push_back(first_extreme); // Point 4: first extreme
// Points 5-6: Cross to second extreme
double cross_mid = first_extreme + (second_extreme - first_extreme) * 0.5;
wave.push_back(cross_mid);
wave.push_back(second_extreme); // Point 6: second extreme
// Points 7-10: Move toward Close from second extreme
double to_close1 = second_extreme + (C - second_extreme) * 0.25;
double to_close2 = second_extreme + (C - second_extreme) * 0.5;
double to_close3 = second_extreme + (C - second_extreme) * 0.75;
wave.push_back(to_close1);
wave.push_back(to_close2);
wave.push_back(to_close3);
wave.push_back(C); // Point 10: Close
// Points 11-12: At Close (volume / last two ticks)
wave.push_back(C);
wave.push_back(C);
// Build MqlTick structs with evenly spaced timestamps within bar
int64_t step_ms = bar_duration_ms / static_cast<int64_t>(wave.size());
for (size_t i = 0; i < wave.size(); ++i) {
MqlTick tick{};
tick.time = (bar_time_ms + static_cast<int64_t>(i) * step_ms) / 1000;
tick.time_msc = bar_time_ms + static_cast<int64_t>(i) * step_ms;
tick.bid = wave[i];
tick.ask = wave[i] + spread;
tick.last = wave[i];
tick.volume = (i == wave.size() - 1) ? static_cast<uint64_t>(bar.tick_volume) : 1;
tick.flags = TickFlag::BID | TickFlag::ASK;
ticks.push_back(tick);
}
return ticks;
}
/**
* Generate 4 canonical ticks for 1 Minute OHLC mode (TickMode::OHLC_M1).
* Follows MT5 sequence:
* Bull bar (Close >= Open): Open -> Low -> High -> Close
* Bear bar (Close < Open): Open -> High -> Low -> Close
*/
static std::vector<MqlTick> generate_ohlc_m1_ticks(
const MqlRates& bar,
double spread,
int64_t bar_time_ms,
int64_t bar_duration_ms = 60000
) {
std::vector<MqlTick> ticks;
ticks.reserve(4);
double O = bar.open;
double H = bar.high;
double L = bar.low;
double C = bar.close;
bool bull_bar = (C >= O);
double prices[4];
prices[0] = O;
prices[1] = bull_bar ? L : H;
prices[2] = bull_bar ? H : L;
prices[3] = C;
int64_t step_ms = bar_duration_ms / 4;
for (int i = 0; i < 4; ++i) {
MqlTick tick{};
tick.time = (bar_time_ms + static_cast<int64_t>(i) * step_ms) / 1000;
tick.time_msc = bar_time_ms + static_cast<int64_t>(i) * step_ms;
tick.bid = prices[i];
tick.ask = prices[i] + spread;
tick.last = prices[i];
tick.volume = (i == 3) ? static_cast<uint64_t>(bar.tick_volume) : 1;
tick.flags = TickFlag::BID | TickFlag::ASK;
ticks.push_back(tick);
}
return ticks;
}
/**
* Generate a single open-price tick for a bar (OPEN_PRICES mode).
*/
static MqlTick generate_open_tick(
const MqlRates& bar,
double spread
) {
MqlTick tick{};
tick.time = bar.time;
tick.time_msc = bar.time * 1000LL;
tick.bid = bar.open;
tick.ask = bar.open + spread;
tick.last = bar.open;
tick.volume = static_cast<uint64_t>(bar.tick_volume);
tick.flags = TickFlag::BID | TickFlag::ASK;
return tick;
}
/**
* Generate all ticks for the full bar array according to TickMode.
* REAL_TICKS mode is not handled here — caller must supply real ticks directly.
*/
static std::vector<std::vector<MqlTick>> generate_all(
const MqlRates* bars,
const double* spread_series, // per-bar spread, or nullptr (use sym.spread_val)
int32_t n_bars,
double default_spread,
TickMode mode,
int64_t bar_duration_ms = 300000 // M5 default
) {
std::vector<std::vector<MqlTick>> result;
result.reserve(n_bars);
for (int32_t i = 0; i < n_bars; ++i) {
double sp = (spread_series && spread_series[i] >= 0.0)
? spread_series[i] : default_spread;
if (mode == TickMode::EVERY_TICK) {
result.push_back(generate_bar_ticks(
bars[i], sp, bars[i].time * 1000LL, bar_duration_ms));
} else if (mode == TickMode::OHLC_M1) {
result.push_back(generate_ohlc_m1_ticks(
bars[i], sp, bars[i].time * 1000LL, bar_duration_ms));
} else {
// OPEN_PRICES: single tick per bar
result.push_back({ generate_open_tick(bars[i], sp) });
}
}
return result;
}
/**
* Infer bar duration in ms from bar timestamps.
* Falls back to M5 (300,000 ms) if insufficient data.
*/
static int64_t infer_bar_duration_ms(const MqlRates* bars, int32_t n) {
if (n < 2) return 300000LL;
// Take median of first 10 diffs to be robust to gaps
std::vector<int64_t> diffs;
int32_t check = std::min(n - 1, 10);
for (int32_t i = 0; i < check; ++i) {
int64_t diff = (bars[i+1].time - bars[i].time) * 1000LL;
if (diff > 0) diffs.push_back(diff);
}
if (diffs.empty()) return 300000LL;
std::sort(diffs.begin(), diffs.end());
return diffs[diffs.size() / 2];
}
};
} // namespace edge