| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466 |
- /**
- * --------------------------------------------------------------------------------
- * FINANCIAL PORTFOLIO TRACKER (TypeScript Port)
- * --------------------------------------------------------------------------------
- * * OVERVIEW:
- * This script fetches financial trading data from a Notion database, retrieves
- * historical price data from Yahoo Finance, calculates performance metrics
- * (IRR, current value, dividends), and pushes the results back to Notion
- * and a TRMNL dashboard.
- * * TYPESCRIPT CONCEPTS COVERED:
- * - Interfaces & Type Definitions
- * - Async/Await & Promises
- * - Strict Null Checks & Type Guards
- * - Optional Chaining (?.) and Nullish Coalescing (??)
- * - Type Assertions (as ...)
- * - External Module Imports
- */
- // ---------------------------------------------------------
- // 0. IMPORTS
- // ---------------------------------------------------------
- // LEARNING TIP: Default vs Named Imports
- // 'yahooFinance' is a default import (export default ...).
- // '{ addDays, ... }' are named imports. You must use the exact name exported by the library.
- import yahooFinance from 'yahoo-finance2';
- import {
- addDays,
- differenceInDays,
- format,
- parseISO,
- subDays,
- isBefore,
- isAfter,
- isEqual,
- startOfDay,
- isWeekend
- } from 'date-fns';
- // ---------------------------------------------------------
- // 1. CONFIGURATION
- // ---------------------------------------------------------
- // LEARNING TIP: 'as const' Assertion
- // By adding `as const`, we tell TypeScript that these are READ-ONLY LITERALS.
- // Instead of being type 'string', LOG_COLORS.error is literally the string '\x1b[31m'.
- // This prevents accidental modification and allows us to create specific Types from keys.
- const LOG_COLORS = {
- error: '\x1b[31m', // Red
- warning: '\x1b[33m', // Yellow
- success: '\x1b[32m', // Green
- info: '\x1b[90m', // Gray
- debug: '\x1b[4m', // Underscore
- endcode: '\x1b[0m' // Reset
- } as const;
- // LEARNING TIP: Lookup Types (keyof typeof)
- // We create a new Type 'LogColorKey' that can ONLY be one of: "error" | "warning" | "success" | ...
- // This gives us autocomplete support when using these keys later.
- type LogColorKey = keyof typeof LOG_COLORS;
- const LOGGING_LEVELS = ["none", "error", "success", "warning", "info", "debug"];
- // Mutable config object (standard JavaScript object)
- const config = {
- // Functionality Switches
- update_notion: false,
- update_TRMNL: true,
- calculate_benchmark: true,
- // Configuration
- programm_cooldown_time: 15, // Minutes
- api_cooldown_time: 100, // Milliseconds
- trmnl_granularity: 80, // Days
- ticker_benchmark: "VGWL.DE",
- // Logging
- selected_logging_level: "warning",
- // API - NOTION
- notion_token: "secret_b7PiPL2FqC9QEikqkAEWOht7LmzPMIJMWTzUPWwbw4H",
- notion_db_id_trades: "95f7a2b697a249d4892d60d855d31bda",
- notion_db_id_investments: "2ba10a5f51bd8160ab9ee982bbef8cc3",
- notion_db_id_performance: "1c010a5f51bd806f90d8e76a1286cfd4",
- // API - TRMNL
- trmnl_url_chart_1: "https://usetrmnl.com/api/custom_plugins/334ea2ed-1f20-459a-bea5-dca2c8cf7714",
- trmnl_url_chart_2: "https://usetrmnl.com/api/custom_plugins/72950759-38df-49eb-99fb-b3e2e67c385e",
- trmnl_url_chart_3: "https://usetrmnl.com/api/custom_plugins/a975543a-51dc-4793-b7fa-d6a101dc4025",
- };
- // Headers configuration for API requests
- const notionHeaders = {
- "Authorization": "Bearer " + config.notion_token,
- "Content-Type": "application/json",
- "Notion-Version": "2022-02-22"
- };
- const trmnlHeaders = { "Content-Type": "application/json" };
- // ---------------------------------------------------------
- // 2. INTERFACES (Type Definitions)
- // ---------------------------------------------------------
- // LEARNING TIP: Interfaces
- // Interfaces describe the "Shape" of an object. TypeScript uses "Structural Typing".
- // If an object has the properties defined here, it is considered a 'Trade', even if
- // we didn't explicitly say `new Trade()`.
- interface Trade {
- ticker: string;
- date_open: string; // ISO Date string YYYY-MM-DD
-
- // LEARNING TIP: Union Types
- // 'date_close' can be a string OR the number 0.
- // TypeScript forces us to check which one it is before using it (Type Narrowing).
- date_close: string | 0;
-
- course_open: number;
- course_close: number;
- course_current: number;
- irr: number;
- units: number;
- dividends: number;
- }
- // LEARNING TIP: Index Signatures
- // 'TradesDict' is an object where the Keys are strings (IDs) and Values are 'Trade' objects.
- // This is used for Hash Maps / Dictionaries.
- interface TradesDict {
- [key: string]: Trade;
- }
- interface Investment {
- ticker: string;
- total_dividends: number;
- current_value: number;
- current_irr: number;
- total_performanance: number;
- }
- interface InvestmentsDict {
- [key: string]: Investment;
- }
- interface HistoryEntry {
- current_amount: number;
- current_invested: number;
- total_dividends: number;
- current_value: number;
- current_irr: number;
- current_course: number;
- total_performanance: number;
- }
- interface HistoryDailyTrade {
- [tradeId: string]: HistoryEntry;
- }
- interface HistoryPerTrade {
- [date: string]: HistoryDailyTrade;
- }
- interface TickerHistoryEntry {
- current_invested: number;
- total_dividends: number;
- current_value: number;
- current_irr: number;
- total_performanance: number;
-
- // LEARNING TIP: Optional Properties (?)
- // The '?' indicates these properties might NOT exist on the object.
- // When accessing them, the result is 'number | undefined'.
- current_amount?: number;
- current_course?: number;
-
- // Recursive Type: An entry can contain a nested benchmark entry
- benchmark?: TickerHistoryEntry;
- }
- interface HistoryDailyTicker {
- [ticker: string]: TickerHistoryEntry;
- }
- interface HistoryPerTicker {
- [date: string]: HistoryDailyTicker;
- }
- // Structure for Yahoo Finance Data map
- interface YFDataMap {
- [ticker: string]: {
- [date: string]: {
- close: number;
- dividends: number;
- }
- }
- }
- // ---------------------------------------------------------
- // 3. HELPER FUNCTIONS
- // ---------------------------------------------------------
- /**
- * Pauses execution for a set amount of time.
- * LEARNING TIP: Promises
- * JavaScript is single-threaded. 'setTimeout' schedules code to run later.
- * We wrap it in a Promise so we can use 'await sleep(1000)' to pause execution flow.
- */
- function sleep(ms: number) {
- return new Promise(resolve => setTimeout(resolve, ms));
- }
- /**
- * Prints messages to the console with colors.
- * Demonstrates default parameter values (newLine = true).
- */
- function logging(message = "", loggingLevel = "", newLine = true) {
- const configLevelIdx = LOGGING_LEVELS.indexOf(config.selected_logging_level);
- let messageLevelIdx = LOGGING_LEVELS.indexOf(loggingLevel);
- if (messageLevelIdx === -1) {
- messageLevelIdx = LOGGING_LEVELS.length;
- }
- if (messageLevelIdx <= configLevelIdx) {
- // LEARNING TIP: Type Assertion (as ...)
- // We know 'loggingLevel' is a string, but we need to tell TypeScript
- // that it matches one of the keys in LOG_COLORS to allow index access.
- const color = LOG_COLORS[loggingLevel as LogColorKey] || LOG_COLORS.info;
- const logText = `${color}[${loggingLevel}] ${LOG_COLORS.endcode}${message}`;
-
- if (newLine) {
- console.log(logText);
- } else {
- // process.stdout.write prints without a newline character at the end
- process.stdout.write(logText + " ");
- }
- }
- }
- /**
- * Calculates Internal Rate of Return (IRR) approximation.
- */
- function calculateIrr(dateNow: Date, dateOpen: Date, valueNow: number, valueOpen: number): number {
- let irr = 0.0;
- try {
- let daysDiff = differenceInDays(dateNow, dateOpen);
- if (daysDiff === 0) daysDiff = 1; // Prevent division by zero logic
- const years = daysDiff / 365.0;
- let ratio = valueNow / valueOpen;
- // Basic Annualized Return Formula logic
- if (ratio < 0) {
- ratio = ratio * -1;
- irr = Math.pow(ratio, 1 / years);
- irr = irr * -1;
- } else {
- irr = Math.pow(ratio, 1 / years);
- }
- return irr;
- } catch (e) {
- logging("Calculation of irr failed", "error");
- return 0.0;
- }
- }
- /**
- * Finds the oldest date in a dictionary of trades.
- */
- function getDateOpenOldestTrade(trades: TradesDict): Date {
- let oldest = new Date(); // Start with today
- for (const key in trades) {
- const trade = trades[key];
-
- // LEARNING TIP: Type Guard
- // In strict mode, dictionary access might return undefined.
- // We check 'if (!trade) continue' to ensure 'trade' exists before using it.
- if (!trade) continue;
-
- const d = parseISO(trade.date_open);
- // date-fns comparison function
- if (isBefore(d, oldest)) {
- oldest = d;
- }
- }
- return oldest;
- }
- /**
- * Extracts a unique list of ticker symbols from trades.
- */
- function filterListOfTickers(trades: TradesDict): string[] {
- const tickers: string[] = [];
- try {
- for (const key in trades) {
- const trade = trades[key];
- if (!trade) continue; // Safety check
- const t = trade.ticker;
- // Only add if not already in list
- if (!tickers.includes(t)) {
- tickers.push(t);
- }
- }
- logging("", "success");
- logging(`${tickers.length} tickers found`, "info");
- return tickers;
- } catch (e) {
- logging(`${e}`, "error");
- return [];
- }
- }
- /**
- * Creates a list of dates (e.g., weekly) from the oldest trade to today.
- */
- function createListFilteredDates(trades: TradesDict, daysSeperation: number): string[] {
- try {
- const stopDate = getDateOpenOldestTrade(trades);
- let indexDate = new Date();
- const listFilteredDates: string[] = [];
- // Loop backwards in time
- while (isAfter(indexDate, stopDate) || isEqual(indexDate, stopDate)) {
- listFilteredDates.push(format(indexDate, 'yyyy-MM-dd'));
- indexDate = subDays(indexDate, daysSeperation);
- }
-
- // Reverse to get chronological order (Oldest -> Newest)
- listFilteredDates.reverse();
-
- logging("", "success");
- logging(`${listFilteredDates.length} dates in weekly list`, "info");
- return listFilteredDates;
- } catch (e) {
- logging(`${e}`, "error");
- return [];
- }
- }
- function addBenchmarkTicker(tickers: string[], tickerBenchmark: string): string[] {
- if (!tickers.includes(tickerBenchmark)) {
- tickers.push(tickerBenchmark);
- }
- logging("", "success");
- return tickers;
- }
- /**
- * Helper to get the very last key in an object (assumes keys are sortable strings like dates).
- */
- function fetchLastKeyFromDict(dict: any): string {
- const keys = Object.keys(dict).sort();
-
- if (keys.length === 0) {
- throw new Error("Dictionary is empty, cannot fetch last key.");
- }
- const lastKey = keys[keys.length - 1];
- // LEARNING TIP: Strict Null Checks
- // Even though logic suggests lastKey exists, TS Strict Mode demands we verify it isn't undefined.
- if (lastKey === undefined) {
- throw new Error("Last key was undefined");
- }
-
- return lastKey;
- }
- // ---------------------------------------------------------
- // 4. NETWORK DOWNLOAD FUNCTIONS
- // ---------------------------------------------------------
- /**
- * Fetches pages from a Notion Database. Handles pagination automatically.
- * Returns Promise<any[]> because Notion objects are complex and we parse them later.
- */
- async function notionGetPages(dbId: string, numPages: number | null = null): Promise<any[]> {
- try {
- const url = `https://api.notion.com/v1/databases/${dbId}/query`;
- const getAll = numPages === null;
- const pageSize = getAll ? 100 : numPages;
-
- let payload: any = { page_size: pageSize };
- let results: any[] = [];
- let hasMore = true;
- let nextCursor = null;
- // Loop to fetch all pages if there are more than 100
- while (hasMore) {
- if (nextCursor) {
- payload['start_cursor'] = nextCursor;
- }
- // Using standard Fetch API
- const response = await fetch(url, {
- method: 'POST',
- headers: notionHeaders as any,
- body: JSON.stringify(payload)
- });
- if (!response.ok) throw new Error(`Notion API error: ${response.status}`);
-
- const data = await response.json();
- results.push(...data.results);
-
- // Check if Notion says there are more pages
- hasMore = data.has_more && getAll;
- nextCursor = data.next_cursor;
-
- if (!getAll && results.length >= (numPages || 0)) break;
- }
- return results;
- } catch (e) {
- logging("Notion Fetch Error", "error");
- return [];
- }
- }
- /**
- * Fetches trades from Notion and formats them into our 'TradesDict' interface.
- */
- async function fetchFormatNotionTrades(dbIdTrades: string): Promise<TradesDict> {
- const trades: TradesDict = {};
- let formatErrors = 0;
- let numberOfTrades = 0;
- // Await the asynchronous fetch
- const data = await notionGetPages(dbIdTrades);
- if (data.length === 0) return {};
- for (const page of data) {
- numberOfTrades++;
- try {
- // Extract properties from Notion's specific JSON structure
- const props = page.properties;
-
- // LEARNING TIP: Optional Chaining (?.)
- // props["Close"] might not exist. If it doesn't, ?. returns undefined immediately
- // instead of crashing with "Cannot read property 'date' of undefined".
- let dateClose: string | 0 = 0;
- if (props["Close"]?.date?.start) {
- dateClose = props["Close"].date.start;
- }
- const dateOpen = props["Open"]?.date?.start;
- if (!dateOpen) throw new Error("Missing Open Date");
- // LEARNING TIP: Nullish Coalescing (??)
- // If the value on the left is null or undefined, use the default value on the right.
- const trade: Trade = {
- ticker: props["Ticker"]?.select?.name ?? "UNKNOWN",
- date_open: dateOpen,
- date_close: dateClose,
- course_open: props["Open (€)"]?.number ?? 0,
- course_close: props["Close (€)"]?.number ?? 0,
- course_current: props["Current (€)"]?.number ?? 0,
- irr: props["IRR (%)"]?.number ?? 0,
- units: props["Units"]?.number ?? 0,
- dividends: props["Dividends (€)"]?.number ?? 0
- };
- trades[page.id] = trade;
- } catch (e) {
- formatErrors++;
- }
- }
- if (formatErrors === 0) {
- logging("", "success");
- logging(`${numberOfTrades} trades received and formatted`, "info");
- } else {
- logging("", "warning");
- logging(`${formatErrors} trades skipped out of ${numberOfTrades}`, "warning");
- }
- return trades;
- }
- /**
- * Similar to above, but fetches the Investment Overview database.
- */
- async function fetchFormatNotionInvestments(dbIdInvestments: string): Promise<InvestmentsDict> {
- const investments: InvestmentsDict = {};
- let formatErrors = 0;
- let numberOfInvestments = 0;
- const data = await notionGetPages(dbIdInvestments);
- for (const page of data) {
- numberOfInvestments++;
- try {
- const props = page.properties;
- investments[page.id] = {
- ticker: props["Ticker"]?.select?.name ?? "UNKNOWN",
- total_dividends: props["Dividends (€)"]?.number ?? 0,
- current_value: props["Current (€)"]?.number ?? 0,
- current_irr: props["IRR (%)"]?.number ?? 0,
- total_performanance: props["Performance (€)"]?.number ?? 0
- };
- } catch (e) {
- formatErrors++;
- }
- }
- if (formatErrors === 0) {
- logging("", "success");
- logging(`${numberOfInvestments} investments received`, "info");
- } else {
- logging("", "warning");
- logging(`${formatErrors} investments skipped`, "warning");
- }
- return investments;
- }
- /**
- * Fetches historical price data for all tickers from Yahoo Finance.
- */
- async function fetchFormatYfData(tickers: string[]): Promise<YFDataMap> {
- const yfData: YFDataMap = {};
- let fetchErrors = 0;
- let formatErrors = 0;
- let numberOfTickers = 0;
- // LEARNING TIP: Runtime Environment Check & Dynamic Instantiation
- // Sometimes Node imports 'yahooFinance' as a Class constructor, sometimes as an Instance object,
- // depending on the exact version and module settings (CommonJS vs ESM).
- // This code checks if it's a function (Class). If so, it creates a 'new' instance.
- const yf = typeof yahooFinance === 'function'
- ? new (yahooFinance as any)({ suppressNotices: ['ripHistorical'] })
- : yahooFinance;
- process.stdout.write(" ");
- for (const ticker of tickers) {
- numberOfTickers++;
- try {
- const queryOptions = {
- period1: '2000-01-01',
- period2: format(new Date(), 'yyyy-MM-dd')
- };
-
- // Await the API call
- const result = await yf.historical(ticker, queryOptions);
-
- // LEARNING TIP: Local Variable for Strict Typing
- // We build the data map in a local variable first.
- // Writing directly to yfData[ticker][date] would cause TypeScript errors
- // because yfData[ticker] might be undefined in the compiler's eyes.
- const tickerDataMap: { [date: string]: { close: number; dividends: number } } = {};
- // Explicitly typing 'row' as any to handle incoming data flexbility
- result.forEach((row: any) => {
- const dateStr = format(row.date, 'yyyy-MM-dd');
-
- tickerDataMap[dateStr] = {
- close: row.close,
- dividends: 0
- };
- });
- // Assign the completed map to the main object
- yfData[ticker] = tickerDataMap;
- } catch (e) {
- fetchErrors++;
- // Check if 'e' is an Error object to safely access .message
- console.log(`\n[Error fetching ${ticker}]: ${e instanceof Error ? e.message : String(e)}`);
- }
-
- process.stdout.write(".");
- // Pause to avoid hitting API Rate Limits
- await sleep(config.api_cooldown_time);
- }
- process.stdout.write(" ");
-
- if (fetchErrors === 0 && formatErrors === 0) {
- logging("", "success");
- logging(`${numberOfTickers} tickers received`, "info");
- } else {
- logging("", "warning");
- logging(`${fetchErrors} fetch errors`, "warning");
- }
- return yfData;
- }
- // ---------------------------------------------------------
- // 5. CALCULATION FUNCTIONS
- // ---------------------------------------------------------
- /**
- * Calculates daily performance history for every single trade.
- * Iterates day-by-day from the trade opening date to today.
- */
- function calcHistoryPerTrade(trades: TradesDict, yfData: YFDataMap): HistoryPerTrade {
- const historyPerTrade: HistoryPerTrade = {};
- let missingDayEntries = 0;
- let daysFormatted = 0;
- let numberOfTrades = 0;
- const dateOpenOldestTrade = getDateOpenOldestTrade(trades);
- // Loop over every trade
- for (const tradeId in trades) {
- numberOfTrades++;
-
- // Strict check: Ensure trade exists
- const trade = trades[tradeId];
- if (!trade) continue;
- // Start from the oldest trade date across the board
- let indexDate = startOfDay(new Date(dateOpenOldestTrade));
- const today = startOfDay(new Date());
- let previousCourse = 0.0;
-
- // Accumulator for dividends specific to this trade
- let runningTotalDividends = 0.0;
- // Determine closure date (or tomorrow if currently active)
- let dateCloseObj = (trade.date_close === 0) ? addDays(today, 1) : parseISO(trade.date_close as string);
- const dateOpenObj = parseISO(trade.date_open);
- // INNER LOOP: Iterate through every day from start to today
- while (isBefore(indexDate, addDays(today, 1))) {
- daysFormatted++;
- const indexDateIso = format(indexDate, 'yyyy-MM-dd');
-
- let currentCourse = 0.0;
- let currentDividendsPerTicker = 0.0;
- // 1. Get Price Data
- // We check if tickerData exists first to avoid undefined errors
- const tickerData = yfData[trade.ticker];
-
- if (tickerData && tickerData[indexDateIso]) {
- currentCourse = tickerData[indexDateIso].close;
- currentDividendsPerTicker = tickerData[indexDateIso].dividends;
- } else {
- // Fallback: Use previous day's price
- currentCourse = previousCourse;
-
- // FIX: Only count as "missing" if it is NOT a weekend
- if (!isWeekend(indexDate)) {
- missingDayEntries++;
- }
- }
- // 2. Handle Closed Trades (Lock price to the closing price)
- if (isEqual(indexDate, dateCloseObj)) {
- currentCourse = trade.course_close;
- }
- previousCourse = currentCourse;
- // 3. Calculate Performance Metrics
- let currentAmount = 0;
- let currentInvested = 0;
- let currentVal = 0;
- let currentIrr = 0;
- let totalPerf = 0;
- // Only calculate if the current date is within the trade's lifespan
- if ((isAfter(indexDate, dateOpenObj) || isEqual(indexDate, dateOpenObj)) &&
- (isBefore(indexDate, dateCloseObj) || isEqual(indexDate, dateCloseObj))) {
-
- currentAmount = trade.units;
- currentInvested = currentAmount * trade.course_open;
- runningTotalDividends += currentAmount * currentDividendsPerTicker;
- currentVal = currentAmount * currentCourse;
- const currentValWithDiv = currentVal + runningTotalDividends;
-
- currentIrr = calculateIrr(indexDate, dateOpenObj, currentValWithDiv, currentInvested);
- totalPerf = currentValWithDiv - currentInvested;
- }
- // 4. Store Results in History Map
- // Initialize day object if missing
- if (!historyPerTrade[indexDateIso]) historyPerTrade[indexDateIso] = {};
-
- historyPerTrade[indexDateIso][tradeId] = {
- current_amount: currentAmount,
- current_invested: currentInvested,
- total_dividends: runningTotalDividends,
- current_value: currentVal,
- current_irr: currentIrr,
- current_course: currentCourse,
- total_performanance: totalPerf
- };
- // Increment the date (Critical to avoid infinite loops)
- indexDate = addDays(indexDate, 1);
- }
- }
- if (missingDayEntries === 0) {
- logging("", "success");
- logging(`History created with ${daysFormatted} points`, "info");
- } else {
- logging("", "warning");
- logging(`Missing YF data in ${missingDayEntries} cases (using previous day)`, "warning");
- }
- return historyPerTrade;
- }
- /**
- * Aggregates trade history into a per-ticker summary.
- * Combines multiple trades of the same stock into one view.
- */
- function calcHistoryPerTicker(historyPerTrade: HistoryPerTrade, tickers: string[], trades: TradesDict): HistoryPerTicker {
- const historyPerTicker: HistoryPerTicker = {};
- let daysFormatted = 0;
- // Get all dates and sort them
- const dates = Object.keys(historyPerTrade).sort();
- for (const dateEntry of dates) {
- daysFormatted++;
-
- // Initialize aggregation object for this day
- const dictDaily: HistoryDailyTicker = {};
-
- // Initialize all tickers with 0 values
- tickers.forEach(t => {
- dictDaily[t] = {
- current_invested: 0,
- total_dividends: 0,
- current_value: 0,
- current_irr: 0,
- total_performanance: 0,
- current_amount: 0,
- current_course: 0
- };
- });
- // Initialize Portfolio Total
- dictDaily["total"] = {
- current_invested: 0,
- total_dividends: 0,
- current_value: 0,
- current_irr: 0,
- total_performanance: 0
- };
- const dailyTrades = historyPerTrade[dateEntry];
-
- // Loop through all trades active on this day
- for (const tradeId in dailyTrades) {
- // Safety checks
- const tData = dailyTrades[tradeId];
- if (!tData) continue;
-
- const trade = trades[tradeId];
- if (!trade) continue;
- const ticker = trade.ticker;
-
- if (!dictDaily[ticker]) continue;
- const tickD = dictDaily[ticker];
- // Safely default to 0 if undefined
- const currentAmount = tickD.current_amount ?? 0;
-
- // Accumulate (Sum) Values
- tickD.current_amount = currentAmount + tData.current_amount;
- tickD.current_invested += tData.current_invested;
- tickD.total_dividends += tData.total_dividends;
- tickD.current_value += tData.current_value;
- tickD.total_performanance += tData.total_performanance;
- tickD.current_course = tData.current_course;
- // Calculate Weighted Average IRR for this ticker
- if (tickD.current_invested === 0 && tData.current_invested === 0) {
- tickD.current_irr = 0;
- } else {
- const prevInvested = tickD.current_invested - tData.current_invested;
- const prevIrr = tickD.current_irr;
-
- if (tickD.current_invested !== 0) {
- tickD.current_irr = (prevIrr * prevInvested + tData.current_irr * tData.current_invested) / tickD.current_invested;
- }
- }
- }
- // Calculate Portfolio Totals (Summing up tickers)
- const total = dictDaily["total"];
- for (const ticker of tickers) {
- const tData = dictDaily[ticker];
- if (!tData) continue;
- total.total_dividends += tData.total_dividends;
- total.current_value += tData.current_value;
- total.total_performanance += tData.total_performanance;
- const prevTotalInvested = total.current_invested;
- total.current_invested += tData.current_invested;
- // Weighted Average IRR for Total Portfolio
- if (total.current_invested !== 0) {
- total.current_irr = (total.current_irr * prevTotalInvested + tData.current_irr * tData.current_invested) / total.current_invested;
- }
- }
- historyPerTicker[dateEntry] = dictDaily;
- }
- logging("", "success");
- logging(`History per ticker created`, "info");
- return historyPerTicker;
- }
- // ---------------------------------------------------------
- // 6. BENCHMARK FUNCTIONS
- // ---------------------------------------------------------
- /**
- * Creates 'Shadow Trades' to simulate what would have happened if
- * the money was invested in the benchmark ETF instead.
- */
- async function createBenchmarkTrades(trades: TradesDict, yfData: YFDataMap): Promise<TradesDict> {
- const benchmarkTrades: TradesDict = {};
- let i = 0;
- for (const tradeId in trades) {
- i++;
- const original = trades[tradeId];
- if (!original) continue;
- const benchId = "benchmark" + i;
- // LEARNING TIP: Spread Operator (...)
- // Copies all properties from 'original' to the new object.
- benchmarkTrades[benchId] = { ...original };
- // Overwrite the ticker
- benchmarkTrades[benchId].ticker = config.ticker_benchmark;
- const amountInvested = original.units * original.course_open;
- // Logic to find valid trading day in YF data for open/close
- let indexDate = parseISO(original.date_open);
- let courseOpenNew = 0;
- let found = false;
-
- const benchData = yfData[config.ticker_benchmark];
-
- // Search forward for valid data (e.g. if trade happened on Sunday, find Monday's price)
- while (!found) {
- const dStr = format(indexDate, 'yyyy-MM-dd');
- if (benchData && benchData[dStr]) {
- courseOpenNew = benchData[dStr].close;
- found = true;
- } else {
- indexDate = addDays(indexDate, 1);
- // Safety break >10 days
- if (differenceInDays(indexDate, parseISO(original.date_open)) > 10) found = true;
- }
- }
- // Avoid division by zero
- benchmarkTrades[benchId].course_open = courseOpenNew || 1;
- benchmarkTrades[benchId].units = amountInvested / (courseOpenNew || 1);
- // Handle Close Logic if trade is closed
- if (original.date_close !== 0) {
- let closeDate = parseISO(original.date_close as string);
- found = false;
- let courseCloseNew = 0;
- while (!found) {
- const dStr = format(closeDate, 'yyyy-MM-dd');
- if (benchData && benchData[dStr]) {
- courseCloseNew = benchData[dStr].close;
- found = true;
- } else {
- closeDate = addDays(closeDate, 1);
- if (differenceInDays(closeDate, parseISO(original.date_close as string)) > 10) found = true;
- }
- }
- benchmarkTrades[benchId].course_close = courseCloseNew;
- }
- }
- logging("", "success");
- return benchmarkTrades;
- }
- /**
- * Merges the benchmark performance data into the main ticker history object.
- */
- function mergeHistories(historyPerTicker: HistoryPerTicker, benchmarkHistory: HistoryPerTicker): HistoryPerTicker {
- const benchTicker = config.ticker_benchmark;
- let errorCount = 0;
- for (const date in historyPerTicker) {
- try {
- if (benchmarkHistory[date] && benchmarkHistory[date][benchTicker]) {
- // LEARNING TIP: Type Casting (as any)
- // We attach 'benchmark' to the object. TypeScript might complain if this
- // isn't strictly defined in the interface, so we use 'as any' to bypass the check.
- (historyPerTicker[date] as any)["benchmark"] = benchmarkHistory[date][benchTicker];
- }
- } catch {
- errorCount++;
- }
- }
- if (errorCount === 0) logging("", "success");
- else logging(`Merge error count: ${errorCount}`, "warning");
-
- return historyPerTicker;
- }
- // ---------------------------------------------------------
- // 7. UPLOAD & SELECT FUNCTIONS
- // ---------------------------------------------------------
- /**
- * Generic function to update a Notion Page.
- */
- async function notionUpdatePage(pageId: string, data: any) {
- const url = `https://api.notion.com/v1/pages/${pageId}`;
- await fetch(url, {
- method: 'PATCH',
- headers: notionHeaders as any,
- body: JSON.stringify({ properties: data })
- });
- }
- async function pushNotionTradesUpdate(trades: TradesDict) {
- let errorCount = 0;
- process.stdout.write(" ");
- for (const id in trades) {
- try {
- const trade = trades[id];
- if (!trade) continue;
- let irrNotion = trade.irr - 1;
- irrNotion = Math.round(irrNotion * 10000) / 10000;
- const update = {
- "Current (€)": { number: trade.course_current },
- "IRR (%)": { number: irrNotion },
- "Dividends (€)": { number: trade.dividends }
- };
- await notionUpdatePage(id, update);
- } catch {
- errorCount++;
- }
- process.stdout.write(".");
- await sleep(config.api_cooldown_time);
- }
- process.stdout.write(" ");
- if(errorCount === 0) logging("", "success");
- else logging(`${errorCount} upload errors`, "warning");
- }
- async function pushNotionInvestmentUpdate(investments: InvestmentsDict) {
- let errorCount = 0;
- process.stdout.write(" ");
- for (const id in investments) {
- try {
- const invest = investments[id];
- if (!invest) continue;
- let irrNotion = invest.current_irr - 1;
- irrNotion = Math.round(irrNotion * 10000) / 10000;
- const update = {
- "Current (€)": { number: invest.current_value },
- "IRR (%)": { number: irrNotion },
- "Performance (€)": { number: invest.total_performanance },
- "Dividends (€)": { number: invest.total_dividends }
- };
- await notionUpdatePage(id, update);
- } catch {
- errorCount++;
- }
- process.stdout.write(".");
- await sleep(config.api_cooldown_time);
- }
- process.stdout.write(" ");
- if(errorCount === 0) logging("", "success");
- else logging(`${errorCount} upload errors`, "warning");
- }
- /**
- * Updates the 'trades' object with the most current calculated values from history.
- */
- function selectCurrentValuePerTrade(trades: TradesDict, historyPerTrade: HistoryPerTrade): TradesDict {
- for (const id in trades) {
- try {
- const trade = trades[id];
- if (!trade) continue;
- let dateIso: string;
- // If open, use today. If closed, use close date.
- if (trade.date_close === 0) {
- dateIso = format(new Date(), 'yyyy-MM-dd');
- } else {
- dateIso = trade.date_close as string;
- }
- // Fallback if date not found
- if (!historyPerTrade[dateIso]) {
- dateIso = fetchLastKeyFromDict(historyPerTrade);
- }
- const h = historyPerTrade[dateIso]?.[id];
- if (h) {
- trade.course_current = h.current_course;
- trade.irr = h.current_irr;
- trade.dividends = h.total_dividends;
- }
- } catch(e) {}
- }
- logging("", "success");
- return trades;
- }
- function selectCurrentValuePerTicker(investments: InvestmentsDict, historyPerTicker: HistoryPerTicker): InvestmentsDict {
- const todayIso = format(new Date(), 'yyyy-MM-dd');
-
- // Determine which date key to use
- const lastKey = Object.keys(historyPerTicker).length > 0
- ? fetchLastKeyFromDict(historyPerTicker)
- : todayIso;
- const useDate = historyPerTicker[todayIso] ? todayIso : lastKey;
- for (const id in investments) {
- try {
- const invest = investments[id];
- if (!invest) continue;
- const ticker = invest.ticker;
- if(historyPerTicker[useDate] && historyPerTicker[useDate][ticker]) {
- const h = historyPerTicker[useDate][ticker];
- invest.total_dividends = h.total_dividends;
- invest.current_value = h.current_value;
- invest.current_irr = h.current_irr;
- invest.total_performanance = h.total_performanance;
- }
- } catch(e) {}
- }
- logging("", "success");
- return investments;
- }
- /**
- * Filters the detailed daily history down to specific dates (e.g. Weekly)
- * and handles data aggregation/averaging.
- */
- function filterHistoryByList(history: HistoryPerTicker, datesList: string[]): HistoryPerTicker {
- const filtered: HistoryPerTicker = {};
-
- // Sort input arrays to ensure processing happens chronologically
- const sortedTargetDates = [...datesList].sort();
- const allHistoryDates = Object.keys(history).sort();
-
- let lastHistoryIdx = 0;
- for (let i = 0; i < sortedTargetDates.length; i++) {
- const targetDate = sortedTargetDates[i];
-
- // FIX 1: Strict Check - Ensure targetDate is defined before using it as an index
- if (!targetDate) continue;
- // CHECK: Is this the last date in our list (usually Today)?
- const isLastDate = i === sortedTargetDates.length - 1;
- if (isLastDate) {
- // --- LOGIC FOR TODAY (Last Entry) ---
- // We do not want to average today's data with last week's. We want the exact latest values.
-
- // Try to find exact match
- let dataToUse = history[targetDate];
- // If not found, use the very last entry in the history (fallback)
- if (!dataToUse && allHistoryDates.length > 0) {
- const lastAvailableDate = allHistoryDates[allHistoryDates.length - 1];
- if (lastAvailableDate) {
- dataToUse = history[lastAvailableDate];
- }
- }
- if (dataToUse) {
- // Deep clone the object using JSON parse/stringify to prevent reference issues
- filtered[targetDate] = JSON.parse(JSON.stringify(dataToUse));
- }
-
- } else {
- // --- LOGIC FOR PAST DATES (Interval Averaging) ---
-
- let currentHistoryIdx = -1;
-
- // Find the index of the target date in the full history array
- for (let j = lastHistoryIdx; j < allHistoryDates.length; j++) {
- const hDate = allHistoryDates[j];
- if (hDate && hDate <= targetDate) {
- currentHistoryIdx = j;
- } else {
- break; // Optimization: stop searching once we pass the date
- }
- }
- if (currentHistoryIdx !== -1 && currentHistoryIdx >= lastHistoryIdx) {
-
- // Get all history days between the last target and this target
- const intervalDates = allHistoryDates.slice(lastHistoryIdx, currentHistoryIdx + 1);
-
- if (intervalDates.length > 0) {
-
- // Initialize Aggregation Accumulator
- const aggregation: Record<string, {
- wSumInvested: number; wSumValue: number; wSumPerf: number; wSumDiv: number;
- totalWeight: number;
-
- wSumIrr: number;
- irrTotalWeight: number;
-
- bSumInvested: number; bSumValue: number; bSumPerf: number; bSumDiv: number;
- bTotalWeight: number;
-
- bSumIrr: number;
- bIrrTotalWeight: number;
- }> = {};
- // Helper to create or retrieve aggregator for a ticker
- const getAgg = (t: string) => {
- if (!aggregation[t]) aggregation[t] = {
- wSumInvested: 0, wSumValue: 0, wSumPerf: 0, wSumDiv: 0, totalWeight: 0,
- wSumIrr: 0, irrTotalWeight: 0,
-
- bSumInvested: 0, bSumValue: 0, bSumPerf: 0, bSumDiv: 0, bTotalWeight: 0,
- bSumIrr: 0, bIrrTotalWeight: 0
- };
- return aggregation[t];
- };
- // Loop through every day in this interval
- for (const date of intervalDates) {
- if (!date) continue; // Safety check
- const dayData = history[date];
- if (!dayData) continue;
- for (const ticker in dayData) {
- const entry = dayData[ticker];
-
- // FIX 2: Strict Check - Ensure entry exists
- if (!entry) continue;
- const agg = getAgg(ticker);
-
- // Weight: If invested amount < 1, treat as 0 (ignore dust)
- const w = entry.current_invested > 1 ? entry.current_invested : 0;
-
- // Outlier Detection: If IRR > 500% or < -500%, ignore it
- const isOutlier = Math.abs(entry.current_irr) > 5.0;
- if (w > 0) {
- // Add weighted values
- agg.wSumInvested += entry.current_invested * w;
- agg.wSumValue += entry.current_value * w;
- agg.wSumPerf += entry.total_performanance * w;
- agg.wSumDiv += entry.total_dividends * w;
- agg.totalWeight += w;
- // Only add IRR if sane
- if (!isOutlier) {
- agg.wSumIrr += entry.current_irr * w;
- agg.irrTotalWeight += w;
- }
- }
- // Handle Benchmark Aggregation (Same logic)
- if (entry.benchmark) {
- const b = entry.benchmark;
- const bw = b.current_invested > 1 ? b.current_invested : 0;
- const isBenchOutlier = Math.abs(b.current_irr) > 5.0;
- if (bw > 0) {
- agg.bSumInvested += b.current_invested * bw;
- agg.bSumValue += b.current_value * bw;
- agg.bSumPerf += b.total_performanance * bw;
- agg.bSumDiv += b.total_dividends * bw;
- agg.bTotalWeight += bw;
- if (!isBenchOutlier) {
- agg.bSumIrr += b.current_irr * bw;
- agg.bIrrTotalWeight += bw;
- }
- }
- }
- }
- }
- // Calculate Final Weighted Averages for the interval
- const resultDaily: HistoryDailyTicker = {};
-
- for (const ticker in aggregation) {
- const agg = aggregation[ticker];
-
- // FIX 3: Strict Check - Ensure agg exists
- if (!agg) continue;
- const div = (n: number, d: number) => d === 0 ? 0 : n / d;
- const entry: TickerHistoryEntry = {
- current_invested: div(agg.wSumInvested, agg.totalWeight),
- current_value: div(agg.wSumValue, agg.totalWeight),
- current_irr: div(agg.wSumIrr, agg.irrTotalWeight),
- total_performanance: div(agg.wSumPerf, agg.totalWeight),
- total_dividends: div(agg.wSumDiv, agg.totalWeight),
- current_amount: 0,
- current_course: 0
- };
- if (agg.bTotalWeight > 0) {
- entry.benchmark = {
- current_invested: div(agg.bSumInvested, agg.bTotalWeight),
- current_value: div(agg.bSumValue, agg.bTotalWeight),
- current_irr: div(agg.bSumIrr, agg.bIrrTotalWeight),
- total_performanance: div(agg.bSumPerf, agg.bTotalWeight),
- total_dividends: div(agg.bSumDiv, agg.bTotalWeight),
- };
- }
- resultDaily[ticker] = entry;
- }
- filtered[targetDate] = resultDaily;
- }
-
- // Update start index for next iteration
- lastHistoryIdx = currentHistoryIdx + 1;
- }
- }
- }
- logging("", "success");
- return filtered;
- }
- /**
- * Formats the data for the TRMNL dashboard API.
- */
- function prepTrmnlChartUpdate(history: HistoryPerTicker, s1: string = "total", d1: string = "current_value", s2: string = "benchmark", d2: string = "current_value") {
- try {
- const lastKey = fetchLastKeyFromDict(history);
- const lastEntry = history[lastKey];
- if (!lastEntry || !lastEntry["total"]) {
- throw new Error("Missing 'total' data for TRMNL update");
- }
- // Prepare Big Numbers (Top of dashboard)
- const dictBigNumbers = {
- current_value: Math.round(lastEntry["total"].current_value).toString(),
- total_performanance: Math.round(lastEntry["total"].total_performanance).toString(),
- current_irr: String(Math.round((lastEntry["total"].current_irr - 1) * 100 * 100) / 100)
- };
- const chart1Data: any[] = [];
- const chart2Data: any[] = [];
- const sortedDates = Object.keys(history).sort();
- // Convert history map to Chart Data Array
- for (const date of sortedDates) {
- const getVal = (series: string, field: string) => {
- let val = 0;
- const dailyData = history[date];
-
- // Strict check: Data must exist
- if (!dailyData) return 0;
- if (series === 'benchmark') {
- // Safe access using Optional Chaining and Nullish Coalescing
- val = (dailyData as any)['benchmark']?.[field] ?? 0;
- } else {
- val = (dailyData as any)[series]?.[field] ?? 0;
- }
-
- if (field === 'current_irr') val = (val - 1) * 100;
- return Math.round(val * 100) / 100;
- };
- const val1 = getVal(s1, d1);
- const val2 = getVal(s2, d2);
-
- // Format: ISO date + Time (required by TRMNL)
- const jsonDate = date + "T00:00:00";
- chart1Data.push([jsonDate, val1]);
- chart2Data.push([jsonDate, val2]);
- }
- // Construct Titles
- let t1 = s1 === "total" ? "Portfolio" : s1;
- if (s1 === "benchmark") t1 = `Benchmark: ${config.ticker_benchmark}`;
- let t2 = s2 === "total" ? "Portfolio" : s2;
- if (s2 === "benchmark") t2 = `Benchmark: ${config.ticker_benchmark}`;
- const cleanName = (n: string) => n.replace("_", " ").replace(/\b\w/g, l => l.toUpperCase()).replace("Irr", "IRR");
- return {
- merge_variables: {
- big_numbers: dictBigNumbers,
- charts: [
- { name: `${cleanName(d1)} ${t1}`, data: chart1Data },
- { name: `${cleanName(d2)} ${t2}`, data: chart2Data }
- ]
- }
- };
- } catch (e) {
- logging(`${e}`, "error");
- return false;
- }
- }
- /**
- * Sends the formatted data to the TRMNL API.
- */
- async function pushTrmnlUpdateChart(data: any, url: string) {
- try {
- const res = await fetch(url, {
- method: 'POST',
- headers: trmnlHeaders,
- body: JSON.stringify(data)
- });
- if (res.status === 200) logging("", "success");
- else if (res.status === 429) logging("Rate limit exceeded", "warning");
- else if (res.status === 422) logging("Payload too large/invalid", "warning");
- else logging(`TRMNL Error ${res.status}`, "error");
- } catch (e) {
- logging(`${e}`, "error");
- }
- }
- // ---------------------------------------------------------
- // 8. MAIN EXECUTION LOOP
- // ---------------------------------------------------------
- /**
- * Main entry point.
- * LEARNING TIP: Async Main
- * Top-level code cannot normally use 'await' (in older Node versions).
- * Wrapping logic in an async function allows us to use await for all our API calls.
- */
- async function main() {
- console.log("Starting TS-Ported Financial Service...");
- // Infinite Loop to keep service running
- while (true) {
- try {
- // --- PART 1: NOTION TRADES ---
- process.stdout.write("Fetching Data from Notion... ");
- let trades = await fetchFormatNotionTrades(config.notion_db_id_trades);
- process.stdout.write("Creating unique tickers... ");
- let tickers = filterListOfTickers(trades);
- if (config.calculate_benchmark) {
- process.stdout.write("Adding benchmark... ");
- tickers = addBenchmarkTicker(tickers, config.ticker_benchmark);
- }
- process.stdout.write("Fetching YFinance data");
- const yfData = await fetchFormatYfData(tickers);
- process.stdout.write("Calculating history per trade... ");
- const historyPerTrade = calcHistoryPerTrade(trades, yfData);
- if (config.update_notion) {
- process.stdout.write("Selecting current values... ");
- trades = selectCurrentValuePerTrade(trades, historyPerTrade);
-
- process.stdout.write("Updating Notion Trades");
- await pushNotionTradesUpdate(trades);
- }
- // --- PART 2: NOTION INVESTMENTS ---
- process.stdout.write("Fetching Notion Investments... ");
- let investments = await fetchFormatNotionInvestments(config.notion_db_id_investments);
- process.stdout.write("Calculating history per ticker... ");
- let historyPerTicker = calcHistoryPerTicker(historyPerTrade, tickers, trades);
- if (config.update_notion) {
- process.stdout.write("Calculating current ticker values... ");
- investments = selectCurrentValuePerTicker(investments, historyPerTicker);
- process.stdout.write("Updating Notion Investments");
- await pushNotionInvestmentUpdate(investments);
- }
- // --- PART 3: BENCHMARK ---
- if (config.calculate_benchmark) {
- process.stdout.write("Creating benchmark trades... ");
- const benchmarkTrades = await createBenchmarkTrades(trades, yfData);
- process.stdout.write("Calculating benchmark history per trade... ");
- const historyBenchTrade = calcHistoryPerTrade(benchmarkTrades, yfData);
- process.stdout.write("Calculating benchmark history overall... ");
- const historyBench = calcHistoryPerTicker(historyBenchTrade, tickers, benchmarkTrades);
- process.stdout.write("Merging histories... ");
- historyPerTicker = mergeHistories(historyPerTicker, historyBench);
- }
- // --- PART 4: TRMNL ---
- if (config.update_TRMNL) {
- process.stdout.write("Creating weekly dates... ");
- const datesList = createListFilteredDates(trades, config.trmnl_granularity);
- process.stdout.write("Filtering history... ");
- const historyFiltered = filterHistoryByList(historyPerTicker, datesList);
- // Chart 1: Value
- process.stdout.write("Pushing Chart 1 (Value)... ");
- const data1 = prepTrmnlChartUpdate(historyFiltered, "total", "current_value", "benchmark", "current_value");
- if(data1) await pushTrmnlUpdateChart(data1, config.trmnl_url_chart_1);
- // Chart 2: IRR
- process.stdout.write("Pushing Chart 2 (IRR)... ");
- const data2 = prepTrmnlChartUpdate(historyFiltered, "total", "current_irr", "benchmark", "current_irr");
- if(data2) await pushTrmnlUpdateChart(data2, config.trmnl_url_chart_2);
- // Chart 3: Performance
- process.stdout.write("Pushing Chart 3 (Perf)... ");
- const data3 = prepTrmnlChartUpdate(historyFiltered, "total", "total_performanance", "benchmark", "total_performanance");
- if(data3) await pushTrmnlUpdateChart(data3, config.trmnl_url_chart_3);
- }
- // --- PART 5: COOLDOWN ---
- // Clear vars for Garbage Collection
- trades = {};
- investments = {};
-
- console.log(`Completed cycle at: ${new Date().toISOString()}`);
- console.log("------------------------------------------------");
-
- // Wait before next execution
- await sleep(config.programm_cooldown_time * 60 * 1000);
- } catch (e) {
- console.error("CRITICAL MAIN LOOP ERROR:", e);
- console.log("Retrying in 1 minute...");
- await sleep(60000);
- }
- }
- }
- // Start the application
- main();
|