diff --git a/lifelib/libraries/appliedlife/IntegratedLife/Assumptions/__init__.py b/lifelib/libraries/appliedlife/IntegratedLife/Assumptions/__init__.py new file mode 100644 index 0000000..d89d913 --- /dev/null +++ b/lifelib/libraries/appliedlife/IntegratedLife/Assumptions/__init__.py @@ -0,0 +1,140 @@ +"""The space representing assumptions + +The Assumptions space represents a set of assumptions. +This space is parameterized with :attr:`asmp_id`. +For each value of :attr:`asmp_id`, +a dynamic subspace of this space is created, +representing the specific assumption set associated +with the value of :attr:`asmp_id`. + +.. rubric:: Parameters + +Attributes: + + asmp_id: a string ID representing an assumption set + +.. rubric:: References + +Attributes: + + base_data: Reference to the :mod:`~appliedlife.IntegratedLife.BaseData` space + +Example: + + The sample code below demonstrates how to examine the contents of + :mod:`~appliedlife.IntegratedLife.Assumptions` + for a specific value of :attr:`asmp_id`. + + .. code-block:: python + + >>> import modelx as mx + + >>> m = mx.read_model("IntegratedLife") + + >>> m.Assumptions["202312"].asmp_file() + WindowsPath('C:/Users/User1/appliedlife/input_tables/assumptions_202312.xlsx') + + >>> m.Assumptions["202312"].lapse_tables() + + L001 L002 L003 L004 + duration + 0 0.03 0.03 0.01 0.05 + 1 0.04 0.04 0.02 0.05 + 2 0.05 0.05 0.03 0.05 + 3 0.06 0.06 0.04 0.05 + 4 0.07 0.07 0.05 0.05 + 5 0.08 0.08 0.06 0.05 + 6 0.09 0.09 0.07 0.05 + 7 0.20 0.10 0.08 0.05 + 8 0.15 0.11 0.09 0.05 + 9 0.10 0.20 0.10 0.05 + 10 0.10 0.15 0.10 0.05 + 11 0.10 0.10 0.10 0.05 + 12 0.10 0.10 0.10 0.05 + 13 0.10 0.10 0.10 0.05 + 14 0.10 0.10 0.10 0.05 +""" + +from modelx.serialize.jsonvalues import * + +_formula = lambda asmp_id: None + +_bases = [] + +_allow_none = None + +_spaces = [] + +# --------------------------------------------------------------------------- +# Cells + +def asmp_file(): + """The file path to an assumption file + + Return a `pathlib`_ Path object representing the file path + of the assumption file. + + The file location is specified by the constant parameter, "table_dir". + The file name is constructed using a prefix and :attr:`asmp_id` + concatenated by an underscore, followed by ".xlsx". + + .. _pathlib: https://docs.python.org/3/library/pathlib.html + """ + + dir_ = base_data.const_params().at["table_dir", "value"] + prefix = base_data.const_params().at["asmp_file_prefix", "value"] + + return _model.path.parent / dir_ / (prefix + "_" + asmp_id + ".xlsx") + + +def dyn_lapse_params(): + """Dynamic lapse parameters""" + return pd.read_excel( + asmp_file(), + sheet_name="DynLapse", + index_col=0) + + +def lapse_len(): + """Duration length of the lapse table""" + return len(lapse_tables()) + + +def lapse_tables(): + """Lapse rate assumptions""" + return pd.read_excel( + asmp_file(), + sheet_name="Lapse", + index_col=0) + + +def mort_scalar_len(): + """Duration length of the mortality scalar table""" + return len(mort_scalar_tables()) + + +def mort_scalar_tables(): + """Mortality scalar tables""" + df = pd.read_excel( + asmp_file(), + sheet_name="Mortality", + index_col=0) + return df + + +def stacked_lapse_tables(): + """Stacked lapse tables""" + return lapse_tables().stack().swaplevel(0, 1).sort_index() + + +def stacked_mort_scalar_tables(): + """Stacked mortality scalar tables""" + return mort_scalar_tables().stack().swaplevel(0, 1).sort_index() + + +# --------------------------------------------------------------------------- +# References + +base_data = ("Interface", ("..", "BaseData"), "auto") + +asmp_id = "202312" \ No newline at end of file diff --git a/lifelib/libraries/appliedlife/IntegratedLife/Assumptions/_data/_dynamic_inputs b/lifelib/libraries/appliedlife/IntegratedLife/Assumptions/_data/_dynamic_inputs new file mode 100644 index 0000000..e69de29 diff --git a/lifelib/libraries/appliedlife/IntegratedLife/BaseData/__init__.py b/lifelib/libraries/appliedlife/IntegratedLife/BaseData/__init__.py new file mode 100644 index 0000000..14322ec --- /dev/null +++ b/lifelib/libraries/appliedlife/IntegratedLife/BaseData/__init__.py @@ -0,0 +1,153 @@ +"""Basic parameters and data + +This space reads parameters from a parameter file. +The parameters are then be referenced in other parts of the model. + +There are a few types of parameters depending on the variability of their values. +Constant parameters have values that are constant anywhere in the model. +Run parameters can have different values for different runs. +Space parameters can have different values for different product spaces. +These three types of parameters are called fixed parameters, +because they have fixed values in each product space. + +Product parameters are specific to individual product spaces. +Each product space has product parameters. +The values of product parameters vary by "product_id" and "plan_id" +defined for the product space. +Product parameters are appended to the model point table for the product space +in :func:`~appliedlife.IntegratedLife.ModelPoints.model_point_table_ext`. + +Other spaces reference this space as ``base_data`` as a convention. + +In addition to the parameters, this space also reads surrender charge tables +to be used in other spaces. + +.. rubric:: References + +Attributes: + + parameter_file: The name of the parameter file + +Example: + + The sample code below demonstrates how to examine the contents of + :mod:`~appliedlife.IntegratedLife.BaseData`. + + .. code-block:: python + + >>> import modelx as mx + + >>> m = mx.read_model("IntegratedLife") + + >>> m.BaseData.const_params() + value + parameter + model_point_dir model_point_data + mp_file_prefix model_point + asmp_file_prefix assumptions + table_dir input_tables + scen_dir economic_data + scen_param_file index_parameters.xlsx + scen_file_prefix risk_free + mort_file mortality_tables.xlsx + spec_tables product_spec_tables.xlsx + + >>> m.BaseData.run_params() + base_date ... description + run_id ... + 1 2023-12-31 ... New business in Jan 2024 + 2 2023-12-31 ... Base run end of Dec 2023 + 3 2023-12-31 ... Interest rate sensitivity end of Dec 2023 + 4 2023-12-31 ... Interest rate sensitivity end of Dec 2023 + 5 2022-12-31 ... Base run end of Dec 2022 + + [5 rows x 6 columns] + + >>> m.BaseData.space_params() + expense_acq expense_maint currency is_lapse_dynamic + space + FIA 5000 500 USD True + GMXB 5000 500 USD True + GLWB 6000 600 USD True + + >>> m.BaseData.product_params("GMXB") + has_gmdb has_gmab ... dyn_lapse_param_id dyn_lapse_floor + product_id plan_id ... + GMDB PLAN_A True False ... DL001A 0.00 + PLAN_B True False ... DL001B 0.00 + GMAB PLAN_A True True ... DL002A 0.03 + PLAN_B True True ... DL002B 0.05 + + [4 rows x 16 columns] +""" + +from modelx.serialize.jsonvalues import * + +_formula = None + +_bases = [] + +_allow_none = None + +_spaces = [] + +# --------------------------------------------------------------------------- +# Cells + +def const_params(): + """Constant parameters""" + return pd.read_excel(_model.path.parent / parameter_file, + sheet_name="ConstParams", + index_col="parameter") + + +def param_list(): + """List of fixed parameters""" + return pd.read_excel(_model.path.parent / parameter_file, + sheet_name="ParamList", + index_col="parameter") + + +def product_params(space_name: str): + """Product parameters""" + return pd.read_excel(_model.path.parent / parameter_file, + sheet_name=space_name, + index_col=[0, 1]) + + +def run_params(): + """Run parameters""" + return pd.read_excel(_model.path.parent / parameter_file, + sheet_name="RunParams", + index_col="run_id", + dtype={"date_id": object, "asmp_id": object}) + + +def space_params(): + """Space parameters""" + return pd.read_excel(_model.path.parent / parameter_file, + sheet_name="SpaceParams", + index_col="space") + + +def stacked_surr_charge_tables(): + """Stacked surrender charge tables""" + return surr_charge_tables().stack().swaplevel(0, 1).sort_index() + + +def surr_charge_len(): + """Duration length of the surrender charge table""" + return len(surr_charge_tables()) + + +def surr_charge_tables(): + """Surrender charge tables""" + dir_ = _model.path.parent / const_params().at["table_dir", "value"] + file = const_params().at["spec_tables", "value"] + return pd.read_excel(dir_ / file, sheet_name="SurrCharge", index_col=0) + + +# --------------------------------------------------------------------------- +# References + +parameter_file = "model_parameters.xlsx" \ No newline at end of file diff --git a/lifelib/libraries/appliedlife/IntegratedLife/ModelPoints/__init__.py b/lifelib/libraries/appliedlife/IntegratedLife/ModelPoints/__init__.py new file mode 100644 index 0000000..95640b7 --- /dev/null +++ b/lifelib/libraries/appliedlife/IntegratedLife/ModelPoints/__init__.py @@ -0,0 +1,123 @@ +"""Model points + +The :mod:`~appliedlife.IntegratedLife.ModelPoints` space represents +a set of policy model points. +This space is parameterized with :attr:`mp_file_id` and :attr:`space_name`. +For each combination of :attr:`mp_file_id` and :attr:`space_name` values, +a dynamic subspace of this space is created, +representing a specific set of model points of :attr:`space_name`. + +.. rubric:: Parameters + +Attributes: + + mp_file_id: a string key representing a set of model points + space_name: a string key representing the name of a product space + +.. rubric:: References in the space + +Attributes: + + base_data: Reference to the :mod:`~appliedlife.IntegratedLife.BaseData` space + + +Example: + + The sample code below demonstrates how to examine the contents of + :mod:`~appliedlife.IntegratedLife.ModelPoints`. + + .. code-block:: python + + >>> import modelx as mx + + >>> m = mx.read_model("IntegratedLife") + + >>> m.ModelPoints["202401NB", "GMXB"].model_point_table() + + product_id plan_id ... av_pp_init accum_prem_init_pp + point_id ... + 1 GMDB PLAN_A ... 0 0 + 2 GMDB PLAN_A ... 0 0 + 3 GMDB PLAN_B ... 0 0 + 4 GMDB PLAN_B ... 0 0 + 5 GMAB PLAN_A ... 0 0 + 6 GMAB PLAN_A ... 0 0 + 7 GMAB PLAN_B ... 0 0 + 8 GMAB PLAN_B ... 0 0 + + [8 rows x 13 columns] + + >>> m.ModelPoints["202401NB", "GMXB"].model_point_table_ext() + + product_id plan_id ... dyn_lapse_param_id dyn_lapse_floor + point_id ... + 1 GMDB PLAN_A ... DL001A 0.00 + 2 GMDB PLAN_A ... DL001A 0.00 + 3 GMDB PLAN_B ... DL001B 0.00 + 4 GMDB PLAN_B ... DL001B 0.00 + 5 GMAB PLAN_A ... DL002A 0.03 + 6 GMAB PLAN_A ... DL002A 0.03 + 7 GMAB PLAN_B ... DL002B 0.05 + 8 GMAB PLAN_B ... DL002B 0.05 + + [8 rows x 29 columns] +""" + +from modelx.serialize.jsonvalues import * + +_formula = lambda mp_file_id, space_name: None + +_bases = [] + +_allow_none = None + +_spaces = [] + +# --------------------------------------------------------------------------- +# Cells + +def model_point_table(): + """Reads a raw model point table from a file and returns it. + + Returns a DataFrame representing a model point table read from a model point file. + The model point table is for a product space identified by :attr:`space_name`. + By default, a CSV file is expected for the model point file. + The path to the model point file is obtained from the value of the "model_point_dir" + parameter in :func:`~appliedlife.IntegratedLife.BaseData.const_params`. + + The file name is constructed using a prefix, :attr:`mp_file_id` and :attr:`space_name`, + all concatenated by underscores, followed by ".csv". + The prefix is obtained from the value of the "model_point_file_prefix" parameter + in :func:`~appliedlife.IntegratedLife.BaseData.const_params`. + """ + dir_name: str = base_data.const_params().at["model_point_dir", "value"] + file_name: str = (base_data.const_params().at["mp_file_prefix", "value"] + + "_" + mp_file_id + "_" + space_name + ".csv") + + return pd.read_csv(_model.path.parent / dir_name / file_name, index_col="point_id", parse_dates=["entry_date"]) + + +def model_point_table_ext(): + """Extends the raw model point table with product parameters and returns it. + + Append product parameter columns to the raw model point table returned by + :func:`model_point_table`. + The product parameters are obtained by passing :attr:`space_name` to + :func:`~appliedlife.IntegratedLife.BaseData.product_params`. + For each model point row in the raw model point table, a corresponding row that has + matching "product_id" and "plan_id" values is appended. + """ + return pd.merge(model_point_table().reset_index(), + base_data.product_params(space_name).reset_index(), + how="left", + on=["product_id", "plan_id"]).set_index('point_id') + + +# --------------------------------------------------------------------------- +# References + +base_data = ("Interface", ("..", "BaseData"), "auto") + +space_name = "GMXB" + +date_id = "202312" \ No newline at end of file diff --git a/lifelib/libraries/appliedlife/IntegratedLife/ModelPoints/_data/_dynamic_inputs b/lifelib/libraries/appliedlife/IntegratedLife/ModelPoints/_data/_dynamic_inputs new file mode 100644 index 0000000..e69de29 diff --git a/lifelib/libraries/appliedlife/IntegratedLife/Mortality/__init__.py b/lifelib/libraries/appliedlife/IntegratedLife/Mortality/__init__.py new file mode 100644 index 0000000..cdb8587 --- /dev/null +++ b/lifelib/libraries/appliedlife/IntegratedLife/Mortality/__init__.py @@ -0,0 +1,205 @@ +"""Mortality tables + +The :mod:`~appliedlife.IntegratedLife.Mortality` space reads +mortality tables from a mortality file +and creates a unified mortality table as a pandas Series +indexed with Table ID, Attained Age and Duration. + +This space is referenced as :attr:`~appliedlife.IntegratedLife.ProductBase.mort_data` +in the :mod:`~appliedlife.IntegratedLife.ProductBase` space. + +Attributes: + + base_data: Reference to the :mod:`~appliedlife.IntegratedLife.BaseData` space + +Example: + + The sample code below demonstrates how to examine the contents of + :mod:`~appliedlife.IntegratedLife.Mortality`. + + .. code-block:: python + + >>> import modelx as mx + + >>> m = mx.read_model("IntegratedLife") + + >>> m.Mortality.mort_file() + WindowsPath('C:/Users/User1/appliedlife/input_tables/mortality_tables.xlsx') + + >>> m.Mortality.table_defs() + + is_used sex ... has_select description + table_id ... + T3363 True M ... True 2017 Unloaded CSO Composite Male ALB + T3364 True F ... True 2017 Unloaded CSO Composite Female ALB + T3275 True M ... True 2015 VBT Unismoke Male ALB + T3276 True F ... True 2015 VBT Unismoke Female ALB + T884 True F ... False Annuity 2000 Basic Female + T885 True M ... False Annuity 2000 Basic Male + + >>> m.Mortality.select_table('T3276') + + 0 1 2 3 ... 21 22 23 24 + 0 0.00022 0.00012 0.00008 0.00007 ... 0.00029 0.00032 0.00035 0.00036 + 1 0.00012 0.00008 0.00007 0.00007 ... 0.00032 0.00035 0.00036 0.00036 + 2 0.00008 0.00007 0.00007 0.00007 ... 0.00035 0.00036 0.00036 0.00035 + 3 0.00007 0.00007 0.00007 0.00007 ... 0.00036 0.00036 0.00035 0.00035 + 4 0.00007 0.00007 0.00007 0.00007 ... 0.00036 0.00035 0.00035 0.00036 + .. ... ... ... ... ... ... ... ... ... + 91 0.02529 0.06300 0.13175 0.16683 ... 0.50000 0.50000 0.50000 0.50000 + 92 0.03682 0.09384 0.16683 0.18406 ... 0.50000 0.50000 0.50000 0.50000 + 93 0.05460 0.12337 0.18406 0.20412 ... 0.50000 0.50000 0.50000 0.50000 + 94 0.08008 0.16292 0.20412 0.22599 ... 0.50000 0.50000 0.50000 0.50000 + 95 0.11495 0.20412 0.22599 0.24952 ... 0.50000 0.50000 0.50000 0.50000 + + [96 rows x 25 columns] + + >>> m.Mortality.merged_table('T3276') + + att_age duration + 0 0 0.00022 + 1 1 0.00012 + 2 2 0.00008 + 3 3 0.00007 + 4 4 0.00007 + + 116 25 0.50000 + 117 25 0.50000 + 118 25 0.50000 + 119 25 0.50000 + 120 25 0.50000 + Length: 2521, dtype: float64 + + >>> m.Mortality.unified_table() + + table_id att_age duration + T3363 0 0 0.00019 + 1 1 0.00012 + 2 2 0.00011 + 3 3 0.00009 + 4 4 0.00009 + + T885 116 0 1.00000 + 117 0 1.00000 + 118 0 1.00000 + 119 0 1.00000 + 120 0 1.00000 + Length: 10326, dtype: float64 + +""" + +from modelx.serialize.jsonvalues import * + +_formula = None + +_bases = [] + +_allow_none = None + +_spaces = [] + +# --------------------------------------------------------------------------- +# Cells + +def merged_table(table_id: str): + """Merged mortality table for a given table ID""" + has_select = table_defs().at[table_id, "has_select"] + has_ultimate = table_defs().at[table_id, "has_ultimate"] + + if has_select: + + select = select_table(table_id).stack() + select.name = "rate" + select.index.names = ["entry_age", "duration"] + select = select.reset_index() + select["att_age"] = select["entry_age"] + select["duration"] + select = select.set_index(["att_age", "duration"])["rate"] + + if has_ultimate: + + ultimate = ultimate_tables()[[table_id]].copy() + ultimate["duration"] = select_duration_len()[table_id] + ultimate = ultimate.set_index("duration", append=True)[table_id] + + + if has_select and has_ultimate: + return pd.concat([select, ultimate]) + + elif has_select and not has_ultimate: + return select + + elif not has_select and has_ultimate: + return ultimate + + else: + raise ValueError(f"Table {table_id} neither has select nor ultimate") + + +def mort_file(): + """Mortality table file""" + dir_ = base_data.const_params().at["table_dir", "value"] + file = base_data.const_params().at["mort_file", "value"] + + return _model.path.parent / dir_ / file + + +def select_duration_len(): + """Selection period length""" + ids = table_defs().index + dur_len = [] + for id_ in ids: + if table_defs().at[id_, "has_select"]: + dur_len.append(len(select_table(id_).columns)) + else: + dur_len.append(0) # 0 for ultimate only + + return pd.Series(dur_len, index=ids) + + +def select_table(table_id: str): + """Reads a select mortality table with the given table ID""" + df = pd.read_excel( + mort_file(), + sheet_name=table_id, + index_col=0) + df.columns = range(len(df.columns)) + return df + + +def table_defs(): + """Table definitions""" + df = pd.read_excel( + mort_file(), + sheet_name="TableDefs", + index_col=0) + + return df.loc[df["is_used"] == True] + + +def table_last_age(): + """Mortality table last age""" + return unified_table().index.to_frame(index=False).groupby("table_id")["att_age"].max() + + +def ultimate_tables(): + """Reads the ultimate mortality tables""" + df = pd.read_excel( + mort_file(), + sheet_name="Ultimate", + index_col=0) + df.index.name = "att_age" + return df + + +def unified_table(): + """Unified mortality table""" + return pd.concat( + {id_: merged_table(id_) for id_ in table_defs().index}, + names=["table_id"] + ) + + +# --------------------------------------------------------------------------- +# References + +base_data = ("Interface", ("..", "BaseData"), "auto") \ No newline at end of file diff --git a/lifelib/libraries/appliedlife/IntegratedLife/ProductBase/__init__.py b/lifelib/libraries/appliedlife/IntegratedLife/ProductBase/__init__.py new file mode 100644 index 0000000..a1187af --- /dev/null +++ b/lifelib/libraries/appliedlife/IntegratedLife/ProductBase/__init__.py @@ -0,0 +1,1856 @@ +"""Base projection logic for all products + +The :mod:`~appliedlife.IntegratedLife.ProductBase` space +serves as the base space for concrete product spaces +defined in the :mod:`~appliedlife.IntegratedLife.Run` space. + +This space defines main projection logic that is +common for all products. + + +.. seealso + + * :mod:`~appliedlife.IntegratedLife.Run.GMXB` +""" + +from modelx.serialize.jsonvalues import * + +_formula = None + +_bases = [] + +_allow_none = None + +_spaces = [] + +# --------------------------------------------------------------------------- +# Cells + +def age(t): + """The attained age at time t. + + Defined as:: + + age_at_entry() + duration(t) + + .. seealso:: + + * :func:`age_at_entry` + * :func:`duration` + + """ + return age_at_entry() + duration(t) + + +def age_at_entry(): + """The age at entry of the model points + + The ``age_at_entry`` column of the DataFrame returned by + :func:`model_point`. + """ + return model_point()["age_at_entry"].values + + +def asmp_id(): + """Assumption ID""" + return fixed_params()["asmp_id"] + + +def av_at(t, timing): + """Account value in-force + + :func:`av_at(t, timing)` calculates + the total amount of account value at time ``t`` for the policies represented + by a model point. + + At each ``t``, the events that change the account value balance + occur in the following order: + + * Maturity + * New business and premium payment + * Fee deduction + + The second parameter ``timing`` takes a string to + indicate the timing of the account value, which is either + ``"BEF_MAT"``, ``"BEF_NB"`` or ``"BEF_FEE"``. + + + .. rubric:: BEF_MAT + + The amount of account value before maturity, defined as:: + + av_pp_at(t, "BEF_PREM") * pols_if_at(t, "BEF_MAT") + + .. rubric:: BEF_NB + + The amount of account value before new business after maturity, + defined as:: + + av_pp_at(t, "BEF_PREM") * pols_if_at(t, "BEF_NB") + + .. rubric:: BEF_FEE + + The amount of account value before lapse and death after new business, + defined as:: + + av_pp_at(t, "BEF_FEE") * pols_if_at(t, "BEF_DECR") + + + .. seealso:: + * :func:`pols_if_at` + * :func:`av_pp_at` + + + """ + if timing == "BEF_MAT": + return av_pp_at(t, "BEF_PREM") * pols_if_at(t, "BEF_MAT") + + elif timing == "BEF_NB": + return av_pp_at(t, "BEF_PREM") * pols_if_at(t, "BEF_NB") + + elif timing == "BEF_FEE": + return av_pp_at(t, "BEF_FEE") * pols_if_at(t, "BEF_DECR") + + else: + raise ValueError("invalid timing") + + +def av_change(t): + """Change in account value + + Change in account value during each period, defined as:: + + av_at(t+1, 'BEF_MAT') - av_at(t, 'BEF_MAT') + + .. seealso:: + + * :func:`net_cf` + + """ + return av_at(t+1, 'BEF_MAT') - av_at(t, 'BEF_MAT') + + +def av_pp_at(t, timing): + """Account value per policy + + :func:`av_at(t, timing)` calculates + the total amount of account value at time ``t`` for the policies in-force. + + At each ``t``, the events that change the account value balance + occur in the following order: + + * Premium payment + * Fee deduction + + Investment income is assumed to be earned throughout each month, + so at the middle of the month when death and lapse occur, + half the investment income for the month is credited. + + The second parameter ``timing`` takes a string to + indicate the timing of the account value, which is either + ``"BEF_PREM"``, ``"BEF_FEE"``, ``"BEF_INV"`` or ``"MID_MTH"``. + + + .. rubric:: BEF_PREM + + Account value before premium payment. + At the start of the projection (i.e. when ``t=0``), + the account value is set to :func:`av_pp_init`. + + .. rubric:: BEF_FEE + + Account value after premium payment before fee deduction + + + .. rubric:: BEF_INV + + Account value after fee deduction before crediting investemnt return + + .. rubric:: MID_MTH + + Account value at middle of month (``t+0.5``) when + half the investment retun for the month is credited + + + .. seealso:: + * :func:`av_pp_init` + * :func:`inv_income_pp` + * :func:`prem_to_av_pp` + * :func:`maint_fee_pp` + * :func:`coi_pp` + * :func:`av_at` + + """ + if timing == "BEF_PREM": + if t == 0: + return av_pp_init() + else: + return av_pp_at(t-1, "BEF_INV") + inv_income_pp(t-1) + + elif timing == "BEF_FEE": + return av_pp_at(t, "BEF_PREM") + prem_to_av_pp(t) + + elif timing == "BEF_INV": + return av_pp_at(t, "BEF_FEE") - maint_fee_pp(t) - coi_pp(t) + + elif timing == "MID_MTH": + return av_pp_at(t, "BEF_INV") + 0.5 * inv_income_pp(t) + + else: + raise ValueError("invalid timing") + + +def av_pp_init(): + """Initial account value per policy + + For existing business at time ``0``, + returns initial per-policy accout value read from + the ``av_pp_init`` column in :func:`model_point`. + For new business, 0 should be entered in the column. + + .. seealso:: + + * :func:`model_point` + * :func:`av_pp_at` + + """ + return model_point()["av_pp_init"].values + + +def base_lapse_rate(t): + """Base lapse rate + + By default, the lapse rate assumption is defined by duration as:: + + max(0.1 - 0.01 * duration(t), 0.02) + + .. seealso:: + + :func:`duration` + + """ + return asmp_data(asmp_id()).stacked_lapse_tables().reindex(lapse_rate_key(t)).values + + +def base_mort_rate(t): + """Base mortality rate to be applied at time t + + Returns a Series of the mortality rates to be applied at time t. + The index of the Series is ``point_id``, + copied from :func:`model_point`. + + .. seealso:: + + * :func:`mort_table_reindexed` + * :func:`mort_rate_mth` + * :func:`model_point` + + """ + return mort_data.unified_table().reindex( + mort_rate_key(t) + ).values + + +def check_av_roll_fwd(): + """Check account value roll-forward + + Returns ``Ture`` if ``av_at(t+1, "BEF_NB")`` equates to + the following expression for all ``t``, otherwise returns ``False``:: + + av_at(t, "BEF_MAT") + + prem_to_av(t) + - maint_fee(t) + - coi(t) + + inv_income(t) + - claims_from_av(t, "DEATH") + - claims_from_av(t, "LAPSE") + - claims_from_av(t, "MATURITY")) + + .. seealso:: + + * :func:`av_at` + * :func:`prem_to_av` + * :func:`maint_fee` + * :func:`coi` + * :func:`inv_income` + * :func:`claims_from_av` + + """ + cols = [] + for t in range(max_proj_len()): + + av = (av_at(t, "BEF_MAT") + + prem_to_av(t) + - maint_fee(t) + - coi(t) + + inv_income(t) + - claims_from_av(t, "DEATH") + - claims_from_av(t, "LAPSE") + - claims_from_av(t, "MATURITY")) + + cols.append(av_at(t+1, "BEF_MAT") - av) + + return np.column_stack(cols) + + +def check_margin(): + """Check consistency between net cashflow and margins + + Returns ``True`` if :func:`net_cf` equates to the sum of + :func:`margin_expense` and :func:`margin_mortality` for all ``t``, + otherwise, returns ``False``. + + .. seealso:: + + * :func:`net_cf` + * :func:`margin_expense` + * :func:`margin_mortality` + + """ + cols = [] + for t in range(max_proj_len()): + cols.append(net_cf(t) - margin_expense(t) - margin_guarantee(t)) + + return np.column_stack(cols) + + +def check_pv_net_cf(): + """Check present value summation + + Check if the present value of :func:`net_cf` matches the + sum of the present values of each cashflow. + Returns the check result as :obj:`True` or :obj:`False`. + + .. seealso:: + + * :func:`net_cf` + * :func:`pv_net_cf` + + """ + return pv_net_cf() - sum(net_cf(t) * disc_factors(t) for t in range(max_proj_len())) + + +def claim_net_pp(t, kind): + """Per policy claim in excess of account value""" + + if kind == "DEATH": + return claim_pp(t, "DEATH") - av_pp_at(t, "MID_MTH") + + elif kind == "LAPSE": + return 0 + + elif kind == "MATURITY": + return claim_pp(t, "MATURITY") - av_pp_at(t, "BEF_PREM") + + else: + raise ValueError("invalid kind") + + +def claim_pp(t, kind): + """Claim per policy + + The claim amount per policy. The second parameter + is to indicate the type of the claim, and + it takes a string, which is either ``"DEATH"``, ``"LAPSE"`` or ``"MATURITY"``. + + The death benefit as denoted by ``"DEATH"``, is + the greater of :func:`sum_assured` and + mid-month account value (:func:`av_pp_at(t, "MID_MTH")`). + + The surrender benefit as denoted by ``"LAPSE"`` and + the maturity benefit as denoted by ``"MATURITY"`` are + equal to the mid-month account value. + + .. seealso:: + + * :func:`sum_assured` + * :func:`av_pp_at` + + """ + + if kind == "DEATH": + return np.where(has_gmdb() == True, + np.maximum(sum_assured(), av_pp_at(t, "MID_MTH")), + av_pp_at(t, "MID_MTH")) + + # return np.maximum(sum_assured(), av_pp_at(t, "MID_MTH")) + + elif kind == "LAPSE": + return av_pp_at(t, "MID_MTH") + + elif kind == "MATURITY": + return np.where(has_gmab() == True, + np.maximum(sum_assured(), av_pp_at(t, "BEF_PREM")), + av_pp_at(t, "BEF_PREM")) + + else: + raise ValueError("invalid kind") + + +def claims(t, kind=None): + """Claims + + The claim amount during the period from ``t`` to ``t+1``. + The optional second parameter is for indicating the type of the claim, and + it takes a string, which is either ``"DEATH"``, ``"LAPSE"`` or ``"MATURITY"``, + or defaults to ``None`` to indicate the total of all the types of claims + during the period. + + + The death benefit as denoted by ``"DEATH"`` is defined as:: + + claim_pp(t) * pols_death(t) + + The surrender benefit as denoted by ``"LAPSE"`` is defined as:: + + claims_from_av(t, "LAPSE") - surr_charge(t) + + The maturity benefit as denoted by ``"MATURITY"`` is defined as:: + + claims_from_av(t, "MATURITY") + + .. seealso:: + + * :func:`claim_pp` + * :func:`pols_death` + * :func:`claims_from_av` + * :func:`surr_charge` + + """ + + if kind == "DEATH": + return claim_pp(t, "DEATH") * pols_death(t) + + elif kind == "LAPSE": + return claims_from_av(t, "LAPSE") - surr_charge(t) + + elif kind == "MATURITY": + return claim_pp(t, "MATURITY") * pols_maturity(t) + + elif kind is None: + return sum(claims(t, k) for k in ["DEATH", "LAPSE", "MATURITY"]) + + else: + raise ValueError("invalid kind") + + +def claims_from_av(t, kind): + """Account value taken out to pay claim + + The part of the claim amount that is paid from account value. + The second parameter takes a string indicating the type of the claim, + which is either ``"DEATH"``, ``"LAPSE"`` or ``"MATURITY"``. + + + Death benefit is denoted by ``"DEATH"``, is defined as:: + + av_pp_at(t, "MID_MTH") * pols_death(t) + + When the account value is greater than the death benefit, + the death benefit equates to the account value. + + Surrender benefit as denoted by ``"LAPSE"`` is defined as:: + + av_pp_at(t, "MID_MTH") * pols_lapse(t) + + As the surrender benefit is defined as account value less surrender + charge, when there is no surrender charge the surrender benefit + equates to the account value. + + Maturity benefit as denoted by ``"MATURITY"`` is defined as:: + + av_pp_at(t, "BEF_PREM") * pols_maturity(t) + + By default, the maturity benefit equates to the account value + of maturing policies. + + .. seealso:: + + * :func:`av_pp_at` + * :func:`pols_death` + * :func:`pols_lapse` + * :func:`pols_maturity` + + """ + + if kind == "DEATH": + return av_pp_at(t, "MID_MTH") * pols_death(t) + + elif kind == "LAPSE": + return av_pp_at(t, "MID_MTH") * pols_lapse(t) + + elif kind == "MATURITY": + return av_pp_at(t, "BEF_PREM") * pols_maturity(t) + + else: + raise ValueError("invalid kind") + + +def claims_over_av(t, kind): + """Claim in excess of account value + + The amount of death benefits in excess of account value. + :func:`coi` net of this amount represents mortality margin. + + .. seealso:: + + * :func:`margin_mortality` + * :func:`coi` + + """ + return claims(t, kind) - claims_from_av(t, kind) + + +def coi(t): + """Cost of insurance charges + + The cost of insurance charges deducted from acccount values + each period. + + .. seealso:: + + * :func:`pols_if_at` + * :func:`coi_pp` + + """ + return coi_pp(t) * pols_if_at(t, "BEF_DECR") + + +def coi_pp(t): + """Cost of insurance charges per policy + + The cost of insurance charges per policy. + Defined as the coi charge rate times net amount at risk per policy. + + .. seealso:: + + * :func:`coi` + * :func:`coi_rate` + * :func:`net_amt_at_risk` + + """ + return coi_rate(t) * net_amt_at_risk(t) + + +def coi_rate(t): + """Cost of insurance rate per account value + + The cost of insuranc rate per account value per month. + By default, it is set to 1.1 times the monthly mortality rate. + + .. seealso:: + + * :func:`mort_rate_mth` + * :func:`coi_pp` + * :func:`coi_rate` + + """ + return 0 #1.1 * mort_rate_mth(t) + + +def commission_rate(): + """Commission rate""" + return model_point()["commission_rate"].values + + +def commissions(t): + """Commissions + + By default, 100% premiums for the first year, 0 otherwise. + + .. seealso:: + + * :func:`premiums` + * :func:`duration` + + """ + return commission_rate() * premiums(t) + + +def csv_pp(t): + """Cash surrender value per policy""" + return (1 - surr_charge_rate(t)) * av_pp_at(t, 'MID_MTH') + + +def date_id(): + """Date ID""" + return fixed_params()["date_id"] + + +def disc_factors(t): + """Discount factors. + + Vector of the discount factors as a Numpy array. Used for calculating + the present values of cashflows. + + .. seealso:: + + :func:`disc_rate_mth` + """ + # return np.array(list((1 + disc_rate_mth()[t])**(-t) for t in range(max_proj_len()))) + return (1 + disc_rate_mth(t))**(-t) + + +def disc_rate(t): + """Discount rate to be applied at time t""" + scen = fixed_params()['sens_int_rate'] + curr = fixed_params()['currency'] + return scen_data(date_id(), scen).spot_rates().at[t//12, curr] + + +def disc_rate_mth(t): + """Monthly discount rate + + Nummpy array of monthly discount rates from time 0 to :func:`max_proj_len` - 1 + defined as:: + + (1 + disc_rate_ann)**(1/12) - 1 + + .. seealso:: + + :func:`disc_rate_ann` + + """ + return (1 + disc_rate(t))**(1/12) - 1 + + +def duration(t): + """Duration of model points at ``t`` in years + + .. seealso:: :func:`duration_mth` + + """ + return duration_mth(t) //12 + + +def duration_mth(t): + """Duration of model points at ``t`` in months + + Indicates how many months the policies have been in-force at ``t``. + The initial values at time 0 are read from the ``duration_mth`` column in + :attr:`model_point_table` through :func:`model_point`. + Increments by 1 as ``t`` increments. + Negative values of :func:`duration_mth` indicate future new business + policies. For example, If the :func:`duration_mth` is + -15 at time 0, the model point is issued at ``t=15``. + + .. seealso:: :func:`model_point` + + """ + if t == 0: + return duration_mth_init().values + else: + return duration_mth(t-1) + 1 + + +def duration_mth_init(): + """Initial duration in month""" + date_start = fixed_params()["base_date"] + pd.Timedelta(days=1) + entry_date = model_point()["entry_date"] + + return (date_start.year * 12 + date_start.month + - entry_date.dt.year * 12 - entry_date.dt.month) + + +def dyn_lapse_factor(t): + """Dynamic lapse factor""" + min_ = np.minimum + max_ = np.maximum + + def factor_DL001(itm): + + U = dyn_lapse_param()["U"].values + L = dyn_lapse_param()["L"].values + M = dyn_lapse_param()["M"].values + D = dyn_lapse_param()["D"].values + + return min_(U, max_(L, 1 - M * (1/itm - D))) + + def factor_DL002(itm): + + Cap = dyn_lapse_param()["FactorCap"].values + Floor = dyn_lapse_param()["FactorFloor"].values + Y = dyn_lapse_param()["Y"].values + Power = dyn_lapse_param()["Power"].values + + return min_(Cap, max_(Floor, Y * (itm**Power))) + + # return params + formula = dyn_lapse_param()["formula_id"] + itm = av_pp_at(t, "MID_MTH") / sum_assured() + + return np.where(formula == "DL001", + factor_DL001(itm), + np.where(formula == "DL002", + factor_DL002(itm), np.nan)) + + +def dyn_lapse_param(): + """Dynamic lapse parameters""" + return asmp_data(asmp_id()).dyn_lapse_params().reindex(model_point()["dyn_lapse_param_id"].values) + + +def excel_sample(point_id=1, scen=1): + """Output sample cashflows to Excel""" + import xlwings as xw + xw.App().books[0].sheets[0]["A1"].value = df = result_sample(point_id, scen) + + return df + + +def expense_acq(): + """Acquisition expense per policy""" + return fixed_params()["expense_acq"] + + +def expense_maint(): + """Annual maintenance expense per policy""" + return fixed_params()["expense_maint"] + + +def expenses(t): + """Expenses + + Expenses during the period from ``t`` to ``t+1`` + defined as the sum of acquisition expenses and maintenance expenses. + The acquisition expenses are modeled as :func:`expense_acq` + times :func:`pols_new_biz`. + The maintenance expenses are modeled as :func:`expense_maint` + times :func:`inflation_factor` times :func:`pols_if_at` before + decrement. + + .. seealso:: + + * :func:`expense_acq` + * :func:`expense_maint` + * :func:`inflation_factor` + * :func:`pols_new_biz` + * :func:`pols_if_at` + """ + + return expense_acq() * pols_new_biz(t) \ + + pols_if_at(t, "BEF_DECR") * expense_maint()/12 * inflation_factor(t) + + +def fixed_params(): + """Fixed parameters""" + params = base_data.param_list() + + const_param_names = (params[params["read_from"] == "CONST"]).index + const_params = base_data.const_params()["value"].loc[const_param_names] + + run_param_names = (params[params["read_from"] == "RUN"]).index + run_params = base_data.run_params().loc[run_id].loc[run_param_names] + + space_param_names = (params[params["read_from"] == "SPACE"]).index + space_params = base_data.space_params().loc[_space.name].loc[space_param_names] + + return pd.concat([const_params, run_params, space_params]) + + +def has_gmab(): + """Whether GMAB is attached""" + return model_point()["has_gmab"] + + +def has_gmdb(): + """Whether GMDB is attached""" + return model_point()["has_gmdb"] + + +def has_surr_charge(): + """Whether surrender charge applies + + ``True`` if surrender charge on account value applies upon lapse, + ``False`` if other wise. + By default, the value is read from the ``has_surr_charge`` column + in :func:`model_point`. + + .. seealso:: + + * :func:`model_point` + + """ + return model_point()['has_surr_charge'].values + + +def inflation_factor(t): + """The inflation factor at time t + + .. seealso:: + + * :func:`inflation_rate` + + """ + return (1 + inflation_rate())**(t/12) + + +def inflation_rate(): + """Inflation rate + + The inflation rate to be applied to the expense assumption. + By defualt it is set to ``0.01``. + + .. seealso:: + + * :func:`inflation_factor` + + """ + return 0.01 + + +def inv_income(t): + """Investment income on account value + + Investment income earned on account value during each period. + For the plicies decreased by lapse and death, half + the investment income is credited. + + .. seealso:: + + * :func:`inv_income_pp` + * :func:`pols_if_at` + * :func:`pols_death` + * :func:`pols_lapse` + + """ + return (inv_income_pp(t) * pols_if_at(t+1, "BEF_MAT") + + 0.5 * inv_income_pp(t) * (pols_death(t) + pols_lapse(t))) + + +def inv_income_pp(t): + """Investment income on account value per policy + + Investment income on account value defined as:: + + inv_return_mth(t) * av_pp_at(t, "BEF_INV") + + .. seealso:: + + * :func:`inv_return_mth` + * :func:`av_pp_at` + + """ + return inv_return_mth(t) * av_pp_at(t, "BEF_INV") + + +def inv_return_mth(t): + """Rate of investment return + + Rate of monthly investment return for :attr:`scen_id` and ``t`` + read from :func:`inv_return_table` + + .. seealso:: + + * :func:`inv_return_table` + * :attr:`scen_id` + + """ + sens = fixed_params()["sens_int_rate"] + ret_t = scen_data(date_id(), sens).return_mth().loc(axis=0)[:, t] + + ret_t = pd.DataFrame( + np.tile(ret_t.values, (len(model_point_table_ext()), 1)), + index=model_point_index(), + columns=ret_t.columns + ) + + fund_indexer = ret_t.columns.get_indexer(model_point()['fund_index']) + return ret_t.values[np.arange(len(ret_t)), fund_indexer] + + +def is_lapse_dynamic(): + """Whether the lapse assumption is dynamic""" + return fixed_params()["is_lapse_dynamic"] + + +def is_wl(): + """Whether the model point is whole life + + ``True`` if the model point is whole life, ``False`` if other wise. + By default, the value is read from the ``is_wl`` column + in :func:`model_point`. + This attribute is used to determin :func:`policy_term`. + If ``True``, :func:`policy_term` is defined + as :func:`mort_table_last_age` minus :func:`age_at_entry`. + If ``False``, :func:`policy_term` is read from :func:`model_point`. + + + .. seealso:: + + * :func:`model_point` + + """ + return model_point()['is_wl'].values + + +def lapse_rate(t): + """Lapse rate""" + if is_lapse_dynamic(): + + floor = model_point()["dyn_lapse_floor"].values + return np.maximum(floor, dyn_lapse_factor(t) * base_lapse_rate(t)) + + else: + return base_lapse_rate(t) + + +def lapse_rate_key(t): + """Index keys to retrieve lapse rates for time t""" + duration_cap = asmp_data(asmp_id()).lapse_len() + + return pd.MultiIndex.from_arrays( + [model_point()["lapse_id"], np.minimum(duration(t), duration_cap)], + names = ["lapse_id", "duration"]) + + +def load_prem_rate(): + """Rate of premium loading + + This rate times :func:`premium_pp` is collected from each premium + and the rest is added to the account value. + + By default, the value is read from the ``load_prem_rate`` column + in :func:`model_point`. + + .. seealso:: + + * :func:`premium_pp` + + """ + return model_point()['load_prem_rate'].values + + +def maint_fee(t): + """Maintenance fee deducted from account value + + .. seealso:: + + * :func:`maint_fee_pp` + + """ + return maint_fee_pp(t) * pols_if_at(t, "BEF_DECR") + + +def maint_fee_pp(t): + """Maintenance fee per policy + + .. seealso:: + + * :func:`maint_fee_rate` + * :func:`av_pp_at` + + """ + return maint_fee_rate() / 12 * av_pp_at(t, "BEF_FEE") + + +def maint_fee_rate(): + """Maintenance fee per account value + + The rate of maintenance fee on account value each month. + Set to ``0.01 / 12`` by default. + + .. seealso:: + + * :func:`maint_fee` + + """ + return model_point()["maint_fee_rate"].values + + +def margin_expense(t): + """Expense margin + + Expense margin is defined as the sum of + premium loading, surrender charge and maintenance fees + net of commissions and expenses. + + The sum of the expense margin and mortality margin add + up to the net cashflow. + + + .. seealso:: + + * :func:`load_prem_rate` + * :func:`premium_pp` + * :func:`pols_if_at` + * :func:`surr_charge` + * :func:`maint_fee` + * :func:`commissions` + * :func:`expenses` + * :func:`check_margin` + + """ + return (load_prem_rate()* premium_pp(t) * pols_if_at(t, "BEF_DECR") + + surr_charge(t) + + maint_fee(t) + - commissions(t) + - expenses(t)) + + +def margin_guarantee(t): + """Mortality margin + + Mortality margin is defined :func:`coi` net of :func:`claims_over_av`. + + The sum of the expense margin and mortality margin add + up to the net cashflow. + + .. seealso:: + + * :func:`coi` + * :func:`claims_over_av` + + """ + return coi(t) - claims_over_av(t, 'DEATH') - claims_over_av(t, 'MATURITY') + + +def max_proj_len(): + """Maximum projection length""" + return max(proj_len()) + + +def model_point(): + """Target model points + + Returns as a DataFrame the model points to be in the scope of calculation. + By default, this Cells returns the entire :func:`model_point_table_ext` + without change. + :func:`model_point_table_ext` is the extended model point table, + which extends :attr:`model_point_table` by joining the columns + in :attr:`product_spec_table`. Do not directly refer to + :attr:`model_point_table` in this formula. + To select model points, change this formula so that this + Cells returns a DataFrame that contains only the selected model points. + + Examples: + To select only the model point 1:: + + def model_point(): + return model_point_table_ext().loc[1:1] + + To select model points whose ages at entry are 40 or greater:: + + def model_point(): + return model_point_table[model_point_table_ext()["age_at_entry"] >= 40] + + Note that the shape of the returned DataFrame must be the + same as the original DataFrame, i.e. :func:`model_point_table_ext`. + + When selecting only one model point, make sure the + returned object is a DataFrame, not a Series, as seen in the example + above where ``model_point_table_ext().loc[1:1]`` is specified + instead of ``model_point_table_ext().loc[1]``. + + Be careful not to accidentally change the original table + held in :func:`model_point_table_ext`. + + .. seealso:: + + * :func:`model_point_table_ext` + + """ + mps = model_point_table_ext() + res = pd.DataFrame( + np.repeat(mps.values, len(scen_index()), axis=0), + index=model_point_index(), + columns=mps.columns + ) + + return res.astype(mps.dtypes) + + +def model_point_index(): + """Index for model points""" + mps = model_point_table_ext() + return pd.MultiIndex.from_product( + [mps.index, scen_index()], + names = mps.index.names + scen_index().names + ) + + +def model_point_table_ext(): + """Extended model point table + + Returns an extended :attr:`model_point_table` by joining + :attr:`product_spec_table` on the ``spec_id`` column. + + .. seealso:: + + * :attr:`model_point_table` + * :attr:`product_spec_table` + + """ + mp_file_id = fixed_params()["mp_file_id"] + return model_point_data(mp_file_id, _space.name).model_point_table_ext() + + +def mort_last_age(): + """The last age of mortality tables""" + return mort_data.table_last_age().reindex(mort_table_id()).values + + +def mort_rate(t): + """Mortality rates for time t""" + return mort_scalar(t) * base_mort_rate(t) + + +def mort_rate_key(t): + """Index keys to retrieve mortality rates for time t""" + duration_cap = mort_data.select_duration_len().reindex(mort_table_id()).values + + return pd.MultiIndex.from_arrays( + [mort_table_id(), age(t), np.minimum(duration(t), duration_cap)], + names = ["table_id", "att_age", "duration"]) + + +def mort_rate_mth(t): + """Monthly mortality rate to be applied at time t + + .. seealso:: + + * :attr:`mort_table` + * :func:`mort_rate` + + """ + return 1-(1- mort_rate(t))**(1/12) + + +def mort_scalar(t): + """Lapse rate + + By default, the lapse rate assumption is defined by duration as:: + + max(0.1 - 0.01 * duration(t), 0.02) + + .. seealso:: + + :func:`duration` + + """ + return asmp_data(asmp_id()).stacked_mort_scalar_tables().reindex(mort_scalar_key(t)).values + + +def mort_scalar_key(t): + """Index keys to retrieve mortality scalars for all model points at time t""" + + duration_cap = asmp_data(asmp_id()).mort_scalar_len() + + return pd.MultiIndex.from_arrays( + [model_point()["mort_scalar_id"], np.minimum(duration(t), duration_cap)], + names = ["mort_scalar_id", "duration"]) + + +def mort_table_id(): + """Mortality table IDs""" + return np.where(model_point()["sex"] == "M", + model_point()["mort_table_male"], + model_point()["mort_table_female"]) + + +def net_amt_at_risk(t): + """Net amount at risk per policy + + Return sum assured net of account value per policy. + + .. seealso:: + + * :func:`sum_assured` + * :func:`av_pp_at` + + + """ + return np.maximum(sum_assured() - av_pp_at(t, 'BEF_FEE'), 0) + + +def net_cf(t): + """Net cashflow + + Net cashflow for the period from ``t`` to ``t+1`` defined as:: + + premiums(t) - claims(t) - expenses(t) - commissions(t) + + .. seealso:: + + * :func:`premiums` + * :func:`claims` + * :func:`expenses` + * :func:`commissions` + + """ + return (premiums(t) + + inv_income(t) - claims(t) - expenses(t) - commissions(t) - av_change(t)) + + +def policy_term(): + """The policy term of the model points. + + The ``policy_term`` column of the DataFrame returned by + :func:`model_point`. + """ + + return (is_wl() * (mort_last_age() - age_at_entry()) + + (is_wl() == False) * model_point()["policy_term"].values) + + +def pols_death(t): + """Number of death + + Number of policies decreased by death between ``t`` and ``t+1`` + """ + return pols_if_at(t, "BEF_DECR") * mort_rate_mth(t) + + +def pols_if(t): + """Number of policies in-force + + :func:`pols_if(t)` is an alias + for :func:`pols_if_at(t, "BEF_MAT")`. + + .. seealso:: + * :func:`pols_if_at` + + """ + return pols_if_at(t, "BEF_MAT") + + +def pols_if_at(t, timing): + """Number of policies in-force + + :func:`pols_if_at(t, timing)` calculates + the number of policies in-force at time ``t``. + The second parameter ``timing`` takes a string value to + indicate the timing of in-force, + which is either + ``"BEF_MAT"``, ``"BEF_NB"`` or ``"BEF_DECR"``. + + .. rubric:: BEF_MAT + + The number of policies in-force before maturity after lapse and death. + At time 0, the value is read from :func:`pols_if_init`. + For time > 0, defined as:: + + pols_if_at(t-1, "BEF_DECR") - pols_lapse(t-1) - pols_death(t-1) + + .. rubric:: BEF_NB + + The number of policies in-force before new business after maturity. + Defined as:: + + pols_if_at(t, "BEF_MAT") - pols_maturity(t) + + .. rubric:: BEF_DECR + + The number of policies in-force before lapse and death after new business. + Defined as:: + + pols_if_at(t, "BEF_NB") + pols_new_biz(t) + + .. seealso:: + * :func:`pols_if_init` + * :func:`pols_lapse` + * :func:`pols_death` + * :func:`pols_maturity` + * :func:`pols_new_biz` + * :func:`pols_if` + + """ + if timing == "BEF_MAT": + + if t == 0: + return pols_if_init() + else: + return pols_if_at(t-1, "BEF_DECR") - pols_lapse(t-1) - pols_death(t-1) + + elif timing == "BEF_NB": + + return pols_if_at(t, "BEF_MAT") - pols_maturity(t) + + elif timing == "BEF_DECR": + + return pols_if_at(t, "BEF_NB") + pols_new_biz(t) + + else: + raise ValueError("invalid timing") + + +def pols_if_init(): + """Initial number of policies in-force + + Number of in-force policies at time 0 referenced from + :func:`pols_if_at(0, "BEF_MAT")`. + """ + return model_point()["policy_count"].where(duration_mth(0) > 0, other=0).values + + +def pols_lapse(t): + """Number of lapse + + Number of policies decreased by lapse during ``t`` and ``t+1``. + + .. seealso:: + * :func:`pols_if_at` + * :func:`lapse_rate` + + """ + return (pols_if_at(t, "BEF_DECR") - pols_death(t)) * (1-(1 - lapse_rate(t))**(1/12)) + + +def pols_maturity(t): + """Number of maturing policies + + The policy maturity occurs when + :func:`duration_mth` equals 12 times :func:`policy_term`. + The amount is equal to :func:`pols_if_at(t, "BEF_MAT")`. + + otherwise ``0``. + """ + return (duration_mth(t) == policy_term() * 12) * pols_if_at(t, "BEF_MAT") + + +def pols_new_biz(t): + """Number of new business policies + + The number of new business policies. + The value :func:`duration_mth(0)` + for the selected model point is read from the ``policy_count`` column in + :func:`model_point`. If the value is 0 or negative, + the model point is new business at t=0 or at t when + :func:`duration_mth(t)` is 0, and the + :func:`pols_new_biz(t)` is read from the ``policy_count`` + in :func:`model_point`. + + .. seealso:: + * :func:`model_point` + + """ + return model_point()['policy_count'].values * (duration_mth(t) == 0) + + +def prem_to_av(t): + """Premium portion put in account value + + The amount of premiums net of loadings, which is put in the accoutn value. + + .. seealso:: + + * :func:`load_prem_rate` + * :func:`premium_pp` + * :func:`pols_if_at` + + """ + return prem_to_av_pp(t) * pols_if_at(t, "BEF_DECR") + + +def prem_to_av_pp(t): + """Per-policy premium portion put in the account value + + The amount of premium per policy net of loading, + which is put in the accoutn value. + + .. seealso:: + + * :func:`load_prem_rate` + * :func:`premium_pp` + * :func:`pols_if_at` + + """ + return (1 - load_prem_rate()) * premium_pp(t) + + +def premium_pp(t): + """Premium amount per policy + + Single premium amount if :func:`premium_type` is ``"SINGLE"``, + monthly premium amount if :func:`premium_type` is ``"LEVEL"``. + + .. seealso:: + + * :func:`premium_type` + * :func:`sum_assured` + * :func:`age_at_entry` + * :func:`policy_term` + + """ + return model_point()['premium_pp'].values * ( + (premium_type() == 'SINGLE') & (duration_mth(t) == 0) | + (premium_type() == 'LEVEL') & (duration_mth(t) < 12 * policy_term())) + + +def premium_type(): + """Type of premium payment + + Returns a string indicating the payment type, which is either + ``"LEVEL"`` if level payment, or ``"SINGLE"`` if single payment. + + """ + return model_point()['premium_type'].values + + +def premiums(t): + """Premium income + + Premium income during the period from ``t`` to ``t+1`` defined as:: + + premium_pp() * pols_if_at(t, "BEF_DECR") + + .. seealso:: + + * :func:`premium_pp` + * :func:`pols_if_at` + + """ + return premium_pp(t) * pols_if_at(t, "BEF_DECR") + + +def proj_len(): + """Projection length in months + + :func:`proj_len` returns how many months the projection + for each model point should be carried out + for all the model point. Defined as:: + + np.maximum(12 * policy_term() - duration_mth(0) + 1, 0) + + Since this model carries out projections for all the model points + simultaneously, the projections are actually carried out + from 0 to :attr:`max_proj_len` for all the model points. + + .. seealso:: + + * :func:`policy_term` + * :func:`duration_mth` + * :attr:`max_proj_len` + + """ + return np.maximum(12 * policy_term() - duration_mth(0) + 1, 0) + + +def pv_av_change(): + """Present value of change in account value + + .. seealso:: + + * :func:`av_change` + * :func:`disc_factors` + * :func:`proj_len` + + """ + return sum(av_change(t) * disc_factors(t) for t in range(max_proj_len())) + + +def pv_claims(kind=None): + """Present value of claims + + See :func:`claims` for the parameter ``kind``. + + .. seealso:: + + * :func:`claims` + * :func:`proj_len` + * :func:`disc_factors` + + + """ + return sum(claims(t, kind) * disc_factors(t) for t in range(max_proj_len())) + + +def pv_claims_from_av(kind=None): + """Present value of claims + + See :func:`claims` for the parameter ``kind``. + + .. seealso:: + + * :func:`claims` + * :func:`proj_len` + * :func:`disc_factors` + + + """ + return sum(claims_from_av(t, kind) * disc_factors(t) for t in range(max_proj_len())) + + +def pv_claims_over_av(kind=None): + """Present value of claims + + See :func:`claims` for the parameter ``kind``. + + .. seealso:: + + * :func:`claims` + * :func:`proj_len` + * :func:`disc_factors` + + + """ + return sum(claims_over_av(t, kind) * disc_factors(t) for t in range(max_proj_len())) + + +def pv_commissions(): + """Present value of commissions + + .. seealso:: + + * :func:`expenses` + * :func:`proj_len` + * :func:`disc_factors` + + """ + return sum(commissions(t) * disc_factors(t) for t in range(max_proj_len())) + + +def pv_expenses(): + """Present value of expenses + + .. seealso:: + + * :func:`expenses` + * :func:`proj_len` + * :func:`disc_factors` + + """ + return sum(expenses(t) * disc_factors(t) for t in range(max_proj_len())) + + +def pv_inv_income(): + """Present value of investment income + + The discounted sum of monthly investment income. + + .. seealso:: + + * :func:`inv_income` + * :func:`proj_len` + * :func:`disc_factors` + + """ + return sum(inv_income(t) * disc_factors(t) for t in range(max_proj_len())) + + +def pv_maint_fee(): + """Present value of maintenance fees""" + return sum(maint_fee(t) * disc_factors(t) for t in range(max_proj_len())) + + +def pv_net_cf(): + """Present value of net cashflows. + + Defined as:: + + pv_premiums() + pv_inv_income() + - pv_claims() - pv_expenses() - pv_commissions() - pv_av_change() + + .. seealso:: + + * :func:`pv_premiums` + * :func:`pv_claims` + * :func:`pv_expenses` + * :func:`pv_commissions` + + """ + return (pv_premiums() + + pv_inv_income() + - pv_claims() + - pv_expenses() + - pv_commissions() + - pv_av_change()) + + +def pv_pols_if(): + """Present value of policies in-force + + .. note:: + This cells is not used by default. + + The discounted sum of the number of in-force policies at each month. + It is used as the annuity factor for calculating :func:`net_premium_pp`. + + """ + return sum(pols_if_at(t, "BEF_DECR") * disc_factors(t) for t in range(max_proj_len())) + + +def pv_premiums(): + """Present value of premiums + + .. seealso:: + + * :func:`premiums` + * :func:`proj_len` + * :func:`disc_factors` + + """ + return sum(premiums(t) * disc_factors(t) for t in range(max_proj_len())) + + +def result_cf(): + """Result table of cashflows + + .. seealso:: + + * :func:`premiums` + * :func:`claims` + * :func:`expenses` + * :func:`commissions` + * :func:`net_cf` + + """ + + t_len = range(max_proj_len()) + + data = { + "Premiums": [sum(premiums(t)) for t in t_len], + "Claims": [sum(claims(t)) for t in t_len], + "Expenses": [sum(expenses(t)) for t in t_len], + "Commissions": [sum(commissions(t)) for t in t_len], + "Net Cashflow": [sum(net_cf(t)) for t in t_len] + } + + return pd.DataFrame(data, index=t_len) + + +def result_pols(): + """Result table of policy decrement + + .. seealso:: + + * :func:`pols_if` + * :func:`pols_maturity` + * :func:`pols_new_biz` + * :func:`pols_death` + * :func:`pols_lapse` + + """ + + t_len = range(max_proj_len()) + + data = { + "pols_if": [sum(pols_if(t)) for t in t_len], + "pols_maturity": [sum(pols_maturity(t)) for t in t_len], + "pols_new_biz": [sum(pols_new_biz(t)) for t in t_len], + "pols_death": [sum(pols_death(t)) for t in t_len], + "pols_lapse": [sum(pols_lapse(t)) for t in t_len] + } + + return pd.DataFrame(data, index=t_len) + + +def result_pv(): + """Result table of present value of cashflows + + .. seealso:: + + * :func:`pv_premiums` + * :func:`pv_claims` + * :func:`pv_expenses` + * :func:`pv_commissions` + * :func:`pv_net_cf` + + """ + + data = { + "Premiums": pv_premiums(), + "Death": pv_claims("DEATH"), + "Surrender": pv_claims("LAPSE"), + "Maturity": pv_claims("MATURITY"), + "Expenses": pv_expenses(), + "Commissions": pv_commissions(), + "Investment Income": pv_inv_income(), + "Change in AV": pv_av_change(), + "Net Cashflow": pv_net_cf() + } + + return pd.DataFrame(data, index=model_point().index) + + +def result_sample(point_id=1, scen=1): + """Sample projection result for a specific model point and scenario""" + + items = [ + + # Cashflows + "premiums", + "inv_income", + "claims", + ["claims", "DEATH"], + ["claims", "LAPSE"], + ["claims", "MATURITY"], + "expenses", + "commissions", + "av_change", + "net_cf", + "blank", + + # Margin Analysis + + # Account Value Roll-forward + ["av_at", "BEF_MAT"], + "prem_to_av", + "maint_fee", + "coi", + "inv_income", + ["claims_from_av", "DEATH"], + ["claims_from_av", "LAPSE"], + ["claims_from_av", "MATURITY"], + "blank", + + # Per policy Values + ["av_pp_at", "BEF_PREM"], + "premiums", + "inv_income_pp", + ["claim_pp", "DEATH"], + ["claim_pp", "LAPSE"], + ["claim_pp", "MATURITY"], + "blank", + + # Policy Decrement + "pols_if", + "pols_maturity", + "pols_new_biz", + "pols_death", + "pols_lapse", + "mort_rate", + "lapse_rate", + "dyn_lapse_factor", + "blank", + + # Economic assumptions + "inv_return_mth", + "disc_rate_mth" + ] + + + iloc = model_point_index().get_loc((point_id, scen)) + t_len = proj_len()[iloc] + + data = {} + for item in items: + + if isinstance(item, str): + name, args = item, () + else: + name, args = item[0], item[1:] + + key = name + (("_" + "_".join(map(str, args))) if args else "") + + if key == "blank": + val = [np.nan] * t_len + else: + cells = _space.cells[name] + if isinstance(cells(0, *args), (np.ndarray, pd.Series)): + val = [cells(t, *args)[iloc] for t in range(t_len)] + else: + val = [cells(t, *args) for t in range(t_len)] + + i=2 + key0 = key + while key in data: + key = f"{key0}({i})" + i += 1 + + data[key] = val + + + return pd.DataFrame(data, index=range(t_len)) + + +def scen_index(): + sens = fixed_params()["sens_int_rate"] + return scen_data(date_id(), sens).return_mth().loc(axis=0)[:, 0].index.get_level_values('scen') + + +def sex(): + """The sex of the model points + + .. note:: + This cells is not used by default. + + The ``sex`` column of the DataFrame returned by + :func:`model_point`. + """ + return model_point()["sex"].values + + +def sum_assured(): + """The sum assured of the model points + + The ``sum_assured`` column of the DataFrame returned by + :func:`model_point`. + """ + return model_point()['sum_assured'].values + + +def surr_charge(t): + """Surrender charge + + Surrender charge rate times account values of lapsed policies + + .. seealso:: + + * :func:`surr_charge_rate` + * :func:`av_pp_at` + * :func:`pols_lapse` + * :func:`proj_len` + * :func:`disc_factors` + + """ + return surr_charge_rate(t) * av_pp_at(t, "MID_MTH") * pols_lapse(t) + + +def surr_charge_id(): + """ID of surrender charge pattern + + A string to indicate the ID of the surrender charge pattern. + The ID should be one of the column names in :attr:`surr_charge_table` + if :func:`has_surr_charge` is ``True``. + + .. seealso:: + + * :attr:`surr_charge_table` + * :func:`has_surr_charge` + + """ + return model_point()['surr_charge_id'] + + +def surr_charge_key(t): + """Index keys to retrieve surrender charge rates at time t""" + duration_cap = base_data.surr_charge_len() + + return pd.MultiIndex.from_arrays( + [surr_charge_id(), np.minimum(duration(t), duration_cap)], + names=["surr_charge_id", "duration"]) + + +def surr_charge_rate(t): + """Surrender charge rate + + Surrender charge rate to be applied for lapsed policies + + .. seealso:: + + * :func:`surr_charge_rate` + * :func:`av_pp_at` + * :func:`pols_lapse` + * :func:`proj_len` + * :func:`disc_factors` + * :func:`surr_charge_max_idx` + * :func:`surr_charge_table_stacked` + """ + return base_data.stacked_surr_charge_tables().reindex( + surr_charge_key(t), fill_value=0).set_axis( + model_point().index).values + + +# --------------------------------------------------------------------------- +# References + +base_data = ("Interface", ("..", "BaseData"), "auto") + +model_point_data = ("Interface", ("..", "ModelPoints"), "auto") + +scen_data = ("Interface", ("..", "Scenarios"), "auto") + +mort_data = ("Interface", ("..", "Mortality"), "auto") + +asmp_data = ("Interface", ("..", "Assumptions"), "auto") \ No newline at end of file diff --git a/lifelib/libraries/appliedlife/IntegratedLife/Run/GLWB/__init__.py b/lifelib/libraries/appliedlife/IntegratedLife/Run/GLWB/__init__.py new file mode 100644 index 0000000..74e1436 --- /dev/null +++ b/lifelib/libraries/appliedlife/IntegratedLife/Run/GLWB/__init__.py @@ -0,0 +1,12 @@ +from modelx.serialize.jsonvalues import * + +_formula = None + +_bases = [ + "..ProductBase" +] + +_allow_none = None + +_spaces = [] + diff --git a/lifelib/libraries/appliedlife/IntegratedLife/Run/GMXB/__init__.py b/lifelib/libraries/appliedlife/IntegratedLife/Run/GMXB/__init__.py new file mode 100644 index 0000000..b589336 --- /dev/null +++ b/lifelib/libraries/appliedlife/IntegratedLife/Run/GMXB/__init__.py @@ -0,0 +1,21 @@ +"""Product space for GMXB products + +:mod:`~appliedlife.IntegratedLife.Run.GMXB` is a product space for GMDB and GMAB products. +It is a child space of :mod:`~appliedlife.IntegratedLife.Run`, +and a subspace of :mod:`~appliedlife.IntegratedLife.ProductBase`. +All the contents are inherited from :mod:`~appliedlife.IntegratedLife.ProductBase`, +so see :mod:`~appliedlife.IntegratedLife.ProductBase` for the details. +""" + +from modelx.serialize.jsonvalues import * + +_formula = lambda product_id, segment_id="ALL": None + +_bases = [ + "..ProductBase" +] + +_allow_none = None + +_spaces = [] + diff --git a/lifelib/libraries/appliedlife/IntegratedLife/Run/__init__.py b/lifelib/libraries/appliedlife/IntegratedLife/Run/__init__.py new file mode 100644 index 0000000..c695f8a --- /dev/null +++ b/lifelib/libraries/appliedlife/IntegratedLife/Run/__init__.py @@ -0,0 +1,70 @@ +"""Projection runs + +The :mod:`~appliedlife.IntegratedLife.Run` space represents projection runs. +This space is parameterized with :attr:`run_id`, and for each value +of :attr:`run_id`, a dynamic subspace is created, +representing a specific run associated to the :attr:`run_id`. + +.. rubric:: Parameters + +Attributes: + + run_id: an integer key representing the run identity + + +Example: + + The sample code below shows how to create ``Run[1]`` and + examine its contents. + + .. code-block:: python + + >>> import modelx as mx + + >>> m = mx.read_model("IntegratedLife") + + >>> m.Run[1] + + + >>> m.Run[1].run_id + 1 + + >>> m.Run[1].GMXB.run_id + 1 + + >>> m.Run[1].GMXB.result_pv() + + Premiums Death ... Change in AV Net Cashflow + point_id scen ... + 1 1 50000000.0 4.535395e+06 ... 1.141797e+07 6.097680e+06 + 2 50000000.0 4.592794e+06 ... 1.244402e+07 6.732840e+06 + 3 50000000.0 4.514334e+06 ... 1.215701e+07 6.499784e+06 + 4 50000000.0 4.667772e+06 ... 1.250695e+07 6.648902e+06 + 5 50000000.0 4.403177e+06 ... 1.138434e+07 6.107113e+06 + ... ... ... ... ... + 8 96 32500000.0 3.013149e+06 ... 5.651292e+06 -1.669267e+07 + 97 32500000.0 3.050556e+06 ... 8.690729e+06 -6.839240e+05 + 98 32500000.0 3.013149e+06 ... 3.701018e+06 -1.436214e+07 + 99 32500000.0 3.230455e+06 ... 8.484874e+06 1.174996e+06 + 100 32500000.0 3.013149e+06 ... 7.439794e+06 -1.503688e+06 + + [800 rows x 9 columns] +""" + +from modelx.serialize.jsonvalues import * + +_formula = lambda run_id: None + +_bases = [] + +_allow_none = None + +_spaces = [ + "GMXB", + "GLWB" +] + +# --------------------------------------------------------------------------- +# References + +run_id = 1 \ No newline at end of file diff --git a/lifelib/libraries/appliedlife/IntegratedLife/Run/_data/_dynamic_inputs b/lifelib/libraries/appliedlife/IntegratedLife/Run/_data/_dynamic_inputs new file mode 100644 index 0000000..e69de29 diff --git a/lifelib/libraries/appliedlife/IntegratedLife/Scenarios/__init__.py b/lifelib/libraries/appliedlife/IntegratedLife/Scenarios/__init__.py new file mode 100644 index 0000000..ac225f8 --- /dev/null +++ b/lifelib/libraries/appliedlife/IntegratedLife/Scenarios/__init__.py @@ -0,0 +1,277 @@ +"""The space representing economic data + +This space is parameterized with :attr:`date_id` and :attr:`sens_id`. +For each combination of :attr:`date_id` and :attr:`sens_id` values, +a dynamic subspace of this space is created, +representing a specific set of economic assumptions. + +By default, the following scenarios are supported. +Users should customize the contents of this space to meet their own needs. + +* Deterministic interest rate scenarios +* Stochastic risk-neutral index return scenarios + +For the interest rate scenarios, +:func:`spot_rates` in this space reads annual spot rates from an Excel file into it. +:func:`spot_rates` uses sens_is as a key to select a sheet from the file. + +For the stochastic risk-neutral index return scenarios, +:func:`log_return_mth` generates stochastic returns, +from the interest rates and volatility parameters read from an Excel file. + +.. rubric:: Parameters + +Attributes: + + date_id: a string key representing the base date + sens_id: a string key representing interest rate sensitivity, which is either + "BASE", "UP" or "DOWN". + +.. rubric:: References in the space + +Attributes: + + base_data: Reference to the :mod:`~appliedlife.IntegratedLife.BaseData` space + + +Example: + + The sample code below demonstrates how to examine the contents of + :mod:`~appliedlife.IntegratedLife.Scenarios` + for specific values of :attr:`date_id` and :attr:`sens_id`, '202312' and 'BASE'. + + .. code-block:: python + + >>> import modelx as mx + + >>> m = mx.read_model("IntegratedLife") + + >>> m.Scenarios['202312', 'BASE'].spot_rates() + + EUR GBP JPY USD + 0 0.03357 0.04735 0.00072 0.04760 + 1 0.02690 0.04021 0.00191 0.04056 + 2 0.02439 0.03668 0.00280 0.03724 + 3 0.02350 0.03475 0.00363 0.03571 + 4 0.02323 0.03355 0.00448 0.03499 + .. ... ... ... ... + 145 0.03241 0.03229 0.03006 0.03364 + 146 0.03243 0.03231 0.03010 0.03365 + 147 0.03244 0.03232 0.03013 0.03365 + 148 0.03245 0.03234 0.03016 0.03366 + 149 0.03247 0.03235 0.03019 0.03366 + + [150 rows x 4 columns] + + >>> m.Scenarios['202312', 'BASE'].forward_rates() + + EUR GBP JPY USD + 0 0.033570 0.047350 0.000720 0.047600 + 1 0.020273 0.033119 0.003101 0.033567 + 2 0.019388 0.029656 0.004582 0.030632 + 3 0.020835 0.028982 0.006124 0.031134 + 4 0.022151 0.028764 0.007887 0.032115 + .. ... ... ... ... + 145 0.033861 0.033741 0.034419 0.035091 + 146 0.035354 0.035234 0.035957 0.035111 + 147 0.033911 0.033791 0.034550 0.033650 + 148 0.033931 0.035304 0.034610 0.035141 + 149 0.035454 0.033841 0.034670 0.033660 + + [150 rows x 4 columns] + + >>> m.Scenarios['202312', 'BASE'].cont_fwd_rates() + + EUR GBP JPY USD + 0 0.033019 0.046263 0.000720 0.046502 + 1 0.020070 0.032582 0.003097 0.033016 + 2 0.019203 0.029225 0.004572 0.030172 + 3 0.020621 0.028570 0.006105 0.030659 + 4 0.021909 0.028358 0.007856 0.031610 + .. ... ... ... ... + 145 0.033300 0.033184 0.033840 0.034489 + 146 0.034744 0.034628 0.035325 0.034509 + 147 0.033349 0.033233 0.033966 0.033096 + 148 0.033368 0.034695 0.034024 0.034538 + 149 0.034840 0.033281 0.034082 0.033106 + + [150 rows x 4 columns] + + >>> m.Scenarios['202312', 'BASE'].log_return_mth() + + FUND1 FUND2 FUND3 FUND4 FUND5 FUND6 + scen t + 1 0 -0.030397 0.047032 -0.010060 0.000816 0.000665 -0.040567 + 1 -0.029103 0.025734 0.004162 -0.018741 0.084592 0.058125 + 2 -0.015052 0.034508 -0.005399 0.003108 0.030602 -0.070345 + 3 0.015784 0.051717 0.015262 0.000348 0.034553 -0.091414 + 4 -0.001168 0.018826 -0.015521 0.002865 0.063022 0.153368 + ... ... ... ... ... ... + 100 1795 0.005044 0.005891 0.006421 -0.009772 0.006747 -0.018034 + 1796 0.005050 -0.030197 -0.027247 0.002810 -0.017504 0.011297 + 1797 0.070869 0.008339 0.012401 -0.002405 0.014219 -0.023541 + 1798 -0.001515 0.049597 0.013523 -0.015077 0.070503 0.027821 + 1799 0.000753 -0.019089 0.017222 0.004629 0.005042 0.000108 + + [180000 rows x 6 columns] + + >>> m.Scenarios['202312', 'BASE'].return_mth() + + FUND1 FUND2 FUND3 FUND4 FUND5 FUND6 + scen t + 1 0 -0.029940 0.048156 -0.010010 0.000816 0.000665 -0.039755 + 1 -0.028684 0.026068 0.004171 -0.018567 0.088273 0.059847 + 2 -0.014940 0.035111 -0.005384 0.003113 0.031075 -0.067928 + 3 0.015909 0.053078 0.015379 0.000348 0.035157 -0.087360 + 4 -0.001168 0.019004 -0.015401 0.002869 0.065050 0.165754 + ... ... ... ... ... ... + 100 1795 0.005057 0.005909 0.006442 -0.009724 0.006770 -0.017872 + 1796 0.005063 -0.029746 -0.026880 0.002814 -0.017352 0.011361 + 1797 0.073441 0.008374 0.012478 -0.002402 0.014320 -0.023266 + 1798 -0.001514 0.050848 0.013615 -0.014964 0.073048 0.028211 + 1799 0.000753 -0.018908 0.017371 0.004640 0.005055 0.000108 + + [180000 rows x 6 columns] +""" + +from modelx.serialize.jsonvalues import * + +_formula = lambda date_id, sens_id: None + +_bases = [] + +_allow_none = None + +_spaces = [] + +# --------------------------------------------------------------------------- +# Cells + +def cont_fwd_rates(): + """Continuous compound forward rates""" + return np.log(1 + forward_rates()) + + +def forward_rates(): + """Forward interest rates by duration and currency + + Returns annual forward interest rates for multiple currencies + as a pandas DataFrame, calculated from :func:`spot_rates`. + """ + df = (1 + spot_rates()).pow(spot_rates().index + 1, axis=0) + return df / df.shift(fill_value=1) - 1 + + +def index_count(): + """The number of func indexes""" + return index_params().index.size + + +def index_params(): + """Fund index parameters + + Reads fund index parameters from an Excel file, + and returns a DataFrame whose columns represents the parameters, + and whose index represents fund index IDs. + """ + + dir_name: str = base_data.const_params().at["scen_dir", "value"] + file_name: str = base_data.const_params().at["scen_param_file", "value"] + + file = _model.path.parent / dir_name / file_name + df = pd.read_excel(file, + sheet_name="Params", + index_col=0) + + return df.T.astype( + {"currency": "object", + "return": "float64", + "volatility": "float64"}) + + +def index_vols(): + """Volatilities of fund indexes""" + return index_params()["volatility"] + + +def log_return_mth(): + """Stochastic scenarios of fund indexes as monthly risk-neutral log returns + + Generates stochastic scenarios of fund indexes + Generates monthly risk-neutral log returns of fund indexes, + Returns a DataFrame with columns of fund IDs + and with a MultiIndex with two levels, scenario ID and time in month. + """ + + # Initialize random number generator + rng = np.random.default_rng(12345) + + # Define parameters + dt = 1/12 + rf = cont_fwd_rates()[index_params()["currency"]].loc[np.repeat(cont_fwd_rates().index, 12)] + vols = index_vols().values + mean = (rf - 0.5 * vols**2) * dt + var = vols * dt**0.5 + + # Generate + result = np.zeros((scen_size() * scen_len() * 12, index_count())) + for i in range(scen_size()): + scen = rng.normal(loc=mean, scale=var) + result[i * scen.shape[0]:(i + 1) * scen.shape[0], 0:scen.shape[1]] = scen + + return pd.DataFrame(result, index=scen_index(), columns=index_params().index) + + +def mth_index(): + return scenarios() + + +def return_mth(): + """Monthly index returns""" + return np.exp(log_return_mth()) - 1 + + +def scen_index(): + """pandas MultiIndex for the scenarios""" + return pd.MultiIndex.from_product( + [range(1, scen_size() + 1), range(12 * scen_len())], + names=["scen", "t"]) + + +def scen_len(): + """The length of scenarios in years""" + return len(spot_rates()) + + +def scen_size(): + """The number of scenarios""" + return 100 + + +def spot_rates(): + """Spot interest rates by duration and currency + + Reads annual spot interest rates for multiple currencies from an Excel file, + and returns them as a pandas DataFrame. + The index and columns of the DataFrame represents duration + years and currencies respectively + + The directory of the Excel file is specified by the user as a constant + parameter named "scen_dir". + """ + + dir_name: str = base_data.const_params().at["scen_dir", "value"] + file_prefix: str = base_data.const_params().at["scen_file_prefix", "value"] + + path = _model.path.parent / dir_name / f"{file_prefix}_{date_id}.xlsx" + return pd.read_excel(path, sheet_name=sens_id, index_col=0) + + +# --------------------------------------------------------------------------- +# References + +base_data = ("Interface", ("..", "BaseData"), "auto") + +date_id = "202312" + +sens_id = "BASE" \ No newline at end of file diff --git a/lifelib/libraries/appliedlife/IntegratedLife/Scenarios/_data/_dynamic_inputs b/lifelib/libraries/appliedlife/IntegratedLife/Scenarios/_data/_dynamic_inputs new file mode 100644 index 0000000..e69de29 diff --git a/lifelib/libraries/appliedlife/IntegratedLife/__init__.py b/lifelib/libraries/appliedlife/IntegratedLife/__init__.py new file mode 100644 index 0000000..3852bbe --- /dev/null +++ b/lifelib/libraries/appliedlife/IntegratedLife/__init__.py @@ -0,0 +1,22 @@ +from modelx.serialize.jsonvalues import * + +_name = "IntegratedLife" + +_allow_none = False + +_spaces = [ + "BaseData", + "Mortality", + "ProductBase", + "Run", + "ModelPoints", + "Scenarios", + "Assumptions" +] + +# --------------------------------------------------------------------------- +# References + +np = ("Module", "numpy") + +pd = ("Module", "pandas") \ No newline at end of file diff --git a/lifelib/libraries/appliedlife/IntegratedLife/_system.json b/lifelib/libraries/appliedlife/IntegratedLife/_system.json new file mode 100644 index 0000000..e5952d9 --- /dev/null +++ b/lifelib/libraries/appliedlife/IntegratedLife/_system.json @@ -0,0 +1 @@ +{"modelx_version": [0, 25, 1], "serializer_version": 6} \ No newline at end of file diff --git a/lifelib/libraries/appliedlife/economic_data/index_parameters.xlsx b/lifelib/libraries/appliedlife/economic_data/index_parameters.xlsx new file mode 100644 index 0000000..1bee284 Binary files /dev/null and b/lifelib/libraries/appliedlife/economic_data/index_parameters.xlsx differ diff --git a/lifelib/libraries/appliedlife/economic_data/risk_free_202212.xlsx b/lifelib/libraries/appliedlife/economic_data/risk_free_202212.xlsx new file mode 100644 index 0000000..dc956ae Binary files /dev/null and b/lifelib/libraries/appliedlife/economic_data/risk_free_202212.xlsx differ diff --git a/lifelib/libraries/appliedlife/economic_data/risk_free_202312.xlsx b/lifelib/libraries/appliedlife/economic_data/risk_free_202312.xlsx new file mode 100644 index 0000000..1196e74 Binary files /dev/null and b/lifelib/libraries/appliedlife/economic_data/risk_free_202312.xlsx differ diff --git a/lifelib/libraries/appliedlife/input_tables/assumptions_202212.xlsx b/lifelib/libraries/appliedlife/input_tables/assumptions_202212.xlsx new file mode 100644 index 0000000..f1de6e0 Binary files /dev/null and b/lifelib/libraries/appliedlife/input_tables/assumptions_202212.xlsx differ diff --git a/lifelib/libraries/appliedlife/input_tables/assumptions_202312.xlsx b/lifelib/libraries/appliedlife/input_tables/assumptions_202312.xlsx new file mode 100644 index 0000000..48540ef Binary files /dev/null and b/lifelib/libraries/appliedlife/input_tables/assumptions_202312.xlsx differ diff --git a/lifelib/libraries/appliedlife/input_tables/mortality_tables.xlsx b/lifelib/libraries/appliedlife/input_tables/mortality_tables.xlsx new file mode 100644 index 0000000..3b2c4eb Binary files /dev/null and b/lifelib/libraries/appliedlife/input_tables/mortality_tables.xlsx differ diff --git a/lifelib/libraries/appliedlife/input_tables/product_spec_tables.xlsx b/lifelib/libraries/appliedlife/input_tables/product_spec_tables.xlsx new file mode 100644 index 0000000..788172c Binary files /dev/null and b/lifelib/libraries/appliedlife/input_tables/product_spec_tables.xlsx differ diff --git a/lifelib/libraries/appliedlife/model_parameters.xlsx b/lifelib/libraries/appliedlife/model_parameters.xlsx new file mode 100644 index 0000000..79b71b9 Binary files /dev/null and b/lifelib/libraries/appliedlife/model_parameters.xlsx differ diff --git a/lifelib/libraries/appliedlife/model_point_data/model_point_2022Q4IF_GMXB.csv b/lifelib/libraries/appliedlife/model_point_data/model_point_2022Q4IF_GMXB.csv new file mode 100644 index 0000000..9caaca0 --- /dev/null +++ b/lifelib/libraries/appliedlife/model_point_data/model_point_2022Q4IF_GMXB.csv @@ -0,0 +1,9 @@ +point_id,product_id,plan_id,entry_date,age_at_entry,sex,policy_term,fund_index,policy_count,sum_assured,duration_mth,premium_pp,av_pp_init,accum_prem_init_pp +1,GMDB,PLAN_A,2020/10/15,70,M,10,FUND4,100,500000,0,500000,475000,500000 +2,GMDB,PLAN_A,2019/08/16,70,M,10,FUND4,100,500000,0,475000,451250,475000 +3,GMDB,PLAN_B,2018/03/03,70,M,10,FUND5,100,500000,0,450000,427500,450000 +4,GMDB,PLAN_B,2018/12/05,70,M,10,FUND6,100,500000,0,425000,403750,425000 +5,GMAB,PLAN_A,2020/10/15,70,M,10,FUND4,100,500000,0,400000,380000,400000 +6,GMAB,PLAN_A,2019/08/16,70,M,10,FUND4,100,500000,0,375000,356250,375000 +7,GMAB,PLAN_B,2018/03/03,70,M,10,FUND5,100,500000,0,350000,332500,350000 +8,GMAB,PLAN_B,2018/12/05,70,M,10,FUND6,100,500000,0,325000,308750,325000 diff --git a/lifelib/libraries/appliedlife/model_point_data/model_point_2023Q4IF_GMXB.csv b/lifelib/libraries/appliedlife/model_point_data/model_point_2023Q4IF_GMXB.csv new file mode 100644 index 0000000..f4d70c3 --- /dev/null +++ b/lifelib/libraries/appliedlife/model_point_data/model_point_2023Q4IF_GMXB.csv @@ -0,0 +1,9 @@ +point_id,product_id,plan_id,entry_date,age_at_entry,sex,policy_term,fund_index,policy_count,sum_assured,duration_mth,premium_pp,av_pp_init,accum_prem_init_pp +1,GMDB,PLAN_A,2020/10/15,70,M,10,FUND6,100,500000,0,500000,550000,500000 +2,GMDB,PLAN_A,2019/08/16,70,F,10,FUND4,100,500000,0,475000,522500,475000 +3,GMDB,PLAN_B,2018/03/03,70,M,10,FUND5,100,500000,0,450000,495000,450000 +4,GMDB,PLAN_B,2018/12/05,70,F,10,FUND6,100,500000,0,425000,467500,425000 +5,GMAB,PLAN_A,2020/10/15,70,M,10,FUND4,100,500000,0,400000,440000,400000 +6,GMAB,PLAN_A,2019/08/16,70,F,10,FUND4,100,500000,0,375000,412500,375000 +7,GMAB,PLAN_B,2018/03/03,70,M,10,FUND5,100,500000,0,350000,385000,350000 +8,GMAB,PLAN_B,2018/12/05,70,F,10,FUND6,100,500000,0,325000,357500,325000 diff --git a/lifelib/libraries/appliedlife/model_point_data/model_point_202401NB_GMXB.csv b/lifelib/libraries/appliedlife/model_point_data/model_point_202401NB_GMXB.csv new file mode 100644 index 0000000..4c122f7 --- /dev/null +++ b/lifelib/libraries/appliedlife/model_point_data/model_point_202401NB_GMXB.csv @@ -0,0 +1,9 @@ +point_id,product_id,plan_id,entry_date,age_at_entry,sex,policy_term,fund_index,policy_count,sum_assured,duration_mth,premium_pp,av_pp_init,accum_prem_init_pp +1,GMDB,PLAN_A,2024/01/01,70,M,10,FUND4,100,500000,0,500000,0,0 +2,GMDB,PLAN_A,2024/01/01,70,F,10,FUND4,100,500000,0,475000,0,0 +3,GMDB,PLAN_B,2024/01/01,70,M,10,FUND5,100,500000,0,450000,0,0 +4,GMDB,PLAN_B,2024/01/01,70,F,10,FUND6,100,500000,0,425000,0,0 +5,GMAB,PLAN_A,2024/01/01,70,M,10,FUND4,100,500000,0,400000,0,0 +6,GMAB,PLAN_A,2024/01/01,70,F,10,FUND4,100,500000,0,375000,0,0 +7,GMAB,PLAN_B,2024/01/01,70,M,10,FUND5,100,500000,0,350000,0,0 +8,GMAB,PLAN_B,2024/01/01,70,F,10,FUND6,100,500000,0,325000,0,0 diff --git a/makedocs/source/images/libraries/appliedlife/IntegratedLife.drawio b/makedocs/source/images/libraries/appliedlife/IntegratedLife.drawio new file mode 100644 index 0000000..ba2f552 --- /dev/null +++ b/makedocs/source/images/libraries/appliedlife/IntegratedLife.drawio @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/makedocs/source/images/libraries/appliedlife/IntegratedLife.png b/makedocs/source/images/libraries/appliedlife/IntegratedLife.png new file mode 100644 index 0000000..f9918b8 Binary files /dev/null and b/makedocs/source/images/libraries/appliedlife/IntegratedLife.png differ diff --git a/makedocs/source/index.rst b/makedocs/source/index.rst index 6aa2f54..a9189fa 100644 --- a/makedocs/source/index.rst +++ b/makedocs/source/index.rst @@ -106,6 +106,7 @@ Contribute your excellent work to lifelib and share it with actuaries from all a * :doc:`libraries/basiclife/index` * :doc:`libraries/savings/index` + * :doc:`libraries/appliedlife/index` * :doc:`libraries/assets/index` * :doc:`libraries/ifrs17a/index` * :doc:`libraries/economic/index` diff --git a/makedocs/source/libraries/appliedlife/Assumptions.rst b/makedocs/source/libraries/appliedlife/Assumptions.rst new file mode 100644 index 0000000..8659503 --- /dev/null +++ b/makedocs/source/libraries/appliedlife/Assumptions.rst @@ -0,0 +1,20 @@ + +The **Assumptions** Space +========================== + +.. automodule:: appliedlife.IntegratedLife.Assumptions + + +Formulas +------------- + +.. autosummary:: + :toctree: ../generated/ + :template: mxbase.rst + + ~lapse_tables + ~asmp_file + ~mort_scalar_tables + ~stacked_lapse_tables + ~stacked_mort_scalar_tables + ~dyn_lapse_params \ No newline at end of file diff --git a/makedocs/source/libraries/appliedlife/BaseData.rst b/makedocs/source/libraries/appliedlife/BaseData.rst new file mode 100644 index 0000000..b346d6c --- /dev/null +++ b/makedocs/source/libraries/appliedlife/BaseData.rst @@ -0,0 +1,22 @@ + +The **BaseData** Space +========================== + +.. automodule:: appliedlife.IntegratedLife.BaseData + + + +Formulas +------------- + +.. autosummary:: + :toctree: ../generated/ + :template: mxbase.rst + + ~param_list + ~const_params + ~run_params + ~space_params + ~product_params + ~surr_charge_tables + ~stacked_surr_charge_tables \ No newline at end of file diff --git a/makedocs/source/libraries/appliedlife/IntegratedLife.rst b/makedocs/source/libraries/appliedlife/IntegratedLife.rst new file mode 100644 index 0000000..e38e470 --- /dev/null +++ b/makedocs/source/libraries/appliedlife/IntegratedLife.rst @@ -0,0 +1,377 @@ +.. module:: appliedlife.IntegratedLife + +The **IntegratedLife** Model +============================== + +.. _DataFrame: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html +.. _Series: https://pandas.pydata.org/docs/reference/api/pandas.Series.html + + +Overview +-------- + +The :mod:`~appliedlife.IntegratedLife` model is a comprehensive and practical projection +tool designed for real-world actuarial tasks. + +In practical actuarial applications, +actuaries need to run projections for multiple products with varying parameters, +assumptions, and scenarios. +These projections often involve different model point sets at various base dates. + +The :mod:`~appliedlife.IntegratedLife` model allows you to define multiple runs to address these needs. +Each run has its own :attr:`~appliedlife.IntegratedLife.Run.run_id`, +enabling you to associate specific assumption files, economic scenarios, +and model point files with each :attr:`~appliedlife.IntegratedLife.Run.run_id`. + +Runs are represented in the model as ItemSpaces within the :mod:`~appliedlife.IntegratedLife.Run` space, +such as ``Run[1]``, ``Run[2]``, and so on. + +:mod:`~appliedlife.IntegratedLife` also supports a mechanism to +define logic and data by products. +In the :mod:`~appliedlife.IntegratedLife.Run` space, +spaces are defined by inheriting from :mod:`~appliedlife.IntegratedLife.ProductBase`. +These are called product spaces. +A product space represents the logic and data for a specific family of products. +At the moment, only one product space, :mod:`~appliedlife.IntegratedLife.Run.GMXB` +is defined, but more product spaces will be added in future releases. +You can define parameters specific to each user space. + + +Model Structure +------------------ + +.. toctree:: + :hidden: + :maxdepth: 1 + + BaseData + Mortality + ModelPoints + Scenarios + Assumptions + ProductBase + Run + + +In :mod:`~appliedlife.IntegratedLife`, following spaces are defined. + +* :mod:`~appliedlife.IntegratedLife.BaseData`: Reads model parameters from the parameter file +* :mod:`~appliedlife.IntegratedLife.Mortality`: Reads mortality tables from the mortality file +* :mod:`~appliedlife.IntegratedLife.ModelPoints`: Reads model point data from model point files +* :mod:`~appliedlife.IntegratedLife.Scenarios`: Reads economic data from files +* :mod:`~appliedlife.IntegratedLife.Assumptions`: Reads assumption data from files +* :mod:`~appliedlife.IntegratedLife.ProductBase`: Serves as the base space for specific product spaces defined in runs +* :mod:`~appliedlife.IntegratedLife.Run`: Represents projection runs + + * :mod:`~appliedlife.IntegratedLife.Run.GMXB`: The Product space for GMAB and GMDB policies + +The diagram below depicts the relationships between the spaces. + +.. figure:: /images/libraries/appliedlife/IntegratedLife.png + :scale: 50% + +The :mod:`~appliedlife.IntegratedLife.BaseData` space reads parameters from a parameter file. +:mod:`~appliedlife.IntegratedLife.BaseData` is referenced in many other spaces, +and its parameters are used universally in the model. +:mod:`~appliedlife.IntegratedLife.BaseData` also reads surrender charge tables, +which are static in the model. +See the :ref:`parameter_file` section and the :mod:`~appliedlife.IntegratedLife.BaseData` page +for how the parameters are defined in the parameter file. + +The :mod:`~appliedlife.IntegratedLife.Mortality` space reads mortality tables from a mortality file. +The file path of the mortality file is defined by the ``table_dir`` and ``mort_file`` parameters +in the parameter file. See the :mod:`~appliedlife.IntegratedLife.Mortality` page for more details. + +The Run space is parameterized with run_id, and is the base space for individual runs. +Each individual run is a dynamic item space of the Run space with a specific +:attr:`~appliedlife.IntegratedLife.Run.run_id` value, such as ``Run[1]``, ``Run[2]``, and so on. +The :mod:`~appliedlife.IntegratedLife.Run` space has product spaces +that are derived from the :mod:`~appliedlife.IntegratedLife.ProductBase` space. +Currently, only one product space, :mod:`~appliedlife.IntegratedLife.Run.GMXB` is defined. +All the logic and names in :mod:`~appliedlife.IntegratedLife.Run.GMXB` +are actually defined in :mod:`~appliedlife.IntegratedLife.ProductBase`, +and nothing is redefined or added in :mod:`~appliedlife.IntegratedLife.Run.GMXB`. + +Depending on the value of :attr:`~appliedlife.IntegratedLife.Run.run_id`, +different values for run parameters are read from the parameter file. +The run parameters include, ``asmp_id`` and ``mp_file_id``, which are used to determine +what assumption file and model point file should be used for the run. + +The :mod:`~appliedlife.IntegratedLife.Assumptions` space is parameterized with +:attr:`~appliedlife.IntegratedLife.Assumptions.asmp_id`. +As explained above, :attr:`~appliedlife.IntegratedLife.Assumptions.asmp_id` is a run parameter, +and is defined in the ``RunParams`` sheet +in the parameter file as a string ID that identifies +a set of assumptions to be used for a specific run. +The assumption file whose name ends with +:attr:`~appliedlife.IntegratedLife.Assumptions.asmp_id` is used for the run. + +The :mod:`~appliedlife.IntegratedLife.ModelPoints` space is parameterized with +:attr:`~appliedlife.IntegratedLife.ModelPoints.mp_file_id` and +:attr:`~appliedlife.IntegratedLife.ModelPoints.space_name`. +:attr:`~appliedlife.IntegratedLife.ModelPoints.mp_file_id` is a run parameter, +and is defined in the ``RunParams`` sheet +in the parameter file as a string ID. +The model point file whose name ends with :attr:`~appliedlife.IntegratedLife.ModelPoints.mp_file_id` +and :attr:`~appliedlife.IntegratedLife.ModelPoints.space_name` is selected for the run. + +The :mod:`~appliedlife.IntegratedLife.Scenarios` space is parameterized +with :attr:`~appliedlife.IntegratedLife.Scenarios.date_id` and +:attr:`~appliedlife.IntegratedLife.Scenarios.sens_id`. +:attr:`~appliedlife.IntegratedLife.Scenarios.date_id` is a run parameter +defined in the ``RunParams`` sheet, +:attr:`~appliedlife.IntegratedLife.Scenarios.sens_id` +is defined as ``sens_int_rate`` in ``RunParams`` in the parameter file. +:attr:`~appliedlife.IntegratedLife.Scenarios.date_id` is used to identify the interest rate file, +while :attr:`~appliedlife.IntegratedLife.Scenarios.sens_id` +is used to identify what sheet should be used in the selected file. + + +Input Files +------------ + +.. _parameter_file: + +Parameter File +^^^^^^^^^^^^^^^ + +By default, the :mod:`~appliedlife.IntegratedLife` model reads model parameters +from a parameter file in :mod:`~appliedlife.IntegratedLife.BaseData`. +The parameter file is an excel file named "model_parameters.xlsx" by default, +and located in the same directory as the model is located. +The name of the parameter file is specified by +:attr:`~appliedlife.IntegratedLife.BaseData.parameter_file` in the model. + +In the parameter file, parameters are defined in the following sheets, +depending on how their values should vary by. + +* ``CostParams`` +* ``RunParams`` +* ``SpaceParams`` +* Sheets with product space names (Only "GMXB" by default) + +The ``ConstParams`` sheet defines *constant parameters*, +whose values are constant over all runs across all products. +The ``RunParams`` sheet defines *run parameters*, whose values vary by runs. +The ``SpaceParams`` sheet defines *space parameters*, +whose values vary by product spaces. +The same parameter can be defined in more than one sheets of the three, +but it must not appear more than once in the same sheet. + +The ``ParamList`` sheet is for listing all the parameters defined in the three sheets, +and its ``read_from`` column indicates from what sheet the value of each parameter should be defined. +However, the following basic parameters should be constant parameters: + +* ``table_dir`` +* ``spec_tables`` +* ``asmp_file_prefix`` +* ``model_point_dir`` +* ``mp_file_prefix`` +* ``mort_file`` +* ``scen_dir`` +* ``scen_param_file`` +* ``scen_file_prefix`` + +For some basic parameters, the ``read_from`` values cannot be changed. +See :mod:`~appliedlife.IntegratedLife.BaseData` for the complete list of these +parameters. + +Parameters defined in these three sheets are called *fixed parameters*, +because their values do not vary by model points within each product space. +All the fixed parameters in a product space are combined in +:func:`~appliedlife.IntegratedLife.ProductBase.fixed_params` as a Series. +The sheets with the names of product spaces are per-space sheets. +by default, only one per-space sheet, ``GMXB`` is defined. +A par-space sheet is specific to the product space that its name represents. +For example, the ``GMXB`` sheet defines parameters specific to the ``GMXB`` space. + +Parameters in a par-space sheet is indexed by the two left most columns, +``product_id`` and ``plan_id``. +The parameters are appended to model point data by looking up ``product_id`` and ``plan_id`` in +:func:`~appliedlife.IntegratedLife.ModelPoints.model_point_table_ext`. + + +Model Point Files +^^^^^^^^^^^^^^^^^^ + +By default, sample model point files are stored in +the *model_point_data* folder in the library. +Model point files are prepared by user space. +The file name is constructed using a prefix, +:attr:`~appliedlife.IntegratedLife.ModelPoints.mp_file_id` and +:attr:`~appliedlife.IntegratedLife.ModelPoints.space_name`, +all concatenated by underscores, followed by ".csv". + +See :mod:`~appliedlife.IntegratedLife.ModelPoints` for more details. + +Assumption Files +^^^^^^^^^^^^^^^^^ + +Assumption files are Excel files containing assumption data. +Assumption files are identified by ``asmp_id``, and associated to +runs through a run parameter, ``asmp_id``. +By default, assumption files are stored in +the *input_tables* folder in the library. +The file name is constructed using a prefix, "assumptions" and +:attr:`~appliedlife.IntegratedLife.Assumptions.asmp_id` +concatenated by underscores, followed by ".xlsx". + +See :mod:`~appliedlife.IntegratedLife.Assumptions` for more details. + +Product Spec File +^^^^^^^^^^^^^^^^^^^ + +A product spec file is an Excel file containing parameters related +to product specs that do not vary by projection dates. +By default, the file is named "product_spec_tables.xlsx", +and located in the *input_tables* folder in the library. +The name and location of the file are specified +by the constant parameters, ``spec_tables`` and ``table_dir``, +in the parameter file. +Currently, only a surrender charge table is defined. + + +Mortality Table File +^^^^^^^^^^^^^^^^^^^^^ + +By default, the mortality table file is named "mortality_tables.xlsx", +and located in the *input_tables* folder in the library. +The name and location of the file are specified +by the constant parameters, ``mort_file`` and ``table_dir``, +in the parameter file. + +See :mod:`~appliedlife.IntegratedLife.Mortality` for more details. + +Economic Data File +^^^^^^^^^^^^^^^^^^^ + +By default, files for economic data are +located in the *economic_data* folder in the library. + +By default, risk free rates are used for discounting and interest rate assumptions. +Risk free rates at a certain date, identified by ``date_id`` +are contained in an Excel file, named "risk_free_YYYYMM.xlsx", +where "YYYYMM" is the ``date_id``. +The name and location of the interest rate files + +Each risk-free rate file has 3 sheets, "BASE", "UP" and "DOWN". +The sheet name is used as a key when determining interest rate sensitivity. + +See :mod:`~appliedlife.IntegratedLife.Scenarios` for more details. + + +Basic Usage +----------- + +Reading the model +^^^^^^^^^^^^^^^^^ + +Create your copy of the *appliedlife* library by following +the steps on the :doc:`/quickstart/index` page. +The model is saved as the folder named *IntegratedLife* in the copied folder. + +To read the model from Spyder with the modelx plug-in, +right-click on the empty space in *MxExplorer*, +and select *Read Model*. +Click the folder icon on the dialog box and select the +*IntegratedLife* folder. + +To read the model on IPython console, use the ``read_model`` function in ``modelx``. + +.. code-block:: python + + >>> import modelx as mx + + >>> m = mx.read_model("IntegratedLife") + + +Running the results +^^^^^^^^^^^^^^^^^^^^ + +To run the model, simply execute result Cells, +such as :func:`~appliedlife.IntegratedLife.ProductBase.result_pv`, +of a product space, such as :mod:`~appliedlife.IntegratedLife.Run.GMXB`, +in a dynamic subspace of :mod:`~appliedlife.IntegratedLife.Run`, +such as ``Run[1]``, +where the number in ``[]`` is a specific :attr:`~appliedlife.IntegratedLife.Run.run_id`. + +.. code-block:: python + + >>> m.Run(1).GMXB.result_pv() + + Premiums Death ... Change in AV Net Cashflow + point_id scen ... + 1 1 50000000.0 4.535395e+06 ... 1.141797e+07 6.097680e+06 + 2 50000000.0 4.592794e+06 ... 1.244402e+07 6.732840e+06 + 3 50000000.0 4.514334e+06 ... 1.215701e+07 6.499784e+06 + 4 50000000.0 4.667772e+06 ... 1.250695e+07 6.648902e+06 + 5 50000000.0 4.403177e+06 ... 1.138434e+07 6.107113e+06 + ... ... ... ... ... + 8 96 32500000.0 3.013149e+06 ... 5.651292e+06 -1.669267e+07 + 97 32500000.0 3.050556e+06 ... 8.690729e+06 -6.839240e+05 + 98 32500000.0 3.013149e+06 ... 3.701018e+06 -1.436214e+07 + 99 32500000.0 3.230455e+06 ... 8.484874e+06 1.174996e+06 + 100 32500000.0 3.013149e+06 ... 7.439794e+06 -1.503688e+06 + + [800 rows x 9 columns] + + + + >>> m.Run(1).GMXB.result_cf() + + Premiums Claims Expenses Commissions Net Cashflow + 0 3.300000e+10 6.885792e+07 4.033333e+08 1.280000e+09 4.305433e+08 + 1 0.000000e+00 6.887329e+07 3.328038e+06 0.000000e+00 2.310506e+07 + 2 0.000000e+00 6.875446e+07 3.322756e+06 0.000000e+00 2.313363e+07 + 3 0.000000e+00 6.868379e+07 3.317490e+06 0.000000e+00 2.311497e+07 + 4 0.000000e+00 6.869832e+07 3.312239e+06 0.000000e+00 2.318471e+07 + .. ... ... ... ... ... + 116 0.000000e+00 2.288671e+08 1.870365e+06 0.000000e+00 7.103725e+06 + 117 0.000000e+00 2.264772e+08 1.851697e+06 0.000000e+00 7.054587e+06 + 118 0.000000e+00 2.238152e+08 1.833222e+06 0.000000e+00 6.974249e+06 + 119 0.000000e+00 2.216954e+08 1.814959e+06 0.000000e+00 6.897696e+06 + 120 0.000000e+00 2.076927e+10 0.000000e+00 0.000000e+00 -2.087250e+09 + + [121 rows x 5 columns] + + >>> m.Run(1).GMXB.result_pols() + + pols_if pols_maturity pols_new_biz pols_death pols_lapse + 0 0.000000 0.000000 80000 15.888508 177.398389 + 1 79806.713103 0.000000 0 15.849889 176.849853 + 2 79614.013360 0.000000 0 15.811388 176.251368 + 3 79421.950604 0.000000 0 15.773013 175.675413 + 4 79230.502178 0.000000 0 15.734762 175.131998 + .. ... ... ... ... ... + 116 40772.225418 0.000000 0 90.248214 350.153733 + 117 40331.823472 0.000000 0 89.268431 346.231945 + 118 39896.323096 0.000000 0 88.299619 341.906948 + 119 39466.116529 0.000000 0 87.342607 337.606448 + 120 39041.167475 39041.167475 0 0.000000 0.000000 + + [121 rows x 5 columns] + +:func:`~appliedlife.IntegratedLife.ProductBase.result_sample` returns +the detailed result for a single model point on a single scenario. +Alternatively, :func:`~appliedlife.IntegratedLife.ProductBase.excel_sample` +opens Excel and output the sample result on an Excel workbook. + +.. code-block:: python + + >>> m.Run(1).GMXB.excel_sample() + + premiums inv_income ... inv_return_mth disc_rate_mth + 0 50000000.0 36668.209367 ... 0.000816 0.003883 + 1 0.0 -832902.827593 ... -0.018567 0.003883 + 2 0.0 136715.311689 ... 0.003113 0.003883 + 3 0.0 15294.022885 ... 0.000348 0.003883 + 4 0.0 125878.367168 ... 0.002869 0.003883 + .. ... ... ... ... ... + 116 0.0 -223430.982443 ... -0.007303 0.002830 + 117 0.0 640062.777645 ... 0.021289 0.002830 + 118 0.0 62510.277083 ... 0.002057 0.002830 + 119 0.0 -427654.831703 ... -0.014189 0.002830 + 120 0.0 0.000000 ... 0.014783 0.002837 + + [121 rows x 38 columns] + diff --git a/makedocs/source/libraries/appliedlife/ModelPoints.rst b/makedocs/source/libraries/appliedlife/ModelPoints.rst new file mode 100644 index 0000000..0f1b2ca --- /dev/null +++ b/makedocs/source/libraries/appliedlife/ModelPoints.rst @@ -0,0 +1,16 @@ +The **ModelPoints** Space +========================== + +.. automodule:: appliedlife.IntegratedLife.ModelPoints + + +Formulas +------------- + +.. autosummary:: + :toctree: ../generated/ + :template: mxbase.rst + + ~model_point_table + ~model_point_table_ext + diff --git a/makedocs/source/libraries/appliedlife/Mortality.rst b/makedocs/source/libraries/appliedlife/Mortality.rst new file mode 100644 index 0000000..c98246b --- /dev/null +++ b/makedocs/source/libraries/appliedlife/Mortality.rst @@ -0,0 +1,22 @@ +The **Mortality** Space +========================== + + +.. automodule:: appliedlife.IntegratedLife.Mortality + + +Formulas +------------- + +.. autosummary:: + :toctree: ../generated/ + :template: mxbase.rst + + table_defs + select_table + ultimate_tables + select_duration_len + merged_table + unified_table + mort_file + table_last_age \ No newline at end of file diff --git a/makedocs/source/libraries/appliedlife/ProductBase.rst b/makedocs/source/libraries/appliedlife/ProductBase.rst new file mode 100644 index 0000000..9a6d529 --- /dev/null +++ b/makedocs/source/libraries/appliedlife/ProductBase.rst @@ -0,0 +1,206 @@ +The **ProductBase** Space +========================== + + +.. automodule:: appliedlife.IntegratedLife.ProductBase + + + +Projection parameters +---------------------- + +.. autosummary:: + :toctree: ../generated/ + :template: mxbase.rst + + fixed_params + proj_len + scen_index + asmp_id + date_id + + +Model point data +------------------ + +.. autosummary:: + :toctree: ../generated/ + :template: mxbase.rst + + model_point + model_point_index + model_point_table_ext + age + sex + age_at_entry + av_pp_init + commission_rate + duration + duration_mth + duration_mth_init + has_gmab + has_gmdb + has_surr_charge + is_wl + maint_fee_rate + policy_term + premium_type + sum_assured + surr_charge_id + + +Assumptions +------------- + +.. autosummary:: + :toctree: ../generated/ + :template: mxbase.rst + + base_lapse_rate + is_lapse_dynamic + dyn_lapse_param + dyn_lapse_factor + lapse_rate_key + lapse_rate + disc_factors + disc_rate + disc_rate_mth + expense_acq + expense_maint + inflation_rate + inflation_factor + mort_last_age + base_mort_rate + mort_rate + mort_rate_key + mort_rate_mth + mort_table_id + + +Policy values +--------------- + +.. autosummary:: + :toctree: ../generated/ + :template: mxbase.rst + + claim_net_pp + claim_pp + coi_pp + coi_rate + premium_pp + surr_charge_key + surr_charge_rate + + +Policy decrement +------------------ + +.. autosummary:: + :toctree: ../generated/ + :template: mxbase.rst + + pols_if + pols_if_at + pols_if_init + pols_lapse + pols_death + pols_maturity + pols_new_biz + + + +Account Value +-------------- + +.. autosummary:: + :toctree: ../generated/ + :template: mxbase.rst + + inv_income + inv_income_pp + inv_return_mth + av_at + av_change + av_pp_at + csv_pp + coi + maint_fee + maint_fee_pp + net_amt_at_risk + prem_to_av + prem_to_av_pp + + +Cashflows +----------- + +.. autosummary:: + :toctree: ../generated/ + :template: mxbase.rst + + claims + claims_from_av + claims_over_av + commissions + expenses + surr_charge + net_cf + + + +Margin Analysis +---------------- + +.. autosummary:: + :toctree: ../generated/ + :template: mxbase.rst + + margin_expense + margin_guarantee + + +Present values +--------------- + +.. autosummary:: + :toctree: ../generated/ + :template: mxbase.rst + + pv_av_change + pv_claims + pv_claims_from_av + pv_claims_over_av + pv_commissions + pv_expenses + pv_inv_income + pv_maint_fee + pv_net_cf + pv_pols_if + pv_premiums + + +Results and output +-------------------- + +.. autosummary:: + :toctree: ../generated/ + :template: mxbase.rst + + result_pv + result_cf + result_pols + result_sample + excel_sample + + +Validation +----------- + +.. autosummary:: + :toctree: ../generated/ + :template: mxbase.rst + + check_av_roll_fwd + check_margin + check_pv_net_cf \ No newline at end of file diff --git a/makedocs/source/libraries/appliedlife/Run.rst b/makedocs/source/libraries/appliedlife/Run.rst new file mode 100644 index 0000000..3a0ef1a --- /dev/null +++ b/makedocs/source/libraries/appliedlife/Run.rst @@ -0,0 +1,10 @@ +The **Run** Space +========================== + +.. automodule:: appliedlife.IntegratedLife.Run + + +The **GMXB** Space +---------------------- + +.. automodule:: appliedlife.IntegratedLife.Run.GMXB \ No newline at end of file diff --git a/makedocs/source/libraries/appliedlife/Scenarios.rst b/makedocs/source/libraries/appliedlife/Scenarios.rst new file mode 100644 index 0000000..3b3c804 --- /dev/null +++ b/makedocs/source/libraries/appliedlife/Scenarios.rst @@ -0,0 +1,20 @@ +The **Scenarios** Space +========================== + +.. automodule:: appliedlife.IntegratedLife.Scenarios + + +Formulas +------------- + +.. autosummary:: + :toctree: ../generated/ + :template: mxbase.rst + + ~spot_rates + ~forward_rates + ~index_vols + ~log_return_mth + ~index_params + ~index_count + ~return_mth diff --git a/makedocs/source/libraries/appliedlife/index.rst b/makedocs/source/libraries/appliedlife/index.rst new file mode 100644 index 0000000..b226d9b --- /dev/null +++ b/makedocs/source/libraries/appliedlife/index.rst @@ -0,0 +1,88 @@ +.. module:: appliedlife +.. include:: /banners.rst + +The **appliedlife** Library +============================= + +|modelx badge| + +.. warning:: + + :mod:`appliedlife` is in its active development phase, and its contents are subject to change. + + +Overview +----------- + +The **appliedlife** library features :mod:`~appliedlife.IntegratedLife`, a comprehensive and practical projection model +designed for real-world actuarial tasks. + +The :mod:`~appliedlife.IntegratedLife` model offers several key features: + +* **Multiple Products:** Support multiple products by inheriting the base logic common to all products. + Currently, the model supports VA GMAB and GMDB products, with plans to add more in future releases. + +* **Flexible Input:** Perform projections with various combinations of input data by simply setting parameters. + For instance, specify the model point file and scenario file for a specific run using a parameter file. + +* **Multiple Runs:** Define multiple runs within the model, + such as a base case for a certain date or a stressed case for another date, + thanks to the flexible input feature. + +* **External File Input:** All input files are stored externally, outside of the model, + allowing for decoupling data and logic. + +* **Excel Output:** Output projection results of a sample model point directly + to Excel using `xlwings`_. + +The cashflow logic in :mod:`~appliedlife.IntegratedLife` is based on the :mod:`~savings.CashValue_ME` model +from the :mod:`savings` library, with several enhancements. + +See :mod:`~appliedlife.IntegratedLife` for more details. + +.. _xlwings: + https://www.xlwings.org/ + +How to Use the Library +------------------------------ + +As explained in the :ref:`create-a-project` section, +Create you own copy of the *appliedlife* library. +For example, to copy as a folder named *appliedlife* +under the path *C:\\path\\to\\your\\*, type below in an IPython console:: + + >>> import lifelib + + >>> lifelib.create("appliedlife", r"C:\path\to\your\applifedlife") + + +:mod:`~appliedlife.IntegratedLife` uses `xlwings`_ in +:func:`ProductBase.excel_sample `. +If not yet installed, install it using ``pip`` or ``conda``. + +.. seealso:: + + * :ref:`FAQ on xlwings ` + +Library Contents +------------------ + +.. toctree:: + :hidden: + :maxdepth: 1 + + IntegratedLife + +.. table:: + :widths: 20 80 + + ========================================= =========================================================================== + File or Folder Description + ========================================= =========================================================================== + IntegratedLife The :mod:`~appliedlife.IntegratedLife` model. + model_parameters.xlsx The parameter file of :mod:`~appliedlife.IntegratedLife` + input_tables Folder containing sample assumptions, mortality tables and product specs + economic_data Folder containing economic data files + model_point_data Folder containing model point files + ========================================= =========================================================================== + diff --git a/makedocs/source/libraries/index.rst b/makedocs/source/libraries/index.rst index abea1e9..7042897 100644 --- a/makedocs/source/libraries/index.rst +++ b/makedocs/source/libraries/index.rst @@ -15,6 +15,7 @@ independent of modelx using modelx's export feature. =============================== =============== =============================================================== :doc:`basiclife/index` |modelx badge| Basic life insurance cashflow models and examples :doc:`savings/index` |modelx badge| Cashflow models of saving products with cash values + :doc:`appliedlife/index` |modelx badge| Comprehensive and practical projection model :doc:`assets/index` |modelx badge| Basic models of bond portfolios :doc:`ifrs17a/index` IFRS17 calculation model and examples :doc:`economic/index` |modelx badge| Basic Hull-White model @@ -29,6 +30,7 @@ independent of modelx using modelx's export feature. basiclife/index.rst savings/index.rst + appliedlife/index.rst assets/index.rst ifrs17a/index.md economic/index.rst diff --git a/makedocs/source/quickstart/faq.md b/makedocs/source/quickstart/faq.md index 203207e..3e514aa 100644 --- a/makedocs/source/quickstart/faq.md +++ b/makedocs/source/quickstart/faq.md @@ -537,7 +537,7 @@ BasicTerm_S.Projection[1].premiums(t=0)=94.84 If you use modelx from Spyder with modelx plug-in, you can do the operations above using GUI in the MxAnalyzer widget. - +(faq_xlwings)= ### How do I output results directly to Excel? To output calculation results directly to Excel, you can use the [`xlwings`](https://www.xlwings.org/) library. diff --git a/makedocs/source/releases/relnotes_v0.10.0.rst b/makedocs/source/releases/relnotes_v0.10.0.rst new file mode 100644 index 0000000..42473bc --- /dev/null +++ b/makedocs/source/releases/relnotes_v0.10.0.rst @@ -0,0 +1,31 @@ +.. currentmodule:: lifelib.libraries + +.. _relnotes_v0.10.0: + +================================== +lifelib v0.10.0 (7 July 2024) +================================== + +To update lifelib, run the following command:: + + >>> pip install lifelib --upgrade + +If you're using Anaconda, use the ``conda`` command instead:: + + >>> conda update lifelib + +New Library +=============== + +This release adds a new library, :mod:`~appliedlife`. +:mod:`~appliedlife` includes the :mod:`~appliedlife.IntegratedLife` model, +a comprehensive and practical projection model. See the :mod:`~appliedlife` page +for more details. + +Fixes +======== + +* In :mod:`~basiclife`: Error due to Mortality rate lookup + before future entry (`GH70 `_) +* In :mod:`~solvency2`: Eliminate multi-inheritance of dynamic spaces due to modelx update + (`The commit reflecting changes `_) diff --git a/makedocs/source/updates.rst b/makedocs/source/updates.rst index f38f609..f9ae447 100644 --- a/makedocs/source/updates.rst +++ b/makedocs/source/updates.rst @@ -14,6 +14,10 @@ Updates Follow lifelib on LinkedIn for more frequent updates.

+* *7 July 2024:* + lifelib :ref:`v0.10.5` is released. + :mod:`~appliedlife`, a new library for comprehensive production modeling is added. + * *18 February 2024:* lifelib :ref:`v0.9.5` is released to support modelx 0.25.0. diff --git a/makedocs/source/whatsnew.rst b/makedocs/source/whatsnew.rst index 7389a66..5151a9c 100644 --- a/makedocs/source/whatsnew.rst +++ b/makedocs/source/whatsnew.rst @@ -33,6 +33,7 @@ Documentation for released versions of lifelib is available under .. toctree:: :maxdepth: 1 + releases/relnotes_v0.10.0 releases/relnotes_v0.9.5 releases/relnotes_v0.9.4 releases/relnotes_v0.9.3