1+ using Microsoft . Extensions . Logging ;
2+ using Moq ;
3+ using Tinkoff . InvestApi . V1 ;
4+ using Xunit ;
5+
6+ namespace TraderBot . Tests ;
7+
8+ using OperationsList = List < ( OperationType Type , DateTime Date , long Quantity , decimal Price ) > ;
9+
10+ public class ProfitCalculationServiceTests
11+ {
12+ private readonly ProfitCalculationService _service ;
13+ private readonly Mock < ILogger < ProfitCalculationService > > _mockLogger ;
14+
15+ public ProfitCalculationServiceTests ( )
16+ {
17+ _mockLogger = new Mock < ILogger < ProfitCalculationService > > ( ) ;
18+ _service = new ProfitCalculationService ( _mockLogger . Object ) ;
19+ }
20+
21+ [ Fact ]
22+ public void CalculateProfit_EmptyOperations_ReturnsZeroProfit ( )
23+ {
24+ // Arrange
25+ var operations = new OperationsList ( ) ;
26+
27+ // Act
28+ var result = _service . CalculateProfit ( operations ) ;
29+
30+ // Assert
31+ Assert . Equal ( 0 , result . TotalAbsoluteProfit ) ;
32+ Assert . Equal ( 0 , result . TotalRelativeProfit ) ;
33+ Assert . Equal ( 0 , result . RealizedAbsoluteProfit ) ;
34+ Assert . Equal ( 0 , result . RealizedRelativeProfit ) ;
35+ Assert . Equal ( 0 , result . UnrealizedAbsoluteProfit ) ;
36+ Assert . Equal ( 0 , result . UnrealizedRelativeProfit ) ;
37+ Assert . Equal ( 0 , result . OpenPositionQuantity ) ;
38+ Assert . Empty ( result . CompletedTrades ) ;
39+ }
40+
41+ [ Fact ]
42+ public void CalculateProfit_OnlyBuyOperations_ShowsUnrealizedProfit ( )
43+ {
44+ // Arrange
45+ var operations = new OperationsList
46+ {
47+ ( OperationType . Buy , DateTime . Now . AddHours ( - 2 ) , 10 , 100.0m ) ,
48+ ( OperationType . Buy , DateTime . Now . AddHours ( - 1 ) , 5 , 110.0m )
49+ } ;
50+ var currentPrice = 120.0m ;
51+
52+ // Act
53+ var result = _service . CalculateProfit ( operations , currentPrice ) ;
54+
55+ // Assert
56+ Assert . Equal ( 15 , result . OpenPositionQuantity ) ;
57+ Assert . Equal ( 1550.0m , result . TotalInvested ) ; // (10*100) + (5*110)
58+ Assert . Equal ( 103.33m , Math . Round ( result . AverageBuyPrice , 2 ) ) ; // 1550/15
59+ Assert . Equal ( 1800.0m - 1550.0m , result . UnrealizedAbsoluteProfit ) ; // (15*120) - 1550
60+ Assert . Equal ( 250.0m , result . UnrealizedAbsoluteProfit ) ;
61+ Assert . Equal ( Math . Round ( ( 250.0m / 1550.0m ) * 100 , 2 ) , Math . Round ( result . UnrealizedRelativeProfit , 2 ) ) ; // ~16.13%
62+ Assert . Equal ( 0 , result . RealizedAbsoluteProfit ) ;
63+ Assert . Empty ( result . CompletedTrades ) ;
64+ }
65+
66+ [ Fact ]
67+ public void CalculateProfit_BuyAndSellOperations_ShowsRealizedProfit ( )
68+ {
69+ // Arrange
70+ var operations = new OperationsList
71+ {
72+ ( OperationType . Buy , DateTime . Now . AddHours ( - 3 ) , 10 , 100.0m ) ,
73+ ( OperationType . Buy , DateTime . Now . AddHours ( - 2 ) , 5 , 110.0m ) ,
74+ ( OperationType . Sell , DateTime . Now . AddHours ( - 1 ) , 8 , 120.0m )
75+ } ;
76+ var currentPrice = 115.0m ;
77+
78+ // Act
79+ var result = _service . CalculateProfit ( operations , currentPrice ) ;
80+
81+ // Assert
82+ // Realized profit: First 8 lots sold at 120, bought at 100 (FIFO)
83+ // 8 * (120 - 100) = 160
84+ Assert . Equal ( 160.0m , result . RealizedAbsoluteProfit ) ;
85+
86+ // Remaining position: 2 lots at 100 + 5 lots at 110 = 7 lots
87+ Assert . Equal ( 7 , result . OpenPositionQuantity ) ;
88+ Assert . Equal ( 750.0m , result . TotalInvested ) ; // (2*100) + (5*110)
89+
90+ // Unrealized profit: 7 lots at current price 115 vs invested 750
91+ var currentValue = 7 * 115.0m ; // 805
92+ var unrealizedProfit = currentValue - 750.0m ; // 55
93+ Assert . Equal ( 55.0m , result . UnrealizedAbsoluteProfit ) ;
94+
95+ // Total profit = realized + unrealized
96+ Assert . Equal ( 215.0m , result . TotalAbsoluteProfit ) ; // 160 + 55
97+
98+ // Should have one completed trade
99+ Assert . Single ( result . CompletedTrades ) ;
100+ Assert . Equal ( 8 , result . CompletedTrades [ 0 ] . Quantity ) ;
101+ Assert . Equal ( 100.0m , result . CompletedTrades [ 0 ] . BuyPrice ) ;
102+ Assert . Equal ( 120.0m , result . CompletedTrades [ 0 ] . SellPrice ) ;
103+ Assert . Equal ( 160.0m , result . CompletedTrades [ 0 ] . AbsoluteProfit ) ;
104+ }
105+
106+ [ Fact ]
107+ public void CalculateProfit_MultipleBuysAndSells_FIFO_Matching ( )
108+ {
109+ // Arrange
110+ var operations = new OperationsList
111+ {
112+ ( OperationType . Buy , DateTime . Now . AddHours ( - 6 ) , 10 , 100.0m ) , // Buy 10 @ 100
113+ ( OperationType . Buy , DateTime . Now . AddHours ( - 5 ) , 5 , 110.0m ) , // Buy 5 @ 110
114+ ( OperationType . Sell , DateTime . Now . AddHours ( - 4 ) , 3 , 120.0m ) , // Sell 3 @ 120 (from first buy)
115+ ( OperationType . Sell , DateTime . Now . AddHours ( - 3 ) , 7 , 125.0m ) , // Sell 7 @ 125 (remaining 7 from first buy)
116+ ( OperationType . Buy , DateTime . Now . AddHours ( - 2 ) , 8 , 105.0m ) , // Buy 8 @ 105
117+ ( OperationType . Sell , DateTime . Now . AddHours ( - 1 ) , 2 , 130.0m ) // Sell 2 @ 130 (from second buy)
118+ } ;
119+
120+ // Act
121+ var result = _service . CalculateProfit ( operations ) ;
122+
123+ // Assert
124+ // Completed trades:
125+ // Trade 1: 3 lots @ 100 -> 120 = 3 * (120-100) = 60
126+ // Trade 2: 7 lots @ 100 -> 125 = 7 * (125-100) = 175
127+ // Trade 3: 2 lots @ 110 -> 130 = 2 * (130-110) = 40
128+ // Total realized: 60 + 175 + 40 = 275
129+ Assert . Equal ( 275.0m , result . RealizedAbsoluteProfit ) ;
130+
131+ // Remaining positions:
132+ // 3 lots from second buy @ 110
133+ // 8 lots from third buy @ 105
134+ // Total: 11 lots, value: (3*110) + (8*105) = 330 + 840 = 1170
135+ Assert . Equal ( 11 , result . OpenPositionQuantity ) ;
136+ Assert . Equal ( 1170.0m , result . TotalInvested ) ;
137+
138+ // Should have 3 completed trades
139+ Assert . Equal ( 3 , result . CompletedTrades . Count ) ;
140+ }
141+
142+ [ Fact ]
143+ public void CalculateProfit_ProfitableTrade_CorrectRelativePercentage ( )
144+ {
145+ // Arrange
146+ var operations = new OperationsList
147+ {
148+ ( OperationType . Buy , DateTime . Now . AddHours ( - 2 ) , 10 , 100.0m ) , // Invest 1000
149+ ( OperationType . Sell , DateTime . Now . AddHours ( - 1 ) , 10 , 110.0m ) // Sell for 1100
150+ } ;
151+
152+ // Act
153+ var result = _service . CalculateProfit ( operations ) ;
154+
155+ // Assert
156+ Assert . Equal ( 100.0m , result . RealizedAbsoluteProfit ) ; // 1100 - 1000
157+ Assert . Equal ( 10.0m , result . RealizedRelativeProfit ) ; // (100/1000) * 100 = 10%
158+ Assert . Equal ( 0 , result . OpenPositionQuantity ) ; // All positions closed
159+ }
160+
161+ [ Fact ]
162+ public void CalculateProfit_LossTrade_NegativeProfit ( )
163+ {
164+ // Arrange
165+ var operations = new OperationsList
166+ {
167+ ( OperationType . Buy , DateTime . Now . AddHours ( - 2 ) , 10 , 100.0m ) , // Invest 1000
168+ ( OperationType . Sell , DateTime . Now . AddHours ( - 1 ) , 10 , 90.0m ) // Sell for 900
169+ } ;
170+
171+ // Act
172+ var result = _service . CalculateProfit ( operations ) ;
173+
174+ // Assert
175+ Assert . Equal ( - 100.0m , result . RealizedAbsoluteProfit ) ; // 900 - 1000
176+ Assert . Equal ( - 10.0m , result . RealizedRelativeProfit ) ; // (-100/1000) * 100 = -10%
177+ }
178+ }
0 commit comments