Skip to main content

Analysis Example: Multi-Contract Portfolio Analysis

This example demonstrates how to model, simulate, and analyze a portfolio of fixed-rate and floating-rate ACTUS contracts using the Awesome Actus Library (AAL).

Step 0: Setup and Imports

We start by importing all required libraries, setting a random seed for reproducibility, and creating a folder to store plots.

import numpy as np
import pandas as pd
from awesome_actus_lib import PAM, ANN, Portfolio, ReferenceIndex, YieldCurve, PublicActusService, LiquidityAnalysis, ValueAnalysis, IncomeAnalysis
np.random.seed(42) # for reproducability

Step 1: Define a Portfolio of Fixed and Floating Contracts

We create a portfolio consisting of:

  • 5 fixed-rate Principal-at-Maturity (PAM) contracts
  • 3 fixed-rate Annuity (ANN) contracts
  • 2 floating-rate Annuity (ANN) contracts linked to a reference index
contracts = []

# Fixed-rate PAM contracts
for i in range(5):
contracts.append(PAM(
contractID=f"pam{i+1}",
contractRole="RPA",
statusDate="2024-12-31",
initialExchangeDate=f"2025-01-0{i+1}",
maturityDate=f"2028-01-0{i+1}",
notionalPrincipal=10000 + i * 1000,
nominalInterestRate=0.02 + i * 0.002,
dayCountConvention="30E360",
currency="USD",
contractDealDate="2024-12-31",
counterpartyID="CP01",
creatorID="Bank01",
cycleOfInterestPayment=["P1YL1","P6ML1","P3ML1","P1YL1","P9ML1"][i],
))

# Fixed-rate ANN contracts
for i in range(3):
contracts.append(ANN(
contractID=f"ann{i+1}",
contractRole="RPA",
statusDate="2024-12-31",
initialExchangeDate=f"2025-02-0{i+1}",
maturityDate=f"2029-02-0{i+1}",
notionalPrincipal=12000 + i * 1500,
nominalInterestRate=0.025 + i * 0.0015,
dayCountConvention="30E360",
cycleOfInterestPayment="P1YL1",
cycleOfPrincipalRedemption="P1YL1",
cycleAnchorDateOfInterestPayment="2026-02-01",
cycleAnchorDateOfPrincipalRedemption="2026-02-01",
currency="USD",
contractDealDate="2024-12-31",
counterpartyID="CP01",
creatorID="Bank01",
))

# Floating-rate ANN contracts
index_code = "US_Floating_6M"
for i in range(2):
contracts.append(ANN(
contractID=f"vann{i+1}",
contractRole="RPA",
statusDate="2024-12-31",
initialExchangeDate=f"2025-03-0{i+1}",
maturityDate=f"2029-03-0{i+1}",
notionalPrincipal=15000 + i * 1000,
nominalInterestRate=0.0, # Overridden by reset
dayCountConvention="30E360",
cycleOfInterestPayment="P1YL1",
cycleOfPrincipalRedemption="P1YL1",
cycleAnchorDateOfInterestPayment="2026-03-01",
cycleAnchorDateOfPrincipalRedemption="2026-03-01",
cycleOfRateReset="P6ML1",
marketObjectCodeOfRateReset=index_code,
rateSpread=0.01,
currency="USD",
contractDealDate="2024-12-31",
counterpartyID="CP02",
creatorID="Bank01",
))

ptf = Portfolio(contracts)

Step 2: Define Risk Factors

We define a discount curve and a reference index:

  • The discount curve is used later for value and income analysis.
  • The reference index drives the rate reset of the floating-rate contracts.

Note: We pass both risk factors to generateEvents() even though the discount curve is not used for event generation — this ensures it is included in the resulting CashFlowStream for later analyses. (An alternative would be to inject it into the CashFlowStream manually after generation.)

tenors = ["6M", "1Y", "2Y", "3Y", "5Y", "7Y", "10Y"]
rates = [0.01, 0.015, 0.02, 0.023, 0.028, 0.032, 0.035]
discount_curve = YieldCurve(
marketObjectCode="USD_DISCOUNT",
referenceDate="2025-01-01",
tenors=tenors,
rates=rates,
base=1.0
)
discount_curve.plot()

index_data = pd.DataFrame({
"date": pd.date_range(start="2023-01-01", end="2030-12-31", periods=20).strftime("%Y-%m-%d"),
"value": np.random.uniform(0.01, 0.04, size=20).round(4)
})
ref_index = ReferenceIndex(marketObjectCode=index_code, source=index_data, base=1.0)
ref_index.plot()

Discount Curve

Floating Index

Step 3: Generate Cash Flow Events

We simulate the full event schedule across all contracts:

service = PublicActusService()
events = service.generateEvents(portfolio=ptf, riskFactors=[discount_curve, ref_index])
events.plot()

Cashflows

Preview of Event Table (first 20 rows)

typetimepayoffcurrencynominalValuenominalRatenominalAccruedcontractId
IED2025-01-01T00:00-10000USD100000.020pam1
IP2025-01-01T00:000USD100000.020pam1
IP2026-01-01T00:00200USD100000.020pam1
IP2027-01-01T00:00200USD100000.020pam1
IP2028-01-01T00:00200USD100000.020pam1
MD2028-01-01T00:0010000USD00.020pam1
IED2025-01-02T00:00-11000USD110000.0220pam2
IP2025-01-02T00:000USD110000.0220pam2
IP2025-07-02T00:00121USD110000.0220pam2
IP2026-01-02T00:00121USD110000.0220pam2
IP2026-07-02T00:00121USD110000.0220pam2
IP2027-01-02T00:00121USD110000.0220pam2
IP2027-07-02T00:00121USD110000.0220pam2
IP2028-01-02T00:00121USD110000.0220pam2
MD2028-01-02T00:0011000USD00.0220pam2
IED2025-01-03T00:00-12000USD120000.0240pam3
IP2025-01-03T00:000USD120000.0240pam3
IP2025-04-03T00:0072USD120000.0240pam3
IP2025-07-03T00:0072USD120000.0240pam3
IP2025-10-03T00:0072USD120000.0240pam3

Step 4: Liquidity Analysis

This shows net inflows and outflows aggregated per year. It is useful to assess funding needs or surpluses.

liq = LiquidityAnalysis(cf_stream=events, freq="Y")
liq.plot()

Liquidity

       netLiquidity
time
2025 -130869.000000
2026 18057.974013
2027 18335.536044
2028 78097.645896
2029 26657.859228

Step 5: Value Analysis

We discount all future cash flows using the yield curve to compute present value as of 2025-01-01.

val = ValueAnalysis(cf_stream=events, as_of_date="2025-01-01", discount_curve_code="USD_DISCOUNT")
  as_of_date  nominal_value          npv
0 2025-01-01 10280.015181 1306.439457

Step 6: Income Analysis

This breaks down recognized income over time, based on the contractual cash flows.

inc = IncomeAnalysis(cf_stream=events, freq="Y")
inc.plot()

Income

        netIncome
time
2025 631.000000
2026 3154.658472
2027 3243.326902
2028 2460.038044
2029 790.991763