Table of Contents

Options

Note

Requires ATAS 8.0.15 or later: ATAS 7.0.9 gives indicators no access to options data.

An indicator on the chart of a futures contract or a stock can read the options whose underlying is that instrument: the expirations, the chain of strikes and the live quotes of the options it chooses. The open interest by strike, the put/call ratio or an implied volatility smile are built from these data.

Where the data comes from

OptionsDataProvider is an IOptionsDataProvider. The options come from a connection that supports options data: the connection of the portfolio selected on the chart when it can, otherwise any connected one that knows the chart's instrument. In the current platform these are Rithmic (with market data) and Interactive Brokers.

IsAvailable tells whether options data can be had now:

// False without an options-capable connection for this instrument or on a tariff without options data
if (OptionsDataProvider is not { IsAvailable: true } provider)
{
    _text = "Options data is not available for this instrument.";
    return;
}

It is false when no such connection is connected or knows the instrument, and when the user's tariff does not give options data to third-party indicators. The requests then return empty collections and SubscribeToOption throws InvalidOperationException. OptionsDataProvider itself is null before the indicator is attached and in hosts without options.

Note

Options data is live only: there is no options history for past bars. An indicator collects it from the moment it subscribes.

The chain

  1. GetOptionSeriesAsync returns the series, one per expiration: OptionSeries with the Code, the Exchange, the UnderlyingCode, the Expiration and the Type (Regular, Weekly or EndOfMonth).
  2. GetOptionsAsync(series) returns the chain of a series: one Security per strike and right, with StrikePrice, OptionType (Call or Put) and Expiration.
private async Task LoadChainAsync(IOptionsDataProvider provider, CancellationToken token)
{
    try
    {
        // The expirations whose underlying is the chart's instrument
        var series = await provider.GetOptionSeriesAsync(token);
        var next = series
            .Where(s => s.Expiration >= DateTime.UtcNow.Date)
            .OrderBy(s => s.Expiration)
            .Select(s => (OptionSeries?)s)
            .FirstOrDefault();

        if (next is not { } nearest)
        {
            _text = "No option series.";
            return;
        }

        // One security per strike and right (call or put)
        var chain = await provider.GetOptionsAsync(nearest, token);
        var price = _lastPrice;

        var strikes = chain
            .Where(o => o.StrikePrice is not null)
            .Select(o => o.StrikePrice!.Value)
            .Distinct()
            .OrderBy(s => Math.Abs(s - price))
            .Take(_strikes)
            .ToHashSet();

        lock (_sync)
        {
            if (token.IsCancellationRequested)
                return;

            // The number of subscriptions per indicator is limited: subscribe only to what you analyze
            foreach (var option in chain.Where(o => o.StrikePrice is { } s && strikes.Contains(s)))
            {
                var subscription = provider.SubscribeToOption(option);
                subscription.Changed += OnQuoteChanged;
                _subscriptions.Add(subscription);
            }
        }

        _text = $"{nearest}: waiting for quotes...";
    }
    catch (OperationCanceledException)
    {
    }
    catch (InvalidOperationException e)
    {
        this.LogWarn("Options subscription refused: {0}", e.Message);
        _text = e.Message;
    }
}

Both requests go to the connection and may take seconds: call them from a task, never from OnCalculate, and pass a cancellation token that OnDispose cancels.

Quotes

SubscribeToOption(option) returns an IOptionQuoteSubscription. Its Summary is the latest SecuritySummary of the option: the best bid and ask with their volumes, the last trade, the open interest, the day's volume; null until the first update. Changed is raised on a data-processing thread after every update:

