lmi-commits
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[lmi-commits] [lmi] master ad8e4f7 3/3: Rework 7702 calculations


From: Greg Chicares
Subject: [lmi-commits] [lmi] master ad8e4f7 3/3: Rework 7702 calculations
Date: Sat, 22 May 2021 13:57:06 -0400 (EDT)

branch: master
commit ad8e4f74085a2697b0838df8581ab5fa2a9082ea
Author: Gregory W. Chicares <gchicares@sbcglobal.net>
Commit: Gregory W. Chicares <gchicares@sbcglobal.net>

    Rework 7702 calculations
    
    Chose to hold a gpt7702 object in a std::shared_ptr rather than in a
    std::unique_ptr: it's notionally unique, but std::unique_ptr requires
    an explicit dtor, which isn't a good enough reason to write one.
    
    For the moment, kept the old code to validate the new; the speed penalty
    of roughly two percent should vanish once the old code is removed.
---
 Speed_gcc_i686-w64-mingw32    |  12 +-
 Speed_gcc_x86_64-pc-linux-gnu |  12 +-
 Speed_gcc_x86_64-w64-mingw32  |  12 +-
 basic_values.hpp              |   2 +
 gpt7702.cpp                   | 552 +++++++++++++++++++++++++++++++++++++++++-
 gpt7702.hpp                   | 117 +++++++++
 gpt_test.cpp                  | 351 ++++++++++++++++++++++++++-
 ihs_acctval.cpp               |  28 ++-
 ihs_avdebug.cpp               |  10 +-
 ihs_avmly.cpp                 |  43 +++-
 ihs_basicval.cpp              |  31 ++-
 11 files changed, 1134 insertions(+), 36 deletions(-)

diff --git a/Speed_gcc_i686-w64-mingw32 b/Speed_gcc_i686-w64-mingw32
index 7cef88a..9b16064 100644
--- a/Speed_gcc_i686-w64-mingw32
+++ b/Speed_gcc_i686-w64-mingw32
@@ -1,7 +1,7 @@
 Test speed:
-  naic, no solve      : 2.639e-02 s mean;      26276 us least of  38 runs
-  naic, specamt solve : 4.948e-02 s mean;      49252 us least of  21 runs
-  naic, ee prem solve : 4.535e-02 s mean;      45199 us least of  23 runs
-  finra, no solve     : 1.095e-02 s mean;      10862 us least of  92 runs
-  finra, specamt solve: 3.224e-02 s mean;      32042 us least of  32 runs
-  finra, ee prem solve: 2.992e-02 s mean;      29749 us least of  34 runs
+  naic, no solve      : 2.869e-02 s mean;      26822 us least of  36 runs
+  naic, specamt solve : 5.277e-02 s mean;      50161 us least of  19 runs
+  naic, ee prem solve : 5.031e-02 s mean;      46117 us least of  20 runs
+  finra, no solve     : 1.218e-02 s mean;      11215 us least of  83 runs
+  finra, specamt solve: 3.411e-02 s mean;      32849 us least of  30 runs
+  finra, ee prem solve: 3.276e-02 s mean;      30374 us least of  31 runs
diff --git a/Speed_gcc_x86_64-pc-linux-gnu b/Speed_gcc_x86_64-pc-linux-gnu
index 7a7338f..77ec7c0 100644
--- a/Speed_gcc_x86_64-pc-linux-gnu
+++ b/Speed_gcc_x86_64-pc-linux-gnu
@@ -1,7 +1,7 @@
 Test speed:
-  naic, no solve      : 1.846e-02 s mean;      18163 us least of  55 runs
-  naic, specamt solve : 3.399e-02 s mean;      33508 us least of  30 runs
-  naic, ee prem solve : 3.095e-02 s mean;      30619 us least of  33 runs
-  finra, no solve     : 5.353e-03 s mean;       5234 us least of 100 runs
-  finra, specamt solve: 1.939e-02 s mean;      19007 us least of  52 runs
-  finra, ee prem solve: 1.789e-02 s mean;      17636 us least of  56 runs
+  naic, no solve      : 1.880e-02 s mean;      18278 us least of  54 runs
+  naic, specamt solve : 3.375e-02 s mean;      33463 us least of  30 runs
+  naic, ee prem solve : 3.077e-02 s mean;      30438 us least of  33 runs
+  finra, no solve     : 5.313e-03 s mean;       5130 us least of 100 runs
+  finra, specamt solve: 1.925e-02 s mean;      19010 us least of  52 runs
+  finra, ee prem solve: 1.776e-02 s mean;      17500 us least of  57 runs
diff --git a/Speed_gcc_x86_64-w64-mingw32 b/Speed_gcc_x86_64-w64-mingw32
index 0228d8c..5433118 100644
--- a/Speed_gcc_x86_64-w64-mingw32
+++ b/Speed_gcc_x86_64-w64-mingw32
@@ -1,7 +1,7 @@
 Test speed:
