lmi-commits
[Top][All Lists]
Advanced

[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



reply via email to

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