User Guides

Comprehensive guides for using JACTUS to model financial contracts.

Getting Started

Introduction to JACTUS

JACTUS is a JAX-based implementation of the ACTUS financial contract standard. It provides:

  • Type-safe contract modeling with full validation

  • JAX integration for automatic differentiation and GPU acceleration

  • 18 contract types covering principal, non-principal, and derivative instruments

  • Event-driven simulation for cash flow generation

  • Composable contracts for complex financial structures

Learning Resources

Interactive Notebooks

For hands-on learning with visualizations, explore the Jupyter notebooks:

  • examples/notebooks/01_annuity_mortgage.ipynb - 30-year mortgage with amortization charts

  • examples/notebooks/02_options_contracts.ipynb - Call/Put options with payoff diagrams

  • examples/notebooks/03_interest_rate_cap.ipynb - Interest rate protection scenarios

  • examples/notebooks/04_stock_commodity.ipynb - Stock and commodity position tracking

Python Examples

Ready-to-run Python scripts are available in examples/:

  • pam_example.py - Principal at Maturity contracts

  • lam_example.py - Linear Amortizer contracts

  • interest_rate_swap_example.py - Plain vanilla swaps

  • fx_swap_example.py - FX swaps

  • cross_currency_basis_swap_example.py - Cross-currency swaps

Basic Workflow

The typical workflow for using JACTUS:

  1. Define Contract Attributes:

    from jactus.core import ContractAttributes, ContractType, ContractRole, ActusDateTime
    
    attrs = ContractAttributes(
        contract_id="LOAN-001",
        contract_type=ContractType.PAM,
        contract_role=ContractRole.RPA,  # Receive principal, pay interest
        status_date=ActusDateTime(2024, 1, 1, 0, 0, 0),
        # ... more attributes
    )
    
  2. Create Risk Factor Observer:

    from jactus.observers import ConstantRiskFactorObserver
    
    rf_observer = ConstantRiskFactorObserver(constant_value=0.05)  # 5% interest rate
    
  3. Create Contract:

    from jactus.contracts import create_contract
    
    contract = create_contract(attrs, rf_observer)
    
  4. Run Simulation:

    result = contract.simulate()
    for event in result.events:
        print(f"{event.event_type}: {event.event_time} -> ${event.payoff:.2f}")
    

Principal Contracts Guide

PAM - Principal at Maturity

Principal at Maturity contracts are interest-only loans where principal is repaid at maturity.

Use Cases:

  • Interest-only mortgages

  • Bullet bonds

  • Short-term loans

  • Treasury securities

Example: 30-Year Mortgage:

from jactus.core import (
    ContractAttributes, ContractType, ContractRole, ActusDateTime,
    DayCountConvention, EndOfMonthConvention
)
from jactus.contracts import create_contract
from jactus.observers import ConstantRiskFactorObserver

# Define mortgage attributes
attrs = ContractAttributes(
    contract_id="MORTGAGE-001",
    contract_type=ContractType.PAM,
    contract_role=ContractRole.RPA,  # Lender perspective
    status_date=ActusDateTime(2024, 1, 1, 0, 0, 0),
    initial_exchange_date=ActusDateTime(2024, 1, 1, 0, 0, 0),
    maturity_date=ActusDateTime(2054, 1, 1, 0, 0, 0),  # 30 years
    notional_principal=500_000.0,  # $500k loan
    nominal_interest_rate=0.06,  # 6% annual
    day_count_convention=DayCountConvention.A360,
    interest_payment_cycle="1M",  # Monthly payments
    interest_payment_anchor=ActusDateTime(2024, 2, 1, 0, 0, 0),
    end_of_month_convention=EndOfMonthConvention.EOM,
    currency="USD",
)

# Create observer with fixed rate
rf_observer = ConstantRiskFactorObserver(constant_value=0.06)

# Create contract and simulate
contract = create_contract(attrs, rf_observer)
result = contract.simulate()

# Analyze results
total_interest = sum(event.payoff for event in result.events
                    if event.event_type.name == "IP")
print(f"Total interest over 30 years: ${total_interest:,.2f}")

