lmi-commits
[Top][All Lists]
Advanced

[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



reply via email to

[Prev in Thread] Current Thread [Next in Thread]