Skip to main content

A lightweight Python package for Commodity Trading Advisor (CTA) strategies

Project description

📈 TinyCTA

A Lightweight Python Package for Commodity Trading Advisor Strategies.

PyPI version MIT License Coverage Downloads CodeFactor OpenSSF Scorecard Rhiza


Quick Links: 📚 Repository📦 PyPI🐛 Issues💬 Discussions


📋 Overview

TinyCTA provides essential tools for quantitative finance and algorithmic trading, particularly for trend-following strategies. The package includes:

  • Polars-based signal processing: oscillators, moving-average crossovers, and volatility-adjusted returns
  • Robust volatility estimation via rolling median absolute deviation
  • Linear algebra utilities that handle matrices with missing values
  • Matrix shrinkage techniques commonly used in portfolio optimization

This package is designed to be the foundation for implementing CTA strategies in just a few lines of code, hence the name "TinyCTA".

📖 New here? Follow the end-to-end CTA tutorial to go from raw prices through signals and the Engine to cash positions.

🚀 Installation

Using pip

pip install tinycta

The core install keeps a minimal dependency footprint (numpy, polars, pydantic, cvx-linalg). The optional Optuna-based hyperparameter-optimisation layer (tinycta.hyper) is installed via the hyper extra:

pip install "tinycta[hyper]"

From source

Clone the repository and install using the provided Makefile:

git clone https://github.com/tschm/tinycta.git
cd tinycta
make install

This will install uv (a fast Python package installer) and create a virtual environment with all dependencies.

💻 Usage

Oscillator signal (Polars)

import polars as pl
from tinycta.osc import osc

prices = pl.DataFrame({"A": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]})
result = prices.with_columns(osc(pl.col("A"), fast=2, slow=6).alias("osc_A"))

Moving-average crossover (Polars)

import polars as pl
from tinycta.ewma import ma_cross

prices = pl.DataFrame({"A": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]})
result = prices.with_columns(
    ma_cross(pl.col("A"), fast=2, slow=6).alias("sig_A")
)

Volatility-adjusted returns (Polars)

import polars as pl
from tinycta.util import vol_adj, adj_log_prices

prices = pl.DataFrame({"A": [100, 101, 99, 102, 98, 103]})
result = prices.with_columns(
    vol_adj(pl.col("A"), vola=3, clip=4.2).alias("vol_adj_A"),
    adj_log_prices(pl.col("A"), vola=3, clip=4.2).alias("adj_log_A"),
)

Robust volatility and matrix shrinkage (tinycta.signal)

moving_absolute_deviation is an outlier-resistant rolling volatility estimate (a scaled rolling median absolute deviation of log returns), and shrink2id blends a square matrix towards the identity — a common regulariser for noisy covariance/correlation matrices.

import numpy as np
import polars as pl
from tinycta.signal import moving_absolute_deviation, shrink2id

prices = pl.DataFrame({"A": [100.0, 101.0, 99.0, 102.0, 98.0, 103.0, 97.0, 104.0]})
vol = prices.with_columns(
    moving_absolute_deviation(pl.col("A"), com=2).alias("vol_A")
)
print(sorted(vol.columns))
print(vol.shape)

cov = np.array([[4.0, 1.0], [1.0, 9.0]])
print(shrink2id(cov, lamb=0.5))  # blend halfway towards the identity
['A', 'vol_A']
(8, 2)
[[2.5 0.5]
 [0.5 5. ]]

Linear algebra operations

import numpy as np
from tinycta.linalg import solve

matrix = np.array([[1.0, 0.5], [0.5, 1.0]])
rhs = np.array([1.0, 2.0])
solution = solve(matrix, rhs)
print(np.round(solution, 10) + 0)
[0. 2.]

Position-sizing engine

The Engine turns aligned price and expected-return (mu) frames into correlation-shrinkage-optimized cash positions. It is configured by a validated Config.

import polars as pl
from tinycta.config import Config
from tinycta.engine import Engine

dates = list(range(1, 9))
prices = pl.DataFrame({"date": dates, "A": [100.0, 101.0, 102.0, 101.5, 103.0, 104.0, 103.5, 105.0]})
mu = pl.DataFrame({"date": dates, "A": [0.0, 0.1, 0.2, 0.1, 0.2, 0.3, 0.2, 0.3]})

cfg = Config(vola=3, corr=3, clip=4.2, shrink=0.5)
positions = Engine(prices=prices, mu=mu, cfg=cfg).cash_position

# .cash_position mirrors the input frame: one row per date, one column per asset.
print(sorted(positions.columns))
print(positions.shape)
['A', 'date']
(8, 2)

Config is a frozen Pydantic model: vola, corr (must be >= vola) and clip must be positive, and shrink must lie in [0, 1].

Hyperparameter optimization

tinycta.hyper.optimize runs an Optuna study over a function that builds a portfolio from a trial and scores it by Sharpe ratio, returning a frozen Study.

from tinycta.hyper import optimize

def suggest_portfolio(trial):
    fast = trial.suggest_int("fast", 2, 20)
    slow = trial.suggest_int("slow", fast + 1, 100)
    # ... build and return a jquantstats Portfolio from the suggested params ...
    return build_portfolio(fast, slow)

study = optimize(suggest_portfolio, n_trials=100, seed=42)
print(study.best_params, study.best_value)