Key Parameters:

  • notional_principal: Loan amount

  • nominal_interest_rate: Annual interest rate

  • interest_payment_cycle: Payment frequency (1M=monthly, 3M=quarterly, 1Y=annual)

  • maturity_date: When principal is repaid

See: examples/pam_example.py for complete examples

LAM - Linear Amortizer

Linear amortizer contracts have fixed principal payments plus decreasing interest.

Use Cases:

  • Auto loans

  • Equipment financing

  • Fixed principal amortization schedules

  • Construction loans

Example: 5-Year Auto Loan:

attrs = ContractAttributes(
    contract_id="AUTO-001",
    contract_type=ContractType.LAM,
    contract_role=ContractRole.RPA,
    status_date=ActusDateTime(2024, 1, 1, 0, 0, 0),
    initial_exchange_date=ActusDateTime(2024, 1, 1, 0, 0, 0),
    maturity_date=ActusDateTime(2029, 1, 1, 0, 0, 0),  # 5 years
    notional_principal=30_000.0,  # $30k car loan
    nominal_interest_rate=0.048,  # 4.8% APR
    interest_payment_cycle="1M",  # Monthly payments
    interest_payment_anchor=ActusDateTime(2024, 2, 1, 0, 0, 0),
    principal_redemption_cycle="1M",  # Monthly principal payments
    principal_redemption_anchor=ActusDateTime(2024, 2, 1, 0, 0, 0),
    next_principal_redemption_amount=500.0,  # $500/month principal
    currency="USD",
)

Amortization Modes:

  • NT (Notional): Principal amount per period

  • NTIED (Notional IED): Based on initial notional

  • NTL (Notional Last): Based on remaining notional

See: examples/lam_example.py for complete examples

Derivative Contracts Guide

Interest Rate Swaps (SWPPV)

Plain vanilla interest rate swaps exchange fixed and floating rate payments.

Use Cases:

  • Convert fixed-rate debt to floating

  • Convert floating-rate debt to fixed

  • Interest rate risk management

  • Speculation on rate movements

Example: 5-Year Fixed vs Floating Swap:

attrs = ContractAttributes(
    contract_id="IRS-001",
    contract_type=ContractType.SWPPV,
    contract_role=ContractRole.RFL,  # Pay fixed, receive floating
    status_date=ActusDateTime(2024, 1, 1, 0, 0, 0),
    maturity_date=ActusDateTime(2029, 1, 1, 0, 0, 0),
    notional_principal=10_000_000.0,  # $10M notional
    nominal_interest_rate=0.05,  # 5% fixed rate
    nominal_interest_rate_2=0.03,  # 3% initial floating rate
    interest_payment_cycle="3M",  # Quarterly payments
    interest_payment_anchor=ActusDateTime(2024, 1, 1, 0, 0, 0),
    rate_reset_cycle="3M",  # Quarterly rate resets
    rate_reset_anchor=ActusDateTime(2024, 1, 1, 0, 0, 0),
    delivery_settlement="D",  # Net settlement
    currency="USD",
)

Settlement Options:

  • D (Net): Only pay/receive the difference

  • S (Gross): Exchange both payments

See: examples/interest_rate_swap_example.py for detailed example

FX Derivatives (FXOUT)

Foreign exchange outright contracts for currency exchange at future dates.

Use Cases:

  • FX hedging

  • Forward FX contracts

  • Currency swaps

  • International payment hedging

Example: EUR/USD FX Swap:

attrs = ContractAttributes(
    contract_id="FX-001",
    contract_type=ContractType.FXOUT,
    contract_role=ContractRole.RPA,
    status_date=ActusDateTime(2024, 1, 1, 0, 0, 0),
    maturity_date=ActusDateTime(2025, 1, 1, 0, 0, 0),
    settlement_date=ActusDateTime(2025, 1, 1, 0, 0, 0),
    currency="EUR",  # Primary currency
    currency_2="USD",  # Secondary currency
    notional_principal=1_000_000.0,  # EUR 1M
    notional_principal_2=1_100_000.0,  # USD 1.1M (at spot)
    delivery_settlement="S",  # Gross settlement
    purchase_date=ActusDateTime(2024, 1, 1, 0, 0, 0),
    price_at_purchase_date=1.10,  # Spot rate
)

