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}";
}
}