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