using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using TSLab.DataSource; using TSLab.Script; using TSLab.Script.Handlers; using TSLab.Script.Handlers.Options; using TSLab.Script.Realtime; using static GanovCubes.ExtTrade; namespace GanovCubes { #region CubeDescription [System.Obsolete] [HandlerCategory("Ganov Cubes. MarketData")] [HandlerName("TradesLogger", Language = "ru-RU")] [Description("Записывает данные о сделках в файл. Файлы со сделками располагаются по адресу: \"Папка ТСЛаб текущего пользователя/DataStorage/TradesLogger\" " + "Один и тот же файл может использоваться для записи сделок нескольких агентов." + " Работает только в режиме агента, записывает данные о сделках закрытых на последнем баре." + " Записывает следующие данные: Имя агента; Tag1; Tag2; Направление; Инструмент; Тикер ;Максимальный размер позиции; Сигнал входа; Дата и время входа; Цена входа; Сигнал выхода;" + " Дата и время выхода; Цена выхода; Прибыль(пункты); Прибыль(деньги); Прибыль %; Счет;" + " Прибыль в пунктах и процентах рассчитывается ТСЛаб. Прибыль в деньгах рассчитывается как Прибыль в пунктах / шаг цены * стоимость шага цены." + " В кубике два входа: первый подключается к инструменту, второй подключается к кубику \"Text\" для получения имени агента")] [HelperLink("http://forum.tslab.ru/ubb/ubbthreads.php?ubb=showflat&Main=9754&Number=86966#Post86966", "Описание на форуме ТСлаб", "ru-ru")] [InputsCount(4)] [Input(0, TemplateTypes.SECURITY, Name = "Sec")] [Input(1, TemplateTypes.DOUBLE, Name = "ComisAbs")] [Input(2, TemplateTypes.DOUBLE, Name = "Comis ")] [Input(3, TemplateTypes.STRING, Name = "AgentName")] [OutputsCount(0)] #endregion public class TradesLogger : IValuesHandlerWithNumber, IContextUses, ISecurityInput0 { /// /// Имя агента и название файла со сделками /// string _tradeName; /// /// Часть имени файла при записи сделок с опцией "AddDateToFileName" /// string _fileDate; #region Properties public IContext Context { get; set; } /// /// Имя общего файла для записи сделок /// [HandlerParameter(true, "Общее имя файла", NotOptimized = true, Name = "Имя общего файла для записи сделок", IsVisibleInBlock = false)] [Description("Устанавливает имя общего файла для записи сделок. Расширение файла указывать не требуется, всегда записывается файл с раширение \"csv\"")] public string CommonFileName { get; set; } = "Общее имя файла"; /// /// ID /// [HandlerParameter(true, "----", NotOptimized = true, Name = "ID", IsVisibleInBlock = false)] [Description("Дополнительное ID, которое необходимо указать в блоке если в скрипте используется несколько" + " инструментов. Указание ID предотвратит возможные конфликты при обращении в кэш. В случае если в качестве" + " ID указано \"----\", то будет использовано ИмяАгента+ИмяИнструмента, если указано что-либо другое, то будет" + " использовано указанное значение")] public string ID { get; set; } = "----"; /// /// Добавлять дату к имени файла /// [HandlerParameter(true, "true", NotOptimized = true, Name = "Добавлять дату к имени файла", IsVisibleInBlock = false)] [Description("При установке данной опции при записи к имени файла добавлятся дата закрытия последней сделки, то есть фактически это дата последнего бара")] public bool AddDateToFileName { get; set; } = true; /// /// Писать в один файл /// [HandlerParameter(true, "false", NotOptimized = true, Name = "Писать в один файл", IsVisibleInBlock = false)] [Description("При установке данной опции сделки всех агентов, в которые добавлен кубик с соответствующим именем файла будут писаться в один и тот же файл")] public bool UseCommonFile { get; set; } = false; /// /// Tag1 /// [HandlerParameter(true, "-", NotOptimized = true, Name = "Tag1", IsVisibleInBlock = false)] [Description("Дополнительный столбец для группировки агентов. Например \"трендовые\" и \"контртрендовые\"")] public string Tag1 { get; set; } = "-"; /// /// Tag2 /// [HandlerParameter(true, "-", NotOptimized = true, Name = "Tag2", IsVisibleInBlock = false)] [Description("Дополнительный столбец для группировки агентов. Например \"трендовые\" и \"контртрендовые\"")] public string Tag2 { get; set; } = "-"; /// /// Выводить сообщение в лог ТСЛаб /// [HandlerParameter(true, "true", NotOptimized = true, Name = "Выводить сообщение в лог ТСЛаб", IsVisibleInBlock = false)] [Description("При установке данное опции при записи каждой сделки в лог ТСЛаб выводиться сообщение," + " аналогичное записываемому в файл для дальнейшей пересылки через менедджер оповещений или целей информирования")] public bool MessageToLog { get; set; } = true; /// /// Тип сообщения /// [HandlerParameter(true, "Info", NotOptimized = true, Name = "Тип сообщения", IsVisibleInBlock = false)] [Description("Тип сообщения (Info, Warning, Error)")] public MessageType Type { get; set; } = MessageType.Info; /// /// Дополнительная пользовательская метка /// [HandlerParameter(true, "MessageTag", NotOptimized = true, Name = "Дополнительная пользовательская метка", IsVisibleInBlock = false)] [Description("Метка записывает в конец записи по каждой сделке")] public string MessageTag { get; set; } = "MessageTag"; /// /// Записывать проскальзывание по Market-приказам /// [HandlerParameter(true, "true", NotOptimized = true, Name = "Записывать проскальзывание по Market-приказам", IsVisibleInBlock = false)] [Description("При установленной опции система записывает данные по проскальзыванию на вход и выход по market-приказам. Приказ считается рыночным" + " если в его названиии имеется MK, например, LEMK, SEMK. Проскальзывание на вход и выход учитывается отдельно, соответственно, если вход был" + " по рынку, а выход - нет, то будет записано проскальзывание только на вход. Проскальзывание записывается в пунктах и будет включать в себя как" + " проскальзывание от нехватки ликвидности, так и проскальзывание от наличия спреда. Данную величину можно будет учитывать при фактическом" + " тестировании стратегий. Проскальзывание считается отрицательным если произошло не в направлении сделки, например, если есть вход дпо рынке для" + " лонговой позиции, то если зашли по цене выше, чем закрытие свечи, где сформировался сигнал, то считаем проскальзывание отрицательным, если зашли" + " по цене ниже, чем планировалось, то положительным. На выход все наоборот: вышли ниже, чем планировалось - отрицательно проскальзование, вышли" + " лучше, чем планировалось - проскальзывание положительное")] public bool WriteSlippageOnMarketOrders { get; set; } = true; #endregion public void Execute(ISecurity sec, int bar) => Execute(sec, 0, 0, "", bar); public void Execute(ISecurity sec, double comisAbs, int bar) => Execute(sec, comisAbs, 0, "", bar); public void Execute(ISecurity sec, double comisAbs, double comisPct, int bar) => Execute(sec, comisAbs, comisPct, "", bar); public void Execute(ISecurity sec, double comisAbs, double comisPct, string agentName, int bar) { if (!Context.Runtime.IsAgentMode || bar != sec.Bars.Count - 1 || !Context.IsLastBarClosed || !Context.IsLastBarUsed || bar == 0) return; var dictionary = new Dictionary { {"$UserMessageTag", MessageTag ?? string.Empty} }; _tradeName = agentName != "" ? agentName : Context.Runtime.TradeName; _fileDate = sec.Bars[bar - 1].Date.ToString("yyyyMMdd"); if (ID == "----") ID = $"{_tradeName}_{sec.CacheName}"; Tag1 ??= ""; Tag2 ??= ""; WritePendedTradesToFile(); if (IsCurrentBarNotWritten()) WriteLastTradesData(); // Метод производит чтение и запись данных о сделках в кеш агента void WritePendedTradesToFile() { if (!(Context.LoadObject($"{ID} TradesLoging postponed trades", true) is List obj) || obj.Count == 0) return; if (MessageToLog) Context.Log("Loger: записываю отложенные данные", MessageType.Warning, true); WriteTradeDataToFile(obj, true); } // Метод производит запись сведений о последней сделке void WriteLastTradesData() { var posList = sec.Positions.GetClosedForBar(Context.BarsCount - 1).ToList(); var msgList = new List(); foreach (var pos in posList) if (pos.IsActiveForBar(Context.BarsCount - 2) && !pos.IsActiveForBar(Context.BarsCount - 1) && !pos.IsVirtual && !pos.IsVirtualClosed) { var direction = pos.IsLong ? "Long" : "Short"; var profit = pos.Profit(); var profitMoney = sec.FinInfo.StepPrice != null || sec.FinInfo.StepPrice != 0 ? $"{pos.Profit() / sec.Tick * sec.FinInfo.StepPrice}" : ""; var portfolioName = Context.Runtime.IsAgentMode ? $"{(sec as ISecurityRt).PortfolioName}" : ""; var entrySignalName = pos.EntrySignalName; var entrySlippage = !string.IsNullOrEmpty(entrySignalName) && (entrySignalName.Contains("LEMK") || entrySignalName.Contains("SEMK")) ? sec.Bars[pos.EntryBarNum - 1].Close - pos.EntryPrice : 0 * (pos.IsLong ? 1 : -1); var exitSignalName = pos.ExitSignalName; var exitSlippage = !string.IsNullOrEmpty(exitSignalName) && (exitSignalName.Contains("LXMK") || exitSignalName.Contains("SXMK")) ? pos.ExitPrice - sec.Bars[pos.ExitBarNum - 1].Close : 0 * (pos.IsLong ? 1 : -1); /* Рассчитываем общую сумму движений по трейду для учета общей суммы комисии. Делаем допушение, что во время всего трейда комиссия не менялась. для учета изменения позиции на баре входа и баре выхода вычитаем из кол-ва лотов после бара входа и до бара выхода количество лотов, которые произошли вследстие изменения позиции через блоки "изменить по" */ var sumOnEnter = pos.EntryPrice * Math.Abs(pos.GetShares(pos.EntryBarNum) - pos.ChangeInfos.Where(posChange => posChange.EntryBarNum == pos.EntryBarNum).Sum(posChange => posChange.SharesChange)); var sumOnExit = pos.ExitPrice * Math.Abs(pos.GetShares(pos.EntryBarNum - 1) - pos.ChangeInfos.Where(posChange => posChange.ExitBarNum == pos.ExitBarNum).Sum(posChange => posChange.SharesChange)); var sumOneChange = pos.ChangeInfos.Sum(posChange => Math.Abs(posChange.SharesChange) * pos.EntryBarNum != -1 ? pos.EntryPrice : pos.ExitPrice); var totalTradeSum = sumOnEnter + sumOneChange + sumOnExit; msgList.Add(_tradeName + ";" + Tag1 + ";" + Tag2 + ";" + direction + ";" + sec.FinInfo.Security.TradePlace.Name + ";" + sec.GetAccountName() + ";" + (sec.FinInfo.ActiveType == ActiveType.Futures ? sec.FinInfo.Security.FullName : sec.Symbol) + ";" + GetSecTicket() + ";" + pos.MaxShares + ";" + pos.EntrySignalName + ";" + sec.Bars[pos.EntryBarNum].Date + ";" + pos.EntryPrice + ";" + pos.ExitSignalName + ";" + sec.Bars[pos.ExitBarNum].Date + ";" + pos.ExitPrice + ";" + profit + ";" + profitMoney + ";" + Math.Round(pos.ProfitPct(), 3, MidpointRounding.ToEven) + ";" + portfolioName + ";" + entrySlippage + ";" + exitSlippage + ";" + comisAbs + ";" + comisPct + ";" + totalTradeSum); if (MessageToLog) Context.Log("Loger: закрыта позиция: " + msgList[msgList.Count - 1], Type, true, dictionary); } Context.StoreObject($"{ID} TradesLoging LastLogingTime", sec.Bars[Context.BarsCount - 1].Date, true); if (msgList.Count != 0) WriteTradeDataToFile(msgList, false); } // Метод определяет являтся ли последняя запись по данному агенту записью текущего бара bool IsCurrentBarNotWritten() { var cacheData = Context.LoadObject($"{ID} TradesLoging LastLogingTime", true); DateTime time = default; if (cacheData != null) time = (DateTime)cacheData; return time < sec.Bars[Context.BarsCount - 1].Date; } /// /// Метод получает тикет инструмента для он-лайн и оф-лайн режима работы /// /// Тикет инструмента, либо сообщение "Off-Line, NoTicket" string GetSecTicket() { try { return sec.FinInfo.Security.Id; } catch { return "Off-Line, NoTicket"; } } } /// /// Метод производит запись файлов в лог /// /// Коллекция сообщений о закрытых сделках /// /// public void WriteTradeDataToFile(List msgCollection, bool fromCache) { var fileName = $"{(AddDateToFileName ? $"_{_fileDate}" : "")}_{(UseCommonFile ? CommonFileName : _tradeName)}.csv"; var filePath = GetFileAddress("TradesLogger", fileName); try { var fileNotExists = !File.Exists(filePath); using StreamWriter sw = new StreamWriter(filePath, true); if (fileNotExists) sw.WriteLine("AgentName;Tag1;Tag2;Direction;TradePlace;Account;Sec;Ticket;MaxShares;EntrySignal;EntryDate;" + "EntryPrice;ExitSignal;ExitDate;ExitPrice;Profit;ProfitMoney;ProfitPct;Account;EntrySlp;ExitSlp;ComisAbs;ComisPct;TotalTradeSum"); msgCollection.ForEach(msg => sw.WriteLine(msg)); msgCollection.Clear(); } catch (Exception e) { AbortiveWritingProcedure(e.Message); } // Процедура осуществляет необходимые в случае неудачи записи в файл void AbortiveWritingProcedure(string errMessage) { if (MessageToLog) Context.Log($"Не удалось записать сделки, ошибка: {errMessage}", MessageType.Warning, true); if (!fromCache) { var cacheData = Context.LoadObject($"{ID} TradesLoging postponed trades", true); var messages = (List)cacheData ?? new List(); msgCollection.ForEach(msg => messages.Add(msg)); Context.StoreObject($"{ID} TradesLoging postponed trades", messages, true); } } } } }