# Forward rate observer
rf_observer = ConstantRiskFactorObserver(constant_value=1.12)  # Forward rate

See: examples/fx_swap_example.py for complete example

Options (OPTNS)

Option contracts provide the right (not obligation) to buy/sell at a strike price.

Types:

  • Call options (right to buy)

  • Put options (right to sell)

  • European (exercise at maturity only)

  • American (exercise anytime)

Example: European Call Option:

attrs = ContractAttributes(
    contract_id="OPT-001",
    contract_type=ContractType.OPTNS,
    contract_role=ContractRole.RPA,
    status_date=ActusDateTime(2024, 1, 1, 0, 0, 0),
    maturity_date=ActusDateTime(2025, 1, 1, 0, 0, 0),
    option_type="C",  # Call option
    option_strike_1=105_000.0,  # Strike price
    option_exercise_type="E",  # European
    settlement_currency="USD",
    currency="USD",
)

Option Parameters:

  • option_type: “C” (call) or “P” (put)

  • option_exercise_type: “E” (European) or “A” (American)

  • option_strike_1: Primary strike price

  • option_strike_2: Secondary strike (for spreads)

Contract Composition Guide

Cross-Currency Basis Swaps (SWAPS)

Generic swaps support multi-leg, multi-currency structures.

Use Cases:

  • Cross-currency basis swaps

  • Multi-currency debt management

  • Foreign subsidiary funding

  • Basis trading

Example: EUR vs USD Basis Swap:

from jactus.observers import MockChildContractObserver

# Create child leg contracts (EUR and USD legs)
eur_leg = create_contract(eur_leg_attrs, eur_rf_observer)
usd_leg = create_contract(usd_leg_attrs, usd_rf_observer)

# Register child contracts
child_observer = MockChildContractObserver()
child_observer.register_child(
    "EUR-LEG",
    events=list(eur_leg.generate_event_schedule().events),
    state=eur_leg.initialize_state(),
    attributes={"notional_principal": 10_000_000.0}
)
child_observer.register_child(
    "USD-LEG",
    events=list(usd_leg.generate_event_schedule().events),
    state=usd_leg.initialize_state(),
    attributes={"notional_principal": 11_000_000.0}
)

# Create parent swap contract
swap_attrs = ContractAttributes(
    contract_id="XCCY-001",
    contract_type=ContractType.SWAPS,
    contract_role=ContractRole.RPA,
    status_date=ActusDateTime(2024, 1, 1, 0, 0, 0),
    maturity_date=ActusDateTime(2029, 1, 1, 0, 0, 0),
    contract_structure='{"FirstLeg": "EUR-LEG", "SecondLeg": "USD-LEG"}',
    delivery_settlement="D",
    currency="USD",
)

swap = create_contract(swap_attrs, rf_observer, child_observer)

Composite Contract Types:

  • SWAPS: Multi-leg swaps

  • CAPFL: Caps/floors on underlier

  • CEG: Credit guarantees on covered contracts

  • CEC: Collateral tracking for covered contracts

See: examples/cross_currency_basis_swap_example.py for detailed example

Credit Enhancement (CEG, CEC)

Credit enhancement contracts provide protection and collateral management.

CEG - Credit Enhancement Guarantee:

Provides credit protection on covered contracts:

attrs = ContractAttributes(
    contract_id="CEG-001",
    contract_type=ContractType.CEG,
    contract_role=ContractRole.RPA,
    status_date=ActusDateTime(2024, 1, 1, 0, 0, 0),
    maturity_date=ActusDateTime(2029, 1, 1, 0, 0, 0),
    contract_structure='{"CoveredContract": "LOAN-001"}',
    coverage=0.8,  # 80% coverage
    credit_event_type="DL",  # Default
    credit_enhancement_guarantee_extent="NO",  # Notional only
    currency="USD",
)

CEC - Credit Enhancement Collateral:

Tracks collateral against exposure:

