kestrel-chartkitv0.33.0

Analytics and evaluation

On-demand read-outs describe the current market from a slice of bars. The evaluation module measures what happened after a signal, from forward excursions to calibration, the deflated Sharpe ratio and a logistic model.

On demand, not streaming

The analytics module holds pure functions over a slice of bars, oldest first. You call one when you need a reading and get a snapshot back; nothing keeps state between calls. They are not indicators: they do not implement the Indicator trait, are not in catalog() and do not feed the composite scoring. Most return None until the slice is long enough.

Function Returns What it reads
classify_trend_regime RegimeReading Trending or ranging, by a vote of ADX, Choppiness and Kaufman efficiency
trend_reading TrendReading Slope of a smoothed close, combined with the regime vote into a five-step phase
price_summary PriceSummary Change, window range, Wilder ATR, its percentile and an ATR stop distance
trend_persistence_reading TrendPersistenceReading How clean and durable the current trend is, 0 to 100
activity_reading ActivityReading ATR percentile, optionally averaged with a volume percentile
fear_gauge_reading FearGaugeReading Williams VIX Fix for sell-offs and its inverse for rallies, against a spike band
monthly_seasonality SeasonalityReport What each calendar month did historically
analytics::forward::* several What happened after comparable situations

Regime vote

classify_trend_regime(bars, adx_len, window) lets three measures vote “trending”: ADX at or above 25, Choppiness at or below 38.2, efficiency ratio at or above 0.5. Two or more votes make the state Trending, otherwise Ranging; trend_votes keeps the count. This is a separate classifier from the four-state classify_regime used by the composite scoring.

trend_reading takes that RegimeReading, a smoother kind and length, and a dead band in percent. A slope inside the dead band is Flat and maps to MarketPhase::Range. StrongUp and StrongDown require the regime vote to say Trending; a directional move in a ranging market is plain Up or Down.

Price and ATR

price_summary(bars, atr_len, stop_mult) spans every bar you pass, so the window length is your choice. range_position places the last close between window low and high (0.5 for a flat window). atr_percentile is the share of the published ATR history at or below the current value, a quick check whether volatility is unusually high or low. stop_distance is stop_mult · atr.

Trend persistence

trend_persistence_reading(bars) combines four sensors over a fixed 34-bar window: R² of the closes against time (weight 40), efficiency ratio (25), ADX strength and slope (20) and fractal dimension (15), smoothed over the last five bars. States run from Strong (75 and up) down to Dead (below 30). driver and drag name the strongest and weakest sensor. transition_risk is derived from the score and the efficiency sensor; it describes exposure, it does not detect a break. The score is not a probability.

Activity and fear gauge

activity_reading(bars, atr_percentile, use_volume, window) takes the ATR percentile from price_summary. The volume component is off unless you enable it, and is None when every volume in the window is zero. The score is then the ATR percentile alone; no substitute is used.

fear_gauge_reading(bars) computes the VIX Fix (wvf) and its inverse (bwvf) over 22 closes and flags a spike when a value reaches mean plus two standard deviations of its last 20 values. A fear spike wins over a complacency spike. absorbed marks a spike on a price that barely moved over five bars, likely a thin wick. Both states are context near a possible bottom or top, not a signal.

Seasonality

monthly_seasonality(bars) measures each month from the previous month’s last close to its own. The first month has no return, the last is marked incomplete and left out of the statistics. MonthStatistics carries samples, the mean, a sample standard deviation (None with fewer than two years) and positive_share. That share is a frequency over the years present, not a forecast for the next one.

Forward paths

analytics::forward answers questions about what followed comparable situations. Choosing which start points are comparable is the caller’s job; the functions only look forward from the index they are given.

Function Question
conditional_end_probability Given a phase has lasted age bars, how likely does it end within horizon?
first_passage Which of two levels does the path touch first, and after how many bars?
summarize_passages Counts, shares with a Wilson interval, median bars to each level
forward_move Close change, highest rise and deepest fall over the horizon
empirical_band Central quantile band of a set of outcomes
band_coverage Share of outcomes that later fell inside their announced band

