Skip to content

rishav-eulb/morpho_curator

Repository files navigation

Morpho Curator Backtest

A comprehensive backtesting framework for evaluating lending market curation strategies on Morpho Blue.

Python 3.11+

Table of Contents


Overview

Morpho Curator Backtest enables you to:

  • Ingest historical market data from the Morpho Blue GraphQL API
  • Define curation strategies via YAML configuration files
  • Backtest strategies against historical data with realistic cost modeling
  • Evaluate performance using standard risk-adjusted metrics (Sharpe, Sortino, max drawdown)
  • Compare strategies against benchmark allocations (equal-weight, top-APY greedy, TVL-weighted)

Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│                              CLI (cli.py)                               │
│    curator-bt data sync | db init | strategy validate | run backtest   │
└─────────────────────────────────────────────────────────────────────────┘
                                     │
            ┌────────────────────────┼────────────────────────┐
            ▼                        ▼                        ▼
┌───────────────────┐    ┌───────────────────┐    ┌───────────────────┐
│    Data Layer     │    │   Strategy Layer  │    │   Engine Layer    │
│  src/data/        │    │  src/strategy/    │    │  src/engine/      │
│                   │    │                   │    │                   │
│  • ingestion.py   │    │  • filters/       │    │  • backtester.py  │
│  • provider.py    │    │  • constraints/   │    │  • optimizer.py   │
│  • models.py      │    │  • utilities/     │    │  • portfolio.py   │
│  • queries.py     │    │  • config.py      │    │  • cost_model.py  │
└───────────────────┘    │  • registry.py    │    │  • schedule.py    │
         │               └───────────────────┘    └───────────────────┘
         │                        │                        │
         ▼                        ▼                        ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                           PostgreSQL Database                           │
│   chains | assets | markets | market_snapshots | asset_prices | ...    │
└─────────────────────────────────────────────────────────────────────────┘
                                     │
                                     ▼
                         ┌───────────────────┐
                         │ Evaluation Layer  │
                         │ src/evaluation/   │
                         │                   │
                         │  • metrics.py     │
                         │  • benchmarks.py  │
                         │  • comparator.py  │
                         │  • export.py      │
                         └───────────────────┘

Layer Responsibilities

Layer Purpose
Data Layer Ingests data from Morpho API, stores in PostgreSQL, provides time-bounded queries via SafeDataProvider
Strategy Layer Defines composable filters, constraints, and utility functions loaded from YAML configs
Engine Layer Orchestrates the backtest loop: scheduling, filtering, optimizing, portfolio tracking
Evaluation Layer Computes performance metrics, runs benchmarks, exports results

Installation

Prerequisites

  • Python 3.11+
  • PostgreSQL 14+
  • pip or pipx

Install from Source

# Clone the repository
git clone <repository-url>
cd morpho-curator-backtest

# Create virtual environment
python -m venv .venv
source .venv/bin/activate  # Linux/macOS
# or: .venv\Scripts\activate  # Windows

# Install dependencies
pip install -e .

# Install dev dependencies (optional)
pip install -e ".[dev]"

Verify Installation

curator-bt --help

Database Setup

1. Create PostgreSQL Database

# Connect to PostgreSQL
psql -U postgres

# Create database and user
CREATE USER curator WITH PASSWORD 'curator';
CREATE DATABASE morpho_backtest OWNER curator;
GRANT ALL PRIVILEGES ON DATABASE morpho_backtest TO curator;
\q

2. Configure Connection

Set environment variables or use defaults:

export CURATOR_BT_DB_HOST=localhost
export CURATOR_BT_DB_PORT=5432
export CURATOR_BT_DB_USER=curator
export CURATOR_BT_DB_PASSWORD=curator
export CURATOR_BT_DB_NAME=morpho_backtest

3. Initialize Schema

# Create tables from ORM models
curator-bt db init

# Or run Alembic migrations (recommended for production)
curator-bt db migrate

