Loading...
Searching...
No Matches
Connectors

Overview

A connector connects ATAS to a source of market data (a built-in connector, also to a broker). The built-in connectors (Rithmic, CQG, dxFeed, the crypto exchanges and others) are written with the same SDK that is available to you: you can write a connector to your own data feed — a vendor API, a recorded data set, a simulator — and use it in ATAS like any built-in connection. Charts, the SmartDOM, the heatmap, indicators and strategies receive its data exactly as from any other connection.

This article describes a market data connector. Trading through third-party connectors is not supported: the base class also carries the trading API (portfolios, positions, orders) of the built-in connectors, but a connector of your own must be a market data feed only.

Connectors work the same way in the classic ATAS and in ATAS X (see ATAS X and macOS).

What you build

A class library (net10.0) that references, from the platform folder and with <Private>false</Private>:

  • ATAS.DataFeedsCore.dll — the connector SDK (namespace ATAS.DataFeedsCore; the settings base class is in OFT.Core);
  • Utils.Common.dll — the compiler asks for it because the SDK base classes use it. Depending on the SDK members you use, it may also ask for OFT.Attributes.dll and OFT.Localization.dll; reference them the same way.

The library contains two public classes:

Class Base class Role
connection settings BaseConnectorSettings<TConnector, TSelf> the connector's name in the «Add connection» dialog, the properties the user edits, saved with the connection
connector BaseConnector<TMessage, TPortfolioKey, TSecurityKey> connects, finds instruments, publishes trades, best prices and the order book

The platform finds the settings class (a public non-abstract class with a public parameterless constructor), creates the connector through it and drives the connector through its base class.

The settings class

Member Mandatory Meaning
[DisplayName] yes the connector name in the dialog. It must be unique — see Name rules
[Description], [Category] no the text in the dialog and the dialog group. The category does not affect the connection limits of the tariff (see Tariff restriction)
Features yes ConnectorFeatures.MarketData. Trading through third-party connectors is not supported, so do not add Executions, StopOrders or OcoOrders
OnApplySettings(connector) yes copies the settings to the connector. Called after the connector is created and after every change of the settings
CompareCredentials(other) yes true if both connections use the same account: the platform does not connect two such connections at once. Return false when there are no credentials
public properties no shown in the connection settings (use [Display], [Range], [Required] from System.ComponentModel.DataAnnotations) and saved with the connection. Use SecureString for passwords
CheckSupported(out message) no return false with a message if the connector cannot run on this machine (for example, a Windows-only vendor library on macOS)

Login/password connectors implement ILoginPasswordConnectorSettings; the platform then shows the standard login fields.

The settings are saved as JSON with the type name of your class. Keep the saved form compatible between versions of your DLL: do not rename the class or its namespace, and do not change the type of a saved property. A saved connection that cannot be restored — the DLL is missing, the constructor or a property setter throws, a saved value no longer fits the property — is not deleted: it is kept unchanged in the connections list, marked as unavailable and is never connected, so restoring the DLL brings it back.

The connector class

The type parameters of BaseConnector<TMessage, TPortfolioKey, TSecurityKey> are your queue message type (see Threading) and the keys you use for accounts and instruments (usually string).

Member Mandatory Meaning
IsSupportedAmericanFutures, IsSupportedAmericanStocks, IsSupportedRussianMarket, IsSupportedCrypto, IsSupportedCfd yes (the first three are abstract) the instrument kinds your feed serves: the platform asks your connector only for these
IsSupportedTradingFunctions, IsSupportedStopOrders, IsSupportedServerOCO yes false for a data feed
OnConnect() yes opens the connection; call OnConnected() when it is established (by default within 60 seconds, the ConnectTimeout property; otherwise the attempt is aborted). An exception fails the attempt: the connection shows the error and stays disconnected — the platform does not retry a first connect by itself (see «Connection loss» below)
OnDisconnect() yes closes the connection; call OnDisconnected()
OnSearchSecurities(filter) yes finds the instrument the user opened, see Instruments
OnSubscribeMarketData(security, types), OnUnsubscribeMarketData(security, types) yes start and stop the data of an instrument. The base class counts subscribers: you are called once per change of the combined subscription
OnProcess(message) yes handles the messages you put into the connector queue
OnSearchSecurities(securityKey), OnRegisterOrder, OnCancelOrder yes (abstract) may be empty / report an error in a data feed
OnSendHeartbeat(), HeartbeatTimeout no periodic check of a live connection

You publish data with the protected methods of the base class:

Method Data
ProcessSecurity(key, securityId, message, update) creates or updates the Security of an instrument
ProcessTick(security, id, time, direction, price, volume) a trade
ProcessBestBidAsk(security, time, MarketDataType.Bid/Ask, price, volume) the best bid or ask
ProcessMarketDepths(security, levels) changed order book levels (MarketDepth: side, price, volume; volume 0 removes the level)
ProcessSummary(key, update) the daily summary (open, high, low, volume)

The Process… methods drop data of instruments that nobody subscribed to, so you do not need to track the subscriptions yourself. Times are UTC.

Connection loss. When your source drops a connection that was established, call OnConnectionError(exception) with the reason and then OnDisconnected(). The platform then reconnects by itself: it calls OnConnect again and, after OnConnected, searches the instruments that are still open again (OnSearchSecurities) and subscribes them again (OnSubscribeMarketData). Report a refusal that a retry cannot fix (for example, rejected credentials) as a ConnectionRefusedException (namespace ATAS.DataFeedsCore.Exceptions): the platform then stops reconnecting and shows the reason. The same two calls during OnConnect, before OnConnected, fail the connect attempt like an exception.

Threading

  • OnConnect and OnDisconnect run on a thread-pool thread. OnSearchSecurities and the subscription methods run on the caller's thread, which may be the UI thread: do not block in them.
  • Every connector has its own queue thread. Enqueue(message) hands a message to OnProcess on that thread, Enqueue(action) runs an action there. Call ProcessSecurity, ProcessTick, ProcessBestBidAsk and ProcessMarketDepths from the queue thread: the state of an instrument is not synchronized otherwise. The usual pattern is that the network callbacks of your API only enqueue, and all processing happens in the queue.
  • An exception in OnProcess or in an enqueued action is logged and the queue continues.
  • AsyncConnector<TPortfolioKey, TSecurityKey> is a variant of the base class for async code: await SwitchToConnectorThreadAsync() continues on the queue thread.

Instruments

The user opens instruments from the ATAS instrument catalogue. When a chart or a SmartDOM opens one, the platform asks every connected connector that supports the instrument kind with a SecurityFilter: Code (the contract code, for example ESZ6), Exchange, Type, ContractCode (the root, ES) and Expiration. If several connections serve the instrument, the one higher in the connections list wins.

Answer with exactly one security through RaiseSearchSecuritiesResult(filter, null, [security]), or with an error (RaiseSearchSecuritiesResult(filter, exception, [])) when you do not serve the instrument. Before that, create the security with ProcessSecurity and fill at least Code, Exchange, Type and TickSize; securityId is your unique id in the form code@exchange.

A connector cannot add instruments that are not in the ATAS catalogue.

History

The connector API covers live data only. Historical bars are not part of it: a connector cannot supply, replace or extend the history of an instrument, and which bars a chart shows before the live part does not depend on the connection that delivers the live data. Make sure the prices of your live data match the catalogue instrument, otherwise the live part will not continue the bars the chart already shows.

Example

A simulated data feed: for a few CME futures it answers the instrument search and produces a random walk of trades, best prices and a synthetic order book. It does not trade.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using OFT.Core;
namespace MyCompany.RandomWalkFeed;
[DisplayName("Random walk feed")]
[Description("Simulated market data: a random walk of trades and a synthetic order book.")]
[Category("Custom")]
public sealed class RandomWalkConnectorSettings : BaseConnectorSettings<RandomWalkConnector, RandomWalkConnectorSettings>
{
[Display(Name = "Trades per second", GroupName = "Simulation", Order = 10)]
[Range(1, 50)]
public int TradesPerSecond { get; set; } = 5;
[Display(Name = "Order book levels per side", GroupName = "Simulation", Order = 20)]
[Range(1, 50)]
public int BookLevels { get; set; } = 10;
// Market data only: the platform offers no trading through this connection.
public override ConnectorFeatures Features => ConnectorFeatures.MarketData;
// Copies the settings into the connector; called after creation and after every change.
protected override void OnApplySettings(RandomWalkConnector connector)
{
connector.TradesPerSecond = TradesPerSecond;
connector.BookLevels = BookLevels;
}
// No credentials: two connections of this type never use the same account.
protected override bool CompareCredentials(RandomWalkConnectorSettings other) => false;
}
public sealed class RandomWalkConnector : BaseConnector<object, string, string>
{
private sealed class Simulation
{
public required Security Security { get; init; }
public decimal Price { get; set; }
public Dictionary<decimal, MarketDataType> Book { get; } = new();
}
// Contract root -> tick size and start price.
private static readonly Dictionary<string, (decimal TickSize, decimal StartPrice)> _contracts = new()
{
["ES"] = (0.25m, 5000m),
["NQ"] = (0.25m, 18000m),
["CL"] = (0.01m, 70m),
};
private readonly Dictionary<string, Simulation> _simulations = new();
private readonly Random _random = new();
private Timer? _timer;
private long _tradeId;
public int TradesPerSecond { get; set; } = 5;
public int BookLevels { get; set; } = 10;
// Instrument kinds this connector serves: the platform asks it only for these.
public override bool IsSupportedAmericanFutures => true;
public override bool IsSupportedAmericanStocks => false;
public override bool IsSupportedRussianMarket => false;
public override bool IsSupportedTradingFunctions => false;
public override bool IsSupportedStopOrders => false;
public override bool IsSupportedServerOCO => false;
// Called on a thread-pool thread. Open the transport here, then report success.
protected override void OnConnect()
{
var period = TimeSpan.FromMilliseconds(1000.0 / Math.Max(1, TradesPerSecond));
_timer = new Timer(_ => Enqueue(Step), null, period, period);
OnConnected();
}
protected override void OnDisconnect()
{
_timer?.Dispose();
_timer = null;
OnDisconnected();
}
// Messages passed to Enqueue(TMessage); this connector enqueues only actions.
protected override void OnProcess(object message)
{
}
// The platform asks every connected connector for the instrument the user opened. Answer with exactly one
// security, or with an error.
protected override void OnSearchSecurities(SecurityFilter filter)
{
Enqueue(() =>
{
if (!_contracts.TryGetValue(filter.ContractCode ?? string.Empty, out var contract))
{
RaiseSearchSecuritiesResult(filter, new InvalidOperationException($"{filter.Code} is not simulated."), []);
return;
}
var securityId = $"{filter.Code}@{filter.Exchange}";
Security? found = null;
ProcessSecurity(securityId, securityId, filter, (f, isNew, security) =>
{
security.Code = f.Code;
security.Exchange = f.Exchange;
security.Instrument = f.ContractCode;
security.Type = f.Type;
security.TickSize = contract.TickSize;
found = security;
});
if (!_simulations.ContainsKey(securityId))
_simulations[securityId] = new Simulation { Security = found!, Price = contract.StartPrice };
RaiseSearchSecuritiesResult(filter, null, [found!]);
});
}
// Searches by the connector's own security key; not needed by this connector.
protected override void OnSearchSecurities(string securityId)
{
}
// The base class counts the subscriptions and drops data nobody subscribed to, so the simulation just runs for
// every known security. A real connector sends its subscribe request here.
protected override void OnSubscribeMarketData(Security security, SubscriptionType subscriptionTypes)
{
}
protected override void OnUnsubscribeMarketData(Security security, SubscriptionType subscriptionTypes)
{
}
// Trading is not supported (Features has no Executions); report an error if an order arrives anyway.
protected override void OnRegisterOrder(Order order)
=> ProcessOrderError(order, "Random walk feed does not support trading.");
protected override void OnCancelOrder(Order order, long extId)
=> ProcessOrderError(order, "Random walk feed does not support trading.");
// One simulation step for every security, on the connector queue thread.
private void Step()
{
var time = DateTime.UtcNow;
foreach (var simulation in _simulations.Values)
{
var security = simulation.Security;
var tick = security.TickSize;
var move = _random.Next(-2, 3);
simulation.Price = Math.Max(tick, simulation.Price + move * tick);
var direction = move >= 0 ? TradeDirection.Buy : TradeDirection.Sell;
ProcessTick(security, ++_tradeId, time, direction, simulation.Price, _random.Next(1, 20));
ProcessBestBidAsk(security, time, MarketDataType.Bid, simulation.Price - tick, _random.Next(1, 100));
ProcessBestBidAsk(security, time, MarketDataType.Ask, simulation.Price + tick, _random.Next(1, 100));
ProcessMarketDepths(security, BuildBook(simulation, time));
}
}
// The changed order book levels: zero volume for every level that left the book (or changed its side), then the
// new levels.
private List<MarketDepth> BuildBook(Simulation simulation, DateTime time)
{
var security = simulation.Security;
var tick = security.TickSize;
var book = new Dictionary<decimal, MarketDataType>();
for (var i = 1; i <= BookLevels; i++)
{
book[simulation.Price - i * tick] = MarketDataType.Bid;
book[simulation.Price + i * tick] = MarketDataType.Ask;
}
var levels = simulation.Book
.Where(old => !book.TryGetValue(old.Key, out var side) || side != old.Value)
.Select(old => Level(security, old.Value, old.Key, 0, time))
.ToList();
levels.AddRange(book.Select(level => Level(security, level.Value, level.Key, _random.Next(1, 200), time)));
simulation.Book.Clear();
foreach (var level in book)
simulation.Book[level.Key] = level.Value;
return levels;
}
private static MarketDepth Level(Security security, MarketDataType side, decimal price, decimal volume, DateTime time)
=> new() { Security = security, Type = side, Price = price, Volume = volume, Time = time };
}
Definition BaseConnector.cs:48
Represents a market depth entry.
Definition MarketDepth.cs:9
Represents an order for trading on a financial exchange.
Definition Order.cs:13
Definition SecurityFilter.cs:9
string ContractCode
Definition SecurityFilter.cs:24
Represents a security entity used in the application.
Definition Security.cs:16
Definition IConnectorSettings.cs:452
Definition AccountKind.cs:1
MarketDataType
Specifies the type of market data.
Definition DataFeedsCore/MarketDataType.cs:7
TradeDirection
Represents the trade direction.
Definition DataFeedsCore/TradeDirection.cs:7
SubscriptionType
Definition SubscriptionType.cs:7
@ Volume
Represents a data series containing volume data.
init
Definition IATMStrategy.cs:18
Definition ConnectorSettingsTabs.cs:2
ConnectorFeatures
Definition IConnectorSettings.cs:32

Installing the connector

Put the DLL into the custom connectors folder of the platform settings: AppData%\ATAS\CustomConnectors for the classic ATAS, AppData%\ATAS X\CustomConnectors for ATAS X (create the folder if it does not exist). It is not the Connectors folder next to it: that one holds the data of the connections.

  • Every DLL of the folder is loaded, whatever its name. Put the libraries your connector depends on next to it; they are found by their assembly name. A library the platform already has (for example Newtonsoft.Json) is not loaded again — the platform's copy is used. Native libraries of a vendor API can be placed in the same folder.
  • The folder is read at the start of the platform. After you replace the DLL, restart the platform; the DLL is locked while the platform runs.
  • The connector appears in the «Add connection» dialog. The platform never creates or connects a custom connection on its own: the user adds it, and «connect automatically» works as for any connection.
  • To debug the connector, attach the debugger to the running platform; keep the .pdb next to the DLL.
  • Problems found while loading (an unreadable DLL, a name conflict, a missing parameterless constructor) are written to the platform log, and the connector is not listed.
  • An exception thrown by your settings class while a saved connection is restored, by the connector constructor or by OnApplySettings is written to the log and shown on that connection; the platform and the other connections keep working. Changed settings whose OnApplySettings throws are not saved.

Keep the start fast

Your code runs inside the platform process, and part of it runs while the platform starts: the DLL is loaded (including any module initializer), saved connections are restored (settings constructors and property setters), and their connectors are created and configured (connector constructor, OnApplySettings, IConnectorSettingsSupportInitialization.Initialize). The platform cannot interrupt code that hangs there, and a hang there stops the platform start. So:

  • keep constructors, property setters and OnApplySettings fast and free of I/O — never open a network connection or wait for anything there;
  • do the slow work in OnConnect (a thread-pool thread, bounded by the connection timeout) or in the connector queue.

Name rules

The [DisplayName] of the settings class identifies the connector: the connections are saved with it, and the data folder of the connector is named after it. A custom connector whose name equals the name of a built-in connector (ignoring case), or whose settings class has the full name of a built-in one, is not loaded — built-in connectors always win. Between two custom connectors with the same name, the one from the DLL that comes first in file name order is loaded. Choose a name that includes your product or company name.

ATAS X and macOS

  • Build for net10.0, not net10.0-windows, and do not use WPF or Windows Forms: the connection settings are edited in the standard property grid of both platforms, so a connector does not need them. ATAS X does not load a DLL that references WPF or Windows Forms (the reason is written to the log).
  • A vendor API that exists only for Windows: return false from CheckSupported on other systems, so the connection shows the reason instead of failing on connect.
  • Native libraries must be built for the system and processor of the platform (for example arm64 on Apple silicon Macs).

Tariff restriction

Custom connectors can be restricted by the tariff (see «Developer API and tariffs»). In that case your connector is still listed in the «Add connection» dialog, but choosing it shows the tariff banner; connections added earlier stay in the connections list, show «Custom connectors are not available on your current plan» and are not connected — automatically or manually. The tariff is checked every time a connection is added or connected, so the connections work again as soon as the restriction is lifted (for example, after the user logs on with another account). The other connection limits of the tariff (adding connections, the number of active connections) apply to custom connectors as well. A custom connection always counts as a crypto connection if its connector reports IsSupportedCrypto, and as a classic (US and EU markets) connection otherwise — whatever [Category] the settings class declares.

Related articles

  • Developer API and tariffs — which API parts the tariff can restrict and what your code observes.
  • Adding logging — writing to the platform log (this.LogInfo(...) works in a connector as well).
  • Debug mode — attaching the debugger to the platform.