Goldify v1.0
Goldify
Giao dịch thông thường
+215%
Lợi nhuận tiềm năng
-42%
Giảm rủi ro
Tutorials Theo Dõi Hiệu Suất Goldify EA Chuyên Nghiệp với Báo Cáo P/L Tự Động lên Google Sheets

Theo Dõi Hiệu Suất Goldify EA Chuyên Nghiệp với Báo Cáo P/L Tự Động lên Google Sheets

April 15, 2025
Goldify Team
notify telegram email

Tìm hiểu cách sử dụng tính năng báo cáo P/L hàng ngày của Goldify EA lên Google Sheets thông qua Gmail.

Theo Dõi Hiệu Suất Goldify EA Chuyên Nghiệp với Báo Cáo P/L Tự Động lên Google Sheets

Theo Dõi Hiệu Suất Goldify EA Chuyên Nghiệp với Báo Cáo P/L Tự Động lên Google Sheets

Tìm hiểu cách sử dụng tính năng báo cáo P/L hàng ngày của Goldify EA lên Google Sheets thông qua Gmail.

Chào mừng các nhà giao dịch Goldify!

Chúng tôi rất vui mừng giới thiệu một tính năng mạnh mẽ mới được tích hợp vào Goldify EA: khả năng tự động gửi báo cáo Lời/Lỗ (P/L) hàng ngày của robot lên Google Sheets thông qua Gmail. Tính năng này giúp bạn theo dõi hiệu suất giao dịch một cách chi tiết, minh bạch và dễ dàng phân tích theo thời gian (ví dụ: P/L theo tuần, tháng, quý).

Tại sao tính năng này hữu ích?

  • Theo dõi tự động: Không cần phải ghi chép thủ công mỗi ngày.
  • Lưu trữ an toàn & tập trung: Dữ liệu P/L được lưu trữ trên Google Sheets, dễ dàng truy cập từ bất kỳ đâu.
  • Phân tích chuyên sâu: Sử dụng các công cụ của Google Sheets (bảng tổng hợp, biểu đồ, hàm tính toán) để phân tích hiệu suất P/L theo tháng, quý, hoặc các tiêu chí khác.
  • Minh bạch hiệu suất: Có cái nhìn rõ ràng về hiệu suất của EA theo thời gian, giúp đưa ra quyết định tốt hơn.
  • Quản lý nhiều tài khoản/EA: Dễ dàng tổng hợp dữ liệu từ nhiều robot Goldify chạy trên các tài khoản khác nhau vào cùng một bảng tính.

Cơ chế hoạt động:

  1. Goldify EA: Vào cuối mỗi ngày giao dịch (theo giờ server) hoặc khi bạn chủ động “Chốt Sổ” (Close All), EA sẽ tự động tính toán P/L của ngày đó.
  2. Gửi Email: EA sẽ gửi một email chứa thông tin P/L (ngày, symbol, magic number, P/L, tài khoản, phiên bản EA) đến một địa chỉ Gmail bạn chỉ định.
  3. Google Apps Script: Một đoạn mã kịch bản (Google Apps Script) chạy trên Google Sheets của bạn sẽ tự động quét hộp thư Gmail đó.
  4. Cập nhật Google Sheet: Khi phát hiện email báo cáo P/L mới, Apps Script sẽ đọc thông tin và tự động ghi dữ liệu vào một bảng Google Sheet đã được thiết lập sẵn.

Nghe có vẻ phức tạp? Đừng lo! Chúng tôi sẽ hướng dẫn bạn từng bước một.


Hướng Dẫn Thiết Lập Tính Năng Báo Cáo P/L Lên Google Sheets

Để sử dụng tính năng này, bạn cần thực hiện cài đặt ở ba nơi: MetaTrader 5 (MT5), Input của Goldify EA, và Google Sheets/Apps Script.

Phần 1: Cài Đặt Trong MetaTrader 5 (Để EA Gửi Email)

