Skip to content

feat: OpenCL GPU acceleration — parallel OB detection, MSS scanning, swing detection #41

Description

@cfournel

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:

  1. Compute swing levels via parallel reduction (Kernel 2 above).
  2. Evaluate the backward break-level scan on GPU — each candle tested independently, FVG condition inlined.
  3. 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

  • OnInit queries CLGetDeviceInfo and prints GPU name or "no GPU — CPU fallback"
  • g_clContext / g_clProgram handles declared in globals.mqh, freed in OnDeinit
  • Kernel 1: scanHistoricalCandles runs on GPU when available, identical OB output
  • Kernel 2: GetLastSwingLow/High batch GPU path, identical swing levels
  • Kernel 3: checkForMSSEntry GPU path, identical MSS detection results
  • All three kernels fall back to existing CPU functions if GPU unavailable
  • Backtest regression check: same trade count and results as CPU baseline

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions