News

Build something

News

Read live and past headlines and story text from the connected venues, in an indicator, a strategy or an add-on.

How it works

News comes from the connected venues that serve it. Each venue type that serves news is one source. When two connections of the same venue type are connected, one of them serves the headlines and the other waits; if the serving one disconnects, the other takes over and fills the gap. So the same headline never arrives twice.

An indicator or a strategy reaches news through its News property, an INewsClient. An add-on uses IAddOnHost.News, an INewsHub (see Add-ons). The types live in SabrTrader.Pipeline.News.

Member What it does
bool IsAvailable True when this place delivers news. False in backtests and the other places in Where it works.
IReadOnlyList<NewsSourceInfo> Sources The connected sources, a snapshot that is safe to enumerate. Empty when none is connected.
NewsFilter ThisInstrument The filter for your script's own instrument.
IDisposable Subscribe(NewsFilter filter, Action<NewsHeadline> onHeadline) Live headlines into your handler.
bool RequestHeadlines(NewsQuery query, Action<NewsHeadlinesResult> onResult) Past headlines, merged over the sources, into one call of your handler.
bool RequestStory(NewsHeadline headline, Action<NewsStoryResult> onResult) The story text of one headline.

Every method returns at once. None of them blocks and none of them touches the network, so you can call them from OnBarUpdate or OnBar. Answers come back later, in your handler, on your script's own thread.

A headline

A NewsHeadline is immutable and shared between everyone who receives it, so you can keep it.

Field Holds
Id A NewsArticleId: the SourceKey of the source that served it plus the venue's own VenueArticleId. Compare headlines by Id, not by record equality.
TimeUtc When the venue published it, in UTC.
Title The headline text.
Summary A short teaser, or null when the venue sends none.
Symbols The symbols the venue tagged it with, trimmed and upper-case. Empty when untagged.
Provider A NewsProvider with Code and DisplayName: who wrote it. One source can carry several providers.
SourceDisplayName The name of the source that served it.
Url A link to the original article, or null.
InlineStory The full story when the venue sends it with the headline; null otherwise. You do not need to read it: RequestStory covers every case.

Choosing headlines

A NewsFilter has three lists: Sources (source keys), Providers (provider codes) and Symbols. A headline matches when it passes every list. For each list, null means all and an empty list means none. NewsFilter.All is every headline from every source.

News.ThisInstrument is the filter for your script's own instrument. The platform works out the news symbol with the instrument's own venue, so you never deal with venue spellings. When the instrument has no news symbol, the filter has an empty symbol list. It then matches nothing, never all news, and never a guess. Check ThisInstrument.Symbols is { Count: 0 } if you want to tell the user.

Keyword matching is up to you: filter on Title or Summary in your handler, as the example strategy does.

Live headlines

Subscribe delivers every new headline that matches the filter, in order, to your handler. It works before any source is connected: sources join as they connect, and come back after a reconnect. You subscribe once and never rewire.

C#IDisposable live = News.Subscribe(News.ThisInstrument, headline =>
    Context.Print($"{headline.TimeUtc:HH:mm} UTC {headline.Provider.DisplayName}: {headline.Title}"));

The subscription ends by itself when your script stops. Dispose the result to end it sooner. Give each subscription its own handler, so you always know which filter a headline came through.

Where news is not available, Subscribe returns a subscription that never delivers, so you need no check before you subscribe.

Past headlines

RequestHeadlines asks every source the query names and gives you one merged answer. A NewsQuery has the filter lists plus a time range and a limit: new NewsQuery(fromUtc, toUtc, limit, Sources, Providers, Symbols). Set Limit to at least 1.

C#DateTime now = DateTime.UtcNow;
var query = new NewsQuery(now.AddDays(-1), now, 50, Symbols: News.ThisInstrument.Symbols);

bool asked = News.RequestHeadlines(query, result =>
{
    foreach (NewsHeadline headline in result.Items)    // newest first
        Context.Print(headline.Title);
});
// asked == false: no news here, and the handler will never run.
  • It returns false, and never calls your handler, when news is not available here. Otherwise your handler runs exactly once, later, on your script's thread. So a script can never wait for an answer that will not come.
  • Items holds the headlines newest first, with duplicates removed, at most Limit over all sources together.
  • Sources holds one NewsSourceOutcome (SourceKey, Status) per source that was asked. One source failing never hides the headlines of the others. It is empty when no source was asked: nothing is connected, or no connected source matches the query.
  • To go further back, ask again with ToUtc set to the oldest headline you have. Each source goes back as far as its NewsSourceInfo.HistoryDepth.
  • A source whose RequiresSymbols is true serves past headlines only per symbol. A query without symbols gets NotAvailable from it.

On a live chart this works during the historical pass too, so an indicator can draw the headlines of the bars on screen.

The story

Venues handle story text in three ways: some send it with the headline, some serve it by id, and some give only a link. RequestStory covers all three, so you never need to know which one a venue does. It has the same contract as RequestHeadlines: false and no call when news is not available, otherwise one call later on your thread.

C#News.RequestStory(headline, result =>
{
    if (result.Status == NewsStatus.Ok)
        Context.Print(result.Story!.Body);             // Text, Html or Markdown: see result.Story.Format
    else if (headline.Url is not null)
        Context.Print($"Read it at {headline.Url}");   // link only
});

When the status is Ok, Story holds a NewsStory with Body, Format and Url. The body keeps the venue's own format: NewsBodyFormat.Text, NewsBodyFormat.Html or NewsBodyFormat.Markdown. When there is only a link, the status is NotAvailable and the headline's Url is where the story is.

Statuses

Every answer carries a NewsStatus. It is the one status type for news, for a whole story and for each source of a headline request.

NewsStatus Meaning
Ok Served. An Ok with no headlines means there were none.
NotAvailable Nothing to serve this from: no matching source, a query without symbols to a source that needs them, or no story for the headline (link only).
NotEntitled The venue refused because the account is not entitled to its news.
Offline The source is not connected.
Failed The venue answered with an error, or the request could not be completed. Ask again later.

When your handler runs

Every handler runs on your script's own thread, never inside OnBarUpdate or OnBar, so you never need a lock.

  • On a chart, handlers run with the chart's other callbacks (AI answers, decisions), on the chart's next bar event. On a quiet market a headline can wait for that next event.
  • While the chart works through history, live headlines are not handed to the indicator yet: they start once the chart is live. Past headlines you ask for during history are handled once that pass is done.
  • In a strategy, handlers run on the strategy's own thread, like OnBar, never during it.

A source that has no live stream is polled every NewsSourceInfo.PollInterval, so its headlines can arrive up to that much after they were published. TimeUtc is always the publish time, not the arrival time.

Where it works

News works on chart indicators and on live strategy runs. Market Replay charts and strategies running on Market Replay get NewsClient.None: today's headlines mean nothing at a replayed time.

In backtests, the Strategy Analyzer, the optimizer, the Market Analyzer and any host without news, News is NewsClient.None and IsAvailable is false. There, Subscribe never delivers and the Request* methods return false. The reason is look-ahead: a bar in a backtest could otherwise see a headline from after it closed.

A strategy that changes its trades on news therefore trades differently in a backtest than live: the backtest never sees a headline. Keep that in mind when you compare results.

NewsClient.None also works as a stand-in in unit tests.

Example indicator

This indicator prints the last day's headlines for the chart's instrument once, then every new one as it arrives.

HeadlinePrinter.csusing System;
using SabrTrader.Pipeline.Indicators;
using SabrTrader.Pipeline.News;
using SabrTrader.Pipeline.Series;

namespace MyCompany.Indicators;

[IndicatorDescription("Prints the headlines for the chart's instrument.")]
[IndicatorCategory(IndicatorCategory.Custom)]
[IndicatorInput(IndicatorInputKind.Bars)]
public sealed class HeadlinePrinter : IndicatorBase
{
    private IDisposable? _live;

    public HeadlinePrinter(IIndicator? parent = null, ISeries<double>? input = null)
        : base(parent, input) { }

    public override void OnInit(IIndicatorContext ctx)
    {
        base.OnInit(ctx);
        if (!News.IsAvailable) return;                  // a backtest or the Market Analyzer: no news

        NewsFilter mine = News.ThisInstrument;
        if (mine.Symbols is { Count: 0 })
        {
            ctx.Print("Headlines: this instrument has no news symbol.");
            return;
        }

        // OnInit runs again on every recalculation, so end the old subscription first.
        _live?.Dispose();
        _live = News.Subscribe(mine, headline => Show("live", headline));

        DateTime now = DateTime.UtcNow;
        News.RequestHeadlines(new NewsQuery(now.AddDays(-1), now, 20, Symbols: mine.Symbols), OnPastHeadlines);
    }

    public override void OnDispose() => _live?.Dispose();

    private void OnPastHeadlines(NewsHeadlinesResult result)
    {
        foreach (NewsSourceOutcome source in result.Sources)
            if (source.Status != NewsStatus.Ok)
                Context.Print($"Headlines from {source.SourceKey}: {source.Status}");

        for (int i = result.Items.Count - 1; i >= 0; i--)   // oldest first
            Show("past", result.Items[i]);
    }

    private void Show(string kind, NewsHeadline headline)
        => Context.Print($"{kind} {headline.TimeUtc:yyyy-MM-dd HH:mm} UTC "
                         + $"{headline.Provider.DisplayName}: {headline.Title}");
}

When the chart recalculates the indicator, the platform ends the indicator's subscriptions and drops headlines it has not handled yet. OnInit then runs again and subscribes again. So subscribe in setup code like this, not once from OnBarUpdate behind a "done" flag: such a flag would stop the indicator from subscribing again after a recalculation. Disposing the old subscription first keeps the example correct on any host.

Example strategy