attrs = ContractAttributes(
    contract_id="CEC-001",
    contract_type=ContractType.CEC,
    contract_role=ContractRole.RPA,
    status_date=ActusDateTime(2024, 1, 1, 0, 0, 0),
    maturity_date=ActusDateTime(2029, 1, 1, 0, 0, 0),
    contract_structure='{"CoveredContract": "LOAN-001", "CoveringContract": "STK-001"}',
    coverage=1.2,  # 120% collateral requirement
    credit_enhancement_guarantee_extent="NO",
    currency="USD",
)

Schedule Generation Guide

Understanding Cycles

ACTUS uses cycle notation for recurring events:

  • 1D: Daily

  • 1W: Weekly

  • 1M: Monthly

  • 3M: Quarterly

  • 6M: Semi-annual

  • 1Y: Annual

Stub Periods:

  • +: Short stub at end

  • -: Short stub at beginning

Example:

  • 3M+: Quarterly with short final period

  • 1M-: Monthly with short initial period

Business Day Conventions

Handle non-business days:

  • SCF: Shift calendar following

  • SCMF: Shift calendar modified following

  • CSF: Calculate shift following

  • CSMF: Calculate shift modified following

Example:

If payment falls on Saturday, SCF moves to Monday.

Day Count Conventions

Calculate interest accrual:

  • A360: Actual/360

  • A365: Actual/365

  • 30E360: 30E/360 (European)

  • AA: Actual/Actual

Advanced Topics

JAX Integration

JACTUS supports JAX automatic differentiation for risk analytics:

import jax
import jax.numpy as jnp
from jactus.observers import JaxRiskFactorObserver

def contract_pv(rate):
    attrs = ContractAttributes(
        contract_id="LOAN",
        contract_type=ContractType.PAM,
        contract_role=ContractRole.RPA,
        status_date=ActusDateTime(2024, 1, 1),
        initial_exchange_date=ActusDateTime(2024, 1, 15),
        maturity_date=ActusDateTime(2025, 1, 15),
        notional_principal=100_000.0,
        nominal_interest_rate=rate,
        interest_payment_cycle="6M",
        day_count_convention="30E360",
    )
    observer = JaxRiskFactorObserver(jnp.array([rate]))
    result = create_contract(attrs, observer).simulate()
    return sum(e.payoff for e in result.events)

dv01 = jax.grad(contract_pv)(0.05) * 0.0001
print(f"DV01: ${dv01:,.2f}")

Array-Mode Portfolio API

For batch simulation of large portfolios, use the array-mode API which runs JIT-compiled JAX kernels over batched arrays (12 of 18 contract types supported):

from jactus.contracts.portfolio import simulate_portfolio

# contracts = list of (ContractAttributes, RiskFactorObserver) tuples
results = simulate_portfolio(contracts)

See Array-Mode Simulation & Portfolio API for full details, vmap patterns, and GPU benchmarks.

Performance Optimization

Tips for optimal performance:

  1. Use array-mode for portfolios (simulate_portfolio())

  2. Use JIT compilation for repeated calculations

  3. Use constant observers when rates don’t change

  4. Enable float64 before importing JACTUS for precision-sensitive analytics

Custom Risk Factor Observers

Implement custom observers by subclassing BaseRiskFactorObserver:

from jactus.observers.risk_factor import BaseRiskFactorObserver

class YieldCurveObserver(BaseRiskFactorObserver):
    def __init__(self, curve):
        super().__init__(name="yield_curve")
        self.curve = curve

    def _get_risk_factor(self, identifier, time, *args, **kwargs):
        # Interpolate rate from yield curve
        return self.curve.get_rate(time)

    def _get_event_data(self, identifier, time, *args, **kwargs):
        return None

Behavioral Risk Factor Observers

JACTUS distinguishes between two categories of risk factor observers:

  • Market observers return values based solely on an identifier and time (e.g., interest rate curves, FX rates). They are stateless with respect to the contract.

  • Behavioral observers are aware of the contract’s internal state (notional, interest rate, loan age, performance status) and can dynamically inject events into the simulation timeline. They model borrower or depositor behavior that depends on the contract’s current condition.

