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