using System; using System.Collections.Generic; using System.Text; using System.Xml; using System.Diagnostics; using System.Web; using TradeIdeas.ServerConnection; using TradeIdeas.XML; using TradeIdeas.TIProData.Interfaces; // OddsMakerManager is heavily based on HistoryManager. // // An oddsmaker both request and receive data in a similar way. But... // - An OddsMaker request cannot be restarted. // - We can cancel the requests. We ignored that at first in history. // It's even more important here. // // Some of the fields in the message seem a little inconsistent. For example // Success $ and % share a double value in the message as they are mutually // exclusive. Whereas for Trailing Bars vs Trailing Percent exit conditions // there are separate field values in the message. Same thing for things like // exit timeouts, minutes after entry, minutes before close, at open days, and // after close days. Right now all that is exposed through the OddsMakerRequest // struct. You have to explicitly set a value regardless of whether it holds // any real meaning. // // ProfitTarget and StopLoss work a different way still. If they are not // present in the message, that conveys that they are unchecked. The presence // of a value conveys that they are checked and the value sent is used. To // set the unchecked state in the API you set these values to null. // // Maybe these inconsistencies should be abstracted away for the purposes of an // API. namespace TradeIdeas.TIProData { public delegate void OddsMakerProgress(string progress, OddsMaker sender); public delegate void OddsMakerProgressXml(XmlNode progress, OddsMaker sender); public delegate void OddsMakerStatus(OddsMaker sender); public delegate void OddsMakerDebug(string debug, OddsMaker sender); public delegate void OddsMakerCSVRow(string csv, OddsMaker sender); public delegate void OddsMakerCSV(string csv, OddsMaker sender); public delegate void OddsMakerCsvUrl(string url, OddsMaker sender); public enum OddsMakerDisposition { NotStarted, Working, Done } public enum TimeoutType { Close, Open, MinutesAfterEntry, FutureClose } public enum ExitConditionType { None, Alert, TrailingBars, TrailingPercent } public enum SelectedLocation { US, Canada } public enum OddsMakerColumnConfiguration { /// /// Use filters that are shown as columns in the strategy /// Strategy, /// /// Used to mean your favorite subset of columns. Now means all available columns. /// Favorites } public class OddsMakerRequest { public string TargetFilter { get; set; } private bool _useTargetFilter = false; public bool UseTargetFilter { get { return _useTargetFilter; } set { _useTargetFilter = value; } } public string StopFilter { get; set; } private bool _useStopFilter = false; public bool UseStopFilter { get { return _useStopFilter; } set { _useStopFilter = value; } } public OddsMakerRequest() { } public bool XmlMode { get; set; } public bool SuccessDirectionUp { get; set; } public bool SuccessTypePercent { get; set; } public double SuccessMinMove { get; set; } public int EntryTimeStart { get; set; } public int EntryTimeEnd { get; set; } public TimeoutType TimeoutType { get; set; } public int BeforeCloseMinutes { get; set; } public int TimeoutMinutes { get; set; } public int AtOpenDays { get; set; } public int AtCloseDays { get; set; } public int DaysOfTest { get; set; } public int SkipDays { get; set; } public double? ProfitTarget { get; set; } public double? StopLoss { get; set; } public bool StopLossWiggle { get; set; } public string EntryCondition { get; set; } public ExitConditionType ExitConditionType { get; set; } public double ExitConditionTrailingStop { get; set; } public string ExitConditionAlert { get; set; } public SelectedLocation Location { get; set; } public bool RequestCsvFile { get; set; } public OddsMakerColumnConfiguration ColumnConfig { get; set; } public void Save(XmlNode description) //save to xml file (Layouts) { try { XmlNode om = description.NewNode("ODDSMAKER_REQUEST"); om.SetProperty("SUCCESS_DIRECTION_UP", SuccessDirectionUp); om.SetProperty("SUCCESS_TYPE_PERCENT", SuccessTypePercent); om.SetProperty("SUCCESS_MIN_MOVE", SuccessMinMove); om.SetProperty("ENTRY_TIME_START", EntryTimeStart); om.SetProperty("ENTRY_TIME_END", EntryTimeEnd); om.SetProperty("TIMEOUT_TYPE", TimeoutType); om.SetProperty("BEFORE_CLOSE_MINUTES", BeforeCloseMinutes); om.SetProperty("TIMEOUT_MINUTES", TimeoutMinutes); om.SetProperty("AT_OPEN_DAYS", AtOpenDays); om.SetProperty("AT_CLOSE_DAYS", AtCloseDays); om.SetProperty("DAYS_OF_TEST", DaysOfTest); om.SetProperty("SKIP_DAYS", SkipDays); if (null != ProfitTarget) om.SetProperty("PROFIT_TARGET", ProfitTarget); om.SetProperty("TARGET_FILTER", TargetFilter); om.SetProperty("USE_TARGET_FILTER", UseTargetFilter); if (null != StopLoss) om.SetProperty("STOP_LOSS", StopLoss); om.SetProperty("STOP_FILTER", StopFilter); om.SetProperty("USE_STOP_FILTER", UseStopFilter); om.SetProperty("STOP_LOSS_WIGGLE", StopLossWiggle); om.SetProperty("ENTRY_CONDITION", EntryCondition); om.SetProperty("EXIT_CONDITION_TYPE", ExitConditionType); om.SetProperty("EXIT_CONDITION_TRAILING_STOP", ExitConditionTrailingStop); om.SetProperty("EXIT_CONDITION_ALERT", ExitConditionAlert); om.SetProperty("LOCATION", Location); om.SetProperty("REQUEST_CSV_FILE", RequestCsvFile); om.SetProperty("COLUMN_CONFIG", ColumnConfig); } catch (Exception e) { string debugView = e.ToString(); } } public void Load(XmlNode source) { XmlNode om = source.Node("ODDSMAKER_REQUEST"); SuccessDirectionUp = om.Property("SUCCESS_DIRECTION_UP", true); SuccessTypePercent = om.Property("SUCCESS_TYPE_PERCENT", false); SuccessMinMove = om.Property("SUCCESS_MIN_MOVE", 0.01); EntryTimeStart = om.Property("ENTRY_TIME_START", 0); EntryTimeEnd = om.Property("ENTRY_TIME_END", 30); TimeoutType = om.PropertyEnum("TIMEOUT_TYPE", TradeIdeas.TIProData.TimeoutType.MinutesAfterEntry); BeforeCloseMinutes = om.Property("BEFORE_CLOSE_MINUTES", 1); TimeoutMinutes = om.Property("TIMEOUT_MINUTES", 1); AtOpenDays = om.Property("AT_OPEN_DAYS", 1); AtCloseDays = om.Property("AT_CLOSE_DAYS", 1); DaysOfTest = om.Property("DAYS_OF_TEST", 1); SkipDays = om.Property("SKIP_DAYS", 1); ProfitTarget = om.Property("PROFIT_TARGET", 0); TargetFilter = om.Property("TARGET_FILTER", ""); UseTargetFilter = om.Property("USE_TARGET_FILTER", false); StopLoss = om.Property("STOP_LOSS", 0); StopFilter = om.Property("STOP_FILTER", ""); UseStopFilter = om.Property("USE_STOP_FILTER", false); StopLossWiggle = om.Property("STOP_LOSS_WIGGLE", false); EntryCondition = om.Property("ENTRY_CONDITION", ""); ExitConditionType = om.PropertyEnum("EXIT_CONDITION_TYPE", TradeIdeas.TIProData.ExitConditionType.None); ExitConditionTrailingStop = om.Property("EXIT_CONDITION_TRAILING_STOP", 1); ExitConditionAlert = om.Property("EXIT_CONDITION_ALERT", ""); Location = om.PropertyEnum("LOCATION", SelectedLocation.US); RequestCsvFile = om.Property("REQUEST_CSV_FILE", true); ColumnConfig = om.PropertyEnum("COLUMN_CONFIG", OddsMakerColumnConfiguration.Strategy); } } public interface OddsMaker { event OddsMakerProgress OddsMakerProgress; event OddsMakerProgressXml OddsMakerProgressXml; event OddsMakerStatus OddsMakerStatus; event OddsMakerDebug OddsMakerDebug; event OddsMakerCSV OddsMakerCSV; event OddsMakerCSVRow OddsMakerCSVRow; event OddsMakerCsvUrl OddsMakerCsvUrl; OddsMakerDisposition OddsMakerDisposition { get; } bool CanStart(); void Start(); // Call this to stop the request early. This will save resources on the server and // client side. void Stop(); } public class OddsMakerManager { public static volatile bool LotsOfResults = false; private object _mutex = new object(); private Dictionary _windows = new Dictionary(); private class OddsMakerImpl : OddsMaker { private static Int64 _lastId = 0; private readonly string _id; private OddsMakerManager _manager; private OddsMakerRequest _request; private StringBuilder _csvData; public OddsMakerImpl(OddsMakerRequest request, OddsMakerManager manager) { lock (typeof(OddsMakerImpl)) { _lastId++; _id = "OM" + _lastId.ToString(); } _request = request; _manager = manager; } public void SendRequestNow() { // Assume we are already in the mutex. List message = new List(); message.Add("command"); message.Add("oddsmaker_new_request"); message.Add("entry_condition"); message.Add(_request.EntryCondition); message.Add("success_direction"); message.Add(_request.SuccessDirectionUp?"+":"-"); message.Add("success_type"); message.Add(_request.SuccessTypePercent?"%":"$"); message.Add("success_min_move"); message.Add(_request.SuccessMinMove); message.Add("at_open_days"); message.Add(_request.AtOpenDays); message.Add("at_close_days"); message.Add(_request.AtCloseDays); message.Add("timeout_type"); switch (_request.TimeoutType) { case TimeoutType.FutureClose: message.Add("future_close"); break; case TimeoutType.MinutesAfterEntry: message.Add("minutes"); break; case TimeoutType.Open: message.Add("open"); break; case TimeoutType.Close: default: message.Add("close"); break; } message.Add("timeout_minutes"); message.Add(_request.TimeoutMinutes); message.Add("before_close_minutes"); message.Add(_request.BeforeCloseMinutes); if (_request.UseTargetFilter) { message.Add("use_target_filter"); message.Add("1"); message.Add("target_filter"); message.Add(_request.TargetFilter); } else if (null != _request.ProfitTarget) { message.Add("profit_target"); message.Add(_request.ProfitTarget); } if (_request.UseStopFilter) { message.Add("use_stop_filter"); message.Add("1"); message.Add("stop_filter"); message.Add(_request.StopFilter); } else if (null != _request.StopLoss) { message.Add("stop_loss"); message.Add(_request.StopLoss); } message.Add("stop_loss_wiggle"); message.Add(_request.StopLossWiggle? "1":""); message.Add("exit_condition_type"); string exitConditionValue = ""; switch (_request.ExitConditionType) { case ExitConditionType.Alert: message.Add("manual"); exitConditionValue = _request.ExitConditionAlert; break; case ExitConditionType.TrailingBars: case ExitConditionType.TrailingPercent: message.Add("auto"); exitConditionValue = "TS"; if (_request.ExitConditionType == ExitConditionType.TrailingPercent) exitConditionValue += "P"; else exitConditionValue += 'V'; if (_request.SuccessDirectionUp) exitConditionValue += "D"; else exitConditionValue += "U"; exitConditionValue = "form=1&Sh_" + exitConditionValue + "=on&Q" + exitConditionValue + "=" + HttpUtility.UrlEncode(_request.ExitConditionTrailingStop.ToString()); break; case ExitConditionType.None: message.Add("none"); break; } message.Add("exit_condition"); message.Add(exitConditionValue); message.Add("entry_time_start"); message.Add(_request.EntryTimeStart); message.Add("entry_time_end"); message.Add(_request.EntryTimeEnd); message.Add("days_of_test"); message.Add(_request.DaysOfTest); message.Add("days_to_skip"); message.Add(_request.SkipDays); message.Add("location"); switch (_request.Location) { case SelectedLocation.Canada: message.Add("Canada"); break; case SelectedLocation.US: default: message.Add("US"); break; } message.Add("xml_mode"); message.Add(_request.XmlMode?"1":"0"); message.Add("window_id"); message.Add(_id); message.Add("return_csv_file"); message.Add(_request.RequestCsvFile ? "streaming" : "0"); if (_request.RequestCsvFile) _csvData = new StringBuilder(); else _csvData = null; if (LotsOfResults) { message.Add("lots_of_results"); message.Add("1"); } _manager.OpenChannel(); _manager._sendManager.SendMessage( TalkWithServer.CreateMessage(message.ToArray())); } public bool CanStart() { return OddsMakerDisposition == OddsMakerDisposition.NotStarted; } public void Start() { lock (_manager._mutex) { Debug.Assert(CanStart()); OddsMakerDisposition = OddsMakerDisposition.Working; _manager._windows.Add(_id, this); SendRequestNow(); } SendStateChanged(); } public void Stop() { bool wasWorking; lock (_manager._mutex) { wasWorking = OddsMakerDisposition == OddsMakerDisposition.Working; OddsMakerDisposition = OddsMakerDisposition.Done; if (wasWorking) _manager._windows.Remove(_id); } if (wasWorking) { // Tell server to stop _manager._sendManager.SendMessage( TalkWithServer.CreateMessage( "command", "oddsmaker_cancel_request", "window_id", _id)); // For consistency, notify the client. He's probably the one who requested // this (so it's probably a reentrant callback) and there isn't much he // can do about this. But maybe he grays out a menu item or something. SendStateChanged(); } } private void SendStateChanged() { OddsMakerStatus callback = OddsMakerStatus; if (null != OddsMakerStatus) try { OddsMakerStatus(this); } catch { //keep working } } private XmlNode _unknownMessageTypeDebug; public void ProcessServerMessage(XmlNode message) { switch (message.Property("TYPE")) { case "progress": { if (_request.XmlMode) { // XML mode OddsMakerProgressXml callback = OddsMakerProgressXml; if (null != callback) try { callback(message.Node("MESSAGE").Node(0), this); } catch { } } else { // RTF mode. OddsMakerProgress callback = OddsMakerProgress; if (null != callback) try { string progress = message.InnerXml; progress = @"{\rtf\ansi" + progress + "}"; callback(progress, this); } catch { // Keep working even if the callback fails. } } break; } case "finished": { bool stateChanged; lock (_manager._mutex) { stateChanged = OddsMakerDisposition != OddsMakerDisposition.NotStarted; OddsMakerDisposition = OddsMakerDisposition.Done; _manager._windows.Remove(_id); } if (stateChanged) SendStateChanged(); break; } case "debug": { OddsMakerDebug callback = OddsMakerDebug; if (null != callback) try { callback(message.InnerXml, this); } catch { // Keep working even if the callback fails. } break; } case "csv_list": {/* This should never happen any more. If you set return_csv_file=1 the server * will send this message. If you set return_csv_file=streaming, you will see * "csv_row" and "csv_header" messages instead. There is not way for a client * to request the new format, and fall back on the old format if the the server * is old. If the client requests "streaming" from an old server, the server * will send no CSV file at all. (The server should always be able to handle * and old client. The reverse is not true.) */ Debug.WriteLine("Unexpected message from the server: “csv_list”"); OddsMakerCSV callback = OddsMakerCSV; if (null != callback) try { callback(message.InnerText.Replace("\n", "\r\n"), this); } catch { // Keep working even if the callback fails. } break; } case "csv_row": { if (null == _csvData) Debug.WriteLine("Unexpected message from the server: “csv_row”"); else { // Is this okay? We're just putting them together in the order we receive them. // The csv_list version was sorted on the server side. string csv = message.InnerText.Replace("\n", "\r\n"); lock (_manager._mutex) { _csvData.Append(csv); } OddsMakerCSVRow callback = OddsMakerCSVRow; if (null != callback) callback(csv, this); } break; } case "csv_header": { StringBuilder csvData; lock (_manager._mutex) { csvData = _csvData; _csvData = null; } if (null == csvData) Debug.WriteLine("Unexpected message from the server: “csv_header”"); else { String whole = message.InnerText.Replace("\n", "\r\n") + csvData.ToString(); OddsMakerCSV callback = OddsMakerCSV; if (null != callback) try { callback(whole, this); } catch (Exception ex) { // Keep working even if the callback fails. Debug.WriteLine("Error in csv_header callback: " + ex); } } break; } case "csv_location": { // This is a quick and dirty solution. I don't expect it to last. I think the // right answer will probably be splitting the CSV file into a lot of small // pieces, but nothing has been decided. OddsMakerCsvUrl callback = OddsMakerCsvUrl; if (null != callback) try { callback(message.InnerText, this); } catch { // Keep working even if the callback fails. } break; } default: // This is reserved. Today if we see it it's probably a bug. But to // allow for expansion we should ignore it. _unknownMessageTypeDebug = message; break; } } public OddsMakerDisposition OddsMakerDisposition { get; private set; } public string DebugData { get; private set; } public string ProgressData { get; private set; } public event OddsMakerProgress OddsMakerProgress; public event OddsMakerProgressXml OddsMakerProgressXml; public event OddsMakerDebug OddsMakerDebug; public event OddsMakerCSV OddsMakerCSV; public event OddsMakerCSVRow OddsMakerCSVRow; public event OddsMakerCsvUrl OddsMakerCsvUrl; public event OddsMakerStatus OddsMakerStatus; } public OddsMaker GetOddsMaker(OddsMakerRequest request) { return new OddsMakerImpl(request, this); } private readonly ISendManager _sendManager; internal OddsMakerManager(ISendManager sendManager) { _sendManager = sendManager; } private bool _channelIsOpen; private void OpenChannel() { // Assume we are already in the mutex. if (_channelIsOpen) return; _channelIsOpen = true; _sendManager.SendMessage( TalkWithServer.CreateMessage("command", "oddsmaker_listen"), Response, streaming: true); } int _deletedOddsMakerDataCount = 0; private void Response(byte[] body, object unused) { XmlNode wholeMessage = XmlHelper.Get(body); if (null == wholeMessage) { lock (_mutex) { _channelIsOpen = false; foreach (KeyValuePair kvp in _windows) kvp.Value.SendRequestNow(); return; } } foreach (XmlNode oddsMakerData in wholeMessage.Node(0).Enum()) { OddsMakerImpl window = null; string id; lock (_mutex) { id = oddsMakerData.Property("WINDOW"); if (_windows.ContainsKey(id)) window = _windows[id]; } if (null == window) { // This shouldn't happen very often. But sometimes it's unavoidable. // We could have sent a cancel message and the server was sending us // data, and the two messages crossed. We send another cancel message // here, just to be safe. _deletedOddsMakerDataCount++; TalkWithServer.CreateMessage("command", "oddsmaker_cancel_request", "window_id", id); } else // parse the data and send it out. window.ProcessServerMessage(oddsMakerData); } } } }