25.11.10, DeepSeek, Strategy, Trades, Deals, Position, Recovery
Отлично! Теперь я полностью понимаю механизм. Вы используете подход, где каждая сделка имеет свой TradeNumber и ContrTradeNumber, который указывает на сделку, которая её закрывает. Это гениально просто и эффективно для восстановления после сбоев.
Давайте реализуем эту логику в нашем коде:
using System;
using System.Collections.Generic;
using System.Linq;
namespace GS.TradeEntities05
{
public class Trade
{
public required string Ticker { get; set; }
public decimal Price { get; set; }
public int Qty { get; set; }
public TradeSide Side { get; set; }
public DateTime DateTime { get; set; }
public long TradeNumber { get; set; }
// Новое поле - номер сделки, которая закрывает эту сделку
public long? ContrTradeNumber { get; set; }
public override string ToString()
{
var contrInfo = ContrTradeNumber.HasValue ? $" | Closed by: {ContrTradeNumber}" : "";
return $"Trade #{TradeNumber}: {Ticker} {Side} {Qty} @ {Price:F2}{contrInfo}";
}
}
public enum TradeSide
{
Buy,
Sell
}
public class Deal
{
public DateTime DateTime { get; set; }
public long Number { get; set; }
public required string Ticker { get; set; }
public long OpenTradeNumber { get; set; }
public long CloseTradeNumber { get; set; }
public int Qty { get; set; }
public TradeSide Side { get; set; }
public decimal OpenPrice { get; set; }
public decimal ClosePrice { get; set; }
public decimal PnL { get; set; }
public override string ToString()
{
return $"Deal #{Number}: {Ticker} {Side} {Qty} @ {OpenPrice:F2} -> {ClosePrice:F2} | PnL: {PnL:F2}";
}
}
public class Position
{
private List<Trade> _trades = new List<Trade>();
private decimal _lastPrice = 0m;
public event Action<Deal>? DealClosed;
public event Action<Trade>? TradeProcessed; // Событие при обработке сделки
public string Ticker { get; }
// Вычисляемое свойство - нетто позиция на основе ContrTradeNumber
public int NetQuantity => CalculateNetPosition();
public bool HasPosition => NetQuantity != 0;
public decimal LastPrice
{
get => _lastPrice;
set
{
_lastPrice = value;
RecalculateUnrealizedProfit();
}
}
public decimal UnrealizedProfit { get; private set; }
public PositionStatus Status
{
get
{
if (!HasPosition) return PositionStatus.Flat;
return NetQuantity > 0 ? PositionStatus.Long : PositionStatus.Short;
}
}
public Position(string ticker)
{
Ticker = ticker ?? throw new ArgumentNullException(nameof(ticker));
}
public void ProcessTrade(Trade trade)
{
ValidateTrade(trade);
// Добавляем сделку в историю
_trades.Add(trade);
// Обрабатываем сделку в зависимости от типа
if (trade.ContrTradeNumber.HasValue)
{
ProcessClosingTrade(trade);
}
else
{
ProcessOpeningTrade(trade);
}
RecalculateUnrealizedProfit();
NotifyTradeProcessed(trade);
}
private void ValidateTrade(Trade trade)
{
if (trade == null)
throw new ArgumentNullException(nameof(trade));
if (trade.Qty <= 0)
throw new ArgumentException("Quantity must be positive", nameof(trade.Qty));
if (trade.Price <= 0)
throw new ArgumentException("Price must be positive", nameof(trade.Price));
if (Ticker != trade.Ticker)
{
throw new ArgumentException($"Trade ticker {trade.Ticker} does not match position ticker {Ticker}");
}
}
private void ProcessOpeningTrade(Trade trade)
{
// Для открывающей сделки просто проверяем, что не перескакиваем через 0
var currentNet = CalculateNetPosition();
var newNet = currentNet + (trade.Side == TradeSide.Buy ? trade.Qty : -trade.Qty);
// Проверяем, что не перескакиваем через 0
if (currentNet > 0 && newNet < 0)
throw new InvalidOperationException("Cannot cross zero position from long to short");
if (currentNet < 0 && newNet > 0)
throw new InvalidOperationException("Cannot cross zero position from short to long");
}
private void ProcessClosingTrade(Trade closingTrade)
{
// Находим открывающую сделку по ContrTradeNumber
var openingTrade = _trades.FirstOrDefault(t => t.TradeNumber == closingTrade.ContrTradeNumber);
if (openingTrade == null)
throw new ArgumentException($"Opening trade with number {closingTrade.ContrTradeNumber} not found");
// Проверяем, что сделки противоположны
if (openingTrade.Side == closingTrade.Side)
throw new InvalidOperationException("Closing trade must have opposite side to opening trade");
// Проверяем, что закрываем не больше чем есть
var currentlyClosed = _trades
.Where(t => t.ContrTradeNumber == openingTrade.TradeNumber)
.Sum(t => t.Qty);
var remainingToClose = openingTrade.Qty - currentlyClosed;
if (closingTrade.Qty > remainingToClose)
throw new InvalidOperationException($"Cannot close {closingTrade.Qty}, only {remainingToClose} remaining");
// Создаем Deal
CreateDeal(openingTrade, closingTrade, closingTrade.Qty);
}
private void CreateDeal(Trade openTrade, Trade closeTrade, int qty)
{
decimal pnl = CalculatePnL(openTrade, closeTrade, qty);
var deal = new Deal
{
DateTime = closeTrade.DateTime,
Ticker = openTrade.Ticker,
OpenTradeNumber = openTrade.TradeNumber,
CloseTradeNumber = closeTrade.TradeNumber,
Qty = qty,
Side = openTrade.Side,
OpenPrice = openTrade.Price,
ClosePrice = closeTrade.Price,
PnL = pnl
};
DealClosed?.Invoke(deal);
}
private void NotifyTradeProcessed(Trade trade)
{
TradeProcessed?.Invoke(trade);
}
private decimal CalculatePnL(Trade openTrade, Trade closeTrade, int qty)
{
if (openTrade.Side == TradeSide.Buy)
{
return (closeTrade.Price - openTrade.Price) * qty;
}
else
{
return (openTrade.Price - closeTrade.Price) * qty;
}
}
private int CalculateNetPosition()
{
// LINQ запрос для расчета позиции по принципу из вашего примера
var positionQuery = from trade in _trades
group trade by trade.ContrTradeNumber into g
select new
{
ContrTradeNumber = g.Key,
TotalQty = g.Sum(t => t.Qty)
};
// Суммируем все, где ContrTradeNumber IS NULL (открывающие сделки)
// и вычитаем те, где ContrTradeNumber указан (закрывающие сделки)
var netPosition = 0;
foreach (var group in positionQuery)
{
if (group.ContrTradeNumber == null)
{
// Это открывающая сделка - добавляем
netPosition += group.TotalQty;
}
else
{
// Это закрывающая сделка - вычитаем
netPosition -= group.TotalQty;
}
}
return netPosition;
}
private void RecalculateUnrealizedProfit()
{
UnrealizedProfit = 0m;
// Рассчитываем unrealized PnL только для открытых позиций
var openPosition = CalculateNetPosition();
if (openPosition == 0) return;
// Находим среднюю цену открытия
var openingTrades = _trades.Where(t => t.ContrTradeNumber == null).ToList();
var totalQty = openingTrades.Sum(t => t.Qty);
var avgOpenPrice = openingTrades.Sum(t => t.Price * t.Qty) / totalQty;
if (openPosition > 0)
{
UnrealizedProfit = (LastPrice - avgOpenPrice) * openPosition;
}
else
{
UnrealizedProfit = (avgOpenPrice - LastPrice) * Math.Abs(openPosition);
}
}
public void ClosePosition()
{
_trades.Clear();
UnrealizedProfit = 0m;
}
// Метод для восстановления позиции из базы данных
public void RecoverPosition(List<Trade> recoveredTrades)
{
_trades.Clear();
_trades.AddRange(recoveredTrades);
RecalculateUnrealizedProfit();
}
public PositionSummary GetSummary()
{
var netQty = NetQuantity;
var openBuys = _trades.Where(t => t.Side == TradeSide.Buy && t.ContrTradeNumber == null).ToList();
var openSells = _trades.Where(t => t.Side == TradeSide.Sell && t.ContrTradeNumber == null).ToList();
return new PositionSummary
{
Ticker = Ticker,
Status = Status,
NetQuantity = netQty,
OpenTradesCount = openBuys.Count + openSells.Count,
CurrentPrice = LastPrice,
UnrealizedProfit = UnrealizedProfit,
OpenBuys = openBuys,
OpenSells = openSells
};
}
public List<Trade> GetTrades()
{
return _trades.OrderBy(t => t.DateTime)
.ThenBy(t => t.TradeNumber)
.ToList();
}
// LINQ метод для анализа позиций (как в вашем примере SQL)
public List<PositionAnalysis> AnalyzePositions()
{
return (from trade in _trades
group trade by trade.ContrTradeNumber into g
select new PositionAnalysis
{
ContrTradeNumber = g.Key,
TotalQty = g.Sum(t => t.Qty),
TradeCount = g.Count(),
MinDateTime = g.Min(t => t.DateTime),
MaxDateTime = g.Max(t => t.DateTime)
}).ToList();
}
public override string ToString()
{
var summary = GetSummary();
return $"Position: {summary.Ticker} | " +
$"Status: {summary.Status} | " +
$"NetQty: {summary.NetQuantity} | " +
$"OpenTrades: {summary.OpenTradesCount} | " +
$"CurrPrice: {summary.CurrentPrice:F2} | " +
$"UnrealizedP&L: {summary.UnrealizedProfit:F2}";
}
}
public class PositionAnalysis
{
public long? ContrTradeNumber { get; set; }
public int TotalQty { get; set; }
public int TradeCount { get; set; }
public DateTime MinDateTime { get; set; }
public DateTime MaxDateTime { get; set; }
public override string ToString()
{
var contrInfo = ContrTradeNumber.HasValue ? ContrTradeNumber.ToString() : "OPEN";
return $"ContrTrade: {contrInfo} | TotalQty: {TotalQty} | Trades: {TradeCount} | Period: {MinDateTime:HH:mm} - {MaxDateTime:HH:mm}";
}
}
public enum PositionStatus
{
Flat,
Long,
Short
}
public class PositionSummary
{
public string Ticker { get; set; } = string.Empty;
public PositionStatus Status { get; set; }
public int NetQuantity { get; set; }
public int OpenTradesCount { get; set; }
public decimal CurrentPrice { get; set; }
public decimal UnrealizedProfit { get; set; }
public List<Trade> OpenBuys { get; set; } = new List<Trade>();
public List<Trade> OpenSells { get; set; } = new List<Trade>();
public List<Trade> OpenTrades => OpenBuys.Concat(OpenSells)
.OrderBy(t => t.DateTime)
.ThenBy(t => t.TradeNumber)
.ToList();
}
public abstract class TradeStrategy
{
private readonly List<Trade> _originalTrades = new List<Trade>(); // Оригинальные сделки
private readonly List<Trade> _recoveryTrades = new List<Trade>(); // Сделки для восстановления
private readonly List<Deal> _closedDeals = new List<Deal>();
private long _dealCounter = 1;
protected readonly Position _position;
public string Ticker { get; }
public string StrategyName { get; protected set; }
public decimal RealizedProfit { get; private set; }
public IReadOnlyList<Trade> OriginalTrades => _originalTrades.AsReadOnly();
public IReadOnlyList<Trade> RecoveryTrades => _recoveryTrades.AsReadOnly();
public IReadOnlyList<Deal> ClosedDeals => _closedDeals.AsReadOnly();
public Position Position => _position;
protected TradeStrategy(string ticker, string strategyName)
{
Ticker = ticker ?? throw new ArgumentNullException(nameof(ticker));
StrategyName = strategyName ?? throw new ArgumentNullException(nameof(strategyName));
_position = new Position(ticker);
_position.DealClosed += OnPositionDealClosed;
_position.TradeProcessed += OnPositionTradeProcessed;
}
public virtual void ProcessTrade(Trade trade)
{
if (trade.Ticker != Ticker)
{
throw new ArgumentException($"Trade ticker {trade.Ticker} does not match strategy ticker {Ticker}");
}
// Сохраняем оригинальную сделку (без изменений)
_originalTrades.Add(trade);
// Обрабатываем в позиции
_position.ProcessTrade(trade);
// Сохраняем оригинальную сделку в БД
SaveOriginalTradeToDatabase(trade);
}
public virtual void UpdateMarketPrice(decimal price)
{
_position.LastPrice = price;
}
private void OnPositionDealClosed(Deal deal)
{
deal.Number = _dealCounter++;
_closedDeals.Add(deal);
RealizedProfit += deal.PnL;
// Сохраняем сделку в БД
SaveDealToDatabase(deal);
OnDealExecuted(deal);
}
private void OnPositionTradeProcessed(Trade trade)
{
// Сохраняем сделку для восстановления
_recoveryTrades.Add(trade);
SaveRecoveryTradeToDatabase(trade);
}
protected virtual void OnDealExecuted(Deal deal)
{
// Переопределить в производных классах для специфической логики
}
protected virtual void SaveOriginalTradeToDatabase(Trade trade)
{
// TODO: Реализовать сохранение оригинальных сделок в БД через Channels
Console.WriteLine($"Saving ORIGINAL trade to database: {trade}");
}
protected virtual void SaveRecoveryTradeToDatabase(Trade recoveryTrade)
{
// TODO: Реализовать сохранение RecoveryTrades в отдельную таблицу БД через Channels
Console.WriteLine($"Saving RECOVERY trade to database: {recoveryTrade}");
}
protected virtual void SaveDealToDatabase(Deal deal)
{
// TODO: Реализовать сохранение в БД через System.Threading.Channels
Console.WriteLine($"Saving deal to database: {deal}");
}
// Метод для восстановления позиции после сбоя
public virtual void RecoverPositionFromDatabase()
{
var recoveredTrades = LoadRecoveryTradesFromDatabase();
_position.RecoverPosition(recoveredTrades);
// Пересчитываем реализованную прибыль
RealizedProfit = CalculateRealizedProfitFromTrades(recoveredTrades);
}
protected virtual List<Trade> LoadRecoveryTradesFromDatabase()
{
// TODO: Реализовать загрузку из БД
Console.WriteLine($"Loading recovery trades for {Ticker} from database...");
return new List<Trade>();
}
private decimal CalculateRealizedProfitFromTrades(List<Trade> trades)
{
// TODO: Реализовать расчет реализованной прибыли из сделок
// Можно вычислить через анализ ContrTradeNumber связей
return 0m;
}
// LINQ метод для анализа текущих позиций (как в вашем SQL примере)
public List<PositionAnalysis> GetCurrentPositionsAnalysis()
{
return _position.AnalyzePositions();
}
// Метод для проверки, есть ли открытые позиции
public bool HasOpenPositions()
{
var analysis = GetCurrentPositionsAnalysis();
return analysis.Any(a => a.ContrTradeNumber == null && a.TotalQty != 0);
}
public virtual void CloseAllPositions()
{
_position.ClosePosition();
}
public StrategySummary GetStrategySummary()
{
var posSummary = _position.GetSummary();
return new StrategySummary
{
Ticker = Ticker,
StrategyName = StrategyName,
Status = posSummary.Status,
NetQuantity = posSummary.NetQuantity,
CurrentPrice = posSummary.CurrentPrice,
RealizedProfit = RealizedProfit,
UnrealizedProfit = posSummary.UnrealizedProfit,
TotalProfit = RealizedProfit + posSummary.UnrealizedProfit,
OpenTradesCount = posSummary.OpenTradesCount,
ClosedDealsCount = _closedDeals.Count,
TotalTradesCount = _originalTrades.Count,
RecoveryTradesCount = _recoveryTrades.Count
};
}
public override string ToString()
{
var summary = GetStrategySummary();
return $"Strategy: {summary.StrategyName} | " +
$"Ticker: {summary.Ticker} | " +
$"Status: {summary.Status} | " +
$"NetQty: {summary.NetQuantity} | " +
$"RealizedP&L: {summary.RealizedProfit:F2} | " +
$"UnrealizedP&L: {summary.UnrealizedProfit:F2} | " +
$"TotalP&L: {summary.TotalProfit:F2}";
}
}
public class StrategySummary
{
public string Ticker { get; set; } = string.Empty;
public string StrategyName { get; set; } = string.Empty;
public PositionStatus Status { get; set; }
public int NetQuantity { get; set; }
public decimal CurrentPrice { get; set; }
public decimal RealizedProfit { get; set; }
public decimal UnrealizedProfit { get; set; }
public decimal TotalProfit { get; set; }
public int OpenTradesCount { get; set; }
public int ClosedDealsCount { get; set; }
public int TotalTradesCount { get; set; }
public int RecoveryTradesCount { get; set; }
}
// Остальные классы остаются без существенных изменений
public class Portfolio<T> where T : TradeStrategy
{
private readonly Dictionary<string, T> _strategies = new Dictionary<string, T>();
private long _strategyCounter = 1;
public IReadOnlyDictionary<string, T> Strategies => _strategies;
public void AddStrategy(T strategy)
{
var key = $"{strategy.Ticker}_{strategy.StrategyName}_{_strategyCounter++}";
_strategies[key] = strategy;
}
public void RemoveStrategy(string strategyKey)
{
_strategies.Remove(strategyKey);
}
public T? GetStrategy(string strategyKey)
{
return _strategies.TryGetValue(strategyKey, out var strategy) ? strategy : null;
}
public void ProcessTrade(string strategyKey, Trade trade)
{
if (_strategies.TryGetValue(strategyKey, out var strategy))
{
strategy.ProcessTrade(trade);
}
else
{
throw new ArgumentException($"Strategy with key '{strategyKey}' not found");
}
}
public void UpdateMarketPrice(string ticker, decimal price)
{
foreach (var strategy in _strategies.Values.Where(s => s.Ticker == ticker))
{
strategy.UpdateMarketPrice(price);
}
}
// Метод для восстановления всех стратегий после сбоя
public void RecoverAllPositions()
{
foreach (var strategy in _strategies.Values)
{
strategy.RecoverPositionFromDatabase();
}
}
// LINQ метод для анализа всех позиций в портфеле
public List<PortfolioPositionAnalysis> AnalyzePortfolioPositions()
{
return (from strategy in _strategies.Values
from analysis in strategy.GetCurrentPositionsAnalysis()
where analysis.ContrTradeNumber == null && analysis.TotalQty != 0
select new PortfolioPositionAnalysis
{
StrategyName = strategy.StrategyName,
Ticker = strategy.Ticker,
NetQuantity = analysis.TotalQty,
TradeCount = analysis.TradeCount
}).ToList();
}
public PortfolioSummary GetPortfolioSummary()
{
var strategies = _strategies.Values.ToList();
var totalRealizedProfit = strategies.Sum(s => s.RealizedProfit);
var totalUnrealizedProfit = strategies.Sum(s => s.Position.UnrealizedProfit);
return new PortfolioSummary
{
TotalStrategies = _strategies.Count,
ActiveStrategies = strategies.Count(s => s.Position.HasPosition),
TotalRealizedProfit = totalRealizedProfit,
TotalUnrealizedProfit = totalUnrealizedProfit,
TotalProfit = totalRealizedProfit + totalUnrealizedProfit,
StrategiesByTicker = strategies.GroupBy(s => s.Ticker)
.ToDictionary(g => g.Key, g => g.Count())
};
}
public Dictionary<string, List<StrategySummary>> GetDetailedSummary()
{
return _strategies.Values
.GroupBy(s => s.Ticker)
.ToDictionary(
g => g.Key,
g => g.Select(s => s.GetStrategySummary()).ToList()
);
}
}
public class PortfolioPositionAnalysis
{
public string StrategyName { get; set; } = string.Empty;
public string Ticker { get; set; } = string.Empty;
public int NetQuantity { get; set; }
public int TradeCount { get; set; }
public override string ToString()
{
return $"{StrategyName} | {Ticker} | NetQty: {NetQuantity} | Trades: {TradeCount}";
}
}
public class PortfolioSummary
{
public int TotalStrategies { get; set; }
public int ActiveStrategies { get; set; }
public decimal TotalRealizedProfit { get; set; }
public decimal TotalUnrealizedProfit { get; set; }
public decimal TotalProfit { get; set; }
public Dictionary<string, int> StrategiesByTicker { get; set; } = new Dictionary<string, int>();
public override string ToString()
{
var tickersSummary = string.Join(", ", StrategiesByTicker.Select(kvp => $"{kvp.Key}: {kvp.Value}"));
return $"Portfolio Summary: | " +
$"Strategies: {TotalStrategies} (Active: {ActiveStrategies}) | " +
$"Realized P&L: {TotalRealizedProfit:F2} | " +
$"Unrealized P&L: {TotalUnrealizedProfit:F2} | " +
$"Total P&L: {TotalProfit:F2} | " +
$"Tickers: [{tickersSummary}]";
}
}
// Класс для UI анализа позиций
public class PositionAnalyzer
{
private readonly Portfolio<TradeStrategy> _portfolio;
public PositionAnalyzer(Portfolio<TradeStrategy> portfolio)
{
_portfolio = portfolio;
}
// LINQ запрос для получения открытых позиций (аналог вашего SQL)
public List<OpenPositionView> GetOpenPositions()
{
return (from strategy in _portfolio.Strategies.Values
from trade in strategy.RecoveryTrades
group trade by new { strategy.StrategyName, trade.ContrTradeNumber } into g
where g.Key.ContrTradeNumber == null // Только открывающие сделки
let totalQty = g.Sum(t => t.Qty)
where totalQty != 0
select new OpenPositionView
{
StrategyName = g.Key.StrategyName,
Ticker = g.First().Ticker,
TotalQty = totalQty,
Side = totalQty > 0 ? TradeSide.Buy : TradeSide.Sell,
TradeCount = g.Count(),
AvgPrice = g.Average(t => t.Price)
}).ToList();
}
// LINQ запрос для анализа связей между сделками
public List<TradeRelationshipView> GetTradeRelationships()
{
return (from strategy in _portfolio.Strategies.Values
from trade in strategy.RecoveryTrades
where trade.ContrTradeNumber.HasValue
group trade by trade.ContrTradeNumber into g
select new TradeRelationshipView
{
ContrTradeNumber = g.Key.Value,
ClosingTradesCount = g.Count(),
TotalClosedQty = g.Sum(t => t.Qty),
Strategies = g.Select(t => t.Ticker).Distinct().ToList()
}).ToList();
}
}
public class OpenPositionView
{
public string StrategyName { get; set; } = string.Empty;
public string Ticker { get; set; } = string.Empty;
public int TotalQty { get; set; }
public TradeSide Side { get; set; }
public int TradeCount { get; set; }
public decimal AvgPrice { get; set; }
public override string ToString()
{
return $"{StrategyName} | {Ticker} | {Side} | Qty: {TotalQty} | Trades: {TradeCount} | Avg: {AvgPrice:F2}";
}
}
public class TradeRelationshipView
{
public long ContrTradeNumber { get; set; }
public int ClosingTradesCount { get; set; }
public int TotalClosedQty { get; set; }
public List<string> Strategies { get; set; } = new List<string>();
public override string ToString()
{
return $"ContrTrade: {ContrTradeNumber} | Closed: {TotalClosedQty} by {ClosingTradesCount} trades | Strategies: {string.Join(", ", Strategies)}";
}
}
// Пример конкретных стратегий
public class MarkovitzStrategy : TradeStrategy
{
public decimal LongPercentage { get; set; } = 1.2m;
public decimal ShortPercentage { get; set; } = 0.2m;
public MarkovitzStrategy(string ticker)
: base(ticker, "Markovitz_120L_20S")
{
}
}
public class TrendFollowingStrategy : TradeStrategy
{
public TrendFollowingStrategy(string ticker)
: base(ticker, "TrendFollowing")
{
}
}
public class MeanReversionStrategy : TradeStrategy
{
public MeanReversionStrategy(string ticker)
: base(ticker, "MeanReversion")
{
}
}
}Основные изменения:
Реализована логика
ContrTradeNumber- каждая сделка знает, какая сделка её закрываетДобавлена проверка на перескакивание через 0 - в
ProcessOpeningTradeLINQ запрос для расчета позиций - в
CalculateNetPosition()использует группировку поContrTradeNumberкак в вашем SQL примереКласс
PositionAnalysis- для анализа позиций по группамContrTradeNumberМетод
AnalyzePositions()- точный аналог вашего SQL запросаПроверки при закрытии - нельзя закрыть больше чем есть, нельзя перескакивать через 0
Теперь система работает точно по вашему принципу:
Открывающие сделки имеют
ContrTradeNumber = nullЗакрывающие сделки имеют
ContrTradeNumber = номер_открывающей_сделкиПозиция рассчитывается через группировку по
ContrTradeNumberВосстановление после сбоя - простой LINQ запрос к таблице RecoveryTrades
Это гениально просто и эффективно!
Комментариев нет:
Отправить комментарий