[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[lmi-commits] [lmi] odd/gpt2 59270a2: Stash pending 7702 work
From: |
Greg Chicares |
Subject: |
[lmi-commits] [lmi] odd/gpt2 59270a2: Stash pending 7702 work |
Date: |
Tue, 11 May 2021 08:13:15 -0400 (EDT) |
branch: odd/gpt2
commit 59270a2f886cac3f7e77dbef478209f3cf6d42d2
Author: Gregory W. Chicares <gchicares@sbcglobal.net>
Commit: Gregory W. Chicares <gchicares@sbcglobal.net>
Stash pending 7702 work
---
gpt_test.cpp | 351 ++++++++++++++++++++++++++++++++++++++++++-
irc7702.cpp | 483 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
irc7702.hpp | 109 ++++++++++++++
3 files changed, 940 insertions(+), 3 deletions(-)
diff --git a/gpt_test.cpp b/gpt_test.cpp
index 541a04e..d729435 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.
@@ -65,13 +68,147 @@ class gpt_test
public:
static void test()
{
+ test_sync();
+ test_1035();
test_guideline_negative();
+ test_7702_f_6();
}
private:
+ static void test_sync();
+ static void test_1035();
static void test_guideline_negative();
+ static void test_7702_f_6();
};
+void gpt_test::test_sync()
+{
+ int const issue_age = 35;
+ int const length = 100 - issue_age;
+
+ gpt_vector_parms v_parms =
+ {.prem_load_target = std::vector<double>(length, 0.00)
+ ,.prem_load_excess = std::vector<double>(length, 0.00)
+ ,.policy_fee_monthly = std::vector<double>(length, 0.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> i20(length,
i_upper_12_over_12_from_i<double>()(0.020));
+ std::vector<double> i40(length,
i_upper_12_over_12_from_i<double>()(0.040));
+
+ irc7702 z(sample_q(issue_age), i20, i20, i40, i40, v_parms);
+
+ gpt_scalar_parms s_parms_0 =
+ {.duration = 0
+ ,.f3_bft = 100000.0
+ ,.endt_bft = 100000.0
+ ,.target_prem = 0.0
+ ,.chg_sa_base = 100000.0
+ ,.dbopt_7702 = mce_option1_for_7702
+ };
+
+ z.initialize_7702
+ (mce_gpt // defn_life_ins
+ ,true // issued_today
+ ,0.0 // inforce_glp
+ ,0.0 // inforce_cum_glp
+ ,0.0 // inforce_gsp
+ ,C0 // inforce_cum_pmt
+ ,s_parms_0
+ );
+ // 7702 !! Should it be permissible to initialize twice?
+ z.initialize_7702
+ (mce_gpt // defn_life_ins
+ ,0.0 // fractional_duration
+ ,0.0 // inforce_glp
+ ,0.0 // inforce_cum_glp
+ ,0.0 // inforce_gsp
+ ,C0 // inforce_cum_pmt
+ ,s_parms_0
+ );
+ z.update(s_parms_0, C0);
+ // 7702 !! Shouldn't it be forbidden to update twice in the same day?
+ z.update(s_parms_0, C0);
+#if 0
+ LMI_TEST_THROW
+ (z.update(s_parms_0, 0.0, C0);
+ ,std::runtime_error
+ ,"Assertion 'cum_prems_paid_ <= guideline_limit()' failed."
+ );
+#endif // 0
+}
+
+void gpt_test::test_1035()
+{
+ int const issue_age = 35;
+ int const length = 100 - issue_age;
+
+ gpt_vector_parms v_parms =
+ {.prem_load_target = std::vector<double>(length, 0.00)
+ ,.prem_load_excess = std::vector<double>(length, 0.00)
+ ,.policy_fee_monthly = std::vector<double>(length, 0.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> i20(length,
i_upper_12_over_12_from_i<double>()(0.020));
+ std::vector<double> i40(length,
i_upper_12_over_12_from_i<double>()(0.040));
+
+ irc7702 z(sample_q(issue_age), i20, i20, i40, i40, v_parms);
+
+ gpt_scalar_parms s_parms_0 =
+ {.duration = 0
+ ,.f3_bft = 100000.0
+ ,.endt_bft = 100000.0
+ ,.target_prem = 0.0
+ ,.chg_sa_base = 100000.0
+ ,.dbopt_7702 = mce_option1_for_7702
+ };
+
+ z.initialize_7702
+ (mce_gpt // defn_life_ins
+ ,true // issued_today
+ ,0.0 // inforce_glp
+ ,0.0 // inforce_cum_glp
+ ,0.0 // inforce_gsp
+ ,C0 // inforce_cum_pmt
+ ,s_parms_0
+ );
+ LMI_TEST(std::fabs( 1799.8355 - z.glp_ ) < 0.01);
+ LMI_TEST(std::fabs( 0.0 - z.cum_glp_) < 0.01);
+ LMI_TEST(std::fabs(25136.3867 - z.gsp_ ) < 0.01);
+ LMI_TEST( C0 == z.forceout_amount_);
+ LMI_TEST( C0 == z.rejected_pmt_ );
+ LMI_TEST( C0 == z.cum_prems_paid_ );
+ // A $30,000.00 exchange is too high.
+ LMI_TEST_THROW
+ (z.enqueue_exch_1035(30'000'00_cents);
+ ,std::runtime_error
+ ,"Assertion 'exch_amt <= guideline_limit()' failed."
+ );
+ z.enqueue_exch_1035(20'000'00_cents);
+ z.update(s_parms_0, C0);
+ LMI_TEST(std::fabs( 1799.8355 - z.glp_ ) < 0.01);
+ LMI_TEST(std::fabs( 0.0 - z.cum_glp_) < 0.01);
+ LMI_TEST(std::fabs(25136.3867 - z.gsp_ ) < 0.01);
+ LMI_TEST( C0 == z.forceout_amount_);
+ LMI_TEST( C0 == z.rejected_pmt_ );
+ LMI_TEST(20'000'00_cents == z.cum_prems_paid_ );
+}
+
/// Validate a guideline-negative example.
///
/// Example similar to SOA textbook, page 101, Table V-4.
@@ -99,8 +236,218 @@ 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 - issue_age;
+
+ 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 =
+ {.duration = 0
+ ,.f3_bft = 100000.0
+ ,.endt_bft = 100000.0
+ ,.target_prem = 0.0
+ ,.chg_sa_base = 100000.0
+ ,.dbopt_7702 = mce_option1_for_7702
+ };
+ z.initialize_7702
+ (mce_gpt // defn_life_ins
+ ,true // issued_today
+ ,0.0 // inforce_glp
+ ,0.0 // inforce_cum_glp
+ ,0.0 // inforce_gsp
+ ,C0 // inforce_cum_pmt
+ ,s_parms_0
+ );
+ z.update(s_parms_0, C0);
+ 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_ );
+
+ for(int j = 45; j < 70; ++j)
+ {
+ ++s_parms_0.duration;
+ z.update(s_parms_0, 20'000'00_cents);
+ }
+ LMI_TEST(std::fabs(z.cum_glp_ - 25 * z.glp_) < 0.01);
+ LMI_TEST(std::fabs(51860.0721 - z.cum_glp_) < 0.01);
+
+ 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
+ };
+ z.enqueue_adj_event();
+ // Call adjust_guidelines() directly for this test. Normally, a
+ // client would instead call this:
+// z.update(s_parms_1, 20'000'00_cents);
+ // but a unit test can call this isolated private member function,
+ // which does nothing other than what its name implies.
+ z.adjust_guidelines(s_parms_1);
+ LMI_TEST(std::fabs(-1845.9882 - z.glp_ ) < 0.01);
+ LMI_TEST(std::fabs(51860.0721 - 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_ );
+#if 0
+std::cout
+ << std::fixed << std::setprecision(4)
+ << z.glp_ << " z.glp_\n"
+ << z.cum_glp_ << " z.cum_glp_\n"
+ << z.gsp_ << " z.gsp_\n"
+ << z.guideline_limit() << " z.guideline_limit()\n"
+ << z.forceout_amount_ << " z.forceout_amount_\n"
+ << z.cum_prems_paid_ << " z.cum_prems_paid_\n"
+ << std::endl
+ ;
+#endif // 0
+}
+
+void gpt_test::test_7702_f_6()
+{
+ int const issue_age = 45;
+ int const length = 100 - issue_age;
+
+ gpt_vector_parms v_parms =
+ {.prem_load_target = std::vector<double>(length, 0.00)
+ ,.prem_load_excess = std::vector<double>(length, 0.00)
+ ,.policy_fee_monthly = std::vector<double>(length, 0.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 =
+ {.duration = 17
+ ,.f3_bft = 100000.0
+ ,.endt_bft = 100000.0
+ ,.target_prem = 0.0
+ ,.chg_sa_base = 100000.0
+ ,.dbopt_7702 = mce_option1_for_7702
+ };
+
+ // Inforce arguments demonstrate failure.
+ LMI_TEST_THROW
+ (z.initialize_7702
+ (mce_gpt // defn_life_ins
+ ,false // issued_today
+ , -1000.0 // inforce_glp
+ ,-10000.0 // inforce_cum_glp
+ ,-13000.0 // inforce_gsp
+ ,C0 // inforce_cum_pmt
+ ,s_parms_0
+ )
+ ,std::runtime_error
+ ,"Assertion 'cum_prems_paid_ <= guideline_limit()' failed."
+ );
+
+ // With the same starting assumption, but a valid inforce
+ // cumulative (c)(1) premiums paid value, no exception is thrown.
+ currency const c{-10000'00_cents};
+ z.initialize_7702
+ (mce_gpt // defn_life_ins
+ ,false // issued_today
+ , -1000.0 // inforce_glp
+ ,-10000.0 // inforce_cum_glp
+ ,-13000.0 // inforce_gsp
+ ,c // inforce_cum_pmt
+ ,s_parms_0
+ );
+ z.update(s_parms_0, C0);
+ LMI_TEST_EQUAL( -1000'00_cents, z.rounded_glp() );
+ LMI_TEST_EQUAL(-10000'00_cents, z.rounded_cum_glp());
+ LMI_TEST_EQUAL(-13000'00_cents, z.rounded_gsp() );
+ LMI_TEST_EQUAL( 0_cents, z.forceout_amount_ );
+ LMI_TEST_EQUAL( 0_cents, z.rejected_pmt_ );
+ LMI_TEST_EQUAL(-10000'00_cents, z.cum_prems_paid_ );
+ LMI_TEST_EQUAL(-10000'00_cents, z.guideline_limit());
+ // In the real world, this contract would probably be maintained
+ // in force as ART under 7702(f)(6), or exchanged, or perhaps even
+ // surrendered. Yet its owner could choose to increase the
+ // specified amount, making the guideline limit positive.
+ gpt_scalar_parms s_parms_1 =
+ {.duration = 18
+ ,.f3_bft = 200000.0
+ ,.endt_bft = 200000.0
+ ,.target_prem = 0.0
+ ,.chg_sa_base = 50000.0
+ ,.dbopt_7702 = mce_option1_for_7702
+ };
+ z.enqueue_adj_event();
+ currency forceout = z.update(s_parms_1, C0);
+ // f2A value was zero, limiting the forceout to zero.
+ LMI_TEST_EQUAL(C0, forceout);
+ LMI_TEST_EQUAL( 3943'37_cents, z.rounded_glp() );
+ LMI_TEST_EQUAL( -6056'63_cents, z.rounded_cum_glp());
+ LMI_TEST_EQUAL( 32424'69_cents, z.rounded_gsp() );
+ LMI_TEST_EQUAL( 0_cents, z.forceout_amount_ );
+ LMI_TEST_EQUAL( 0_cents, z.rejected_pmt_ );
+ LMI_TEST_EQUAL(-10000'00_cents, z.cum_prems_paid_ );
+ // That may be unwise, but because it is allowed, the server must
+ // still perform the calculations correctly.
+ //
+ // What happens if a forceout amount exceeded the account value,
+ // so that less than the full amount was actually distributed?
+ // The guideline limit is of course equal to the GSP now...
+ LMI_TEST_EQUAL( 32424'69_cents, z.guideline_limit());
+ LMI_TEST_EQUAL(z.rounded_gsp() , z.guideline_limit());
+ // ...but, supposing that only half of the $10000.00 forceout
+ // amount was actually distributed, what premium can now be paid?
+ currency const limit = z.guideline_limit();
+ currency const allowed = std::max(C0, limit - z.cum_prems_paid_);
+ currency const allowed0 = std::max(C0, limit - -10000'00_cents);
+ currency const allowed1 = std::max(C0, limit - - 5000'00_cents);
+ LMI_TEST_EQUAL( 42424'69_cents, allowed );
+ LMI_TEST_EQUAL( 42424'69_cents, allowed0);
+ LMI_TEST_EQUAL( 37424'69_cents, allowed1);
+#if 0
+std::cout
+ << std::fixed << std::setprecision(4)
+ << z.glp_ << " z.glp_\n"
+ << z.cum_glp_ << " z.cum_glp_\n"
+ << z.gsp_ << " z.gsp_\n"
+ << z.guideline_limit() << " z.guideline_limit()\n"
+ << z.forceout_amount_ << " z.forceout_amount_\n"
+ << z.cum_prems_paid_ << " z.cum_prems_paid_\n"
+ << allowed << " allowed\n"
+ << allowed0 << " allowed0\n"
+ << allowed1 << " allowed1\n"
+ << std::endl
+ ;
+#endif // 0
}
int test_main(int, char*[])
diff --git a/irc7702.cpp b/irc7702.cpp
index a5cd5e5..0e24d22 100644
--- a/irc7702.cpp
+++ b/irc7702.cpp
@@ -23,4 +23,485 @@
#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.
+///
+/// Initial GLP and GSP may be wanted even for CVAT contracts, e.g. to
+/// illustrate a premium pattern such as "GSP for one year, then zero"
+/// for both GPT and CVAT. 'defn_life_ins' facilitates skipping GPT
+/// restrictions and adjustment for CVAT contracts in such a use case.
+///
+/// Asserted preconditions:
+/// - if the policy was issued today, its duration must be zero
+/// - if duration != 0, the policy cannot have been issued today
+/// - inforce arguments are all zero if the policy is issued today
+/// The values of 'inforce_.*' arguments are otherwise unrestricted.
+///
+/// Asserted postcondition:
+/// - the guideline limit is not violated
+///
+/// The argument (and members) of type gpt_scalar_parms are parameter
+/// objects that are forwarded to calculate_premium(), which asserts
+/// appropriate preconditions for them.
+
+void irc7702::initialize_7702
+ (mcenum_defn_life_ins defn_life_ins
+ ,bool issued_today
+ ,double inforce_glp
+ ,double inforce_cum_glp
+ ,double inforce_gsp
+ ,currency inforce_cum_pmt
+ ,gpt_scalar_parms const& arg_parms
+ )
+{
+ defn_life_ins_ = defn_life_ins;
+ issued_today_ = issued_today;
+
+ if(issued_today_)
+ {
+ LMI_ASSERT(0 == arg_parms.duration);
+ }
+ if(0 != arg_parms.duration)
+ {
+ LMI_ASSERT(!issued_today_);
+ }
+
+ s_parms_ = arg_parms;
+
+ if(issued_today_)
+ {
+ LMI_ASSERT(0.0 == inforce_glp );
+ LMI_ASSERT(0.0 == inforce_cum_glp);
+ LMI_ASSERT(0.0 == inforce_gsp );
+ LMI_ASSERT(C0 == inforce_cum_pmt);
+ glp_ = cf_.calculate_premium(oe_glp, s_parms_);
+ gsp_ = cf_.calculate_premium(oe_gsp, s_parms_);
+ }
+ else
+ {
+ // 7702 !! Assume that a client provides unrounded values for
+ // arguments of type 'double'. If it provides rounded values,
+ // they may need to be "unrounded" somehow (perhaps, e.g., by
+ // substituting the next representable value toward positive
+ // infinity). Alternatively, properly rounded values could be
+ // passed to this function as objects of class currency.
+ glp_ = inforce_glp ;
+ cum_glp_ = inforce_cum_glp;
+ gsp_ = inforce_gsp ;
+ cum_prems_paid_ = inforce_cum_pmt;
+ }
+
+ LMI_ASSERT(cum_prems_paid_ <= guideline_limit());
+}
+
+/// 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.
+///
+/// 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, currency f2A_value)
+{
+ bool must_increment_duration {arg_parms.duration != s_parms_.duration};
+ if(must_increment_duration)
+ {
+ ++s_parms_.duration;
+ LMI_ASSERT(arg_parms.duration == s_parms_.duration);
+ }
+
+ 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(arg_parms);}
+ else
+ {LMI_ASSERT(arg_parms == s_parms_);}
+
+ // 7702 !! An admin system would pass either the calendar date or
+ // a fractional duration such as 11 + 23/366, from which the value
+ // of 'issued_today_' could be set. For illustrations only,
+ // everything comes out right if the flag is turned off here,
+ // after all queued GPT transactions have been processed--even
+ // though the day hasn't actually changed, it has ended for GPT
+ // purposes.
+ issued_today_ = false;
+ if(must_increment_duration)
+ {increment_boy();}
+ return force_out(f2A_value);
+}
+
+/// 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_;
+}
+
+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_;
+}
+
+/// 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) 'death 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)
+{
+ // There can be no adjustment event on the issue date.
+ LMI_ASSERT(!issued_today_);
+
+ gpt_scalar_parms const b_parms = arg_parms;
+
+ gpt_scalar_parms c_parms = s_parms_;
+ c_parms.duration = b_parms.duration;
+
+ 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()
+{
+ 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 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.
+///
+/// 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(issued_today_);
+ LMI_ASSERT(0 == s_parms_.duration);
+ LMI_ASSERT(C0 == cum_prems_paid_);
+ LMI_ASSERT(C0 <= exch_amt);
+ LMI_ASSERT(exch_amt <= guideline_limit());
+ 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(!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 would be appropriate and correct to store a single snapshot
+/// of C's arguments here, overwriting any previously stored. An
+/// early version of this code did exactly that, producing a nice
+/// symmetry: every /enqueue_.*()/ function took an argument and
+/// stored it in a data member, and every /dequeue_.*/ function
+/// used that stored member.
+///
+/// However, that proved to be needlessly complicated. Each of the
+/// other /enqueue_.*/ functions stores only one one scalar datum,
+/// whereas this one stored a gpt_scalar_parms object, and required
+/// its caller(s) to pass that entire object. But update() takes a
+/// a gpt_scalar_parms argument, as is appropriate to ensure that no
+/// adjustment is missed. It seemed that this update() argument and
+/// the queued gpt_scalar_parms data member should be identical, but
+/// an assertion to that effect was observed to throw when update()
+/// was called in a new year with no adjustment event. Adding extra
+/// code to conditionalize the assertion was unreasonable: the
+/// superfluous data member imposed too much overhead for no benefit.
+
+void irc7702::enqueue_adj_event()
+{
+ LMI_ASSERT(!issued_today_);
+ queued_adj_event_ = true;
+}
+
+/// 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(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(!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(gpt_scalar_parms const& arg_parms)
+{
+ LMI_ASSERT(!issued_today_);
+ adjust_guidelines(arg_parms);
+ queued_adj_event_ = false;
+}
+
+/// Force money out to the extent necessary and possible.
+///
+/// If 7702(f)(1)(A) 'premiums paid' exceeds the guideline limit, any
+/// excess is forced out of the contract. If that excess is greater
+/// than the 7702(f)(2)(A) value (akin, but not identical, to account
+/// value), then the entire 7702(f)(2)(A) value is forced out.
+///
+/// (In that case, 7702(f)(6) does offer the option of maintaining the
+/// policy in force with a strictly zero 7702(f)(2)(A) value by making
+/// bare minimum payments in excess of the guideline limit. If that
+/// option is elected, it is assumed that the client illustration or
+/// admin system enforces the 7702(f)(6) limit. If it is not possible
+/// to force out the entire 7702(f)(2)(A) value (e.g., because it
+/// includes a refundable sales load or an experience-rating reserve
+/// that is available only on full surrender), then the policy lapses.
+///
+/// 7702(f)(1)(A) prescribes that 'premiums paid' is decreased only by
+/// the amount "with respect to which there is a distribution": i.e.,
+/// limited to the available 7702(f)(2)(A) value, which is an argument
+/// to this function.
+///
+/// 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(currency f2A_value)
+{
+ forceout_amount_ = C0;
+
+ if(cum_prems_paid_ <= guideline_limit())
+ {return C0;}
+
+ forceout_amount_ = std::min(f2A_value, 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..724f065 100644
--- a/irc7702.hpp
+++ b/irc7702.hpp
@@ -24,8 +24,117 @@
#include "config.hpp"
+#include "currency.hpp"
+#include "gpt_commutation_functions.hpp"
+#include "mc_enum_type_enums.hpp" // mcenum_defn_life_ins
+
+#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
+
+/// Guideline premium test.
+///
+/// 7702 !! This should be a base class, with distinct derived classes
+/// for illustration and admin systems.
+
class irc7702
{
+ 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
+ (mcenum_defn_life_ins defn_life_ins
+ ,bool issued_today
+ ,double inforce_glp
+ ,double inforce_cum_glp
+ ,double inforce_gsp
+ ,currency inforce_cum_pmt
+ ,gpt_scalar_parms const& arg_parms
+ );
+
+ // return amount forced out
+ currency update(gpt_scalar_parms const&, currency f2A_value);
+
+ // return amount rejected
+ currency accept_payment (currency);
+
+ // const accessors
+ currency rounded_glp () const;
+ currency rounded_cum_glp () const;
+ currency rounded_gsp () const;
+ currency cum_prems_paid () const;
+
+ 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 ();
+
+ private:
+ void dequeue_exch_1035 ();
+ void dequeue_prems_paid_decrease();
+ void dequeue_adj_event (gpt_scalar_parms const&);
+
+ currency force_out(currency f2A_value);
+
+ currency guideline_limit () const;
+
+ // unchangeable basis of calculations (subsumes gpt_vector_parms)
+ gpt_cf_triad cf_;
+
+ // changeable policy status (all scalar)
+ gpt_scalar_parms s_parms_ {};
+
+ // queued data
+ currency queued_exch_1035_amt_ {C0};
+ currency queued_prems_paid_decrement_ {C0};
+
+ // tableau data
+ double glp_ {0.0};
+ double cum_glp_ {0.0};
+ double gsp_ {0.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};
+
+ // server state
+ mcenum_defn_life_ins defn_life_ins_ {mce_gpt};
+ bool issued_today_ {true};
};
#endif // irc7702_hpp