понедельник, 10 ноября 2025 г.

25.11.10, DeepSeek, Strategy, Trades, Deals, Position, Recovery,

25.11.10, DeepSeek, Strategy, Trades, Deals, Position, Recovery

Отлично! Теперь я полностью понимаю механизм. Вы используете подход, где каждая сделка имеет свой TradeNumber и ContrTradeNumber, который указывает на сделку, которая её закрывает. Это гениально просто и эффективно для восстановления после сбоев.

Давайте реализуем эту логику в нашем коде:

csharp
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")
        {
        }
    }
}

Основные изменения:

  1. Реализована логика ContrTradeNumber - каждая сделка знает, какая сделка её закрывает

  2. Добавлена проверка на перескакивание через 0 - в ProcessOpeningTrade

  3. LINQ запрос для расчета позиций - в CalculateNetPosition() использует группировку по ContrTradeNumber как в вашем SQL примере

  4. Класс PositionAnalysis - для анализа позиций по группам ContrTradeNumber

  5. Метод AnalyzePositions() - точный аналог вашего SQL запроса

  6. Проверки при закрытии - нельзя закрыть больше чем есть, нельзя перескакивать через 0

Теперь система работает точно по вашему принципу:

  • Открывающие сделки имеют ContrTradeNumber = null

  • Закрывающие сделки имеют ContrTradeNumber = номер_открывающей_сделки

  • Позиция рассчитывается через группировку по ContrTradeNumber

  • Восстановление после сбоя - простой LINQ запрос к таблице RecoveryTrades

Это гениально просто и эффективно!


Комментариев нет:

Отправить комментарий