using System; using System.ComponentModel; using System.IO; using TSLab.Script.Handlers; using TSLab.Script.Handlers.Options; using static GanovCubes.ExtTrade; using static GanovCubes.FileLogger; namespace GanovCubes { #region CubeDescription [HandlerCategory("Ganov Cubes. Different")] [HelperName("BalanceControl", Language = "ru-RU")] [Description("Кубик работает только в режиме агента, в режиме лаборатории всегда возвращается ноль. Данный кубик получает данные со входа, сравнивает их суммой," + " полученной на прошлом пересчете и если разница составляет более указанного процента, то возвращается ранее полученную сумму. Таким образом, предотвращается" + " передача в систему существенно неверных данных, присутствие которых может привести к покупке активов на сумму, выходящую за имеющиеся свободные средства." + " Кубик рекомендуется запускать в отдельном агенте с пересчетом по метроному и передавать данные в другие агенты через механизм глобального кеша, таким образом, " + " у всех агентов будет актуальная и отфильтрованная информация о текущем балансе счета. Кубик для хранения прошлых данных использует локальный кеш и файл на диске" + " по указанном пути. Обращение к файлу происходит только в случае если кеш пустой, в остальных случаях кубик использует кеш. В файл данные" + " записываются 1 раз в 5 минут (настройкой \"периодичность записи в файл\" можно изменить интервал перезаписи). Данные сохраняются в файл, расположенный в папке:" + @"C: \Users\UserName\AppData\Local\TSLab\TSLab 2.0\DataStorage\AccountData\")] [InputsCount(1)] [Input(0, TemplateTypes.DOUBLE, Name = "Balance")] [OutputsCount(1)] [OutputType(TemplateTypes.DOUBLE)] #endregion public class BalanceControl : IValuesHandler, 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 WriteToLog { get; set; } = false; #endregion public double Execute(double accBalance, int bar) { if (!Context.Runtime.IsAgentMode || bar != 0) return _retValue; var directory = $@"{GetTSLabAddDataDirectory()}AccountData\"; if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); _filePath = $"{directory}{Context.Runtime.TradeName}_AccBalance_{ID}.csv"; var cacheBalanceID = $"BalanceControl{directory}{ID}"; var cacheWriteTimeID = $"BalanceControlWriteTime{directory}{ID}"; // Читаем данные из кеша/файла, если данных нет, то берем текущие данные по счету var storedBalance = 0d; var cacheData = Context.LoadObject(cacheBalanceID); if (cacheData == null || !double.TryParse(cacheData.ToString(), out double cacheBalance) || cacheBalance == 0) { if (File.Exists(_filePath)) if (double.TryParse(File.ReadAllLines(_filePath)[0], out double fileBalance) && fileBalance != 0) storedBalance = fileBalance; } else storedBalance = cacheBalance; _retValue = storedBalance == 0 || accBalance < storedBalance * (1 + BalanceDifferencePct / 100) ? accBalance : storedBalance; if (WriteToLog) LogDebug($"ID: {ID}, retValue: {_retValue}, storedBalance: {storedBalance}, accBalance: {accBalance}"); // Пишем данные в локальный кеш и при истечении интервала перезаписи в файл Context.StoreObject(cacheBalanceID, _retValue, true); var lastWriteDateTime = Context.LoadObject(cacheWriteTimeID); if (WriteToFile && (lastWriteDateTime == null || (DateTime.TryParse(lastWriteDateTime.ToString(), out var lastWriteTime) && (DateTime.Now - lastWriteTime).TotalMinutes > FileWriteInterval && _retValue != 0))) { try { File.WriteAllText(_filePath, _retValue.ToString()); Context.StoreObject(cacheWriteTimeID, DateTime.Now, true); } catch (Exception e) { Context.Log($"BalanceControl: Невозможно записать данные по счету в файл {e.Message}"); } } return _retValue; } } }