conditional_end_probability is a Kaplan–Meier estimate: phases still running are counted as censored (PhaseDuration { ended: false }), and the result carries at_risk and a Greenwood standard error. first_passage returns None when the data ends before the horizon, so incomplete paths do not count as Neither. When one bar touches both levels, Tie decides; OHLC does not record the order. band_coverage is the check that an 80 % band contains about 80 % of what happened.

Evaluating signals

The evaluation module looks at signals after the fact. It does not decide whether a signal is good; it reports what followed, in units you can compare.

Forward price outcomes

ForwardPriceOutcome::compute(direction, entry, bars, horizon) walks the bars after entry and records the return per bar, the maximum favourable and adverse excursion (MFE, MAE, never negative) and the bar on which each was reached. PriceDirection handles the sign for Long and Short. Everything is in price units of one instrument: no contract size, no currency conversion.

PriceOutcomeStats::compute aggregates samples into hit rate, average return in percent and in ATR, and average MFE and MAE in percent. PriceStats does the same for plain profit and loss values, counting a missing value as closed but not won.

Excursion summary

evaluation::excursion::evaluate_outcomes(signals, bars, horizon) enters each signal at the close of the first bar at or after its timestamp and measures MFE and MAE over the next horizon bars, in percent of entry. Signals without enough forward bars are skipped. win_rate here is the share of signals with MFE at least MAE, a proxy and explicitly not a calibrated success rate. by_hour groups the average MFE by UTC hour of the signal.

Trade statistics

OutcomeRecorder follows setups bar by bar: record_setup takes entry, target, stop and a maximum duration, on_bar checks target and stop. A setup closes as Win, Loss or Expired; when one bar reaches both target and stop, IntrabarFillPolicy decides, StopFirst by default. Each completed setup becomes a SignalEvaluationRecord with its realised R-multiple: exit minus entry, divided by the initial risk.

TradeStats::compute(&records) aggregates them:

Field Meaning
winrate Share of Win records
profit_factor Sum of gains over sum of losses in R; infinite with gains and no losses
average_r_multiple Mean R per trade
expectancy_r The same mean, read as expected value per trade in R
max_drawdown_r Largest drop of cumulative R from its peak

The outcome decides the sign: a Win counts positive, a Loss negative, BreakEven as zero. cohort_aggregate computes the same statistics per group, keyed by any label you derive from a record.

Calibration

A record’s agreement is the share of inputs that agreed when the signal fired. It is not a probability. compute_calibration(&records, num_buckets) tests whether it behaves like one: it treats agreement as a predicted win rate, reports the Brier score and sorts records into buckets of roughly equal size, each with its mean agreement and observed win rate. A well-calibrated signal shows the two tracking each other.

To turn a raw score into a probability, IsotonicCalibrator::fit learns a monotone mapping from training data only. compute_calibration_metrics scores predictions on unseen data: Brier score, a constant baseline, the Brier skill score, log loss and expected calibration error. block_bootstrap_brier gives a mean and 5 % / 95 % percentiles of the Brier score under time dependence. split_trades_purged keeps training trades from overlapping the test period.

Deflated Sharpe ratio

deflated_sharpe(returns, trials, trial_sharpe_variance) follows Bailey and López de Prado (2014). Trying many strategies and keeping the best inflates its Sharpe ratio. The function estimates the Sharpe ratio the best of trials strategies without skill would reach (expected_max_sharpe) and returns the probability that the true Sharpe ratio exceeds it, given the sample length, skewness and kurtosis. With one trial it is the probabilistic Sharpe ratio against zero. The Sharpe ratio is per observation, not annualised. normal_quantile is the inverse normal CDF it uses, also exported on its own.

Logistic regression

LogisticRegression::fit(x, y, l2) fits P(y = 1 | x) = σ(b + w·x) with an L2 penalty on the weights (not the intercept), by Newton–Raphson. It is meant for tens of features, one weight each, so the model stays explainable. fit returns None for empty or ragged input, a negative l2 or a singular system, such as perfectly separable data without a penalty. predict(row) returns the probability. Fit and check it on separate data; an in-sample fit says little about the next signal.

Edit this page on GitHub · Docs for v0.33.0