Behavioral observers implement the BehaviorRiskFactorObserver protocol, which extends the standard risk factor observer with a contract_start() method. This method inspects the contract attributes and returns a list of CalloutEvent objects that are merged into the simulation schedule. At each callout time, the simulation engine evaluates the behavioral observer with the current contract state.

Built-in behavioral observers:

  • PrepaymentSurfaceObserver – Models prepayment rates as a function of interest rate spread and loan age using a 2D interpolation surface. Returns a Multiplicative Reduction Delta (MRD) that reduces the outstanding notional.

  • DepositTransactionObserver – Models deposit inflows and outflows for UMP (Undefined Maturity Profile) contracts. Returns an Absolute Funded Delta (AFD) representing the change in deposit balance.

Example: Prepayment model with a PAM contract

Define a 2D prepayment surface and attach it to a PAM contract:

import jax.numpy as jnp
from jactus.core import (
    ContractAttributes, ContractType, ContractRole, ActusDateTime,
)
from jactus.contracts import create_contract
from jactus.observers import ConstantRiskFactorObserver
from jactus.utilities.surface import Surface2D
from jactus.observers.prepayment import PrepaymentSurfaceObserver

# Define a prepayment surface: spread (rows) x loan age in years (columns)
surface = Surface2D(
    x_margins=jnp.array([-2.0, 0.0, 1.0, 2.0, 3.0]),   # spread %
    y_margins=jnp.array([0.0, 1.0, 2.0, 3.0, 5.0, 10.0]),  # age years
    values=jnp.array([
        [0.00, 0.00, 0.00, 0.00, 0.00, 0.00],  # spread=-2%
        [0.00, 0.00, 0.01, 0.00, 0.00, 0.00],  # spread= 0%
        [0.00, 0.01, 0.02, 0.00, 0.00, 0.00],  # spread= 1%
        [0.00, 0.02, 0.05, 0.03, 0.005, 0.00], # spread= 2%
        [0.01, 0.05, 0.10, 0.07, 0.02, 0.00],  # spread= 3%
    ]),
)

# Create prepayment observer with semi-annual observations
prepayment_observer = PrepaymentSurfaceObserver(
    surface=surface,
    fixed_market_rate=0.04,   # Current market rate for spread calculation
    prepayment_cycle="6M",    # Observe every 6 months
    model_id="ppm01",
)

# Define PAM contract
attrs = ContractAttributes(
    contract_id="MORTGAGE-001",
    contract_type=ContractType.PAM,
    contract_role=ContractRole.RPA,
    status_date=ActusDateTime(2024, 1, 1),
    initial_exchange_date=ActusDateTime(2024, 1, 15),
    maturity_date=ActusDateTime(2034, 1, 15),
    notional_principal=500_000.0,
    nominal_interest_rate=0.065,
    interest_payment_cycle="1M",
    interest_payment_anchor=ActusDateTime(2024, 2, 15),
    currency="USD",
)

# Create contract and simulate with behavioral observer
rf_observer = ConstantRiskFactorObserver(constant_value=0.0)
contract = create_contract(attrs, rf_observer)
result = contract.simulate(behavior_observers=[prepayment_observer])

# Callout events inject PP (Principal Prepayment) observations into the timeline
for event in result.events:
    print(f"{event.event_type}: {event.event_time} -> ${event.payoff:.2f}")

Scenario Management

The Scenario class bundles market and behavioral observers into a single, named, reusable simulation configuration. This enables easy comparison between different simulation environments (e.g., base case vs. stress scenarios).

Example: Creating and using a Scenario:

from jactus.observers import ConstantRiskFactorObserver
from jactus.observers.scenario import Scenario
from jactus.observers.prepayment import PrepaymentSurfaceObserver

# Base case: low rates, moderate prepayment
base_scenario = Scenario(
    scenario_id="base-case",
    description="Stable rates with moderate prepayment behavior",
    market_observers={
        "rates": ConstantRiskFactorObserver(constant_value=0.04),
    },
    behavior_observers={
        "prepayment": prepayment_observer,
    },
)

