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));
}
}
}
}