This strategy stops entering for a number of minutes after a headline that holds one of a few words. The pause counts from the headline's publish time, and compares it with the close time of the bar, both in UTC. Exits are not paused.

NewsPauseStrategy.cs (excerpt)public sealed class NewsPauseStrategy : Strategy
{
    // A headline whose title holds one of these words pauses new entries.
    private static readonly string[] PauseWords = { "FOMC", "CPI", "Powell", "payrolls" };

    private StrategyParameter<int> _pauseMinutes = null!;
    private DateTime _pauseUntilUtc = DateTime.MinValue;

    protected override void OnInitialize()
    {
        _pauseMinutes = IntParameter("Pause minutes", 15, 1, 240, 1);
    }

    protected override void OnStart()
    {
        // Every headline from every source. News.ThisInstrument would keep only this instrument's.
        News.Subscribe(NewsFilter.All, OnHeadline);
    }

    // Runs on the strategy's thread, like OnBar, never during it.
    private void OnHeadline(NewsHeadline headline)
    {
        if (!HasPauseWord(headline.Title)) return;
        DateTime until = headline.TimeUtc.AddMinutes(_pauseMinutes.Value);
        if (until <= _pauseUntilUtc) return;
        _pauseUntilUtc = until;
        Log($"No new entries until {until:HH:mm} UTC: {headline.Title}");
    }

    protected override void OnBar()
    {
        if (CurrentBar.EndUtc < _pauseUntilUtc) return;     // in a news pause: no new entries
        if (!IsFlat || BarCount < 2) return;
        if (CurrentBar.Close > GetBar(1).Close) EnterLong(1);
    }

    private static bool HasPauseWord(string title)
    {
        foreach (string word in PauseWords)
            if (title.Contains(word, StringComparison.OrdinalIgnoreCase)) return true;
        return false;
    }
}

The subscription ends by itself when the run stops, so the strategy does not keep the handle. The excerpt leaves out the usings (System, SabrTrader.Pipeline.News, SabrTrader.Pipeline.Strategies) and the [StrategyMetadata] attribute. See Your first strategy.

In a backtest this strategy never pauses, because there is no news there (see Where it works).

Add-ons

An add-on reads news through IAddOnHost.News, an INewsHub?. It is null in a host without news, so check it, the same as the other optional host members. The hub has the same three verbs as the script client, with Tasks instead of callbacks:

Member What it does
IReadOnlyList<NewsSourceInfo> Sources The connected sources, a snapshot.
event Action? SourcesChanged Raised, on any thread, when a source joins or leaves.
IDisposable Subscribe(NewsFilter filter, Action<NewsHeadline> onHeadline) Live headlines. Dispose the result to stop.
Task<NewsHeadlinesResult> GetHeadlinesAsync(NewsQuery query, CancellationToken ct) Past headlines, merged over the sources, as in Past headlines.
Task<NewsStoryResult> GetStoryAsync(NewsHeadline headline, CancellationToken ct) The story, as in The story.
NewsFilter FilterForInstrument(string venue, string instrumentId) The filter for one instrument of one venue. An instrument with no news symbol gives an empty symbol list, which matches nothing.

The two Task methods report every expected failure as a status. Only cancellation throws.

live headlines in an add-on windowprivate readonly ChartPanelLabel _latest = new("Latest", "No headline yet");
private IDisposable? _news;

public void OnOpened()
{
    INewsHub? news = _host.News;
    if (news is null)
    {
        _latest.Text = "This installation has no news.";
        return;
    }

    // The venue key and the venue's own instrument id, the same pair History.LoadAsync takes.
    NewsFilter filter = news.FilterForInstrument(venueKey, instrumentId);
    _news = news.Subscribe(filter, headline => _latest.Text = $"{headline.TimeUtc:HH:mm} {headline.Title}");
}

public void OnClosed()
{
    _news?.Dispose();               // always unsubscribe when the window goes
    _news = null;
}
Each subscription gets its own delivery thread. Headlines for a subscription arrive in order on a pool thread of its own. A slow handler only delays its own subscription, never a venue or another subscriber. Writing to a view element from there is safe, as it is from any thread.

For past headlines, build a NewsQuery from the filter and await it:

past headlines in an add-onDateTime now = DateTime.UtcNow;
var query = new NewsQuery(now.AddDays(-1), now, 100, filter.Sources, filter.Providers, filter.Symbols);

NewsHeadlinesResult result = await news.GetHeadlinesAsync(query, ct);
foreach (NewsSourceOutcome source in result.Sources)
    if (source.Status != NewsStatus.Ok)
        _host.Log.Warning($"News from {source.SourceKey}: {source.Status}");

_count.Text = result.Items.Count.ToString();

Other plugin code reaches the same hub through NewsAmbient.Hub, which is never null. With no news installed it is NullNewsHub.Instance: no sources, and every call answered at once with nothing.

Full type reference

Every type and member, plus how a venue plugin supplies news: Reference: News.