Announcing Tycheon: Calibrated Forecasts and Risk Numbers in Python
DEV Community

Announcing Tycheon: Calibrated Forecasts and Risk Numbers in Python

Tycheon is a free, open-source Python library (Apache-2.0) that tells you how much to trust a forecast. It sits on top of forecasting models like Kronos, TimesFM and Chronos-2 and adds what they leave out: point-in-time data, calibrated intervals, risk numbers like Value at Risk, and an evaluation that always compares against a simple baseline.

Who it is for: Python developers, ML and quant engineers, fintech builders, risk teams and researchers who use forecasting models and need to know when the output holds up. It is not a trading tool and gives no buy or sell signals.

The short version: Kronos forecasts the path; Tycheon tells you how much to trust it. This post walks through what you can run today, offline, on the base install. No GPU, no market data, no account.

Install

pip install tycheon # core: baselines, calibration, risk, backtest (no torch)
pip install "tycheon[report]" # + the HTML risk report

Foundation models are optional extras so the base install stays small. import tycheon.models never imports torch. Add tycheon[kronos], [timesfm] or [chronos] when you need them. Python 3.11 or 3.12.

A forecast is a distribution, and every read is point-in-time

Tycheon ships no market data. It bundles synthetic series so everything runs offline. Each bar has two clocks: timestamp (the period it describes) and available_at (when it became known).

from tycheon.data import load_sample
from tycheon.errors import LookaheadError
from tycheon.models.baselines import GARCHForecaster, RandomWalkForecaster
bars = load_sample("SYN-GARCH")# synthetic series with two clocks
history = bars.iloc[:900]
as_of = history["available_at"].iloc[-1]# when this history became known
for model in (RandomWalkForecaster(), GARCHForecaster()):
    forecast = model.predict(history, horizon=10, n_samples=200, as_of=as_of)
    print(forecast.summary())
try:# bars published after as_of are refused before any model runs
    RandomWalkForecaster().predict(bars, horizon=10, n_samples=50, as_of=as_of)
except LookaheadError as exc:
    print("refused:", exc)

The project README shows this output from a clone:

random-walk [uncalibrated, sample paths] as_of 2021-06-12T00:00:00+00:00: last close 95.32; step 10 median 95.19, 90% interval [89.86, 99.72]. For research and risk analytics. Not investment advice.

garch [uncalibrated, sample paths] as_of 2021-06-12T00:00:00+00:00: last close 95.32; step 10 median 95.14, 90% interval [88.75, 100.5]. For research and risk analytics. Not investment advice.

refused: available_at reaches 2023-09-30T00:00:00+00:00, after as_of=2021-06-12T00:00:00+00:00

Two things to notice. Both forecasts say uncalibrated. Calibration is something a forecast earns on a holdout, not a label you set. And the last call failed before any model ran, because the data reached past as_of. That error is the whole point.

What travels with every forecast

Every forecaster returns a ForecastDistribution. It carries quantiles, optional joint sample paths, a calibration status (calibrated, stale or uncalibrated) with the evidence behind it, the model mix, as_of, a model-card reference and the disclaimer. The type refuses a status without evidence.

Supported forecasters:

Calibration in one paragraph

Tycheon calibrates in log-return space with one additive shift per quantile level and per step ahead. The score is the realised return minus the forecast quantile, and the shift is the matching quantile of past scores. Per-level shifts fix bias, not just width. There is split conformal and an ACI-style adaptive method for drifting data, and outcomes only feed back once they would have been published. The status comes from replaying recent origins with only what was known at each one.

The one rule for risk

For risk figures, Tycheon calibrates with ConformalCalibrator(widen_only=True). Lower quantiles may only move down, upper ones only up, the median stays fixed. Here is the idea as a sketch (illustration, not Tycheon's source):

import numpy as np
def widen_only(shifts, levels):
    out = np.array(shifts, dtype=float)
    levels = np.asarray(levels)
    out[levels < 0.5] = np.minimum(out[levels < 0.5], 0.0)
    out[levels > 0.5] = np.maximum(out[levels > 0.5], 0.0)
    out[levels == 0.5] = 0.0
    return out
print(widen_only([+0.012, -0.004, 0.003, -0.002, +0.010], [0.05, 0.25, 0.5, 0.75, 0.95]))
[ 0.-0.0040. 0. 0.01 ]

Why: in one manual check on real daily AAPL bars, two-sided calibration fitted on 60 calm origins cut 5-day VaR 95% from 5.38% to 3.93%, about 27% less risk. Every calculation was correct. It was one stock and one window, so treat it as a design lesson, not a statistic. I wrote the full story in my Field Notes newsletter: [newsletter link]. Forecast intervals are still calibrated both ways. Only risk figures use the widen-only rule.

Evaluation that publishes when the baseline wins

git clone https://github.com/anilatambharii/tycheon.git
cd tycheon
make setup
make benchmark-small # CPU, synthetic data, a few minutes

The walk-forward engine uses an embargo and leakage guards at every origin, and every config must include the random walk or the runner rejects it. A model only counts as beating the random walk if a one-sided Diebold-Mariano test (Harvey-Leybourne-Newbold corrected) gives p < 0.05 on both squared error and CRPS.

The first published run, 3 synthetic series and 108 origins, at a 5-bar horizon: No model beat the random walk. On synthetic series with no hidden pattern, that is the right answer, and baselines winning is a result Tycheon publishes rather than hides.

How often each model's 90% range held on Tycheon's synthetic benchmark. Synthetic data only. Synthetic data, small sample, and Kronos-mini ran with 20 sample paths against 100 for the others. Nothing here says anything about real markets. TimesFM and Chronos-2 are supported but were not in this first published run.

Serving

uv sync --extra agents --extra report
tycheon-serve --insecure-dev # REST + OpenAPI at http://127.0.0.1:8080/docs
tycheon-mcp# the same tools over MCP (stdio)

The serve and agents extras depend on Keelgate, pinned to a commit, so the README recommends working from a clone for those. The optional agent workflow routes every tool call through policy and an audit chain, and an independent verifier checks every number before a report is released.

Contribute

There are good first issues tagged in the repo. make check runs lint, format check, mypy --strict and the fast tests, and every PR needs tests. Lookahead leakage is treated as a security issue.

Tycheon builds on Kronos (shiyu-coder/Kronos, MIT licence). Thanks to its authors for releasing it openly. For research and risk analytics. Not investment advice.

About the author

I'm Anil Prasad, founder of Ambharii Labs. I lead software, data and AI engineering in healthcare, life sciences, energy and finance, and I trained my first neural network at ISRO in 1996. I've held engineering leadership roles at UnitedHealth Group, Medtronic, Xcel Energy, R1 RCM, Ambry Genetics and Duke Energy. At Ambharii Labs I build open-source tools for AI that has to be right when it matters: Tycheon, Keelgate, AgentMesh, Bulwark and Provenir.

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.