Forked from shotasenga/wealthsimple-transaction-for-ynab.user.js
Last active
December 28, 2025 18:19
-
-
Save aleung/78bab19c8151c0f85c362869698d4c9f to your computer and use it in GitHub Desktop.
Export transactions from Wealthsimple to a CSV file for Actual Budget import
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Wealthsimple Activity Export (Visible Chequing) | |
| // @namespace https://leoliang.cn.eu.org | |
| // @version 2025.12.28.4 | |
| // @description Directly export currently visible Chequing transactions to CSV | |
| // @author Gemini / Shota Senga | |
| // @match https://my.wealthsimple.com/app/activity* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=wealthsimple.com | |
| // @grant none | |
| // ==/UserScript== | |
| (function () { | |
| "use strict"; | |
| const SELECTORS = { | |
| activityHeader: "//h1[contains(., 'Activity')]", | |
| dateGroups: "h2", | |
| }; | |
| init(); | |
| function init() { | |
| const observer = new MutationObserver(() => { | |
| const header = document.evaluate(SELECTORS.activityHeader, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; | |
| if (header && !document.getElementById('ws-export-container')) { | |
| addExportButton(header); | |
| } | |
| }); | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| } | |
| function addExportButton(header) { | |
| const container = document.createElement("div"); | |
| container.id = 'ws-export-container'; | |
| container.style.display = "inline-block"; | |
| const btn = document.createElement("button"); | |
| btn.innerText = "Export Visible Chequing"; | |
| btn.onclick = runImmediateExport; | |
| btn.style.cssText = ` | |
| margin-left: 15px; padding: 6px 14px; background-color: #000; | |
| color: #fff; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; font-size: 14px; | |
| `; | |
| container.appendChild(btn); | |
| header.parentElement.appendChild(container); | |
| } | |
| function runImmediateExport() { | |
| const status = createStatusBox(); | |
| updateStatus(status, 'Parsing visible transactions...'); | |
| try { | |
| const transactions = extractVisibleTransactions(); | |
| if (transactions.length === 0) { | |
| updateStatus(status, '⚠️ No Chequing items found on page'); | |
| setTimeout(() => status.remove(), 3000); | |
| return; | |
| } | |
| const csv = generateCsv(transactions); | |
| downloadCsv(csv); | |
| updateStatus(status, `✅ Exported ${transactions.length} items`); | |
| setTimeout(() => status.remove(), 2000); | |
| } catch (err) { | |
| console.error(err); | |
| updateStatus(status, '❌ Error - Check Console'); | |
| setTimeout(() => status.remove(), 5000); | |
| } | |
| } | |
| function extractVisibleTransactions() { | |
| const results = []; | |
| const h2s = Array.from(document.querySelectorAll(SELECTORS.dateGroups)); | |
| h2s.forEach(h2 => { | |
| const dateStr = h2.innerText; | |
| const dateObj = parseWsDate(dateStr); | |
| const isoDate = dateObj.toISOString().split('T')[0]; | |
| // Traverse siblings until next h2 to find buttons | |
| let nextEl = h2.nextElementSibling; | |
| while (nextEl && nextEl.tagName !== 'H2') { | |
| const buttons = nextEl.querySelectorAll('button[type="button"]'); | |
| buttons.forEach(btn => { | |
| const pElements = Array.from(btn.querySelectorAll('p')); | |
| const isChequing = pElements.some(p => p.innerText === "Chequing"); | |
| if (isChequing) { | |
| // Payee is usually the first p with unmasked privacy rule | |
| const payee = pElements[0]?.innerText || "Unknown Payee"; | |
| const amountRaw = pElements.find(p => p.innerText.includes('$'))?.innerText || "0"; | |
| results.push({ | |
| date: isoDate, | |
| payee: payee, | |
| amount: cleanAmount(amountRaw) | |
| }); | |
| } | |
| }); | |
| nextEl = nextEl.nextElementSibling; | |
| } | |
| }); | |
| return results; | |
| } | |
| function parseWsDate(str) { | |
| const now = new Date(); | |
| const lower = str.toLowerCase(); | |
| if (lower.includes('today')) return new Date(now.setHours(0,0,0,0)); | |
| if (lower.includes('yesterday')) { | |
| const y = new Date(); | |
| y.setDate(y.getDate() - 1); | |
| return new Date(y.setHours(0,0,0,0)); | |
| } | |
| const d = new Date(str); | |
| if (isNaN(d.getTime())) return now; | |
| return d; | |
| } | |
| function cleanAmount(val) { | |
| return val.replace(/[−\u2212]/g, '-') | |
| .replace(/[CAD\s$,]/g, '') | |
| .trim(); | |
| } | |
| function generateCsv(data) { | |
| const headers = ["Date", "Payee", "Amount"]; | |
| const rows = [headers.join(",")]; | |
| data.forEach(t => { | |
| const row = [ | |
| `"${t.date}"`, | |
| `"${t.payee.replace(/"/g, '""')}"`, | |
| `"${t.amount}"` | |
| ]; | |
| rows.push(row.join(",")); | |
| }); | |
| return rows.join("\n"); | |
| } | |
| function downloadCsv(content) { | |
| const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' }); | |
| const url = URL.createObjectURL(blob); | |
| const link = document.createElement("a"); | |
| const timestamp = new Date().toISOString().slice(0,10); | |
| link.href = url; | |
| link.download = `ws-export-${timestamp}.csv`; | |
| link.click(); | |
| setTimeout(() => URL.revokeObjectURL(url), 100); | |
| } | |
| function createStatusBox() { | |
| const s = document.createElement('div'); | |
| s.style.cssText = ` | |
| position: fixed; bottom: 20px; right: 20px; z-index: 10002; | |
| background: #333; color: #fff; padding: 12px 20px; | |
| border-radius: 8px; font-family: monospace; font-size: 12px; | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.3); | |
| `; | |
| document.body.appendChild(s); | |
| return s; | |
| } | |
| function updateStatus(el, text) { if (el) el.innerText = text; } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment