Core concepts

Core concepts

Core concepts

The mental model behind every chapter: how plugins load, what you implement, how data is indexed, and the few patterns that recur across indicators, strategies, bar types, providers and the rest. Read this once and the per-kind chapters become quick reference.

The plugin model

A plugin is one .NET assembly compiled against public contracts. You implement a base class or an interface, build a net10.0 DLL, and drop it into the host's Plugins folder. On launch the host scans that folder, inspects each type, and registers anything that implements a known contract into the matching catalog — the Add-Indicator dialog, the strategy list, the bar type picker, the connection factory list, and so on.

There is no manifest, no registration call, and no host config to edit. Discovery is by type: the host finds your plugin by the contracts it implements, not by a name you declare anywhere. This is why the only hard rule is that the host must be able to construct your type — it instantiates your class before applying any settings.

You compile against contracts, never the engine. The SDK assemblies hold only interfaces, abstract base classes, attributes, enums and data records. The real engine lives in the host and is supplied at load time. That separation is what lets the host's implementation change underneath you with no change to your plugin — and it is the reason you deploy only your own DLL, never copies of the contract assemblies (which would create a second, incompatible copy of every contract type).

The contract assemblies

TradeStrike.Sdk brings in the three assemblies most plugins need; two more cover lower-level integration points. Each declares several namespaces — the folder name and the namespace often differ, so always using the namespace the type actually declares (the per-kind chapters and the reference spell these out).

Assembly What it holds
TradeStrike.Pipeline.Contracts The bulk of the surface: indicators (TradeStrike.Pipeline.Indicators), plots (TradeStrike.Pipeline.Plots), series (TradeStrike.Pipeline.Series), bars (TradeStrike.Pipeline.Bars), market depth (TradeStrike.Pipeline.MarketDepth), drawing tools (TradeStrike.Pipeline.Drawing) and data providers (TradeStrike.Pipeline.Providers / TradeStrike.Pipeline.Runtime).
TradeStrike.Pipeline.Trading.Contracts Orders, positions, accounts, risk, commissions and ATM bracket types (TradeStrike.Pipeline.Trading and sub-namespaces). The trading-provider and risk contracts live here.
TradeStrike.Pipeline.Strategies.Contracts The Strategy base class, its IStrategyContext runtime context, the strategy metadata attribute and backtest/analytics types (TradeStrike.Pipeline.Strategies).
TradeStrike.Pipeline.Brokerage.Contracts Brokerage gateway interfaces (TradeStrike.Pipeline.Brokerage) — a lower-level seam than a trading provider, splitting trading, account and data gateways. Reference it directly when building a gateway.
TradeStrike.Pipeline.TradeHistory.Contracts The trade-history store and backfill contracts plus the PersistedFill record (TradeStrike.Pipeline.TradeHistory). Reference it directly for trade-history plugins.

Base class or interface, plus optional capability interfaces

This is the single most important pattern in the SDK, and it recurs in every chapter. You implement one primary contract to be a plugin of a given kind — subclass IndicatorBase, subclass Strategy, subclass DrawingToolBase, implement IDataProviderFactory, implement ITradingProvider. Then you opt into extra abilities by also implementing small "capability" interfaces. The host detects them with a type check (is IFoo) and lights up the corresponding feature only when present.

The base behaviour stays minimal, and you pay for nothing you don't use. A few concrete examples drawn straight from the contracts:

Primary contract Optional capability interface What it adds
IndicatorBase IChartCustomRender Paint custom chart decorations beyond standard plot series (footprint cells, profiles, price lines).
IndicatorBase IIndicatorPanelHint Declare a default panel placement (price overlay vs. its own subpanel).
IndicatorBase IIndicatorToolbarMenu Contribute items to the chart toolbar menu.
IndicatorBase IRepaintNotifier Request a coalesced repaint when visual state changes off the bar cadence.
a data provider IMarketByOrderFeed Expose a live Market-By-Order (Level-3) feed; the host uses it only if you implement it.
a data provider IContinuousBarsCapableProvider Advertise that the provider can serve continuous historical time bars.

Here is the shape in code — one indicator that is also a custom renderer. The lifecycle methods come from IndicatorBase; OnCustomRender comes from the capability interface.

VolumeProfile.csusing TradeStrike.Pipeline.Indicators;

// Primary contract: IndicatorBase makes it an indicator.
// Capability interface: IChartCustomRender opts into the chart paint pass.
public sealed class VolumeProfile : IndicatorBase, IChartCustomRender
{
    public override void OnBarUpdate(IIndicatorContext ctx)
    {
        // ... accumulate per-price volume from ctx.Bars(0) ...
    }

    // Only called because the host saw `is IChartCustomRender`.
    public void OnCustomRender(IIndicatorRenderContext ctx)
    {
        // ... draw the histogram into ctx.PlotArea ...
    }

    // Background decoration: candles draw on top.
    public CustomRenderLayer Layer => CustomRenderLayer.BelowBars;
}
How to read each chapter. Find the one primary contract for the kind you are building, then scan its "optional" interfaces. You never have to implement all of them — implement the primary one to exist, add capability interfaces to grow.

