Add sync-crypto.js for other cryptocurrencies besides just BTC (#31)
* add sync-crypto.js * update crypto payee name
This commit is contained in:
@@ -407,3 +407,43 @@ node sync-bitcoin.js
|
|||||||
```
|
```
|
||||||
|
|
||||||
It is recommended to run this script once per day or week.
|
It is recommended to run this script once per day or week.
|
||||||
|
|
||||||
|
### Tracking Crypto Prices
|
||||||
|
|
||||||
|
This script tracks the value of any cryptocurrency. Similar to the Bitcoin
|
||||||
|
script, it adds new transactions to keep the account balance equal to the
|
||||||
|
latest value. Values are retrieved from the Kraken API, but the script is
|
||||||
|
customizable to use other APIs if needed.
|
||||||
|
|
||||||
|
To use, set these tags in the account notes:
|
||||||
|
|
||||||
|
- `CRYPTO:<symbol>,<symbol>,...` - where each symbol is a cryptocurrency
|
||||||
|
you want to track, e.g. `CRYPTO:BTC,ETH,DOGE`
|
||||||
|
- `<symbol>:<amount>[,<krakenPair>]` - where `<symbol>` is the cryptocurrency
|
||||||
|
symbol and `<amount>` is the amount you own. If using Kraken, specify the Kraken
|
||||||
|
pair to use for the price. Obtain this by visiting
|
||||||
|
[https://api.kraken.com/0/public/Ticker?pair=<symbol\>usd](https://api.kraken.com/0/public/Ticker?pair=btcusd)
|
||||||
|
and locating the key under `result` that matches your symbol.
|
||||||
|
|
||||||
|
For example, if you own 0.01 BTC and 1 ETH, your account note would look like:
|
||||||
|
```
|
||||||
|
CRYPTO:BTC,ETH
|
||||||
|
BTC:0.01,XXBTZUSD
|
||||||
|
ETH:1,XXETHZUSD
|
||||||
|
```
|
||||||
|
|
||||||
|
To use a different API, set the following in your `.env` file: (example using Bitcoin)
|
||||||
|
```shell
|
||||||
|
CRYPTO_PRICE_URL_BTC="https://api.kraken.com/0/public/Ticker?pair=xbtgbp"
|
||||||
|
CRYPTO_PRICE_JSON_PATH_BTC="result.XXBTZGBP.c[0]"
|
||||||
|
```
|
||||||
|
|
||||||
|
To run:
|
||||||
|
|
||||||
|
```console
|
||||||
|
node sync-crypto.js
|
||||||
|
```
|
||||||
|
|
||||||
|
It is recommended to run this script once per day or week. If you are retrieving
|
||||||
|
BTC prices using this script, DO NOT run the `sync-bitcoin.js` script as well, as it will
|
||||||
|
set the account balance to the value of your BTC holdings only.
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
const { closeBudget, openBudget, getTransactions, getAccountNote, getAccountBalance, getTagValue, ensurePayee } = require('./utils');
|
||||||
|
const api = require('@actual-app/api');
|
||||||
|
|
||||||
|
function getValueAtPath(obj, path) {
|
||||||
|
const keys = path.split('.').filter(Boolean);
|
||||||
|
|
||||||
|
return keys.reduce((acc, key) => {
|
||||||
|
const match = key.match(/^([^\[\]]+)(\[(\d+)\])?$/);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const property = match[1];
|
||||||
|
const index = match[3];
|
||||||
|
|
||||||
|
acc = acc[property];
|
||||||
|
|
||||||
|
if (index !== undefined) {
|
||||||
|
acc = acc[parseInt(index, 10)];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
acc = acc[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCryptoPrice(crypto, krakenPath) {
|
||||||
|
const url = process.env[`CRYPTO_PRICE_URL_${crypto.toUpperCase()}`] || `https://api.kraken.com/0/public/Ticker?pair=${crypto}usd`;
|
||||||
|
if (!krakenPath && !process.env[`CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()}`]) {
|
||||||
|
console.error(`No Kraken path provided for ${crypto}. Please set CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()} environment variable or provide krakenPath argument.`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const path = process.env[`CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()}`] || `result.${krakenPath}.c[0]`;
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const json = await response.json();
|
||||||
|
return getValueAtPath(json, path);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching price for ${crypto}:`, error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await openBudget();
|
||||||
|
const payeeId = await ensurePayee(process.env.CRYPTO_PAYEE_NAME || 'Crypto Price Change');
|
||||||
|
const accounts = await api.getAccounts();
|
||||||
|
for (const account of accounts) {
|
||||||
|
if (account.closed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to set cutoff date to tomorrow to ensure we get the latest transactions
|
||||||
|
const cutoffDate = new Date();
|
||||||
|
cutoffDate.setDate(cutoffDate.getDate() + 1);
|
||||||
|
|
||||||
|
const note = await getAccountNote(account, cutoffDate);
|
||||||
|
|
||||||
|
// Get cryptos list from the note
|
||||||
|
let cryptos = getTagValue(note, 'CRYPTO', null);
|
||||||
|
if (!cryptos) continue;
|
||||||
|
cryptos = cryptos.split(',').map(c => c.trim()).filter(c => c.length > 0);
|
||||||
|
if (cryptos.length === 0) continue;
|
||||||
|
|
||||||
|
console.log(`Processing account: ${account.name}, Cryptos: ${cryptos.join(', ')}`);
|
||||||
|
|
||||||
|
let totalTargetBalance = 0;
|
||||||
|
for (const crypto of cryptos) {
|
||||||
|
const cryptoTag = getTagValue(note, crypto, 0.0);
|
||||||
|
if (!cryptoTag) continue;
|
||||||
|
cryptoTagSplit = cryptoTag.split(',');
|
||||||
|
|
||||||
|
// Get the amount from the first part of the tag
|
||||||
|
amount = parseFloat(cryptoTagSplit[0]);
|
||||||
|
if (isNaN(amount) || amount <= 0) continue;
|
||||||
|
console.log(`Crypto: ${crypto}, Amount: ${amount}`);
|
||||||
|
|
||||||
|
// Get Kraken path from the second part of the tag, if it exists
|
||||||
|
let krakenPath = cryptoTagSplit[1]
|
||||||
|
console.log(`Kraken Path: ${krakenPath}`);
|
||||||
|
|
||||||
|
// Get the crypto price
|
||||||
|
const cryptoPrice = await getCryptoPrice(crypto, krakenPath);
|
||||||
|
if (!cryptoPrice) {
|
||||||
|
console.error(`Unable to retrieve price for ${crypto}. Check your CRYPTO_PRICE_URL_${crypto.toUpperCase()} and CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()} environment variables`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Crypto: ${crypto}, Price: ${cryptoPrice}`);
|
||||||
|
|
||||||
|
const targetBalance = Math.round(cryptoPrice * amount * 100);
|
||||||
|
totalTargetBalance += targetBalance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we need to check if the total target balance is different from the current balance and update it
|
||||||
|
const currentBalance = await getAccountBalance(account, cutoffDate);
|
||||||
|
const diff = totalTargetBalance - currentBalance;
|
||||||
|
console.log(`Account: ${account.name}, Total Target Balance: ${totalTargetBalance}, Current Balance: ${currentBalance}, Diff: ${diff}`);
|
||||||
|
if (diff != 0) {
|
||||||
|
await api.importTransactions(account.id, [{
|
||||||
|
date: new Date(),
|
||||||
|
payee: payeeId,
|
||||||
|
amount: diff,
|
||||||
|
cleared: true,
|
||||||
|
reconciled: true,
|
||||||
|
notes: `Updated Crypto Price on ${new Date().toLocaleString()} (${cryptos.join(', ')})`,
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await closeBudget();
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user