Overview
Add OpenCL acceleration for the 3 most expensive CPU hotspots in the EA. Check GPU availability at OnInit and fall back to CPU silently if unavailable.
OnInit GPU Detection
int g_clContext = INVALID_HANDLE;
int g_clProgram = INVALID_HANDLE;
void InitOpenCL()
{
int devices[];
if(CLGetDeviceInfo(CL_DEVICE_TYPE_GPU, devices) == false || ArraySize(devices) == 0)
{
Print("OpenCL: no GPU available — running on CPU");
return;
}
g_clContext = CLContextCreate(devices[0]);
if(g_clContext == INVALID_HANDLE)
{
Print("OpenCL: CLContextCreate failed — running on CPU");
return;
}
// compile kernels...
Print("OpenCL: GPU ready — ", CLGetInfoString(devices[0], CL_DEVICE_NAME));
}
If g_clContext == INVALID_HANDLE at any call site → fall back to existing CPU path.
Hotspots Identified for Acceleration
Kernel 1 — scanHistoricalCandles() · Priority: HIGH · Speedup: 50–100×
File: OrderBlock.mq5
Problem: Sequential loop over lookback bars (~500), each calling detectNewOB() which itself does an O(n) duplicate guard scan over the growing obBuffer[].
// Current: serial, O(lookback × obBuffer_size)
for(int shift = lookback-1; shift >= 0; shift--)
{
CopyRates(_Symbol, CTOB, shift, 5, rA);
detectNewOB(); // includes O(n) dup guard inside
}
GPU approach: transfer the full 500-bar OHLCV matrix to a GPU buffer once. A single kernel evaluates every 3-candle window (bearish+bullish+higher-close pattern) in parallel and outputs a hit-bitmap. Host then allocates obBuffer slots only for hits — no per-bar CopyRates round-trip.
Kernel 2 — GetLastSwingLow() / GetLastSwingHigh() · Priority: HIGH · Speedup: 20–40×
File: OBInclude/helpers.mqh
Problem: Called 20+ times per OB evaluation (from checkForMSSEntry, getSLStart, checkForCrossLiquidity). Each call iterates candlesBack bars (20–80), and inside the loop calls iLowest/iHighest again → O(n²) per OB check.
for(int i = startIndex; i < (candlesBack + startIndex); i++)
{
double l = iLow(_Symbol, tf, i);
// ...
int lowestIndex = iLowest(_Symbol, tf, MODE_LOW, candlesBack, startIndex); // O(n) inside O(n) loop!
}
GPU approach: upload a window of 80 bars to a GPU buffer, run a parallel reduction to find min/max and the index of the structural swing in one pass. Batch multiple OB swing queries into a single kernel launch.
Kernel 3 — checkForMSSEntry() · Priority: CRITICAL · Speedup: 50–150×
File: OBInclude/cOrderBlock.mqh
Problem: Most expensive per-OB computation. Contains 4× GetLastSwingLow/High calls (already O(n²) each), then 2 backward scan loops each calling detectFVG() inside — yielding O(n³) overall. Called 250–1250 times per day.
// 4 swing scans
double MSSLowerLevel = GetLastSwingLow(lastHighIndex, candlesBack, pht);
double MSSHigherLevel = GetLastSwingHigh(lastHighIndex, candlesBack, pht);
// ...
// Then backward loop with detectFVG nested inside
for(int a = lastHighIndex; a >= 0; a--)
{
if(detectFVG(a, true, MSSLowerLevel, tf) == true) // O(n) inside O(n) loop
{ ... }
}
GPU approach:
- Compute swing levels via parallel reduction (Kernel 2 above).
- Evaluate the backward break-level scan on GPU — each candle tested independently, FVG condition inlined.
- Output: MSS found flag + break price + candle index.
Full Call Volume Estimate (2025 daily)
| Function |
Calls/day |
Complexity |
Total bar ops/day |
GetLastSwingLow/High |
~5 000 |
O(n²), n=20–80 |
~8 000 000 |
checkForMSSEntry |
~750 |
O(n³), n=50 |
~94 000 000 |
scanHistoricalCandles |
1 (init) |
O(500×n) |
~25 000 |
→ checkForMSSEntry dominates by 2 orders of magnitude.
Fallback Design
All three kernels wrap the existing CPU function behind a g_clContext != INVALID_HANDLE guard:
bool checkForMSSEntry(...)
{
if(g_clContext != INVALID_HANDLE)
return checkForMSSEntry_GPU(...);
return checkForMSSEntry_CPU(...); // existing code, unchanged
}
CPU path stays untouched — zero risk of regression when GPU is unavailable.
Out of Scope
cleanOBBuffer / OnTick per-tick loop — OB count (10–50) is too small; kernel launch overhead (~50 µs) exceeds compute savings
checkForCISDEntry — only 16 loop iterations; overhead not worth it
- DB writes / SQLite updates — I/O, not parallelizable
Acceptance Criteria
Overview
Add OpenCL acceleration for the 3 most expensive CPU hotspots in the EA. Check GPU availability at
OnInitand fall back to CPU silently if unavailable.OnInit GPU Detection
If
g_clContext == INVALID_HANDLEat any call site → fall back to existing CPU path.Hotspots Identified for Acceleration
Kernel 1 —
scanHistoricalCandles()· Priority: HIGH · Speedup: 50–100×File:
OrderBlock.mq5Problem: Sequential loop over
lookbackbars (~500), each callingdetectNewOB()which itself does an O(n) duplicate guard scan over the growingobBuffer[].GPU approach: transfer the full 500-bar OHLCV matrix to a GPU buffer once. A single kernel evaluates every 3-candle window (bearish+bullish+higher-close pattern) in parallel and outputs a hit-bitmap. Host then allocates
obBufferslots only for hits — no per-barCopyRatesround-trip.Kernel 2 —
GetLastSwingLow()/GetLastSwingHigh()· Priority: HIGH · Speedup: 20–40×File:
OBInclude/helpers.mqhProblem: Called 20+ times per OB evaluation (from
checkForMSSEntry,getSLStart,checkForCrossLiquidity). Each call iteratescandlesBackbars (20–80), and inside the loop callsiLowest/iHighestagain → O(n²) per OB check.GPU approach: upload a window of 80 bars to a GPU buffer, run a parallel reduction to find min/max and the index of the structural swing in one pass. Batch multiple OB swing queries into a single kernel launch.
Kernel 3 —
checkForMSSEntry()· Priority: CRITICAL · Speedup: 50–150×File:
OBInclude/cOrderBlock.mqhProblem: Most expensive per-OB computation. Contains 4×
GetLastSwingLow/Highcalls (already O(n²) each), then 2 backward scan loops each callingdetectFVG()inside — yielding O(n³) overall. Called 250–1250 times per day.GPU approach:
Full Call Volume Estimate (2025 daily)
GetLastSwingLow/HighcheckForMSSEntryscanHistoricalCandles→
checkForMSSEntrydominates by 2 orders of magnitude.Fallback Design
All three kernels wrap the existing CPU function behind a
g_clContext != INVALID_HANDLEguard:CPU path stays untouched — zero risk of regression when GPU is unavailable.
Out of Scope
cleanOBBuffer/OnTickper-tick loop — OB count (10–50) is too small; kernel launch overhead (~50 µs) exceeds compute savingscheckForCISDEntry— only 16 loop iterations; overhead not worth itAcceptance Criteria
OnInitqueriesCLGetDeviceInfoand prints GPU name or "no GPU — CPU fallback"g_clContext/g_clProgramhandles declared inglobals.mqh, freed inOnDeinitscanHistoricalCandlesruns on GPU when available, identical OB outputGetLastSwingLow/Highbatch GPU path, identical swing levelscheckForMSSEntryGPU path, identical MSS detection results