Page 2 of 2

Re: How to ask support for historical spread data and what they send

Posted: Thu Sep 24, 2026 7:45 pm
by PTScalper
I guess that you allready know that FILE_SHARE_READ buffering—is an architectural anti-pattern. MetaTrader runs OnTick() synchronously. If the disk controller stalls, or if you are running 20 charts across 5 brokers on your HP Z1 workstation, your I/O thread will eventually bottleneck and miss ticks during high-volatility news events.

The true enterprise architecture decouples the MetaTrader client from the storage layer. We will turn MT4/MT5 into a pure, stateless telemetry sensor.

This version queues ticks in a memory buffer (an array of structs) and flushes them as a batched JSON payload via HTTP POST to a local ASP.NET Core microservice. Your C# backend can then handle the database ingestion (e.g., to MS SQL Server via Entity Framework Core) asynchronously.

Re: How to ask support for historical spread data and what they send

Posted: Thu Sep 24, 2026 7:48 pm
by PTScalper
1. The Universal MT4/MT5 Telemetry Sensor

This EA builds a raw JSON array in memory to avoid external MQL JSON library dependencies, then uses MetaTrader's native WebRequest to fire the batch to your C# API.

Note: You must add http://localhost:5000 to the allowed WebRequest URLs in MetaTrader (Tools -> Options -> Expert Advisors).

Code: Select all

//+------------------------------------------------------------------+
//|                                     EnterpriseSpreadSensor.mq4/5 |
//|                     Stateless JSON Telemetry over HTTP POST      |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026"
#property strict

//--- Inputs
input string   InpApiEndpoint    = "http://localhost:5000/api/ticks"; // C# Microservice URL
input int      InpBatchSize      = 50;                                // Ticks per JSON payload
input string   InpSessionStart   = "07:00:00";                        // UTC Start
input string   InpSessionEnd     = "10:00:00";                        // UTC End

//--- Tick Buffer Struct
struct TickRecord {
   ulong    time_msc;
   double   bid;
   double   ask;
   double   spread_pips;
};

TickRecord     g_tickQueue[];
int            g_queueCount = 0;
double         g_lastBid = 0.0;
double         g_lastAsk = 0.0;

//+------------------------------------------------------------------+
int OnInit()
{
   ArrayResize(g_tickQueue, InpBatchSize);
   Print("[Telemetry Sensor] Initialized. Target: ", InpApiEndpoint);
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
void OnTick()
{
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick)) return;

   // Filter redundant quotes
   if(tick.bid == g_lastBid && tick.ask == g_lastAsk) return;

   datetime gmtNow = TimeGMT();
   if(!IsWithinSession(gmtNow, InpSessionStart, InpSessionEnd)) return;

   double pipSize = GetPipUnit();
   double spreadPips = (pipSize > 0) ? (tick.ask - tick.bid) / pipSize : 0.0;

   // Enqueue Tick
   g_tickQueue[g_queueCount].time_msc = tick.time_msc;
   g_tickQueue[g_queueCount].bid = tick.bid;
   g_tickQueue[g_queueCount].ask = tick.ask;
   g_tickQueue[g_queueCount].spread_pips = spreadPips;
   g_queueCount++;

   g_lastBid = tick.bid;
   g_lastAsk = tick.ask;

   // Flush Batch over HTTP
   if(g_queueCount >= InpBatchSize)
   {
      FlushTelemetry();
      g_queueCount = 0;
   }
}

//+------------------------------------------------------------------+
//| HTTP POST & JSON Serialization                                   |
//+------------------------------------------------------------------+
void FlushTelemetry()
{
   // Manually build JSON array to avoid external DLLs/libraries in MT
   string json = "[";
   for(int i = 0; i < g_queueCount; i++)
   {
      string record = StringFormat("{\"symbol\":\"%s\",\"time_msc\":%I64u,\"bid\":%.5f,\"ask\":%.5f,\"spread\":%.2f}",
                                   _Symbol, g_tickQueue[i].time_msc, 
                                   g_tickQueue[i].bid, g_tickQueue[i].ask, g_tickQueue[i].spread_pips);
      json += record;
      if(i < g_queueCount - 1) json += ",";
   }
   json += "]";

   char postData[];
   StringToCharArray(json, postData, 0, WHOLE_ARRAY, CP_UTF8);
   
   // Strip trailing null terminator injected by StringToCharArray
   int dataSize = ArraySize(postData) - 1; 

   char result[];
   string resultHeaders;
   string headers = "Content-Type: application/json\r\n";

   int res = WebRequest("POST", InpApiEndpoint, headers, 1000, postData, dataSize, result, resultHeaders);
   
   if(res != 200)
   {
      Print(StringFormat("[Telemetry Error] HTTP %d. Is the C# backend running?", res));
   }
}

