Repaint requests

Indicators

Repaint requests INTERFACE

Drive the chart to repaint on your own schedule — per tick, on a timer, on demand — without polling, and let the chart coalesce your requests into smooth frames.

The repaint model

The chart never polls your indicator. Instead, you raise an event when your visual state changes, and the chart coalesces every request inside one frame down to a single paint, capped at the display refresh rate (~60 fps). This is what lets a footprint indicator fire on thousands of ticks/sec and still render smoothly — the work is bounded by the screen, not by the tick rate.

You opt in by implementing IRepaintNotifier: one event, RepaintRequested, and one method, RequestRepaint. Raise the event whenever something the eye should see has changed. RequestRepaint is the inbound direction — the settings dialog calls it after writing render-time parameter edits (colours, modes) so they take effect next frame without waiting for a tick. Override it to raise RepaintRequested.

Example: a 1 Hz bar timer

A countdown chip showing time-to-close needs to repaint once a second, even when no tick arrives. It owns its own timer and fires RepaintRequested from it.

BarTimer.csusing System;
using System.Timers;
using TradeStrike.Pipeline.Indicators;

[IndicatorDescription("Shows the time remaining until the current bar closes, as a chip in the corner.")]
[IndicatorCategory(IndicatorCategory.Structure)]
[IndicatorInput(IndicatorInputKind.None)]
public sealed class BarTimer : IndicatorBase, IChartCustomRender, IRepaintNotifier
{
    private readonly Timer _timer = new(1000) { AutoReset = true };

    public event Action? RepaintRequested;

    public BarTimer(IIndicator? parent = null) : base(parent)
    {
        _timer.Elapsed += (_, _) => RepaintRequested?.Invoke();   // fire from the timer thread — chart marshals
        _timer.Start();
    }

    public void OnCustomRender(IIndicatorRenderContext ctx)
    {
        var bars = ctx.Bars;
        if (bars is null || !bars.TryGet(0, out var bar)) return;

        TimeSpan remaining = bar.EndUtc - ctx.Now;          // ctx.Now is in the bar timeline
        string text = remaining > TimeSpan.Zero ? remaining.ToString(@"mm\:ss") : "00:00";

        double x = ctx.PlotArea.Right - 60;
        double y = ctx.ContentTopPx + 14;                   // below the legend
        ctx.Renderer.DrawText(x, y, text, new ChartColor(200, 200, 200), 12);
    }

    public override void OnDispose() => _timer.Dispose();   // always dispose your timer
}
Threading is handled for you. RepaintRequested may be raised from any thread — the chart subscribes when the indicator is added, unsubscribes on remove, and marshals to the UI thread inside its handler. You never dispatch yourself.

You choose the cadence

The framework imposes no cadence; you drive it to suit your indicator:

  • Tick-driven — fire RepaintRequested from OnMarketData or OnBarUpdate. 10K ticks/sec is fine; the chart coalesces to vsync.
  • Timer-driven — own a timer (the BarTimer above), fire at whatever rate you like.
  • On-demand — fire once at init for a static decoration, then never again.

Repaint vs. recalculate

A repaint only redraws — it re-runs OnCustomRender with the values your output series already hold. When a parameter change makes the computed output itself stale, you need a recalculation: implement IRecalculateNotifier and raise its RecalculateRequested event. The host responds by re-running the full pipeline — it clears output, replays OnInit + OnDataLoaded over the cached context, then repaints.

IRepaintNotifier IRecalculateNotifier
Event RepaintRequested RecalculateRequested
Host does Re-runs OnCustomRender only. Clears output, replays OnInit + OnDataLoaded, then repaints.
Fire when A render-only thing changed (colour, orientation, hover). A math-affecting thing changed (a profile window, a smoothing period, an enable gating which data is accumulated).
recalculate on a math-affecting editpublic sealed class MyProfile : IndicatorBase, IRecalculateNotifier
{
    public event Action? RecalculateRequested;

    private int _window = 20;
    public int Window
    {
        get => _window;
        set { if (value != _window) { _window = value; RecalculateRequested?.Invoke(); } }
    }
}