Trading

GF API Documentation

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.

Orders

First, let's cleanup our previous code:

Initial Trading Example
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}");
        }
    }
}
Regular Orders

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

Place Order
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)

Modify

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

Modify Order
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 note 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

Cancel

Suppose we need to add a cancel rule:

  • If the order price is 4 ticks away from the last price, cancel instead of modify

Cancel Order
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.

Order State Diagram

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:

Order State Changed
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:

Order Confirmed
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

Synthetic orders

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):

Iceberg
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:

Trailing Stop
new OrderDraftBuilder()
    .WithOrderType(OrderType.TrailingStopLoss)
    .WithTrailingStop(new TrailingStopData(contract.CurrentPrice.LastPrice, (10 - 1) * contract.TickSize))
    .Build();
Risk Violations

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:

OnError
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.

One-Sends-Other (OSO) and One-Cancels-Other (OCO)

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

Fills

Data about fills can be useful for further trading decisions.

Let's dump the fills to console output:

Order Filled
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 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.

Positions

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:

Average Position Changed
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 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)

Balances

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:

Balance Changed
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}");
}