Blunt Review of the TD Ameritrade API for automated trading

For the past couple of months, I’ve been utilizing the TD Ameritrade API to work on a couple of applications that can allow you to trade outside the platform and automate some trading processes. To save you time from reading the rest of this post, it is just okay. Not good, not bad, just okay. Now if you want to know why, read on, and a warning that I ramble quite a bit.

I’ve been working on an application that acts as a custom screener for options and can close out vertical spreads after you’ve opened them using the trading platform once a profit target has been reached. I chose this platform because I already utilize them and they are one of the few brokerages out there that allow options trading via an API.

To get started, I had to sign up for a developer account here. Then from here, you have to sign in to the developer platform and create an App. The App name and callback url need to be created and you have to make sure that you keep track of these things as you will need them later. After you have created your app and received approval and can see the application in the portal you can get started using their API.

Their documentation covers mostly everything you need which is found here. The only improvement in their documentation that I see is that they could better present some of the data structures of their responses returned from the API. To actually request information that’s up to date from their API, you have to implement an Oauth2 authentication process within your application or you can manually manage tokens from the initial process of authenticating for generating the first token and use their API to periodically refresh tokens. They provide some documentation for the application here on how to actually do this. To generate the initial tokens for authentication you have to navigate to a given URL, replacing the query parameters with your App Name and callback URL that you created earlier in the process. After you do this, you’ll be prompted to log in to your TD Ameritrade account and authenticate using your normal credentials(note this can be a user attempting to use your application). After authentication, the user is directed to the callback URL with a “code” query parameter that needs to be utilized to send a POST request to their authentication API found here: https://api.tdameritrade.com/v1/oauth2/token. If done properly with a refresh token, you can store the refresh token and utilize it perpetually. Those that know how Oauth2 works should be familiar with these concepts and if not you can review them here. In my case, for one of my applications, I ended up creating my own provider for a pre-built Oauth2 module called Socialite that can be installed in your Laravel project. If I receive requests to put this on GitHub, I will do so. For my second application/prototype. I ended up initially just manually managing the tokens and creating a field that I can paste the token into.

Once you have authentication figured out you can query quotes, manage watchlists, view account information/balances, view options data, and place trades.

My first application mainly focuses on options and so for this, I extensively use the Options Chain API which lets me retrieve ALL of the option for a symbol. Because I can’t and don’t want to scan ALL stocks because of the 120 queries/minute rate limit, I use the Watchlist API to retrieve options quotes for the symbols I want and iterate through them all to determine if a symbol has options that meet my criteria. Now this is where things don’t quite work right and there is little documentation as to why. The fields to filter the request data don’t quite work as expected. For example, the date ranges don’t really work, at least when the strategy is set to VERTICAL. “toDate” doesn’t seem to do anything and the “fromDate” appears to work as the “toDate”??? What is going on here? As a result, I’ve had to iterate through more data than expected and use filtering mechanisms to filter out what I don’t need using list/collection data structures. Note: I’m doing this server-side, not client-side as this needs to run in the background in a scheduled cron job. Thankfully the response data structure is pretty usable as you can see below. The options chain can get pretty large on popular stocks but I don’t see a particularly better way of structuring the data.