Bạn cần cho phép MT5 gửi email.

  1. Mở phần mềm MetaTrader 5 của bạn.
  2. Vào menu Tools > Options.
  3. Chọn tab Email.
  4. Đánh dấu chọn (check) vào ô “Enable email notifications”.
  5. Điền các thông tin SMTP của tài khoản Gmail bạn sẽ dùng để gửi đi các báo cáo P/L này (đây có thể là một tài khoản Gmail riêng của bạn).
    • SMTP server: smtp.gmail.com:587 (nếu dùng Gmail, các nhà cung cấp khác sẽ có thông số riêng).
    • SMTP login: Địa chỉ email đầy đủ của bạn (ví dụ: goldify.reporter@gmail.com).
    • SMTP password:
      • Nếu tài khoản Gmail này KHÔNG bật “Xác minh 2 bước”, đây là mật khẩu Gmail thông thường của bạn.
      • Nếu tài khoản Gmail này ĐÃ BẬT “Xác minh 2 bước” (khuyến nghị), bạn cần tạo một “Mật khẩu ứng dụng” (App Password) riêng cho MetaTrader 5 từ cài đặt bảo mật tài khoản Google của bạn và điền mật khẩu đó vào đây.
    • From: Thường giống SMTP login (ví dụ: goldify.reporter@gmail.com).
    • To: QUAN TRỌNG: Nhập địa chỉ email mà Google Apps Script sẽ quét để lấy dữ liệu. Đây là email nhận báo cáo P/L. Ví dụ: my.goldify.data@gmail.com. Hãy ghi nhớ địa chỉ email này, vì bạn sẽ cần nó cho Input của EA và cài đặt Google Apps Script.
  6. Nhấn nút Test. Nếu bạn nhận được email thử nghiệm tại địa chỉ “To”, cấu hình đã thành công.
  7. Nhấn OK. MT5 Email Settings PC Phần 2: Cài Đặt Input Cho Goldify EA

Khi bạn gắn Goldify EA vào chart, hãy cấu hình các thông số (Inputs) sau:

  1. Tìm đến nhóm input: ✅ [NOTIFY_PL] Báo cáo P/L Hàng ngày qua Email.
  2. [NOTIFY_PL] Bật/Tắt gửi báo cáo P/L hàng ngày qua email?: Đặt thành true để bật tính năng gửi báo cáo P/L.
  3. [NOTIFY_PL] Email nhận báo cáo P/L (cho GAS xử lý): QUAN TRỌNG: Nhập chính xác địa chỉ email bạn đã điền vào ô “To” ở Phần 1, Bước 5 (ví dụ: my.goldify.data@gmail.com). Đây là email mà Apps Script sẽ đọc.
  4. [NOTIFY_PL] Tiền tố tiêu đề email báo cáo P/L: Mặc định là “Goldify Daily P/L”. Bạn có thể giữ nguyên. Nếu thay đổi, bạn cần cập nhật giá trị này trong Google Apps Script ở Phần 3.
  5. Cấu hình các input khác của EA như bình thường và nhấn OK.

Phần 3: Thiết Lập Google Sheet và Google Apps Script

