[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[lmi-commits] [lmi] odd/gpt c5e5305 5/5: Stash pending 7702 work
From: |
Greg Chicares |
Subject: |
[lmi-commits] [lmi] odd/gpt c5e5305 5/5: Stash pending 7702 work |
Date: |
Tue, 27 Apr 2021 12:13:46 -0400 (EDT) |
branch: odd/gpt
commit c5e53054c79b4d24738e3621281fda42102812e0
Author: Gregory W. Chicares <gchicares@sbcglobal.net>
Commit: Gregory W. Chicares <gchicares@sbcglobal.net>
Stash pending 7702 work
---
gpt_test.cpp | 64 +++++++++-
irc7702.cpp | 396 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
irc7702.hpp | 93 +++++++++++++-
3 files changed, 549 insertions(+), 4 deletions(-)
diff --git a/gpt_test.cpp b/gpt_test.cpp
index 541a04e..26fa89b 100644
--- a/gpt_test.cpp
+++ b/gpt_test.cpp
@@ -30,6 +30,9 @@
#include "ssize_lmi.hpp"
#include "test_tools.hpp"
+#include <cmath> // fabs()
+#include <vector>
+
namespace
{
/// Convert annual mortality rates to monthly.
@@ -99,8 +102,65 @@ class gpt_test
void gpt_test::test_guideline_negative()
{
- std::cout << "watch this space" << std::endl;
- LMI_ASSERT(100 == lmi::ssize(sample_q(0)));
+ int const issue_age = 45;
+ int const length = 100 - 45;
+
+ gpt_vector_parms v_parms =
+ {.prem_load_target = std::vector<double>(length, 0.05)
+ ,.prem_load_excess = std::vector<double>(length, 0.05)
+ ,.policy_fee_monthly = std::vector<double>(length, 5.00)
+ ,.policy_fee_annual = std::vector<double>(length, 0.00)
+ ,.specamt_load_monthly = std::vector<double>(length, 0.00)
+ ,.qab_gio_rate = std::vector<double>(length, 0.00)
+ ,.qab_adb_rate = std::vector<double>(length, 0.00)
+ ,.qab_term_rate = std::vector<double>(length, 0.00)
+ ,.qab_spouse_rate = std::vector<double>(length, 0.00)
+ ,.qab_child_rate = std::vector<double>(length, 0.00)
+ ,.qab_waiver_rate = std::vector<double>(length, 0.00)
+ };
+
+ std::vector<double> i45(length,
i_upper_12_over_12_from_i<double>()(0.045));
+ std::vector<double> i60(length,
i_upper_12_over_12_from_i<double>()(0.060));
+
+ irc7702 z(sample_q(issue_age), i45, i45, i60, i60, v_parms);
+
+ gpt_scalar_parms s_parms_0 =
+ {.f3_bft = 100000.0
+ ,.endt_bft = 100000.0
+ ,.target_prem = 0.0
+ ,.chg_sa_base = 100000.0
+ ,.dbopt_7702 = mce_option1_for_7702
+ ,.issued_today = true
+ };
+ z.initialize_7702(s_parms_0);
+ LMI_TEST(std::fabs( 2074.4029 - z.glp_ ) < 0.01);
+ LMI_TEST(std::fabs( 0.0 - z.cum_glp_) < 0.01);
+ LMI_TEST(std::fabs(24486.3207 - z.gsp_ ) < 0.01);
+ LMI_TEST( C0 == z.forceout_amount_);
+ LMI_TEST( C0 == z.rejected_pmt_ );
+ LMI_TEST( C0 == z.cum_prems_paid_ );
+
+ gpt_scalar_parms s_parms_1 =
+ {.duration = 70 - issue_age
+ ,.f3_bft = 50000.0
+ ,.endt_bft = 50000.0
+ ,.target_prem = 0.0
+ ,.chg_sa_base = 50000.0
+ ,.dbopt_7702 = mce_option1_for_7702
+ ,.issued_today = false
+ };
+ z.enqueue_adj_event(s_parms_1);
+ // Call adjust_guidelines() directly for this test. Normally, a
+ // client would instead call this:
+// z.update(s_parms_1);
+ // but that does additional work beyond what's tested here.
+ z.adjust_guidelines(s_parms_1);
+ LMI_TEST(std::fabs(-1845.9882 - z.glp_ ) < 0.01);
+ LMI_TEST(std::fabs( 0.0 - z.cum_glp_) < 0.01);
+ LMI_TEST(std::fabs(-5267.2627 - z.gsp_ ) < 0.01);
+ LMI_TEST( C0 == z.forceout_amount_);
+ LMI_TEST( C0 == z.rejected_pmt_ );
+ LMI_TEST( C0 == z.cum_prems_paid_ );
}
int test_main(int, char*[])
diff --git a/irc7702.cpp b/irc7702.cpp
index a5cd5e5..ce29a95 100644
--- a/irc7702.cpp
+++ b/irc7702.cpp
@@ -23,4 +23,398 @@
#include "irc7702.hpp"
-// implementation of class irc7702
+#include "assert_lmi.hpp"
+#include "oecumenic_enumerations.hpp" // oenum_glp_or_gsp
+#include "round_to.hpp"
+
+#include <algorithm> // max(), min()
+
+static round_to<double> const round_max_premium(2, r_downward);
+
+irc7702::irc7702
+ (std::vector<double> const& qc
+ ,std::vector<double> const& glp_ic
+ ,std::vector<double> const& glp_ig
+ ,std::vector<double> const& gsp_ic
+ ,std::vector<double> const& gsp_ig
+ ,gpt_vector_parms const& charges
+ )
+ :cf_(qc, glp_ic, glp_ig, gsp_ic, gsp_ig, charges)
+{
+}
+
+/// Set initial guideline premiums.
+///
+/// The parameters used here may not be readily ascertainable when the
+/// constructor executes. If the specified amount is given and an
+/// illustration system is to determine the payment pattern as GLP or
+/// GSP, then the only common complication is that premium loads may
+/// change at a target-premium breakpoint, and a closed-form algebraic
+/// solution is straightforward. But if the specified amount is to be
+/// determined as a function of a given premium amount, then the
+/// calculation is more complicated:
+/// - target premium is generally a (not necessarily simple) function
+/// of specified amount, which is the unknown dependent variable;
+/// - a load per dollar of specified amount might apply only up to
+/// some fixed limit;
+/// - the amount of a QAB such as ADB might equal specified amount,
+/// but only up to some maximum determined by underwriting;
+/// so that the best approach is iterative--and that requires an
+/// instance of this class to be created before the specified amount
+/// is determined.
+///
+/// To support inforce illustrations, several inforce parameters are
+/// passed from an admin-system extract, representing the historical
+/// GPT calculations it has performed. The full history of relevant
+/// transaction could be voluminous and is generally not available;
+/// without it, those parameters cannot be validated here.
+
+void irc7702::initialize_7702(gpt_scalar_parms const& arg_parms)
+{
+ assert_sanity(arg_parms);
+ s_parms_ = arg_parms;
+ if(s_parms_.issued_today)
+ {
+ glp_ = cf_.calculate_premium(oe_glp, s_parms_);
+ gsp_ = cf_.calculate_premium(oe_gsp, s_parms_);
+ }
+ else
+ {
+ glp_ = s_parms_.inforce_glp ;
+ cum_glp_ = s_parms_.inforce_cum_glp ;
+ gsp_ = s_parms_.inforce_gsp ;
+ // 7702 !! The other three /inforce_.*/ parameters are appropriately
+ // double, but this one should be of currency type (and rounding
+ // it either up or down here is wrong):
+ cum_prems_paid_ = round_max_premium.c(s_parms_.inforce_cum_pmt);
+ }
+}
+
+/// Handle an update notification from the client.
+///
+/// It is assumed that the client can call into this server, which
+/// however cannot call back into the client. Therefore, the client
+/// must periodically push a notification.
+///
+/// This implementation is correct for an illustration system that
+/// restricts all changes that might constitute adjustment events to
+/// policy anniversaries only. For an admin system, this function
+/// would be called daily instead of annually, and increment_boy()
+/// would be called only on anniversary.
+///
+/// 7702 !! reconsider this:
+/// A current parameter object is passed as an argument. Arguably
+/// enqueue_adj_event() therefore needs no such argument. At any rate,
+/// that object should have no 'gross_1035' member--the 1035 amount is
+/// an enqueue_exch_1035() argument, and not part of the tableau.
+///
+/// Alternative not pursued: Dispense with queuing; instead, add
+/// two more arguments, and enqueue them here, thus:
+/// enqueue_exch_1035 (queued_exch_1035_amt);
+/// enqueue_prems_paid_decrease(queued_prems_paid_decrement);
+/// enqueue_adj_event (arg_parms);
+/// But that would make the client responsible for assembling those
+/// arguments correctly. It is better for the client simply to send
+/// notifications as the need arises, relying on the server to handle
+/// them correctly--not least because the server can be unit-tested
+/// far more easily than the client.
+
+currency irc7702::update(gpt_scalar_parms const& arg_parms)
+{
+ LMI_ASSERT(arg_parms == queued_parms_);
+
+ if(queued_prems_paid_decrease_)
+ {dequeue_prems_paid_decrease();}
+ else
+ {LMI_ASSERT(C0 == queued_prems_paid_decrement_);}
+
+ if(queued_exch_1035_)
+ {dequeue_exch_1035();}
+ else
+ {LMI_ASSERT(C0 == queued_exch_1035_amt_);}
+
+ if(queued_adj_event_)
+ {dequeue_adj_event();}
+ else
+ {LMI_ASSERT(arg_parms == s_parms_);}
+
+ increment_boy();
+ return force_out();
+}
+
+/// Accept payment up to limit; return any excess.
+///
+/// The excess (if any) is "returned" in the programming sense only,
+/// and not in the accounting sense. If $100 is remitted when only $90
+/// is allowed, then the entire remittance would be rejected by an
+/// actual admin system. In the hypothetical world of illustrations,
+/// the $100 is deemed to have been so rejected and replaced by a $90
+/// remittance.
+///
+/// The "returned" excess is stored in a private data member in order
+/// to complete the tableau, which provides a summary of a set of
+/// transactions for testing and debugging. That member deliberately
+/// has no accessor; clients must use this function's return value
+/// only. That member is zeroed upon entry to this function. Unlike
+/// adjustment events, payments need not be combined--there can be
+/// more than one in a day--so the tableau reflects only the most
+/// recent payment.
+
+currency irc7702::accept_payment(currency payment)
+{
+ rejected_pmt_ = C0;
+
+ if(C0 == payment)
+ {return C0;}
+
+ LMI_ASSERT(C0 < payment);
+ currency const allowed = std::max(C0, guideline_limit() - cum_prems_paid_);
+ currency const accepted = std::min(allowed, payment);
+ rejected_pmt_ = payment - accepted;
+ LMI_ASSERT(C0 <= rejected_pmt_);
+ LMI_ASSERT(accepted + rejected_pmt_ == payment);
+ cum_prems_paid_ += accepted;
+ return rejected_pmt_;
+}
+
+/// Process an adjustment event.
+///
+/// A = guideline premium before change
+/// B = guideline premium at attained age for new f3_bft and new dbo
+/// C = guideline premium at attained age for old f3_bft and old dbo
+/// New guideline premium = A + B - C
+///
+/// As '7702.html' explains, the endowment benefit
+/// "is reset to the new SA upon each adjustment event, but only
+/// with respect to the seven-pay premium and the quantity B in
+/// in the A + B - C formula (ΒΆ5/4); the quantities A and C use
+/// the SA immediately prior to the adjustment event."
+/// Because gpt_scalar_parms::endt_bft specifies the endowment
+/// benefit, it is not necessary to know the specified amount here.
+///
+/// Similarly, because gpt_scalar_parms::f3_bft specifies the
+/// 7702(f)(3) benefit, the client can choose whether that means
+/// death benefit (recommended) or specified amount--that choice
+/// is not made here.
+
+void irc7702::adjust_guidelines(gpt_scalar_parms const& arg_parms)
+{
+ // 7702 !! maintain a local duration, to validate s_parms.duration?
+
+ assert_sanity(arg_parms);
+ // There can be no adjustment event on the issue date.
+ LMI_ASSERT(!arg_parms.issued_today);
+ LMI_ASSERT(arg_parms == queued_parms_);
+
+ gpt_scalar_parms const b_parms = arg_parms;
+
+ gpt_scalar_parms c_parms = s_parms_;
+ c_parms.duration = b_parms.duration;
+ c_parms.issued_today = b_parms.issued_today;
+
+ s_parms_ = arg_parms;
+
+ double const glp_a = glp_;
+ double const gsp_a = gsp_;
+ double const glp_b = cf_.calculate_premium(oe_glp, b_parms);
+ double const gsp_b = cf_.calculate_premium(oe_gsp, b_parms);
+ double const glp_c = cf_.calculate_premium(oe_glp, c_parms);
+ double const gsp_c = cf_.calculate_premium(oe_gsp, c_parms);
+
+ glp_ = glp_a + glp_b - glp_c;
+ gsp_ = gsp_a + gsp_b - gsp_c;
+}
+
+/// Update cumulative guideline level premium on anniversary.
+///
+/// This implementation is correct for an illustration system that
+/// restricts all changes that might constitute adjustment events to
+/// policy anniversaries only. For an admin system, the effect of
+/// adjustment events would be prorated.
+///
+/// The accumulation of GLP here is the reason why guideline-premium
+/// data members are of type double rather than currency. If, say,
+/// GLP is $50.00999, then after twenty years the sum is $1000.19
+/// after rounding, as opposed to only $1000.00 if GLP were rounded.
+/// Both the benefit and the cost may seem immaterial, but there are
+/// two strong reasons for preferring the more precise calculation:
+/// - This reference implementation may be used to validate another
+/// system; the GPT is a bright-line test, and it would be wrong to
+/// deem the other system incorrect just because it is precise.
+/// - Retaining all available precision likewise facilitates testing
+/// this code against manual spreadsheet calculations--agreement to
+/// ten significant digits, say, is a more powerful witness to
+/// accuracy than agreement to four.
+
+void irc7702::increment_boy()
+{
+ // 7702 !! pass duration as an argument for consistency testing?
+ ++s_parms_.duration;
+ s_parms_.issued_today = false;
+ cum_glp_ += glp_;
+}
+
+/// Enqueue a 1035 exchange, storing the gross amount of the exchange.
+///
+/// Asserted preconditions:
+/// - No other 1035 exchange has been queued. In the rare case that
+/// several policies are exchanged for one, the client is assumed
+/// to have combined them.
+/// - The exchange amount is nonnegative.
+/// - The exchange occurs as of the issue date.
+///
+/// The exchange amount is required to be nonnegative, as negative
+/// exchanges seem never to occur in practice. A 1035 exchange carries
+/// over the basis, which may be advantageous even if the exchanged
+/// amount is arbitrarily low or perhaps even zero.
+
+void irc7702::enqueue_exch_1035(currency exch_amt)
+{
+ LMI_ASSERT(!queued_exch_1035_);
+ LMI_ASSERT(C0 == queued_exch_1035_amt_);
+ LMI_ASSERT(C0 <= exch_amt);
+ LMI_ASSERT(s_parms_.issued_today);
+ queued_exch_1035_ = true;
+ queued_exch_1035_amt_ = exch_amt;
+}
+
+/// Enqueue a decrease in premiums paid, storing the decrement.
+///
+/// Asserted preconditions:
+/// - No other such decrease has been queued.
+/// - The decrement is positive.
+/// - The decrease doesn't occur on the issue date.
+///
+/// The contemplated purpose is to net nontaxable withdrawals against
+/// premiums paid (the client being responsible for determining the
+/// extent to which they're nontaxable). This function could also
+/// handle exogenous events that decrease premiums paid, such as a
+/// payment returned to preserve a non-MEC, but it is assumed that no
+/// such payment need be returned because an admin system would refuse
+/// to accept it. If it is desired to accept multiple decrements, this
+/// code would need to be modified to accumulate them.
+
+void irc7702::enqueue_prems_paid_decrease(currency decrement)
+{
+ LMI_ASSERT(!queued_prems_paid_decrease_);
+ LMI_ASSERT(C0 == queued_prems_paid_decrement_);
+ LMI_ASSERT(C0 < decrement);
+ LMI_ASSERT(!s_parms_.issued_today);
+ queued_prems_paid_decrease_ = true;
+ queued_prems_paid_decrement_ = decrement;
+}
+
+/// Enqueue a potential adjustment event.
+///
+/// Multiple adjustment events occurring on the same day must be
+/// combined together and processed as one single change. In the
+/// A + B - C formula, only the respective sets of arguments to
+/// calculate_premium() matter. A's are already known. B's are
+/// the same as A's except that the current duration is used. C's
+/// simply represent the final state resulting from all changes
+/// taken together, so they're just a snapshot of the applicable
+/// arguments as of the moment before the combined change is
+/// processed. Therefore, if multiple events occur asynchronously,
+/// it is appropriate and correct to store a single snapshot of
+/// C's arguments, overwriting any previously stored.
+
+void irc7702::enqueue_adj_event(gpt_scalar_parms const& arg_parms)
+{
+ assert_sanity(arg_parms);
+ queued_parms_ = arg_parms;
+ queued_adj_event_ = true;
+}
+
+currency irc7702::rounded_glp () const
+{
+ return round_max_premium.c(glp_);
+}
+
+currency irc7702::rounded_cum_glp () const
+{
+ return round_max_premium.c(cum_glp_);
+}
+
+currency irc7702::rounded_gsp () const
+{
+ return round_max_premium.c(gsp_);
+}
+
+currency irc7702::cum_prems_paid () const
+{
+ return cum_prems_paid_;
+}
+
+/// Dequeue a 1035 exchange.
+///
+/// Add the exchanged amount to cumulative premiums paid.
+///
+/// Asserted preconditions:
+/// - The exchange occurs as of the issue date.
+/// - Cumulative premiums paid equals zero.
+/// - The exchange amount is nonnegative.
+/// - The exchange amount does not exceed the guideline limit.
+
+void irc7702::dequeue_exch_1035()
+{
+ LMI_ASSERT(s_parms_.issued_today);
+ LMI_ASSERT(0 == s_parms_.duration);
+ LMI_ASSERT(C0 == cum_prems_paid_);
+ LMI_ASSERT(C0 <= queued_exch_1035_amt_);
+ LMI_ASSERT(queued_exch_1035_amt_ <= guideline_limit());
+ cum_prems_paid_ += queued_exch_1035_amt_;
+ queued_exch_1035_ = false;
+ queued_exch_1035_amt_ = C0;
+}
+
+/// Dequeue a decrease in premiums paid.
+///
+/// Subtract the decrement from cumulative premiums paid.
+///
+/// Asserted preconditions:
+/// - The decrease doesn't occur on the issue date.
+/// - The decrement is positive.
+
+void irc7702::dequeue_prems_paid_decrease()
+{
+ LMI_ASSERT(!s_parms_.issued_today);
+ LMI_ASSERT(C0 < queued_prems_paid_decrement_);
+ cum_prems_paid_ -= queued_prems_paid_decrement_;
+ queued_prems_paid_decrease_ = false;
+ queued_prems_paid_decrement_ = C0;
+}
+
+/// Dequeue a potential adjustment event.
+///
+/// Delegate the real work to adjust_guidelines().
+
+void irc7702::dequeue_adj_event()
+{
+ adjust_guidelines(queued_parms_);
+ queued_adj_event_ = false;
+}
+
+/// Force money out of the contract to the extent necessary.
+///
+/// The amount forced out is stored in a private data member in order
+/// to complete the tableau, which provides a summary of a set of
+/// transactions for testing and debugging. That member deliberately
+/// has no accessor; clients must use this function's return value
+/// only. That member is zeroed upon entry to this function.
+
+currency irc7702::force_out()
+{
+ forceout_amount_ = C0;
+
+ if(cum_prems_paid_ <= guideline_limit())
+ {return C0;}
+
+ forceout_amount_ = cum_prems_paid_ - guideline_limit();
+ cum_prems_paid_ -= forceout_amount_;
+ return forceout_amount_;
+}
+
+currency irc7702::guideline_limit() const
+{
+ return round_max_premium.c(std::max(cum_glp_, gsp_)); // r_downward
+}
diff --git a/irc7702.hpp b/irc7702.hpp
index d63fe82..e006e59 100644
--- a/irc7702.hpp
+++ b/irc7702.hpp
@@ -24,8 +24,99 @@
#include "config.hpp"
-class irc7702
+#include "currency.hpp"
+#include "gpt_commutation_functions.hpp"
+
+#include <vector>
+
+// https://lists.nongnu.org/archive/html/lmi/2014-06/msg00002.html
+//
+// ---- triggers ---- | -------------- data ---------------
+// queue queue queue | cum
+// prems adjust pos | cum rejected prems
+// paid- event pmt | GLP GLP GSP forceout pmt paid
+// -----------------------------------------------------------------------
+// non-1035 issue - - - | - - - - - -
+// 1035 issue - - t | - - - - - -
+// dbo change - t - | - - - - - -
+// specamt change - t - | - - - - - -
+// withdrawal t t - | - - - - - -
+// -----------------------------------------------------------------------
+// initialization - - - | i i i - - i
+// GPT adjustment - - - | u u u - - -
+// march of time - - - | r u - - - -
+// decr prems paid - - - | - - - - - u
+// forceout - - - | - r r w - u
+// new premium - - - | - r r - w u
+
+class irc7702 // 7702 !! this should be a base class
{
+ friend class gpt_test;
+
+ public:
+ irc7702
+ (std::vector<double> const& qc
+ ,std::vector<double> const& glp_ic
+ ,std::vector<double> const& glp_ig
+ ,std::vector<double> const& gsp_ic
+ ,std::vector<double> const& gsp_ig
+ ,gpt_vector_parms const& charges
+ );
+
+ void initialize_7702 (gpt_scalar_parms const&);
+
+ currency update (gpt_scalar_parms const&);
+
+ // return amount rejected
+ currency accept_payment (currency);
+
+ private: // 7702 !! or perhaps protected?
+ void adjust_guidelines (gpt_scalar_parms const&);
+ void increment_boy ();
+
+ // queue notifications
+ void enqueue_exch_1035 (currency);
+ void enqueue_prems_paid_decrease(currency);
+ void enqueue_adj_event (gpt_scalar_parms const&);
+
+ // const accessors
+ currency rounded_glp () const;
+ currency rounded_cum_glp () const;
+ currency rounded_gsp () const;
+ currency cum_prems_paid () const;
+
+ private:
+ void dequeue_exch_1035 ();
+ void dequeue_prems_paid_decrease();
+ void dequeue_adj_event ();
+
+ currency force_out();
+
+ currency guideline_limit () const;
+
+ // unchangeable basis of calculations (all vector)
+ gpt_cf_triad cf_;
+
+ // changeable policy status (all scalar)
+ gpt_scalar_parms s_parms_ {};
+
+ // queued data
+ gpt_scalar_parms queued_parms_ {};
+ currency queued_exch_1035_amt_ {C0};
+ currency queued_prems_paid_decrement_ {C0};
+
+ // tableau data
+ double glp_ {0};
+ double cum_glp_ {0};
+ double gsp_ {0};
+ currency forceout_amount_ {C0};
+ currency rejected_pmt_ {C0};
+ currency cum_prems_paid_ {C0};
+
+ // queued agenda
+ bool queued_exch_1035_ {false};
+ bool queued_prems_paid_decrease_ {false};
+ bool queued_adj_event_ {false};
};
#endif // irc7702_hpp