2

Has anyone gotten the TD Ameritrade streaming API to work with C#? They have some documentation and JavaScript examples here https://developer.tdameritrade.com/content/streaming-data. I have gotten the JavaScript examples to work on https://js.do/, but can't get anything similar to work in .NET. This is a shortened version of what I'm trying to do. I can't include exactly what I'm sending because I'm trying to send the login message which includes account information, but I can say that I copy and pasted the exact JSON message that is working in my JavaScript tests into the file LoginJSON.txt in this example. In this example the socket will just close as soon as I send the message, no text response at all. If however I send an intentionally malformatted message I'll actually get text response saying the message is malformatted and then get a socket disconnect. Their support has been non-responsive which I understand to the the norm. There are some python examples here https://www.youtube.com/channel/UCBsTB02yO0QGwtlfiv5m25Q, but I've watched them all and haven't learned anything to help me get my code working.

        ClientWebSocket socket = new ClientWebSocket();
        var connectAsync = socket.ConnectAsync(new Uri("wss://streamer-ws.tdameritrade.com/ws"), CancellationToken.None);
        string loginRequest;
        using (StreamReader re = new StreamReader("LoginJSON.txt")) {
            loginRequest = re.ReadToEnd();
        }

        connectAsync.Wait();

        Thread readThread = new Thread(
            delegate(object obj)
            {
                while (true) {
                    if (socket.State == WebSocketState.Open) {
                        Console.Out.WriteLine("Waiting");
                        byte[] recBytes = new byte[1024];
                        var clientBuffer = new ArraySegment<byte>(recBytes);
                        var receiveAsync = socket.ReceiveAsync(clientBuffer, CancellationToken.None);
                        receiveAsync.Wait();
                        switch (receiveAsync.Result.MessageType) {
                            case WebSocketMessageType.Text:
                                var s = Encoding.UTF8.GetString(recBytes);
                                Console.Out.WriteLine(s.Trim());
                                break;
                            case WebSocketMessageType.Close:
                                Console.Out.WriteLine("Close message received");
                                break;
                            default:
                                throw new ArgumentOutOfRangeException();
                        }
                    }
                }
            });

        readThread.Start();
        socket.SendAsync(Encoding.UTF8.GetBytes(loginRequest), WebSocketMessageType.Text, true, CancellationToken.None);
        Console.ReadLine();
user2062445
  • 81
  • 2
  • 6
  • Use a sniffer like wireshark or fiddler. Check the response status like 200 OK to determine the error. Best way of debugging is to compare the headers of first request in working and non working apps. The default headers in c# are different from other methods. To fix issue make the headers in c# look like the headers in working applictions. – jdweng Apr 25 '20 at 07:47
  • @jdweng I tried this but am sadly unfamiliar with Wire Shark and network captures. I have two captures from both applications. There are only 20 or so relevant entries in each. The last entry in the failing application is highlighted in red, so I assume that is the disconnect. I don't see anything in either that looks like 200 OK. Anything else I might look for? – user2062445 Apr 25 '20 at 17:02
  • You are only getting the body of the response and not the full message. You can download for free either wireshark or fiddler which give a lot of useful info in debugging network issues. – jdweng Apr 25 '20 at 18:00
  • @jdweng Sorry, by unfamiliar I meant I just haven't used it much, but Wire Shark is what I used to capture the above mentioned network traffic. I see messages like, "Client Hello", "Server Hello, Change Cipher Spec", "Encrypted Handshake Message", and several "Application Data" messages. They look slightly different from one app to the other but I'm not sure what is meaningful. They look slightly different from one capture of the same app to the other. Thanks for the suggestion, but this might be a little over my head. – user2062445 Apr 25 '20 at 23:45
  • An HTTP message uses TCP as the Transport Layer. TCP has max length of ~1500 bytes. So each HTTP message consists of one or more TCP messages. Also for secure https there is a security protocol for TLS/SSL that is done in TCP. You should start by only looking at the first http request headers. There is a negotiation between client and server where the headers from the client and the headers on the webpage determine the mode of operation. You are going into the wrong mode due to the headers being wrong in your first request. You need to modify the headers in c# to be same as working app. – jdweng Apr 26 '20 at 03:10
  • @jdweng I was under the impression that WebSockets were a distinct protocol from HTTP. Should I expect to see the same types of messages? – user2062445 Apr 26 '20 at 17:17
  • The only difference between a web and http is web you have a viewer like a browser while http you just get a text response. – jdweng Apr 26 '20 at 23:03