//OptionChain:
{
  "symbol": "string",
  "status": "string",
  "underlying": {
    "ask": 0,
    "askSize": 0,
    "bid": 0,
    "bidSize": 0,
    "change": 0,
    "close": 0,
    "delayed": false,
    "description": "string",
    "exchangeName": "string",
    "fiftyTwoWeekHigh": 0,
    "fiftyTwoWeekLow": 0,
    "highPrice": 0,
    "last": 0,
    "lowPrice": 0,
    "mark": 0,
    "markChange": 0,
    "markPercentChange": 0,
    "openPrice": 0,
    "percentChange": 0,
    "quoteTime": 0,
    "symbol": "string",
    "totalVolume": 0,
    "tradeTime": 0
  },
  "strategy": "'SINGLE' or 'ANALYTICAL' or 'COVERED' or 'VERTICAL' or 'CALENDAR' or 'STRANGLE' or 'STRADDLE' or 'BUTTERFLY' or 'CONDOR' or 'DIAGONAL' or 'COLLAR' or 'ROLL'",
  "interval": 0,
  "isDelayed": false,
  "isIndex": false,
  "daysToExpiration": 0,
  "interestRate": 0,
  "underlyingPrice": 0,
  "volatility": 0,
  "callExpDateMap": "object",
  "putExpDateMap": "object"
}
 //StrikePriceMap:
{}
 //Option:
{
  "putCall": "'PUT' or 'CALL'",
  "symbol": "string",
  "description": "string",
  "exchangeName": "string",
  "bidPrice": 0,
  "askPrice": 0,
  "lastPrice": 0,
  "markPrice": 0,
  "bidSize": 0,
  "askSize": 0,
  "lastSize": 0,
  "highPrice": 0,
  "lowPrice": 0,
  "openPrice": 0,
  "closePrice": 0,
  "totalVolume": 0,
  "quoteTimeInLong": 0,
  "tradeTimeInLong": 0,
  "netChange": 0,
  "volatility": 0,
  "delta": 0,
  "gamma": 0,
  "theta": 0,
  "vega": 0,
  "rho": 0,
  "timeValue": 0,
  "openInterest": 0,
  "isInTheMoney": false,
  "theoreticalOptionValue": 0,
  "theoreticalVolatility": 0,
  "isMini": false,
  "isNonStandard": false,
  "optionDeliverablesList": [
    {
      "symbol": "string",
      "assetType": "string",
      "deliverableUnits": "string",
      "currencyType": "string"
    }
  ],
  "strikePrice": 0,
  "expirationDate": "string",
  "expirationType": "string",
  "multiplier": 0,
  "settlementType": "string",
  "deliverableNote": "string",
  "isIndexOption": false,
  "percentChange": 0,
  "markChange": 0,
  "markPercentChange": 0
}
 //Underlying:
{
  "ask": 0,
  "askSize": 0,
  "bid": 0,
  "bidSize": 0,
  "change": 0,
  "close": 0,
  "delayed": false,
  "description": "string",
  "exchangeName": "'IND' or 'ASE' or 'NYS' or 'NAS' or 'NAP' or 'PAC' or 'OPR' or 'BATS'",
  "fiftyTwoWeekHigh": 0,
  "fiftyTwoWeekLow": 0,
  "highPrice": 0,
  "last": 0,
  "lowPrice": 0,
  "mark": 0,
  "markChange": 0,
  "markPercentChange": 0,
  "openPrice": 0,
  "percentChange": 0,
  "quoteTime": 0,
  "symbol": "string",
  "totalVolume": 0,
  "tradeTime": 0
}
 //ExpirationDate:
{
  "date": "string"
}
 //OptionDeliverables:
{
  "symbol": "string",
  "assetType": "string",
  "deliverableUnits": "string",
  "currencyType": "string"
}

The account API is pretty straight forward, you querying to get account information for all of your accounts or just a single account. You can get related accounting informtion for each account from here.

Placing Orders is where things get complicated as you have to deliver a structured JSON request that gives you the ability to make things as complicated or as simple as you want. You can place simple Market Orders or you can place custom trigger orders with multiple tiers to multiple one-cancels-the-other orders. You do have to figure out what you are buying selling to open or close however which confused me initially. They do have an example guide here. But the rate limits will prevent you from doing any high-frequency trading which led me to my next project.

The think or swim platform that lets you trade has a problem. It only lets you buy/sell based on quantity and the triggers only allow you to set value based on a value deviating away from the original price. So I cannot place a trade for say $100 or $1000 and close out once I’ve hit $120 or $1200 or set a stop loss at $900. I have to specify the exact quantity I want to buy and either a pip value, % value, or $ value of share price relative to current price (say the stock is at $100, I have to specify +20 or -10). This looks like this:

So to get around this I wanted to put together an application that generates the quantity values for me given a absolute dollar value. But if this is for day trading the current value costs have to be calculated quickly. The traditional REST API provided by TD Ameritrade won’t work for this as the price could have changed significantly from when the price is originally queried and there is a 120/minute rate limit (after some tests its actually a bit less). So I thought I would try out their streaming api that pushes data through websockets.

The whole process for the streaming api is differant from the REST API. You have to use a REST API call to their UserPrincipals API which requires you to go through the whole oauth2 authentication process mentioned above and you have to make sure that you are requesting the following in your query parameters: streamerSubscriptionKeys,streamerConnectionInfo

You need to store the data here for a new request you will make to the websockets API to login to the streaming services. After you get this information you have to open a connection to their API. You can use either HTTP requests or websockets. I chose to try out websockets because I need a stream of quote prices for a specific symbol. In order to connect to the websockets api, you have to open a connection to a specific url:

"wss://" + userPrincipalsResponse.streamerInfo.streamerSocketUrl + "/ws"

Note the url is retrieved in the userprincipals. Once you have opened the connection you have to send a “LOGIN” command with credential data to the server. And this is where I couldn’t get any further.

I matched their specifications and documentation and I continue to get a “Bad formatting error”. This is where the documentation to access the API could be improved. For example, the tables that specify parameters overlap with variables that don’t show up under the parameter field objects in the example output request JSON. In order to login to the API you have to build out a request object that looks like this:

