The 3 Step Process

Step 1: Collect JSON Data

{
    "time": "2025.10.16 15:32:51",
    "dailyOrders": [
        {
            "type": "BUY",
            "volume": 0.01,
            "openTime": "2025.10.16 15:30:19",
            "openPrice": 1.16543,
            "closeTime": "2025.10.16 15:30:44",
            "closePrice": 1.16548,
            "stopLoss": 0,
            "takeProfit": 0,
            "profit": 0.05,
            "commission": 0,
            "swap": 0
        },
        {
            "type": "BUY",
            "volume": 0.01,
            "openTime": "2025.10.16 15:31:20",
            "openPrice": 1.16530,
            "closeTime": "2025.10.16 15:31:33",
            "closePrice": 1.16550,
            "stopLoss": 0,
            "takeProfit": 0,
            "profit": 0.02,
            "commission": 0,
            "swap": 0
        },
        {
            "type": "SELL",
            "volume": 0.01,
            "openTime": "2025.10.16 15:32:03",
            "openPrice": 1.16555,
            "closeTime": "2025.10.16 15:32:44",
            "closePrice": 1.16585,
            "stopLoss": 0,
            "takeProfit": 0,
            "profit": -0.03,
            "commission": 0,
            "swap": 0
        }
    ]
}

Step 2: Collect PNG Output

Step 3: Visualize Data

Other Features:

  1. The Visualization Script accepts two date arguments to specify what files should be combined into a log output.
  2. The script also allows the user to specify what events to visualize (market switches, trades entered, zones cancelled, etc.)

Log Visualization Script

(Constructed using NLP with Gemini)

/**
 * Mewtwo Image & JSON Combiner (v69 - Daily Summary Feature)
 * * Description:
 * Adds a new workflow to handle standalone JSON files from a '[REDACTED_DIR_NAME_D]'
 * directory. Instead of pairing with a PNG, the script now calculates trade
 * statistics from the JSON data and generates a new, standalone summary image.
 * The file discovery logic is updated to handle both paired and standalone jobs.
 * * Usage:
 * node --expose-gc process_logs.js
 */

const fs = require('fs');
const path = require('path');
const sharp = require('sharp');

// --- CONFIGURATION ---
const ROOT_DIR = '.';
const OUTPUT_DIR = 'log_outputs';
const EXCLUDE_DIRS = ['log_outputs', 'node_modules', '.git'];
const PADDING = 10;
const VERTICAL_PADDING = 20;
const BACKGROUND_COLOR = '#141414';
const TEXT_COLOR = '#DCDCDC';
const TABLE_BACKGROUND_COLOR = '#2a2a2a';
const BID_COLOR = '#ff4d4d'; // Red
const BEARISH = BID_COLOR;
const ASK_COLOR = '#33cc33'; // Green
const BULLISH = ASK_COLOR;
const DEMAND_COLOR = '#4da6ff'; // Blue
const SUPPLY_COLOR = '#FFA500'; // Orange
const FONT_SIZE = 14;
const HEADER_FONT_SIZE = 18;
const LINE_HEIGHT = FONT_SIZE * 1.4;
const GRID_COLUMN_WIDTH = 180;
const GRID_COLUMN_GAP = 25;
const HEADER_TO_BOX_SPACING = LINE_HEIGHT * 0.75;
const SVG_MIN_HEIGHT_PX = 150;

const [REDACTED_STRATEGY_CONFIG] = {
    '[REDACTED_INSTRUMENT_KEY]': {
        [REDACTED_PARAM_KEY]: 1200
    }
};

// --- SVG GENERATION LOGIC ---
function getColumnX(columnIndex) {
    return PADDING + columnIndex * (GRID_COLUMN_WIDTH + GRID_COLUMN_GAP);
}

function getOrdinal(n) {
    if (n > 3 && n < 21) return 'th';
    switch (n % 10) {
        case 1: return "st";
        case 2: return "nd";
        case 3: return "rd";
        default: return "th";
    }
}
function escapeHtml(text) {
    if (typeof text !== 'string') return text;
    return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '>').replace(/"/g, '&quot;');
}

