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 }