toraniko

repository·main·Indexed 21 days ago

https://github.com/0xfdf/toraniko

A multi-factor equity risk model for institutional-scale quantitative and systematic trading. Version 1.1.0 provides tools to create custom factors, estimate factor returns (market, sector, and style), and compute factor covariance matrices for portfolio optimization. It includes specialized implementations for momentum, size, and value factor scores, as well as mathematical utilities for cross-sectional normalization, winsorization, and exponential decay weighting using Polars and NumPy.

Tokens
5.4K
Snippets
18
Records
20
Agent score
75%

What's inside toraniko

  1. Required data for complete model estimation

    main

    To run a complete risk model estimation, you must provide three types of data in a format compatible with Polars/Pandas:

    1. Sector scores: Used for estimating market and sector factor returns. Typically GICS level 1. Format: One row per asset, with 0s in each column except for the asset's sector, which is filled with 1.
    2. Daily asset returns: Symbol-by-symbol daily returns for a large universe of equities. Required columns: date (str), symbol (str), asset_returns (f64).
    3. Value factor data: For calculating value metrics. Required columns: date (str), symbol (str), book_price (f64), sales_price (f64), cf_price (f64), and market_cap (f64).
  2. Calculate momentum factor scores

    main

    Use toraniko.styles.factor_mom to estimate momentum factor scores from asset returns.

    Parameters:

    • df: A Polars DataFrame containing at least symbol, date, and asset_returns.
    • trailing_days: The lookback period for momentum calculation.
    • winsor_factor: The factor used for winsorization.
    from toraniko.styles import factor_mom
    
    mom_df = factor_mom(df.select("symbol", "date", "asset_returns"), trailing_days=252, winsor_factor=0.01).collect()
  3. Calculate value factor scores

    main

    Use toraniko.styles import factor_val to estimate value factor scores using price-to-book, price-to-sales, and price-to-cash-flow metrics.

    Parameters:

    • df: A Polars DataFrame containing date, symbol, book_price, sales_price, and cf_price.
    from toraniko.styles import factor_val
    
    value_df = factor_val(df.select("date", "symbol", "book_price", "sales_price", "cf_price")).collect()
  4. Filter top N assets by group using top_n_by_group

    main

    Use toraniko.utils.top_n_by_group to select the top $N$ assets within a group (e.g., top 3000 by market cap per date) to define your model universe.

    Parameters:

    • lazy_df: A Polars LazyFrame.
    • n: The number of assets to keep per group.
    • group_by_col: The column to group by (e.g., "market_cap").
    • grouping_keys: A tuple of columns to use for grouping (e.g., ("date",)).
    • keep_group_col: Boolean indicating if the grouping column should be kept.
    from toraniko.utils import top_n_by_group
    
    ddf = top_n_by_group(
        ddf.lazy(),
        3000,
        "market_cap",
        ("date",),
        True
    ).collect()
  5. Estimate factor returns

    main

    Use toraniko.model.estimate_factor_returns to estimate factor returns (market, sector, and style) from your prepared datasets.

    Parameters:

    • returns_df: DataFrame with date, symbol, and asset_returns.
    • mkt_cap_df: DataFrame with date, symbol, and market_cap.
    • sector_df: DataFrame containing sector scores (one row per asset, sector columns as 1/0).
    • style_df: DataFrame containing estimated style factor scores (e.g., value, momentum, size).
    • winsor_factor: Factor for winsorization (default is 0.1).
    • residualize_styles: Boolean indicating whether to residualize styles.
    from toraniko.model import estimate_factor_returns
    
    fac_df, eps_df = estimate_factor_returns(returns_df, mkt_cap_df, sector_df, style_df, winsor_factor=0.1, residualize_styles=False)
  6. Estimate momentum factor scores with factor_mom

    main

    Use factor_mom to estimate rolling symbol-by-symbol momentum factor scores based on asset returns. The function applies exponential weighting to returns over a trailing period and applies a lag to the observations.

    Important: This function does not handle NaN or null values, nor does it account for pathological data distributions. You must ensure your input data is clean using the math and utils modules before calling this function.

    Input Requirements: returns_df must be a Polars DataFrame or LazyFrame containing the following columns:

    • date
    • symbol
    • asset_returns

    Parameters:

    • returns_df: The input Polars DataFrame/LazyFrame.
    • trailing_days (int, default 504): Look-back period for momentum measurement.
    • half_life (int, default 126): Decay rate for exponential weighting (in days).
    • lag (int, default 20): Number of days to lag the current day's return (e.g., 20 trading days for one month).
    • winsor_factor (float, default 0.01): Percentile used for winsorization.

    Returns: A Polars LazyFrame containing:

    • date
    • symbol
    • mom_score
    import polars as pl
    from toraniko.styles import factor_mom
    
    # Example usage with a Polars DataFrame
    df = pl.DataFrame({
        "date": ["2023-01-01", "2023-01-02"],
        "symbol": ["AAPL", "AAPL"],
        "asset_returns": [0.01, -0.005]
    })
    
    mom_scores = factor_mom(df, trailing_days=252, lag=20)
  7. Cross-sectionally normalize data with `norm_xsection()`

    main

    Use norm_xsection() to rescale a column to a specific interval (defaulting to [0, 1]) based on the min/max values within a partition defined by over_col.

    • target_col: The column to normalize.
    • over_col: The column used for partitioning (e.g., a date or group ID).
    • lower: The lower bound of the rescaling interval.
    • upper: The upper bound of the rescaling interval.

    Returns a Polars expression. NaN values are preserved and not used in min/max calculations.

    # Rescale 'price' to [0, 100] within each 'sector'
    df.with_columns(
        norm_xsection(target_col='price', over_col='sector', lower=0, upper=100)
    )
  8. Generate exponential decay weights with `exp_weights()`

    main

    The exp_weights() function generates an array of exponentially decaying weights for a given window size.

    • window: The number of points in the trailing lookback period (must be a positive integer).
    • half_life: The decay rate (must be a positive integer).

    Returns a NumPy array of weights that decay by half every half_life indices.

    import numpy as np
    from toraniko.math import exp_weights
    
    # Generate weights for a 10-day window with a 5-day half-life
    weights = exp_weights(window=10, half_life=5)
  9. Smooth feature columns with `smooth_features()`

    main

    The smooth_features() function applies a rolling mean to specified columns to reduce noise.

    Process:

    1. Sorts the data by sort_col.
    2. Calculates a rolling mean for each column in features using a trailing window of window_size.
    3. The calculation is partitioned by over_col to ensure smoothing stays within specific groups.

    Parameters:

    • df: A pl.DataFrame or pl.LazyFrame.
    • features: A tuple[str, ...] of column names to smooth.
    • sort_col: The column name used to sort the data (typically a timestamp or sequence).
    • over_col: The column name used to partition the data (e.g., a user ID or sensor ID).
    • window_size: An int representing the number of trailing periods for the moving average window.

    Returns: A pl.LazyFrame where the features columns have been replaced by their moving average values.

    Errors:

    • Raises TypeError if df is not a Polars DataFrame or LazyFrame.
    • Raises ValueError if any of the features, sort_col, or over_col are missing from the input DataFrame.
    import polars as pl
    from toraniko.utils import smooth_features
    
    # Example usage
    df = pl.DataFrame({
        "id": [1, 1, 1, 1, 2, 2, 2, 2],
        "time": [1, 2, 3, 4, 1, 2, 3, 4],
        "val": [1.0, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0]
    })
    
    smoothed_lf = smooth_features(
        df=df, 
        features=("val",), 
        sort_col="time", 
        over_col="id", 
        window_size=2
    )
  10. Select top N rows per group with `top_n_by_group()`

    main

    The top_n_by_group() function identifies the highest-ranking rows within specific groups based on a ranking variable.

    Parameters:

    • df: A pl.DataFrame or pl.LazyFrame.
    • n: An int specifying how many top rows to select per group.
    • rank_var: The column name used to determine the rank (descending order).
    • group_var: A tuple[str, ...] of column names used to define the groups.
    • filter: A bool (default True).
      • If True: Returns only the top n rows per group. The rank column is dropped.
      • If False: Returns all original rows, but adds a rank_mask column (1 if the row is in the top n, 0 otherwise).

    Returns: A pl.LazyFrame containing the processed data.

    Errors:

    • Raises TypeError if df is not a Polars DataFrame or LazyFrame.
    • Raises ValueError if rank_var or any of the group_var columns are missing.
    import polars as pl
    from toraniko.utils import top_n_by_group
    
    # Example usage: Get top 2 values per group
    df = pl.DataFrame({
        "group": ["A", "A", "A", "B", "B", "B"],
        "score": [10, 50, 30, 5, 100, 20]
    })
    
    # Returns only the top 2 rows per group
    top_rows_lf = top_n_by_group(
        df=df, 
        n=2, 
        rank_var="score", 
        group_var=("group",),
        filter=True
    )
    
    # Returns all rows with a 'rank_mask' column
    masked_lf = top_n_by_group(
        df=df, 
        n=2, 
        rank_var="score", 
        group_var=("group",),
        filter=False
    )
  11. Estimate size factor scores with factor_sze

    main

    Use factor_sze to estimate rolling symbol-by-symbol size factor scores using market capitalization. The implementation follows the Fama-French SMB (Small Minus Big) logic, meaning it multiplies the log of market cap by -1 to capture the size risk premium associated with smaller firms.

    Input Requirements: mkt_cap_df must be a Polars DataFrame or LazyFrame containing:

    • date
    • symbol
    • market_cap

    Parameters:

    • mkt_cap_df: The input Polars DataFrame/LazyFrame.
    • lower_decile (float, default 0.2): Lower bound for percentile calculation.
    • upper_decile (float, default 0.8): Upper bound for percentile calculation.

    Returns: A Polars LazyFrame containing:

    • date
    • symbol
    • sze_score
    import polars as pl
    from toraniko.styles import factor_sze
    
    # Example usage
    mkt_cap_df = pl.DataFrame({
        "date": ["2023-01-01", "2023-01-01"],
        "symbol": ["AAPL", "MSFT"],
        "market_cap": [3e12, 2e12]
    })
    
    sze_scores = factor_sze(mkt_cap_df)