4. Sync Market Data

# Full sync from Morpho API (Ethereum + Base by default)
curator-bt data sync

# Incremental sync (only new data since last sync)
curator-bt data sync --incremental

# Specify chains explicitly
curator-bt data sync --chains 1,8453

# Check data coverage
curator-bt data status

Quick Start

1. Validate a Strategy

curator-bt strategy validate config/strategies/aggressive_yield.yaml

2. Run a Backtest

curator-bt run backtest config/strategies/aggressive_yield.yaml

3. Export Results

curator-bt results export config/strategies/aggressive_yield.yaml --output-dir ./output

CLI Reference

Database Commands

curator-bt db init      # Create all tables from ORM models
curator-bt db migrate   # Run Alembic migrations to latest revision
curator-bt db drop -y   # Drop all tables (requires confirmation)

Data Commands

curator-bt data sync                    # Full sync from Morpho API
curator-bt data sync --incremental      # Only fetch new data
curator-bt data sync --chains 1         # Ethereum only
curator-bt data sync --interval HOUR    # Hourly snapshots
curator-bt data status                  # Show data coverage report

Strategy Commands

curator-bt strategy list                            # List saved strategies
curator-bt strategy show <name>                     # Print strategy YAML
curator-bt strategy validate <path>                 # Validate and check components
curator-bt strategy components                      # List all registered components

Run Commands

curator-bt run backtest <strategy.yaml>                    # Run backtest
curator-bt run backtest <strategy.yaml> --start 2024-06-01 # Override start date
curator-bt run backtest <strategy.yaml> --end 2025-01-01   # Override end date
curator-bt run backtest <strategy.yaml> --capital 500000   # Override capital
curator-bt run backtest <strategy.yaml> --frequency daily  # Override frequency

Results Commands

curator-bt results export <strategy.yaml> --output-dir ./output --fmt csv
curator-bt results export <strategy.yaml> --fmt json
curator-bt results export <strategy.yaml> --fmt all

Strategy Configuration

Strategies are defined in YAML files. The configuration schema:

strategy:
  name: "my_strategy_v1"
  description: "Description of the strategy"

  # Rebalance frequency: daily | weekly | biweekly | monthly | quarterly
  rebalance_frequency: "weekly"
  
  # Starting capital in USD
  initial_capital: 1000000

  # Backtest period
  backtest:
    start_date: "2024-01-01"
    end_date: "2025-01-01"

  # Transaction cost model
  cost_model:
    enabled: true
    fixed_cost_per_rebalance_usd: 5.0   # Gas + fixed fees
    proportional_cost_bps: 1.0           # Slippage in basis points

  # Filters applied sequentially to eliminate ineligible markets
  filters:
    - name: "whitelist"
    - name: "min_market_age"
      params:
        min_days: 14
    - name: "min_trailing_apy"
      params:
        min_apy: 0.05
        lookback_days: 14

  # Constraints for the optimizer
  constraints:
    - name: "max_weight_per_market"
      params:
        max_weight: 0.30

  # Utility function to maximize
  utility:
    name: "trailing_net_supply_apy"
    params:
      lookback_days: 14
      field: "net_supply_apy"

Component System

All filters, constraints, and utilities are registered in a central registry and referenced by name in YAML configs.

Filters

Filters eliminate ineligible markets. Applied sequentially in YAML order.

Category Filters
Basic whitelist, min_market_age, loan_asset_type
Yield min_trailing_apy, max_trailing_apy, min_apy_stability
Utilization min_avg_utilization, max_avg_utilization, max_peak_utilization
Liquidity min_avg_tvl, min_liquidity_ratio
Risk max_collateral_drawdown, no_bad_debt, no_red_warnings

Constraints

Constraints define bounds for the optimizer. Return scipy-compatible constraint dicts.

