using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; using System.Xml; using System.Xml.Serialization; using TradeIdeas.MiscSupport; using TradeIdeas.ServerConnection; using TradeIdeas.TIProData.Interfaces; using TradeIdeas.XML; namespace TradeIdeas.TIProData { /// /// This class requests a grid from the server. This takes care of a lot of details, like automatic /// retries. The public interface to this class should only be called from the GUI thread, and the /// callback will return in the GUI thread. /// /// This required data from the TIQ server. This will only work if you are connected to the proxy. /// /// This could be used in other places. I almost put this into the TIProData library. But this /// doesn't follow the conventions of that library. In particular, this class uses the Control /// class, and that library doesn't reference any GUI stuff, so it wouldn't compile. /// public class SingleGridRequest { [XmlRoot("API")] public class SingleGridResponse { [XmlAttribute("symbol")] public string Symbol; [XmlAttribute("type")] public string Type; [XmlText] public string CsvData; }; public class Response { public string Symbol; public string CsvData; public bool IsPrimarySymbol { get; set; } public bool IsComparisonSymbol => !IsPrimarySymbol; public bool IsValid => !string.IsNullOrEmpty(Symbol) && !string.IsNullOrEmpty(CsvData); }; public readonly string PrimarySymbol; public List ComparisonSymbols; public string AllSymbolsMessageRequest { get { var allSymbols = new List { PrimarySymbol }; if (ComparisonSymbols != null && ComparisonSymbols.Any()) { allSymbols.AddRange(ComparisonSymbols); } return string.Join(" ", allSymbols); } } public int ExpectedCallbackCount { get { var primarySymbolCount = 1; var comparisonSymbolCount = ComparisonSymbols != null ? ComparisonSymbols.Count() : 0; return primarySymbolCount + comparisonSymbolCount; } } private Dictionary _message; private Control _owner; private ISendManager _sendManager; private Action _callBack; /// /// Send a request to the server. /// /// Request data for this symbol. /// Probably the output of PrototypeEditor.GetTcl(). /// We don't use the exact values of row_count or at_time in /// the prototype. You can leave these blank. /// You want this many good rows of data. If you set pack=0 /// in the prototype you might get a lot of empty candles mixed /// in with the output. Either way, the empty candles don't /// count toward row_count /// We should grab this many additional candles before the candles /// that we plan to show someone. For example, if you are including a 5 period SMA, that field /// might not work well for the first 5 candles of the result. In this case set row_count to /// the number of rows you plan to display, and set extra_row_count to 5. The default /// value for this is 0. Like row_count this only countss candles with valid data, regardless of the /// "pack" setting in the "prototype" field. /// If you already have some data, but you are requesting /// more history, start_before should be the start time /// of the oldest candle you already have. The result will /// start at the candle immediately before this one and go /// backward as far as it needs to. The default is the /// current time. If you don't fill this in the server will /// give you the most recent data it has. /// Allows using the chart_data function /// to return all bars since a certain date. /// Some indicators are anchored at a certain time (anchored /// VWAP) so using extra_row_count won't work well. Use this /// parameter to tell chart_data to go back at least to this /// time. /// Use this to send the request. /// /// Use this to move server responses into the correct thread. Also, if this is a /// form and the form closes, the callback and any automatic retries are /// automatically canceled. If this is null, everything will still work except for /// the automatic cancel. /// /// /// This will be called when the data comes from the server. This will only be called /// once. This will be called in the GUI thread. /// public SingleGridRequest(string primarySymbol, string prototype, int rowCount, int extraRowCount, DateTime? startBefore, DateTime? sinceDate, DateTime? oldestAnchor, ISendManager sendManager, Control owner, Action callBack, List comparisonSymbols = null) { PrimarySymbol = primarySymbol; ComparisonSymbols = comparisonSymbols; _owner = owner ?? new Control(); _sendManager = sendManager; _callBack = callBack; string startDebug = ""; long? startBeforeTimeT = null; if (startBefore.HasValue) { startBeforeTimeT = ServerFormats.ToTimeT(startBefore); startDebug = startBefore.Value.ToString("yyyy-MM-dd hh:mm"); } string sinceDebug = ""; long? sinceDateTimeT = null; if (sinceDate.HasValue) { sinceDateTimeT = ServerFormats.ToTimeT(sinceDate); sinceDebug = sinceDate.Value.ToString("yyyy-MM-dd hh:mm"); } long? oldestAnchorTimeT = null; if (oldestAnchor.HasValue) oldestAnchorTimeT = ServerFormats.ToTimeT(oldestAnchor.Value); const string COMMAND = "candles_command"; const string SUB_COMMAND = "chart_data"; var message = new object[] { "command", COMMAND, "subcommand", SUB_COMMAND, "prototype", prototype, "symbols", this.AllSymbolsMessageRequest, "start_before", startBeforeTimeT, "at_least_from", oldestAnchorTimeT, "since", sinceDateTimeT, "row_count", rowCount, "extra_row_count", extraRowCount }; _cancelTokenForMessageRequests = new TalkWithServer.CancelToken(); _message = TalkWithServer.CreateMessage(message); _sendManager.SendMessage(_message, GetDataResponse, streaming: true, cancelToken: _cancelTokenForMessageRequests); } // Cancel any callbacks. Unlike the TalkWithServer messages, this takes // effect immediately. public void Abort() { _callBack = null; _message?.Clear(); _message = null; _sendManager = null; } private TalkWithServer.CancelToken _cancelTokenForMessageRequests; public void CancelListenerCallback() { if (null != _cancelTokenForMessageRequests) { _sendManager.CancelMessage(_cancelTokenForMessageRequests); _cancelTokenForMessageRequests = null; } } private void GetDataResponse(byte[] body, object clientId) { BeginInvokeIfRequired(_owner, (MethodInvoker)delegate { ResponseInThread(body); }); } private int CallbackCount = 0; private void ResponseInThread(byte[] body) { if (null == _callBack) // Aborted return; if (null == body) { // Communication problem. Try again. _sendManager.SendMessage(_message, GetDataResponse); // Reset our CallbackCount CallbackCount = 0; } else { var response = PopulateResponseSymbolData(body); if (response.IsValid) { CallbackCount++; } //If we've received the last CallBack response, cancel the message reqeust listener if (CallbackCount == ExpectedCallbackCount) { CancelListenerCallback(); } _callBack(response); } } private Response PopulateResponseSymbolData(byte[] body) { var xmlDocument = XmlHelper.Get(body); var serializer = new System.Xml.Serialization.XmlSerializer(typeof(SingleGridResponse)); SingleGridResponse singleGridResponse = null; using (XmlReader reader = new XmlNodeReader(xmlDocument)) { singleGridResponse = (SingleGridResponse)serializer.Deserialize(reader); } var isPrimarySymbol = singleGridResponse.Symbol == this.PrimarySymbol; var csvData = singleGridResponse.Type == "result" ? singleGridResponse.CsvData : string.Empty; var response = new Response { Symbol = singleGridResponse.Symbol, CsvData = csvData, IsPrimarySymbol = isPrimarySymbol, }; return response; } private static void BeginInvokeIfRequired(Control control, MethodInvoker code) { if (control.InvokeRequired) { try { // Thread.Sleep(5000); // Test for a possible race condition. control.BeginInvoke(code); } catch (InvalidOperationException) { // This probably means that the window was disposed of after the call to InvokeRequired // but before the call to Invoke(). // Note: Most of our code does not check for this race condition. It probably should. // This probably doesn't happen very often. } } else if (!control.IsDisposed) code(); } } }