UNPKG

ynab_investment_tracking

Version:

Node module to keep track of investments value in YNAB. You Need Investment Tracking.

197 lines 10.6 kB
"use strict"; 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