Đây là phần cốt lõi để tự động lấy dữ liệu từ email.

  1. Chuẩn bị Google Sheet:

    • Truy cập Google Sheets và tạo một bảng tính mới hoặc CÁCH TỐT NHÂT - tạo bản sao của Google Sheet này
    • Đặt tên cho bảng tính, ví dụ: “Goldify PNL Tracking”.
    • Đặt tên cho sheet đầu tiên (tab ở dưới cùng) là DailyPL (hoặc tên khác, nhưng phải nhớ để dùng trong script). (bỏ qua các bước này nếu Tạo bản sao của Google có sẵn ở trên tới Bước 2. Tạo Google Apps Script)
    • Trong sheet “DailyPL”, tạo các tiêu đề cột ở hàng đầu tiên (A1, B1, C1,…):
      • A1: Timestamp
      • B1: REPORT_DATE
      • C1: SERVER_TIME_REPORT
      • D1: SYMBOL
      • E1: ACCOUNT
      • F1: MAGIC_NUMBER
      • G1: EA_STATUS
      • H1: REALIZED_PNL_TODAY
      • I1: FLOATING_PL_CURRENT
      • J1: BALANCE_CURRENT
      • K1: EQUITY_CURRENT
      • L1: MAX_EQUITY_TODAY
      • M1: TRADES_CLOSED_TODAY
      • N1: WIN_RATE_TODAY
      • O1: Email_Subject Google Sheet Template
  2. Tạo Google Apps Script:

    • Trong Google Sheet vừa tạo, đi tới menu Extensions > Apps Script.
    • Một tab mới sẽ mở ra. Đặt tên cho project (ví dụ: “GoldifyEmailProcessor”).
    • Xóa toàn bộ code mặc định trong file Code.gs.
    • Copy toàn bộ đoạn mã script chúng tôi cung cấp (xem Phụ Lục A ở cuối bài viết này) và dán vào file Code.gs. Google Apps Script
  3. Cấu hình Script:

    • Trong đoạn code bạn vừa dán, tìm các dòng sau ở phần đầu:
      const SPREADSHEET_ID = "YOUR_SPREADSHEET_ID";
      const SHEET_NAME = "DailyPL";
      const EMAIL_SUBJECT_PREFIX = "Goldify Daily P/L";
    • SPREADSHEET_ID:
      • Quay lại Google Sheet của bạn. Nhìn vào URL trên thanh địa chỉ trình duyệt, nó sẽ có dạng: https://docs.google.com/spreadsheets/d/ABC123XYZ789/edit#gid=0
      • Copy phần ID ở giữa (ví dụ: ABC123XYZ789).
      • Thay thế "YOUR_SPREADSHEET_ID" trong script bằng ID bạn vừa copy.
    • SHEET_NAME: Đảm bảo giá trị này ("DailyPL") khớp với tên sheet bạn đã đặt ở Bước 1.
    • EMAIL_SUBJECT_PREFIX: Đảm bảo giá trị này ("Goldify Daily P/L") khớp với input Tiền tố tiêu đề Email báo cáo P/L bạn đã đặt cho EA.
    • Lưu script lại (biểu tượng đĩa mềm).
  4. Cấp Quyền cho Script:

    • Trong trình soạn thảo Apps Script, từ menu thả xuống (thường ghi “Select function”), chọn hàm processDailyPLReports.
    • Nhấn nút Run (biểu tượng ▶️). Google Apps Script
    • Lần đầu tiên, Google sẽ yêu cầu bạn cấp quyền. Nhấn Review permissions, chọn tài khoản Google của bạn. Google Apps Script
    • Nếu thấy cảnh báo “Google hasn’t verified this app”, nhấn Advanced, sau đó nhấn Go to [Tên project của bạn] (unsafe). Google Apps Script
    • Xem lại các quyền và nhấn Allow.
  5. Thiết lập Trigger (Tự động chạy):

    • Trong trình soạn thảo Apps Script, nhấp vào biểu tượng đồng hồ (Triggers) ở thanh menu bên trái.
    • Nhấp nút + Add Trigger (góc dưới bên phải). Google Apps Script
    • Cấu hình như sau:
      • Choose which function to run: processDailyPLReports
      • Choose which deployment should run: Head
      • Select event source: Time-driven
      • Select type of time based trigger: Hour timer (Bộ hẹn giờ theo giờ)
      • Select hour interval: Every hour (Mỗi giờ) - Hoặc bạn có thể chọn “Day timer” và đặt một thời điểm cụ thể trong ngày (ví dụ: sau nửa đêm).
      • Failure notification settings: Chọn Notify me daily hoặc Notify me immediately.
    • Nhấn Save. Google Apps Script

Hoàn Tất!

Bây giờ, hệ thống của bạn đã sẵn sàng. Goldify EA sẽ gửi P/L hàng ngày đến email bạn đã cấu hình. Google Apps Script sẽ tự động quét email đó mỗi giờ (hoặc theo lịch bạn đặt), đọc dữ liệu và ghi vào Google Sheet của bạn.

Cách sử dụng dữ liệu trong Google Sheet:

Khi dữ liệu đã được điền vào Google Sheet, bạn có thể:

  • Sắp xếp và lọc: Theo ngày, symbol, magic number.
  • Tính tổng P/L: Sử dụng hàm SUM() để tính tổng P/L cho một khoảng thời gian.
  • Tạo Bảng Tổng Hợp (Pivot Table):
    • Chọn toàn bộ dữ liệu.
    • Vào Insert > Pivot table.
    • Kéo cột “Date” (hoặc cột “MonthYear” nếu bạn tạo thêm) vào phần “Rows” (và nhóm theo Tháng/Năm).
    • Kéo cột “PNL” vào phần “Values” và chọn Summarize by: SUM.
    • Bạn sẽ thấy tổng P/L cho mỗi tháng, giúp theo dõi hiệu suất dài hạn.
  • Vẽ Biểu Đồ: Trực quan hóa P/L theo thời gian.

Phụ Lục A: Mã Google Apps Script (Dán vào Code.gs)

// ----- CẤU HÌNH SCRIPT -----
const SPREADSHEET_ID = "YOUR_SPREADSHEET_ID"; // <<< THAY THẾ BẰNG ID GOOGLE SHEET CỦA BẠN
const SHEET_NAME = "DailyPL";                 // Tên sheet bạn đã tạo (ví dụ: "DailyPL")
const EMAIL_SUBJECT_PREFIX = "Goldify Daily P/L"; // Tiền tố tiêu đề email mà EA gửi (phải khớp với input EA)
const LABEL_NAME_PROCESSED = "GoldifyPNLProcessed"; // Tên nhãn để đánh dấu email đã xử lý

