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