-  naic, no solve      : 2.596e-02 s mean;      25846 us least of  39 runs
-  naic, specamt solve : 4.469e-02 s mean;      44563 us least of  23 runs
-  naic, ee prem solve : 4.097e-02 s mean;      40845 us least of  25 runs
-  finra, no solve     : 9.862e-03 s mean;       9376 us least of 100 runs
-  finra, specamt solve: 2.774e-02 s mean;      26410 us least of  37 runs
-  finra, ee prem solve: 2.644e-02 s mean;      24773 us least of  38 runs
+  naic, no solve      : 2.709e-02 s mean;      26541 us least of  37 runs
+  naic, specamt solve : 4.692e-02 s mean;      45140 us least of  22 runs
+  naic, ee prem solve : 4.165e-02 s mean;      41304 us least of  25 runs
+  finra, no solve     : 1.053e-02 s mean;       9897 us least of  95 runs
+  finra, specamt solve: 2.946e-02 s mean;      27047 us least of  34 runs
+  finra, ee prem solve: 2.535e-02 s mean;      25183 us least of  40 runs
diff --git a/basic_values.hpp b/basic_values.hpp
index 938a224..508717a 100644
--- a/basic_values.hpp
+++ b/basic_values.hpp
@@ -60,6 +60,7 @@ class Irc7702A;
 class Loads;
 class MortalityRates;
 class death_benefits;
+class gpt7702;
 class i7702;
 class lingo;
 class modal_outlay;
@@ -128,6 +129,7 @@ class LMI_SO BasicValues
     std::shared_ptr<stratified_charges const> const StratifiedCharges_;
 
     std::unique_ptr<i7702          const> i7702_;
+    std::shared_ptr<gpt7702             > gpt7702_;
 
     std::unique_ptr<MortalityRates const> MortalityRates_;
     std::unique_ptr<InterestRates       > InterestRates_;
diff --git a/gpt7702.cpp b/gpt7702.cpp
index 88d2eba..685ad26 100644
--- a/gpt7702.cpp
+++ b/gpt7702.cpp
@@ -23,4 +23,554 @@
 
 #include "gpt7702.hpp"
 
