add track-investments script
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
# Actual Budget cache
|
# Actual Budget cache
|
||||||
cache/
|
cache/
|
||||||
|
simplefin.credentials
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ This is a collection of useful scripts to help you manage your Actual Budget.
|
|||||||
- [Loan Interest Calculator](#loan-interest-calculator)
|
- [Loan Interest Calculator](#loan-interest-calculator)
|
||||||
- [Tracking Home Prices (Zillow's Zestimate)](#tracking-home-prices-zillows-zestimate)
|
- [Tracking Home Prices (Zillow's Zestimate)](#tracking-home-prices-zillows-zestimate)
|
||||||
- [Tracking Car Prices (Kelley Blue Book)](#tracking-car-prices-kelley-blue-book)
|
- [Tracking Car Prices (Kelley Blue Book)](#tracking-car-prices-kelley-blue-book)
|
||||||
|
- [Tracking Investment Accounts](#tracking-investment-accounts)
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -157,3 +158,50 @@ $ node kbb.js
|
|||||||
|
|
||||||
It is recommended to run this script once per month. Note that you will have
|
It is recommended to run this script once per month. Note that you will have
|
||||||
to periodically update the mileage in the account note.
|
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.
|
||||||
|
|||||||
Generated
+10
-1
@@ -7,7 +7,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actual-app/api": "^6.8.1",
|
"@actual-app/api": "^6.8.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"jsdom": "^24.1.0"
|
"jsdom": "^24.1.0",
|
||||||
|
"readline-sync": "^1.4.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actual-app/api": {
|
"node_modules/@actual-app/api": {
|
||||||
@@ -667,6 +668,14 @@
|
|||||||
"node": ">= 6"
|
"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": {
|
"node_modules/requires-port": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
|
|||||||
+2
-1
@@ -2,6 +2,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actual-app/api": "^6.8.1",
|
"@actual-app/api": "^6.8.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"jsdom": "^24.1.0"
|
"jsdom": "^24.1.0",
|
||||||
|
"readline-sync": "^1.4.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
})();
|
||||||
@@ -43,6 +43,17 @@ module.exports = {
|
|||||||
return data.data;
|
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()) {
|
getLastTransactionDate: async function (account, cutoffDate=new Date()) {
|
||||||
const data = await api.runQuery(
|
const data = await api.runQuery(
|
||||||
api.q('transactions')
|
api.q('transactions')
|
||||||
|
|||||||
Reference in New Issue
Block a user