File indexing completed on 2024-05-12 16:42:37
0001 /* 0002 SPDX-FileCopyrightText: 2007-2010 Alvaro Soliverez <asoliverez@gmail.com> 0003 SPDX-FileCopyrightText: 2017-2018 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com> 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "mymoneyforecast.h" 0008 0009 // ---------------------------------------------------------------------------- 0010 // QT Includes 0011 0012 #include <QString> 0013 #include <QList> 0014 #include <QDebug> 0015 #include <QDate> 0016 0017 // ---------------------------------------------------------------------------- 0018 // KDE Includes 0019 0020 // ---------------------------------------------------------------------------- 0021 // Project Includes 0022 0023 #include "mymoneyfile.h" 0024 #include "mymoneyaccount.h" 0025 #include "mymoneyaccountloan.h" 0026 #include "mymoneysecurity.h" 0027 #include "mymoneybudget.h" 0028 #include "mymoneyschedule.h" 0029 #include "mymoneyprice.h" 0030 #include "mymoneymoney.h" 0031 #include "mymoneysplit.h" 0032 #include "mymoneytransaction.h" 0033 #include "mymoneytransactionfilter.h" 0034 #include "mymoneyfinancialcalculator.h" 0035 #include "mymoneyexception.h" 0036 #include "mymoneyenums.h" 0037 0038 enum class eForecastMethod {Scheduled = 0, Historic = 1 }; 0039 0040 /** 0041 * daily balances of an account 0042 */ 0043 typedef QMap<QDate, MyMoneyMoney> dailyBalances; 0044 0045 /** 0046 * map of trends of an account 0047 */ 0048 typedef QMap<int, MyMoneyMoney> trendBalances; 0049 0050 class MyMoneyForecastPrivate 0051 { 0052 Q_DECLARE_PUBLIC(MyMoneyForecast) 0053 0054 public: 0055 explicit MyMoneyForecastPrivate(MyMoneyForecast *qq) : 0056 q_ptr(qq), 0057 m_accountsCycle(30), 0058 m_forecastCycles(3), 0059 m_forecastDays(90), 0060 m_beginForecastDay(0), 0061 m_forecastMethod(eForecastMethod::Scheduled), 0062 m_historyMethod(1), 0063 m_skipOpeningDate(true), 0064 m_includeUnusedAccounts(false), 0065 m_forecastDone(false), 0066 m_includeFutureTransactions(true), 0067 m_includeScheduledTransactions(true) 0068 { 0069 } 0070 0071 eForecastMethod forecastMethod() const 0072 { 0073 return m_forecastMethod; 0074 } 0075 0076 /** 0077 * Returns the list of accounts to create a budget. Only Income and Expenses are returned. 0078 */ 0079 QList<MyMoneyAccount> budgetAccountList() 0080 { 0081 auto file = MyMoneyFile::instance(); 0082 0083 QList<MyMoneyAccount> accList; 0084 QStringList emptyStringList; 0085 //Get all accounts from the file and check if they are of the right type to calculate forecast 0086 file->accountList(accList, emptyStringList, false); 0087 QList<MyMoneyAccount>::iterator accList_t = accList.begin(); 0088 for (; accList_t != accList.end();) { 0089 auto acc = *accList_t; 0090 if (acc.isClosed() //check the account is not closed 0091 || (!acc.isIncomeExpense())) { 0092 //remove the account if it is not of the correct type 0093 accList_t = accList.erase(accList_t); 0094 } else { 0095 ++accList_t; 0096 } 0097 } 0098 return accList; 0099 } 0100 0101 /** 0102 * calculate daily forecast balance based on historic transactions 0103 */ 0104 void calculateHistoricDailyBalances() 0105 { 0106 Q_Q(MyMoneyForecast); 0107 auto file = MyMoneyFile::instance(); 0108 0109 calculateAccountTrendList(); 0110 0111 //Calculate account daily balances 0112 QSet<QString>::ConstIterator it_n; 0113 for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { 0114 auto acc = file->account(*it_n); 0115 0116 //set the starting balance of the account 0117 setStartingBalance(acc); 0118 0119 switch (q->historyMethod()) { 0120 case 0: 0121 case 1: { 0122 for (QDate f_day = q->forecastStartDate(); f_day <= q->forecastEndDate();) { 0123 for (auto t_day = 1; t_day <= q->accountsCycle(); ++t_day) { 0124 MyMoneyMoney balanceDayBefore = m_accountList[acc.id()][(f_day.addDays(-1))];//balance of the day before 0125 MyMoneyMoney accountDailyTrend = m_accountTrendList[acc.id()][t_day]; //trend for that day 0126 //balance of the day is the balance of the day before multiplied by the trend for the day 0127 m_accountList[acc.id()][f_day] = balanceDayBefore; 0128 m_accountList[acc.id()][f_day] += accountDailyTrend; //movement trend for that particular day 0129 m_accountList[acc.id()][f_day] = m_accountList[acc.id()][f_day].convert(acc.fraction()); 0130 //m_accountList[acc.id()][f_day] += m_accountListPast[acc.id()][f_day.addDays(-q->historyDays())]; 0131 f_day = f_day.addDays(1); 0132 } 0133 } 0134 } 0135 break; 0136 case 2: { 0137 QDate baseDate = QDate::currentDate().addDays(-q->accountsCycle()); 0138 for (auto t_day = 1; t_day <= q->accountsCycle(); ++t_day) { 0139 auto f_day = 1; 0140 QDate fDate = baseDate.addDays(q->accountsCycle() + 1); 0141 while (fDate <= q->forecastEndDate()) { 0142 0143 //the calculation is based on the balance for the last month, that is then multiplied by the trend 0144 m_accountList[acc.id()][fDate] = m_accountListPast[acc.id()][baseDate] + (m_accountTrendList[acc.id()][t_day] * MyMoneyMoney(f_day, 1)); 0145 m_accountList[acc.id()][fDate] = m_accountList[acc.id()][fDate].convert(acc.fraction()); 0146 ++f_day; 0147 fDate = baseDate.addDays(q->accountsCycle() * f_day); 0148 } 0149 baseDate = baseDate.addDays(1); 0150 } 0151 } 0152 } 0153 } 0154 } 0155 0156 /** 0157 * calculate monthly budget balance based on historic transactions 0158 */ 0159 void calculateHistoricMonthlyBalances() 0160 { 0161 Q_Q(MyMoneyForecast); 0162 auto file = MyMoneyFile::instance(); 0163 0164 //Calculate account monthly balances 0165 QSet<QString>::ConstIterator it_n; 0166 for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { 0167 auto acc = file->account(*it_n); 0168 0169 for (QDate f_date = q->forecastStartDate(); f_date <= q->forecastEndDate();) { 0170 for (auto f_day = 1; f_day <= q->accountsCycle() && f_date <= q->forecastEndDate(); ++f_day) { 0171 MyMoneyMoney accountDailyTrend = m_accountTrendList[acc.id()][f_day]; //trend for that day 0172 //check for leap year 0173 if (f_date.month() == 2 && f_date.day() == 29) 0174 f_date = f_date.addDays(1); //skip 1 day 0175 m_accountList[acc.id()][QDate(f_date.year(), f_date.month(), 1)] += accountDailyTrend; //movement trend for that particular day 0176 f_date = f_date.addDays(1); 0177 } 0178 } 0179 } 0180 } 0181 0182 /** 0183 * calculate monthly budget balance based on historic transactions 0184 */ 0185 void calculateScheduledMonthlyBalances() 0186 { 0187 Q_Q(MyMoneyForecast); 0188 auto file = MyMoneyFile::instance(); 0189 0190 //Calculate account monthly balances 0191 QSet<QString>::ConstIterator it_n; 0192 for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { 0193 auto acc = file->account(*it_n); 0194 0195 for (QDate f_date = q->forecastStartDate(); f_date <= q->forecastEndDate(); f_date = f_date.addDays(1)) { 0196 //get the trend for the day 0197 MyMoneyMoney accountDailyBalance = m_accountList[acc.id()][f_date]; 0198 0199 //do not add if it is the beginning of the month 0200 //otherwise we end up with duplicated values as reported by Marko Käning 0201 if (f_date != QDate(f_date.year(), f_date.month(), 1)) 0202 m_accountList[acc.id()][QDate(f_date.year(), f_date.month(), 1)] += accountDailyBalance; 0203 } 0204 } 0205 } 0206 0207 /** 0208 * calculate forecast based on future and scheduled transactions 0209 */ 0210 void doFutureScheduledForecast() 0211 { 0212 Q_Q(MyMoneyForecast); 0213 auto file = MyMoneyFile::instance(); 0214 0215 if (q->isIncludingFutureTransactions()) 0216 addFutureTransactions(); 0217 0218 if (q->isIncludingScheduledTransactions()) 0219 addScheduledTransactions(); 0220 0221 //do not show accounts with no transactions 0222 if (!q->isIncludingUnusedAccounts()) 0223 purgeForecastAccountsList(m_accountList); 0224 0225 //adjust value of investments to deep currency 0226 QSet<QString>::ConstIterator it_n; 0227 for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { 0228 auto acc = file->account(*it_n); 0229 0230 if (acc.isInvest()) { 0231 //get the id of the security for that account 0232 MyMoneySecurity undersecurity = file->security(acc.currencyId()); 0233 0234 //only do it if the security is not an actual currency 0235 if (! undersecurity.isCurrency()) { 0236 //set the default value 0237 MyMoneyMoney rate = MyMoneyMoney::ONE; 0238 0239 for (QDate it_day = QDate::currentDate(); it_day <= q->forecastEndDate();) { 0240 //get the price for the tradingCurrency that day 0241 const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), it_day); 0242 if (price.isValid()) { 0243 rate = price.rate(undersecurity.tradingCurrency()); 0244 } 0245 //value is the amount of shares multiplied by the rate of the deep currency 0246 m_accountList[acc.id()][it_day] = m_accountList[acc.id()][it_day] * rate; 0247 it_day = it_day.addDays(1); 0248 } 0249 } 0250 } 0251 } 0252 } 0253 0254 /** 0255 * add future transactions to forecast 0256 */ 0257 void addFutureTransactions() 0258 { 0259 Q_Q(MyMoneyForecast); 0260 MyMoneyTransactionFilter filter; 0261 auto file = MyMoneyFile::instance(); 0262 0263 // collect and process all transactions that have already been entered but 0264 // are located in the future. 0265 filter.setDateFilter(q->forecastStartDate(), q->forecastEndDate()); 0266 filter.setReportAllSplits(false); 0267 0268 foreach (const auto transaction, file->transactionList(filter)) { 0269 foreach (const auto split, transaction.splits()) { 0270 if (!split.shares().isZero()) { 0271 auto acc = file->account(split.accountId()); 0272 if (q->isForecastAccount(acc)) { 0273 dailyBalances balance; 0274 balance = m_accountList[acc.id()]; 0275 //if it is income, the balance is stored as negative number 0276 if (acc.accountType() == eMyMoney::Account::Type::Income) { 0277 balance[transaction.postDate()] += (split.shares() * MyMoneyMoney::MINUS_ONE); 0278 } else { 0279 balance[transaction.postDate()] += split.shares(); 0280 } 0281 m_accountList[acc.id()] = balance; 0282 } 0283 } 0284 } 0285 } 0286 0287 #if 0 0288 QFile trcFile("forecast.csv"); 0289 trcFile.open(QIODevice::WriteOnly); 0290 QTextStream s(&trcFile); 0291 0292 { 0293 s << "Already present transactions\n"; 0294 QMap<QString, dailyBalances>::Iterator it_a; 0295 QSet<QString>::ConstIterator it_n; 0296 for (it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { 0297 auto acc = file->account(*it_n); 0298 it_a = m_accountList.find(*it_n); 0299 s << "\"" << acc.name() << "\","; 0300 for (auto i = 0; i < 90; ++i) { 0301 s << "\"" << (*it_a)[i].formatMoney("") << "\","; 0302 } 0303 s << "\n"; 0304 } 0305 } 0306 #endif 0307 0308 } 0309 0310 /** 0311 * add scheduled transactions to forecast 0312 */ 0313 void addScheduledTransactions() 0314 { 0315 Q_Q(MyMoneyForecast); 0316 auto file = MyMoneyFile::instance(); 0317 0318 // now process all the schedules that may have an impact 0319 QList<MyMoneySchedule> schedule; 0320 0321 schedule = file->scheduleList(QString(), eMyMoney::Schedule::Type::Any, eMyMoney::Schedule::Occurrence::Any, eMyMoney::Schedule::PaymentType::Any, 0322 QDate(), q->forecastEndDate(), false); 0323 if (schedule.count() > 0) { 0324 QList<MyMoneySchedule>::Iterator it; 0325 do { 0326 qSort(schedule); 0327 it = schedule.begin(); 0328 if (it == schedule.end()) 0329 break; 0330 0331 if ((*it).isFinished()) { 0332 schedule.erase(it); 0333 continue; 0334 } 0335 0336 QDate date = (*it).nextPayment((*it).lastPayment()); 0337 if (!date.isValid()) { 0338 schedule.erase(it); 0339 continue; 0340 } 0341 0342 QDate nextDate = 0343 (*it).adjustedNextPayment((*it).adjustedDate((*it).lastPayment(), 0344 (*it).weekendOption())); 0345 if (nextDate > q->forecastEndDate()) { 0346 // We're done with this schedule, let's move on to the next 0347 schedule.erase(it); 0348 continue; 0349 } 0350 0351 // found the next schedule. process it 0352 0353 auto acc = (*it).account(); 0354 0355 if (!acc.id().isEmpty()) { 0356 try { 0357 if (acc.accountType() != eMyMoney::Account::Type::Investment) { 0358 auto t = (*it).transaction(); 0359 0360 // only process the entry, if it is still active 0361 if (!(*it).isFinished() && nextDate != QDate()) { 0362 // make sure we have all 'starting balances' so that the autocalc works 0363 QMap<QString, MyMoneyMoney> balanceMap; 0364 0365 foreach (const auto split, t.splits()) { 0366 auto accountFromSplit = file->account(split.accountId()); 0367 if (q->isForecastAccount(accountFromSplit)) { 0368 // collect all overdues on the first day 0369 QDate forecastDate = nextDate; 0370 if (QDate::currentDate() >= nextDate) 0371 forecastDate = QDate::currentDate().addDays(1); 0372 0373 dailyBalances balance; 0374 balance = m_accountList[accountFromSplit.id()]; 0375 for (QDate f_day = QDate::currentDate(); f_day < forecastDate;) { 0376 balanceMap[accountFromSplit.id()] += m_accountList[accountFromSplit.id()][f_day]; 0377 f_day = f_day.addDays(1); 0378 } 0379 } 0380 } 0381 0382 // take care of the autoCalc stuff 0383 q->calculateAutoLoan(*it, t, balanceMap); 0384 0385 // now add the splits to the balances 0386 foreach (const auto split, t.splits()) { 0387 auto accountFromSplit = file->account(split.accountId()); 0388 if (q->isForecastAccount(accountFromSplit)) { 0389 dailyBalances balance; 0390 balance = m_accountList[accountFromSplit.id()]; 0391 //auto offset = QDate::currentDate().daysTo(nextDate); 0392 //if(offset <= 0) { // collect all overdues on the first day 0393 // offset = 1; 0394 //} 0395 // collect all overdues on the first day 0396 QDate forecastDate = nextDate; 0397 if (QDate::currentDate() >= nextDate) 0398 forecastDate = QDate::currentDate().addDays(1); 0399 0400 if (accountFromSplit.accountType() == eMyMoney::Account::Type::Income) { 0401 balance[forecastDate] += (split.shares() * MyMoneyMoney::MINUS_ONE); 0402 } else { 0403 balance[forecastDate] += split.shares(); 0404 } 0405 m_accountList[accountFromSplit.id()] = balance; 0406 } 0407 } 0408 } 0409 } 0410 (*it).setLastPayment(date); 0411 0412 } catch (const MyMoneyException &e) { 0413 qDebug() << Q_FUNC_INFO << " Schedule " << (*it).id() << " (" << (*it).name() << "): " << e.what(); 0414 0415 schedule.erase(it); 0416 } 0417 } else { 0418 // remove schedule from list 0419 schedule.erase(it); 0420 } 0421 } while (1); 0422 } 0423 0424 #if 0 0425 { 0426 s << "\n\nAdded scheduled transactions\n"; 0427 QMap<QString, dailyBalances>::Iterator it_a; 0428 QSet<QString>::ConstIterator it_n; 0429 for (it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { 0430 auto acc = file->account(*it_n); 0431 it_a = m_accountList.find(*it_n); 0432 s << "\"" << acc.name() << "\","; 0433 for (auto i = 0; i < 90; ++i) { 0434 s << "\"" << (*it_a)[i].formatMoney("") << "\","; 0435 } 0436 s << "\n"; 0437 } 0438 } 0439 #endif 0440 } 0441 0442 /** 0443 * calculate daily forecast balance based on future and scheduled transactions 0444 */ 0445 void calculateScheduledDailyBalances() 0446 { 0447 Q_Q(MyMoneyForecast); 0448 auto file = MyMoneyFile::instance(); 0449 0450 //Calculate account daily balances 0451 QSet<QString>::ConstIterator it_n; 0452 for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { 0453 auto acc = file->account(*it_n); 0454 0455 //set the starting balance of the account 0456 setStartingBalance(acc); 0457 0458 for (QDate f_day = q->forecastStartDate(); f_day <= q->forecastEndDate();) { 0459 MyMoneyMoney balanceDayBefore = m_accountList[acc.id()][(f_day.addDays(-1))];//balance of the day before 0460 m_accountList[acc.id()][f_day] += balanceDayBefore; //running sum 0461 f_day = f_day.addDays(1); 0462 } 0463 } 0464 } 0465 0466 /** 0467 * set the starting balance for an accounts 0468 */ 0469 void setStartingBalance(const MyMoneyAccount& acc) 0470 { 0471 Q_Q(MyMoneyForecast); 0472 auto file = MyMoneyFile::instance(); 0473 0474 //Get current account balance 0475 if (acc.isInvest()) { //investments require special treatment 0476 //get the security id of that account 0477 MyMoneySecurity undersecurity = file->security(acc.currencyId()); 0478 0479 //only do it if the security is not an actual currency 0480 if (! undersecurity.isCurrency()) { 0481 //set the default value 0482 MyMoneyMoney rate = MyMoneyMoney::ONE; 0483 //get te 0484 const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), QDate::currentDate()); 0485 if (price.isValid()) { 0486 rate = price.rate(undersecurity.tradingCurrency()); 0487 } 0488 m_accountList[acc.id()][QDate::currentDate()] = file->balance(acc.id(), QDate::currentDate()) * rate; 0489 } 0490 } else { 0491 m_accountList[acc.id()][QDate::currentDate()] = file->balance(acc.id(), QDate::currentDate()); 0492 } 0493 0494 //if the method is linear regression, we have to add the opening balance to m_accountListPast 0495 if (forecastMethod() == eForecastMethod::Historic && q->historyMethod() == 2) { 0496 //FIXME workaround for stock opening dates 0497 QDate openingDate; 0498 if (acc.accountType() == eMyMoney::Account::Type::Stock) { 0499 auto parentAccount = file->account(acc.parentAccountId()); 0500 openingDate = parentAccount.openingDate(); 0501 } else { 0502 openingDate = acc.openingDate(); 0503 } 0504 0505 //add opening balance only if it opened after the history start 0506 if (openingDate >= q->historyStartDate()) { 0507 0508 MyMoneyMoney openingBalance; 0509 0510 openingBalance = file->balance(acc.id(), openingDate); 0511 0512 //calculate running sum 0513 for (QDate it_date = openingDate; it_date <= q->historyEndDate(); it_date = it_date.addDays(1)) { 0514 //investments require special treatment 0515 if (acc.isInvest()) { 0516 //get the security id of that account 0517 MyMoneySecurity undersecurity = file->security(acc.currencyId()); 0518 0519 //only do it if the security is not an actual currency 0520 if (! undersecurity.isCurrency()) { 0521 //set the default value 0522 MyMoneyMoney rate = MyMoneyMoney::ONE; 0523 0524 //get the rate for that specific date 0525 const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), it_date); 0526 if (price.isValid()) { 0527 rate = price.rate(undersecurity.tradingCurrency()); 0528 } 0529 m_accountListPast[acc.id()][it_date] += openingBalance * rate; 0530 } 0531 } else { 0532 m_accountListPast[acc.id()][it_date] += openingBalance; 0533 } 0534 } 0535 } 0536 } 0537 } 0538 0539 /** 0540 * Returns the day moving average for the account @a acc based on the daily balances of a given number of @p forecastTerms 0541 * It returns the moving average for a given @p trendDay of the forecastTerm 0542 * With a term of 1 month and 3 terms, it calculates the trend taking the transactions occurred 0543 * at that day and the day before,for the last 3 months 0544 */ 0545 MyMoneyMoney accountMovingAverage(const MyMoneyAccount& acc, const qint64 trendDay, const int forecastTerms) 0546 { 0547 Q_Q(MyMoneyForecast); 0548 //Calculate a daily trend for the account based on the accounts of a given number of terms 0549 //With a term of 1 month and 3 terms, it calculates the trend taking the transactions occurred at that day and the day before, 0550 //for the last 3 months 0551 MyMoneyMoney balanceVariation; 0552 0553 for (auto it_terms = 0; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms) { //sum for each term 0554 MyMoneyMoney balanceBefore = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-2)]; //get balance for the day before 0555 MyMoneyMoney balanceAfter = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; 0556 balanceVariation += (balanceAfter - balanceBefore); //add the balance variation between days 0557 } 0558 //calculate average of the variations 0559 return (balanceVariation / MyMoneyMoney(forecastTerms, 1)).convert(10000); 0560 } 0561 0562 /** 0563 * Returns the weighted moving average for a given @p trendDay 0564 */ 0565 MyMoneyMoney accountWeightedMovingAverage(const MyMoneyAccount& acc, const qint64 trendDay, const int totalWeight) 0566 { 0567 Q_Q(MyMoneyForecast); 0568 MyMoneyMoney balanceVariation; 0569 0570 for (auto it_terms = 0, weight = 1; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms, ++weight) { //sum for each term multiplied by weight 0571 MyMoneyMoney balanceBefore = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-2)]; //get balance for the day before 0572 MyMoneyMoney balanceAfter = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; 0573 balanceVariation += ((balanceAfter - balanceBefore) * MyMoneyMoney(weight, 1)); //add the balance variation between days multiplied by its weight 0574 } 0575 //calculate average of the variations 0576 return (balanceVariation / MyMoneyMoney(totalWeight, 1)).convert(10000); 0577 } 0578 0579 /** 0580 * Returns the linear regression for a given @p trendDay 0581 */ 0582 MyMoneyMoney accountLinearRegression(const MyMoneyAccount &acc, const qint64 trendDay, const qint64 actualTerms, const MyMoneyMoney& meanTerms) 0583 { 0584 Q_Q(MyMoneyForecast); 0585 MyMoneyMoney meanBalance, totalBalance, totalTerms; 0586 totalTerms = MyMoneyMoney(actualTerms, 1); 0587 0588 //calculate mean balance 0589 for (auto it_terms = q->forecastCycles() - actualTerms; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms) { //sum for each term 0590 totalBalance += m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; 0591 } 0592 meanBalance = totalBalance / MyMoneyMoney(actualTerms, 1); 0593 meanBalance = meanBalance.convert(10000); 0594 0595 //calculate b1 0596 0597 //first calculate x - mean x multiplied by y - mean y 0598 MyMoneyMoney totalXY, totalSqX; 0599 auto term = 1; 0600 for (auto it_terms = q->forecastCycles() - actualTerms; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms, ++term) { //sum for each term 0601 MyMoneyMoney balance = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; 0602 0603 MyMoneyMoney balMeanBal = balance - meanBalance; 0604 MyMoneyMoney termMeanTerm = (MyMoneyMoney(term, 1) - meanTerms); 0605 0606 totalXY += (balMeanBal * termMeanTerm).convert(10000); 0607 0608 totalSqX += (termMeanTerm * termMeanTerm).convert(10000); 0609 } 0610 totalXY = (totalXY / MyMoneyMoney(actualTerms, 1)).convert(10000); 0611 totalSqX = (totalSqX / MyMoneyMoney(actualTerms, 1)).convert(10000); 0612 0613 //check zero 0614 if (totalSqX.isZero()) 0615 return MyMoneyMoney(); 0616 0617 MyMoneyMoney linReg = (totalXY / totalSqX).convert(10000); 0618 0619 return linReg; 0620 } 0621 0622 /** 0623 * calculate daily forecast trend based on historic transactions 0624 */ 0625 void calculateAccountTrendList() 0626 { 0627 Q_Q(MyMoneyForecast); 0628 auto file = MyMoneyFile::instance(); 0629 qint64 auxForecastTerms; 0630 qint64 totalWeight = 0; 0631 0632 //Calculate account trends 0633 QSet<QString>::ConstIterator it_n; 0634 for (it_n = m_forecastAccounts.begin(); it_n != m_forecastAccounts.end(); ++it_n) { 0635 auto acc = file->account(*it_n); 0636 m_accountTrendList[acc.id()][0] = MyMoneyMoney(); // for today, the trend is 0 0637 0638 auxForecastTerms = q->forecastCycles(); 0639 if (q->skipOpeningDate()) { 0640 0641 QDate openingDate; 0642 if (acc.accountType() == eMyMoney::Account::Type::Stock) { 0643 auto parentAccount = file->account(acc.parentAccountId()); 0644 openingDate = parentAccount.openingDate(); 0645 } else { 0646 openingDate = acc.openingDate(); 0647 } 0648 0649 if (openingDate > q->historyStartDate()) { //if acc opened after forecast period 0650 auxForecastTerms = 1 + ((openingDate.daysTo(q->historyEndDate()) + 1) / q->accountsCycle()); // set forecastTerms to a lower value, to calculate only based on how long this account was opened 0651 } 0652 } 0653 0654 switch (q->historyMethod()) { 0655 //moving average 0656 case 0: { 0657 for (auto t_day = 1; t_day <= q->accountsCycle(); ++t_day) 0658 m_accountTrendList[acc.id()][t_day] = accountMovingAverage(acc, t_day, auxForecastTerms); //moving average 0659 break; 0660 } 0661 //weighted moving average 0662 case 1: { 0663 //calculate total weight for moving average 0664 if (auxForecastTerms == q->forecastCycles()) { 0665 totalWeight = (auxForecastTerms * (auxForecastTerms + 1)) / 2; //totalWeight is the triangular number of auxForecastTerms 0666 } else { 0667 //if only taking a few periods, totalWeight is the sum of the weight for most recent periods 0668 auto i = 1; 0669 for (qint64 w = q->forecastCycles(); i <= auxForecastTerms; ++i, --w) 0670 totalWeight += w; 0671 } 0672 for (auto t_day = 1; t_day <= q->accountsCycle(); ++t_day) 0673 m_accountTrendList[acc.id()][t_day] = accountWeightedMovingAverage(acc, t_day, totalWeight); 0674 break; 0675 } 0676 case 2: { 0677 //calculate mean term 0678 MyMoneyMoney meanTerms = MyMoneyMoney((auxForecastTerms * (auxForecastTerms + 1)) / 2, 1) / MyMoneyMoney(auxForecastTerms, 1); 0679 0680 for (auto t_day = 1; t_day <= q->accountsCycle(); ++t_day) 0681 m_accountTrendList[acc.id()][t_day] = accountLinearRegression(acc, t_day, auxForecastTerms, meanTerms); 0682 break; 0683 } 0684 default: 0685 break; 0686 } 0687 } 0688 } 0689 0690 /** 0691 * set the internal list of accounts to be forecast 0692 */ 0693 void setForecastAccountList() 0694 { 0695 Q_Q(MyMoneyForecast); 0696 //get forecast accounts 0697 QList<MyMoneyAccount> accList; 0698 accList = q->forecastAccountList(); 0699 0700 QList<MyMoneyAccount>::const_iterator accList_t = accList.constBegin(); 0701 for (; accList_t != accList.constEnd(); ++accList_t) { 0702 m_forecastAccounts.insert((*accList_t).id()); 0703 } 0704 } 0705 0706 /** 0707 * set the internal list of accounts to create a budget 0708 */ 0709 void setBudgetAccountList() 0710 { 0711 //get budget accounts 0712 QList<MyMoneyAccount> accList; 0713 accList = budgetAccountList(); 0714 0715 QList<MyMoneyAccount>::const_iterator accList_t = accList.constBegin(); 0716 for (; accList_t != accList.constEnd(); ++accList_t) { 0717 m_forecastAccounts.insert((*accList_t).id()); 0718 } 0719 } 0720 0721 /** 0722 * get past transactions for the accounts to be forecast 0723 */ 0724 void pastTransactions() 0725 { 0726 Q_Q(MyMoneyForecast); 0727 auto file = MyMoneyFile::instance(); 0728 MyMoneyTransactionFilter filter; 0729 0730 filter.setDateFilter(q->historyStartDate(), q->historyEndDate()); 0731 filter.setReportAllSplits(false); 0732 0733 //Check past transactions 0734 foreach (const auto transaction, file->transactionList(filter)) { 0735 foreach (const auto split, transaction.splits()) { 0736 if (!split.shares().isZero()) { 0737 auto acc = file->account(split.accountId()); 0738 0739 //workaround for stock accounts which have faulty opening dates 0740 QDate openingDate; 0741 if (acc.accountType() == eMyMoney::Account::Type::Stock) { 0742 auto parentAccount = file->account(acc.parentAccountId()); 0743 openingDate = parentAccount.openingDate(); 0744 } else { 0745 openingDate = acc.openingDate(); 0746 } 0747 0748 if (q->isForecastAccount(acc) //If it is one of the accounts we are checking, add the amount of the transaction 0749 && ((openingDate < transaction.postDate() && q->skipOpeningDate()) 0750 || !q->skipOpeningDate())) { //don't take the opening day of the account to calculate balance 0751 dailyBalances balance; 0752 //FIXME deal with leap years 0753 balance = m_accountListPast[acc.id()]; 0754 if (acc.accountType() == eMyMoney::Account::Type::Income) {//if it is income, the balance is stored as negative number 0755 balance[transaction.postDate()] += (split.shares() * MyMoneyMoney::MINUS_ONE); 0756 } else { 0757 balance[transaction.postDate()] += split.shares(); 0758 } 0759 // check if this is a new account for us 0760 m_accountListPast[acc.id()] = balance; 0761 } 0762 } 0763 } 0764 } 0765 0766 //purge those accounts with no transactions on the period 0767 if (q->isIncludingUnusedAccounts() == false) 0768 purgeForecastAccountsList(m_accountListPast); 0769 0770 //calculate running sum 0771 QSet<QString>::ConstIterator it_n; 0772 for (it_n = m_forecastAccounts.begin(); it_n != m_forecastAccounts.end(); ++it_n) { 0773 auto acc = file->account(*it_n); 0774 m_accountListPast[acc.id()][q->historyStartDate().addDays(-1)] = file->balance(acc.id(), q->historyStartDate().addDays(-1)); 0775 for (QDate it_date = q->historyStartDate(); it_date <= q->historyEndDate();) { 0776 m_accountListPast[acc.id()][it_date] += m_accountListPast[acc.id()][it_date.addDays(-1)]; //Running sum 0777 it_date = it_date.addDays(1); 0778 } 0779 } 0780 0781 //adjust value of investments to deep currency 0782 for (it_n = m_forecastAccounts.begin(); it_n != m_forecastAccounts.end(); ++it_n) { 0783 auto acc = file->account(*it_n); 0784 0785 if (acc.isInvest()) { 0786 //get the id of the security for that account 0787 MyMoneySecurity undersecurity = file->security(acc.currencyId()); 0788 if (! undersecurity.isCurrency()) { //only do it if the security is not an actual currency 0789 MyMoneyMoney rate = MyMoneyMoney::ONE; //set the default value 0790 0791 for (QDate it_date = q->historyStartDate().addDays(-1) ; it_date <= q->historyEndDate();) { 0792 //get the price for the tradingCurrency that day 0793 const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), it_date); 0794 if (price.isValid()) { 0795 rate = price.rate(undersecurity.tradingCurrency()); 0796 } 0797 //value is the amount of shares multiplied by the rate of the deep currency 0798 m_accountListPast[acc.id()][it_date] = m_accountListPast[acc.id()][it_date] * rate; 0799 it_date = it_date.addDays(1); 0800 } 0801 } 0802 } 0803 } 0804 } 0805 0806 /** 0807 * calculate the day to start forecast and sets the begin date 0808 * The quantity of forecast days will be counted from this date 0809 * Depends on the values of begin day and accounts cycle 0810 * The rules to calculate begin day are as follows: 0811 * - if beginDay is 0, begin date is current date 0812 * - if the day of the month set by beginDay has not passed, that will be used 0813 * - if adding an account cycle to beginDay, will not go past the beginDay of next month, 0814 * that date will be used, otherwise it will add account cycle to beginDay until it is past current date 0815 * It returns the total amount of Forecast Days from current date. 0816 */ 0817 qint64 calculateBeginForecastDay() 0818 { 0819 Q_Q(MyMoneyForecast); 0820 auto fDays = q->forecastDays(); 0821 auto beginDay = q->beginForecastDay(); 0822 auto accCycle = q->accountsCycle(); 0823 QDate beginDate; 0824 0825 //if 0, beginDate is current date and forecastDays remains unchanged 0826 if (beginDay == 0) { 0827 q->setBeginForecastDate(QDate::currentDate()); 0828 return fDays; 0829 } 0830 0831 //adjust if beginDay more than days of current month 0832 if (QDate::currentDate().daysInMonth() < beginDay) 0833 beginDay = QDate::currentDate().daysInMonth(); 0834 0835 //if beginDay still to come, calculate and return 0836 if (QDate::currentDate().day() <= beginDay) { 0837 beginDate = QDate(QDate::currentDate().year(), QDate::currentDate().month(), beginDay); 0838 fDays += QDate::currentDate().daysTo(beginDate); 0839 q->setBeginForecastDate(beginDate); 0840 return fDays; 0841 } 0842 0843 //adjust beginDay for next month 0844 if (QDate::currentDate().addMonths(1).daysInMonth() < beginDay) 0845 beginDay = QDate::currentDate().addMonths(1).daysInMonth(); 0846 0847 //if beginDay of next month comes before 1 interval, use beginDay next month 0848 if (QDate::currentDate().addDays(accCycle) >= 0849 (QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1).addDays(beginDay - 1))) { 0850 beginDate = QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1).addDays(beginDay - 1); 0851 fDays += QDate::currentDate().daysTo(beginDate); 0852 } else { //add intervals to current beginDay and take the first after current date 0853 beginDay = ((((QDate::currentDate().day() - beginDay) / accCycle) + 1) * accCycle) + beginDay; 0854 beginDate = QDate::currentDate().addDays(beginDay - QDate::currentDate().day()); 0855 fDays += QDate::currentDate().daysTo(beginDate); 0856 } 0857 0858 q->setBeginForecastDate(beginDate); 0859 return fDays; 0860 } 0861 0862 /** 0863 * remove accounts from the list if the accounts has no transactions in the forecast timeframe. 0864 * Used for scheduled-forecast method. 0865 */ 0866 void purgeForecastAccountsList(QMap<QString, dailyBalances>& accountList) 0867 { 0868 m_forecastAccounts.intersect(accountList.keys().toSet()); 0869 } 0870 0871 MyMoneyForecast *q_ptr; 0872 0873 /** 0874 * daily forecast balance of accounts 0875 */ 0876 QMap<QString, dailyBalances> m_accountList; 0877 0878 /** 0879 * daily past balance of accounts 0880 */ 0881 QMap<QString, dailyBalances> m_accountListPast; 0882 0883 /** 0884 * daily forecast trends of accounts 0885 */ 0886 QMap<QString, trendBalances> m_accountTrendList; 0887 0888 /** 0889 * list of forecast account ids. 0890 */ 0891 QSet<QString> m_forecastAccounts; 0892 0893 /** 0894 * cycle of accounts in days 0895 */ 0896 qint64 m_accountsCycle; 0897 0898 /** 0899 * number of cycles to use in forecast 0900 */ 0901 qint64 m_forecastCycles; 0902 0903 /** 0904 * number of days to forecast 0905 */ 0906 qint64 m_forecastDays; 0907 0908 /** 0909 * date to start forecast 0910 */ 0911 QDate m_beginForecastDate; 0912 0913 /** 0914 * day to start forecast 0915 */ 0916 qint64 m_beginForecastDay; 0917 0918 /** 0919 * forecast method 0920 */ 0921 eForecastMethod m_forecastMethod; 0922 0923 /** 0924 * history method 0925 */ 0926 int m_historyMethod; 0927 0928 /** 0929 * start date of history 0930 */ 0931 QDate m_historyStartDate; 0932 0933 /** 0934 * end date of history 0935 */ 0936 QDate m_historyEndDate; 0937 0938 /** 0939 * start date of forecast 0940 */ 0941 QDate m_forecastStartDate; 0942 0943 /** 0944 * end date of forecast 0945 */ 0946 QDate m_forecastEndDate; 0947 0948 /** 0949 * skip opening date when fetching transactions of an account 0950 */ 0951 bool m_skipOpeningDate; 0952 0953 /** 0954 * include accounts with no transactions in the forecast timeframe. default is false. 0955 */ 0956 bool m_includeUnusedAccounts; 0957 0958 /** 0959 * forecast already done 0960 */ 0961 bool m_forecastDone; 0962 0963 /** 0964 * include future transactions when doing a scheduled-based forecast 0965 */ 0966 bool m_includeFutureTransactions; 0967 0968 /** 0969 * include scheduled transactions when doing a scheduled-based forecast 0970 */ 0971 bool m_includeScheduledTransactions; 0972 }; 0973 0974 MyMoneyForecast::MyMoneyForecast() : 0975 d_ptr(new MyMoneyForecastPrivate(this)) 0976 { 0977 setHistoryStartDate(QDate::currentDate().addDays(-forecastCycles()*accountsCycle())); 0978 setHistoryEndDate(QDate::currentDate().addDays(-1)); 0979 } 0980 0981 MyMoneyForecast::MyMoneyForecast(const MyMoneyForecast& other) : 0982 d_ptr(new MyMoneyForecastPrivate(*other.d_func())) 0983 { 0984 this->d_ptr->q_ptr = this; 0985 } 0986 0987 void swap(MyMoneyForecast& first, MyMoneyForecast& second) 0988 { 0989 using std::swap; 0990 swap(first.d_ptr, second.d_ptr); 0991 swap(first.d_ptr->q_ptr, second.d_ptr->q_ptr); 0992 } 0993 0994 MyMoneyForecast::MyMoneyForecast(MyMoneyForecast && other) : MyMoneyForecast() 0995 { 0996 swap(*this, other); 0997 } 0998 0999 MyMoneyForecast & MyMoneyForecast::operator=(MyMoneyForecast other) 1000 { 1001 swap(*this, other); 1002 return *this; 1003 } 1004 1005 MyMoneyForecast::~MyMoneyForecast() 1006 { 1007 Q_D(MyMoneyForecast); 1008 delete d; 1009 } 1010 1011 void MyMoneyForecast::doForecast() 1012 { 1013 Q_D(MyMoneyForecast); 1014 auto fDays = d->calculateBeginForecastDay(); 1015 auto fMethod = d->forecastMethod(); 1016 auto fAccCycle = accountsCycle(); 1017 auto fCycles = forecastCycles(); 1018 1019 //validate settings 1020 if (fAccCycle < 1 1021 || fCycles < 1 1022 || fDays < 1) { 1023 throw MYMONEYEXCEPTION_CSTRING("Illegal settings when calling doForecast. Settings must be higher than 0"); 1024 } 1025 1026 //initialize global variables 1027 setForecastDays(fDays); 1028 setForecastStartDate(QDate::currentDate().addDays(1)); 1029 setForecastEndDate(QDate::currentDate().addDays(fDays)); 1030 setAccountsCycle(fAccCycle); 1031 setForecastCycles(fCycles); 1032 setHistoryStartDate(forecastCycles() * accountsCycle()); 1033 setHistoryEndDate(QDate::currentDate().addDays(-1)); //yesterday 1034 1035 //clear all data before calculating 1036 d->m_accountListPast.clear(); 1037 d->m_accountList.clear(); 1038 d->m_accountTrendList.clear(); 1039 1040 //set forecast accounts 1041 d->setForecastAccountList(); 1042 1043 switch (fMethod) { 1044 case eForecastMethod::Scheduled: 1045 d->doFutureScheduledForecast(); 1046 d->calculateScheduledDailyBalances(); 1047 break; 1048 case eForecastMethod::Historic: 1049 d->pastTransactions(); 1050 d->calculateHistoricDailyBalances(); 1051 break; 1052 default: 1053 break; 1054 } 1055 1056 //flag the forecast as done 1057 d->m_forecastDone = true; 1058 } 1059 1060 bool MyMoneyForecast::isForecastAccount(const MyMoneyAccount& acc) 1061 { 1062 Q_D(MyMoneyForecast); 1063 if (d->m_forecastAccounts.isEmpty()) { 1064 d->setForecastAccountList(); 1065 } 1066 return d->m_forecastAccounts.contains(acc.id()); 1067 } 1068 1069 QList<MyMoneyAccount> MyMoneyForecast::accountList() 1070 { 1071 auto file = MyMoneyFile::instance(); 1072 1073 QList<MyMoneyAccount> accList; 1074 QStringList emptyStringList; 1075 //Get all accounts from the file and check if they are present 1076 file->accountList(accList, emptyStringList, false); 1077 QList<MyMoneyAccount>::iterator accList_t = accList.begin(); 1078 for (; accList_t != accList.end();) { 1079 auto acc = *accList_t; 1080 if (!isForecastAccount(acc)) { 1081 accList_t = accList.erase(accList_t); //remove the account 1082 } else { 1083 ++accList_t; 1084 } 1085 } 1086 return accList; 1087 } 1088 1089 MyMoneyMoney MyMoneyForecast::calculateAccountTrend(const MyMoneyAccount& acc, qint64 trendDays) 1090 { 1091 auto file = MyMoneyFile::instance(); 1092 MyMoneyTransactionFilter filter; 1093 MyMoneyMoney netIncome; 1094 QDate startDate; 1095 QDate openingDate = acc.openingDate(); 1096 1097 //validate arguments 1098 if (trendDays < 1) { 1099 throw MYMONEYEXCEPTION_CSTRING("Illegal arguments when calling calculateAccountTrend. trendDays must be higher than 0"); 1100 } 1101 1102 //If it is a new account, we don't take into account the first day 1103 //because it is usually a weird one and it would mess up the trend 1104 if (openingDate.daysTo(QDate::currentDate()) < trendDays) { 1105 startDate = (acc.openingDate()).addDays(1); 1106 } else { 1107 startDate = QDate::currentDate().addDays(-trendDays); 1108 } 1109 //get all transactions for the period 1110 filter.setDateFilter(startDate, QDate::currentDate()); 1111 if (acc.accountGroup() == eMyMoney::Account::Type::Income // 1112 || acc.accountGroup() == eMyMoney::Account::Type::Expense) { 1113 filter.addCategory(acc.id()); 1114 } else { 1115 filter.addAccount(acc.id()); 1116 } 1117 1118 filter.setReportAllSplits(false); 1119 1120 //add all transactions for that account 1121 foreach (const auto transaction, file->transactionList(filter)) { 1122 foreach (const auto split, transaction.splits()) { 1123 if (!split.shares().isZero()) { 1124 if (acc.id() == split.accountId()) netIncome += split.value(); 1125 } 1126 } 1127 } 1128 1129 //calculate trend of the account in the past period 1130 MyMoneyMoney accTrend; 1131 1132 //don't take into account the first day of the account 1133 if (openingDate.daysTo(QDate::currentDate()) < trendDays) { 1134 accTrend = netIncome / MyMoneyMoney(openingDate.daysTo(QDate::currentDate()) - 1, 1); 1135 } else { 1136 accTrend = netIncome / MyMoneyMoney(trendDays, 1); 1137 } 1138 return accTrend; 1139 } 1140 1141 MyMoneyMoney MyMoneyForecast::forecastBalance(const MyMoneyAccount& acc, const QDate &forecastDate) 1142 { 1143 Q_D(MyMoneyForecast); 1144 dailyBalances balance; 1145 MyMoneyMoney MM_amount = MyMoneyMoney(); 1146 1147 //Check if acc is not a forecast account, return 0 1148 if (!isForecastAccount(acc)) { 1149 return MM_amount; 1150 } 1151 1152 if (d->m_accountList.contains(acc.id())) { 1153 balance = d->m_accountList.value(acc.id()); 1154 } 1155 if (balance.contains(forecastDate)) { //if the date is not in the forecast, it returns 0 1156 MM_amount = balance.value(forecastDate); 1157 } 1158 return MM_amount; 1159 } 1160 1161 /** 1162 * Returns the forecast balance trend for account @a acc for offset @p int 1163 * offset is days from current date, inside forecast days. 1164 * Returns 0 if offset not in range of forecast days. 1165 */ 1166 MyMoneyMoney MyMoneyForecast::forecastBalance(const MyMoneyAccount& acc, qint64 offset) 1167 { 1168 QDate forecastDate = QDate::currentDate().addDays(offset); 1169 return forecastBalance(acc, forecastDate); 1170 } 1171 1172 qint64 MyMoneyForecast::daysToMinimumBalance(const MyMoneyAccount& acc) 1173 { 1174 Q_D(MyMoneyForecast); 1175 QString minimumBalance = acc.value("minBalanceAbsolute"); 1176 MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); 1177 dailyBalances balance; 1178 1179 //Check if acc is not a forecast account, return -1 1180 if (!isForecastAccount(acc)) { 1181 return -1; 1182 } 1183 1184 balance = d->m_accountList[acc.id()]; 1185 1186 for (QDate it_day = QDate::currentDate() ; it_day <= forecastEndDate();) { 1187 if (minBalance > balance[it_day]) { 1188 return QDate::currentDate().daysTo(it_day); 1189 } 1190 it_day = it_day.addDays(1); 1191 } 1192 return -1; 1193 } 1194 1195 qint64 MyMoneyForecast::daysToZeroBalance(const MyMoneyAccount& acc) 1196 { 1197 Q_D(MyMoneyForecast); 1198 dailyBalances balance; 1199 1200 //Check if acc is not a forecast account, return -1 1201 if (!isForecastAccount(acc)) { 1202 return -2; 1203 } 1204 1205 balance = d->m_accountList[acc.id()]; 1206 1207 if (acc.accountGroup() == eMyMoney::Account::Type::Asset) { 1208 for (QDate it_day = QDate::currentDate() ; it_day <= forecastEndDate();) { 1209 if (balance[it_day] < MyMoneyMoney()) { 1210 return QDate::currentDate().daysTo(it_day); 1211 } 1212 it_day = it_day.addDays(1); 1213 } 1214 } else if (acc.accountGroup() == eMyMoney::Account::Type::Liability) { 1215 for (QDate it_day = QDate::currentDate() ; it_day <= forecastEndDate();) { 1216 if (balance[it_day] > MyMoneyMoney()) { 1217 return QDate::currentDate().daysTo(it_day); 1218 } 1219 it_day = it_day.addDays(1); 1220 } 1221 } 1222 return -1; 1223 } 1224 1225 1226 MyMoneyMoney MyMoneyForecast::accountCycleVariation(const MyMoneyAccount& acc) 1227 { 1228 Q_D(MyMoneyForecast); 1229 MyMoneyMoney cycleVariation; 1230 1231 if (d->forecastMethod() == eForecastMethod::Historic) { 1232 switch (historyMethod()) { 1233 case 0: 1234 case 1: { 1235 for (auto t_day = 1; t_day <= accountsCycle() ; ++t_day) { 1236 cycleVariation += d->m_accountTrendList[acc.id()][t_day]; 1237 } 1238 } 1239 break; 1240 case 2: { 1241 cycleVariation = d->m_accountList[acc.id()][QDate::currentDate().addDays(accountsCycle())] - d->m_accountList[acc.id()][QDate::currentDate()]; 1242 break; 1243 } 1244 } 1245 } 1246 return cycleVariation; 1247 } 1248 1249 MyMoneyMoney MyMoneyForecast::accountTotalVariation(const MyMoneyAccount& acc) 1250 { 1251 MyMoneyMoney totalVariation; 1252 1253 totalVariation = forecastBalance(acc, forecastEndDate()) - forecastBalance(acc, QDate::currentDate()); 1254 return totalVariation; 1255 } 1256 1257 QList<QDate> MyMoneyForecast::accountMinimumBalanceDateList(const MyMoneyAccount& acc) 1258 { 1259 QList<QDate> minBalanceList; 1260 qint64 daysToBeginDay; 1261 1262 daysToBeginDay = QDate::currentDate().daysTo(beginForecastDate()); 1263 1264 for (auto t_cycle = 0; ((t_cycle * accountsCycle()) + daysToBeginDay) < forecastDays() ; ++t_cycle) { 1265 MyMoneyMoney minBalance = forecastBalance(acc, (t_cycle * accountsCycle() + daysToBeginDay)); 1266 QDate minDate = QDate::currentDate().addDays(t_cycle * accountsCycle() + daysToBeginDay); 1267 for (auto t_day = 1; t_day <= accountsCycle() ; ++t_day) { 1268 if (minBalance > forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day)) { 1269 minBalance = forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day); 1270 minDate = QDate::currentDate().addDays((t_cycle * accountsCycle()) + daysToBeginDay + t_day); 1271 } 1272 } 1273 minBalanceList.append(minDate); 1274 } 1275 return minBalanceList; 1276 } 1277 1278 QList<QDate> MyMoneyForecast::accountMaximumBalanceDateList(const MyMoneyAccount& acc) 1279 { 1280 QList<QDate> maxBalanceList; 1281 qint64 daysToBeginDay; 1282 1283 daysToBeginDay = QDate::currentDate().daysTo(beginForecastDate()); 1284 1285 for (auto t_cycle = 0; ((t_cycle * accountsCycle()) + daysToBeginDay) < forecastDays() ; ++t_cycle) { 1286 MyMoneyMoney maxBalance = forecastBalance(acc, ((t_cycle * accountsCycle()) + daysToBeginDay)); 1287 QDate maxDate = QDate::currentDate().addDays((t_cycle * accountsCycle()) + daysToBeginDay); 1288 1289 for (auto t_day = 0; t_day < accountsCycle() ; ++t_day) { 1290 if (maxBalance < forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day)) { 1291 maxBalance = forecastBalance(acc, (t_cycle * accountsCycle()) + daysToBeginDay + t_day); 1292 maxDate = QDate::currentDate().addDays((t_cycle * accountsCycle()) + daysToBeginDay + t_day); 1293 } 1294 } 1295 maxBalanceList.append(maxDate); 1296 } 1297 return maxBalanceList; 1298 } 1299 1300 MyMoneyMoney MyMoneyForecast::accountAverageBalance(const MyMoneyAccount& acc) 1301 { 1302 MyMoneyMoney totalBalance; 1303 for (auto f_day = 1; f_day <= forecastDays() ; ++f_day) { 1304 totalBalance += forecastBalance(acc, f_day); 1305 } 1306 return totalBalance / MyMoneyMoney(forecastDays(), 1); 1307 } 1308 1309 void MyMoneyForecast::createBudget(MyMoneyBudget& budget, QDate historyStart, QDate historyEnd, QDate budgetStart, QDate budgetEnd, const bool returnBudget) 1310 { 1311 Q_D(MyMoneyForecast); 1312 // clear all data except the id and name 1313 QString name = budget.name(); 1314 budget = MyMoneyBudget(budget.id(), MyMoneyBudget()); 1315 budget.setName(name); 1316 1317 //check parameters 1318 if (historyStart > historyEnd || 1319 budgetStart > budgetEnd || 1320 budgetStart <= historyEnd) { 1321 throw MYMONEYEXCEPTION_CSTRING("Illegal parameters when trying to create budget"); 1322 } 1323 1324 //get forecast method 1325 auto fMethod = d->forecastMethod(); 1326 1327 //set start date to 1st of month and end dates to last day of month, since we deal with full months in budget 1328 historyStart = QDate(historyStart.year(), historyStart.month(), 1); 1329 historyEnd = QDate(historyEnd.year(), historyEnd.month(), historyEnd.daysInMonth()); 1330 budgetStart = QDate(budgetStart.year(), budgetStart.month(), 1); 1331 budgetEnd = QDate(budgetEnd.year(), budgetEnd.month(), budgetEnd.daysInMonth()); 1332 1333 //set forecast parameters 1334 setHistoryStartDate(historyStart); 1335 setHistoryEndDate(historyEnd); 1336 setForecastStartDate(budgetStart); 1337 setForecastEndDate(budgetEnd); 1338 setForecastDays(budgetStart.daysTo(budgetEnd) + 1); 1339 if (budgetStart.daysTo(budgetEnd) > historyStart.daysTo(historyEnd)) { //if history period is shorter than budget, use that one as the trend length 1340 setAccountsCycle(historyStart.daysTo(historyEnd)); //we set the accountsCycle to the base timeframe we will use to calculate the average (eg. 180 days, 365, etc) 1341 } else { //if one timeframe is larger than the other, but not enough to be 1 time larger, we take the lowest value 1342 setAccountsCycle(budgetStart.daysTo(budgetEnd)); 1343 } 1344 setForecastCycles((historyStart.daysTo(historyEnd) / accountsCycle())); 1345 if (forecastCycles() == 0) //the cycles must be at least 1 1346 setForecastCycles(1); 1347 1348 //do not skip opening date 1349 setSkipOpeningDate(false); 1350 1351 //clear and set accounts list we are going to use. Categories, in this case 1352 d->m_forecastAccounts.clear(); 1353 d->setBudgetAccountList(); 1354 1355 //calculate budget according to forecast method 1356 switch (fMethod) { 1357 case eForecastMethod::Scheduled: 1358 d->doFutureScheduledForecast(); 1359 d->calculateScheduledMonthlyBalances(); 1360 break; 1361 case eForecastMethod::Historic: 1362 d->pastTransactions(); //get all transactions for history period 1363 d->calculateAccountTrendList(); 1364 d->calculateHistoricMonthlyBalances(); //add all balances of each month and put at the 1st day of each month 1365 break; 1366 default: 1367 break; 1368 } 1369 1370 //flag the forecast as done 1371 d->m_forecastDone = true; 1372 1373 //only fill the budget if it is going to be used 1374 if (returnBudget) { 1375 //setup the budget itself 1376 auto file = MyMoneyFile::instance(); 1377 budget.setBudgetStart(budgetStart); 1378 1379 //go through all the accounts and add them to budget 1380 for (auto it_nc = d->m_forecastAccounts.constBegin(); it_nc != d->m_forecastAccounts.constEnd(); ++it_nc) { 1381 auto acc = file->account(*it_nc); 1382 1383 MyMoneyBudget::AccountGroup budgetAcc; 1384 budgetAcc.setId(acc.id()); 1385 budgetAcc.setBudgetLevel(eMyMoney::Budget::Level::MonthByMonth); 1386 1387 for (QDate f_date = forecastStartDate(); f_date <= forecastEndDate();) { 1388 MyMoneyBudget::PeriodGroup period; 1389 1390 //add period to budget account 1391 period.setStartDate(f_date); 1392 period.setAmount(forecastBalance(acc, f_date)); 1393 budgetAcc.addPeriod(f_date, period); 1394 1395 //next month 1396 f_date = f_date.addMonths(1); 1397 } 1398 //add budget account to budget 1399 budget.setAccount(budgetAcc, acc.id()); 1400 } 1401 } 1402 } 1403 qint64 MyMoneyForecast::historyDays() const 1404 { 1405 Q_D(const MyMoneyForecast); 1406 return (d->m_historyStartDate.daysTo(d->m_historyEndDate) + 1); 1407 } 1408 1409 void MyMoneyForecast::setAccountsCycle(qint64 accountsCycle) 1410 { 1411 Q_D(MyMoneyForecast); 1412 d->m_accountsCycle = accountsCycle; 1413 } 1414 1415 void MyMoneyForecast::setForecastCycles(qint64 forecastCycles) 1416 { 1417 Q_D(MyMoneyForecast); 1418 d->m_forecastCycles = forecastCycles; 1419 } 1420 1421 void MyMoneyForecast::setForecastDays(qint64 forecastDays) 1422 { 1423 Q_D(MyMoneyForecast); 1424 d->m_forecastDays = forecastDays; 1425 } 1426 1427 void MyMoneyForecast::setBeginForecastDate(const QDate &beginForecastDate) 1428 { 1429 Q_D(MyMoneyForecast); 1430 d->m_beginForecastDate = beginForecastDate; 1431 } 1432 1433 void MyMoneyForecast::setBeginForecastDay(qint64 beginDay) 1434 { 1435 Q_D(MyMoneyForecast); 1436 d->m_beginForecastDay = beginDay; 1437 } 1438 1439 void MyMoneyForecast::setForecastMethod(qint64 forecastMethod) 1440 { 1441 Q_D(MyMoneyForecast); 1442 d->m_forecastMethod = static_cast<eForecastMethod>(forecastMethod); 1443 } 1444 1445 void MyMoneyForecast::setHistoryStartDate(const QDate &historyStartDate) 1446 { 1447 Q_D(MyMoneyForecast); 1448 d->m_historyStartDate = historyStartDate; 1449 } 1450 1451 void MyMoneyForecast::setHistoryEndDate(const QDate &historyEndDate) 1452 { 1453 Q_D(MyMoneyForecast); 1454 d->m_historyEndDate = historyEndDate; 1455 } 1456 1457 void MyMoneyForecast::setHistoryStartDate(qint64 daysToStartDate) 1458 { 1459 setHistoryStartDate(QDate::currentDate().addDays(-daysToStartDate)); 1460 } 1461 1462 void MyMoneyForecast::setHistoryEndDate(qint64 daysToEndDate) 1463 { 1464 setHistoryEndDate(QDate::currentDate().addDays(-daysToEndDate)); 1465 } 1466 1467 void MyMoneyForecast::setForecastStartDate(const QDate &_startDate) 1468 { 1469 Q_D(MyMoneyForecast); 1470 d->m_forecastStartDate = _startDate; 1471 } 1472 1473 void MyMoneyForecast::setForecastEndDate(const QDate &_endDate) 1474 { 1475 Q_D(MyMoneyForecast); 1476 d->m_forecastEndDate = _endDate; 1477 } 1478 1479 void MyMoneyForecast::setSkipOpeningDate(bool _skip) 1480 { 1481 Q_D(MyMoneyForecast); 1482 d->m_skipOpeningDate = _skip; 1483 } 1484 1485 void MyMoneyForecast::setHistoryMethod(int historyMethod) 1486 { 1487 Q_D(MyMoneyForecast); 1488 d->m_historyMethod = historyMethod; 1489 } 1490 1491 void MyMoneyForecast::setIncludeUnusedAccounts(bool _bool) 1492 { 1493 Q_D(MyMoneyForecast); 1494 d->m_includeUnusedAccounts = _bool; 1495 } 1496 1497 void MyMoneyForecast::setForecastDone(bool _bool) 1498 { 1499 Q_D(MyMoneyForecast); 1500 d->m_forecastDone = _bool; 1501 } 1502 1503 void MyMoneyForecast::setIncludeFutureTransactions(bool _bool) 1504 { 1505 Q_D(MyMoneyForecast); 1506 d->m_includeFutureTransactions = _bool; 1507 } 1508 1509 void MyMoneyForecast::setIncludeScheduledTransactions(bool _bool) 1510 { 1511 Q_D(MyMoneyForecast); 1512 d->m_includeScheduledTransactions = _bool; 1513 } 1514 1515 qint64 MyMoneyForecast::accountsCycle() const 1516 { 1517 Q_D(const MyMoneyForecast); 1518 return d->m_accountsCycle; 1519 } 1520 1521 qint64 MyMoneyForecast::forecastCycles() const 1522 { 1523 Q_D(const MyMoneyForecast); 1524 return d->m_forecastCycles; 1525 } 1526 1527 qint64 MyMoneyForecast::forecastDays() const 1528 { 1529 Q_D(const MyMoneyForecast); 1530 return d->m_forecastDays; 1531 } 1532 1533 QDate MyMoneyForecast::beginForecastDate() const 1534 { 1535 Q_D(const MyMoneyForecast); 1536 return d->m_beginForecastDate; 1537 } 1538 1539 qint64 MyMoneyForecast::beginForecastDay() const 1540 { 1541 Q_D(const MyMoneyForecast); 1542 return d->m_beginForecastDay; 1543 } 1544 1545 QDate MyMoneyForecast::historyStartDate() const 1546 { 1547 Q_D(const MyMoneyForecast); 1548 return d->m_historyStartDate; 1549 } 1550 1551 QDate MyMoneyForecast::historyEndDate() const 1552 { 1553 Q_D(const MyMoneyForecast); 1554 return d->m_historyEndDate; 1555 } 1556 1557 QDate MyMoneyForecast::forecastStartDate() const 1558 { 1559 Q_D(const MyMoneyForecast); 1560 return d->m_forecastStartDate; 1561 } 1562 1563 QDate MyMoneyForecast::forecastEndDate() const 1564 { 1565 Q_D(const MyMoneyForecast); 1566 return d->m_forecastEndDate; 1567 } 1568 1569 bool MyMoneyForecast::skipOpeningDate() const 1570 { 1571 Q_D(const MyMoneyForecast); 1572 return d->m_skipOpeningDate; 1573 } 1574 1575 int MyMoneyForecast::historyMethod() const 1576 { 1577 Q_D(const MyMoneyForecast); 1578 return d->m_historyMethod; 1579 } 1580 1581 bool MyMoneyForecast::isIncludingUnusedAccounts() const 1582 { 1583 Q_D(const MyMoneyForecast); 1584 return d->m_includeUnusedAccounts; 1585 } 1586 1587 bool MyMoneyForecast::isForecastDone() const 1588 { 1589 Q_D(const MyMoneyForecast); 1590 return d->m_forecastDone; 1591 } 1592 1593 bool MyMoneyForecast::isIncludingFutureTransactions() const 1594 { 1595 Q_D(const MyMoneyForecast); 1596 return d->m_includeFutureTransactions; 1597 } 1598 1599 bool MyMoneyForecast::isIncludingScheduledTransactions() const 1600 { 1601 Q_D(const MyMoneyForecast); 1602 return d->m_includeScheduledTransactions; 1603 } 1604 1605 void MyMoneyForecast::calculateAutoLoan(const MyMoneySchedule& schedule, MyMoneyTransaction& transaction, const QMap<QString, MyMoneyMoney>& balances) 1606 { 1607 if (schedule.type() == eMyMoney::Schedule::Type::LoanPayment) { 1608 1609 //get amortization and interest autoCalc splits 1610 MyMoneySplit amortizationSplit = transaction.amortizationSplit(); 1611 MyMoneySplit interestSplit = transaction.interestSplit(); 1612 const bool interestSplitValid = !interestSplit.id().isEmpty(); 1613 1614 if (!amortizationSplit.id().isEmpty()) { 1615 MyMoneyAccountLoan acc(MyMoneyFile::instance()->account(amortizationSplit.accountId())); 1616 MyMoneyFinancialCalculator calc; 1617 QDate dueDate; 1618 1619 // FIXME: setup dueDate according to when the interest should be calculated 1620 // current implementation: take the date of the next payment according to 1621 // the schedule. If the calculation is based on the payment reception, and 1622 // the payment is overdue then take the current date 1623 dueDate = schedule.nextDueDate(); 1624 if (acc.interestCalculation() == MyMoneyAccountLoan::paymentReceived) { 1625 if (dueDate < QDate::currentDate()) 1626 dueDate = QDate::currentDate(); 1627 } 1628 1629 // we need to calculate the balance at the time the payment is due 1630 1631 MyMoneyMoney balance; 1632 if (balances.count() == 0) 1633 balance = MyMoneyFile::instance()->balance(acc.id(), dueDate.addDays(-1)); 1634 else 1635 balance = balances[acc.id()]; 1636 1637 // FIXME: for now, we only support interest calculation at the end of the period 1638 calc.setBep(); 1639 // FIXME: for now, we only support periodic compounding 1640 calc.setDisc(); 1641 1642 calc.setPF(MyMoneySchedule::eventsPerYear(schedule.baseOccurrence())); 1643 eMyMoney::Schedule::Occurrence compoundingOccurrence = static_cast<eMyMoney::Schedule::Occurrence>(acc.interestCompounding()); 1644 if (compoundingOccurrence == eMyMoney::Schedule::Occurrence::Any) 1645 compoundingOccurrence = schedule.baseOccurrence(); 1646 1647 calc.setCF(MyMoneySchedule::eventsPerYear(compoundingOccurrence)); 1648 1649 calc.setPv(balance.toDouble()); 1650 calc.setIr(acc.interestRate(dueDate).abs().toDouble()); 1651 calc.setPmt(acc.periodicPayment().toDouble()); 1652 1653 MyMoneyMoney interest(calc.interestDue(), 100), amortization; 1654 interest = interest.abs(); // make sure it's positive for now 1655 amortization = acc.periodicPayment() - interest; 1656 1657 if (acc.accountType() == eMyMoney::Account::Type::AssetLoan) { 1658 interest = -interest; 1659 amortization = -amortization; 1660 } 1661 1662 amortizationSplit.setShares(amortization); 1663 if (interestSplitValid) 1664 interestSplit.setShares(interest); 1665 1666 // FIXME: for now we only assume loans to be in the currency of the transaction 1667 amortizationSplit.setValue(amortization); 1668 if (interestSplitValid) 1669 interestSplit.setValue(interest); 1670 1671 transaction.modifySplit(amortizationSplit); 1672 if (interestSplitValid) 1673 transaction.modifySplit(interestSplit); 1674 } 1675 } 1676 } 1677 1678 QList<MyMoneyAccount> MyMoneyForecast::forecastAccountList() 1679 { 1680 auto file = MyMoneyFile::instance(); 1681 1682 QList<MyMoneyAccount> accList; 1683 //Get all accounts from the file and check if they are of the right type to calculate forecast 1684 file->accountList(accList); 1685 QList<MyMoneyAccount>::iterator accList_t = accList.begin(); 1686 for (; accList_t != accList.end();) { 1687 auto acc = *accList_t; 1688 if (acc.isClosed() //check the account is not closed 1689 || (!acc.isAssetLiability())) { 1690 //|| (acc.accountType() == eMyMoney::Account::Type::Investment) ) {//check that it is not an Investment account and only include Stock accounts 1691 //remove the account if it is not of the correct type 1692 accList_t = accList.erase(accList_t); 1693 } else { 1694 ++accList_t; 1695 } 1696 } 1697 return accList; 1698 }