Showcase
Each example shows real output produced by kestrel-chartkit itself, from files in the project repository. Input on the left, output on the right.
Market regime
examples/site_showcase.rsDetail: Market regime →fn regime_color(regime: MarketRegime) -> &'static str {
match regime {
MarketRegime::BullishExpansion => "#26a69a",
MarketRegime::BearishExpansion => "#ef5350",
MarketRegime::Consolidation => "#78909c",
MarketRegime::Transition => "#ffb74d",
}
}
fn market_regime(width: u32, height: u32) -> Chart {
let bars = market_legs();
let mut adx = build_checked("adx", &HashMap::new())?;
let mut atr = build_checked("atr", &HashMap::new())?;
let mut regimes = Vec::new();
let mut adx_line = Vec::new();
for (i, bar) in bars.iter().enumerate() {
let (Some(a), Some(t)) = (adx.on_bar(bar), atr.on_bar(bar)) else {
continue;
};
// The ATR indicator reports 100 * ATR / close; classify_regime expects the fraction.
let regime = classify_regime(&bars[..=i], a.value, t.value / 100.0);
regimes.push((bar.timestamp, regime));
adx_line.push((bar.timestamp as f64, a.value));
}
let (first, last) = (bars[0].timestamp, bars[bars.len() - 1].timestamp);
let min = bars.iter().map(|b| b.low).fold(f64::MAX, f64::min);
let max = bars.iter().map(|b| b.high).fold(f64::MIN, f64::max);
// Headroom above the highest high for the title.
let (low, high) = (min - 1.0, max + (max - min) * 0.2);
// Price pane: one shaded box per run of equal regimes, the close on top.
let mut price = Pane::new("price", 3.0);
price.axes = vec![
Axis::time("time", first, last),
Axis::value("price", low, high),
];
let mut start = 0;
for i in 1..=regimes.len() {
if i == regimes.len() || regimes[i].1 != regimes[start].1 {
let x1 = regimes.get(i).map_or(last, |r| r.0);
price.upsert_object(SceneObject::new(
format!("regime-{start}"),
0,
0.3,
SceneObjectKind::BoundedBox {
x0: regimes[start].0 as f64,
y0: low,
x1: x1 as f64,
y1: high,
fill_color: Some(regime_color(regimes[start].1).to_string()),
border_color: None,
},
));
start = i;
}
}
price.upsert_object(SceneObject::new(
"close",
1,
1.0,
SceneObjectKind::Polyline {
points: bars.iter().map(|b| (b.timestamp as f64, b.close)).collect(),
color: "#e0e6ed".to_string(),
style: LineStyle::Solid,
width: 1.5,
},
));
price.upsert_object(SceneObject::new(
"title",
2,
1.0,
SceneObjectKind::Text {
x: first as f64 + 120.0,
y: max + (max - min) * 0.07,
content: "Market regime per bar".to_string(),
color: "#e0e6ed".to_string(),
},
));
// ADX pane with the trend threshold classify_regime uses.
let adx_max = adx_line.iter().map(|p| p.1).fold(0.0, f64::max) * 1.15;
let mut trend = Pane::new("adx", 1.0);
trend.axes = vec![
Axis::time("time", first, last),
Axis::value("adx", 0.0, adx_max),
];
trend.upsert_object(SceneObject::new(
"adx",
1,
1.0,
SceneObjectKind::Polyline {
points: adx_line,
color: "#90caf9".to_string(),
style: LineStyle::Solid,
width: 1.5,
},
));
trend.upsert_object(SceneObject::new(
"threshold",
0,
1.0,
SceneObjectKind::Polyline {
points: vec![(first as f64, 20.0), (last as f64, 20.0)],
color: "#ffb74d".to_string(),
style: LineStyle::Dashed,
width: 1.0,
},
));
let mut scene = Scene::new();
scene.upsert_pane(price);
scene.upsert_pane(trend);
Ok(render_scene_svg(&scene, width, height, &Theme::default()))
}Composite signal
examples/site_showcase.rsDetail: Composite signal →fn composite_signal() -> Chart {
let bars = market_legs();
let names = ["macd", "vortex", "trend_quality"];
let mut indicators = names
.iter()
.map(|name| build_checked(name, &HashMap::new()))
.collect::<Result<Vec<_>, _>>()?;
let mut adx = build_checked("adx", &HashMap::new())?;
let mut atr = build_checked("atr", &HashMap::new())?;
let mut markers = Vec::new();
let mut prev = TriggerAction::Hold;
for (i, bar) in bars.iter().enumerate() {
let mut subscores = Vec::new();
for (name, indicator) in names.iter().zip(indicators.iter_mut()) {
if let Some(out) = indicator.on_bar(bar) {
subscores.push(score_indicator(name, &out, &indicator.alerts()));
}
}
let (Some(a), Some(t)) = (adx.on_bar(bar), atr.on_bar(bar)) else {
continue;
};
if subscores.len() < names.len() {
continue;
}
let regime = classify_regime(&bars[..=i], a.value, t.value / 100.0);
let signal = aggregate_subscores(
subscores,
None,
regime,
Vec::new(),
Some(bar),
t.extra["raw"],
);
// Mark the bar where a trigger starts, not every bar it persists.
if signal.trigger != prev && signal.trigger != TriggerAction::Hold {
markers.push(ChartMarkerData {
timestamp: bar.timestamp,
price: bar.close,
label: format!("{:?} {:+.2}", signal.trigger, signal.score),
action: signal.trigger,
});
}
prev = signal.trigger;
}
let data = ChartRenderData {
title: "Composite signal triggers".to_string(),
bars: candles(&bars),
series: Vec::new(),
zones: Vec::new(),
markers,
};
Ok(render_chart_svg(&data, 800, 400))
}Bollinger Bands
examples/site_showcase.rsDetail: Bollinger Bands →fn bollinger_bands() -> Chart {
let bars = walk(7, 120);
let mut bb = build_checked("bollinger", ¶ms(&[("len", 20.0), ("mult", 2.0)]))?;
let (mut upper, mut basis, mut lower) = (Vec::new(), Vec::new(), Vec::new());
for bar in &bars {
if let Some(out) = bb.on_bar(bar) {
upper.push((bar.timestamp, out.extra["upper"]));
basis.push((bar.timestamp, out.extra["basis"]));
lower.push((bar.timestamp, out.extra["lower"]));
}
}
let data = ChartRenderData {
title: "Bollinger Bands (20, 2)".to_string(),
bars: candles(&bars),
series: vec![
line("Upper band", "#90caf9", upper),
line("Basis", "#ffb74d", basis),
line("Lower band", "#90caf9", lower),
],
zones: Vec::new(),
markers: Vec::new(),
};
Ok(render_chart_svg(&data, 800, 400))
}Supertrend
examples/site_showcase.rsDetail: Supertrend →fn supertrend() -> Chart {
let bars = market_legs();
let mut st = build_checked(
"supertrend",
¶ms(&[("period", 10.0), ("multiplier", 3.0)]),
)?;
// One line segment per trend run, a marker wherever the trend flips.
let mut segments: Vec<ChartSeries> = Vec::new();
let mut markers = Vec::new();
let mut prev_trend = 0.0;
for bar in &bars {
let Some(out) = st.on_bar(bar) else { continue };
let trend = out.extra["trend"];
if trend != prev_trend {
let (name, color, label, action) = if trend > 0.0 {
("Supertrend long", "#26a69a", "flip up", TriggerAction::Buy)
} else {
(
"Supertrend short",
"#ef5350",
"flip down",
TriggerAction::Sell,
)
};
if prev_trend != 0.0 {
markers.push(ChartMarkerData {
timestamp: bar.timestamp,
price: bar.close,
label: label.to_string(),
action,
});
}
segments.push(line(name, color, Vec::new()));
prev_trend = trend;
}
if let Some(segment) = segments.last_mut() {
segment.points.push((bar.timestamp, out.value));
}
}
let data = ChartRenderData {
title: "Supertrend (10, 3)".to_string(),
bars: candles(&bars),
series: segments,
zones: Vec::new(),
markers,
};
Ok(render_chart_svg(&data, 800, 400))
}Volume profile
examples/site_showcase.rsDetail: Volume profile →fn volume_profile() -> Chart {
let bars = walk(23, 120);
let lookback = 70;
let mut vp = build_checked(
"volume_profile",
¶ms(&[("lookback", lookback as f64), ("num_bins", 30.0)]),
)?;
let mut last = None;
for bar in &bars {
if let Some(out) = vp.on_bar(bar) {
last = Some(out);
}
}
let out = last.ok_or("volume profile produced no output")?;
let (poc, vah, val) = (out.extra["vpoc"], out.extra["vah"], out.extra["val"]);
let from = bars[bars.len() - lookback].timestamp;
let to = bars[bars.len() - 1].timestamp;
let data = ChartRenderData {
title: "Volume profile, last 70 bars".to_string(),
bars: candles(&bars),
series: vec![line(
"Point of control",
"#ffb74d",
vec![(from, poc), (to, poc)],
)],
zones: vec![ChartZoneData {
name: format!("Value area {val:.2} to {vah:.2}"),
price_top: vah,
price_bottom: val,
color: "#29b6f6".to_string(),
}],
markers: Vec::new(),
};
Ok(render_chart_svg(&data, 800, 400))
}