{
    "service": "ADMIN", 
    "requestid": "1", 
    "command": "LOGIN", 
    "account": "your_account", 
    "source": "your_source_id", 
    "parameters": {
        "token": "027363a5a5acd542622c125e04ca674be3cc5d5b", 
        "version": "1.0", 
        "credential": "userid%3DMYUSER20%26token%3D027363a5a5acd542622c125e04ca674be3cc5d5b%26company%3DAMER%26segment%3DAMER%26cddomain%3DCDI%26usergroup%3DACCT%26accesslevel%3DACCT%26authorized%3DY%26acl%3DDADSDFA4%26timestamp%3D1400607504057%26appid%3DMYAPP"
    }
}

Notice the url encoded credential below? This is a object that is populated with data from the userPrincipals from above. I created a request object that matches this exactly with the appropriate variables replaced to match my account information and my application could not get past that “Bad formatting error”.

Here is my prototype code that builds out the object, let me know if you see if I made a mistake in the comments (Note: This was built in c# as a quick prototype):

    private async Task getPrincipals()
    {
        var response = await ApiCall("userprincipals?fields=streamerSubscriptionKeys,streamerConnectionInfo");
        userPrincipals = JsonConvert.DeserializeObject<Principals>(response);

        streamingCredentials.userid = userPrincipals.accounts.First(x => x.accountId == accountsList.SelectedItem.ToString()).accountId;
        streamingCredentials.token = userPrincipals.streamerInfo.token;
        streamingCredentials.company = userPrincipals.accounts.First(x => x.accountId == accountsList.SelectedItem.ToString()).company;
        streamingCredentials.segment = userPrincipals.accounts.First(x => x.accountId == accountsList.SelectedItem.ToString()).segment;
        streamingCredentials.cddomain = userPrincipals.accounts.First(x => x.accountId == accountsList.SelectedItem.ToString()).accountCdDomainId;
        streamingCredentials.usergroup = userPrincipals.streamerInfo.userGroup;
        streamingCredentials.accesslevel = userPrincipals.streamerInfo.accessLevel;
        streamingCredentials.authorized = "Y";
        DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        DateTime tokenDate = Convert.ToDateTime(userPrincipals.streamerInfo.tokenTimestamp);
        TimeSpan tokenEpoch = tokenDate.ToUniversalTime() - epoch;
        long timestamp = (long)Math.Floor(tokenEpoch.TotalMilliseconds);
        streamingCredentials.timestamp = timestamp;
        streamingCredentials.appid = userPrincipals.streamerInfo.appId;
        streamingCredentials.acl = userPrincipals.streamerInfo.acl;
    }
            await getPrincipals();
            Console.WriteLine("Connecting");
            if (streamingClient.State == WebSocketState.CloseReceived)
            {
                await streamingClient.CloseAsync(WebSocketCloseStatus.NormalClosure, "Server sent Close signal", CancellationToken.None);
                streamingClient = new ClientWebSocket();
            }
            if (streamingClient.State == WebSocketState.Closed || streamingClient.State == WebSocketState.None)
            {
                await streamingClient.ConnectAsync(new Uri("wss://" + userPrincipals.streamerInfo.streamerSocketUrl + "/ws"), CancellationToken.None);
            }
            var loginRequest = new Request
            {
                service = "ADMIN",
                command = "LOGIN",
                requestid = "0",
                account = userPrincipals.accounts.First(x => x.accountId == accountsList.SelectedItem.ToString()).accountId,
                source = userPrincipals.streamerInfo.appId,
                parameters = new Parameters
                {
                    credentials =HttpUtility.UrlEncode(UrlHelpers.ToQueryString(streamingCredentials,"&")),
                    token = userPrincipals.streamerInfo.token
                }
            };
            Console.WriteLine("Sending Login Request");
            Console.WriteLine(JsonConvert.SerializeObject(loginRequest, new JsonSerializerSettings{NullValueHandling = NullValueHandling.Ignore}));
            await streamingClient.SendAsync(new ArraySegment<Byte>(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(loginRequest, new JsonSerializerSettings{NullValueHandling = NullValueHandling.Ignore}))), 0, true, CancellationToken.None);

I went out and built the rest of the application utilizing the normal REST API. I just need to access the streaming price and switch that part out in order to get it working as expected. But here is what it looks like. Not much to look at but the idea is that it’ll run over thinkorswim next to the charts.

This has been me rambling on about this API and the issues I see. I’ll have to round up the quantities because TDAmeritrade does not do fractional shares yet but Charles Schwab which aquired them does do them so it’ll eventually get there. What inspired me to build this is the fact that Webull actually does have this feature AND they do fractional shares. But Webull is missing some popular equities and it seems to slow down at times, almost like its built on Electron (the library that lets you build native desktop applications using javascript) and could use some performance tweaks but that’s a different conversation.

I do not work for TD Ameritrade. If you want to try it out and open an account with them feel free to do so. If you don’t already have an account, let me know and I can send you a referral and you’ll get a “special offer” and I’ll get $50 once you deposit $3000. No. Seriously.


Posted

in

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Share via
Copy link