Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Page 1 of 9
1. Key Concepts
The LMAX .NET API is a thin .NET layer over our existing XML over HTTPS (REST) protocol. The goal of the API is to provide a simplified programming model for .NET-based clients connecting to the LMAX Trader platform. The design of the API contains 5 key concepts: Requests, Callbacks, Events, EventListeners, and the Session. Requests and Callbacks are used when making requests to LMAX Trader, e.g. placing orders or subscribing to order book events. EventListeners are used to propagate Events that are received asynchronously from LMAX Trader, e.g. market data or execution reports. The final concept is the Session which is the core interface used to send requests to LMAX Trader or register EventListeners.
Once the LmaxApi instance has been created, the next thing to do is construct a LoginRequest. LoginRequest requires a username and password as part of the constructor. There is an optional third parameter called ProductType which, if left out, defaults to ProductType.CFD_LIVE for connecting to production, but may be set to ProductType.CFD_DEMO to connect to the testapi system.
public static void Main(string[] args) { // ... lines omitted. LoginRequest loginRequest = new LoginRequest("myusername", "mypassword", ProductType.CFD_DEMO); lmaxApi.Login(loginRequest, LoginCallback, FailureCallback("log in")); }
The final step of the login process is to create a LoginCallback. The OnLogin(ISession session) Callback delegate should be considered the "entry point" of the client application. It is from within this Callback where the application should setup listeners and create subscriptions to the information that it is interested in. It is the point where the Session is provided to the application. A reference to the session should be captured, and held onto for later use. A common pattern is to use a delegate from inside the main application instance.
class MyTradingBot { private ISession _session; private void LoginCallback(ISession session) { Console.WriteLine("Logged in, account ID: " + session.AccountDetails.AccountId); _session = session; // Register EventListeners, subscribe to data and start the session. // More detail on this later. } private static OnFailure FailureCallback(string failedFunction) { return failureResponse => Console.Error.WriteLine("Failed to " + failedFunction + " due to: " + failureResponse.Message); }
file:///C:/Users/msischka.GAZPROMUK/AppData/Local/Temp/wz93be/LmaxNetCli...
19/09/2013
Page 2 of 9
public static void Main(string[] args) { MyTradingBot myTradingBot = new MyTradingBot(); LmaxApi lmaxApi = new LmaxApi("https://testapi.lmaxtrader.com"); lmaxApi.Login(new LoginRequest("myusername", "mypassword", ProductType.CFD_DEMO), myTradingBot.LoginCallback, FailureCallback("log in")); } }
Success callbacks such as the LoginCallback above may accept different arguments depending upon the request made. However, failure callbacks will always contain a FailureResponse object. This is covered in more detail in the section on Handling Errors on Requests.
5. Placing Orders
The LMAX Trader platform supports a number of different order types and we are continually adding more. This section will just cover the basics of Market, Limit and Stop orders. Placing an order is very simple. It is just another case of constructing a request and waiting for a Callback. However, obtaining information as to what happened as a result of placing the order is not as simple. When an order is placed, the web server only parses the request, does some basic validation before sending it asynchronously to the broker. Obtaining the results of the order placement involves subscribing to another type of data, in this case execution reports. The request objects for Market, Limit and Stop orders are called MarketOrderSpecification, LimitOrderSpecification and StopOrderSpecification. To differentiate between buy orders and sell orders the LMAX Trader uses signed quantities. To place a buy use a positive quantity to place a sell use a negative one. When placing an order, an instructionId must be provided. This should be retained so that it can be used to cancel or amend the order later.
class MyTradingBot { private readonly List<string> _newOrders = new List<string>(); private readonly List<string> _pendingOrders = new List<string>(); private readonly List<Order.Order> _placedOrders = new List<Order.Order>(); //Sample order specifications for market, limit and stop buy-orders, with //hard-coded instruction ids private readonly MarketOrderSpecification _marketOrderSpecification = new MarketOrderSpecification("1", 4001, +2m, TimeInForce.ImmediateOrCancel); private readonly LimitOrderSpecification _limitOrderSpecification = new LimitOrderSpecification("2", 4002, 1.62m, +2m, TimeInForce.GoodForDay); private readonly StopOrderSpecification _stopOrderSpecification = new StopOrderSpecification("3", 4003, 1.62m, +2m, TimeInForce.GoodForDay); private ISession _session; public void MarketDataUpdate(OrderBookEvent orderBookEvent) { if (shouldTradeGivenCurrentMarketData(orderBookEvent)) { PlaceMarketOrder(orderBookEvent.InstrumentId, generateNextInstructionId()); decimal sellPrice = calculateSellPrice(orderBookEvent); PlaceSellLimitOrder(orderBookEvent.InstrumentId, sellPrice, generateNextInstructionId()); } } public void ExecutionEventListener(Execution execution) { if (_newOrders.Remove(execution.Order.InstructionId)) {
file:///C:/Users/msischka.GAZPROMUK/AppData/Local/Temp/wz93be/LmaxNetCli...
19/09/2013
Page 3 of 9
_placedOrders.Add(execution.Order); } } public void PlaceMarketOrder(long instrumentId, string instructionId) { // Place a market order to buy - note that we can re-use an // order specification to place multiple orders but the instructionId // must be reset each time a new order is placed _marketOrderSpecification.InstrumentId = instrumentId; _marketOrderSpecification.InstructionId = instructionId; _newOrders.Add(instructionId); } public void PlaceSellLimitOrder(long instrumentId, decimal sellPrice, string instructionId) { // Place a limit order to sell. _limitOrderSpecification.InstrumentId = instrumentId; _limitOrderSpecification.Price = sellPrice; _limitOrderSpecification.Quantity = -2.0m;// Negative to indicate sell _limitOrderSpecification.InstructionId = instructionId; _newOrders.Add(instructionId); _session.PlaceLimitOrder(_limitOrderSpecification, PlaceOrderSuccess, OrderPlacementFailureCallback(instructionId, "place limit order")); } public void PlaceStopOrder(long instrumentId, decimal buyStopPrice, string instructionId) { // Place a limit order to sell. _stopOrderSpecification.InstrumentId = instrumentId; _stopOrderSpecification.StopPrice = buyStopPrice; _limitOrderSpecification.Quantity = 2.0m; // Positive to indicate buy _limitOrderSpecification.InstructionId = instructionId; _newOrders.Add(instructionId); _session.PlaceStopOrder(_stopOrderSpecification, PlaceOrderSuccess, OrderPlacementFailureCallback(instructionId, "place stop order")); } private void PlaceOrderSuccess(string placeOrderInstructionId) { // note - this will be the same instructionId as the one passed to the API, // it confirms this success is related to that specific place order request //move from "new" to "pending" to show the order was successfully placed _newOrders.Remove(placeOrderInstructionId); _pendingOrders.Add(placeOrderInstructionId); } private OnFailure OrderPlacementFailureCallback(string instructionId, string failedFunction) { return failureResponse => { _newOrders.Remove(instructionId); Console.Error.WriteLine("Failed to " + failedFunction + " due to: " + failureResponse.Message); }; } public void LoginCallback(ISession session) { // ... lines omitted. session.OrderExecuted += ExecutionEventListener; session.Subscribe(new ExecutionSubscriptionRequest(), () => Console.WriteLine("Successful subscription"), failureResponse => Console.Error.WriteLine("Failed to subscribe: {0}", failureResponse)); } }
6. Cancelling Orders
Cancelling orders is very similar to placing orders, and shares the same Callback interface. A CancelOrderRequest requires the instrumentId and the instructionId ("originalInstructionId") of the original order that is now being cancelled. On a successful placement of CancelOrderRequest, the instructionId of the CancelOrderRequest is returned.
class MyTradingBot { private Session _session; private readonly long _instrumentId; // ..... private readonly List<string> _pendingOrders = new List<string>(); private readonly List<string> _workingOrders = new List<string>(); public void CancelAllOrders() { CancelOrders(_pendingOrders); CancelOrders(_workingOrders); } public void CancelOrders(List<string> instructionIds) { foreach (string originalInstructionId in instructionIds) { string cancelRequestInstructionId = generateNextInstructionId(); _session.CancelOrder(new CancelOrderRequest(_instrumentId, originalInstructionId, cancelRequestInstructionId), instructionId => Console.WriteLine("Cancel order instruction placed: %s", instructionId), failureResponse => Console.WriteLine("Failed to cancel order: %s%n", failureResponse)); } }
file:///C:/Users/msischka.GAZPROMUK/AppData/Local/Temp/wz93be/LmaxNetCli...
19/09/2013
Page 4 of 9
7. Handling Rejects
Because the LMAX Trader platform is asynchronous it is not possible for all classes of order failures to be handled in the Failure Callback. A Request may be syntactically valid, but could be rejected at some point later by the Broker or the MTF. Possible reasons for rejections could include: EXPOSURE_CHECK_FAILURE or INSUFFICIENT_LIQUIDITY (full set available in instructionRejected-event.rng). These events are returned in the same way that executions are returned, via the event stream. So to be notified of rejection events it is necessary to register an InstructionRejected listener with the session. There is no specific subscription for instruction rejects, using ExecutionSubscriptionRequest will also subscribe to instruction rejected events.
class MyTradingBot { private void FailOnInstructionRejected(InstructionRejectedEvent instructionRejected) { Console.WriteLine("Rejection received: {0}", instructionRejected); } public void LoginCallback(ISession session) { // ... lines omitted. session.InstructionRejected += FailOnInstructionRejected; //should be subscribe to this once only, and it will fire the instruction reject and execution listeners session.Subscribe(new ExecutionSubscriptionRequest(), () => Console.WriteLine("Successful subscription"), failureResponse => Console.Error.WriteLine("Failed to subscribe: {0}", failureResponse)); } }
file:///C:/Users/msischka.GAZPROMUK/AppData/Local/Temp/wz93be/LmaxNetCli...
19/09/2013
Page 5 of 9
Stops can also be amended after the order was placed, using the AmendStopLossProfitRequest. An important point to remember is that a null will remove a previously
file:///C:/Users/msischka.GAZPROMUK/AppData/Local/Temp/wz93be/LmaxNetCli...
19/09/2013
Page 6 of 9
specified stop. So, if you only want to remove one of the stops (e.g. stop loss) you should remember to respecify the other one if required (e.g. stop profit).
class MyTradingBot { private readonly List<string> _amendInstructions = new List<string>(); private ISession _session; public void AmendStops(long instrumentId, string originalInstructionId, string instructionId, decimal stopLossOffset, decimal stopProfitOffset) { _session.AmendStops(new AmendStopLossProfitRequest(instrumentId, originalInstructionId, instructionId, stopLossOffset, stopProfitOffset), AmendSuccess, AmendFailure); } public void RemoveStopLoss(long instrumentId, string originalInstructionId, string instructionId, decimal oldStopProfitOffset) { // Use the oldStopProfitOffset to retain the stop profit for the order. _session.AmendStops(new AmendStopLossProfitRequest(instrumentId, originalInstructionId, instructionId, null, oldStopProfitOffset), AmendSuccess, AmendFailure); } private void AmendSuccess(string amendRequestInstructionId) { _amendInstructions.Add(amendRequestInstructionId); } private void AmendFailure(FailureResponse failureResponse) { Console.WriteLine("Failed to amend stop: {0}", failureResponse); } }
Amending a stop may fail - for example, if the stop has already fired.
NB for simplicity the above example does not include a market data subscription, but it is perfectly permissible for a client to subscribe for both types of events at once
file:///C:/Users/msischka.GAZPROMUK/AppData/Local/Temp/wz93be/LmaxNetCli...
19/09/2013
Page 7 of 9
private void SubscriptionCallback() { _session.RequestAccountState(new AccountStateRequest(), () => Console.WriteLine("AccountStateRequest sent"), failureResponse => Console.Error.WriteLine("AccountStateRequest failed")); } }
To find a specific instrument the "id: (instrumentId)" form can be used. To do a general search, use a term such as "CURRENCY", which will find all of the currency instruments. A search term like "UK" will find all of the instruments that have "UK" in the name.
On a successful call the results will be returned in a List<Instrument> containing the first 25 results, ordered alphabetically by name. If there are more results, the parameter hasMoreResults will be set to true. To retrieve the next 25 instruments do another search, passing the id from the last instrument as the offsetInstrumentId of this new search. For example:
class MyTradingBotAccountState { private ISession _session; public void LoginCallback(ISession session) { string query = ""; // see above for how to do a more specific search long offsetInstrumentId = 0; // see above for more details on this offset parameter _session = session; session.SearchInstruments(new SearchRequest(query, offsetInstrumentId), SearchCallback, failureResponse => Console.Error.WriteLine("Failed to subscribe: {0}", failureResponse)); _session.Start(); } private void SearchCallback(List<Instrument> instruments, bool hasMoreResults) { Console.WriteLine("Instruments Retrieved: {0}", instruments); if(hasMoreResults) { Console.WriteLine("To continue retrieving all instruments please start next search from: {0}", instruments.get(instruments.size() } } }
TopOfBookHistoricMarketDataRequest AggregateHistoricMarketDataRequest
- Best bid & ask tick data. - Aggregated price & volume data by day or minute.
The data is delivered as a gzip-compressed CSV file. The following steps are required to receive historic market data: 1. 2. 3. 4. Implement and register a historic market data delegate Subscribe to historic market data events Make historic market data requests Retrieve URLs within an authenticated session
The code snipets below are extracted from the class HistoricMarketDataRequester in the API samples. 15.1. Implement and register a historic market data delegate To receive historic market data, you must first implement a delegate matching the signature of LmaxApi.OnHistoricMarketDataEvent and register the delegate with the session:
// Implement the delegate private void OnHistoricMarketData(string instructionId, List uris) { // do something with the URIs... see section 15.4 for a sample implementation } // Register the delegate _session.HistoricMarketDataReceived += OnHistoricMarketData;
15.2. Subscribe to historic market data events Use the standard _session.Subscribe() mechanism to subscribe to historic market data requests:
_session.Subscribe(new HistoricMarketDataSubscriptionRequest(), () => Console.WriteLine("Successful subscription"), failureResponse => Console.Error.WriteLine("Failed to subscribe: {0}", failureResponse));
As with other subscriptions, you can Subscribe before you call _session.Start(). 15.3. Make historic market data requests
file:///C:/Users/msischka.GAZPROMUK/AppData/Local/Temp/wz93be/LmaxNetCli...
19/09/2013
Page 8 of 9
To request historic market data for a specific instrument and date range, create an instance of TopOfBookHistoricMarketDataRequest or AggregateHistoricMarketDataRequest and call the requestHistoricMarketData method on the session:
_session.RequestHistoricMarketData(new AggregateHistoricMarketDataRequest(instructionId, instrumentId, DateTime.Parse("2011-05-11"), DateTime.Parse("2011-06-13"), Resolution.Day, Format.Csv, Option.Bid), () => Console.WriteLine("Successful request"), failureResponse => Console.Error.WriteLine("Failed request: {0}", failureResponse));
15.4. Retrieve URLs within an authenticated session When the data is ready, the delegate you registered in step 15.1 will receive an asynchronous message with a list of URLs. The asynchronous message also includes the instructionId you included with the request. The URLs must be retrieved using an authenticated connection. You can use the session's OpenUri() method to open each URL. The files at the URLs are compressed with gzip. The code snippet below shows how to retrieve the URL and print out the uncompressed data:
private void OnHistoricMarketData(string instructionId, List uris) { foreach (var uri in uris) { _session.OpenUri(uri, OnUriResponse, FailureCallback("open uri")); } } private static void OnUriResponse(Uri uri, BinaryReader reader) { using (var stream = new GZipStream(reader.BaseStream, CompressionMode.Decompress)) { const int size = 1024; var buffer = new byte[size]; var numBytes = stream.Read(buffer, 0, size); while (numBytes > 0) { Console.Write(Encoding.UTF8.GetString(buffer, 0, numBytes)); numBytes = stream.Read(buffer, 0, size); } } }
16. Protocol Version Checking By default, the LMAX .NET API checks that the current protocol version used by the LMAX Trader Platform matches the version that the LMAX .NET API was built for. This strict checking can be disabled when constructing the LoginRequest by supplying false as the fourth parameter:
LoginRequest loginRequest = new LoginRequest("myusername", "mypassword", ProductType.CFD_DEMO, false);
Since our API is based upon our XML protocol the information that we can return on the API is restricted to what is included in the XML messages. For a similar scenario our XML protocol only emits a single order event at the end of the matching cycle, therefore the data output would be:
<order> <timeInForce>ImmediateOrCancel</timeInForce> <instructionId>1733844027851145216</instructionId> <originalInstructionId>1733844027851145216</originalInstructionId> <orderId>AAK8oAAAAAAAAAAF</orderId> <accountId>1393236922</accountId> <instrumentId>179361</instrumentId> <quantity>30</quantity> <matchedQuantity>30</matchedQuantity> <matchedCost>22.1</matchedCost> <cancelledQuantity>0</cancelledQuantity> <timestamp>2011-12-22T10:09:25</timestamp> <orderType>STOP_COMPOUND_MARKET</orderType> <openQuantity>20</openQuantity> <openCost>22.1</openCost> <cumulativeCost>22.1</cumulativeCost> <commission>0</commission> <stopReferencePrice>111</stopReferencePrice> <stopLossOffset /> <stopProfitOffset /> <executions> <executionId>3</executionId> <execution> <price>110</price> <quantity>10</quantity> </execution> <execution> <price>111</price> <quantity>10</quantity>
file:///C:/Users/msischka.GAZPROMUK/AppData/Local/Temp/wz93be/LmaxNetCli...
19/09/2013
Page 9 of 9
This means that the .NET API does not have the information about the individual filled quantities as a result of each execution, only the final state of the order. This can make it a little tricky to find which Execution event represents the end of the matching cycle. The information seen by a user of the .NET API for the same scenario would be:
Quantity = 10, Order.FilledQuantity = 30 Quantity = 10, Order.FilledQuantity = 30 Quantity = 10, Order.FilledQuantity = 30
However, it is possible to use some of the additional events that are available on the .NET API to derive the same behaviour. By listening to both Execution and Order events we can track the cumulative quantities as we go. It does require a little bit more state management by the client, but the logic is fairly straight forward:
class ExecutionTracking { private private private private private ISession _session; Dictionary<string, Order> _lastOrderStateByInstructionId = new Dictionary<string,Order>(); Order _currentOrder; decimal _currentFilledQuantity; decimal _currentCancelledQuantity;
public ExecutionTracking(ISession session) { _session = session; } private void OnOrder(Order order) { Order previousOrderState; if (_lastOrderStateByInstructionId.TryGetValue(order.InstructionId, out previousOrderState)) { // Track the current filled/cancelled quantity as a delta between this // order event and the previous one of the same order. _currentFilledQuantity = previousOrderState.FilledQuantity; _currentCancelledQuantity = previousOrderState.CancelledQuantity; } else { // The is the first order event for this order, so start from zero. _currentFilledQuantity = 0; _currentCancelledQuantity = 0; } _currentOrder = order; } private void OnExecution(Execution execution) { // As we receive the executions for the order increment the quantities for each execution _currentFilledQuantity += execution.Quantity; _currentCancelledQuantity += execution.CancelledQuantity; // Once our per execution tracking of the order matches the totals on the // order itself, we've found the last execution for a given order event. if (_currentOrder.FilledQuantity == _currentFilledQuantity && _currentOrder.CancelledQuantity == _currentCancelledQuantity) { Console.Out.WriteLine("Last Execution: " + execution + " for order: " + _currentOrder); if (isComplete(execution.Order)) { // The order has completed, all quantity is filled or cancelled, so remove the order from _lastOrderStateByInstructionId.Remove(_currentOrder.InstructionId); } else { // Track the order event for the next match _lastOrderStateByInstructionId.Add(_currentOrder.InstructionId, _currentOrder); } _currentOrder = null; } } private Boolean isComplete(Order order) { decimal completedQuantity = order.FilledQuantity + order.CancelledQuantity; return order.Quantity == completedQuantity; } public void Subscribe() { _session.OrderChanged += OnOrder; _session.OrderExecuted += OnExecution; _session.Subscribe(new ExecutionSubscriptionRequest(), SubscriptionSuccessCallback, SubscriptionFailureCallback); } }
file:///C:/Users/msischka.GAZPROMUK/AppData/Local/Temp/wz93be/LmaxNetCli...
19/09/2013