kestrel-chartkitv0.11.3

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()))
}
Market regime per bar
  • classify_regime
  • adx
  • atr
  • render_scene_svg

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))
}
Composite signal triggersBuy +0.56Buy +0.71Sell -0.45Sell -0.42Sell -0.41Buy +0.26Buy +0.23Buy +0.24Sell -0.29
  • score_indicator
  • aggregate_subscores
  • render_chart_svg

Bollinger Bands

examples/site_showcase.rsDetail: Bollinger Bands →
fn bollinger_bands() -> Chart {
    let bars = walk(7, 120);
    let mut bb = build_checked("bollinger", &params(&[("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))
}
Bollinger Bands (20, 2)
  • bollinger
  • render_chart_svg

Supertrend

examples/site_showcase.rsDetail: Supertrend →
fn supertrend() -> Chart {
    let bars = market_legs();
    let mut st = build_checked(
        "supertrend",
        &params(&[("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))
}
Supertrend (10, 3)flip downflip up
  • supertrend
  • render_chart_svg

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",
        &params(&[("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))
}
Volume profile, last 70 barsValue area 86.10 to 91.31
  • volume_profile
  • render_chart_svg