1
1
package net .swofty ;
2
2
3
- import java .time .LocalDateTime ;
3
+ import net .swofty .orders .OrderType ;
4
+
5
+ import java .time .*;
6
+ import java .time .format .DateTimeFormatter ;
4
7
import java .time .temporal .ChronoUnit ;
5
- import java .util .ArrayList ;
6
- import java .util .List ;
8
+ import java .util .*;
7
9
import java .util .concurrent .atomic .AtomicInteger ;
8
10
import java .util .concurrent .atomic .DoubleAdder ;
11
+ import java .util .concurrent .ConcurrentHashMap ;
9
12
10
13
public class AlgorithmStatistics {
11
14
private final String algorithmId ;
12
15
private final LocalDateTime startTime ;
13
- private final AtomicInteger totalTrades ;
14
16
private final DoubleAdder totalProfit ;
15
17
private final DoubleAdder maxDrawdown ;
16
18
private final DoubleAdder peakValue ;
@@ -19,6 +21,13 @@ public class AlgorithmStatistics {
19
21
private final List <Double > dailyReturns ;
20
22
private volatile double initialValue ;
21
23
24
+ // New fields for enhanced statistics
25
+ private final Map <String , TickerStatistics > tickerStats ;
26
+ private final Map <LocalDate , WeeklyPerformance > weeklyPerformance = new ConcurrentHashMap <>();
27
+ private final Map <String , TradeRecord > openTrades = new ConcurrentHashMap <>();
28
+ private final List <TradeRecord > tradeHistory ;
29
+ private final AtomicInteger totalTrades ;
30
+
22
31
public AlgorithmStatistics (String algorithmId , double initialValue , LocalDateTime startTime ) {
23
32
this .algorithmId = algorithmId ;
24
33
this .totalTrades = new AtomicInteger (0 );
@@ -31,18 +40,51 @@ public AlgorithmStatistics(String algorithmId, double initialValue, LocalDateTim
31
40
this .startTime = startTime ;
32
41
this .initialValue = initialValue ;
33
42
this .peakValue .add (initialValue );
43
+
44
+ // Initialize new tracking structures
45
+ this .tickerStats = new ConcurrentHashMap <>();
46
+ this .tradeHistory = Collections .synchronizedList (new ArrayList <>());
47
+ }
48
+
49
+ public void recordTrade (String ticker , OrderType type , int quantity , double price ,
50
+ double portfolioValueBefore , LocalDateTime timestamp ) {
51
+ TradeRecord trade = new TradeRecord (
52
+ ticker , type , quantity , price , portfolioValueBefore , timestamp
53
+ );
54
+ tradeHistory .add (trade );
55
+ totalTrades .incrementAndGet ();
56
+
57
+ // Update ticker-specific statistics
58
+ tickerStats .computeIfAbsent (ticker , k -> new TickerStatistics ())
59
+ .updateStats (trade );
60
+
61
+ // Handle weekly performance tracking
62
+ switch (type ) {
63
+ case BUY , SHORT -> openTrades .put (ticker , trade );
64
+ case SELL , COVER -> {
65
+ TradeRecord openTrade = openTrades .remove (ticker );
66
+ if (openTrade != null ) {
67
+ // Get the week of the SELL/COVER trade
68
+ LocalDate weekStart = timestamp .toLocalDate ().with (DayOfWeek .MONDAY );
69
+ weeklyPerformance .computeIfAbsent (weekStart , k -> new WeeklyPerformance ())
70
+ .recordCompletedTrade (openTrade , trade );
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ public int getTotalTrades () {
77
+ return totalTrades .get ();
34
78
}
35
79
36
80
public void updateStatistics (double currentValue , double riskFreeRate ) {
37
- // Update total profit/loss
81
+ // Existing statistics logic
38
82
totalProfit .reset ();
39
83
totalProfit .add (currentValue - initialValue );
40
84
41
- // Update total value
42
85
totalValue .reset ();
43
86
totalValue .add (currentValue );
44
87
45
- // Update peak value and calculate drawdown
46
88
if (currentValue > peakValue .sum ()) {
47
89
peakValue .reset ();
48
90
peakValue .add (currentValue );
@@ -53,13 +95,14 @@ public void updateStatistics(double currentValue, double riskFreeRate) {
53
95
maxDrawdown .add (currentDrawdown );
54
96
}
55
97
56
- // Calculate and store daily return
57
98
double dailyReturn = (currentValue - initialValue ) / initialValue ;
58
99
dailyReturns .add (dailyReturn );
59
100
60
- // Calculate Sharpe Ratio using proper daily returns
61
101
if (dailyReturns .size () > 1 ) {
62
- double averageReturn = dailyReturns .stream ().mapToDouble (Double ::doubleValue ).average ().orElse (0.0 );
102
+ double averageReturn = dailyReturns .stream ()
103
+ .mapToDouble (Double ::doubleValue )
104
+ .average ()
105
+ .orElse (0.0 );
63
106
double stdDev = calculateStandardDeviation (dailyReturns , averageReturn );
64
107
double annualizedSharpe = stdDev != 0 ?
65
108
(Math .sqrt (252 ) * (averageReturn - riskFreeRate /252 ) / stdDev ) : 0 ;
@@ -78,33 +121,15 @@ private double calculateStandardDeviation(List<Double> returns, double mean) {
78
121
);
79
122
}
80
123
81
- public void setTrades (int trades ) {
82
- totalTrades .set (trades );
83
- }
84
-
85
- public int getTotalTrades () {
86
- return totalTrades .get ();
87
- }
88
-
89
- public double getTotalProfit () {
90
- return totalProfit .sum ();
91
- }
92
-
93
- public double getMaxDrawdown () {
94
- return maxDrawdown .sum ();
95
- }
96
-
97
- public double getSharpeRatio () {
98
- return sharpeRatio .sum ();
99
- }
100
-
101
124
@ Override
102
125
public String toString () {
126
+ StringBuilder sb = new StringBuilder ();
103
127
long daysRun = ChronoUnit .DAYS .between (startTime , LocalDateTime .now ());
104
128
double annualizedReturn = dailyReturns .isEmpty () ? 0 :
105
129
Math .pow (1 + dailyReturns .get (dailyReturns .size () - 1 ), 252 ) - 1 ;
106
130
107
- return String .format ("""
131
+ // Overall Performance
132
+ sb .append (String .format ("""
108
133
Algorithm Statistics for %s:
109
134
Backtest Period: %d days
110
135
Total Trades: %d
@@ -114,16 +139,157 @@ public String toString() {
114
139
Sharpe Ratio: %.2f
115
140
Average Trades Per Day: %.2f
116
141
Total Value: $%.2f
142
+
143
+ Per-Ticker Performance:
144
+ =====================
117
145
""" ,
118
- algorithmId ,
119
- daysRun ,
120
- totalTrades .get (),
121
- totalProfit .sum (),
122
- annualizedReturn * 100 ,
123
- maxDrawdown .sum (),
124
- sharpeRatio .sum (),
146
+ algorithmId , daysRun , totalTrades .get (), totalProfit .sum (),
147
+ annualizedReturn * 100 , maxDrawdown .sum (), sharpeRatio .sum (),
125
148
daysRun > 0 ? (double ) totalTrades .get () / daysRun : 0 ,
126
149
totalValue .sum ()
127
- );
150
+ ));
151
+
152
+ // Add per-ticker statistics
153
+ tickerStats .forEach ((ticker , stats ) -> {
154
+ double winRate = stats .totalSells > 0 ?
155
+ ((double ) stats .profitableSells / stats .totalSells ) * 100 : 0.0 ;
156
+
157
+ sb .append (String .format ("""
158
+ %s:
159
+ Total Sells: %d
160
+ Profitable Sells: %d (%.1f%%)
161
+ Total P/L: $%.2f
162
+ Average P/L per Sale: $%.2f
163
+ Largest Gain: $%.2f
164
+ Largest Loss: $%.2f
165
+ Win Rate: %.1f%%
166
+
167
+ """ ,
168
+ ticker , stats .totalSells , stats .profitableSells ,
169
+ winRate ,
170
+ stats .totalPnL ,
171
+ stats .totalSells > 0 ? stats .totalPnL / stats .totalSells : 0.0 ,
172
+ stats .largestGain ,
173
+ stats .largestLoss ,
174
+ winRate
175
+ ));
176
+ });
177
+
178
+ // Add monthly performance
179
+ sb .append ("\n Weekly Performance:\n ===================\n " );
180
+ if (weeklyPerformance .isEmpty ()) {
181
+ sb .append ("No completed trades yet\n " );
182
+ } else {
183
+ DateTimeFormatter weekFormatter = DateTimeFormatter .ofPattern ("MM/dd/yyyy" );
184
+ weeklyPerformance .entrySet ().stream ()
185
+ .sorted (Map .Entry .comparingByKey ())
186
+ .filter (entry -> entry .getValue ().hasActivity ())
187
+ .forEach (entry -> {
188
+ WeeklyPerformance perf = entry .getValue ();
189
+ LocalDate weekStart = entry .getKey ();
190
+ LocalDate weekEnd = weekStart .plusDays (6 );
191
+ String weekRange = String .format ("%s - %s" ,
192
+ weekStart .format (weekFormatter ),
193
+ weekEnd .format (weekFormatter ));
194
+
195
+ sb .append (String .format ("Week %s:\n " , weekRange ));
196
+ sb .append (String .format (" P/L: $%.2f\n " , perf .totalPnL ));
197
+ sb .append (String .format (" Completed Trades: %d\n " , perf .totalSells ));
198
+ if (perf .totalSells > 0 ) {
199
+ sb .append (String .format (" Average P/L per Share: $%.2f\n " , perf .profitPerShare ));
200
+ }
201
+ sb .append ("\n " );
202
+ });
203
+ }
204
+
205
+ return sb .toString ();
128
206
}
207
+
208
+ private static class TickerStatistics {
209
+ private int totalSells ; // Changed from totalTrades
210
+ private int profitableSells ; // Changed from profitableTrades
211
+ private double totalPnL ;
212
+ private double largestGain ;
213
+ private double largestLoss ;
214
+ private Double lastBuyPrice ;
215
+ private int lastBuyQuantity ;
216
+
217
+ public synchronized void updateStats (TradeRecord trade ) {
218
+ switch (trade .type ()) {
219
+ case BUY -> {
220
+ lastBuyPrice = trade .price ();
221
+ lastBuyQuantity = trade .quantity ();
222
+ }
223
+ case SELL -> {
224
+ if (lastBuyPrice != null ) {
225
+ totalSells ++; // Only count sells
226
+ double profit = (trade .price () - lastBuyPrice ) * trade .quantity ();
227
+ totalPnL += profit ;
228
+
229
+ if (profit > 0 ) {
230
+ profitableSells ++; // Only increment on profitable sells
231
+ largestGain = Math .max (largestGain , profit );
232
+ } else {
233
+ largestLoss = Math .min (largestLoss , profit );
234
+ }
235
+
236
+ lastBuyPrice = null ;
237
+ lastBuyQuantity = 0 ;
238
+ }
239
+ }
240
+ case SHORT -> {
241
+ lastBuyPrice = trade .price ();
242
+ lastBuyQuantity = trade .quantity ();
243
+ }
244
+ case COVER -> {
245
+ if (lastBuyPrice != null ) {
246
+ totalSells ++; // Count covers as sells for shorts
247
+ double profit = (lastBuyPrice - trade .price ()) * trade .quantity ();
248
+ totalPnL += profit ;
249
+
250
+ if (profit > 0 ) {
251
+ profitableSells ++;
252
+ largestGain = Math .max (largestGain , profit );
253
+ } else {
254
+ largestLoss = Math .min (largestLoss , profit );
255
+ }
256
+
257
+ lastBuyPrice = null ;
258
+ lastBuyQuantity = 0 ;
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+
265
+ private static class WeeklyPerformance {
266
+ private int totalSells ;
267
+ private double totalPnL ;
268
+ private double profitPerShare ;
269
+
270
+ public synchronized void recordCompletedTrade (TradeRecord buyTrade , TradeRecord sellTrade ) {
271
+ totalSells ++;
272
+ double profit ;
273
+ if (sellTrade .type () == OrderType .SELL ) {
274
+ profit = (sellTrade .price () - buyTrade .price ()) * sellTrade .quantity ();
275
+ } else { // COVER
276
+ profit = (buyTrade .price () - sellTrade .price ()) * sellTrade .quantity ();
277
+ }
278
+ profitPerShare = profit / sellTrade .quantity ();
279
+ totalPnL += profit ;
280
+ }
281
+
282
+ public boolean hasActivity () {
283
+ return totalSells > 0 || totalPnL != 0.0 ;
284
+ }
285
+ }
286
+
287
+ private record TradeRecord (
288
+ String ticker ,
289
+ OrderType type ,
290
+ int quantity ,
291
+ double price ,
292
+ double portfolioValueBefore ,
293
+ LocalDateTime timestamp
294
+ ) {}
129
295
}
0 commit comments