//+------------------------------------------------------------------+
//| Utilities                                                        |
//+------------------------------------------------------------------+
double GetPipUnit() { return (_Digits == 3 || _Digits == 5) ? _Point * 10.0 : _Point; }

bool IsWithinSession(datetime time, string startStr, string endStr)
{
   datetime sod = StringToTime(TimeToString(time, TIME_DATE));
   int currentSec = (int)(time - sod);
   
   string sParts[], eParts[];
   StringSplit(startStr, ':', sParts);
   StringSplit(endStr, ':', eParts);
   
   int startSec = (int)StringToInteger(sParts[0])*3600 + (int)StringToInteger(sParts[1])*60 + (int)StringToInteger(sParts[2]);
   int endSec = (int)StringToInteger(eParts[0])*3600 + (int)StringToInteger(eParts[1])*60 + (int)StringToInteger(eParts[2]);

   return (startSec <= endSec) ? (currentSec >= startSec && currentSec <= endSec) 
                               : (currentSec >= startSec || currentSec <= endSec);
}

Re: How to ask support for historical spread data and what they send

Posted: Thu Sep 24, 2026 7:48 pm
by PTScalper
2. The ASP.NET Core "Catcher" (High-Performance Ingestion)

To handle the incoming HTTP POSTs without blocking, we use an ASP.NET Core Minimal API coupled with System.Threading.Channels. This creates an in-memory queue on the C# side, allowing the API to instantly return HTTP 200 to MetaTrader while a background service bulk-inserts the data into MS SQL Server or a master CSV.

Create a new .NET 8 Web API project and replace Program.cs:

Code: Select all

using System.Threading.Channels;

var builder = WebApplication.CreateBuilder(args);

// Create an unbounded channel for high-throughput, non-blocking ingestion
var tickChannel = Channel.CreateUnbounded<TickRecord>();
builder.Services.AddSingleton(tickChannel.Writer);
builder.Services.AddSingleton(tickChannel.Reader);
builder.Services.AddHostedService<TickIngestionWorker>();

var app = builder.Build();

// Minimal API Endpoint - O(1) execution time, instantly frees the MT4/MT5 thread
app.MapPost("/api/ticks", async (TickRecord[] payload, ChannelWriter<TickRecord> writer) =>
{
    foreach (var tick in payload)
    {
        await writer.WriteAsync(tick);
    }
    return Results.Ok();
});

app.Run("http://localhost:5000");

// --- Models ---
public record TickRecord(string Symbol, ulong Time_msc, double Bid, double Ask, double Spread);

// --- Background Worker (Decoupled Database Writer) ---
public class TickIngestionWorker : BackgroundService
{
    private readonly ChannelReader<TickRecord> _reader;
    private readonly ILogger<TickIngestionWorker> _logger;

    public TickIngestionWorker(ChannelReader<TickRecord> reader, ILogger<TickIngestionWorker> logger)
    {
        _reader = reader;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Tick Ingestion Worker started.");

        // Read dynamically as ticks arrive from the channel
        await foreach (var tick in _reader.ReadAllAsync(stoppingToken))
        {
            // TODO: Map to Entity Framework Core -> MS SQL Server
            // or batch write to a daily partitioned CSV file.
            
            _logger.LogInformation("[{Symbol}] Spread: {Spread} | Bid: {Bid}", tick.Symbol, tick.Spread, tick.Bid);
        }
    }
}

Re: How to ask support for historical spread data and what they send

Posted: Thu Sep 24, 2026 7:49 pm
by PTScalper
Here is the pure MQL4 implementation. Because MT4 Expert Advisors are strictly single-threaded, WebRequest blocks the entire EA execution until the HTTP response is received. This makes the decoupled architecture even more critical for MT4 than MT5—your C# backend must ingest the payload and return an HTTP status immediately.