Category Constraints
Allocation max_weight_per_market, min_weight_per_market, max_weight_per_collateral, max_weight_per_loan_asset
Utilization max_avg_utilization, max_peak_utilization, min_liquidity_ratio
Risk max_portfolio_concentration

Utilities

Utilities score allocation vectors. The optimizer maximizes this score.

Category Utilities
Yield trailing_net_supply_apy, weighted_avg_apy
Risk-Adjusted sharpe_ratio, sortino_ratio, min_variance
Composite composite (weighted combination of other utilities)

List Available Components

curator-bt strategy components

Example Strategies

Aggressive Yield Strategy

Maximizes net supply APY with minimal constraints.

# config/strategies/aggressive_yield.yaml
strategy:
  name: "aggressive_yield_v1"
  
  filters:
    - name: "whitelist"
    - name: "min_market_age"
      params: { min_days: 14 }
    - name: "min_trailing_apy"
      params: { min_apy: 0.05, lookback_days: 14 }
    - name: "min_avg_utilization"
      params: { min_util: 0.30, lookback_days: 14 }
    - name: "min_avg_tvl"
      params: { min_usd: 1000000, lookback_days: 14 }
    - name: "no_bad_debt"

  constraints:
    - name: "max_weight_per_market"
      params: { max_weight: 0.30 }

  utility:
    name: "trailing_net_supply_apy"
    params: { lookback_days: 14, field: "net_supply_apy" }

Conservative Stablecoin Strategy

Low-risk stablecoin lending with yield stability focus.

# config/strategies/conservative_stable.yaml
strategy:
  name: "conservative_stablecoin_v1"
  
  filters:
    - name: "whitelist"
    - name: "min_market_age"
      params: { min_days: 30 }
    - name: "loan_asset_type"
      params: { allowed_tags: ["stablecoin"] }
    - name: "min_avg_tvl"
      params: { min_usd: 5000000, lookback_days: 30 }
    - name: "max_collateral_drawdown"
      params: { max_drawdown: 0.15, lookback_days: 90 }
    - name: "no_bad_debt"
    - name: "no_red_warnings"

  constraints:
    - name: "max_weight_per_market"
      params: { max_weight: 0.25 }
    - name: "max_weight_per_collateral"
      params: { max_weight: 0.40 }

  utility:
    name: "composite"
    params:
      components:
        - name: "sharpe_ratio"
          weight: 0.6
          params: { lookback_days: 60, field: "net_supply_apy" }
        - name: "min_variance"
          weight: 0.4
          params: { lookback_days: 60, field: "net_supply_apy" }

Backtest Flow

The backtest engine follows this flow for each evaluation date:

1. Load Strategy Config
   └── Parse YAML → Validate → Instantiate components

2. Generate Schedule
   ├── Rebalance dates (weekly/monthly/etc.)
   └── Daily evaluation grid

3. For each evaluation date:
   │
   ├── If REBALANCE DAY:
   │   │
   │   ├── Get all active markets
   │   │
   │   ├── FILTER CHAIN (sequential)
   │   │   ├── Filter 1: whitelist
   │   │   ├── Filter 2: min_market_age
   │   │   ├── Filter 3: min_trailing_apy
   │   │   └── ... → survivors
   │   │
   │   ├── BUILD CONSTRAINTS
   │   │   └── Each constraint → scipy constraint dict
   │   │
   │   ├── OPTIMIZE
   │   │   └── scipy.minimize(-utility, constraints) → weights
   │   │
   │   └── APPLY REBALANCE
   │       └── Update portfolio, deduct costs
   │
   └── MARK TO MARKET
       └── Fetch actual APYs, update portfolio value

4. Compute Aggregate Metrics
   └── Total return, Sharpe, max drawdown, etc.

Database Schema

Core Tables

Table Description
chains Blockchain network metadata (Ethereum, Base, etc.)
assets ERC-20 token information (symbol, decimals, tags)
markets Morpho Blue market parameters (LLTV, oracle, IRM)
market_snapshots Time-series state: APY, utilization, TVL, etc.
asset_prices Historical USD prices per asset

