add track-investments script

This commit is contained in:
Robert Dyer
2024-06-19 19:28:40 -05:00
parent bf6c723ade
commit b677570a2a
6 changed files with 200 additions and 2 deletions
+1
View File
@@ -1,5 +1,6 @@
# Actual Budget cache
cache/
simplefin.credentials
# Logs
logs
+48
View File
@@ -10,6 +10,7 @@ This is a collection of useful scripts to help you manage your Actual Budget.
- [Loan Interest Calculator](#loan-interest-calculator)
- [Tracking Home Prices (Zillow's Zestimate)](#tracking-home-prices-zillows-zestimate)
- [Tracking Car Prices (Kelley Blue Book)](#tracking-car-prices-kelley-blue-book)
- [Tracking Investment Accounts](#tracking-investment-accounts)
## Requirements
@@ -157,3 +158,50 @@ $ node kbb.js
It is recommended to run this script once per month. Note that you will have
to periodically update the mileage in the account note.
### Tracking Investment Accounts
**NOTE: This script only works with SimpleFIN accounts.**
This script tracks the value of an investment account. It adds new
transactions to keep the account balance equal to the latest value. This
requires connecting to SimpleFIN to grab the reported account balance, so
that the script can update the transactions to reflect that balance.
Note that I have some rules set up on the accounts that the script assumes.
First, all payees are set to the same name "Investment". Second, any money I
add to the account to fund it I set the category to something different. In
my case, since I am funding these with my paycheck I categorize them as
"Paycheck" but the key is to set a category different from the ones the script
will utilize. Then all other transactions are categorized as "Investment".
There are three tags you can set in the account notes:
- `calcInvestment` - this is the tag that tells the script to track the
balance. You will want this on each account, and then optionally one of the
following.
- `zeroSmall` - this is a helper tag that will zero out any small transactions
(less than $10). This is useful for accounts that have a lot of small
transactions that you don't want to track. For example, one of my accounts
shows dividends as separate transactions, so I use this tag to ignore
those as my account balance does not actually change when those occur (they
are reinvested).
- `dropPayments` - this is a helper tag that will zero out anything in the
payment column. One of my accounts lists stock purchases as separate
transactions, so I ignore those as my account balance does not actually
change when those occur. But these are typically larger payments and I want
to keep small payments (interest accrued) so I use this tag on that account
instead of `zeroSmall`.
Note that the code has a function named `shouldDrop` that might need to be
modified. This function lists transactions whose note contains certain
strings that are targeted when using `zeroSmall` and `dropPayments`. You may
need to update this to add additional notes to look for.
To run:
```console
$ node track-investments.js
```
It is recommended to run this script once per month.
+10 -1
View File
@@ -7,7 +7,8 @@
"dependencies": {
"@actual-app/api": "^6.8.1",
"dotenv": "^16.4.5",
"jsdom": "^24.1.0"
"jsdom": "^24.1.0",
"readline-sync": "^1.4.10"
}
},
"node_modules/@actual-app/api": {
@@ -667,6 +668,14 @@
"node": ">= 6"
}
},
"node_modules/readline-sync": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz",
"integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+2 -1
View File
@@ -2,6 +2,7 @@
"dependencies": {
"@actual-app/api": "^6.8.1",
"dotenv": "^16.4.5",
"jsdom": "^24.1.0"
"jsdom": "^24.1.0",
"readline-sync": "^1.4.10"
}
}
+128
View File
@@ -0,0 +1,128 @@
const api = require('@actual-app/api');
const fs = require('fs');
const readline = require('readline-sync');
const { closeBudget, ensureCategory, ensurePayee, getAccountBalance, getAccountNote, getTransactions, openBudget } = require('./utils');
require("dotenv").config();
const getCredentials = async () => {
const token = readline.question('Enter your SimpleFIN setup token: ');
const url = atob(token.trim());
const response = await fetch(url, { method: 'post' });
const api_url = await response.text();
const rest = api_url.split('//', 2)[1];
const auth = rest.split('@', 1)[0];
const username = auth.split(':')[0];
const pw = auth.split(':')[1];
const data = `${username}:${pw}`;
fs.writeFileSync('simplefin.credentials', data);
return data;
};
const loadCredentials = () => {
try {
return fs.readFileSync('simplefin.credentials', 'utf8');
} catch (err) {
return undefined;
}
};
const getSimplefinBalances = async () => {
let credentials = loadCredentials();
if (!credentials) {
credentials = await getCredentials();
}
const username = credentials.split(':')[0];
const pw = credentials.split(':')[1];
const url = `https://beta-bridge.simplefin.org/simplefin/accounts?start-date=${new Date().getTime()}&end-date=${new Date().getTime()}`;
const response = await fetch(url, {
headers: {
'Authorization': `Basic ${btoa(`${username}:${pw}`)}`
}
});
const data = await response.json();
const accounts = data.accounts;
const balances = {};
accounts.forEach(a => balances[a.name] = parseFloat(a.balance));
return balances;
};
const shouldDrop = (payment) => {
const note = payment.notes;
return note && (note.indexOf('YOU BOUGHT ') > -1 || note == 'Buy Other' || note == 'Sell Other');
};
const zeroTransaction = async (payment) => {
await api.updateTransaction(
payment.id,
{ 'amount': 0 }
);
}
(async () => {
await openBudget();
const payeeId = await ensurePayee(process.env.IMPORTER_INVESTMENT_PAYEE_NAME || 'Investment');
const categoryId = await ensureCategory(process.env.IMPORTER_INVESTMENT_CATEGORY_NAME || 'Investment');
const simplefinBalances = await getSimplefinBalances();
const accounts = await api.getAccounts();
for (const account of accounts) {
if (account.closed) {
continue;
}
const note = await getAccountNote(account);
if (note) {
const data = await getTransactions(account);
if (note.indexOf('zeroSmall') > -1) {
const payments = data.filter(payment => payment.amount > -10000 && payment.amount < 10000 && payment.amount != 0 && payment.category == categoryId)
for (const payment of payments) {
if (shouldDrop(payment)) {
await zeroTransaction(payment);
}
}
}
if (note.indexOf('dropPayments') > -1) {
const payments = data.filter(payment => payment.amount < 0)
for (const payment of payments) {
if (shouldDrop(payment)) {
await zeroTransaction(payment);
}
}
}
if (note.indexOf('calcInvestment') > -1) {
const currentBalance = await getAccountBalance(account);
const simplefinBalance = parseInt(simplefinBalances[account.name] * 100);
const diff = simplefinBalance - currentBalance;
if (diff != 0) {
console.log('Account:', account.name);
console.log('Simplefin Balance:', simplefinBalance);
console.log('Current Balance:', currentBalance);
console.log('Difference:', diff);
await api.importTransactions(account.id, [{
date: new Date(),
payee: payeeId,
amount: diff,
category: categoryId,
notes: `Update investment balance to ${simplefinBalance / 100}`,
}]);
}
}
}
}
await closeBudget();
})();
+11
View File
@@ -43,6 +43,17 @@ module.exports = {
return data.data;
},
getTransactions: async function (account) {
const data = await api.runQuery(
api.q('transactions')
.select('*')
.filter({
'account': account.id,
})
);
return data.data;
},
getLastTransactionDate: async function (account, cutoffDate=new Date()) {
const data = await api.runQuery(
api.q('transactions')