1. The MT4 Sensor (SpreadTelemetry.mq4)
This script is compiled specifically for the MetaEditor 4 compiler. It builds the JSON payload natively to avoid relying on external MT4 DLLs that often break across terminal updates.

Code: Select all

//+------------------------------------------------------------------+
//|                                              SpreadTelemetry.mq4 |
//|                     Stateless JSON Telemetry over HTTP POST      |
//+------------------------------------------------------------------+
#property copyright "Enterprise Telemetry"
#property strict

//--- Inputs
input string   InpApiEndpoint    = "http://localhost:5000/api/ticks"; 
input int      InpBatchSize      = 50;                                
input string   InpSessionStart   = "07:00:00";                        
input string   InpSessionEnd     = "10:00:00";                        

//--- Structs & Globals
struct TickRecord {
   long     time_msc;
   double   bid;
   double   ask;
   double   spread_pips;
};

TickRecord     g_tickQueue[];
int            g_queueCount = 0;
double         g_lastBid = 0.0;
double         g_lastAsk = 0.0;

//+------------------------------------------------------------------+
int OnInit()
{
   ArrayResize(g_tickQueue, InpBatchSize);
   Print("[MT4 Telemetry] Initialized. Target: ", InpApiEndpoint);
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
void OnTick()
{
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick)) return;

   if(tick.bid == g_lastBid && tick.ask == g_lastAsk) return;

   datetime gmtNow = TimeGMT();
   if(!IsWithinSession(gmtNow, InpSessionStart, InpSessionEnd)) return;

   double pipSize = (_Digits == 3 || _Digits == 5) ? _Point * 10.0 : _Point;
   double spreadPips = (pipSize > 0) ? (tick.ask - tick.bid) / pipSize : 0.0;

   // Enqueue
   g_tickQueue[g_queueCount].time_msc = tick.time_msc;
   g_tickQueue[g_queueCount].bid = tick.bid;
   g_tickQueue[g_queueCount].ask = tick.ask;
   g_tickQueue[g_queueCount].spread_pips = spreadPips;
   g_queueCount++;

   g_lastBid = tick.bid;
   g_lastAsk = tick.ask;

   if(g_queueCount >= InpBatchSize)
   {
      FlushTelemetry();
      g_queueCount = 0;
   }
}

//+------------------------------------------------------------------+
void FlushTelemetry()
{
   string json = "[";
   for(int i = 0; i < g_queueCount; i++)
   {
      string record = StringFormat("{\"symbol\":\"%s\",\"time_msc\":%I64d,\"bid\":%.5f,\"ask\":%.5f,\"spread\":%.2f}",
                                   _Symbol, g_tickQueue[i].time_msc, 
                                   g_tickQueue[i].bid, g_tickQueue[i].ask, g_tickQueue[i].spread_pips);
      json += record;
      if(i < g_queueCount - 1) json += ",";
   }
   json += "]";

   char postData[];
   StringToCharArray(json, postData, 0, WHOLE_ARRAY, CP_UTF8);
   
   // MT4 StringToCharArray appends a null terminator, strip it for clean JSON
   int dataSize = ArraySize(postData) - 1; 

   char result[];
   string resultHeaders;
   string headers = "Content-Type: application/json\r\n";

   // Timeout set to 500ms to prevent MT4 thread locking
   int res = WebRequest("POST", InpApiEndpoint, headers, 500, postData, dataSize, result, resultHeaders);
   
   if(res != 200)
   {
      if(res == 4014) 
         Print("[MT4 Telemetry] ERR 4014: Add ", InpApiEndpoint, " to allowed WebRequest URLs in Options -> Expert Advisors.");
      else 
         Print("[MT4 Telemetry] HTTP Error: ", res);
   }
}

//+------------------------------------------------------------------+
bool IsWithinSession(datetime time, string startStr, string endStr)
{
   datetime sod = StringToTime(TimeToString(time, TIME_DATE));
   int currentSec = (int)(time - sod);
   
   string sParts[], eParts[];
   StringSplit(startStr, ':', sParts);
   StringSplit(endStr, ':', eParts);
   
   int startSec = (int)StringToInteger(sParts[0])*3600 + (int)StringToInteger(sParts[1])*60 + (int)StringToInteger(sParts[2]);
   int endSec = (int)StringToInteger(eParts[0])*3600 + (int)StringToInteger(eParts[1])*60 + (int)StringToInteger(eParts[2]);

   return (startSec <= endSec) ? (currentSec >= startSec && currentSec <= endSec) 
                               : (currentSec >= startSec || currentSec <= endSec);
}