Backtest Result Tables

Table Description
backtest_runs Execution metadata and aggregate results
backtest_snapshots Per-timestep portfolio state
backtest_allocations Per-market allocation at each snapshot
backtest_filter_log Audit log of filter eliminations

Environment Variables

All settings can be overridden via environment variables:

Variable Default Description
CURATOR_BT_DB_HOST localhost PostgreSQL host
CURATOR_BT_DB_PORT 5432 PostgreSQL port
CURATOR_BT_DB_USER curator Database user
CURATOR_BT_DB_PASSWORD curator Database password
CURATOR_BT_DB_NAME morpho_backtest Database name
CURATOR_BT_MORPHO_GRAPHQL_URL https://blue-api.morpho.org/graphql Morpho API endpoint
CURATOR_BT_DEFAULT_CHAINS [1, 8453] Default chains to ingest
CURATOR_BT_DEFAULT_CAPITAL 1000000 Default initial capital
CURATOR_BT_LOG_LEVEL INFO Logging level

Development

Running Tests

# Run all tests
pytest

# Run with coverage
pytest --cov=src --cov-report=html

# Run specific test file
pytest tests/test_engine.py -v

Project Structure

morpho-curator-backtest/
├── cli.py                      # CLI entry point
├── pyproject.toml              # Dependencies and build config
├── alembic.ini                 # Alembic migration config
├── alembic/
│   ├── env.py
│   └── versions/               # Migration scripts
├── config/
│   ├── settings.py             # Pydantic settings
│   └── strategies/             # Example strategy YAMLs
│       ├── aggressive_yield.yaml
│       └── conservative_stable.yaml
├── src/
│   ├── data/
│   │   ├── models.py           # SQLAlchemy ORM models
│   │   ├── ingestion.py        # API data ingestion
│   │   ├── provider.py         # Data provider interface
│   │   └── queries.py          # Database queries
│   ├── strategy/
│   │   ├── base.py             # Abstract base classes
│   │   ├── config.py           # YAML loader
│   │   ├── registry.py         # Component registry
│   │   ├── filters/            # Filter implementations
│   │   ├── constraints/        # Constraint implementations
│   │   └── utilities/          # Utility implementations
│   ├── engine/
│   │   ├── backtester.py       # Main backtest orchestrator
│   │   ├── optimizer.py        # scipy-based optimizer
│   │   ├── portfolio.py        # Portfolio tracking
│   │   ├── cost_model.py       # Transaction cost model
│   │   └── schedule.py         # Rebalance scheduling
│   ├── evaluation/
│   │   ├── metrics.py          # Performance metrics
│   │   ├── benchmarks.py       # Benchmark strategies
│   │   ├── comparator.py       # Strategy comparison
│   │   └── export.py           # Result export
│   └── dashboard/              # (Future) Web dashboard
└── tests/
    ├── test_engine.py
    ├── test_evaluation.py
    └── test_strategy.py

Adding a New Filter

  1. Create filter class in src/strategy/filters/:
from src.strategy.base import BacktestContext, BaseFilter
from src.strategy.registry import register

@register("filter", "my_custom_filter")
class MyCustomFilter(BaseFilter):
    """Description of what this filter does."""

    @property
    def name(self) -> str:
        return "my_custom_filter"

    async def apply(self, ctx: BacktestContext, market_ids: list[str]) -> list[str]:
        threshold = self.params.get("threshold", 0.5)
        survivors = []
        for mid in market_ids:
            # Your filtering logic here
            if should_include(mid):
                survivors.append(mid)
        return survivors
  1. Import in src/strategy/filters/__init__.py

  2. Use in YAML:

filters:
  - name: "my_custom_filter"
    params:
      threshold: 0.5

License

[Add license information]


Contributing

[Add contribution guidelines]

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published