using System; using System.Collections.Generic; using System.Text; using System.Threading; using System.Web; using System.Xml; using TradeIdeas.MiscSupport; using TradeIdeas.ServerConnection; using TradeIdeas.TIProData.Interfaces; using TradeIdeas.XML; /* This is the new way to access the top list data. You can use this to fill a * traditional top list window. You can also request specific data about a specific * stock. The previous API, in TopListManager.cs, was created for the former. It * could accomplish the latter, but it was harder for the programmer to use and it * was a lot less efficient, when compared to this API. This file talks with * cpp_alert_server/source/fast_alert_search/TopListMicroService.C on the server. */ /* * Like most items in TIProData * 1) You can call these functions from any thread. * 2) The response can come in any thread. You might even get a response in the calling * thread, before the initial request returns. * 3) You should always cancel a request when you are done. This will release resources * on the client and the server. This does NOT guarentee that the responses will * stop immediately. Because of threading issues you don't know how long it will be * before the last response arrives. This is very important for streaming data, but * is also helpful for single requests. */ namespace TradeIdeas.TIProData { public class TopListRequest { /// /// Use this to create a unique name for each request. Names are required /// so we can cancel the requests. The server is designed to require a /// unique name for each one, even if it's a one time request or for some /// other reason you don't plan to ever cancel the request. It seemed /// simpler just to always require this. /// private static Int64 _counter = 0; /// /// A standard top list collaborate string. Do not include the "http" part. /// This is optional. If you want to run a traditional top list window, set /// this as always. You can use some of the other properties to customize /// this. Or you can leave this as null, and only use the other properties /// to describe your request. /// public String Collaborate { get; set; } /// /// Do you want meta data? If you're not going to use it, say no. /// /// Meta data only applies to the collaborate string. If you don't provide one, /// it really doesn't matter what you say here, there will be no meta data. /// public Boolean? SkipMetaData { get; set; } /// /// This value is ignored if you set the collaborate string. We always need /// the database to interpret a collaborate string. If this is false you'll /// lose certain features, like the ability to use a user's custom formulas. /// If you set this to true the initialization might take longer. /// public Boolean? UseDatabase { get; set; } /// /// When is the last time we saw this data? /// /// If this is a new request leave this field as NULL, to say we've never /// received any data. This means two things to the server. /// 1) We want the first request without any delays. /// 2) If there are multiple requests that are ready for data now, a /// higher priority is given to requests that have never received /// data compared to requests that just have old data. /// /// Keep track of the time each time you receive data from the server. /// If you have to automatically resend the request, use this field to /// provide the time we received data. /// /// HasBeenSeen was used at one time for the same purpose. It was a Boolean /// so it wasn't precise enough. Setting HasBeenSeen to false is like /// setting LastUpdate to NULL. Setting HasBeenSeen to true is like setting /// LastUpdate to 0 or to the current time, depending which version of the /// server you are attached to. Neither 0 nor the current time was a great /// answer, so now the server expects the client to give it more data. /// /// Warning: The default value will request the highest priority. If /// a lot of requests keep the default value just because they were lazy, /// there would be too many high priority requests. The priority system /// only works if some jobs have a lower priority. /// public DateTime? LastUpdate { get; set; } /// /// If this is true the server will continue to send updates until we say /// stop. If this if false, the server will send exactly one set of data. /// public Boolean Streaming { get; set; } /// /// This only applies if you send a collaborate string. If this is true we /// save the collaborate string to the Most Recently Used list. This only /// looks at the collaborate string. If you use other properties to customize /// the request, those do not show up in the MRU list. /// public Boolean? SaveToMru { get; set; } /// /// Do we want the columns from the collaborate string? /// public Boolean? CollaborateColumns { get; set; } /// /// Set this to true if you want to see the most recent data regardless /// of the time. Set this to false if you want to see the most recent /// data during market hours, but you want to freeze the data at the /// close. If you don't specify this value here we look at the value /// in the collaborate string. /// public Boolean? OutsideMarketHours { get; set; } /// /// How many results do you want to see? By default this value is copied /// from the the collaborate string. /// public int? ResultCount { get; set; } /// /// If you set this field the request will only return data for for this /// symbol. Otherwise it will look at all stocks. This has the same /// effect as setting a single symbol in the collaborate string. You /// shouldn't set this value if the collaborate string includes a single /// symbol or a symbol list. (That's different from ResultCount or /// OutsideMarketHours where a value specified directly in this object /// will completely override a value set in the collaborate string.) /// public String SingleSymbol { get; set; } /// /// The format for this field is more or less the same as what you put /// in the formula editor. The default value is what is specified in /// the collaborate string. /// /// This always returns the smallest value first. In the top list config /// window the user had a sort field and a choice of smallest or largest /// first. If you use this and you want the largest value first, try adding /// a minus sign to your formula. Under the hood that's how the traditional /// top list window works. /// public String SortFormula { get; set; } /// /// The format for this field is more or less the same as what you put /// in the formula editor. The default value is what is specified in /// the collaborate string. If neither is specified this defaults to /// "1" i.e. true. /// public String WhereFormula { get; set; } /// /// These are data points that you want to see for each row. These are /// in addition to any that come from the collaborate string. Each key /// is the name of the field. That same name is used in the response. /// Each value a formula. The format of the formula is more or less /// the same as what you put in the formula editor. /// /// This is initialized to null. null means the same thing as an empty /// dictionary. Any method in this class which adds to /// ExtraColumnFormulas will automatically check if this is null and /// replace it with an empty dictionary first. /// public Dictionary ExtraColumnFormulas { get; set; } /// /// Add a column to ExtraColumnFormulas. /// If this is a duplicate request, silently ignore it. /// If you try to change the formula associated with a wireName, that's an exception. /// /// The key to find this item in a RowData object /// /// The data we want for this key. /// The format is basically the same as the formula editor formulas. /// public void AddExtraColumn(string wireName, string formula) { if (null == formula) throw new ArgumentNullException("formula"); if (null == ExtraColumnFormulas) ExtraColumnFormulas = new Dictionary(); else if (ExtraColumnFormulas.ContainsKey(wireName)) { if (ExtraColumnFormulas[wireName] != formula) throw new ArgumentException("Formula for '" + wireName + "' changed from '" + ExtraColumnFormulas[wireName] + "' to '" + formula + "'"); else return; // Ignore duplicate/identical request. } ExtraColumnFormulas[wireName] = formula; } /// /// This will ask for data for a column. /// /// This will not affect the meta data in any way. /// /// It is safe to call this more than once. It is safe AND EFFICIENT to /// call this even if you've also requested the same column via the /// collaborate string. It's not defined what happens if you try to /// use this to CHANGE the definition of a column. /// /// /// The name of the filter, like "Price", "Vol", or "Vol5D" /// /// /// The wire name associated with this column. You can use this as a key /// when requesting a field from a RowData object. /// public string AddExtraColumn(string internalCode) { if (internalCode == "") // It's tempting to fail an assertion here. System.Diagnostics.Debug.WriteLine("internalCode should not be the empty string!"); string wireName = CommonAlertFields.GetFilterWireName(internalCode); string formula = '[' + internalCode + ']'; AddExtraColumn(wireName, formula); return wireName; } /// /// These are called "magic columns" in some places. We request these for most requests /// that might show up in a row in a table. When the user right clicks or double clicks /// he might use some shared code, like the "send to" feature. That feature will always /// expect to see the exchange and the symbol, even if the user did not want to see /// those fields. /// /// These are not specific to the top list. Presumably we'll use these in the alert /// window at some point. /// private static readonly Dictionary StandardRowDataColumns = new Dictionary { { CommonAlertFields.SYMBOL, "[D_Symbol]"}, { CommonAlertFields.EXCHANGE, "[D_Exch]" }, { "four_digits", "price<1.0" } }; /// /// These are called "magic columns" in some places. We request these for most requests /// that might show up in a row in a table. When the user right clicks or double clicks /// he might use some shared code, like the "send to" feature. That feature will always /// expect to see the exchange and the symbol, even if the user did not want to see /// those fields. /// /// These will get added to the ExtraColumnFormulas dictionary. /// public void AddStandardRowDataColumns() { foreach (var kvp in StandardRowDataColumns) AddExtraColumn(kvp.Key, kvp.Value); } public interface Listener { /// /// This is the normal callback from a top list. This means we have rows to display. /// /// The data requested. /// Oldest time /// Newest time /// The same object returned by Send(). void OnRowData(List rows, DateTime? start, DateTime? end, Token token); /// /// This is optional. Typically this comes once at the beginning, before the first row of data. /// /// /// The same object returned by Send(). /// GetMetaData(), when called on this object, will return something other than null. /// We fill in the meta data field shortly before making this callback. void OnMetaData(Token token); /// /// We were disconnected before the request was done. /// This class does not perform automatic retries. /// Another class can resend the request if required. /// /// The same object returned by Send(). void OnDisconnect(Token token); } private String Quote(String original) { return HttpUtility.UrlEncode(original); } /// /// Returns null if and only if the input is null. /// /// /// private String Quote(Dictionary original) { if (null == original) return null; StringBuilder result = null; foreach (var kvp in original) { if (null == result) result = new StringBuilder(); else result.Append('&'); result.Append(Quote(kvp.Key)); result.Append('='); result.Append(Quote(kvp.Value)); } if (null == result) return null; else return result.ToString(); } private class CallbackInfo { public readonly Listener Listener; public readonly TokenImpl TokenImpl; public CallbackInfo(Listener listener, TokenImpl tokenImpl) { Listener = listener; TokenImpl = tokenImpl; } }; /// /// It is safe to reuse a TopListRequest object. As soon as Send() returns it /// is safe to modify a TopListRequest object. /// /// /// /// This can be null. /// Defaults to null. /// public Token Send(ISendManager sendManager, Listener listener, Object clientInfo = null) { System.Diagnostics.Debug.Assert(null != listener); TalkWithServer.CancelToken cancelToken = new TalkWithServer.CancelToken(); string name = "tlr" + Interlocked.Increment(ref _counter); TokenImpl result = new TokenImpl(sendManager, name, cancelToken, clientInfo, this); CallbackInfo callbackInfo = new CallbackInfo(listener, result); var message = TalkWithServer.CreateMessage( "command", "ms_top_list_start", "name", name, "collaborate", Collaborate, "skip_metadata", SkipMetaData, "use_database", UseDatabase, "last_update", LastUpdate, "streaming", Streaming, "save_to_mru", SaveToMru, "collaborate_columns", CollaborateColumns, "outside_market_hours", OutsideMarketHours, "result_count", ResultCount, "single_symbol", SingleSymbol, "sort_formula", SortFormula, "where_formula", WhereFormula, "extra_column_formulas", Quote(ExtraColumnFormulas)); sendManager.SendMessage(message, OnResponse, Streaming, callbackInfo, cancelToken); return result; } private void OnResponse(byte[] body, object clientId) { CallbackInfo callbackInfo = (CallbackInfo)clientId; if (null == body) // Disconnected. Report that to the call back. callbackInfo.Listener.OnDisconnect(callbackInfo.TokenImpl); else { XmlNode message = XmlHelper.Get(body).Node(0).Node("TOPLIST"); string messageType = message.Property("TYPE"); switch (messageType) { case "data": { // This next part is taken from TopListManager.cs. It's tempting to make a // library routine to do this. The server side is using the same library // to encode both messages. But I think TopListManager is going away, so // there's no point. DateTime? start = ServerFormats.DecodeServerTime(message.Property("START_TIME")); DateTime? end = ServerFormats.DecodeServerTime(message.Property("END_TIME")); callbackInfo.Listener.OnRowData(DecodeRows(message), start, end, callbackInfo.TokenImpl); break; } case "info": { string clientCookie = message.Node("CLIENT_COOKIE").Property("VALUE"); // TODO -- Use this! TopListInfo metaData = new TopListInfo(); metaData.Config = message.Property("SHORT_FORM", null); metaData.WindowName = message.Property("WINDOW_NAME"); if (null != message.Node("SORT_BY")) metaData.ServerSort = message.Node("SORT_BY").Property("FIELD"); List columns = new List(); foreach (XmlNode columnXml in message.Node("COLUMNS").Enum()) // add parameter for form type - RVH20210602 //columns.Add(new ColumnInfo(columnXml)); columns.Add(new ColumnInfo(columnXml, "TOP_LIST_WINDOW")); metaData.Columns = columns.AsReadOnly(); callbackInfo.TokenImpl.SetMetaData(metaData); callbackInfo.Listener.OnMetaData(callbackInfo.TokenImpl); break; } default: // We couldn't parse the response. // . It's tempting to report this to the callback, just like a disconnect. // That would probably cause an immeidate retry. // . It's tempting to reset the connection. That's what we used to do. // . We could return an empty list. That would probably clear the // screen for a previously working top list, which makes it obvious to the // user there is a problem. That makes sense for our own reports, but // we usually try to minimize that for our users. // . Note that we're not distinguishing between a parsing problem and an // unknown node type. If we add something, like Debug, and and old client // doesn't know what to do with it, the client should silently ignore it. // There isn't a great answer. So we just ignore the message. break; } } } private static RowData DecodeRow(XmlNode asXml) { RowData result = new RowData(); foreach (XmlAttribute attribute in asXml.Attributes) result.Data[attribute.LocalName] = attribute.Value; return result; } private static List DecodeRows(XmlNode asXml) { List result = new List(); foreach (XmlNode rowNode in asXml.Enum()) result.Add(DecodeRow(rowNode)); return result; } /// /// You get a new one of these each time you call Send(). You can use == or != on it /// when you get a response to make sure the response came from the correct call to Send(). /// public interface Token { /// /// Cancel the given request. It is safe to call this even after the request /// has already been canceled. /// void Cancel(); /// /// At one time this was used to distinguish between two requests made by the same /// listener. Normally you can use the Token object for that. However, in a multi- /// threaded environment this could still help. You don't know the identity of /// the Token object until Send() returns. But the result of a send might appear /// in another thread before Send() returns. /// /// This can be used for anything. It might be null. /// /// Whatever value you gave to Send(). object GetClientInfo(); /// /// /// /// This is the original request object that you called Send() on. This might /// or might not be unique, depending on the client. /// TopListRequest GetRequest(); // Initially this will return null. Once it's set it should never be null again. TopListInfo GetMetaData(); } private class TokenImpl : Token { private readonly ISendManager _sendManager; private readonly String _name; private readonly TalkWithServer.CancelToken _cancelToken; private readonly object _clientInfo; public object GetClientInfo() { return _clientInfo; } /// /// We keep _request just to make it easier for the client. We only use /// the original TopListRequest object in Send(). We copy any fields that /// we might need later. The client is free to modify this value at any /// time, so it would be bad for us to try to use this later. /// private readonly TopListRequest _request; public TopListRequest GetRequest() { return _request; } private TopListInfo _metaData; public TopListInfo GetMetaData() { return _metaData; } public void SetMetaData(TopListInfo newValue) { System.Diagnostics.Debug.Assert(null != newValue); _metaData = newValue; } public TokenImpl(ISendManager sendManager, String name, TalkWithServer.CancelToken cancelToken, object clientInfo, TopListRequest request) { _sendManager = sendManager; _name = name; _cancelToken = cancelToken; _clientInfo = clientInfo; _request = request; } public void Cancel() { _cancelToken.Cancel(); _sendManager.SendMessage(TalkWithServer.CreateMessage("command", "ms_top_list_stop", "name", _name)); } } } }