function formatTimestamp(rawTimestamp) {
    if (!rawTimestamp) return '';
    const parsableTimestamp = rawTimestamp.replace(/\./g, '-');
    const date = new Date(parsableTimestamp);
    if (isNaN(date.getTime())) return rawTimestamp;
    const options = { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit', second: '2-digit', hour12: true };
    return date.toLocaleString('en-US', options);
}

function formatCandleTable(title, candleData, width, height) {
    if (!candleData) return { svg: '', width: 0 };
    let titleColor = TEXT_COLOR;
    if (candleData.isIndecisive) {
        if (candleData.close > candleData.open) titleColor = BULLISH;
        else if (candleData.close < candleData.open) titleColor = BEARISH;
    }
    let tspanElements = `<tspan x="${PADDING}" dy="${LINE_HEIGHT}" font-weight="bold" fill="${titleColor}">${title}</tspan>`;
    const indecisiveValueColor = candleData.isIndecisive ? BULLISH : BEARISH;
    tspanElements += `<tspan x="${PADDING}" dy="${LINE_HEIGHT}">Indecisive: </tspan><tspan fill="${indecisiveValueColor}">${String(candleData.isIndecisive)}</tspan>`;
    const otherDetails = [`Open: ${candleData.open}`, `Close: ${candleData.close}`, `High: ${candleData.high}`, `Low: ${candleData.low}`];
    otherDetails.forEach(detail => tspanElements += `<tspan x="${PADDING}" dy="${LINE_HEIGHT}">${escapeHtml(detail)}</tspan>`);
    const svg = `<g><rect x="0" y="0" width="${width}" height="${height}" fill="${TABLE_BACKGROUND_COLOR}" rx="5" /><text y="${FONT_SIZE}" font-family="monospace" font-size="${FONT_SIZE}" fill="${TEXT_COLOR}" xml:space="preserve">${tspanElements}</text></g>`;
    return { svg, width: width + GRID_COLUMN_GAP };
}

function [REDACTED_formatStrategyTableA](volume, width, height, pairTimeframeKey) {
    if (volume === undefined) return { svg: '', width: 0 };
    let tspanElements = `<tspan x="${PADDING}" dy="${LINE_HEIGHT}" font-weight="bold">[REDACTED_HEADER]</tspan>`;
    const config = [REDACTED_STRATEGY_CONFIG][pairTimeframeKey];
    if (config) {
        const volumeColor = volume >= config.[REDACTED_PARAM_KEY] ? BULLISH : BEARISH;
        tspanElements += `<tspan x="${PADDING}" dy="${LINE_HEIGHT}" fill="${volumeColor}">${volume}</tspan>`;
    } else {
        tspanElements += `<tspan x="${PADDING}" dy="${LINE_HEIGHT}">${volume}</tspan>`;
    }
    const svg = `<g><rect x="0" y="0" width="${width}" height="${height}" fill="${TABLE_BACKGROUND_COLOR}" rx="5" /><text y="${FONT_SIZE}" font-family="monospace" font-size="${FONT_SIZE}" fill="${TEXT_COLOR}">${tspanElements}</text></g>`;
    return { svg, width: width + GRID_COLUMN_GAP };
}

function [REDACTED_formatStrategyTableB](structure, width, height) {
    if (!structure) return { svg: '', width: 0 };
    let [REDACTED_color] = TEXT_COLOR;
    if (structure === 'BEARISH') [REDACTED_color] = BEARISH;
    else if (structure === 'BULLISH') [REDACTED_color] = BULLISH;
    let tspanElements = `<tspan x="${PADDING}" dy="${LINE_HEIGHT}" font-weight="bold">[REDACTED_HEADER]</tspan>`;
    tspanElements += `<tspan x="${PADDING}" dy="${LINE_HEIGHT}" fill="${[REDACTED_color]}">${structure}</tspan>`;
    const svg = `<g><rect x="0" y="0" width="${width}" height="${height}" fill="${TABLE_BACKGROUND_COLOR}" rx="5" /><text y="${FONT_SIZE}" font-family="monospace" font-size="${FONT_SIZE}" fill="${TEXT_COLOR}">${tspanElements}</text></g>`;
    return { svg, width: width + GRID_COLUMN_GAP };
}

function [REDACTED_formatStrategyTableC](structure, width, height) {
    let zoneTypeText = '', zoneTypeColor = TEXT_COLOR;
    if (structure === 'BULLISH') {
        zoneTypeText = '[REDACTED_VALUE_A]';
        zoneTypeColor = DEMAND_COLOR;
    } else if (structure === 'BEARISH') {
        zoneTypeText = '[REDACTED_VALUE_B]';
        zoneTypeColor = SUPPLY_COLOR;
    } else {
        return { svg: '', width: 0 };
    }
    let tspanElements = `<tspan x="${PADDING}" dy="${LINE_HEIGHT}" font-weight="bold">[REDACTED_HEADER]</tspan>`;
    tspanElements += `<tspan x="${PADDING}" dy="${LINE_HEIGHT}" fill="${zoneTypeColor}">${zoneTypeText}</tspan>`;
    const svg = `<g><rect x="0" y="0" width="${width}" height="${height}" fill="${TABLE_BACKGROUND_COLOR}" rx="5" /><text y="${FONT_SIZE}" font-family="monospace" font-size="${FONT_SIZE}" fill="${TEXT_COLOR}">${tspanElements}</text></g>`;
    return { svg, width: width + GRID_COLUMN_GAP };
}

function createDataSvg(jsonData, pairTimeframeKey, sourceDir) {
    const rawTimestamp = jsonData.time || null;
    const timestamp = rawTimestamp ? formatTimestamp(rawTimestamp) : null;
    if (jsonData.time) delete jsonData.time;

    const { [REDACTED_STRATEGY_LABEL_A]: [REDACTED_STRATEGY_VAR_A], ...details } = jsonData;
    const [REDACTED_STRATEGY_VAR_B] = details.[REDACTED_STRATEGY_LABEL_B];
    let svgElements = '';
    let totalRequiredWidth = PADDING;
    let currentY = VERTICAL_PADDING;
    let columnIndex = 0;

    if (timestamp) {
        svgElements += '%%TIMESTAMP_PLACEHOLDER%%';
        currentY += FONT_SIZE + VERTICAL_PADDING / 2;
    }

    const headerY = currentY + HEADER_FONT_SIZE;
    svgElements += `<text x="${PADDING}" y="${headerY}" font-family="sans-serif" font-size="${HEADER_FONT_SIZE}" font-weight="bold" fill="${TEXT_COLOR}">[REDACTED_HEADER]</text>`;
    currentY = headerY + HEADER_TO_BOX_SPACING;

    const contextRowY = currentY;
    columnIndex = 0;
    const candleTableLineCount = 6;
    const commonTableHeight = (candleTableLineCount * LINE_HEIGHT) + (VERTICAL_PADDING * 1.5);

    if (details.[REDACTED_STRATEGY_LABEL_B]) {
        const msResult = [REDACTED_formatStrategyTableB]([REDACTED_STRATEGY_VAR_B], GRID_COLUMN_WIDTH, commonTableHeight);
        svgElements += `<g transform="translate(${getColumnX(columnIndex++)}, ${contextRowY})">${msResult.svg}</g>`;
    }

    if (sourceDir === '[REDACTED_DIR_NAME_A]' && details.[REDACTED_STRATEGY_LABEL_B]) {
        const ztResult = [REDACTED_formatStrategyTableC]([REDACTED_STRATEGY_VAR_B], GRID_COLUMN_WIDTH, commonTableHeight);
        if (ztResult.width > 0) svgElements += `<g transform="translate(${getColumnX(columnIndex++)}, ${contextRowY})">${ztResult.svg}</g>`;
    }

    if (details.[REDACTED_STRATEGY_LABEL_C] !== undefined) {
        const ovResult = [REDACTED_formatStrategyTableA](details.[REDACTED_STRATEGY_LABEL_C], GRID_COLUMN_WIDTH, commonTableHeight, pairTimeframeKey);
        svgElements += `<g transform="translate(${getColumnX(columnIndex++)}, ${contextRowY})">${ovResult.svg}</g>`;
        delete details.[REDACTED_STRATEGY_LABEL_C];
    }

    if (details.[REDACTED_STRATEGY_LABEL_D]) {
        const icResult = formatCandleTable('[REDACTED_HEADER]', details.[REDACTED_STRATEGY_LABEL_D], GRID_COLUMN_WIDTH, commonTableHeight);
        svgElements += `<g transform="translate(${getColumnX(columnIndex++)}, ${contextRowY})">${icResult.svg}</g>`;
        delete details.[REDACTED_STRATEGY_LABEL_D];
    }

    delete details.[REDACTED_STRATEGY_LABEL_B];
    currentY = contextRowY + commonTableHeight + VERTICAL_PADDING;
    totalRequiredWidth = Math.max(totalRequiredWidth, getColumnX(columnIndex - 1) + GRID_COLUMN_WIDTH);

    if ([REDACTED_STRATEGY_VAR_A] && Object.keys([REDACTED_STRATEGY_VAR_A]).length > 0) {
        const [REDACTED_header_Y_A] = currentY + HEADER_FONT_SIZE;
        svgElements += `<text x="${PADDING}" y="${[REDACTED_header_Y_A]}" font-family="sans-serif" font-size="${HEADER_FONT_SIZE}" font-weight="bold" fill="${TEXT_COLOR}">[REDACTED_HEADER]</text>`;
        currentY = [REDACTED_header_Y_A] + HEADER_TO_BOX_SPACING;
        const [REDACTED_tables_Y_A] = currentY;
        columnIndex = 0;
        const tableKeys = Object.keys([REDACTED_STRATEGY_VAR_A]).sort((a, b) => parseInt(b) - parseInt(a));
        let maxLines = tableKeys.reduce((max, key) => Math.max(max, 2 + Math.max([REDACTED_STRATEGY_VAR_A][key].bids.length, [REDACTED_STRATEGY_VAR_A][key].asks.length)), 0);
        const uniformTableHeight = (maxLines * LINE_HEIGHT) + (VERTICAL_PADDING * 1.5);
        for (const key of tableKeys) {
            const number = parseInt(key);
            const title = (number === 1) ? '[REDACTED_HEADER]' : `${number - 1}${getOrdinal(number - 1)} Preceding Candle`;
            let tableTspanElements = `<tspan x="${PADDING}" dy="${LINE_HEIGHT}" font-weight="bold">${title}</tspan>`;
            const asksX = PADDING + 80;
            tableTspanElements += `<tspan x="${PADDING}" dy="${LINE_HEIGHT}" text-decoration="underline">Bids</tspan><tspan x="${asksX}" text-decoration="underline">Asks</tspan>`;
            const { bids, asks } = [REDACTED_STRATEGY_VAR_A][key];
            for (let i = 0; i < Math.max(bids.length, asks.length); i++) {
                let bidColor = TEXT_COLOR, askColor = TEXT_COLOR;
                if ([REDACTED_STRATEGY_VAR_B] === 'BEARISH' && i === 0) { bidColor = BEARISH; askColor = ASK_COLOR; }
                if ([REDACTED_STRATEGY_VAR_B] === 'BULLISH' && i === Math.max(bids.length, asks.length) - 1) { bidColor = BEARISH; askColor = ASK_COLOR; }
                tableTspanElements += `<tspan x="${PADDING}" dy="${LINE_HEIGHT}" fill="${bidColor}">${escapeHtml(bids[i] ?? '')}</tspan><tspan x="${asksX}" fill="${askColor}">${escapeHtml(asks[i] ?? '')}</tspan>`;
            }
            svgElements += `<g transform="translate(${getColumnX(columnIndex++)}, ${[REDACTED_tables_Y_A]})"><rect x="0" y="0" width="${GRID_COLUMN_WIDTH}" height="${uniformTableHeight}" fill="${TABLE_BACKGROUND_COLOR}" rx="5" /><text y="${FONT_SIZE}" font-family="monospace" font-size="${FONT_SIZE}" fill="${TEXT_COLOR}">${tableTspanElements}</text></g>`;
        }
        currentY = [REDACTED_tables_Y_A] + uniformTableHeight;
        totalRequiredWidth = Math.max(totalRequiredWidth, getColumnX(columnIndex - 1) + GRID_COLUMN_WIDTH);
    }

    const [REDACTED_timeframe_list] = ['M1', 'M5', 'M15', 'M30', 'H1'];
    const [REDACTED_candle_list] = [];
    const allDetails = { ...details, ...(details.[REDACTED_STRATEGY_LABEL_E] || {}) };
    for (const tf of [REDACTED_timeframe_list]) {
        if (allDetails[tf]) {
            [REDACTED_candle_list].push({ title: `${tf} Candle`, data: allDetails[tf] });
            delete allDetails[tf];
        }
    }
    delete details.[REDACTED_STRATEGY_LABEL_E];
    if ([REDACTED_candle_list].length > 0) {
        currentY += VERTICAL_PADDING * 1.5;
        const [REDACTED_header_Y_B] = currentY + HEADER_FONT_SIZE;
        svgElements += `<text x="${PADDING}" y="${[REDACTED_header_Y_B]}" font-family="sans-serif" font-size="${HEADER_FONT_SIZE}" font-weight="bold" fill="${TEXT_COLOR}">[REDACTED_HEADER]</text>`;
        currentY = [REDACTED_header_Y_B] + HEADER_TO_BOX_SPACING;
        const [REDACTED_tables_Y_B] = currentY;
        columnIndex = 0;
        for (const candle of [REDACTED_candle_list]) {
            const result = formatCandleTable(candle.title, candle.data, GRID_COLUMN_WIDTH, commonTableHeight);
            svgElements += `<g transform="translate(${getColumnX(columnIndex++)}, ${[REDACTED_tables_Y_B]})">${result.svg}</g>`;
        }
        currentY = [REDACTED_tables_Y_B] + commonTableHeight;
        totalRequiredWidth = Math.max(totalRequiredWidth, getColumnX(columnIndex - 1) + GRID_COLUMN_WIDTH);
    }

    if (sourceDir === '[REDACTED_DIR_NAME_B]' && details.[REDACTED_STRATEGY_LABEL_F]) {
        currentY += VERTICAL_PADDING * 1.5;
        const reasonHeaderY = currentY + HEADER_FONT_SIZE;
        svgElements += `<text x="${PADDING}" y="${reasonHeaderY}" font-family="sans-serif" font-size="${HEADER_FONT_SIZE}" font-weight="bold" fill="${TEXT_COLOR}">[REDACTED_HEADER]</text>`;
        currentY = reasonHeaderY + HEADER_TO_BOX_SPACING;
        const boxWidth = (GRID_COLUMN_WIDTH * 2) + GRID_COLUMN_GAP;
        const boxHeight = (1 * LINE_HEIGHT) + VERTICAL_PADDING;
        const reasonBoxY = currentY;
        let tspanElements = `<tspan x="${PADDING}" dy="${VERTICAL_PADDING}">${escapeHtml(details.[REDACTED_STRATEGY_LABEL_F])}</tspan>`;
        svgElements += `<g transform="translate(${getColumnX(0)}, ${reasonBoxY})"><rect x="0" y="0" width="${boxWidth}" height="${boxHeight}" fill="${TABLE_BACKGROUND_COLOR}" rx="5" /><text y="0" font-family="monospace" font-size="${FONT_SIZE}" fill="${TEXT_COLOR}">${tspanElements}</text></g>`;
        currentY = reasonBoxY + boxHeight + VERTICAL_PADDING;
        totalRequiredWidth = Math.max(totalRequiredWidth, PADDING + boxWidth);
        delete details.[REDACTED_STRATEGY_LABEL_F];
    }

    const [REDACTED_STRATEGY_VAR_C] = details.[REDACTED_STRATEGY_LABEL_G];
    if ([REDACTED_STRATEGY_VAR_C]) {
        currentY += VERTICAL_PADDING * 1.5;
        const tradeSectionHeader = sourceDir === '[REDACTED_DIR_NAME_C]' ? 'Active Trade' : 'Prospective Trade';
        const tradeHeaderY = currentY + HEADER_FONT_SIZE;
        svgElements += `<text x="${PADDING}" y="${tradeHeaderY}" font-family="sans-serif" font-size="${HEADER_FONT_SIZE}" font-weight="bold" fill="${TEXT_COLOR}">${tradeSectionHeader}</text>`;
        currentY = tradeHeaderY + HEADER_TO_BOX_SPACING;
        const dynamicLimitColor = ([REDACTED_STRATEGY_VAR_B] === 'BEARISH') ? SUPPLY_COLOR : DEMAND_COLOR;
        const tradeLines = [];
        if ([REDACTED_STRATEGY_VAR_C].[REDACTED_PROP_A] !== undefined) tradeLines.push({ label: '[REDACTED_LABEL]: ', value: [REDACTED_STRATEGY_VAR_C].[REDACTED_PROP_A], color: dynamicLimitColor });
        if ([REDACTED_STRATEGY_VAR_C].[REDACTED_PROP_B] !== undefined) tradeLines.push({ label: '[REDACTED_LABEL]: ', value: [REDACTED_STRATEGY_VAR_C].[REDACTED_PROP_B], color: BULLISH });
        if ([REDACTED_STRATEGY_VAR_C].[REDACTED_PROP_C] !== undefined) tradeLines.push({ label: '[REDACTED_LABEL]: ', value: [REDACTED_STRATEGY_VAR_C].[REDACTED_PROP_C], color: BEARISH });
        if ([REDACTED_STRATEGY_VAR_C].[REDACTED_PROP_D] !== undefined) tradeLines.push({ label: '[REDACTED_LABEL]: ', value: [REDACTED_STRATEGY_VAR_C].[REDACTED_PROP_D] });
        if ([REDACTED_STRATEGY_VAR_C].[REDACTED_PROP_E] !== undefined) tradeLines.push({ label: '[REDACTED_LABEL]: ', value: [REDACTED_STRATEGY_VAR_C].[REDACTED_PROP_E] });
        if ([REDACTED_STRATEGY_VAR_C].[REDACTED_PROP_F] !== undefined) tradeLines.push({ label: '[REDACTED_LABEL]: ', value: [REDACTED_STRATEGY_VAR_C].[REDACTED_PROP_F] });
        tradeLines.unshift({ label: '[REDACTED_HEADER]', isTitle: true });
        const boxHeight = (tradeLines.length * LINE_HEIGHT) + VERTICAL_PADDING;
        const boxWidth = GRID_COLUMN_WIDTH;
        const tradeBoxY = currentY;
        let tspanElements = '';
        tradeLines.forEach((line, index) => {
            const dy = index === 0 ? VERTICAL_PADDING : LINE_HEIGHT;
            if (line.isTitle) tspanElements += `<tspan x="${PADDING}" dy="${dy}" font-weight="bold">${escapeHtml(line.label)}</tspan>`;
            else tspanElements += `<tspan x="${PADDING}" dy="${dy}">${escapeHtml(line.label)}</tspan><tspan${line.color ? ` fill="${line.color}"` : ''}>${escapeHtml(line.value)}</tspan>`;
        });
        svgElements += `<g transform="translate(${getColumnX(0)}, ${tradeBoxY})"><rect x="0" y="0" width="${boxWidth}" height="${boxHeight}" fill="${TABLE_BACKGROUND_COLOR}" rx="5" /><text y="0" font-family="monospace" font-size="${FONT_SIZE}" fill="${TEXT_COLOR}" xml:space="preserve">${tspanElements}</text></g>`;
        currentY = tradeBoxY + boxHeight + VERTICAL_PADDING;
        totalRequiredWidth = Math.max(totalRequiredWidth, PADDING + boxWidth);
        delete details.[REDACTED_STRATEGY_LABEL_G];
    }

    const remainingDetailsText = JSON.stringify(details, null, 2);
    if (remainingDetailsText !== '{}') {
        let detailsTspanElements = `<tspan x="0" dy="0" font-weight="bold">Remaining Details (JSON)</tspan>`;
        const detailLines = remainingDetailsText.split('\n');
        detailLines.forEach(line => detailsTspanElements += `<tspan x="${PADDING}" dy="${LINE_HEIGHT}">${escapeHtml(line)}</tspan>`);
        const detailsTextY = currentY + FONT_SIZE;
        svgElements += `<text x="${PADDING}" y="${detailsTextY}" font-family="monospace" font-size="${FONT_SIZE}" fill="${TEXT_COLOR}">${detailsTspanElements}</text>`;
        currentY = detailsTextY + ((detailLines.length + 1) * LINE_HEIGHT);
    }
    return {
        svgContent: svgElements,
        calculatedWidth: totalRequiredWidth + PADDING,
        calculatedHeight: currentY + VERTICAL_PADDING + SVG_MIN_HEIGHT_PX,
        timestamp
    };
}

// --- CORE SCRIPT LOGIC ---
async function createCombinedImage(pngPath, jsonPath) {
    try {
        const baseName = path.basename(pngPath, path.extname(pngPath));
        const parts = baseName.split('_');
        let pairTimeframeKey = (parts.length >= 2) ? `${parts[0].toUpperCase()}_${parts[1]}` : '';
        const sourceDir = path.basename(path.dirname(pngPath));
        const jsonContent = fs.readFileSync(jsonPath, 'utf8').replace(/^\uFEFF/, '');
        const jsonData = JSON.parse(jsonContent);
        const cleanPngBuffer = await sharp(pngPath).png().toBuffer();
        const metadata = await sharp(cleanPngBuffer).metadata();
        if (!metadata?.width || !metadata?.height) throw new Error('Failed to read valid dimensions from sanitized image.');

        const { svgContent, calculatedWidth, calculatedHeight, timestamp } = createDataSvg(jsonData, pairTimeframeKey, sourceDir);

        let finalSvgContent = svgContent;
        if (timestamp) {
            const timestampY = VERTICAL_PADDING + FONT_SIZE;
            const timestampX = calculatedWidth - PADDING;
            const timestampSvg = `<text x="${timestampX}" y="${timestampY}" font-family="monospace" font-size="${FONT_SIZE}" fill="${TEXT_COLOR}" text-anchor="end">${escapeHtml(timestamp)}</text>`;
            finalSvgContent = svgContent.replace('%%TIMESTAMP_PLACEHOLDER%%', timestampSvg);
        }

        const newWidth = metadata.width + calculatedWidth + PADDING;
        const newHeight = Math.ceil(Math.max(metadata.height + VERTICAL_PADDING * 2, calculatedHeight));
        const finalSvg = `<svg width="${calculatedWidth}" height="${newHeight}" xmlns="http://www.w3.org/2000/svg">${finalSvgContent}</svg>`;

        const outputBuffer = await sharp({ create: { width: newWidth, height: newHeight, channels: 4, background: BACKGROUND_COLOR } })
            .composite([{ input: cleanPngBuffer, top: VERTICAL_PADDING, left: PADDING }, { input: Buffer.from(finalSvg), top: 0, left: metadata.width + PADDING }])
            .png().toBuffer();
        const relativeDir = path.dirname(path.relative(ROOT_DIR, pngPath));
        const outputSubfolder = path.join(OUTPUT_DIR, relativeDir);
        fs.mkdirSync(outputSubfolder, { recursive: true });
        const outputPath = path.join(outputSubfolder, `${path.basename(pngPath, path.extname(pngPath))}.png`);
        fs.writeFileSync(outputPath, outputBuffer);
        console.log(`   āœ… Created: ${outputPath}`);
    } catch (e) {
        console.error(`   āŒ Failed to process ${path.basename(pngPath)}: ${e.message}`);
    }
}

// --- UPDATED FUNCTION FOR DAILY SUMMARIES ---
async function createDailySummaryImage(jsonPath) {
    try {
        const jsonContent = fs.readFileSync(jsonPath, 'utf8');
        const jsonData = JSON.parse(jsonContent);

        // --- 1. Process data and calculate totals ---
        const profitOrders = [];
        const lossOrders = [];
        let totalProfit = 0;
        let totalLoss = 0;

        if (jsonData.[REDACTED_STRATEGY_LABEL_H]) {
            for (const order of jsonData.[REDACTED_STRATEGY_LABEL_H]) {
                // Filter to only include actual BUY and SELL trades
                if (order.type === 'BUY' || order.type === 'SELL') {
                    if (order.profit >= 0) {
                        profitOrders.push(order);
                        totalProfit += order.profit;
                    } else {
                        lossOrders.push(order);
                        totalLoss += order.profit;
                    }
                }
            }
        }
        const netProfit = totalProfit + totalLoss;

        // --- 2. Dynamically Generate SVG Content ---
        const cellLineCount = 4;
        const cellHeight = (cellLineCount * LINE_HEIGHT) + (PADDING * 2);
        const cellSpacing = 10;
        const maxTradesInColumn = Math.max(profitOrders.length, lossOrders.length, 1);

        const mainHeaderY = VERTICAL_PADDING + HEADER_FONT_SIZE;
        const columnTitlesY = mainHeaderY + (LINE_HEIGHT * 1.75);
        const cellsStartY = columnTitlesY + HEADER_TO_BOX_SPACING;

        const totalColumnContentHeight = (maxTradesInColumn * cellHeight) + (Math.max(0, maxTradesInColumn - 1) * cellSpacing);
        
        // NEW: Calculate position and add height for the summary section
        const summaryStartY = cellsStartY + totalColumnContentHeight + VERTICAL_PADDING * 1.5;
        const summaryHeight = (3 * LINE_HEIGHT); // 3 lines of text
        const height = summaryStartY + summaryHeight + VERTICAL_PADDING;
        const width = (GRID_COLUMN_WIDTH * 2) + GRID_COLUMN_GAP + (PADDING * 2);

        let svgContent = '';
        const timestamp = formatTimestamp(jsonData.time);

        // Main Header
        svgContent += `<text x="${PADDING}" y="${mainHeaderY}" font-family="sans-serif" font-size="${HEADER_FONT_SIZE}" font-weight="bold" fill="${TEXT_COLOR}">[REDACTED_HEADER]</text>`;
        svgContent += `<text x="${width - PADDING}" y="${mainHeaderY}" font-family="monospace" font-size="${FONT_SIZE}" fill="${TEXT_COLOR}" text-anchor="end">${timestamp}</text>`;

        const createTradeCell = (order) => {
            const profitColor = order.profit >= 0 ? BULLISH : BEARISH;
            const profitLabel = order.profit >= 0 ? 'Profit:' : 'Loss:';
            const profitValue = Math.abs(order.profit).toFixed(2);
            const labelX = PADDING;
            const valueX = PADDING + 75;

            let tspan = `<tspan x="${labelX}" font-weight="bold">${order.type}</tspan>`;
            tspan += `<tspan x="${labelX}" dy="${LINE_HEIGHT}">Open:</tspan><tspan x="${valueX}">${order.openPrice}</tspan>`;
            tspan += `<tspan x="${labelX}" dy="${LINE_HEIGHT}">Close:</tspan><tspan x="${valueX}">${order.closePrice}</tspan>`;
            tspan += `<tspan x="${labelX}" dy="${LINE_HEIGHT}">${profitLabel}</tspan><tspan x="${valueX}" fill="${profitColor}">${profitValue}</tspan>`;

            const initialTextY = PADDING + FONT_SIZE;

            return `<g>
                        <rect x="0" y="0" width="${GRID_COLUMN_WIDTH}" height="${cellHeight}" fill="${TABLE_BACKGROUND_COLOR}" rx="5" />
                        <text y="${initialTextY}" font-family="monospace" font-size="${FONT_SIZE}" fill="${TEXT_COLOR}">${tspan}</text>
                    </g>`;
        };

        const createColumn = (title, orders, titleColor, columnIndex) => {
            let columnSvg = '';
            const columnX = getColumnX(columnIndex);
            columnSvg += `<text x="${columnX + PADDING}" y="${columnTitlesY}" font-family="sans-serif" font-size="${HEADER_FONT_SIZE}" fill="${titleColor}">${title}</text>`;
            let currentY = cellsStartY;
            for (const order of orders) {
                const cellSvg = createTradeCell(order);
                columnSvg += `<g transform="translate(${columnX}, ${currentY})">${cellSvg}</g>`;
                currentY += cellHeight + cellSpacing;
            }
            return columnSvg;
        };

        svgContent += createColumn('WINS', profitOrders, BULLISH, 0);
        svgContent += createColumn('LOSSES', lossOrders, BEARISH, 1);

        // NEW: Add Summary Section at the bottom
        const summaryLabelX = PADDING;
        const summaryValueX = PADDING + 150;
        let summaryTspan = `<tspan x="${summaryLabelX}">Total Profits:</tspan><tspan x="${summaryValueX}" fill="${BULLISH}">${totalProfit.toFixed(2)}</tspan>`;
        summaryTspan += `<tspan x="${summaryLabelX}" dy="${LINE_HEIGHT}">Total Losses:</tspan><tspan x="${summaryValueX}" fill="${BEARISH}">${Math.abs(totalLoss).toFixed(2)}</tspan>`;
        const netColor = netProfit >= 0 ? BULLISH : BEARISH;
        summaryTspan += `<tspan x="${summaryLabelX}" dy="${LINE_HEIGHT}">Net Profit/Loss:</tspan><tspan x="${summaryValueX}" fill="${netColor}">${netProfit.toFixed(2)}</tspan>`;
        
        svgContent += `<text y="${summaryStartY + FONT_SIZE}" font-family="monospace" font-size="${FONT_SIZE}" fill="${TEXT_COLOR}">${summaryTspan}</text>`;


        const finalSvg = `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
                            <rect x="0" y="0" width="100%" height="100%" fill="${BACKGROUND_COLOR}" />
                            ${svgContent}
                          </svg>`;

        // --- 3. Save as PNG (No changes here) ---
        const outputBuffer = await sharp(Buffer.from(finalSvg)).png().toBuffer();
        const relativeDir = path.dirname(path.relative(ROOT_DIR, jsonPath));
        const outputSubfolder = path.join(OUTPUT_DIR, relativeDir);
        fs.mkdirSync(outputSubfolder, { recursive: true });
        const outputPath = path.join(outputSubfolder, `${path.basename(jsonPath, '.json')}.png`);
        fs.writeFileSync(outputPath, outputBuffer);
        console.log(`   āœ… Created Log: ${outputPath}`);

    } catch (e) {
        console.error(`   āŒ Failed to process summary for ${path.basename(jsonPath)}: ${e.message}`);
    }
}


function setupOutputDirectory() {
    if (fs.existsSync(OUTPUT_DIR)) {
        fs.readdirSync(OUTPUT_DIR).forEach(item => {
            if (item !== '.obsidian') fs.rmSync(path.join(OUTPUT_DIR, item), { recursive: true, force: true });
        });
    }
    fs.mkdirSync(OUTPUT_DIR, { recursive: true });
    console.log(`Directory '${OUTPUT_DIR}' is ready.`);
}

/**
 * UPDATED: Handles both paired files and standalone JSONs in specific folders.
 */
function findFilePairs() {
    const pairs = {};
    console.log(`Scanning for .png/.json pairs and summary jobs in '${path.resolve(ROOT_DIR)}'...`);
    function scan(dir) {
        const items = fs.readdirSync(dir);
        for (const item of items) {
            const fullPath = path.join(dir, item);
            if (EXCLUDE_DIRS.includes(item)) continue;
            if (fs.statSync(fullPath).isDirectory()) {
                scan(fullPath);
            } else {
                const ext = path.extname(fullPath);
                const relativePath = path.relative(ROOT_DIR, fullPath);
                const key = path.join(path.dirname(relativePath), path.basename(relativePath, ext)).replace(/\\/g, '/');
                const dirName = path.basename(path.dirname(fullPath));

                // NEW: Logic for standalone summary jobs
                if (dirName === '[REDACTED_DIR_NAME_D]' && ext === '.json') {
                    pairs[key] = {
                        type: 'summary',
                        '.json': fullPath
                    };
                // Existing logic for paired jobs
                } else if (ext === '.png' || ext === '.json') {
                    if (!pairs[key]) pairs[key] = { type: 'pair' };
                    pairs[key][ext] = fullPath;
                }
            }
        }
    }
    scan(ROOT_DIR);
    // UPDATED: Filter for both complete pairs and summary jobs
    return Object.values(pairs).filter(p => (p.type === 'pair' && p['.png'] && p['.json']) || p.type === 'summary');
}


async function main() {
    setupOutputDirectory();
    const jobs = findFilePairs(); // Renamed to 'jobs' for clarity
    if (jobs.length === 0) {
        console.log("No matching .png/.json pairs or summary jobs found.");
        return;
    }
    console.log(`\nFound ${jobs.length} jobs. Starting processing...\n`);
    for (let i = 0; i < jobs.length; i++) {
        const job = jobs[i];
        console.log(`Processing job ${i + 1}/${jobs.length}: ${path.basename(job['.json'])}`);

        // UPDATED: Route job based on its type
        if (job.type === 'summary') {
            await createDailySummaryImage(job['.json']);
        } else if (job.type === 'pair') {
            await createCombinedImage(job['.png'], job['.json']);
        }

        if (global.gc) global.gc();
    }
    console.log(`\nšŸŽ‰ Finished! Processed ${jobs.length} jobs.`);
}

(async () => {
    try {
        await main();
    } catch (error) {
        console.error(`āŒ A critical error occurred: ${error.message}`);
    }
})();