Experiment setup (get_config)

tinycta.hyper.get_config bundles the config sections and a configured logger for a notebook experiment. It reads a shared config.yml and, for an experiment named name, an optional experiment-specific config/{name}.yml; each data / params / optuna section is taken from config.yml when present, otherwise from the sibling file. All three sections are optional.

# config.yml — every section is optional
data:
  output_path: output   # output dir, relative to the notebooks directory (default: "output")
params:
  fast: 12              # arbitrary experiment parameters
optuna:
  n_trials: 100         # arbitrary Optuna settings

Paths resolve relative to the notebooks directory — the parent of config.yml, or its grandparent when config.yml lives inside a config/ subdirectory. Outputs are written to {notebooks}/{output_path}/{name}/, and a config-supplied output_path is confined to the notebooks directory (a traversing or absolute path raises ValueError). Set the NOTEBOOK_OUTPUT_FOLDER environment variable to override the output directory entirely — this explicit operator override is trusted and not confined.

from tinycta.hyper import get_config

cfg = get_config("my_experiment")            # reads ./config.yml (+ ./config/my_experiment.yml)
cfg.logger.info("run starting")              # loguru logger, also writing to output.log
fast = cfg.params["fast"]                    # config sections as plain dicts

📚 API Reference

Signal Processing (tinycta.osc, tinycta.ewma, tinycta.util)

  • osc(x, fast, slow, min_samples=1) — analytically scaled EWMA-difference oscillator (Polars)
  • ma_cross(prices, fast, slow, min_samples=1) — sign of fast-vs-slow EWM crossover: -1, 0, or +1 (Polars)
  • vol_adj(x, vola, clip, min_samples=1) — clipped, volatility-adjusted log returns (Polars)
  • adj_log_prices(x, vola, clip, min_samples=1) — cumulative sum of volatility-adjusted log returns (Polars)

Signal Utilities (tinycta.signal)

  • moving_absolute_deviation(price, com=32) — robust rolling volatility estimate via median absolute deviation (Polars)
  • shrink2id(matrix, lamb=1.0) — shrink a matrix towards the identity matrix

Linear Algebra (tinycta.linalg)

  • valid(matrix) — extract the finite subset of a matrix by filtering NaN rows/columns
  • a_norm(vector, matrix=None) — matrix-norm of a vector
  • inv_a_norm(vector, matrix=None) — inverse matrix-norm of a vector
  • solve(matrix, rhs) — solve a linear system, handling matrices with NaN values

Position-Sizing Engine (tinycta.engine, tinycta.config)

  • Config(vola, corr, clip, shrink) — frozen Pydantic config; corr >= vola, vola/corr/clip > 0, shrink ∈ [0, 1]
  • Engine(prices, mu, cfg) — correlation-aware position optimizer; .cash_position returns per-asset cash positions
    • .assets, .ret_adj, .vola, .cor — intermediate per-asset/per-timestamp quantities

Hyperparameter Optimization (tinycta.hyper)

  • optimize(suggest_portfolio_fn, n_trials=100, seed=42) — run an Optuna study scored by Sharpe; returns a Study
  • Study — frozen result wrapper exposing best_params, best_value, n_completed, n_trials, and .plot(output_dir)
  • get_config(name, config_path=None) — load merged data/params/optuna sections and a configured logger; returns an ExperimentConfig
  • ExperimentConfigNamedTuple bundling name, logger, and the optional params, optuna and data sections

🛠️ Development

Setting up the development environment

make install

Running tests

make test

Code formatting and linting

make fmt

Cleaning up

make clean

📄 License

TinyCTA is licensed under the MIT License. See the LICENSE file for details.

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tinycta-0.14.0.tar.gz (1.7 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tinycta-0.14.0-py3-none-any.whl (21.7 kB view details)

Uploaded Python 3

File details

Details for the file tinycta-0.14.0.tar.gz.

File metadata

  • Download URL: tinycta-0.14.0.tar.gz
  • Upload date:
  • Size: 1.7 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for tinycta-0.14.0.tar.gz
Algorithm Hash digest
SHA256 926e589800bf32dee0f70ce11d6138b5db808883433c1f232f894a4b803571e8
MD5 cf70fc43c544ec5f7b1b4eb4303ab714
BLAKE2b-256 41bceb2f076dd93c086b987e2ca16f8abd24d2deab3214627273aede2550e0a9

See more details on using hashes here.

Provenance

The following attestation bundles were made for tinycta-0.14.0.tar.gz:

Publisher: rhiza_release.yml on tschm/TinyCTA

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tinycta-0.14.0-py3-none-any.whl.

File metadata

  • Download URL: tinycta-0.14.0-py3-none-any.whl
  • Upload date:
  • Size: 21.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for tinycta-0.14.0-py3-none-any.whl
Algorithm Hash digest
SHA256 80d7af70fc134450dd3e719ff4727f2d1576a61a861b5065e227686f57a58fb0
MD5 f2610a63adf42ed3675f043418b0aa6d
BLAKE2b-256 cc885e24a0a9588f77b3dccc06dd1cff30acaab3976d2ed167aa3bc181eccbe3

See more details on using hashes here.

Provenance

The following attestation bundles were made for tinycta-0.14.0-py3-none-any.whl:

Publisher: rhiza_release.yml on tschm/TinyCTA

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page