Series & bars-ago indexing

Bars and indicator outputs are exposed as series with bars-ago indexing, the same convention NinjaTrader users know: [0] is the most recent value, [1] the one before, and so on. For a bar series, [0] is the current bar (in-progress when live, most-recent-closed in history).

C#var bars = ctx.Bars(0);          // the primary IBarSeries
Bar latest = bars[0];            // most recent bar
double prevClose = bars[1].Close; // the bar before it
int n = bars.Count;              // total bars ever observed (monotonic)

Two interfaces carry this. IBarSeries is the read-only OHLCV series with bars-ago indexing, a Count (total bars ever observed) and a Capacity (the ring size, the largest lookback it can serve). It also exposes cached price views — Close, High, Median, Typical and so on — each an ISeries<double>. ISeries<T> is the generic read-only series used for indicator output plots.

For your own writable output, the SDK provides ChunkedSeries<double> in TradeStrike.Pipeline.Series: an append-only, single-writer / multi-reader series. You append one value per bar and hand the series to a plot.

Warmup is NaN, not zero. For double-typed series, positions that aren't computed yet hold double.NaN — that is how warmup bars render as gaps rather than a misleading zero. Use TryGet for a safe read during warmup instead of indexing into an unpopulated slot. Code that distinguishes "not computed" from "computed as zero" must check for NaN.

Ambient providers

Some plugins need a shared host service that the framework can't pass in through a constructor — because the host instantiates your type generically, before any context exists. The SDK solves this with ambient providers: a small static accessor in the contracts that the host sets once at startup, and that your plugin reads on demand. It is a deliberately narrow seam — the host injects an implementation; you consume an interface.

Examples that exist in the contract assemblies (consume them; the host owns the wiring):

  • IndicatorResolverAmbient (TradeStrike.Pipeline.Indicators) — exposes the host's indicator catalog (built-ins and plugin-supplied) so an indicator that hosts other indicators can discover and create them by type.
  • TradingHoursAmbient (TradeStrike.Pipeline.Indicators.TradingHours) — resolves trading-hours templates so session-aware logic can ask the host for the right session windows.
  • ContractRolloverAmbient (TradeStrike.Pipeline.Indicators.ContractRollover) — resolves futures contract-rollover dates for an instrument.

The shape is the same across all of them: a static class with a Resolver-style property the host sets, plus a scoped Use(...) helper for tests. Conceptually, think of an ambient provider as "a singleton service the host hands you, reachable without a constructor argument." You generally read these; setting them is the host's job (and the test path's).

Prefer the context. Where a lifecycle context is available — IIndicatorContext, IStrategyContext — get shared data from it first (it already surfaces trading hours, tick size, the indicator resolver, and more). Reach for an ambient provider only when no context is in scope at the point you need the service.

Lifecycle phases

Every plugin kind that processes market data follows the same three phases, even though the method names differ per kind. Understanding the phases — not memorising names — is what matters.

  1. Init. The host has constructed your object and applied settings; validate parameters and allocate buffers. (Indicators: OnInit. Strategies: OnInitialize / OnStart.)
  2. History / data loaded. Backfill has arrived; compute over the whole history in one pass, oldest bar first. (Indicators: OnDataLoaded.)
  3. Live updates. New data flows in; update incrementally so live values exactly match what the history pass produced. (Indicators: OnBarUpdate per closed bar, plus optional OnMarketData per tick and OnMarketDepth / OnMarketByOrder for depth. Strategies: OnBar, OnFill, OnOrderUpdate, OnPositionUpdate.)

The flow below is the indicator lifecycle; strategies, bar builders and providers follow the same init → history → live arc.

flowchart TD
  A[Host constructs your type] --> B[Apply settings / parameters]
  B --> C[Init: validate & allocate]
  C --> D[History: compute over backfill, oldest first]
  D --> E{Live data}
  E -->|bar closes| F[Per-bar update]
  E -->|tick / depth| G[Per-tick / depth update]
  F --> E
  G --> E
  E -->|teardown| H[Dispose / stop]
History and live must agree. The golden rule across plugin kinds: the value computed for a bar during the history pass and the value computed for that same bar live must be identical. Share the maths between the two paths so they can never drift apart.

Threading expectations

At a high level: lifecycle callbacks for one instance are delivered in order — you are not called re-entrantly for the same object — so per-instance state needs no locking on the hot path. Series follow a single-writer / multi-reader model: your plugin writes its output; the chart and other readers read it, possibly from another thread. Hot-path callbacks (per-tick processing, custom render, which can fire up to 60×/second) should be allocation-free and fast; do slow or blocking work off the callback. Where a contract is explicitly thread-safe or has a specific delivery model, its chapter and the reference say so — follow those notes rather than assuming.

Discovery & deployment at a glance

Build a net10.0 DLL, copy only that DLL into the host's Plugins folder (the SDK's MSBuild target can do this on every build), and restart. The loader scans the folder once at startup, finds your types by the contracts they implement, and registers them. The Building & deploying chapter covers the folder choices, dependency isolation and troubleshooting in full.