using System; using System.ComponentModel; using System.IO; using System.Linq; using TSLab.DataSource; using TSLab.Script.Handlers; using TSLab.Script.Handlers.Options; using static GanovCubes.ExtTrade; using static GanovCubes.FileLogger; namespace GanovCubes { #region CubeDescription [HandlerCategory("Ganov Cubes. Filters")] [HelperName("BalanceControl", Language = "ru-RU")] [Description("Кубик работает только в режиме агента, в режиме лаборатории всегда возвращается ноль. Данный кубик получает данные со входа, сравнивает их суммой," + " полученной на прошлом пересчете и если разница составляет более указанного процента, то возвращается ранее полученную сумму. Таким образом, предотвращается" + " передачe в систему существенно неверных данных, присутствие которых может привести к покупке активов на сумму, выходящую за имеющиеся свободные средства." + " Кубик рекомендуется запускать в отдельном агенте с пересчетом по метроному и передавать данные в другие агенты через механизм глобального кеша, таким образом, " + " у всех агентов будет актуальная и отфильтрованная информация о текущем балансе счета. Кубик для хранения прошлых данных использует локальный кеш и файл на диске" + " по указанном пути. Обращение к файлу происходит только в случае если кеш пустой, в остальных случаях кубик использует кеш. В файл данные" + " записываются 1 раз в 5 минут (настройкой \"периодичность записи в файл\" можно изменить интервал перезаписи). Данные сохраняются в файл, расположенный в папке: \"AccountData\"" + "\nКубик имеет следующие входы: " + "\n======================" + "\nBalance - баланс счета или любое иное значение типа double, которое необходимо контролировать" + "\nEnabled - вход типа bool, на который можно подавать условия включения кубика в работу, в случае если условие false, то кубик пропускает данные со входа на выход" + " без какого-либо контроля. В случае если вход не подключается, то считается, что на вход подано True и кубик работает в режиме контроля данных. Для данного входа также" + " предусмотрен механизм задержки начала работы, регулируется настройкой входа \"Задержка включения\"" + "\nActivationDelay - Время в минутах до включения механизма контроля с момента возникновения сигнала true на входе в кубик. В случае, если вход не подключен " + " значение принимаете равным нулю. В этом случае кубик сразу работает в режиме контроля данных")] [HelperLink("http://forum.tslab.ru/ubb/ubbthreads.php?ubb=showflat&Number=87028&page=1", "Страница на форуме TSLab", "ru-ru")] [InputsCount(3)] [Input(0, TemplateTypes.DOUBLE, Name = "Balance")] [Input(1, TemplateTypes.BOOL, Name = "Enabled")] [Input(2, TemplateTypes.DOUBLE, Name = "ActivationDelay")] [OutputsCount(1)] [OutputType(TemplateTypes.DOUBLE)] #endregion public class BalanceControl : IValuesHandlerWithNumber, IDoubleInput0, IDoubleReturns, IContextUses { #region Fields double _retValue = 0; string _filePath; #endregion #region Properties public IContext Context { get; set; } /// /// ID Контроллера /// [HandlerParameter(true, "ID#", NotOptimized = true, Name = "ID Контроллера", IsVisibleInBlock = false)] [Description("Произвольный текст. ID необходимо указывать в случае если в рамках одного скрипта работает два и" + " более контроллера баланса, т.к. они оба используют локальный кеш, то при наличии одной и той же метки" + " будут перетирать данные друг друга")] public string ID { get; set; } = "ID#"; /// /// % несоответствия баланса /// [HandlerParameter(true, "50", NotOptimized = true, Name = "% несоответствия баланса", IsVisibleInBlock = false)] [Description("Процент, на который различается текущий и получаемый на вход баланс. В случае если разница больше" + " указанного процента, то система использует ранее сохраненный баланс")] public double BalanceDifferencePct { get; set; } = 50; /// /// Писать в файл /// [HandlerParameter(true, "true", NotOptimized = true, Name = "Писать в файл", IsVisibleInBlock = false)] [Description("При установке данной опции с указанной периодичностью (по умолчанию 5 минут) данные по входной величине будут" + " писаться в файл, откуда будут браться в случае если в локальном кеше нет данных или данные равны нулю. Данная настройка" + " по сути является избыточной, т.к.у локального кеша есть функционал сохранения на диск даже при отключении ТСЛаб, но когда" + " вопрос стоит о деньгах, то лишняя \"страховка\" не помешает. Если такая необходимость отсутствует, то данную настройку" + " можно отключить. По умолчанию настройка включена")] public bool WriteToFile { get; set; } = true; /// /// Период перезаписи файла /// [HandlerParameter(true, "5", NotOptimized = true, Name = "Период перезаписи файла", IsVisibleInBlock = false)] [Description("Время в минутах между перезаписью данных в файл")] public double FileWriteInterval { get; set; } = 5; /// /// Писать данные в лог-файл /// [HandlerParameter(true, "false", NotOptimized = true, Name = "Писать данные в лог-файл", IsVisibleInBlock = false)] [Description("При установленной опции все полученные со счета данные будут выведены в лог")] public bool WriteLog { get; set; } = false; #endregion public double Execute(double accBalance, int bar) => Execute(accBalance, true, 0, bar); public double Execute(double accBalance, bool enabled, int bar) => Execute(accBalance, enabled, 0, bar); public double Execute(double accBalance, bool enabled, double activationDelay, int bar) { if (!Context.Runtime.IsAgentMode || bar != 0) return _retValue; var cacheDataName = "lastActivationTime"; var initDateTime = DateTime.MinValue; if (!enabled) { var activationDataContainer = new NotClearableContainer(initDateTime); Context.StoreObject(cacheDataName, activationDataContainer); _retValue = accBalance; return _retValue; }; if (enabled && activationDelay > 0) { var cacheActivationData = Context.LoadObject(cacheDataName); if (!(cacheActivationData is NotClearableContainer activationDataContainer) || !(activationDataContainer.Content is DateTime cacheDateTime) || cacheDateTime == initDateTime) { activationDataContainer = new NotClearableContainer(DateTime.Now); Context.StoreObject(cacheDataName, activationDataContainer); } else if ((DateTime.Now - cacheDateTime).TotalMinutes < activationDelay) { if (WriteLog) LogDebug($"{GetLogID()}: с момента активации прошло меньше чем {activationDelay} мин.," + $" контроль не активирован, время активации входа {cacheDateTime}"); _retValue = accBalance; return _retValue; } } var tradeName = Context.Runtime.TradeName; var cacheBalanceID = $"{tradeName}_balanceControl_value_{ID}"; var cacheWriteTimeID = $"{tradeName}_balanceControl_writeTime_{ID}"; var fileName = $"{tradeName}_balanceControl_{ID}.csv"; _filePath = GetFileAddress("AccountData", fileName); // Читаем данные из кеша/файла, если данных нет, то берем текущие данные по счету var storedBalance = 0d; var cacheData = Context.LoadObject(cacheBalanceID, true); if (cacheData is NotClearableContainer valueContainer && valueContainer.Content is double cacheBalance && cacheBalance != 0) { storedBalance = cacheBalance; } else { if (File.Exists(_filePath)) if (double.TryParse(File.ReadAllLines(_filePath).FirstOrDefault(), out double fileBalance) && fileBalance != 0) storedBalance = fileBalance; } if (storedBalance == 0 || Math.Abs(accBalance - storedBalance) < storedBalance * BalanceDifferencePct / 100) { _retValue = accBalance; } else { _retValue = storedBalance; Context.Log($"BalanceControl: от брокера получен неверный баланс {accBalance:F2}, в систему выдан сохраненный баланс {storedBalance:F2}"); } if (WriteLog) LogDebug($"{GetLogID()}: ID: {ID}, retValue: {_retValue:F2}, storedBalance: {storedBalance:F2}, accBalance: {accBalance:F2}"); // Пишем данные в локальный кеш и при истечении интервала перезаписи, либо при отсутствии записи в файл valueContainer = new NotClearableContainer(_retValue); Context.StoreObject(cacheBalanceID, valueContainer, true); cacheData = Context.LoadObject(cacheWriteTimeID, true); if (WriteToFile && _retValue != 0 && (!(cacheData is NotClearableContainer timeContainer) || !(timeContainer.Content is DateTime lastWriteTime) || (DateTime.Now - lastWriteTime).TotalMinutes > FileWriteInterval)) { try { File.WriteAllText(_filePath, _retValue.ToString()); timeContainer = new NotClearableContainer(DateTime.Now); Context.StoreObject(cacheWriteTimeID, timeContainer, true); } catch (Exception e) { Context.Log($"{GetLogID()}: Невозможно записать данные по счету в файл {e.Message}"); } } return _retValue; } /// Метод формирует строку-индентификатор для лога /// /// private string GetLogID() => $"{GetType().Name}, {Context.Runtime.TradeName}"; } }