Re: How to ask support for historical spread data and what they send

Posted: Thu Sep 24, 2026 7:49 pm
by PTScalper
2. The ASP.NET Core Ingestion Service (EF Core & MS SQL)

Because MT4 will freeze if the HTTP response is delayed, the C# API acts solely as a high-speed router. It accepts the JSON, drops it into an unbounded memory channel, and instantly returns 200 OK. A background IHostedService asynchronously flushes the channel into an MS SQL Server database via Entity Framework Core.

Code: Select all

using System.Threading.Channels;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Configure Entity Framework Core for MS SQL Server
builder.Services.AddDbContext<TelemetryDbContext>(options =>
    options.UseSqlServer("Server=localhost;Database=MarketTelemetry;Trusted_Connection=True;TrustServerCertificate=True;"));

// Create high-throughput channel
var tickChannel = Channel.CreateUnbounded<TickRecord>();
builder.Services.AddSingleton(tickChannel.Writer);
builder.Services.AddSingleton(tickChannel.Reader);

// Register Background Worker
builder.Services.AddHostedService<SqlIngestionWorker>();

var app = builder.Build();

// O(1) Minimal API - Unblocks the MT4 thread instantly
app.MapPost("/api/ticks", async (TickRecord[] payload, ChannelWriter<TickRecord> writer) =>
{
    foreach (var tick in payload)
    {
        await writer.WriteAsync(tick);
    }
    return Results.Ok();
});

app.Run("http://localhost:5000");

// --- Models ---
public record TickRecord(string Symbol, long Time_msc, double Bid, double Ask, double Spread);

public class TelemetryEntity
{
    public int Id { get; set; }
    public string Symbol { get; set; } = string.Empty;
    public DateTime TimestampUtc { get; set; }
    public double Bid { get; set; }
    public double Ask { get; set; }
    public double SpreadPips { get; set; }
}

public class TelemetryDbContext : DbContext
{
    public TelemetryDbContext(DbContextOptions<TelemetryDbContext> options) : base(options) { }
    public DbSet<TelemetryEntity> Ticks { get; set; }
}

// --- Background Worker ---
public class SqlIngestionWorker : BackgroundService
{
    private readonly ChannelReader<TickRecord> _reader;
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<SqlIngestionWorker> _logger;

    public SqlIngestionWorker(ChannelReader<TickRecord> reader, IServiceProvider serviceProvider, ILogger<SqlIngestionWorker> logger)
    {
        _reader = reader;
        _serviceProvider = serviceProvider;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("SQL Ingestion Worker Active.");

        while (!stoppingToken.IsCancellationRequested)
        {
            var batch = new List<TelemetryEntity>();

            // Read up to 500 records at a time from the channel
            while (batch.Count < 500 && _reader.TryRead(out var tick))
            {
                batch.Add(new TelemetryEntity
                {
                    Symbol = tick.Symbol,
                    TimestampUtc = DateTimeOffset.FromUnixTimeMilliseconds(tick.Time_msc).UtcDateTime,
                    Bid = tick.Bid,
                    Ask = tick.Ask,
                    SpreadPips = tick.Spread
                });
            }

            if (batch.Any())
            {
                using var scope = _serviceProvider.CreateScope();
                var dbContext = scope.ServiceProvider.GetRequiredService<TelemetryDbContext>();
                
                await dbContext.Ticks.AddRangeAsync(batch, stoppingToken);
                await dbContext.SaveChangesAsync(stoppingToken);
                
                _logger.LogInformation("Inserted {Count} ticks into MS SQL Database.", batch.Count);
            }
            else
            {
                // Wait for data if channel is empty
                await _reader.WaitToReadAsync(stoppingToken); 
            }
        }
    }
}

Re: How to ask support for historical spread data and what they send

Posted: Thu Sep 24, 2026 7:50 pm
by PTScalper
Here is the pure MT5 implementation. MetaTrader 5 offers a much more robust execution environment than MT4, but EAs still run synchronously on a single thread per chart. To prevent the EA from missing ticks during NFP or high-liquidity sweeps, the WebRequest timeout is kept ultra-short, and the batching architecture remains strictly decoupled.

I have updated the headers and optimized the MQL5 syntax specifically for MT5’s memory management.