// ----- HÀM CHÍNH ĐỂ XỬ LÝ EMAIL -----
function processDailyPLReports() {
  try {
    createLabelIfNotExists(LABEL_NAME_PROCESSED);

    // Lấy các luồng có email CHƯA ĐỌC và CHƯA CÓ NHÃN PROCESSED
    // Quan trọng: Nếu một luồng có cả email đã đọc (và đã xử lý) và email chưa đọc (mới),
    // nó vẫn sẽ được trả về nếu luồng đó CHƯA có nhãn LABEL_NAME_PROCESSED.
    // Nếu luồng đã có nhãn LABEL_NAME_PROCESSED, nó sẽ không được search ra.
    const searchQuery = `subject:("${EMAIL_SUBJECT_PREFIX}") -label:${LABEL_NAME_PROCESSED} is:unread`;
    const threads = GmailApp.search(searchQuery);

    if (threads.length === 0) {
      Logger.log("Không tìm thấy luồng email P/L mới nào (chưa xử lý và có email chưa đọc) để xử lý.");
      return;
    }

    Logger.log(`Tìm thấy ${threads.length} luồng email P/L có chứa email mới.`);

    const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
    const sheet = ss.getSheetByName(SHEET_NAME);

    const expectedHeaders = [
      "Timestamp", "REPORT_DATE", "SERVER_TIME_REPORT", "SYMBOL", "ACCOUNT",
      "MAGIC_NUMBER", "EA_STATUS", "REALIZED_PNL_TODAY", "FLOATING_PL_CURRENT",
      "BALANCE_CURRENT", "EQUITY_CURRENT", "MAX_EQUITY_TODAY", "TRADES_CLOSED_TODAY",
      "WIN_RATE_TODAY", "Email_Subject"
    ];

    if (sheet.getLastRow() === 0) {
      sheet.appendRow(expectedHeaders);
      Logger.log("Đã tạo hàng tiêu đề cho sheet.");
    } else {
      // ... (logic kiểm tra và cập nhật tiêu đề giữ nguyên) ...
       const currentHeaders = sheet.getRange(1, 1, 1, sheet.getMaxColumns()).getValues()[0];
      let headersModified = false;
      if (currentHeaders.length !== expectedHeaders.length) {
        sheet.getRange(1, 1, 1, expectedHeaders.length).setValues([expectedHeaders]);
        Logger.log("Đã cập nhật lại hàng tiêu đề do số lượng cột thay đổi.");
        headersModified = true;
      } else {
        for (let k = 0; k < expectedHeaders.length; k++) {
          if (currentHeaders[k] !== expectedHeaders[k]) {
            sheet.getRange(1, 1, 1, expectedHeaders.length).setValues([expectedHeaders]);
            Logger.log("Đã cập nhật lại hàng tiêu đề do tên/thứ tự cột không khớp.");
            headersModified = true;
            break;
          }
        }
      }
    }

    for (let i = 0; i < threads.length; i++) {
      const thread = threads[i];
      const messages = thread.getMessages();
      let processedAnyMessageInThisThread = false; // Cờ để biết có xử lý message nào trong thread này không

      Logger.log(`Bắt đầu xử lý luồng ${i+1}/${threads.length}, Subject: "${messages[0].getSubject()}" (Có ${messages.length} message)`);

      for (let j = 0; j < messages.length; j++) {
        const message = messages[j];

        // Chỉ xử lý những message CHƯA ĐỌC trong luồng này
        if (!message.isUnread()) {
          // Logger.log(`Message ${j+1} trong luồng đã được đọc, bỏ qua.`);
          continue;
        }

        const subject = message.getSubject();
        const body = message.getPlainBody();

        Logger.log(`  Đang xử lý message ${j+1}/${messages.length}: "${subject}"`);

        try {
          const data = parseEmailBody(body);
          Logger.log(`  Dữ liệu đã parse: ${JSON.stringify(data)}`);

          const requiredKeys = ["REPORT_DATE", "SYMBOL", "MAGIC_NUMBER", "REALIZED_PNL_TODAY", "ACCOUNT"];
          let missingKey = false;
          for (const key of requiredKeys) {
            if (data[key] === undefined || data[key] === null || String(data[key]).trim() === "") {
              Logger.log(`  Lỗi: Thiếu key bắt buộc "${key}" hoặc giá trị rỗng.`);
              missingKey = true;
              break;
            }
          }
          if (missingKey) {
            Logger.log(`  Body email để kiểm tra (nếu thiếu key):\n${body}`);
            // Đánh dấu đã đọc để không xử lý lại message lỗi này
            message.markRead();
            continue;
          }

          const realizedPnl = parseFloat(String(data.REALIZED_PNL_TODAY).replace(",", "."));
          // ... (các parseFloat khác giữ nguyên) ...
          const floatingPl = data.FLOATING_PL_CURRENT ? parseFloat(String(data.FLOATING_PL_CURRENT).replace(",", ".")) : 0;
          const balanceCurrent = data.BALANCE_CURRENT ? parseFloat(String(data.BALANCE_CURRENT).replace(",", ".")) : 0;
          const equityCurrent = data.EQUITY_CURRENT ? parseFloat(String(data.EQUITY_CURRENT).replace(",", ".")) : 0;
          const maxEquityToday = data.MAX_EQUITY_TODAY ? parseFloat(String(data.MAX_EQUITY_TODAY).replace(",", ".")) : 0;
          const tradesClosedToday = data.TRADES_CLOSED_TODAY ? parseInt(data.TRADES_CLOSED_TODAY) : 0;
          const winRateToday = data.WIN_RATE_TODAY ? parseFloat(String(data.WIN_RATE_TODAY).replace(",", ".")) : 0;


          if (isNaN(realizedPnl)) {
            Logger.log(`  Lỗi: REALIZED_PNL_TODAY "${data.REALIZED_PNL_TODAY}" không phải là số hợp lệ.`);
            message.markRead(); // Đánh dấu đã đọc
            continue;
          }

          if (isDuplicateEntry(sheet, data.REPORT_DATE, data.SYMBOL, data.MAGIC_NUMBER)) {
            Logger.log(`  Bỏ qua mục trùng lặp: Ngày ${data.REPORT_DATE}, Symbol ${data.SYMBOL}, Magic ${data.MAGIC_NUMBER}`);
            message.markRead(); // Đánh dấu đã đọc dù trùng
            // Không addLabel cho thread ở đây vội, có thể có message khác trong thread chưa xử lý
            continue;
          }

          const rowData = [
            new Date(), data.REPORT_DATE, data.SERVER_TIME_REPORT || "", data.SYMBOL, data.ACCOUNT,
            data.MAGIC_NUMBER, data.EA_STATUS || "N/A", realizedPnl, floatingPl, balanceCurrent,
            equityCurrent, maxEquityToday, tradesClosedToday, winRateToday, subject
          ];
          sheet.appendRow(rowData);
          Logger.log(`  Đã ghi dữ liệu: ${JSON.stringify(rowData)}`);

          message.markRead(); // ĐÁNH DẤU MESSAGE NÀY LÀ ĐÃ ĐỌC sau khi xử lý thành công
          processedAnyMessageInThisThread = true;

        } catch (e) {
          Logger.log(`  Lỗi khi xử lý message "${subject}": ${e.toString()} \nStack: ${e.stack}`);
           if (message.isUnread()) message.markRead(); // Đánh dấu đã đọc nếu có lỗi để không thử lại
        }
      } // end messages loop

      // Sau khi xử lý tất cả các message trong thread, NẾU có ít nhất 1 message được xử lý thành công
      // thì mới gắn nhãn cho TOÀN BỘ THREAD.
      if (processedAnyMessageInThisThread) {
        thread.addLabel(GmailApp.getUserLabelByName(LABEL_NAME_PROCESSED));
        Logger.log(`Đã gắn nhãn "${LABEL_NAME_PROCESSED}" cho luồng: "${messages[0].getSubject()}"`);
        // thread.moveToArchive(); // Tùy chọn
      } else {
        Logger.log(`Không có message nào được xử lý mới trong luồng: "${messages[0].getSubject()}". Không gắn nhãn cho luồng.`);
      }

    } // end threads loop
  } catch (err) {
    Logger.log(`Lỗi tổng thể trong processDailyPLReports: ${err.toString()} \nStack: ${err.stack}`);
  }
}