# Stress scenario: rising rates, no prepayment
stress_scenario = Scenario(
    scenario_id="rising-rates",
    description="Rising rate environment, no prepayment",
    market_observers={
        "rates": ConstantRiskFactorObserver(constant_value=0.07),
    },
    behavior_observers={},
)

# Run both scenarios and compare
base_result = contract.simulate(scenario=base_scenario)
stress_result = contract.simulate(scenario=stress_scenario)

print(f"Base case net cashflow:   {sum(e.payoff for e in base_result.events):,.2f}")
print(f"Stress case net cashflow: {sum(e.payoff for e in stress_result.events):,.2f}")

A scenario provides two key methods:

  • get_observer() – Returns a unified RiskFactorObserver composing all market observers, suitable for passing to the simulation engine.

  • get_callout_events(attributes) – Collects and merges callout events from all behavioral observers, sorted by time.

Callout Event Integration in BaseContract.simulate()

The simulate() method on BaseContract natively supports behavioral observers. When behavioral observers are provided (via a Scenario, the behavior_observers parameter, or as the risk_factor_observer itself), the simulation engine:

  1. Calls contract_start() on each behavioral observer to collect callout events

  2. Merges callout events into the scheduled event timeline

  3. At each callout time, evaluates the behavioral observer with the current state

This means no special setup is required beyond creating the behavioral observer and passing it to simulate().

CLI Guide

JACTUS ships a jactus CLI (auto-installed with pip). Outputs rich tables in TTY, JSON when piped.

Exploring Contracts

# List all 18 contract types
jactus contract list

# Get schema for a specific type
jactus contract schema --type PAM

# List available observers
jactus observer list

Simulating

# Simulate from JSON attributes
jactus simulate --type PAM --attrs '{
  "contract_id": "LOAN-001",
  "status_date": "2024-01-01",
  "contract_role": "RPA",
  "initial_exchange_date": "2024-01-15",
  "maturity_date": "2025-01-15",
  "notional_principal": 100000,
  "nominal_interest_rate": 0.05,
  "interest_payment_cycle": "6M",
  "day_count_convention": "30E360"
}'

# Validate before simulating
jactus contract validate --type PAM --attrs loan.json

Risk Analytics

# DV01 (dollar value of a basis point)
jactus risk dv01 --type PAM --attrs loan.json

# Duration, convexity, and full sensitivities
jactus risk sensitivities --type PAM --attrs loan.json

Portfolio Management

# Simulate a portfolio
jactus portfolio simulate --file portfolio.json

# Aggregate cash flows by frequency
jactus portfolio aggregate --file portfolio.json --frequency quarterly

Output Formats

# JSON output for pipelines
jactus simulate --type PAM --attrs loan.json --output json | jq '.summary'

# CSV output, non-zero events only
jactus simulate --type PAM --attrs loan.json --output csv --nonzero

MCP Server

JACTUS includes an MCP (Model Context Protocol) server for integration with AI assistants like Claude Code and Claude Desktop.

Installation

pip install git+https://github.com/pedronahum/JACTUS.git#subdirectory=tools/mcp-server

The MCP server provides tools for:

  • Listing and discovering contract types

  • Getting contract schemas with working examples

  • Validating contract attributes

  • Simulating contracts

  • Computing risk metrics (DV01, delta, gamma)

  • Simulating portfolios

  • Searching documentation

For Claude Code, place .mcp.json in the project root to enable auto-discovery when opening the JACTUS workspace.

See Also

Additional Resources

Working Examples:

  • examples/pam_example.py - PAM mortgages and bonds

  • examples/lam_example.py - Amortizing loans

  • examples/interest_rate_swap_example.py - Interest rate swaps

  • examples/fx_swap_example.py - FX swaps

  • examples/cross_currency_basis_swap_example.py - Cross-currency swaps

Documentation:

  • docs/ARCHITECTURE.md - High-level architecture

  • docs/ARRAY_MODE.md - Array-mode portfolio API

  • docs/PAM.md - PAM implementation walkthrough

  • docs/derivatives.md - All derivative contract types

External Resources: