ynab_investment_tracking
Version:
Node module to keep track of investments value in YNAB. You Need Investment Tracking.
197 lines • 10.6 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.run = void 0;
const ynab_1 = require("ynab");
const yahoo_finance2_1 = __importDefault(require("yahoo-finance2"));
const TransactionFlagColor_1 = require("ynab/dist/models/TransactionFlagColor");
const TransactionClearedStatus_1 = require("ynab/dist/models/TransactionClearedStatus");
const run = () => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b;
if (!((_a = process.env) === null || _a === void 0 ? void 0 : _a.YNAB_API_TOKEN) || !((_b = process.env) === null || _b === void 0 ? void 0 : _b.YNAB_BUDGET_ID)) {
throw "Missing environment variables";
}
else {
const budgetID = process.env.YNAB_BUDGET_ID;
const ynabAPI = new ynab_1.API(process.env.YNAB_API_TOKEN);
console.log(`[YNIT] Fetching budget...`);
const { data: { budget } } = yield ynabAPI.budgets.getBudgetById(budgetID);
console.log(`[YNIT] Fetched.`);
console.log(`[YNIT] Fetching accounts...`);
const { data: { accounts } } = yield ynabAPI.accounts.getAccounts(budget.id);
console.log(`[YNIT] Fetched ${accounts.length} accounts.`);
const investmentAccounts = accounts.filter((account) => { var _a; return (_a = account === null || account === void 0 ? void 0 : account.note) === null || _a === void 0 ? void 0 : _a.includes("INVESTMENT_ACCOUNT"); });
console.log(`[YNIT] Found ${investmentAccounts.length} investment accounts.`);
const accountsWithHoldings = yield Promise.all(investmentAccounts.map((account) => __awaiter(void 0, void 0, void 0, function* () {
const holdings = yield processAccount(ynabAPI, budgetID, account);
return { account, holdings };
})));
const symbolsToFetch = [];
accountsWithHoldings.forEach(({ holdings }) => {
[...holdings.keys()].forEach(k => {
if (k !== "CASH" && symbolsToFetch.find(j => j === k) === undefined) {
symbolsToFetch.push(k);
}
});
});
if (symbolsToFetch.length) {
console.log(`[YNIT] Fetching quotes for: ${symbolsToFetch}`);
const quotes = yield yahoo_finance2_1.default.quote(symbolsToFetch, { fields: ["regularMarketPrice"] });
console.log(`[YNIT] Fetched quotes.`);
console.log(`[YNIT] Current prices:`);
quotes.forEach((q) => {
console.log(` [${q.symbol}] Price: ${q.regularMarketPrice}`);
});
const bulkTransactions = yield Promise.all(accountsWithHoldings.map(({ account, holdings }) => __awaiter(void 0, void 0, void 0, function* () { return yield buildUpdates(account, holdings, quotes); })));
const mergedTransactions = bulkTransactions.reduce(({ transactions: at }, { transactions: vt }) => ({ transactions: [...at, ...vt] }), { transactions: [] });
if (mergedTransactions.transactions.length) {
console.log(`[YNIT] Posting transactions to YNAB...`);
yield ynabAPI.transactions.createTransactions(budgetID, mergedTransactions);
console.log(`[YNIT] Posted transactions.`);
}
}
else {
console.log(`[YNIT] No symbols to fetch.`);
}
}
});
exports.run = run;
const buildUpdates = (account, accountHoldings, quotes) => __awaiter(void 0, void 0, void 0, function* () {
const bulkTransactions = buildUpdateTransactionsFromQuotesAndHoldings(account, quotes, accountHoldings);
if (bulkTransactions.transactions.length === 0) {
console.log(`[YNIT] No updates needed for ${account.name}.`);
}
else {
console.log(`[YNIT] Updates for ${account.name}:`);
bulkTransactions.transactions.forEach((tx) => {
tx.memo.split(",").forEach((item) => {
console.log(` [${item.split("|")[0]}] $${Number(item.split("|")[1]) / 1000}`);
});
});
}
return bulkTransactions;
});
const processAccount = (ynabAPI, budgetID, account) => __awaiter(void 0, void 0, void 0, function* () {
console.log(`[YNIT] Processing account ${account.name}...`);
console.log(`[YNIT] Fetching transactions...`);
const { data } = yield ynabAPI.transactions.getTransactionsByAccount(budgetID, account.id);
console.log(`[YNIT] Fetched ${data.transactions.length} transactions.`);
const accountHoldings = buildAccountHoldingsFromTransactions(data);
console.log(`[YNIT] Current holdings for ${account.name}:`);
accountHoldings.forEach(({ count, value }, ticker) => {
console.log(` [${ticker}] Shares: ${count} Value: $${value}`);
});
return accountHoldings;
});
const isTransferOut = (t) => (t === null || t === void 0 ? void 0 : t.transfer_account_id) && t.amount < 0;
const isStockUpdate = (t) => { var _a; return (t === null || t === void 0 ? void 0 : t.memo) && ((_a = t === null || t === void 0 ? void 0 : t.memo) === null || _a === void 0 ? void 0 : _a.startsWith("$")); };
const updateHoldingCount = (ticker, count, holdings) => {
const holding = holdings.get(ticker) || { count: 0, value: 0 };
holding.count = holding.count + count;
holdings.set(ticker, holding);
return holdings;
};
const updateHoldingValue = (ticker, value, holdings) => {
const holding = holdings.get(ticker) || { count: 0, value: 0 };
holding.value = holding.value + value;
holdings.set(ticker, holding);
return holdings;
};
const buildAccountHoldingsFromTransactions = (data) => {
let accountHoldings = new Map();
const transactions = data.transactions.map(t => {
return Object.assign(Object.assign({}, t), { date: Date.parse(t.date) });
});
console.log(`[YNIT] Processing transactions...`);
transactions.forEach(t => {
if (t.payee_name === "Bulk Investment Value Update") {
t === null || t === void 0 ? void 0 : t.memo.split(",").forEach((item) => {
const [ticker, amount] = item.replace("$", "").split("|");
accountHoldings = updateHoldingValue(ticker, Number(amount) / 1000, accountHoldings);
});
}
else if (!isTransferOut(t) && isStockUpdate(t)) {
const [ticker, action] = t.memo.replace("$", "").split("|");
accountHoldings = updateHoldingValue(ticker, t.amount / 1000, accountHoldings);
const holding = accountHoldings.get(ticker) || { count: 0, value: 0 };
if (action.startsWith("BUY")) {
holding.count = holding.count + +action.split(" ")[1];
if (!(t === null || t === void 0 ? void 0 : t.transfer_account_id)) {
accountHoldings = updateHoldingCount("CASH", -t.amount / 1000, accountHoldings);
}
}
if (action.startsWith("SELL")) {
if (action.endsWith("ALL")) {
holding.count = 0;
}
else {
holding.count = holding.count - +action.split(" ")[1];
}
accountHoldings = updateHoldingCount("CASH", -t.amount / 1000, accountHoldings);
}
accountHoldings.set(ticker, holding);
}
else {
accountHoldings = updateHoldingCount("CASH", (t.amount / 1000), accountHoldings);
accountHoldings = updateHoldingValue("CASH", (t.amount / 1000), accountHoldings);
}
});
console.log(`[YNIT] Transactions processed.`);
return accountHoldings;
};
const buildUpdateTransactionsFromQuotesAndHoldings = (account, quotes, holdings) => {
const groupedTransactions = { transactions: [] };
const currentDate = new Date();
const offset = currentDate.getTimezoneOffset();
holdings.forEach(({ count, value }, ticker) => {
const quote = ticker === "CASH" ? { regularMarketPrice: 1 } : quotes.find(k => k.symbol === ticker);
if (!(quote === null || quote === void 0 ? void 0 : quote.regularMarketPrice)) {
console.log(`[WARNING] Failed to get quote for: ${ticker}`);
}
else {
const difference = Math.round(((quote.regularMarketPrice * count) - value) * 1000);
if (difference !== 0) {
const memo = `$${ticker}|`;
const last = groupedTransactions.transactions[groupedTransactions.transactions.length - 1];
if (!last || last.memo.length + memo.length + 1 >= 500) {
groupedTransactions.transactions.push({
"account_id": account.id,
"date": new Date(currentDate.getTime() - (offset * 60 * 1000)).toISOString().split("T")[0],
"amount": difference,
"payee_name": `Bulk Investment Value Update`,
"memo": `$${ticker}|${difference}`,
"cleared": TransactionClearedStatus_1.TransactionClearedStatus.Cleared,
"approved": true,
"flag_color": TransactionFlagColor_1.TransactionFlagColor.Blue
});
}
else {
groupedTransactions.transactions[groupedTransactions.transactions.length - 1] = Object.assign(Object.assign({}, last), { amount: last.amount + difference, memo: `${last.memo},$${ticker}|${difference}` });
}
}
}
});
return groupedTransactions;
};
(() => __awaiter(void 0, void 0, void 0, function* () {
try {
yield (0, exports.run)();
}
catch (err) {
console.log(err);
process.exit(1);
}
process.exit(0);
}))();
//# sourceMappingURL=index.js.map