// ----- CÁC HÀM PHỤ TRỢ GIỮ NGUYÊN -----
// parseEmailBody, createLabelIfNotExists, isDuplicateEntry, testProcessSingleEmail
// (Bạn có thể dán lại các hàm đó vào đây từ phiên bản trước)
// ----- HÀM PHỤ TRỢ ĐỂ PHÂN TÍCH NỘI DUNG EMAIL -----
function parseEmailBody(body) {
  const data = {};
  if (!body || typeof body !== 'string') {
    Logger.log("parseEmailBody: Input body không hợp lệ hoặc rỗng.");
    return data;
  }
  const lines = body.split('\n');
  lines.forEach(line => {
    line = line.trim();
    if (line.startsWith("---") || line === "") {
      return;
    }
    const parts = line.split(':');
    if (parts.length >= 2) {
      const key = parts[0].trim().toUpperCase().replace(/-/g, "_");
      const value = parts.slice(1).join(':').trim();
      data[key] = value;
    }
  });
  return data;
}

// ----- HÀM PHỤ TRỢ ĐỂ TẠO NHÃN TRONG GMAIL (NẾU CHƯA CÓ) -----
function createLabelIfNotExists(labelName) {
  try {
    const label = GmailApp.getUserLabelByName(labelName);
    if (label) {
      return; // Nhãn đã tồn tại
    }
    GmailApp.createLabel(labelName);
    Logger.log(`Đã tạo nhãn: "${labelName}"`);
  } catch (e) {
    Logger.log(`Lỗi khi tạo hoặc kiểm tra nhãn "${labelName}": ${e.toString()}`);
  }
}