-// implementation of class gpt7702
+#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);
+
+gpt7702::gpt7702
+    (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:
+///  - argument 'fractional_duration' is in [0.0, 1.0)
+///  - 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 gpt7702::initialize_gpt
+    (mcenum_defn_life_ins    defn_life_ins
+    ,double                  fractional_duration
+    ,double                  inforce_glp
+    ,double                  inforce_cum_glp
+    ,double                  inforce_gsp
+    ,currency                inforce_cum_f1A
+    ,gpt_scalar_parms const& arg_parms
+    )
+{
+    LMI_ASSERT(0.0 <= fractional_duration && fractional_duration < 1.0);
+
+    defn_life_ins_       = defn_life_ins;
+    fractional_duration_ = fractional_duration;
+    s_parms_             = arg_parms;
+
+    if(is_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_f1A);
+        glp_ = cf_.calculate_premium(oe_glp, s_parms_);
+        cum_glp_ = glp_;
+        gsp_ = cf_.calculate_premium(oe_gsp, s_parms_);
+        cum_f1A_ = C0;
+        }
+    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_f1A_ = inforce_cum_f1A;
+        // Inforce data are as of the last moment of the preceding
+        // day, so an illustration for an exact anniversary must
+        // increment cumulative GLP.
+        if(0.0 == fractional_duration_)
+            {increment_boy();}
+        }
+
+    if(mce_gpt == defn_life_ins_)
+        {LMI_ASSERT(cum_f1A_ <= 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 call this function--perhaps only once a year for
+/// an illustration system that restricts relevant transactions to
+/// policy anniversaries, but once a day for an admin system.
+///
+/// The 'fractional_duration' argument specifies what proportion of
+/// the present year has elapsed. All adjustment events that occur on
+/// the same date must be combined together and processed as a single
+/// change; therefore, for a policy ninety-three days past its eighth
+/// anniversary, an admin system would pass
+///    8 as 'arg_parms.duration'
+///   93/365 or 93/366, as the case may be, as 'fractional_duration'
+/// while an illustration system might specify 'fractional_duration'
+/// as 0.0 always (or perhaps as something like 3/12).
+///
+/// Return amount forced out, or zero if there is no forceout.
+///
+/// Asserted preconditions:
+///  - argument 'fractional_duration' is in [0.0, 1.0)
+///  - 'arg_parms.duration' is either the same as 's_parms_.duration',
+///    or greater by one, in which case the latter is incremented here
+///  - 's_parms_.duration' is incremented only on exact anniversaries
+///  - the queued agenda is self-consistent
+///
+/// Alternative not pursued: In lieu of 'arg_parms.duration' and
+/// 'fractional_duration', pass calendar dates and perform date
+/// arithmetic here.
+///
+/// Alternative not pursued: An adjustment event occurs if either
+///   spec amt and DB both changed, or
+///   dbopt changed, or
+///   a QAB changed (but QABs are not yet supported here),
+/// and that determination could be made here instead of making it
+/// the client's responsibility. That would require passing both
+/// spec amt and DB here, where they would be used for no other
+/// purpose; but the determination is simple, and the client already
+/// has the necessary information. Furthermore, other adjustment
+/// events such as a change in mortality or interest basis cannot
+/// reasonably be discerned here.
+///
+/// Alternative not pursued: Dispense with queuing; instead, add
+/// two more arguments, and enqueue them here, thus:
+///   enqueue_exch_1035    (queued_exch_1035_amt);
+///   enqueue_f1A_decrease (queued_f1A_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 gpt7702::update_gpt
+    (gpt_scalar_parms const& arg_parms
+    ,double                  fractional_duration
+    ,currency                f2A_value
+    )
+{
+    if(mce_gpt != defn_life_ins_)
+        {return C0;}
+
+    LMI_ASSERT(0.0 <= fractional_duration && fractional_duration < 1.0);
+    fractional_duration_ = fractional_duration;
+    bool must_increment_duration {arg_parms.duration != s_parms_.duration};
+    if(must_increment_duration)
+        {
+        LMI_ASSERT(0.0 == fractional_duration_);
+        ++s_parms_.duration;
+        LMI_ASSERT(arg_parms.duration == s_parms_.duration);
+        }
+
+    if(queued_f1A_decrease_)
+        {dequeue_f1A_decrease();}
+    else
+        {LMI_ASSERT(C0 == queued_f1A_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.duration       == s_parms_.duration      );
+        // A specamt change is an adjustment event only if DB also
+        // changes; the client is responsible for determining that.
+//      LMI_ASSERT(arg_parms.f3_bft         == s_parms_.f3_bft        );
+//      LMI_ASSERT(arg_parms.endt_bft       == s_parms_.endt_bft      );
+        // A target premium change is not an adjustment event per se.
+//      LMI_ASSERT(arg_parms.target_prem    == s_parms_.target_prem   );
+        LMI_ASSERT(arg_parms.chg_sa_base    == s_parms_.chg_sa_base   );
+        LMI_ASSERT(arg_parms.dbopt_7702     == s_parms_.dbopt_7702    );
+        LMI_ASSERT(arg_parms.qab_gio_amt    == s_parms_.qab_gio_amt   );
+        LMI_ASSERT(arg_parms.qab_adb_amt    == s_parms_.qab_adb_amt   );
+        LMI_ASSERT(arg_parms.qab_term_amt   == s_parms_.qab_term_amt  );
+        LMI_ASSERT(arg_parms.qab_spouse_amt == s_parms_.qab_spouse_amt);
+        LMI_ASSERT(arg_parms.qab_child_amt  == s_parms_.qab_child_amt );
+        LMI_ASSERT(arg_parms.qab_waiver_amt == s_parms_.qab_waiver_amt);
+        }
+
+    if(must_increment_duration)
+        {increment_boy();}
+    return force_out(f2A_value);
+}
+
+/// Accept payment up to limit; return the amount accepted.
+///
+/// The excess (if any) is "rejected" 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 "rejected" 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 "rejected" payment.
+
+currency gpt7702::accept_payment(currency payment)
+{
+    if(mce_gpt != defn_life_ins_)
+        {return payment;}
+
+    rejected_pmt_ = C0;
+
+    if(C0 == payment)
+        {return C0;}
+
+    LMI_ASSERT(C0 < payment);
+    currency const allowed = std::max(C0, guideline_limit() - cum_f1A_);
+    currency const accepted = std::min(allowed, payment);
+    rejected_pmt_ = payment - accepted;
+    LMI_ASSERT(C0 <= rejected_pmt_);
+    LMI_ASSERT(accepted + rejected_pmt_ == payment);
+    cum_f1A_ += accepted;
+    return accepted;
+}
+
+/// 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 gpt7702::enqueue_exch_1035(currency exch_amt)
+{
+    if(mce_gpt != defn_life_ins_)
+        {return;}
+
+    LMI_ASSERT(!queued_exch_1035_);
+    LMI_ASSERT(C0 == queued_exch_1035_amt_);
+    LMI_ASSERT(is_issued_today());
+    LMI_ASSERT(0  == s_parms_.duration);
+    LMI_ASSERT(C0 == cum_f1A_);
+    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 gpt7702::enqueue_f1A_decrease(currency decrement)
+{
+    if(mce_gpt != defn_life_ins_)
+        {return;}
+
+    LMI_ASSERT(!queued_f1A_decrease_);
+    LMI_ASSERT(C0 == queued_f1A_decrement_);
+    LMI_ASSERT(C0 < decrement);
+    LMI_ASSERT(!is_issued_today());
+    queued_f1A_decrease_ = true;
+    queued_f1A_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_gpt() takes
+/// a gpt_scalar_parms argument, as is appropriate to ensure that no
+/// adjustment is missed. It seemed that this update_gpt() argument
+/// and the queued gpt_scalar_parms data member should be identical,
+/// but an assertion to that effect failed when update_gpt() 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 gpt7702::enqueue_adj_event()
+{
+    if(mce_gpt != defn_life_ins_)
+        {return;}
+
+    LMI_ASSERT(!is_issued_today());
+    queued_adj_event_ = true;
+}
+
+double gpt7702::raw_glp           () const
+{return glp_;}
+
+double gpt7702::raw_cum_glp       () const
+{return cum_glp_;}
+
+double gpt7702::raw_gsp           () const
+{return gsp_;}
+
+currency gpt7702::rounded_glp     () const
+{return round_max_premium.c(glp_);}
+
+currency gpt7702::rounded_cum_glp () const
+{return round_max_premium.c(cum_glp_);}
+
+currency gpt7702::rounded_gsp     () const
+{return round_max_premium.c(gsp_);}
+
+currency gpt7702::cum_f1A         () const
+{return cum_f1A_;}
+
+/// 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 gpt7702::adjust_guidelines(gpt_scalar_parms const& arg_parms)
+{
+    // There can be no adjustment event on the issue date.
+    LMI_ASSERT(!is_issued_today());
+
+    // 7702 !! Off-anniversary adjustments are not yet supported.
+    LMI_ASSERT(0.0 == fractional_duration_);
+
+    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 gpt7702::increment_boy()
+{
+    cum_glp_ += glp_;
+}
+
+/// 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 gpt7702::dequeue_exch_1035()
+{
+    LMI_ASSERT(is_issued_today());
+    LMI_ASSERT(0  == s_parms_.duration);
+    LMI_ASSERT(C0 == cum_f1A_);
+    LMI_ASSERT(C0 <= queued_exch_1035_amt_);
+    LMI_ASSERT(queued_exch_1035_amt_ <= guideline_limit());
+    cum_f1A_ += 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 gpt7702::dequeue_f1A_decrease()
+{
+    LMI_ASSERT(!is_issued_today());
+    LMI_ASSERT(C0 < queued_f1A_decrement_);
+    cum_f1A_ -= queued_f1A_decrement_;
+    queued_f1A_decrease_ = false;
+    queued_f1A_decrement_ = C0;
+}
+
+/// Dequeue a potential adjustment event.
+///
+/// Delegate the real work to adjust_guidelines().
+
+void gpt7702::dequeue_adj_event(gpt_scalar_parms const& arg_parms)
+{
+    LMI_ASSERT(!is_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 gpt7702::force_out(currency f2A_value)
+{
+    forceout_amount_ = C0;
+
+    if(cum_f1A_ <= guideline_limit())
+        {return C0;}
+
+    forceout_amount_ = std::min(f2A_value, cum_f1A_ - guideline_limit());
+    cum_f1A_ -= forceout_amount_;
+    return forceout_amount_;
+}
+
+bool gpt7702::is_issued_today() const
+{
+    return 0 == s_parms_.duration && 0.0 == fractional_duration_;
+}
+
+currency gpt7702::guideline_limit() const
+{
+    return round_max_premium.c(std::max(cum_glp_, gsp_)); // r_downward
+}
diff --git a/gpt7702.hpp b/gpt7702.hpp
index da96145..2784ef6 100644
--- a/gpt7702.hpp
+++ b/gpt7702.hpp
@@ -24,8 +24,125 @@
 
 #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 gpt7702
 {
+    friend class gpt_test;
+
+  public:
+    gpt7702
+        (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_gpt
+        (mcenum_defn_life_ins    defn_life_ins
+        ,double                  fractional_duration
+        ,double                  inforce_glp
+        ,double                  inforce_cum_glp
+        ,double                  inforce_gsp
+        ,currency                inforce_cum_f1A
+        ,gpt_scalar_parms const& arg_parms
+        );
+
+    // return amount forced out
+    currency update_gpt
+        (gpt_scalar_parms const&
+        ,double                  fractional_duration
+        ,currency                f2A_value
+        );
+
+    // return amount rejected
+    currency accept_payment         (currency);
+
+    // queue notifications
+    void enqueue_exch_1035          (currency);
+    void enqueue_f1A_decrease       (currency);
+    void enqueue_adj_event          ();
+
+    // const accessors
+    double   raw_glp                () const;
+    double   raw_cum_glp            () const;
+    double   raw_gsp                () const;
+    currency rounded_glp            () const;
+    currency rounded_cum_glp        () const;
+    currency rounded_gsp            () const;
+    currency cum_f1A                () const;
+
+  protected:
+    void adjust_guidelines          (gpt_scalar_parms const&);
+    void increment_boy              ();
+
+  private:
+    void dequeue_exch_1035          ();
+    void dequeue_f1A_decrease       ();
+    void dequeue_adj_event          (gpt_scalar_parms const&);
+
+    currency force_out(currency f2A_value);
+
+    bool     is_issued_today        () const;
+    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_f1A_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_f1A_                   {C0};
+
+    // queued agenda
+    bool queued_exch_1035_              {false};
+    bool queued_f1A_decrease_           {false};
+    bool queued_adj_event_              {false};
+
+    // server state
+    mcenum_defn_life_ins defn_life_ins_ {mce_gpt};
+    double   fractional_duration_       {0.0};
 };
 
 #endif // gpt7702_hpp
diff --git a/gpt_test.cpp b/gpt_test.cpp
index b6fe89f..8102b55 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));
+
+    gpt7702 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_gpt
+        (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_f1A
+        ,s_parms_0
+        );
+    // 7702 !! Should it be permissible to initialize twice?
+    z.initialize_gpt
+        (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_f1A
+        ,s_parms_0
+        );
+    z.update_gpt(s_parms_0, 0.0, C0);
+    // 7702 !! Shouldn't it be forbidden to update twice in the same day?
+    z.update_gpt(s_parms_0, 0.0, C0);
+#if 0
+    LMI_TEST_THROW
+        (z.update_gpt(s_parms_0, 0.0, C0);
+        ,std::runtime_error
+        ,"Assertion 'cum_f1A_ <= 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));
+
+    gpt7702 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_gpt
+        (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_f1A
+        ,s_parms_0
+        );
+    LMI_TEST(std::fabs( 1799.8355 - z.glp_    ) < 0.01);
+    LMI_TEST(std::fabs( 1799.8355 - 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_f1A_        );
+    // 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_gpt(s_parms_0, 0.0, C0);
+    LMI_TEST(std::fabs( 1799.8355 - z.glp_    ) < 0.01);
+    LMI_TEST(std::fabs( 1799.8355 - 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_f1A_        );
+}
+
 /// 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));
+
+    gpt7702 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_gpt
+        (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_f1A
+        ,s_parms_0
+        );
+    z.update_gpt(s_parms_0, 0.0, C0);
+    LMI_TEST(std::fabs( 2074.4029 - z.glp_    ) < 0.01);
+    LMI_TEST(std::fabs( 2074.4029 - 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_f1A_        );
+
+    for(int j = 46; j < 70; ++j)
+        {
+        ++s_parms_0.duration;
+        z.update_gpt(s_parms_0, 0.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_gpt(s_parms_1, 0.0, 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_f1A_        );
+#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_f1A_ << " z.cum_f1A_\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));
+
+    gpt7702 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_gpt
+            (mce_gpt  // defn_life_ins
+            ,0.1      // fractional_duration
+            , -1000.0 // inforce_glp
+            ,-10000.0 // inforce_cum_glp
+            ,-13000.0 // inforce_gsp
+            ,C0       // inforce_cum_f1A
+            ,s_parms_0
+            )
+        ,std::runtime_error
+        ,"Assertion 'cum_f1A_ <= guideline_limit()' failed."
+        );
+
+    // With the same starting assumptions, but a valid inforce
+    // cumulative (c)(1) premiums paid value, no exception is thrown.
+    currency const c{-10000'00_cents};
+    z.initialize_gpt
+        (mce_gpt   // defn_life_ins
+        ,0.1       // fractional_duration
+        , -1000.0  // inforce_glp
+        ,-10000.0  // inforce_cum_glp
+        ,-13000.0  // inforce_gsp
+        ,c         // inforce_cum_f1A
+        ,s_parms_0
+        );
+    z.update_gpt(s_parms_0, 0.1, 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_f1A_         );
+    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_gpt(s_parms_1, 0.0, 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_f1A_         );
+    // 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_f1A_);
+    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_f1A_ << " z.cum_f1A_\n"
+    << allowed  << " allowed\n"
+    << allowed0 << " allowed0\n"
+    << allowed1 << " allowed1\n"
+    << std::endl
+    ;
+#endif // 0
 }
 
 int test_main(int, char*[])
diff --git a/ihs_acctval.cpp b/ihs_acctval.cpp
index 5279b67..7ad8e8d 100644
--- a/ihs_acctval.cpp
+++ b/ihs_acctval.cpp
@@ -30,6 +30,7 @@
 #include "database.hpp"
 #include "dbnames.hpp"
 #include "death_benefits.hpp"
+#include "gpt7702.hpp"
 #include "i7702.hpp"
 #include "ihs_irc7702.hpp"
 #include "ihs_irc7702a.hpp"
@@ -440,6 +441,26 @@ void AccountValue::InitializeLife(mcenum_run_basis a_Basis)
         ;
     gpt_chg_sa_base_ = std::min(gpt_chg_sa_base_, SpecAmtLoadLimit);
 
+    // Normally 'f3_bft' is death benefit, not specified amount; but
+    // on the issue date, it is defined as specified amount.
+    gpt_scalar_parms s_parms =
+        {.duration       = yare_input_.InforceYear
+        ,.f3_bft         = dblize(specamt_for_7702(0))
+        ,.endt_bft       = dblize(specamt_for_7702(0))
+        ,.target_prem    = dblize(annual_target_premium)
+        ,.chg_sa_base    = dblize(gpt_chg_sa_base_)
+        ,.dbopt_7702     = effective_dbopt_7702(DeathBfts_->dbopt()[0], 
Effective7702DboRop)
+        };
+    gpt7702_->initialize_gpt
+        (yare_input_.DefinitionOfLifeInsurance // defn_life_ins
+        ,yare_input_.InforceMonth / 12.0       // fractional_duration
+        ,yare_input_.InforceGlp                // inforce_glp
+        ,yare_input_.InforceCumulativeGlp      // inforce_cum_glp
+        ,yare_input_.InforceGsp                // inforce_gsp
+        ,round_minutiae().c(yare_input_.InforceCumulativeGptPremiumsPaid) // 
inforce_cum_f1A
+        ,s_parms
+        );
+
     // It is at best superfluous to do this for every basis.
     // TAXATION !! Don't do that then.
     Irc7702_->Initialize7702
@@ -449,8 +470,11 @@ void AccountValue::InitializeLife(mcenum_run_basis a_Basis)
         ,dblize(annual_target_premium)
         );
 
-    InvariantValues().InitGLP = Irc7702_->RoundedGLP();
-    InvariantValues().InitGSP = Irc7702_->RoundedGSP();
+    LMI_ASSERT(materially_equal(gpt7702_->raw_glp(), Irc7702_->glp()));
+    LMI_ASSERT(materially_equal(gpt7702_->raw_gsp(), Irc7702_->gsp()));
+
+    InvariantValues().InitGLP = dblize(gpt7702_->rounded_glp());
+    InvariantValues().InitGSP = dblize(gpt7702_->rounded_gsp());
 
     // This is notionally called once per *current*-basis run
     // and actually called once per run, with calculations suppressed
diff --git a/ihs_avdebug.cpp b/ihs_avdebug.cpp
index 22ecfc3..02efc55 100644
--- a/ihs_avdebug.cpp
+++ b/ihs_avdebug.cpp
@@ -26,7 +26,7 @@
 #include "assert_lmi.hpp"
 #include "configurable_settings.hpp"
 #include "global_settings.hpp"
-#include "ihs_irc7702.hpp"
+#include "gpt7702.hpp"
 #include "ihs_irc7702a.hpp"
 #include "ledger_invariant.hpp"
 #include "mc_enum_types_aux.hpp"        // mc_str()
@@ -510,10 +510,10 @@ void AccountValue::DebugPrint()
 
     if(!irc7702_data_irrelevant)
         {
-        SetMonthlyDetail(eGLP                ,Irc7702_->glp              ());
-        SetMonthlyDetail(eCumGLP             ,Irc7702_->cum_glp          ());
-        SetMonthlyDetail(eGSP                ,Irc7702_->gsp              ());
-        SetMonthlyDetail(e7702PremiumsPaid   ,Irc7702_->premiums_paid    ());
+        SetMonthlyDetail(eGLP                ,gpt7702_->raw_glp          ());
+        SetMonthlyDetail(eCumGLP             ,gpt7702_->raw_cum_glp      ());
+        SetMonthlyDetail(eGSP                ,gpt7702_->raw_gsp          ());
+        SetMonthlyDetail(e7702PremiumsPaid   ,gpt7702_->cum_f1A          ());
         }
     else
         {
diff --git a/ihs_avmly.cpp b/ihs_avmly.cpp
index 98db324..32b2fe0 100644
--- a/ihs_avmly.cpp
+++ b/ihs_avmly.cpp
@@ -29,6 +29,7 @@
 #include "database.hpp"
 #include "dbnames.hpp"
 #include "death_benefits.hpp"
+#include "gpt7702.hpp"
 #include "ihs_irc7702.hpp"
 #include "ihs_irc7702a.hpp"
 #include "interest_rates.hpp"
@@ -113,6 +114,15 @@ void AccountValue::DoMonthDR()
     TxSpecAmtChange();
     TxTakeWD();
 
+    if(!SolvingForGuarPremium && Solving || mce_run_gen_curr_sep_full == 
RunBasis_)
+        {
+        // Illustration-reg guaranteed premium ignores GPT limit.
+        if(0 == Year && 0 == Month)
+            {
+            currency const z {External1035Amount + Internal1035Amount};
+            gpt7702_->enqueue_exch_1035(z);
+            }
+        }
     TxTestGPT();
     // TODO ?? TAXATION !! Doesn't this mean dumpins and 1035s get ignored?
     LMI_ASSERT(C0 <= Dcv);
@@ -1137,10 +1147,27 @@ void AccountValue::TxTestGPT()
             ,old_dbopt
             ,dblize(AnnualTargetPrem)
             );
+        gpt7702_->enqueue_adj_event();
         }
 
+    // 7702 !! call this only to maintain internal consistency
     // CURRENCY !! already rounded by class Irc7702--appropriately?
-    GptForceout = round_minutiae().c(Irc7702_->Forceout());
+    round_minutiae().c(Irc7702_->Forceout());
+
+    gpt_scalar_parms s_parms =
+        {.duration       = Year
+        ,.f3_bft         = dblize(DBReflectingCorr + TermDB)
+        ,.endt_bft       = dblize(specamt_for_7702(Year))
+        ,.target_prem    = dblize(AnnualTargetPrem)
+        ,.chg_sa_base    = dblize(gpt_chg_sa_base_)
+        ,.dbopt_7702     = new_dbopt
+        };
+    GptForceout = gpt7702_->update_gpt
+        (s_parms
+        ,Month / 12.0
+        ,TotalAccountValue()
+        );
+
     // TODO ?? TAXATION !! On other bases, nothing is forced out, and payments 
aren't limited.
     process_distribution(GptForceout);
     YearsTotalGptForceout += GptForceout;
@@ -1222,7 +1249,8 @@ void AccountValue::TxAscertainDesiredPayment()
             // CURRENCY !! return modified value instead of altering argument
             double z = dblize(eepmt);
             Irc7702_->ProcessGptPmt(Year, z);
-            eepmt = round_gross_premium().c(z);
+//          eepmt = round_gross_premium().c(z);
+            eepmt = gpt7702_->accept_payment(eepmt);
             }
         EeGrossPmts[Month] += eepmt;
         GrossPmts  [Month] += eepmt;
@@ -1238,7 +1266,8 @@ void AccountValue::TxAscertainDesiredPayment()
             // CURRENCY !! return modified value instead of altering argument
             double z = dblize(erpmt);
             Irc7702_->ProcessGptPmt(Year, z);
-            erpmt = round_gross_premium().c(z);
+//          erpmt = round_gross_premium().c(z);
+            erpmt = gpt7702_->accept_payment(erpmt);
             }
         ErGrossPmts[Month] += erpmt;
         GrossPmts  [Month] += erpmt;
@@ -1258,7 +1287,8 @@ void AccountValue::TxAscertainDesiredPayment()
             // CURRENCY !! return modified value instead of altering argument
             double z = dblize(Dumpin);
             Irc7702_->ProcessGptPmt(Year, z);
-            Dumpin = round_gross_premium().c(z);
+//          Dumpin = round_gross_premium().c(z);
+            Dumpin = gpt7702_->accept_payment(Dumpin);
             }
         EeGrossPmts[Month] += Dumpin;
         GrossPmts  [Month] += Dumpin;
@@ -2627,6 +2657,11 @@ void AccountValue::TxTakeWD()
             // 'premiums_paid_increment' is modified?
             double premiums_paid_increment = -dblize(GrossWD);
             Irc7702_->ProcessGptPmt(Year, premiums_paid_increment);
+            gpt7702_->enqueue_f1A_decrease(GrossWD);
+            // A withdrawal might trigger a GPT adjustment, or it
+            // might not. If it does, that'll be detected at the
+            // appropriate moment, elsewhere, so don't call
+            // enqueue_adj_event() here.
             }
         }
 
diff --git a/ihs_basicval.cpp b/ihs_basicval.cpp
index 5a855f9..a67ccce 100644
--- a/ihs_basicval.cpp
+++ b/ihs_basicval.cpp
@@ -35,6 +35,7 @@
 #include "financial.hpp"                // list_bill_premium()
 #include "fund_data.hpp"
 #include "global_settings.hpp"
+#include "gpt7702.hpp"
 #include "gpt_specamt.hpp"
 #include "i7702.hpp"
 #include "ieee754.hpp"                  // ldbl_eps_plus_one_times()
@@ -435,6 +436,32 @@ void BasicValues::Init7702()
         || mce_variable_loan_rate != yare_input_.LoanRateType
         );
 
+/// TAXATION !! No contemporary authority seems to believe that a
+/// change in the premium-tax rate, even if passed through to the
+/// policyowner, is a 7702A material change or a GPT adjustment event.
+/// Premium loads should instead reflect the lowest premium-tax rate.
+    gpt_vector_parms charges =
+        {.prem_load_target     = 
Loads_->target_premium_load_excluding_premium_tax()
+        ,.prem_load_excess     = 
Loads_->excess_premium_load_excluding_premium_tax()
+        ,.policy_fee_monthly   = dblize(Loads_->monthly_policy_fee 
(mce_gen_curr))
+        ,.policy_fee_annual    = dblize(Loads_->annual_policy_fee  
(mce_gen_curr))
+        ,.specamt_load_monthly = Loads_->specified_amount_load     
(mce_gen_curr)
+        ,.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)
+        };
+    gpt7702_ = std::make_shared<gpt7702>
+        (Mly7702qc
+        ,i7702_->ic_glp()
+        ,i7702_->ig_glp()
+        ,i7702_->ic_gsp()
+        ,i7702_->ig_gsp()
+        ,charges
+        );
+
     // TODO ?? We should avoid reading the rate file again; but
     // the GPT server doesn't initialize a MortalityRates object
     // that would hold those rates. TAXATION !! Rework this.
@@ -472,10 +499,6 @@ void BasicValues::Init7702()
         ,dblize(SpecAmtLoadLimit)
         ,local_mly_charge_add
         ,local_adb_limit
-/// TAXATION !! No contemporary authority seems to believe that a
-/// change in the premium-tax rate, even if passed through to the
-/// policyowner, is a 7702A material change or a GPT adjustment event.
-/// Premium loads should instead reflect the lowest premium-tax rate.
         ,Loads_->target_premium_load_excluding_premium_tax()
         ,Loads_->excess_premium_load_excluding_premium_tax()
         ,InitialTargetPremium



reply via email to

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