/*************************************************************************** mymoneyforecast.cpp ------------------- begin : Wed May 30 2007 copyright : (C) 2007 by Alvaro Soliverez email : asoliverez@gmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifdef HAVE_CONFIG_H #include #endif // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyforecast.h" #include "../kmymoneyglobalsettings.h" #include "mymoneyfile.h" #include "mymoneytransactionfilter.h" #include "mymoneyfinancialcalculator.h" MyMoneyForecast::MyMoneyForecast() : m_forecastMethod(0), m_historyMethod(0), m_skipOpeningDate(true), m_includeUnusedAccounts(false), m_forecastDone(false) { setForecastCycles(KMyMoneyGlobalSettings::forecastCycles()); setAccountsCycle(KMyMoneyGlobalSettings::forecastAccountCycle()); setHistoryStartDate(TQDate::tqcurrentDate().addDays(-forecastCycles()*accountsCycle())); setHistoryEndDate(TQDate::tqcurrentDate().addDays(-1)); setForecastDays(KMyMoneyGlobalSettings::forecastDays()); setBeginForecastDay(KMyMoneyGlobalSettings::beginForecastDay()); setForecastMethod(KMyMoneyGlobalSettings::forecastMethod()); setHistoryMethod(KMyMoneyGlobalSettings::historyMethod()); setIncludeFutureTransactions(KMyMoneyGlobalSettings::includeFutureTransactions()); setIncludeScheduledTransactions(KMyMoneyGlobalSettings::includeScheduledTransactions()); } void MyMoneyForecast::doForecast() { int fDays = calculateBeginForecastDay(); int fMethod = forecastMethod(); int fAccCycle = accountsCycle(); int fCycles = forecastCycles(); //validate settings if(fAccCycle < 1 || fCycles < 1 || fDays < 1) { throw new MYMONEYEXCEPTION("Illegal settings when calling doForecast. Settings must be higher than 0"); } //initialize global variables setForecastDays(fDays); setForecastStartDate(TQDate::tqcurrentDate().addDays(1)); setForecastEndDate(TQDate::tqcurrentDate().addDays(fDays)); setAccountsCycle(fAccCycle); setForecastCycles(fCycles); setHistoryStartDate(forecastCycles() * accountsCycle()); setHistoryEndDate(TQDate::tqcurrentDate().addDays(-1)); //yesterday //clear all data before calculating m_accountListPast.clear(); m_accountList.clear(); m_accountTrendList.clear(); //set forecast accounts setForecastAccountList(); switch(fMethod) { case eScheduled: doFutureScheduledForecast(); calculateScheduledDailyBalances(); break; case eHistoric: pastTransactions(); calculateHistoricDailyBalances(); break; default: break; } //flag the forecast as done m_forecastDone = true; } MyMoneyForecast::~MyMoneyForecast() { } void MyMoneyForecast::pastTransactions() { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyTransactionFilter filter; filter.setDateFilter(historyStartDate(), historyEndDate()); filter.setReportAllSplits(false); TQValueList transactions = file->transactionList(filter); TQValueList::const_iterator it_t = transactions.begin(); //Check past transactions for(; it_t != transactions.end(); ++it_t ) { const TQValueList& splits = (*it_t).splits(); TQValueList::const_iterator it_s = splits.begin(); for(; it_s != splits.end(); ++it_s ) { if(!(*it_s).shares().isZero()) { MyMoneyAccount acc = file->account((*it_s).accountId()); //workaround for stock accounts which have faulty opening dates TQDate openingDate; if(acc.accountType() == MyMoneyAccount::Stock) { MyMoneyAccount tqparentAccount = file->account(acc.tqparentAccountId()); openingDate = tqparentAccount.openingDate(); } else { openingDate = acc.openingDate(); } if(isForecastAccount(acc) //If it is one of the accounts we are checking, add the amount of the transaction && ( (openingDate < (*it_t).postDate() && skipOpeningDate()) || !skipOpeningDate() ) ){ //don't take the opening day of the account to calculate balance dailyBalances balance; //FIXME deal with leap years balance = m_accountListPast[acc.id()]; if(acc.accountType() == MyMoneyAccount::Income) {//if it is income, the balance is stored as negative number balance[(*it_t).postDate()] += ((*it_s).shares() * MyMoneyMoney(-1, 1)); } else { balance[(*it_t).postDate()] += (*it_s).shares(); } // check if this is a new account for us m_accountListPast[acc.id()] = balance; } } } } //purge those accounts with no transactions on the period if(isIncludingUnusedAccounts() == false) purgeForecastAccountsList(m_accountListPast); //calculate running sum TQMap::Iterator it_n; for(it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { MyMoneyAccount acc = file->account(*it_n); m_accountListPast[acc.id()][historyStartDate().addDays(-1)] = file->balance(acc.id(), historyStartDate().addDays(-1)); for(TQDate it_date = historyStartDate(); it_date <= historyEndDate(); ) { m_accountListPast[acc.id()][it_date] += m_accountListPast[acc.id()][it_date.addDays(-1)]; //Running sum it_date = it_date.addDays(1); } } //adjust value of investments to deep currency for ( it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n ) { MyMoneyAccount acc = file->account ( *it_n ); if ( acc.isInvest() ) { //get the id of the security for that account MyMoneySecurity undersecurity = file->security ( acc.currencyId() ); if ( ! undersecurity.isCurrency() ) //only do it if the security is not an actual currency { MyMoneyMoney rate = MyMoneyMoney ( 1, 1 ); //set the default value MyMoneyPrice price; for ( TQDate it_date = historyStartDate().addDays(-1) ; it_date <= historyEndDate();) { //get the price for the tradingCurrency that day price = file->price ( undersecurity.id(), undersecurity.tradingCurrency(), it_date ); if ( price.isValid() ) { rate = price.rate ( undersecurity.tradingCurrency() ); } //value is the amount of shares multiplied by the rate of the deep currency m_accountListPast[acc.id() ][it_date] = m_accountListPast[acc.id() ][it_date] * rate; it_date = it_date.addDays(1); } } } } } bool MyMoneyForecast::isForecastAccount(const MyMoneyAccount& acc) { if(m_nameIdx.isEmpty()) { setForecastAccountList(); } TQMap::Iterator it_n; for(it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { if(*it_n == acc.id()) { return true; } } return false; } void MyMoneyForecast::calculateAccountTrendList() { MyMoneyFile* file = MyMoneyFile::instance(); int auxForecastTerms; int totalWeight = 0; //Calculate account trends TQMap::Iterator it_n; for(it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { MyMoneyAccount acc = file->account(*it_n); m_accountTrendList[acc.id()][0] = MyMoneyMoney(0,1); // for today, the trend is 0 auxForecastTerms = forecastCycles(); if(skipOpeningDate()) { TQDate openingDate; if(acc.accountType() == MyMoneyAccount::Stock) { MyMoneyAccount tqparentAccount = file->account(acc.tqparentAccountId()); openingDate = tqparentAccount.openingDate(); } else { openingDate = acc.openingDate(); } if(openingDate > historyStartDate() ) { //if acc opened after forecast period auxForecastTerms = 1 + ((openingDate.daysTo(historyEndDate()) + 1)/ accountsCycle()); // set forecastTerms to a lower value, to calculate only based on how long this account was opened } } switch (historyMethod()) { //moving average case 0: { for(int t_day = 1; t_day <= accountsCycle(); t_day++) m_accountTrendList[acc.id()][t_day] = accountMovingAverage(acc, t_day, auxForecastTerms); //moving average break; } //weighted moving average case 1: { //calculate total weight for moving average if(auxForecastTerms == forecastCycles()) { totalWeight = (auxForecastTerms * (auxForecastTerms + 1))/2; //totalWeight is the triangular number of auxForecastTerms } else { //if only taking a few periods, totalWeight is the sum of the weight for most recent periods for(int i = 1, w = forecastCycles(); i <= auxForecastTerms; ++i, --w) totalWeight += w; } for(int t_day = 1; t_day <= accountsCycle(); t_day++) m_accountTrendList[acc.id()][t_day] = accountWeightedMovingAverage(acc, t_day, totalWeight); break; } case 2: { //calculate mean term MyMoneyMoney meanTerms = MyMoneyMoney((auxForecastTerms * (auxForecastTerms + 1))/2, 1) / MyMoneyMoney(auxForecastTerms, 1); for(int t_day = 1; t_day <= accountsCycle(); t_day++) m_accountTrendList[acc.id()][t_day] = accountLinearRegression(acc, t_day, auxForecastTerms, meanTerms); break; } default: break; } } } TQValueList MyMoneyForecast::forecastAccountList(void) { MyMoneyFile* file = MyMoneyFile::instance(); TQValueList accList; //Get all accounts from the file and check if they are of the right type to calculate forecast file->accountList(accList); TQValueList::iterator accList_t = accList.begin(); for(; accList_t != accList.end(); ) { MyMoneyAccount acc = *accList_t; if(acc.isClosed() //check the account is not closed || (!acc.isAssetLiability()) ) { //|| (acc.accountType() == MyMoneyAccount::Investment) ) {//check that it is not an Investment account and only include Stock accounts accList.remove(accList_t); //remove the account if it is not of the correct type accList_t = accList.begin(); } else { ++accList_t; } } return accList; } TQValueList MyMoneyForecast::accountList(void) { MyMoneyFile* file = MyMoneyFile::instance(); TQValueList accList; TQStringList emptyStringList; //Get all accounts from the file and check if they are present file->accountList(accList, emptyStringList, false); TQValueList::iterator accList_t = accList.begin(); for(; accList_t != accList.end(); ) { MyMoneyAccount acc = *accList_t; if(!isForecastAccount( acc ) ) { accList.remove(accList_t); //remove the account accList_t = accList.begin(); } else { ++accList_t; } } return accList; } MyMoneyMoney MyMoneyForecast::calculateAccountTrend(const MyMoneyAccount& acc, int trendDays) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyTransactionFilter filter; MyMoneyMoney netIncome; TQDate startDate; TQDate openingDate = acc.openingDate(); //validate arguments if(trendDays < 1) { throw new MYMONEYEXCEPTION("Illegal arguments when calling calculateAccountTrend. trendDays must be higher than 0"); } //If it is a new account, we don't take into account the first day //because it is usually a weird one and it would mess up the trend if(openingDate.daysTo(TQDate::tqcurrentDate()) transactions = file->transactionList(filter); TQValueList::const_iterator it_t = transactions.begin(); //add all transactions for that account for(; it_t != transactions.end(); ++it_t ) { const TQValueList& splits = (*it_t).splits(); TQValueList::const_iterator it_s = splits.begin(); for(; it_s != splits.end(); ++it_s ) { if(!(*it_s).shares().isZero()) { if(acc.id()==(*it_s).accountId()) netIncome += (*it_s).value(); } } } //calculate trend of the account in the past period MyMoneyMoney accTrend; //don't take into account the first day of the account if(openingDate.daysTo(TQDate::tqcurrentDate())::ConstIterator it_n; for(it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { MyMoneyAccount acc = file->account(*it_n); //set the starting balance of the account setStartingBalance(acc); switch(historyMethod()) { case 0: case 1: { for(TQDate f_day = forecastStartDate(); f_day <= forecastEndDate(); ) { for(int t_day = 1; t_day <= accountsCycle(); ++t_day) { MyMoneyMoney balanceDayBefore = m_accountList[acc.id()][(f_day.addDays(-1))];//balance of the day before MyMoneyMoney accountDailyTrend = m_accountTrendList[acc.id()][t_day]; //trend for that day //balance of the day is the balance of the day before multiplied by the trend for the day m_accountList[acc.id()][f_day] = balanceDayBefore; m_accountList[acc.id()][f_day] += accountDailyTrend; //movement trend for that particular day m_accountList[acc.id()][f_day] = m_accountList[acc.id()][f_day].convert(acc.fraction()); //m_accountList[acc.id()][f_day] += m_accountListPast[acc.id()][f_day.addDays(-historyDays())]; f_day = f_day.addDays(1); } } } break; case 2: { TQDate baseDate = TQDate::tqcurrentDate().addDays(-accountsCycle()); for(int t_day = 1; t_day <= accountsCycle(); ++t_day) { int f_day = 1; TQDate fDate = baseDate.addDays(accountsCycle()+1); while (fDate <= forecastEndDate()) { //the calculation is based on the balance for the last month, that is then multiplied by the trend m_accountList[acc.id()][fDate] = m_accountListPast[acc.id()][baseDate] + (m_accountTrendList[acc.id()][t_day] * MyMoneyMoney(f_day,1)); m_accountList[acc.id()][fDate] = m_accountList[acc.id()][fDate].convert(acc.fraction()); ++f_day; fDate = baseDate.addDays(accountsCycle() * f_day); } baseDate = baseDate.addDays(1); } } } } } MyMoneyMoney MyMoneyForecast::forecastBalance(const MyMoneyAccount& acc, TQDate forecastDate) { dailyBalances balance; MyMoneyMoney MM_amount = MyMoneyMoney(0,1); //Check if acc is not a forecast account, return 0 if ( !isForecastAccount ( acc ) ) { return MM_amount; } balance = m_accountList[acc.id() ]; if ( balance.tqcontains ( forecastDate ) ) { //if the date is not in the forecast, it returns 0 MM_amount = m_accountList[acc.id() ][forecastDate]; } return MM_amount; } /** * Returns the forecast balance trend for account @a acc for offset @p int * offset is days from current date, inside forecast days. * Returns 0 if offset not in range of forecast days. */ MyMoneyMoney MyMoneyForecast::forecastBalance (const MyMoneyAccount& acc, int offset ) { TQDate forecastDate = TQDate::tqcurrentDate().addDays(offset); return forecastBalance(acc, forecastDate); } void MyMoneyForecast::doFutureScheduledForecast(void) { MyMoneyFile* file = MyMoneyFile::instance(); if(isIncludingFutureTransactions()) addFutureTransactions(); if(isIncludingScheduledTransactions()) addScheduledTransactions(); //do not show accounts with no transactions if(!isIncludingUnusedAccounts()) purgeForecastAccountsList(m_accountList); //adjust value of investments to deep currency TQMap::ConstIterator it_n; for ( it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n ) { MyMoneyAccount acc = file->account ( *it_n ); if ( acc.isInvest() ) { //get the id of the security for that account MyMoneySecurity undersecurity = file->security ( acc.currencyId() ); //only do it if the security is not an actual currency if ( ! undersecurity.isCurrency() ) { //set the default value MyMoneyMoney rate = MyMoneyMoney ( 1, 1 ); MyMoneyPrice price; for (TQDate it_day = TQDate::tqcurrentDate(); it_day <= forecastEndDate(); ) { //get the price for the tradingCurrency that day price = file->price ( undersecurity.id(), undersecurity.tradingCurrency(), it_day ); if ( price.isValid() ) { rate = price.rate ( undersecurity.tradingCurrency() ); } //value is the amount of shares multiplied by the rate of the deep currency m_accountList[acc.id() ][it_day] = m_accountList[acc.id() ][it_day] * rate; it_day = it_day.addDays(1); } } } } } void MyMoneyForecast::addFutureTransactions(void) { MyMoneyTransactionFilter filter; MyMoneyFile* file = MyMoneyFile::instance(); // collect and process all transactions that have already been entered but // are located in the future. filter.setDateFilter(forecastStartDate(), forecastEndDate()); filter.setReportAllSplits(false); TQValueList transactions = file->transactionList(filter); TQValueList::const_iterator it_t = transactions.begin(); for(; it_t != transactions.end(); ++it_t ) { const TQValueList& splits = (*it_t).splits(); TQValueList::const_iterator it_s = splits.begin(); for(; it_s != splits.end(); ++it_s ) { if(!(*it_s).shares().isZero()) { MyMoneyAccount acc = file->account((*it_s).accountId()); if(isForecastAccount(acc)) { dailyBalances balance; balance = m_accountList[acc.id()]; //if it is income, the balance is stored as negative number if(acc.accountType() == MyMoneyAccount::Income) { balance[(*it_t).postDate()] += ((*it_s).shares() * MyMoneyMoney(-1, 1)); } else { balance[(*it_t).postDate()] += (*it_s).shares(); } m_accountList[acc.id()] = balance; } } } } #if 0 TQFile trcFile("forecast.csv"); trcFile.open(IO_WriteOnly); TQTextStream s(&trcFile); { s << "Already present transactions\n"; TQMap::Iterator it_a; TQMap::ConstIterator it_n; for(it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { MyMoneyAccount acc = file->account(*it_n); it_a = m_accountList.tqfind(*it_n); s << "\"" << acc.name() << "\","; for(int i = 0; i < 90; ++i) { s << "\"" << (*it_a)[i].formatMoney("") << "\","; } s << "\n"; } } #endif } void MyMoneyForecast::addScheduledTransactions (void) { MyMoneyFile* file = MyMoneyFile::instance(); // now process all the schedules that may have an impact TQValueList schedule; schedule = file->scheduleList("", MyMoneySchedule::TYPE_ANY, MyMoneySchedule::OCCUR_ANY, MyMoneySchedule::STYPE_ANY, TQDate::tqcurrentDate(), forecastEndDate()); if(schedule.count() > 0) { TQValueList::Iterator it; do { qBubbleSort(schedule); it = schedule.begin(); if(it == schedule.end()) break; if((*it).isFinished()) { schedule.erase(it); continue; } TQDate date = (*it).nextPayment((*it).lastPayment()); if(!date.isValid()) { schedule.remove(it); continue; } TQDate nextDate = (*it).adjustedNextPayment((*it).lastPayment()); if (nextDate > forecastEndDate()) { // We're done with this schedule, let's move on to the next schedule.remove(it); continue; } // found the next schedule. process it MyMoneyAccount acc = (*it).account(); if(!acc.id().isEmpty()) { try { if(acc.accountType() != MyMoneyAccount::Investment) { MyMoneyTransaction t = (*it).transaction(); // only process the entry, if it is still active if(!(*it).isFinished() && nextDate != TQDate()) { // make sure we have all 'starting balances' so that the autocalc works TQValueList::const_iterator it_s; TQMap balanceMap; for(it_s = t.splits().begin(); it_s != t.splits().end(); ++it_s ) { MyMoneyAccount acc = file->account((*it_s).accountId()); if(isForecastAccount(acc)) { // collect all overdues on the first day TQDate forecastDate = nextDate; if(TQDate::tqcurrentDate() >= nextDate) forecastDate = TQDate::tqcurrentDate().addDays(1); dailyBalances balance; balance = m_accountList[acc.id()]; for(TQDate f_day = TQDate::tqcurrentDate(); f_day < forecastDate; ) { balanceMap[acc.id()] += m_accountList[acc.id()][f_day]; f_day = f_day.addDays(1); } } } // take care of the autoCalc stuff calculateAutoLoan(*it, t, balanceMap); // now add the splits to the balances for(it_s = t.splits().begin(); it_s != t.splits().end(); ++it_s ) { MyMoneyAccount acc = file->account((*it_s).accountId()); if(isForecastAccount(acc)) { dailyBalances balance; balance = m_accountList[acc.id()]; //int offset = TQDate::tqcurrentDate().daysTo(nextDate); //if(offset <= 0) { // collect all overdues on the first day // offset = 1; //} // collect all overdues on the first day TQDate forecastDate = nextDate; if(TQDate::tqcurrentDate() >= nextDate) forecastDate = TQDate::tqcurrentDate().addDays(1); if(acc.accountType() == MyMoneyAccount::Income) { balance[forecastDate] += ((*it_s).shares() * MyMoneyMoney(-1, 1)); } else { balance[forecastDate] += (*it_s).shares(); } m_accountList[acc.id()] = balance; } } } } (*it).setLastPayment(date); } catch(MyMoneyException* e) { kdDebug(2) << __func__ << " Schedule " << (*it).id() << " (" << (*it).name() << "): " << e->what() << endl; schedule.remove(it); delete e; } } else { // remove schedule from list schedule.remove(it); } } while(1); } #if 0 { s << "\n\nAdded scheduled transactions\n"; TQMap::Iterator it_a; TQMap::ConstIterator it_n; for(it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { MyMoneyAccount acc = file->account(*it_n); it_a = m_accountList.tqfind(*it_n); s << "\"" << acc.name() << "\","; for(int i = 0; i < 90; ++i) { s << "\"" << (*it_a)[i].formatMoney("") << "\","; } s << "\n"; } } #endif } void MyMoneyForecast::calculateScheduledDailyBalances (void) { MyMoneyFile* file = MyMoneyFile::instance(); //Calculate account daily balances TQMap::ConstIterator it_n; for(it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { MyMoneyAccount acc = file->account(*it_n); //set the starting balance of the account setStartingBalance(acc); for(TQDate f_day = forecastStartDate(); f_day <= forecastEndDate(); ) { MyMoneyMoney balanceDayBefore = m_accountList[acc.id()][(f_day.addDays(-1))];//balance of the day before m_accountList[acc.id()][f_day] += balanceDayBefore; //running sum f_day = f_day.addDays(1); } } } int MyMoneyForecast::daysToMinimumBalance(const MyMoneyAccount& acc) { TQString minimumBalance = acc.value("minBalanceAbsolute"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); dailyBalances balance; //Check if acc is not a forecast account, return -1 if(!isForecastAccount(acc)) { return -1; } balance = m_accountList[acc.id()]; for(TQDate it_day = TQDate::tqcurrentDate() ; it_day <= forecastEndDate(); ) { if(minBalance > balance[it_day]) { return TQDate::tqcurrentDate().daysTo(it_day); } it_day = it_day.addDays(1); } return -1; } int MyMoneyForecast::daysToZeroBalance(const MyMoneyAccount& acc) { dailyBalances balance; //Check if acc is not a forecast account, return -1 if(!isForecastAccount(acc)) { return -2; } balance = m_accountList[acc.id()]; if (acc.accountGroup() == MyMoneyAccount::Asset) { for (TQDate it_day = TQDate::tqcurrentDate() ; it_day <= forecastEndDate(); ) { if ( balance[it_day] < MyMoneyMoney ( 0, 1 ) ) { return TQDate::tqcurrentDate().daysTo(it_day); } it_day = it_day.addDays(1); } } else if (acc.accountGroup() == MyMoneyAccount::Liability) { for (TQDate it_day = TQDate::tqcurrentDate() ; it_day <= forecastEndDate(); ) { if ( balance[it_day] > MyMoneyMoney ( 0, 1 ) ) { return TQDate::tqcurrentDate().daysTo(it_day); } it_day = it_day.addDays(1); } } return -1; } void MyMoneyForecast::setForecastAccountList(void) { //get forecast accounts TQValueList accList; accList = forecastAccountList(); TQValueList::const_iterator accList_t = accList.begin(); for(; accList_t != accList.end(); ++accList_t ) { MyMoneyAccount acc = *accList_t; // check if this is a new account for us if(m_nameIdx[acc.id()] != acc.id()) { m_nameIdx[acc.id()] = acc.id(); } } } MyMoneyMoney MyMoneyForecast::accountCycleVariation(const MyMoneyAccount& acc) { MyMoneyMoney cycleVariation; if (forecastMethod() == eHistoric) { for(int t_day = 1; t_day <= accountsCycle() ; ++t_day) { cycleVariation += m_accountTrendList[acc.id()][t_day]; } } return cycleVariation; } MyMoneyMoney MyMoneyForecast::accountTotalVariation(const MyMoneyAccount& acc) { MyMoneyMoney totalVariation; totalVariation = forecastBalance(acc, forecastEndDate()) - forecastBalance(acc, TQDate::tqcurrentDate()); return totalVariation; } TQValueList MyMoneyForecast::accountMinimumBalanceDateList(const MyMoneyAccount& acc) { TQValueList minBalanceList; int daysToBeginDay; daysToBeginDay = TQDate::tqcurrentDate().daysTo(beginForecastDate()); for(int t_cycle = 0; ((t_cycle * accountsCycle()) + daysToBeginDay) < forecastDays() ; ++t_cycle) { MyMoneyMoney minBalance = forecastBalance(acc, (t_cycle * accountsCycle() + daysToBeginDay)); TQDate minDate = TQDate::tqcurrentDate().addDays(t_cycle * accountsCycle() + daysToBeginDay); for(int t_day = 1; t_day <= accountsCycle() ; ++t_day) { if( minBalance > forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day) ) { minBalance = forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day ); minDate = TQDate::tqcurrentDate().addDays( (t_cycle * accountsCycle()) + daysToBeginDay + t_day); } } minBalanceList.append(minDate); } return minBalanceList; } TQValueList MyMoneyForecast::accountMaximumBalanceDateList(const MyMoneyAccount& acc) { TQValueList maxBalanceList; int daysToBeginDay; daysToBeginDay = TQDate::tqcurrentDate().daysTo(beginForecastDate()); for(int t_cycle = 0; ((t_cycle * accountsCycle()) + daysToBeginDay) < forecastDays() ; ++t_cycle) { MyMoneyMoney maxBalance = forecastBalance(acc, ((t_cycle * accountsCycle()) + daysToBeginDay)); TQDate maxDate = TQDate::tqcurrentDate().addDays((t_cycle * accountsCycle()) + daysToBeginDay); for(int t_day = 0; t_day < accountsCycle() ; ++t_day) { if( maxBalance < forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day) ) { maxBalance = forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day ); maxDate = TQDate::tqcurrentDate().addDays((t_cycle * accountsCycle()) + daysToBeginDay + t_day); } } maxBalanceList.append(maxDate); } return maxBalanceList; } MyMoneyMoney MyMoneyForecast::accountAverageBalance(const MyMoneyAccount& acc) { MyMoneyMoney totalBalance; for(int f_day = 1; f_day <= forecastDays() ; ++f_day) { totalBalance += forecastBalance(acc, f_day); } return totalBalance / MyMoneyMoney( forecastDays(), 1); } int MyMoneyForecast::calculateBeginForecastDay() { int fDays = forecastDays(); int beginDay = beginForecastDay(); int accCycle = accountsCycle(); TQDate beginDate; //if 0, beginDate is current date and forecastDays remains unchanged if(beginDay == 0) { setBeginForecastDate(TQDate::tqcurrentDate()); return fDays; } //adjust if beginDay more than days of current month if(TQDate::tqcurrentDate().daysInMonth() < beginDay) beginDay = TQDate::tqcurrentDate().daysInMonth(); //if beginDay still to come, calculate and return if(TQDate::tqcurrentDate().day() <= beginDay) { beginDate = TQDate( TQDate::tqcurrentDate().year(), TQDate::tqcurrentDate().month(), beginDay); fDays += TQDate::tqcurrentDate().daysTo(beginDate); setBeginForecastDate(beginDate); return fDays; } //adjust beginDay for next month if(TQDate::tqcurrentDate().addMonths(1).daysInMonth() < beginDay) beginDay = TQDate::tqcurrentDate().addMonths(1).daysInMonth(); //if beginDay of next month comes before 1 interval, use beginDay next month if(TQDate::tqcurrentDate().addDays(accCycle) >= (TQDate(TQDate::tqcurrentDate().addMonths(1).year(), TQDate::tqcurrentDate().addMonths(1).month(), 1).addDays(beginDay-1) ) ) { beginDate = TQDate(TQDate::tqcurrentDate().addMonths(1).year(), TQDate::tqcurrentDate().addMonths(1).month(), 1).addDays(beginDay-1); fDays += TQDate::tqcurrentDate().daysTo(beginDate); } else //add intervals to current beginDay and take the first after current date { beginDay = ((((TQDate::tqcurrentDate().day()-beginDay)/accCycle) + 1) * accCycle) + beginDay; beginDate = TQDate::tqcurrentDate().addDays(beginDay - TQDate::tqcurrentDate().day()); fDays += TQDate::tqcurrentDate().daysTo(beginDate); } setBeginForecastDate(beginDate); return fDays; } void MyMoneyForecast::purgeForecastAccountsList(TQMap& accountList) { TQMap::Iterator it_n; for ( it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ) { if(!accountList.tqcontains(*it_n)) { m_nameIdx.remove(it_n); it_n = m_nameIdx.begin(); } else ++it_n; } } void MyMoneyForecast::createBudget ( MyMoneyBudget& budget, TQDate historyStart, TQDate historyEnd, TQDate budgetStart, TQDate budgetEnd, const bool returnBudget ) { // clear all data except the id and name TQString name = budget.name(); budget = MyMoneyBudget(budget.id(), MyMoneyBudget()); budget.setName(name); //check parameters if ( historyStart > historyEnd || budgetStart > budgetEnd || budgetStart <= historyEnd ) { throw new MYMONEYEXCEPTION ( "Illegal parameters when trying to create budget" ); } //get forecast method int fMethod = forecastMethod(); //set start date to 1st of month and end dates to last day of month, since we deal with full months in budget historyStart = TQDate ( historyStart.year(), historyStart.month(), 1 ); historyEnd = TQDate ( historyEnd.year(), historyEnd.month(), historyEnd.daysInMonth() ); budgetStart = TQDate ( budgetStart.year(), budgetStart.month(), 1 ); budgetEnd = TQDate ( budgetEnd.year(), budgetEnd.month(), budgetEnd.daysInMonth() ); //set forecast parameters setHistoryStartDate ( historyStart ); setHistoryEndDate ( historyEnd ); setForecastStartDate ( budgetStart ); setForecastEndDate ( budgetEnd ); setForecastDays ( budgetStart.daysTo ( budgetEnd ) + 1 ); if ( budgetStart.daysTo ( budgetEnd ) > historyStart.daysTo ( historyEnd ) ) { //if history period is shorter than budget, use that one as the trend length setAccountsCycle ( historyStart.daysTo ( historyEnd ) ); //we set the accountsCycle to the base timeframe we will use to calculate the average (eg. 180 days, 365, etc) } else { //if one timeframe is larger than the other, but not enough to be 1 time larger, we take the lowest value setAccountsCycle ( budgetStart.daysTo ( budgetEnd ) ); } setForecastCycles ( ( historyStart.daysTo ( historyEnd ) / accountsCycle() ) ); if ( forecastCycles() == 0 ) //the cycles must be at least 1 setForecastCycles ( 1 ); //do not skip opening date setSkipOpeningDate ( false ); //clear and set accounts list we are going to use. Categories, in this case m_nameIdx.clear(); setBudgetAccountList(); //calculate budget according to forecast method switch(fMethod) { case eScheduled: doFutureScheduledForecast(); calculateScheduledMonthlyBalances(); break; case eHistoric: pastTransactions(); //get all transactions for history period calculateAccountTrendList(); calculateHistoricMonthlyBalances(); //add all balances of each month and put at the 1st day of each month break; default: break; } //flag the forecast as done m_forecastDone = true; //only fill the budget if it is going to be used if ( returnBudget ) { //setup the budget itself MyMoneyFile* file = MyMoneyFile::instance(); budget.setBudgetStart ( budgetStart ); //go through all the accounts and add them to budget TQMap::ConstIterator it_nc; for ( it_nc = m_nameIdx.begin(); it_nc != m_nameIdx.end(); ++it_nc ) { MyMoneyAccount acc = file->account ( *it_nc ); MyMoneyBudget::AccountGroup budgetAcc; budgetAcc.setId ( acc.id() ); budgetAcc.setBudgetLevel ( MyMoneyBudget::AccountGroup::eMonthByMonth ); for ( TQDate f_date = forecastStartDate(); f_date <= forecastEndDate(); ) { MyMoneyBudget::PeriodGroup period; //add period to budget account period.setStartDate ( f_date ); period.setAmount ( forecastBalance ( acc, f_date ) ); budgetAcc.addPeriod ( f_date, period ); //next month f_date = f_date.addMonths ( 1 ); } //add budget account to budget budget.setAccount ( budgetAcc, acc.id() ); } } } void MyMoneyForecast::setBudgetAccountList(void) { //get budget accounts TQValueList accList; accList = budgetAccountList(); TQValueList::const_iterator accList_t = accList.begin(); for(; accList_t != accList.end(); ++accList_t ) { MyMoneyAccount acc = *accList_t; // check if this is a new account for us if(m_nameIdx[acc.id()] != acc.id()) { m_nameIdx[acc.id()] = acc.id(); } } } TQValueList MyMoneyForecast::budgetAccountList(void) { MyMoneyFile* file = MyMoneyFile::instance(); TQValueList accList; TQStringList emptyStringList; //Get all accounts from the file and check if they are of the right type to calculate forecast file->accountList(accList, emptyStringList, false); TQValueList::iterator accList_t = accList.begin(); for(; accList_t != accList.end(); ) { MyMoneyAccount acc = *accList_t; if(acc.isClosed() //check the account is not closed || (!acc.isIncomeExpense()) ) { accList.remove(accList_t); //remove the account if it is not of the correct type accList_t = accList.begin(); } else { ++accList_t; } } return accList; } void MyMoneyForecast::calculateHistoricMonthlyBalances() { MyMoneyFile* file = MyMoneyFile::instance(); //Calculate account monthly balances TQMap::ConstIterator it_n; for(it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { MyMoneyAccount acc = file->account(*it_n); for( TQDate f_date = forecastStartDate(); f_date <= forecastEndDate(); ) { for(int f_day = 1; f_day <= accountsCycle() && f_date <= forecastEndDate(); ++f_day) { MyMoneyMoney accountDailyTrend = m_accountTrendList[acc.id()][f_day]; //trend for that day //check for leap year if(f_date.month() == 2 && f_date.day() == 29) f_date = f_date.addDays(1); //skip 1 day m_accountList[acc.id()][TQDate(f_date.year(), f_date.month(), 1)] += accountDailyTrend; //movement trend for that particular day f_date = f_date.addDays(1); } } } } void MyMoneyForecast::calculateScheduledMonthlyBalances() { MyMoneyFile* file = MyMoneyFile::instance(); //Calculate account monthly balances TQMap::ConstIterator it_n; for(it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { MyMoneyAccount acc = file->account(*it_n); for( TQDate f_date = forecastStartDate(); f_date <= forecastEndDate(); f_date = f_date.addDays(1) ) { //get the trend for the day MyMoneyMoney accountDailyBalance = m_accountList[acc.id()][f_date]; //do not add if it is the beginning of the month //otherwise we end up with duplicated values as reported by Marko Käning if(f_date != TQDate(f_date.year(), f_date.month(), 1) ) m_accountList[acc.id()][TQDate(f_date.year(), f_date.month(), 1)] += accountDailyBalance; } } } void MyMoneyForecast::setStartingBalance(const MyMoneyAccount &acc) { MyMoneyFile* file = MyMoneyFile::instance(); //Get current account balance if ( acc.isInvest() ) { //investments require special treatment //get the security id of that account MyMoneySecurity undersecurity = file->security ( acc.currencyId() ); //only do it if the security is not an actual currency if ( ! undersecurity.isCurrency() ) { //set the default value MyMoneyMoney rate = MyMoneyMoney ( 1, 1 ); //get te MyMoneyPrice price = file->price ( undersecurity.id(), undersecurity.tradingCurrency(), TQDate::tqcurrentDate() ); if ( price.isValid() ) { rate = price.rate ( undersecurity.tradingCurrency() ); } m_accountList[acc.id()][TQDate::tqcurrentDate()] = file->balance(acc.id(), TQDate::tqcurrentDate()) * rate; } } else { m_accountList[acc.id()][TQDate::tqcurrentDate()] = file->balance(acc.id(), TQDate::tqcurrentDate()); } //if the method is linear regression, we have to add the opening balance to m_accountListPast if(forecastMethod() == eHistoric && historyMethod() == 2) { //FIXME workaround for stock opening dates TQDate openingDate; if(acc.accountType() == MyMoneyAccount::Stock) { MyMoneyAccount tqparentAccount = file->account(acc.tqparentAccountId()); openingDate = tqparentAccount.openingDate(); } else { openingDate = acc.openingDate(); } //add opening balance only if it opened after the history start if(openingDate >= historyStartDate()) { MyMoneyMoney openingBalance; openingBalance = file->balance(acc.id(), openingDate); //calculate running sum for(TQDate it_date = openingDate; it_date <= historyEndDate(); it_date = it_date.addDays(1) ) { //investments require special treatment if ( acc.isInvest() ) { //get the security id of that account MyMoneySecurity undersecurity = file->security ( acc.currencyId() ); //only do it if the security is not an actual currency if ( ! undersecurity.isCurrency() ) { //set the default value MyMoneyMoney rate = MyMoneyMoney ( 1, 1 ); //get the rate for that specific date MyMoneyPrice price = file->price ( undersecurity.id(), undersecurity.tradingCurrency(), it_date ); if ( price.isValid() ) { rate = price.rate ( undersecurity.tradingCurrency() ); } m_accountListPast[acc.id()][it_date] += openingBalance * rate; } } else { m_accountListPast[acc.id()][it_date] += openingBalance; } } } } } void MyMoneyForecast::calculateAutoLoan(const MyMoneySchedule& schedule, MyMoneyTransaction& transaction, const TQMap& balances) { if (schedule.type() == MyMoneySchedule::TYPE_LOANPAYMENT) { //get amortization and interest autoCalc splits MyMoneySplit amortizationSplit = transaction.amortizationSplit(); MyMoneySplit interestSplit = transaction.interestSplit(); if(!amortizationSplit.id().isEmpty() && !interestSplit.id().isEmpty()) { MyMoneyAccountLoan acc(MyMoneyFile::instance()->account(amortizationSplit.accountId())); MyMoneyFinancialCalculator calc; TQDate dueDate; // FIXME: setup dueDate according to when the interest should be calculated // current implementation: take the date of the next payment according to // the schedule. If the calculation is based on the payment reception, and // the payment is overdue then take the current date dueDate = schedule.nextDueDate(); if(acc.interestCalculation() == MyMoneyAccountLoan::paymentReceived) { if(dueDate < TQDate::tqcurrentDate()) dueDate = TQDate::tqcurrentDate(); } // we need to calculate the balance at the time the payment is due MyMoneyMoney balance; if(balances.count() == 0) balance = MyMoneyFile::instance()->balance(acc.id(), dueDate.addDays(-1)); else balance = balances[acc.id()]; /* TQValueList list; TQValueList::ConstIterator it; MyMoneySplit split; MyMoneyTransactionFilter filter(acc.id()); filter.setDateFilter(TQDate(), dueDate.addDays(-1)); list = MyMoneyFile::instance()->transactionList(filter); for(it = list.begin(); it != list.end(); ++it) { try { split = (*it).splitByAccount(acc.id()); balance += split.value(); } catch(MyMoneyException *e) { // account is not referenced within this transaction delete e; } } */ // FIXME: for now, we only support interest calculation at the end of the period calc.setBep(); // FIXME: for now, we only support periodic compounding calc.setDisc(); calc.setPF(MyMoneySchedule::eventsPerYear(schedule.occurence())); MyMoneySchedule::occurenceE compoundingOccurence = static_cast(acc.interestCompounding()); if(compoundingOccurence == MyMoneySchedule::OCCUR_ANY) compoundingOccurence = schedule.occurence(); calc.setCF(MyMoneySchedule::eventsPerYear(compoundingOccurence)); calc.setPv(balance.toDouble()); calc.setIr(static_cast (acc.interestRate(dueDate).abs().toDouble())); calc.setPmt(acc.periodicPayment().toDouble()); MyMoneyMoney interest(calc.interestDue()), amortization; interest = interest.abs(); // make sure it's positive for now amortization = acc.periodicPayment() - interest; if(acc.accountType() == MyMoneyAccount::AssetLoan) { interest = -interest; amortization = -amortization; } amortizationSplit.setShares(amortization); interestSplit.setShares(interest); // FIXME: for now we only assume loans to be in the currency of the transaction amortizationSplit.setValue(amortization); interestSplit.setValue(interest); transaction.modifySplit(amortizationSplit); transaction.modifySplit(interestSplit); } } }