Re: How to ask support for historical spread data and what they send

Posted: Thu Sep 24, 2026 7:50 pm
by PTScalper
1. The MT5 Sensor (SpreadTelemetry.mq5)

Because this avoids external DLLs or third-party JSON libraries, you can compile this directly in MetaEditor 5 and drop it onto any chart.

Code: Select all

//+------------------------------------------------------------------+
//|                                              SpreadTelemetry.mq5 |
//|                               Copyright 2026, AI Profi Solutions |
//|                                     https://aiprofisolutions.com |
//+------------------------------------------------------------------+
#property strict

//--- Inputs
input string   InpApiEndpoint    = "http://localhost:5000/api/ticks"; 
input int      InpBatchSize      = 50;                                
input string   InpSessionStart   = "07:00:00";                        
input string   InpSessionEnd     = "10:00:00";                        

//--- Structs & Globals
struct TickRecord {
   ulong    time_msc;
   double   bid;
   double   ask;
   double   spread_pips;
};

TickRecord     g_tickQueue[];
int            g_queueCount = 0;
double         g_lastBid = 0.0;
double         g_lastAsk = 0.0;

//+------------------------------------------------------------------+
int OnInit()
{
   ArrayResize(g_tickQueue, InpBatchSize);
   Print("[MT5 Telemetry] Initialized. Target: ", InpApiEndpoint);
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
void OnTick()
{
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick)) return;

   // Ignore ticks where bid/ask haven't changed (e.g., volume-only updates)
   if(tick.bid == g_lastBid && tick.ask == g_lastAsk) return;

   datetime gmtNow = TimeTradeServer();
   if(!IsWithinSession(gmtNow, InpSessionStart, InpSessionEnd)) return;

   double pipSize = (_Digits == 3 || _Digits == 5) ? _Point * 10.0 : _Point;
   double spreadPips = (pipSize > 0) ? (tick.ask - tick.bid) / pipSize : 0.0;

   // Enqueue
   g_tickQueue[g_queueCount].time_msc = tick.time_msc;
   g_tickQueue[g_queueCount].bid = tick.bid;
   g_tickQueue[g_queueCount].ask = tick.ask;
   g_tickQueue[g_queueCount].spread_pips = spreadPips;
   g_queueCount++;

   g_lastBid = tick.bid;
   g_lastAsk = tick.ask;

   if(g_queueCount >= InpBatchSize)
   {
      FlushTelemetry();
      g_queueCount = 0;
   }
}

//+------------------------------------------------------------------+
void FlushTelemetry()
{
   // Manual JSON array construction for zero-dependency compilation
   string json = "[";
   for(int i = 0; i < g_queueCount; i++)
   {
      string record = StringFormat("{\"symbol\":\"%s\",\"time_msc\":%I64u,\"bid\":%.5f,\"ask\":%.5f,\"spread\":%.2f}",
                                   _Symbol, g_tickQueue[i].time_msc, 
                                   g_tickQueue[i].bid, g_tickQueue[i].ask, g_tickQueue[i].spread_pips);
      json += record;
      if(i < g_queueCount - 1) json += ",";
   }
   json += "]";

   char postData[];
   StringToCharArray(json, postData, 0, WHOLE_ARRAY, CP_UTF8);
   
   // StringToCharArray appends a null terminator, strip it for clean JSON payload
   int dataSize = ArraySize(postData) - 1; 

   char result[];
   string resultHeaders;
   string headers = "Content-Type: application/json\r\n";

   // Timeout set to 500ms. MT5 will not block longer than this.
   int res = WebRequest("POST", InpApiEndpoint, headers, 500, postData, dataSize, result, resultHeaders);
   
   if(res != 200)
   {
      if(res == 4014) 
         Print("[MT5 Telemetry] ERR 4014: Add ", InpApiEndpoint, " to allowed WebRequest URLs in Tools -> Options -> Expert Advisors.");
      else 
         Print("[MT5 Telemetry] HTTP Error: ", res);
   }
}