// ----- HÀM PHỤ TRỢ ĐỂ KIỂM TRA TRÙNG LẶP -----
function isDuplicateEntry(sheet, reportDateStr, symbol, magic) {
  if (!reportDateStr || !symbol || !magic) {
      Logger.log(`isDuplicateEntry: Thiếu thông tin đầu vào để kiểm tra trùng lặp (Date: ${reportDateStr}, Symbol: ${symbol}, Magic: ${magic}).`);
      return false;
  }
  const dataRange = sheet.getDataRange();
  if (dataRange.getNumRows() <= 1) return false; // Chỉ có tiêu đề hoặc rỗng

  const data = dataRange.getValues();
  const reportDateColIndex = 1; // Cột B là REPORT_DATE
  const symbolColIndex = 3;     // Cột D là SYMBOL
  const magicColIndex = 5;      // Cột F là MAGIC_NUMBER

  for (let i = 1; i < data.length; i++) {
    const rowDate = data[i][reportDateColIndex] ? String(data[i][reportDateColIndex]).trim() : "";
    const rowSymbol = data[i][symbolColIndex] ? String(data[i][symbolColIndex]).trim() : "";
    const rowMagic = data[i][magicColIndex] ? String(data[i][magicColIndex]).trim() : "";

    if (rowDate === String(reportDateStr).trim() &&
        rowSymbol === String(symbol).trim() &&
        rowMagic === String(magic).trim()) {
      return true;
    }
  }
  return false;
}

