Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save aleung/78bab19c8151c0f85c362869698d4c9f to your computer and use it in GitHub Desktop.

Select an option

Save aleung/78bab19c8151c0f85c362869698d4c9f to your computer and use it in GitHub Desktop.
Export transactions from Wealthsimple to a CSV file for Actual Budget import
// ==UserScript==
// @name Wealthsimple Activity Export (Stable Version)
// @namespace https://leoliang.cn.eu.org
// @version 2026.02.04.1
// @description Stable export button injection for Wealthsimple
// @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";
// 1. 每隔 1 秒检查一次页面,确保按钮存在
const checkInterval = setInterval(injectButton, 1000);
function injectButton() {
// 如果按钮已经存在,就不再注入
if (document.getElementById('ws-export-container')) return;
// 寻找 Activity 页面的主标题
// Wealthsimple 的标题通常在 main 标签内的第一个 h1
const header = document.querySelector('main h1');
if (header && header.innerText.includes('Activity')) {
const container = document.createElement("div");
container.id = 'ws-export-container';
container.style.display = "inline-flex";
container.style.alignItems = "center";
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;
height: fit-content;
`;
container.appendChild(btn);
header.parentElement.appendChild(container);
console.log("Wealthsimple Export: Button injected successfully.");
}
}
function runImmediateExport() {
const results = [];
const h2s = Array.from(document.querySelectorAll('h2'));
if (h2s.length === 0) {
alert("No transactions found on page. Please make sure you are on the Activity tab.");
return;
}
h2s.forEach(h2 => {
const dateObj = parseWsDate(h2.innerText);
const isoDate = dateObj.toISOString().split('T')[0];
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) {
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;
}
});
if (results.length > 0) {
downloadCsv(results);
} else {
alert("No Chequing transactions found in the visible list.");
}
}
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);
return isNaN(d.getTime()) ? now : d;
}
function cleanAmount(val) {
return val.replace(/[\u2212]/g, '-').replace(/[CAD\s$,]/g, '').trim();
}
function downloadCsv(data) {
const rows = [["Date", "Payee", "Amount"].join(",")];
data.forEach(t => {
rows.push([`"${t.date}"`, `"${t.payee.replace(/"/g, '""')}"`, `"${t.amount}"`].join(","));
});
const blob = new Blob([rows.join("\n")], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `ws-export-${new Date().toISOString().slice(0,10)}.csv`;
link.click();
setTimeout(() => URL.revokeObjectURL(url), 100);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment