This topic contains the following sections:
The core business of GF API is trading functionality. The API allows you to send orders and check positions and balances directly.
First, let's cleanup our previous code:
using System; using System.Linq; using GF.Api; using GF.Api.Connection; using GF.Api.Contracts; using GF.Api.Contracts.Lookup; using GF.Api.Contracts.Lookup.Request; using GF.Api.Impl; using GF.Api.Threading; using GF.Api.Values.Contracts.Lookup; namespace GettingStarted.Trading { internal class Program { private static SymbolLookupRequestID _frontEsSymbolLookupRequestID; private static System.Timers.Timer _statusTimer; private static void Main(string[] args) { Console.WriteLine("Hello OEC!"); var gfClient = GFApi.CreateClient(); var runner = new GFClientRunner(gfClient); runner.Start(); gfClient.Connection.Aggregate.LoginCompleted += GFClient_OnLoginCompleted; gfClient.Connection.Aggregate.LoginFailed += GFClient_OnLoginFailed; gfClient.Connection.Aggregate.Disconnected += GFClient_OnDisconnected; gfClient.Contracts.Lookup.SymbolLookupReceived += GFClient_OnSymbolLookupReceived; gfClient.Subscriptions.Price.PriceTick += GFClient_OnPriceTick; gfClient.Connection.Aggregate.Connect( new ConnectionContextBuilder() .WithUserName("username") .WithPassword("password") .WithPort(9210) .WithHost("api.gainfutures.com") .WithUUID("9e61a8bc-0a31-4542-ad85-33ebab0e4e86") .WithForceLogin(true) .Build()); Console.WriteLine("Connecting..."); _statusTimer = new System.Timers.Timer {Interval = TimeSpan.FromSeconds(10).TotalMilliseconds}; _statusTimer.Elapsed += (_, __) => StatusTimer_Tick(gfClient); _statusTimer.Start(); Console.WriteLine("Press any key to exit"); Console.ReadKey(); gfClient.Connection.Aggregate.Disconnect(); _statusTimer.Stop(); runner.Stop(); } private static void GFClient_OnLoginFailed(IGFClient client, LoginFailedEventArgs e) { Console.WriteLine($"OnLoginFailed: {e.FailReason}"); } private static void GFClient_OnDisconnected(IGFClient client, DisconnectedEventArgs e) { Console.WriteLine($"OnDisconnected: {e.Message}"); } private static void GFClient_OnLoginCompleted(IGFClient client, LoginCompleteEventArgs e) { Console.WriteLine($"OnLoginComplete: IsConnected={client.Connection.Aggregate.IsConnected}"); Console.WriteLine($"\tAccounts: {client.Accounts.Get().Count}, orders: {client.Orders.Get().Count}, base contracts: {client.Contracts.Base.Get().Count}"); DisplayAccount(client.Accounts.Get().First()); _frontEsSymbolLookupRequestID = client.Contracts.Lookup.ByCriteria( new SymbolLookupRequestBuilder() .WithResultCount(1) .WithSymbol("ES", TextSearchMode.StartsWith) .Build()); } private static void GFClient_OnSymbolLookupReceived(IGFClient client, SymbolLookupEventArgs e) { if (_frontEsSymbolLookupRequestID != null && e.RequestID == _frontEsSymbolLookupRequestID) { if (e.Contracts.Count > 0) { client.Subscriptions.Price.Subscribe(e.Contracts.First().ID); } } } private static void GFClient_OnPriceTick(IGFClient client, PriceChangedEventArgs e) { // TODO: check price and make a decision to place an order } private static void StatusTimer_Tick(IGFClient client) { client.Threading.BeginInvoke(() => { if (client.Connection.Aggregate.IsConnected) DisplayAccount(client.Accounts.Get().First()); }); } private static void DisplayAccount(GF.Api.Accounts.IAccount account) { Console.WriteLine($"Account: {account.Spec}"); GF.Api.Balances.IBalance totalBalance = account.TotalBalance; Console.WriteLine($"\tNetLiq: {totalBalance.NetLiq:c}"); Console.WriteLine($"\tCash: {totalBalance.Cash:c}"); Console.WriteLine($"\tOpen P/L: {totalBalance.OpenPnL:c}"); Console.WriteLine($"\tTotal P/L: {totalBalance.RealizedPnL + totalBalance.OpenPnL:c}"); Console.WriteLine($"\tInitial Margin: {totalBalance.InitialMargin:c}"); var netOptionsValue = totalBalance.LongCallOptionsValue + totalBalance.LongPutOptionsValue + totalBalance.ShortCallOptionsValue + totalBalance.ShortPutOptionsValue; Console.WriteLine($"\tNet Options Value: {netOptionsValue:c}"); Console.WriteLine($"Average Positions: {account.AvgPositions.Count}"); } } }
Let's place an order with the following rules:
Sell when Last price touches Bid price, and Buy when Last price touches Ask price
Do not place a new order if the last one is still working
Instruct GF Server to keep the order in working state not longer than one minute
public class PlaceOrderExample { private static void GFClient_OnPriceTick(IGFClient client, PriceChangedEventArgs e) { if (Math.Abs(e.Price.LastPrice - e.Price.BidPrice) < e.Contract.TickSize) PlaceOrder(client, e.Contract, OrderSide.Buy, e.Price.BidPrice, "By Bid"); else if (Math.Abs(e.Price.LastPrice - e.Price.AskPrice) < e.Contract.TickSize) PlaceOrder(client, e.Contract, OrderSide.Sell, e.Price.AskPrice, "By Ask"); } private static void PlaceOrder(GF.Api.IGFClient client, GF.Api.Contracts.IContract contract, OrderSide orderSide, double limitPrice, string comments) { if (client.Orders.Get().Count == 0 || client.Orders.Get().Last().IsFinalState) { OrderDraft orderDraft = new OrderDraftBuilder() .WithAccountID(client.Accounts.Get().First().ID) .WithContractID(contract.ID) .WithSide(orderSide) .WithOrderType(OrderType.Limit) .WithPrice(limitPrice) .WithQuantity(1) .WithEnd(DateTime.UtcNow.AddMinutes(1)) .WithComments(comments) .Build(); IReadOnlyList<OrderDraftValidationError> validationErrors = client.Orders.Drafts.Validate(orderDraft); if (validationErrors.Any()) { Console.WriteLine($"ERROR. Order {orderSide} {orderDraft.Quantity} {contract.Symbol} @ {contract.PriceToString(limitPrice)} Limit is invalid:"); foreach (var error in validationErrors) Console.WriteLine($"\t{error.Message}"); } else { GF.Api.Orders.IOrder order = client.Orders.SendOrder(orderDraft); Console.WriteLine($"Order {order} was sent"); } } } }
For the simplest trading strategy, only these steps are required:
Create an order draft (IOrderDraft)
Initialize its mandatory properties (AccountID, ContractID, Side, Quantity, Type)
Initialize conditional properties such as Price and Price2
Initialize conditional extended properties for trailing and iceberg orders (for example, TrailingStopData and IcebergData)
Initialize optional ones (Flags, Comments, Start, End)
Validate with GFClient.Orders.Drafts.Validate(GF.Api.Orders.Drafts.IOrderDraft)
Send SendOrder(IOrderDraft)
Suppose we decide to add more rules:
Modify the working order to move toward Last price from time to time, but not more frequently than once per 10 seconds
If previous command was not executed successfully, do not modify
private static void StatusTimer_Tick(GF.Api.IGFClient client) { client.Threading.Invoke(() => { if (client.Connection.Aggregate.IsConnected) { DisplayAccount(client.Accounts.Get().First()); ModifyOrder(client); } }); } private static void ModifyOrder(IGFClient client) { if (client.Orders.Get().Any() && !client.Orders.Get().Last().IsFinalState) { GF.Api.Orders.IOrder currentWorkingOrder = client.Orders.Get().Last(); if (currentWorkingOrder.Commands.Last().State == CommandState.Executed) { var priceChange = currentWorkingOrder.IsBuySide ? currentWorkingOrder.Contract.TickSize : -currentWorkingOrder.Contract.TickSize; ModifyOrderDraft modifyOrderDraft = new ModifyOrderDraftBuilder() .FromOrder(currentWorkingOrder) .WithPrice((currentWorkingOrder.Price ?? 0.0) + priceChange) .Build(); IReadOnlyList<OrderDraftValidationError> validationErrors = client.Orders.Drafts.Validate(modifyOrderDraft); if (validationErrors.Any()) { Console.WriteLine($"ERROR. Attempt to modify the order {currentWorkingOrder} failed:"); foreach (var error in validationErrors) Console.WriteLine($"\t{error.Message}"); } else { GF.Api.Orders.IOrder order = client.Orders.ModifyOrder(modifyOrderDraft); Console.WriteLine($"Modify request has been sent: {order}"); } } } }
Some properties cannot be modified, such as these:
Account
Contract
Side
Comments
CustomCompound
Caution |
---|
Even if GF API successfully sends a modify request, it could still be rejected by the exchange. If the request fails, Order.Commands.Last().State will become Failed |
Suppose we need to add a cancel rule:
If the order price is 4 ticks away from the last price, cancel instead of modify
private static void StatusTimer_Tick(GF.Api.IGFClient client) { if (client.Connection.Aggregate.IsConnected) { DisplayAccount(client.Accounts.Get().First()); CheckAndCancelOrder(client); ModifyOrder(client); } } private static void CheckAndCancelOrder(GF.Api.IGFClient client) { if (client.Orders.Get().Any() && !client.Orders.Get().Last().IsFinalState) { GF.Api.Orders.IOrder currentWorkingOrder = client.Orders.Get().Last(); if (currentWorkingOrder.Commands.Last().State == CommandState.Executed) { if (client.Orders.Drafts.GetPriceCount(currentWorkingOrder.Type) > 0) { if (Math.Abs((currentWorkingOrder.Price ?? 0.0) - currentWorkingOrder.Contract.CurrentPrice.LastPrice) >= 3 * currentWorkingOrder.Contract.TickSize) { Console.WriteLine($"Send request to cancel: {currentWorkingOrder}"); client.Orders.CancelOrder(currentWorkingOrder.ID, SubmissionType.Automatic); } } } } }
A cancel request can be rejected by the exchange. For example:
The order is already filled
The exchange can cancel only the remaining quantity of the order. The order will have the Completed status.
The current order status can be checked at any time of its lifetime: IOrderCurrentState.
Regular state diagram: Sent -> Working -> Completed
Orders that held by GF server until some condition triggers (for example, Market-If-Touched orders or orders with 'time release' instruction) will have the intermediate state, Held: Sent -> Held -> Working -> ..
In some situations, the transition to 'Working' status is extended: .. -> Accepted -> Working -> ..
If the order is rejected by risk engine or some other preliminary check: Sent -> Rejected
In some situations, if the order is accepted by risk engine, but rejected by exchange: .. -> Accepted -> Rejected
Cancelled order: .. -> Working -> Cancelled or .. -> Held -> Cancelled
To catch all status transitions of our orders, we need to listen to the GFClient.Orders.OnOrderStateChanged event:
private static void RegisterOrderStateChanged(GF.Api.IGFClient client) { client.Orders.OrderStateChanged += GFClient_OnOrderStateChanged; } private static void GFClient_OnOrderStateChanged(GF.Api.IGFClient client, GF.Api.Orders.OrderStateChangedEventArgs e) { Console.WriteLine($"{e.Order.ID} order state changed from {e.PreviousOrderState} to {e.Order.CurrentState}"); }
When an order is initially created, it will have a negative temporary "local" OrderID that is replaced by a global positive ID by the GF servers. The OnOrderConfirmed event will help you map between local and global order IDs:
private static void RegisterOnOrderConfirmed(GF.Api.IGFClient client) { client.Orders.OrderConfirmed += GFClient_OnOrderConfirmed; } private static void GFClient_OnOrderConfirmed(GF.Api.IGFClient client, GF.Api.Orders.OrderConfirmedEventArgs e) { Console.WriteLine($"Order #{e.OriginalOrderID} confirmed. New order ID is {e.Order.ID}"); }
States of particular order commands like modify and cancel can be caught by the OnCommandUpdated event
Besides straight Market, Limit, Stop and Stop Limit orders, GF API also provides synthetic orders like Trailing Stops, Iceberg, Market-If-Touched, Market-On-Close and Market-On-Open.
Market-On-Close and Market-On-Open draft property initialization is similar to a normal Market order, but other kinds require additional parameters.
Market-If-Touched requires initializing the IBaseOrderDraft.Price property. The order will be converted to Market as soon as last market price touches this value.
To place Iceberg, add the following code (suppose we need to break our order quantity by 1-lot pieces):
new OrderDraftBuilder() .WithOrderType(OrderType.Iceberg) .WithIceberg(new IcebergData(1)) .Build();
Here is a TrailingStop that should start trailing as soon as last price touches the current last price one more time and follows with 10 ticks distance:
new OrderDraftBuilder() .WithOrderType(OrderType.TrailingStopLoss) .WithTrailingStop(new TrailingStopData(contract.CurrentPrice.LastPrice, (10 - 1) * contract.TickSize)) .Build();
If your order is rejected, you can find the reason in ResultComments.
To get more details about a risk violation rejection, you can capture messages in the OnError event:
private static void Main(GF.Api.IGFClient client) { client.Logging.ErrorOccurred += GFClient_OnErrorOccurred; } private static void GFClient_OnErrorOccurred(GF.Api.IGFClient client, GF.Api.Utils.ErrorEventArgs e) { Console.WriteLine($"OnError: {e.Exception.Message}"); }
Risk violation text will look like this:
Risk violation: 1540:API001540 Max credit = 0.0000000000 USD,Order requires [3600.00000 USD] and needs additional credit of 130.00000 USD (cash leveraged) Cash and credit balance 125.00000 USD ...
where 1540 is AccountID and API001540 is Spec.
To place an OSO (or bracket order), call SendLinkedOrders.
For OCO orders, call SendOCOOrders.
All other functionality remains the same.
OCO and OSO orders have links to their involved orders:
Parent references the main order from a dependent order
Linked references dependent orders from the main OSO order
OCO references another order from an OCO order
Data about fills can be useful for further trading decisions.
Let's dump the fills to console output:
private static void RegisterOnOrderFilled(GF.Api.IGFClient client) { client.Orders.OrderFilled += GFClient_OnOrderFilled; } private static void GFClient_OnOrderFilled(GF.Api.IGFClient client, GF.Api.Orders.OrderFilledEventArgs e) { Console.WriteLine( "#{0} New fill: {1} @ {2} ({3}). Total filled qty: {4}, avg. price: {5}", e.Order.ID, e.Fill.Quantity, e.Fill.Contract.PriceToString(e.Fill.Price), e.Fill.IsActive ? "active" : "cancelled", e.Order.Fills.TotalQuantity, e.Order.Contract.PriceToString(e.Order.Fills.AvgPrice)); }
Note |
---|
IFillContract can differ from IOrderContract orders with some contract kinds like spreads and custom compounds. |
Note |
---|
An existing fill can have a cancelled status (IsActive = false) in some situations, like re-distribution of allocation block fills or manual cancel of pit fills by the GF TradeDesk. |
GF API has two types of positions: average and detailed. As the name suggests, average position contains aggregated values for all trades for an account-contract pair. It includes traded volume (bought, sold, net), previous position, released and open p/l, amount of commissions, and margin requirements. For options, it will include market value and cost basis. Collection of detailed positions is intended for deep analysis of particular trade pairings.
For our example, let's display only high level details of the average position on every change:
private static void RegisterOnAvgPositionChanged(GF.Api.IGFClient client) { client.Accounts.AvgPositionChanged += GFClient_OnAvgPositionChanged; } private static void GFClient_OnAvgPositionChanged(GF.Api.IGFClient client, GF.Api.Positions.PositionChangedEventArgs e) { Console.WriteLine( "Average Position. {0}/{1}: Net Pos: {2} @ {3}, Bought: {4}, Sold {5}, Prev Pos: {6} P/L: {7:c}", e.Account.Spec, e.ContractPosition.Contract.Symbol, e.ContractPosition.Net.Volume, e.ContractPosition.Net.Price, e.ContractPosition.Long.Volume, e.ContractPosition.Short.Volume, e.ContractPosition.Prev.Volume, e.ContractPosition.OTE + e.ContractPosition.Gain); }
Note |
---|
The OnAvgPositionChanged event is raised on any average position change, including Open P/L changes for open positions. So, the event is raised when the position's contract quote updates. |
For p/l values, positions have pairs: one is the value of the positions's currency (like CurrencyOTE) and one is the USD value including commissions (for example, OTE)
Every account has a collection of currency balances and a total one. Like positions, currency balances do not include commissions.
Our example is already displaying some balance values by timer, so let's show them as soon as they change:
private static void RegisterOnBalanceChanged(GF.Api.IGFClient client) { client.Accounts.AccountSummaryChanged += GFClient_OnAccountSummaryChanged; client.Accounts.BalanceChanged += GFClient_OnBalanceChanged; } private static void GFClient_OnAccountSummaryChanged(GF.Api.IGFClient client, GF.Api.Accounts.AccountSummaryChangedEventArgs e) { DisplayBalance("Account Summary Changed", e.Account, e.Currency); } private static void GFClient_OnBalanceChanged(GF.Api.IGFClient client, GF.Api.Accounts.BalanceChangedEventArgs e) { DisplayBalance("Balance Changed", e.Account, e.Currency); } private static void DisplayBalance(string comment, GF.Api.Accounts.IAccount account, GF.Api.Currencies.ICurrency currency) { GF.Api.Balances.IBalance balance = account.Balances[currency]; Console.WriteLine($"{comment}. {account.Spec} ({currency.Name}): P/L = {Math.Round(balance.OpenPnL + balance.RealizedPnL, 2)}. Total Net Liq: {account.TotalBalance.NetLiq:c}"); }