5 Answers5

2

I faced the same problem and I managed to solve it. In my case, the timestamp was not prepared correctly, it is necessary to calculate the timestamp to get the TokenTimestamp property, which should be converted to universal time. Sorry for my english from google translate. :) Here is the correct code:

DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
DateTime tokenDate = Convert.ToDateTime(userPrincipal.StreamerInfo.TokenTimestamp);
TimeSpan tokenEpoch = tokenDate.ToUniversalTime() - epoch;
long timestamp = (long)Math.Floor(tokenEpoch.TotalMilliseconds);

var credentials = new Credentials
{
    userid = userPrincipal.Accounts[0].AccountId,
    token = userPrincipal.StreamerInfo.Token,
    company = userPrincipal.Accounts[0].Company,
    segment = userPrincipal.Accounts[0].Segment,
    cddomain = userPrincipal.Accounts[0].AccountCdDomainId,
    usergroup = userPrincipal.StreamerInfo.UserGroup,
    accesslevel = userPrincipal.StreamerInfo.AccessLevel,
    authorized = "Y",
    timestamp = timestamp,
    appid = userPrincipal.StreamerInfo.AppId,
    acl = userPrincipal.StreamerInfo.Acl
};
var credentialArr = credentials.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public).Select(p => new KeyValuePair<string, string>(p.Name, p.GetValue(credentials, null).ToString()));
var loginRequest = new Request
{
    service = "ADMIN",
    command = "LOGIN",
    requestid = "0",
    account = userPrincipal.Accounts[0].AccountId,
    source = userPrincipal.StreamerInfo.AppId,
    parameters = new Parameters
    {
        credential = string.Join("&", credentialArr.Where(c => !string.IsNullOrWhiteSpace(c.Value)).Select(c => string.Format("{0}={1}", HttpUtility.UrlEncode(c.Key, Encoding.UTF8), HttpUtility.UrlEncode(c.Value, Encoding.UTF8)))),
        token = userPrincipal.StreamerInfo.Token,
        version = "1.0",
        qoslevel = "0"
    }
};
var req = JsonConvert.SerializeObject(Requests.ToRequests(loginRequest), Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
socketClient = new WebSocket(string.Format("wss://{0}/ws", userPrincipal.StreamerInfo.StreamerSocketUrl));
if(Environment.OSVersion.Version.Major > 5)
{
    socketClient.SslConfiguration.EnabledSslProtocols = (System.Security.Authentication.SslProtocols)3072;
    socketClient.SslConfiguration.ServerCertificateValidationCallback = (sender, cert, chain, sslPolicyErrors) => { return true; };
}
socketClient.Connect();
socketClient.Send(req);
Mikhail
  • 36
  • 3
  • What type is socketClient in your code? I was using System.Net.WebSockets.ClientWebSocket and could never get it to work. I don't see a simple Send method on it, only a SendAsync. I'll try to use your approach exactly, but I was able to confirm that sending the exact same text in javascript worked, including the timestamp formatting. – user2062445 Jun 25 '20 at 13:39
  • I have tried many clients, as well as the online service https://www.websocket.org/echo.html. But the problem was identical to yours, the connection was closed without errors and reasons. With this connection behavior, you might think that the headers are incorrect or the WebSocket client is bad, in fact, the server sees that the structure is correct, and the data has not passed validation. I am using https://github.com/sta/websocket-sharp for Net.Framework 4.0 Updated the sample code for working with the Web socket. – Mikhail Jun 26 '20 at 09:28
  • 1
    I was able to get a login call to work using System.Net.WebSockets.ClientWebSocket. I was having the same issue and it turned out to be setting the timestamp on the Login.Credential. What I think happens is during the call the user preferences, the token timestamp is converted to local time. Then when generating the Login request, the timestamp milliseconds is based on local time and not UTC. I made a change to timestamp generation using this https://stackoverflow.com/questions/5955883/datetimes-representation-in-milliseconds and was able to get a appropriate Login response. – Robert P Jul 13 '20 at 21:59
  • I had almost identical code but was using a different Socket Client. Switched over to the WebSocket-Sharp and added that conditional-if (Environment.OSVersion...) statement and everything works properly now. Thanks. – Daniel Pavlovsky Jan 19 '21 at 14:16
0

I tried the WebSocketClient approach and never got it working as well. I got exactly the same errors you are getting. Exactly. I found that WebSocketClient actually complicates what is very simple to implement in javascript. Just have your C# call a javascript function to execute the javascript and send the response back to you. I've got it working that way using C# in a Blazor app, and it works seamlessly.

  • Fair enough. I eventually got it working in python and will probably use that to bridge the gap into my windows app. Seems sort of round about, but glad to know I'm not the only one with the issue. I've used websockets before with crypto exchange market data with some success so it was disappointing to not get this one working. – user2062445 May 24 '20 at 01:36
  • There are 0 votes on this but has been marked as accepted answer. There is no code example either. onejbsmith can you post your Blazer code? – Ashok Garg Dec 11 '21 at 16:25
0

@Mikhail, would you share your code for getting user principals. this is my code but I get status=401 even though my access token is valid(I have tested it via API page):

    using System;
    using WebSocketSharp;
    using System.Net.Http;
    using System.Threading.Tasks;

namespace TdLogin
{
    class Program
    {
        static async Task  Main(string[] args)
        {
            string accessToken = util.accessToken; // get the access token from util 
            Console.WriteLine("Hello World!");
            var client = new HttpClient();
            client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", accessToken);
            var result = await client.GetAsync("https://api.tdamer`enter code here`itrade.com/v1/userprincipals?fields=streamerSubscriptionKeys%2CstreamerConnectionInfo");
            Console.WriteLine("status= {0}", result.StatusCode);
            Console.WriteLine(result.Content);
            Console.ReadKey();
        }
    }
}
D.J.
  • 3,644
  • 2
  • 15
  • 22
0

Well TDA Quotes are always going to be in (New York) Eastern Time because it's the NYSE standard. Which is why old watches would display more than one time.

I haven't really played with streaming yet, but worked through the epoch time. I am in Eastern time, so I don't have to deal with conversions. Therefore, the following conversions are not battle tested, but the following C# methods may help someone that stumbles on this thread in the future.

    private static readonly TimeZoneInfo TDAServerTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
    public static DateTime ToServerTime(this DateTime dateTime)
    {
        return TimeZoneInfo.ConvertTime(dateTime, TDAServerTimeZone);
    }

    public static DateTime ToLocalTime(this DateTime dateTime)
    {
        TimeZoneInfo LocalTimeZone = TimeZoneInfo.Local;
        return TimeZoneInfo.ConvertTime(dateTime, TDAServerTimeZone, LocalTimeZone);
    }

    public static DateTime ToUTCTime(this DateTime dateTime)
    {
        TimeZoneInfo UTCTimeZone = TimeZoneInfo.Utc;
        return TimeZoneInfo.ConvertTime(dateTime, TDAServerTimeZone, UTCTimeZone);
    }

    public static DateTime FromUnixTime(long unixTime)
    {
        DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0);
        return epoch.AddMilliseconds(unixTime);
    }

    public static long ToUnixTime(DateTime dtConvert)
    {
        TimeSpan t = dtConvert - new DateTime(1970, 1, 1);
        long SinceEpoch = (long)t.TotalMilliseconds;
        return SinceEpoch;
    }
  • This does not really answer the question. If you have a different question, you can ask it by clicking [Ask Question](https://stackoverflow.com/questions/ask). To get notified when this question gets new answers, you can [follow this question](https://meta.stackexchange.com/q/345661). Once you have enough [reputation](https://stackoverflow.com/help/whats-reputation), you can also [add a bounty](https://stackoverflow.com/help/privileges/set-bounties) to draw more attention to this question. - [From Review](/review/late-answers/30881381) – John Glenn Jan 26 '22 at 22:03
-1

Here is a C# API on GitHub. They also support other languages: C#, Python, JavaScript, C++ and Ruby. https://github.com/td-ameritrade

I do not know if this is officially from TD Ameritrade.

Andrew
  • 129
  • 2
  • 8