private void OnQuoteChanged(IOptionQuoteSubscription changed)
{
    // A data-processing thread: sum the latest summaries and publish a text for OnRender
    decimal callsOi = 0, putsOi = 0;
    IOptionQuoteSubscription? atmCall = null;
    var price = _lastPrice;

    lock (_sync)
    {
        foreach (var subscription in _subscriptions)
        {
            var option = subscription.Option;
            var oi = subscription.Summary?.OpenInterest ?? 0;

            if (option.OptionType == OptionTypes.Call)
            {
                callsOi += oi;

                if (atmCall is null || Math.Abs(option.StrikePrice!.Value - price) < Math.Abs(atmCall.Option.StrikePrice!.Value - price))
                    atmCall = subscription;
            }
            else
                putsOi += oi;
        }
    }

    _text = $"OI calls {callsOi:N0}, puts {putsOi:N0}" + FormatGreeks(atmCall, price);
    RedrawChart();
}
  • An indicator can hold up to 512 option subscriptions at a time, about 256 strikes of calls and puts; subscribe to the strikes you analyze, not to the whole chain.
  • Dispose() of a subscription releases it. When the indicator is disposed, the platform releases the subscriptions it still holds.

Greeks

The connections deliver prices, not greeks. Compute them with BlackScholesPricer, the Black-Scholes-Merton model the platform uses for its own options board: ImpliedVolatility from the option price, then CalculateGreeks (price, delta, gamma, vega and theta per day). GetTimeToExpiration turns the expiration into a fraction of a year and treats a date without a time as the end of that trading day. For options on futures pass the same value as rate and dividendYield (zero cost of carry, the Black-76 model).

private static string FormatGreeks(IOptionQuoteSubscription? call, decimal spot)
{
    // The feed gives prices, not greeks: derive the implied volatility from the option price, then the greeks
    if (call?.Summary is not { } summary || summary.BestBidPrice is not { } bid || summary.BestAskPrice is not { } ask || spot <= 0)
        return "";

    var option = call.Option;
    var strike = (double)option.StrikePrice!.Value;
    var time = BlackScholesPricer.GetTimeToExpiration(option.Expiration, DateTime.UtcNow);
    var mid = (double)(bid + ask) / 2;

    if (BlackScholesPricer.ImpliedVolatility(OptionTypes.Call, mid, (double)spot, strike, time) is not { } iv)
        return "";

    var greeks = BlackScholesPricer.CalculateGreeks(OptionTypes.Call, (double)spot, strike, time, iv);

    return $"; ATM call {strike}: IV {iv:P1}, delta {greeks.Delta:F2}, gamma {greeks.Gamma:F4}";
}

Example

The open interest of the calls and the puts of the nearest expiration on the strikes around the current price, and the implied volatility and the greeks of the call nearest to the money, written in the corner of the chart.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Drawing;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

using ATAS.DataFeedsCore;
using ATAS.DataFeedsCore.Pricing;
using ATAS.Indicators;
using OFT.Rendering.Context;
using OFT.Rendering.Tools;
using Utils.Common.Logging;

namespace ATAS.Docs.Samples.ChartIndicators.MarketData;

[DisplayName("Options open interest")]
public class OptionsOpenInterestIndicator : Indicator
{
    private readonly RenderFont _font = new("Arial", 11);
    private readonly CancellationTokenSource _cancellation = new();
    private readonly List<IOptionQuoteSubscription> _subscriptions = new();
    private readonly object _sync = new();

    private int _strikes = 10;
    private decimal _lastPrice;
    private volatile string _text = "";

    [Display(Name = "Strikes around the price", GroupName = "Options")]
    [Range(1, 50)]
    public int Strikes
    {
        get => _strikes;
        set => _strikes = value;
    }

    public OptionsOpenInterestIndicator()
        : base(true)
    {
        ((ValueDataSeries)DataSeries[0]).IsHidden = true;

        EnableCustomDrawing = true;
        SubscribeToDrawingEvents(DrawingLayouts.Final);
    }

    protected override void OnInitialize()
    {
        _lastPrice = CurrentBar > 0 ? GetCandle(CurrentBar - 1).Close : 0;

        // False without an options-capable connection for this instrument or on a tariff without options data
        if (OptionsDataProvider is not { IsAvailable: true } provider)
        {
            _text = "Options data is not available for this instrument.";
            return;
        }

        _ = LoadChainAsync(provider, _cancellation.Token);
    }

    protected override void OnCalculate(int bar, decimal value)
    {
        if (bar == CurrentBar - 1)
            _lastPrice = GetCandle(bar).Close;
    }