//+------------------------------------------------------------------+
bool IsWithinSession(datetime time, string startStr, string endStr)
{
   datetime sod = time - (time % 86400); // Fast MT5 Start of Day calculation
   int currentSec = (int)(time - sod);
   
   string sParts[], eParts[];
   StringSplit(startStr, ':', sParts);
   StringSplit(endStr, ':', eParts);
   
   int startSec = (int)StringToInteger(sParts[0])*3600 + (int)StringToInteger(sParts[1])*60 + (int)StringToInteger(sParts[2]);
   int endSec = (int)StringToInteger(eParts[0])*3600 + (int)StringToInteger(eParts[1])*60 + (int)StringToInteger(eParts[2]);

   return (startSec <= endSec) ? (currentSec >= startSec && currentSec <= endSec) 
                               : (currentSec >= startSec || currentSec <= endSec);
}

Re: How to ask support for historical spread data and what they send

Posted: Thu Sep 24, 2026 7:50 pm
by PTScalper
2. The ASP.NET Core Ingestion Service

For a multi-core workstation like the HP Z1, this C# backend fully utilizes hardware concurrency. The Minimal API instantly accepts the JSON payload and pushes it into an unbounded memory channel (returning 200 OK to MT5 in less than 2 milliseconds).

A background worker service then pulls from that channel and performs bulk inserts into your local MS SQL Server instance via Entity Framework Core.

Code: Select all

using System.Threading.Channels;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Configure Entity Framework Core for MS SQL Server
builder.Services.AddDbContext<TelemetryDbContext>(options =>
    options.UseSqlServer("Server=localhost;Database=MarketTelemetry;Trusted_Connection=True;TrustServerCertificate=True;"));

// Create high-throughput channel for thread decoupling
var tickChannel = Channel.CreateUnbounded<TickRecord>();
builder.Services.AddSingleton(tickChannel.Writer);
builder.Services.AddSingleton(tickChannel.Reader);

// Register Background Worker
builder.Services.AddHostedService<SqlIngestionWorker>();

var app = builder.Build();

// O(1) Minimal API - Unblocks the MT5 thread instantly
app.MapPost("/api/ticks", async (TickRecord[] payload, ChannelWriter<TickRecord> writer) =>
{
    foreach (var tick in payload)
    {
        await writer.WriteAsync(tick);
    }
    return Results.Ok();
});

app.Run("http://localhost:5000");

// --- Models ---
public record TickRecord(string Symbol, ulong Time_msc, double Bid, double Ask, double Spread);

public class TelemetryEntity
{
    public int Id { get; set; }
    public string Symbol { get; set; } = string.Empty;
    public DateTime TimestampUtc { get; set; }
    public double Bid { get; set; }
    public double Ask { get; set; }
    public double SpreadPips { get; set; }
}

public class TelemetryDbContext : DbContext
{
    public TelemetryDbContext(DbContextOptions<TelemetryDbContext> options) : base(options) { }
    public DbSet<TelemetryEntity> Ticks { get; set; }
}

// --- Background Worker ---
public class SqlIngestionWorker : BackgroundService
{
    private readonly ChannelReader<TickRecord> _reader;
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<SqlIngestionWorker> _logger;

    public SqlIngestionWorker(ChannelReader<TickRecord> reader, IServiceProvider serviceProvider, ILogger<SqlIngestionWorker> logger)
    {
        _reader = reader;
        _serviceProvider = serviceProvider;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("SQL Ingestion Worker Active.");

        while (!stoppingToken.IsCancellationRequested)
        {
            var batch = new List<TelemetryEntity>();

            // Read up to 1000 records at a time from the channel
            while (batch.Count < 1000 && _reader.TryRead(out var tick))
            {
                batch.Add(new TelemetryEntity
                {
                    Symbol = tick.Symbol,
                    TimestampUtc = DateTimeOffset.FromUnixTimeMilliseconds((long)tick.Time_msc).UtcDateTime,
                    Bid = tick.Bid,
                    Ask = tick.Ask,
                    SpreadPips = tick.Spread
                });
            }

            if (batch.Any())
            {
                using var scope = _serviceProvider.CreateScope();
                var dbContext = scope.ServiceProvider.GetRequiredService<TelemetryDbContext>();
                
                // Bulk insert into MS SQL Server
                await dbContext.Ticks.AddRangeAsync(batch, stoppingToken);
                await dbContext.SaveChangesAsync(stoppingToken);
                
                _logger.LogInformation("Inserted {Count} ticks into MS SQL Database.", batch.Count);
            }
            else
            {
                // Yield thread if channel is empty
                await _reader.WaitToReadAsync(stoppingToken); 
            }
        }
    }
}