// ----- HÀM ĐỂ TEST THỦ CÔNG -----
function testProcessSingleEmail() {
  const TEST_EMAIL_ID = "ID_EMAIL_MAU_CUA_BAN"; // <<< THAY ID EMAIL MẪU THỰC TẾ VÀO ĐÂY
  if (TEST_EMAIL_ID === "ID_EMAIL_MAU_CUA_BAN" || TEST_EMAIL_ID === "") {
      Logger.log("Vui lòng cung cấp ID email hợp lệ trong hàm testProcessSingleEmail để chạy thử nghiệm.");
      return;
  }

  try {
    const message = GmailApp.getMessageById(TEST_EMAIL_ID);
    if (!message) {
      Logger.log(`Không tìm thấy email với ID: ${TEST_EMAIL_ID}`);
      return;
    }

    const subject = message.getSubject();
    const body = message.getPlainBody();
    Logger.log(`--- Bắt đầu Test Với Email Subject: ${subject} ---`);
    Logger.log("Nội dung Email Body:\n" + body);

    const data = parseEmailBody(body);
    Logger.log("Dữ liệu đã Parse: " + JSON.stringify(data));

    const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
    const sheet = ss.getSheetByName(SHEET_NAME);

    const requiredKeys = ["REPORT_DATE", "SYMBOL", "MAGIC_NUMBER", "REALIZED_PNL_TODAY", "ACCOUNT"];
    let missingKeyTest = false;
    for (const key of requiredKeys) {
      if (data[key] === undefined || data[key] === null || String(data[key]).trim() === "") {
        Logger.log(`Test: Thiếu key bắt buộc "${key}" hoặc giá trị rỗng.`);
        missingKeyTest = true;
        break;
      }
    }

    if (!missingKeyTest) {
      const realizedPnl = parseFloat(String(data.REALIZED_PNL_TODAY).replace(",", "."));
      if(isNaN(realizedPnl)) {
        Logger.log(`Test: REALIZED_PNL_TODAY "${data.REALIZED_PNL_TODAY}" không hợp lệ.`);
        return;
      }
      const floatingPl = data.FLOATING_PL_CURRENT ? parseFloat(String(data.FLOATING_PL_CURRENT).replace(",", ".")) : 0;
      const balanceCurrent = data.BALANCE_CURRENT ? parseFloat(String(data.BALANCE_CURRENT).replace(",", ".")) : 0;
      const equityCurrent = data.EQUITY_CURRENT ? parseFloat(String(data.EQUITY_CURRENT).replace(",", ".")) : 0;
      const maxEquityToday = data.MAX_EQUITY_TODAY ? parseFloat(String(data.MAX_EQUITY_TODAY).replace(",", ".")) : 0;
      const tradesClosedToday = data.TRADES_CLOSED_TODAY ? parseInt(data.TRADES_CLOSED_TODAY) : 0;
      const winRateToday = data.WIN_RATE_TODAY ? parseFloat(String(data.WIN_RATE_TODAY).replace(",", ".")) : 0;

      const rowDataTest = [
        new Date(), data.REPORT_DATE, data.SERVER_TIME_REPORT || "", data.SYMBOL, data.ACCOUNT,
        data.MAGIC_NUMBER, data.EA_STATUS || "N/A", realizedPnl, floatingPl, balanceCurrent,
        equityCurrent, maxEquityToday, tradesClosedToday, winRateToday, subject
      ];
      Logger.log("Dữ liệu sẽ được ghi (test): " + JSON.stringify(rowDataTest));
      
      // ** Quan trọng: Để test thực sự ghi, bạn cần bỏ comment dòng dưới **
      // sheet.appendRow(rowDataTest);
      // Logger.log("Test data appended to sheet (nếu không comment dòng sheet.appendRow).");

    } else {
      Logger.log("Test: Thiếu dữ liệu thiết yếu, không thực hiện ghi.");
    }
  } catch (e) {
    Logger.log(`Lỗi trong testProcessSingleEmail: ${e.toString()} \nStack: ${e.stack}`);
  } finally {
    Logger.log(`--- Kết thúc Test Với Email ---`);
  }
}

Chúc bạn thành công và có những phân tích hiệu quả với Goldify EA! Nếu có bất kỳ câu hỏi nào, đừng ngần ngại liên hệ.

Hướng dẫn liên quan

1

Hướng Dẫn Khởi Động Lại VPS Forex Hàng Tuần: Tối Ưu Hiệu Suất Giao Dịch

Hướng dẫn chi tiết cách khởi động lại (reboot) VPS Forex hàng tuần để giải phóng tài nguyên, khắc phục lỗi và đảm bảo MT4/MT5 hoạt động ổn định. Bao gồm cách reboot thủ công và tự động.

notifytelegramemail
Đọc tiếp
2

Chọn Phiên Bản Windows Nào Cho VPS Forex? Hướng Dẫn Tối Ưu Cho Robot Giao Dịch (EA)

Hướng dẫn chi tiết lựa chọn phiên bản Windows (Windows Server 2022, 2019, 2016, 2012 R2 hay Windows 7, 10) tối ưu nhất cho VPS chạy robot giao dịch Forex (EA), đảm bảo ổn định, bảo mật và hiệu suất cao.

notifytelegramemail
Đọc tiếp
3

Hướng dẫn sử dụng tính năng Gửi thông báo (Notify) trong Goldify EA

Hướng dẫn chi tiết cách sử dụng tính năng Thông báo trong robot giao dịch Goldify, bao gồm cả 3 phương thức: Thông báo đẩy MT5, Email và Telegram

notifytelegramemail
Đọc tiếp