    private async Task LoadChainAsync(IOptionsDataProvider provider, CancellationToken token)
    {
        try
        {
            // The expirations whose underlying is the chart's instrument
            var series = await provider.GetOptionSeriesAsync(token);
            var next = series
                .Where(s => s.Expiration >= DateTime.UtcNow.Date)
                .OrderBy(s => s.Expiration)
                .Select(s => (OptionSeries?)s)
                .FirstOrDefault();

            if (next is not { } nearest)
            {
                _text = "No option series.";
                return;
            }

            // One security per strike and right (call or put)
            var chain = await provider.GetOptionsAsync(nearest, token);
            var price = _lastPrice;

            var strikes = chain
                .Where(o => o.StrikePrice is not null)
                .Select(o => o.StrikePrice!.Value)
                .Distinct()
                .OrderBy(s => Math.Abs(s - price))
                .Take(_strikes)
                .ToHashSet();

            lock (_sync)
            {
                if (token.IsCancellationRequested)
                    return;

                // The number of subscriptions per indicator is limited: subscribe only to what you analyze
                foreach (var option in chain.Where(o => o.StrikePrice is { } s && strikes.Contains(s)))
                {
                    var subscription = provider.SubscribeToOption(option);
                    subscription.Changed += OnQuoteChanged;
                    _subscriptions.Add(subscription);
                }
            }

            _text = $"{nearest}: waiting for quotes...";
        }
        catch (OperationCanceledException)
        {
        }
        catch (InvalidOperationException e)
        {
            this.LogWarn("Options subscription refused: {0}", e.Message);
            _text = e.Message;
        }
    }

    private void OnQuoteChanged(IOptionQuoteSubscription changed)
    {
        // A data-processing thread: sum the latest summaries and publish a text for OnRender
        decimal callsOi = 0, putsOi = 0;
        IOptionQuoteSubscription? atmCall = null;
        var price = _lastPrice;

        lock (_sync)
        {
            foreach (var subscription in _subscriptions)
            {
                var option = subscription.Option;
                var oi = subscription.Summary?.OpenInterest ?? 0;

                if (option.OptionType == OptionTypes.Call)
                {
                    callsOi += oi;

                    if (atmCall is null || Math.Abs(option.StrikePrice!.Value - price) < Math.Abs(atmCall.Option.StrikePrice!.Value - price))
                        atmCall = subscription;
                }
                else
                    putsOi += oi;
            }
        }

        _text = $"OI calls {callsOi:N0}, puts {putsOi:N0}" + FormatGreeks(atmCall, price);
        RedrawChart();
    }

    private static string FormatGreeks(IOptionQuoteSubscription? call, decimal spot)
    {
        // The feed gives prices, not greeks: derive the implied volatility from the option price, then the greeks
        if (call?.Summary is not { } summary || summary.BestBidPrice is not { } bid || summary.BestAskPrice is not { } ask || spot <= 0)
            return "";

        var option = call.Option;
        var strike = (double)option.StrikePrice!.Value;
        var time = BlackScholesPricer.GetTimeToExpiration(option.Expiration, DateTime.UtcNow);
        var mid = (double)(bid + ask) / 2;

        if (BlackScholesPricer.ImpliedVolatility(OptionTypes.Call, mid, (double)spot, strike, time) is not { } iv)
            return "";

        var greeks = BlackScholesPricer.CalculateGreeks(OptionTypes.Call, (double)spot, strike, time, iv);

        return $"; ATM call {strike}: IV {iv:P1}, delta {greeks.Delta:F2}, gamma {greeks.Gamma:F4}";
    }

    protected override void OnRender(RenderContext context, DrawingLayouts layout)
    {
        var text = _text;

        if (text.Length != 0)
            context.DrawString(text, _font, Color.Gray, ChartArea.X + 8, ChartArea.Y + 8);
    }

    protected override void OnDispose()
    {
        _cancellation.Cancel();

        lock (_sync)
        {
            foreach (var subscription in _subscriptions)
            {
                subscription.Changed -= OnQuoteChanged;
                subscription.Dispose();
            }

            _subscriptions.Clear();
        }
    }
}
  • Tariffs: what an indicator sees when options data is not in the tariff.
  • Lifecycle and threads: starting tasks in OnInitialize and stopping them in OnDispose.