Page 10 of 13
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 2:00 pm
by PTScalper
Here is the complete C# architecture to batch your basket liquidations:
Code: Select all
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using cAlgo.API;
namespace cAlgo.Robots
{
public partial class InteractiveFiatHUD : Robot
{
[Parameter("Batch Window (Milliseconds)", Group = "Telegram API", DefaultValue = 1000)]
public int BatchWindowMs { get; set; }
// 1. Holding collection for trades that closed in rapid succession
private List<HistoricalTrade> _pendingBatchTrades = new List<HistoricalTrade>();
// 2. Tracks the exact local time the last trade closed
private DateTime _lastTradeClosedTime;
protected override void OnStart()
{
Positions.Closed += OnPositionClosed;
// 3. Start a native cTrader timer to check the batch status twice a second
Timer.Start(TimeSpan.FromMilliseconds(500));
}
private void OnPositionClosed(PositionClosedEventArgs args)
{
if (args.Reason == PositionCloseReason.StopLoss)
{
var historicalTrade = History.FirstOrDefault(t => t.PositionId == args.Position.Id);
if (historicalTrade != null)
{
// Add the trade to the holding list instead of capturing immediately
_pendingBatchTrades.Add(historicalTrade);
// Reset the debouncer countdown using local system time
_lastTradeClosedTime = DateTime.UtcNow;
}
}
}
protected override void OnTimer()
{
// If the holding list is empty, do nothing
if (_pendingBatchTrades.Count == 0) return;
// If the time since the LAST closed trade exceeds your Batch Window, execute the capture
if ((DateTime.UtcNow - _lastTradeClosedTime).TotalMilliseconds >= BatchWindowMs)
{
ProcessBatchedScreenshots();
}
}
private void ProcessBatchedScreenshots()
{
// 1. Copy the list and instantly clear the original to safely catch any new trades
// that might close while we are generating the screenshot.
var tradesToProcess = _pendingBatchTrades.ToList();
_pendingBatchTrades.Clear();
List<string> temporaryChartObjects = new List<string>();
double totalNetPnL = 0;
// 2. Build the Telegram Caption Header
StringBuilder captionBuilder = new StringBuilder();
captionBuilder.AppendLine($"🛑 <b>BASKET LIQUIDATED ({tradesToProcess.Count} Trades)</b>");
// 3. Iterate through the batch to draw lines and aggregate data
for (int i = 0; i < tradesToProcess.Count; i++)
{
var t = tradesToProcess[i];
totalNetPnL += t.NetProfit;
// Telegram image captions have a hard limit of 1024 characters.
// If a grid closes 30 trades, we only detail the first 10 to avoid API rejection.
if (i < 10)
{
captionBuilder.AppendLine($"• {t.SymbolName} {t.TradeType}: {t.NetProfit:C2}");
}
else if (i == 10)
{
captionBuilder.AppendLine($"<i>...and {tradesToProcess.Count - 10} more.</i>");
}
// Draw the execution line for this specific trade
string lineName = $"BatchSL_{t.PositionId}";
Chart.DrawTrendLine(lineName, t.EntryTime, t.EntryPrice, t.ClosingTime, t.ClosingPrice, Color.Crimson, 2, LineStyle.Solid);
temporaryChartObjects.Add(lineName);
}
captionBuilder.AppendLine($"\n<b>Total Net Impact:</b> {totalNetPnL:C2}");
// 4. Thread Block & Capture
Thread.Sleep(50);
byte[] screenshotBytes = CaptureScreenToMemory();
// 5. Cleanup the chart
foreach (var obj in temporaryChartObjects)
{
Chart.RemoveObject(obj);
}
// 6. Enqueue the single combined payload
if (screenshotBytes != null)
{
Print($"[Batch Processed] Captured {tradesToProcess.Count} trades. Enqueuing single Telegram payload.");
_uploadQueue.Enqueue(new TelegramPayload
{
ImageBytes = screenshotBytes,
Caption = captionBuilder.ToString()
});
Task.Run(() => ProcessUploadQueueAsync());
}
}
}
}
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 2:01 pm
by PTScalper
Architectural Mechanics
The Debouncer (DateTime.UtcNow vs Server.Time): We strictly use DateTime.UtcNow to track the countdown interval rather than the broker's Server.Time. Server.Time only advances when a new price tick arrives. If a massive stop-out occurs at exactly 21:59:59 on a Friday, the market closes immediately after. Server.Time freezes, and the batch would remain permanently stuck in memory all weekend. UtcNow ensures the timer runs in real-time regardless of market liquidity.
Main-Thread UI Safety: OnPositionClosed and OnTimer both run exclusively on the cBot's main execution thread. Because they share the same thread, adding items to the generic List<HistoricalTrade> in one method and clearing it in the other is 100% thread-safe without requiring complex lock{} blocks.
Caption Truncation: Telegram enforces a strict 1024-character limit for sendPhoto captions. If you dump 25 trades into the StringBuilder, the string will exceed the limit, and the Telegram API will throw an HTTP 400 Bad Request, causing the screenshot to fail silently. The logic if (i < 10) ensures you get a detailed summary of the primary trades and a clean mathematical aggregate (Total Net Impact) without breaking the API constraints.
Decoupled Architecture: This batching logic sits purely on the front-end data collection side. It interfaces perfectly with the ConcurrentQueue and ProcessUploadQueueAsync background worker you already built, meaning your network safety, circuit breakers, and RAM protections remain fully intact.
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 2:04 pm
by PTScalper
To silently append these trades to a local CSV file without interrupting your execution engine, you must apply the same Thread Decoupling principles we used for the Telegram API.
Writing to a physical hard drive involves Disk I/O. If your SSD is busy or your antivirus scans the file at the exact moment cTrader attempts to write to it, a synchronous write operation will freeze your bot for several milliseconds.
To prevent file-locking crashes when multiple batches try to write to the same file simultaneously, we implement a dedicated SemaphoreSlim specifically for the CSV file.
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 2:04 pm
by PTScalper
Here is the isolated architecture to inject into your cBot:
Code: Select all
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using cAlgo.API;
namespace cAlgo.Robots
{
public partial class InteractiveFiatHUD : Robot
{
[Parameter("Enable CSV Logging", Group = "Local Logging", DefaultValue = true)]
public bool EnableCsvLogging { get; set; }
[Parameter("CSV Directory Path", Group = "Local Logging", DefaultValue = @"C:\cTrader_Logs")]
public string CsvDirectoryPath { get; set; }
// Dedicated asynchronous lock to prevent "File in Use" IOExceptions
private SemaphoreSlim _csvFileLock = new SemaphoreSlim(1, 1);
/// <summary>
/// Inject this into your existing ProcessBatchedScreenshots method.
/// </summary>
private void ProcessBatchedScreenshots()
{
var tradesToProcess = _pendingBatchTrades.ToList();
_pendingBatchTrades.Clear();
// 1. FIRE AND FORGET THE CSV LOGGER
// Pass the copied list to a background thread so disk I/O doesn't block the main thread.
if (EnableCsvLogging)
{
Task.Run(() => AppendBatchedTradesToCsvAsync(tradesToProcess));
}
// ... (Your existing Telegram string building and screenshot capture logic continues here instantly)
}
/// <summary>
/// Asynchronously creates the file/headers if missing, and appends the new trade rows.
/// </summary>
private async Task AppendBatchedTradesToCsvAsync(List<HistoricalTrade> batchedTrades)
{
// 2. THE FILE LOCK
// Wait for access if another thread is currently writing to the CSV.
await _csvFileLock.WaitAsync();
try
{
// Ensure the target directory exists on the hard drive
if (!Directory.Exists(CsvDirectoryPath))
{
Directory.CreateDirectory(CsvDirectoryPath);
}
// Organize logs by month (e.g., "BasketLogs_2026_10.csv") to prevent massive, bloated files
string fileName = $"BasketLogs_{DateTime.Now:yyyy_MM}.csv";
string fullPath = Path.Combine(CsvDirectoryPath, fileName);
// Check if the file is brand new so we can inject headers
bool isNewFile = !File.Exists(fullPath);
// 3. ASYNCHRONOUS DISK STREAM
// 'append: true' ensures we add to the end of the file rather than overwriting it
using (StreamWriter writer = new StreamWriter(fullPath, append: true, Encoding.UTF8))
{
if (isNewFile)
{
// Write standard CSV headers
await writer.WriteLineAsync("CloseTime_UTC,PositionId,Symbol,TradeType,Volume,EntryPrice,ClosePrice,NetProfit,Duration_Secs");
}
// 4. BATCH WRITING
foreach (var t in batchedTrades)
{
// Calculate total duration in seconds for deeper post-analysis
double durationSeconds = Math.Round((t.ClosingTime - t.EntryTime).TotalSeconds, 1);
// Build the comma-separated row.
// ISO 8601 formatting ("O") ensures Excel and databases parse the date perfectly.
string csvRow = $"{t.ClosingTime:O},{t.PositionId},{t.SymbolName},{t.TradeType}," +
$"{t.VolumeInUnits},{t.EntryPrice},{t.ClosingPrice},{t.NetProfit},{durationSeconds}";
await writer.WriteLineAsync(csvRow);
}
}
}
catch (Exception ex)
{
// Swallow the exception so background disk errors don't crash the bot
Print($"[CSV Logger Error] Failed to write to {CsvDirectoryPath}: {ex.Message}");
}
finally
{
// 5. RELEASE THE LOCK
_csvFileLock.Release();
}
}
}
}
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 2:05 pm
by PTScalper
Architectural Mechanics
Asynchronous File Locking (_csvFileLock): A standard C# lock{} statement completely blocks the active thread until the resource is free, which defeats the purpose of async/await. SemaphoreSlim.WaitAsync() tells the .NET engine: "If the file is currently busy, put this Task to sleep, go do other things, and wake this Task back up when the file is free." This guarantees 100% thread safety without wasting a single CPU cycle.
Monthly File Partitioning: Appending to a CSV file that has 500,000 rows becomes progressively slower over time, and opening it in Excel becomes cumbersome. The logic $"BasketLogs_{DateTime.Now:yyyy_MM}.csv" acts as a self-maintaining log rotation. On the 1st of every month, the bot automatically spawns a fresh CSV file and injects the headers, keeping your data perfectly organized by month.
ISO 8601 Time Formatting ({t.ClosingTime:O}): Date formatting is a common pitfall in CSVs. If you use standard ToString(), Excel might interpret 10/08/2026 as October 8th or August 10th depending on your regional Windows settings. The :O parameter forces the strict ISO format (2026-10-08T16:03:00.0000000Z), completely eliminating chronological ambiguity when you import the data into Python (Pandas) or Excel later.
The @ Directory Literal: Windows file paths rely on backslashes (\). In C#, a backslash is an escape character. Prefixing the string with @ (@"C:\cTrader_Logs") converts it into a verbatim string literal, allowing you to bypass double-escaping ("C:\\cTrader_Logs").
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 2:08 pm
by PTScalper
Appending to an HTML file introduces a structural challenge that a CSV does not have: The Closing Tag Dilemma. A CSV is a flat text stream where you simply append a new line to the bottom. An HTML document is a closed DOM tree ending with </tbody></table></body></html>. If you just append text to the end of the file, it lands outside the HTML body and the page breaks.
To solve this while mimicking the classic MetaTrader 4/5 visual aesthetic (Tahoma font, grey borders, colored Buy/Sell text), we must read the existing file, locate the </tbody> tag, inject the new rows exactly at that injection point, and rewrite the file.
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 2:09 pm
by PTScalper
Here is the complete C# architecture to generate and append to a native-looking MT4/MT5 HTML statement dynamically:
Code: Select all
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using cAlgo.API;
namespace cAlgo.Robots
{
public partial class InteractiveFiatHUD : Robot
{
[Parameter("Enable HTML Logging", Group = "Local Logging", DefaultValue = true)]
public bool EnableHtmlLogging { get; set; }
[Parameter("Statement Directory", Group = "Local Logging", DefaultValue = @"C:\cTrader_Statements")]
public string HtmlDirectoryPath { get; set; }
// Dedicated lock for the HTML file
private SemaphoreSlim _htmlFileLock = new SemaphoreSlim(1, 1);
private void ProcessBatchedScreenshots()
{
var tradesToProcess = _pendingBatchTrades.ToList();
_pendingBatchTrades.Clear();
// Fire and forget the HTML exporter
if (EnableHtmlLogging)
{
Task.Run(() => AppendBatchedTradesToHtmlAsync(tradesToProcess));
}
// ... (Your Telegram snapshot logic continues here)
}
private async Task AppendBatchedTradesToHtmlAsync(List<HistoricalTrade> batchedTrades)
{
await _htmlFileLock.WaitAsync();
try
{
if (!Directory.Exists(HtmlDirectoryPath))
{
Directory.CreateDirectory(HtmlDirectoryPath);
}
// Rotate logs monthly to prevent massive DOM manipulation slow-downs
string fileName = $"DetailedStatement_{DateTime.Now:yyyy_MM}.html";
string fullPath = Path.Combine(HtmlDirectoryPath, fileName);
bool isNewFile = !File.Exists(fullPath);
// 1. Build the HTML Rows for the current batch
StringBuilder newRows = new StringBuilder();
foreach (var t in batchedTrades)
{
// MT4 Aesthetic: Blue for Buy, Red for Sell
string typeColor = t.TradeType == TradeType.Buy ? "#0000ff" : "#ff0000";
string typeString = t.TradeType.ToString().ToLower();
// MT4 formats volume differently (e.g., 100,000 units = 1.00 lots)
double lotSize = t.VolumeInUnits / 100000.0;
// Construct the table row
newRows.AppendLine($"<tr align=right>");
newRows.AppendLine($"<td>{t.PositionId}</td>");
newRows.AppendLine($"<td>{t.EntryTime:yyyy.MM.dd HH:mm:ss}</td>");
newRows.AppendLine($"<td style='color:{typeColor}'>{typeString}</td>");
newRows.AppendLine($"<td>{lotSize:F2}</td>");
newRows.AppendLine($"<td align=left>{t.SymbolName}</td>");
newRows.AppendLine($"<td>{t.EntryPrice}</td>");
newRows.AppendLine($"<td>{t.ClosingTime:yyyy.MM.dd HH:mm:ss}</td>");
newRows.AppendLine($"<td>{t.ClosingPrice}</td>");
newRows.AppendLine($"<td>{t.NetProfit:F2}</td>");
newRows.AppendLine($"</tr>");
}
// 2. Handle File Injection
if (isNewFile)
{
// If file is missing, build the full MT4 HTML skeleton and CSS
StringBuilder htmlDoc = new StringBuilder();
htmlDoc.AppendLine("<!DOCTYPE html><html><head><title>Statement</title>");
htmlDoc.AppendLine("<style>");
htmlDoc.AppendLine("body { font-family: Tahoma, sans-serif; font-size: 11px; margin: 20px; }");
htmlDoc.AppendLine("table { width: 100%; border-collapse: collapse; }");
htmlDoc.AppendLine("td, th { border: 1px solid #c0c0c0; padding: 3px; text-align: right; }");
htmlDoc.AppendLine("th { background-color: #e0e0e0; font-weight: bold; text-align: center; }");
htmlDoc.AppendLine(".header-table { width: 100%; border: none; margin-bottom: 20px; font-size: 16px; font-weight: bold; }");
htmlDoc.AppendLine(".header-table td { border: none; text-align: left; background-color: transparent; }");
htmlDoc.AppendLine("</style></head><body>");
// Account Header Info
htmlDoc.AppendLine("<table class='header-table'><tr>");
htmlDoc.AppendLine($"<td>Account: {Account.Number}</td>");
htmlDoc.AppendLine($"<td style='text-align:right'>Currency: {Account.Asset.Name}</td>");
htmlDoc.AppendLine("</tr></table>");
// Trade Table Headers
htmlDoc.AppendLine("<table><thead><tr>");
htmlDoc.AppendLine("<th>Ticket</th><th>Open Time</th><th>Type</th><th>Size</th><th>Item</th>");
htmlDoc.AppendLine("<th>Price</th><th>Close Time</th><th>Price</th><th>Profit</th>");
htmlDoc.AppendLine("</tr></thead>");
// Body Injection Point
htmlDoc.AppendLine("<tbody>");
htmlDoc.Append(newRows.ToString());
htmlDoc.AppendLine("</tbody></table>");
htmlDoc.AppendLine("</body></html>");
await File.WriteAllTextAsync(fullPath, htmlDoc.ToString());
}
else
{
// 3. The HTML Injection Method
// Read the entire existing DOM into memory
string existingHtml = await File.ReadAllTextAsync(fullPath);
// Locate the closing body tag of the table
string injectionTarget = "</tbody>";
if (existingHtml.Contains(injectionTarget))
{
// Replace the closing tag with the new rows + a new closing tag
string updatedHtml = existingHtml.Replace(injectionTarget, newRows.ToString() + "\n</tbody>");
// Overwrite the file with the updated DOM
await File.WriteAllTextAsync(fullPath, updatedHtml);
}
}
}
catch (Exception ex)
{
Print($"[HTML Logger Error] Failed to update statement: {ex.Message}");
}
finally
{
_htmlFileLock.Release();
}
}
}
}
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 2:09 pm
by PTScalper
Architectural Mechanics
The DOM Injection Engine (existingHtml.Replace): Unlike a CSV StreamWriter that locks a file and streams bytes to the end of it, this code loads the entire HTML string into RAM, finds the </tbody> string, shoves the new <tr> blocks directly above it, and saves the file back to disk. Because you are rotating the files monthly (via yyyy_MM), the file size will rarely exceed a few megabytes. C# string manipulation will execute this replacement in less than 2 milliseconds, making it highly efficient.
Lot Size Translation (t.VolumeInUnits / 100000.0): cTrader natively reports positions in raw unit volume (e.g., 10,000). MT4/MT5 natively reports positions in standardized lots (e.g., 0.10). The division translates the raw cTrader data into the exact format a MetaTrader user expects to see on the report.
Inline Styling Parity: The <style> block directly mimics the native MT4 stylesheet. The Tahoma font at 11px, the #c0c0c0 grey table borders, and the #e0e0e0 background headers perfectly recreate the visual weight of a legacy MetaQuotes terminal report.
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 2:10 pm
by PTScalper
To solve the DOM manipulation challenge of updating a total below a growing list of trades, you must implement an HTML Comment Anchor strategy.
Because you cannot simply append a summary to the end of an HTML file (it would fall outside the </body> tag and break the page), you wrap the summary block in invisible HTML comments: <!-- SUMMARY START --> and <!-- SUMMARY END -->. When a new batch arrives, the bot uses a Regular Expression (Regex) to locate that exact block, delete it, and inject a freshly calculated summary block in its place.
Re: Spread Filters for Scalping: When Not to Enter
Posted: Thu Oct 08, 2026 2:10 pm
by PTScalper
Here is the updated architecture that natively queries your account state and dynamically replaces the summary footer:
Code: Select all
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using cAlgo.API;
namespace cAlgo.Robots
{
public partial class InteractiveFiatHUD : Robot
{
[Parameter("Enable HTML Logging", Group = "Local Logging", DefaultValue = true)]
public bool EnableHtmlLogging { get; set; }
[Parameter("Statement Directory", Group = "Local Logging", DefaultValue = @"C:\cTrader_Statements")]
public string HtmlDirectoryPath { get; set; }
private SemaphoreSlim _htmlFileLock = new SemaphoreSlim(1, 1);
private async Task AppendBatchedTradesToHtmlAsync(List<HistoricalTrade> batchedTrades)
{
await _htmlFileLock.WaitAsync();
try
{
if (!Directory.Exists(HtmlDirectoryPath)) Directory.CreateDirectory(HtmlDirectoryPath);
string fileName = $"DetailedStatement_{DateTime.Now:yyyy_MM}.html";
string fullPath = Path.Combine(HtmlDirectoryPath, fileName);
bool isNewFile = !File.Exists(fullPath);
// 1. Build the new trade rows
StringBuilder newRows = new StringBuilder();
foreach (var t in batchedTrades)
{
string typeColor = t.TradeType == TradeType.Buy ? "#0000ff" : "#ff0000";
string typeString = t.TradeType.ToString().ToLower();
double lotSize = t.VolumeInUnits / 100000.0;
newRows.AppendLine($"<tr align=right>");
newRows.AppendLine($"<td>{t.PositionId}</td>");
newRows.AppendLine($"<td>{t.EntryTime:yyyy.MM.dd HH:mm:ss}</td>");
newRows.AppendLine($"<td style='color:{typeColor}'>{typeString}</td>");
newRows.AppendLine($"<td>{lotSize:F2}</td>");
newRows.AppendLine($"<td align=left>{t.SymbolName}</td>");
newRows.AppendLine($"<td>{t.EntryPrice}</td>");
newRows.AppendLine($"<td>{t.ClosingTime:yyyy.MM.dd HH:mm:ss}</td>");
newRows.AppendLine($"<td>{t.ClosingPrice}</td>");
newRows.AppendLine($"<td>{t.NetProfit:F2}</td>");
newRows.AppendLine($"</tr>");
}
// 2. Generate the dynamic Account Summary Block
string summaryBlock = BuildDynamicSummaryBlock();
if (isNewFile)
{
// Build the fresh DOM
StringBuilder htmlDoc = new StringBuilder();
htmlDoc.AppendLine("<!DOCTYPE html><html><head><title>Statement</title>");
htmlDoc.AppendLine("<style>");
htmlDoc.AppendLine("body { font-family: Tahoma, sans-serif; font-size: 11px; margin: 20px; }");
htmlDoc.AppendLine("table { width: 100%; border-collapse: collapse; }");
htmlDoc.AppendLine("td, th { border: 1px solid #c0c0c0; padding: 3px; text-align: right; }");
htmlDoc.AppendLine("th { background-color: #e0e0e0; font-weight: bold; text-align: center; }");
htmlDoc.AppendLine(".header-table { width: 100%; border: none; margin-bottom: 20px; font-size: 16px; font-weight: bold; }");
htmlDoc.AppendLine(".header-table td { border: none; text-align: left; background-color: transparent; }");
htmlDoc.AppendLine("</style></head><body>");
htmlDoc.AppendLine("<table class='header-table'><tr>");
htmlDoc.AppendLine($"<td>Account: {Account.Number}</td>");
htmlDoc.AppendLine($"<td style='text-align:right'>Currency: {Account.Asset.Name}</td>");
htmlDoc.AppendLine("</tr></table>");
htmlDoc.AppendLine("<table><thead><tr>");
htmlDoc.AppendLine("<th>Ticket</th><th>Open Time</th><th>Type</th><th>Size</th><th>Item</th>");
htmlDoc.AppendLine("<th>Price</th><th>Close Time</th><th>Price</th><th>Profit</th>");
htmlDoc.AppendLine("</tr></thead>");
htmlDoc.AppendLine("<tbody>");
htmlDoc.Append(newRows.ToString());
htmlDoc.AppendLine("</tbody></table>");
// Inject initial summary at the bottom
htmlDoc.AppendLine(summaryBlock);
htmlDoc.AppendLine("</body></html>");
await File.WriteAllTextAsync(fullPath, htmlDoc.ToString());
}
else
{
string existingHtml = await File.ReadAllTextAsync(fullPath);
// 3. Inject new rows just above the closing table body tag
if (existingHtml.Contains("</tbody>"))
{
existingHtml = existingHtml.Replace("</tbody>", newRows.ToString() + "\n</tbody>");
}
// 4. THE REPLACEMENT ENGINE: Swap out the old summary block for the new one
string regexPattern = @"<!-- SUMMARY START -->.*?<!-- SUMMARY END -->";
existingHtml = Regex.Replace(existingHtml, regexPattern, summaryBlock, RegexOptions.Singleline);
await File.WriteAllTextAsync(fullPath, existingHtml);
}
}
catch (Exception ex)
{
Print($"[HTML Logger Error] {ex.Message}");
}
finally
{
_htmlFileLock.Release();
}
}
/// <summary>
/// Calculates current account metrics and builds an MT4-styled summary footer.
/// </summary>
private string BuildDynamicSummaryBlock()
{
// Calculate the total PnL for the current month to match the log rotation file
double currentMonthClosedPnL = History
.Where(t => t.ClosingTime.Month == DateTime.Now.Month && t.ClosingTime.Year == DateTime.Now.Year)
.Sum(t => t.NetProfit);
// Calculate the live floating PnL of open positions
double floatingPnL = Account.Equity - Account.Balance;
string floatingColor = floatingPnL >= 0 ? "#0000ff" : "#ff0000"; // Blue for profit, Red for drawdown
StringBuilder sb = new StringBuilder();
// The anchor tags used by the Regex engine
sb.AppendLine("<!-- SUMMARY START -->");
sb.AppendLine("<div style='margin-top: 20px; float: right; width: 300px;'>");
sb.AppendLine("<table style='border: none; width: 100%;'>");
// Monthly Closed PnL
sb.AppendLine($"<tr><td style='border: none; text-align: right; font-weight: bold;'>Monthly Closed P/L:</td>");
sb.AppendLine($"<td style='border: none; text-align: right;'>{currentMonthClosedPnL:F2}</td></tr>");
// Live Floating PnL
sb.AppendLine($"<tr><td style='border: none; text-align: right; font-weight: bold;'>Floating P/L:</td>");
sb.AppendLine($"<td style='border: none; text-align: right; color: {floatingColor};'>{floatingPnL:F2}</td></tr>");
// Margin & Equity
sb.AppendLine($"<tr><td style='border: none; text-align: right; font-weight: bold;'>Margin:</td>");
sb.AppendLine($"<td style='border: none; text-align: right;'>{Account.Margin:F2}</td></tr>");
sb.AppendLine($"<tr><td style='border: none; text-align: right; font-weight: bold;'>Free Margin:</td>");
sb.AppendLine($"<td style='border: none; text-align: right;'>{Account.FreeMargin:F2}</td></tr>");
sb.AppendLine($"<tr><td style='border: none; text-align: right; font-weight: bold; font-size: 14px;'>Live Equity:</td>");
sb.AppendLine($"<td style='border: none; text-align: right; font-weight: bold; font-size: 14px;'>{Account.Equity:F2}</td></tr>");
sb.AppendLine("</table></div><div style='clear: both;'></div>");
sb.AppendLine("<!-- SUMMARY END -->");
return sb.ToString();
}
}
}