11package net .swofty ;
22
3- import java .time .LocalDateTime ;
3+ import net .swofty .orders .OrderType ;
4+
5+ import java .time .*;
6+ import java .time .format .DateTimeFormatter ;
47import java .time .temporal .ChronoUnit ;
5- import java .util .ArrayList ;
6- import java .util .List ;
8+ import java .util .*;
79import java .util .concurrent .atomic .AtomicInteger ;
810import java .util .concurrent .atomic .DoubleAdder ;
11+ import java .util .concurrent .ConcurrentHashMap ;
912
1013public class AlgorithmStatistics {
1114 private final String algorithmId ;
1215 private final LocalDateTime startTime ;
13- private final AtomicInteger totalTrades ;
1416 private final DoubleAdder totalProfit ;
1517 private final DoubleAdder maxDrawdown ;
1618 private final DoubleAdder peakValue ;
@@ -19,6 +21,13 @@ public class AlgorithmStatistics {
1921 private final List <Double > dailyReturns ;
2022 private volatile double initialValue ;
2123
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+
2231 public AlgorithmStatistics (String algorithmId , double initialValue , LocalDateTime startTime ) {
2332 this .algorithmId = algorithmId ;
2433 this .totalTrades = new AtomicInteger (0 );
@@ -31,18 +40,51 @@ public AlgorithmStatistics(String algorithmId, double initialValue, LocalDateTim
3140 this .startTime = startTime ;
3241 this .initialValue = initialValue ;
3342 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 ();
3478 }
3579
3680 public void updateStatistics (double currentValue , double riskFreeRate ) {
37- // Update total profit/loss
81+ // Existing statistics logic
3882 totalProfit .reset ();
3983 totalProfit .add (currentValue - initialValue );
4084
41- // Update total value
4285 totalValue .reset ();
4386 totalValue .add (currentValue );
4487
45- // Update peak value and calculate drawdown
4688 if (currentValue > peakValue .sum ()) {
4789 peakValue .reset ();
4890 peakValue .add (currentValue );
@@ -53,13 +95,14 @@ public void updateStatistics(double currentValue, double riskFreeRate) {
5395 maxDrawdown .add (currentDrawdown );
5496 }
5597
56- // Calculate and store daily return
5798 double dailyReturn = (currentValue - initialValue ) / initialValue ;
5899 dailyReturns .add (dailyReturn );
59100
60- // Calculate Sharpe Ratio using proper daily returns
61101 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 );
63106 double stdDev = calculateStandardDeviation (dailyReturns , averageReturn );
64107 double annualizedSharpe = stdDev != 0 ?
65108 (Math .sqrt (252 ) * (averageReturn - riskFreeRate /252 ) / stdDev ) : 0 ;
@@ -78,33 +121,15 @@ private double calculateStandardDeviation(List<Double> returns, double mean) {
78121 );
79122 }
80123
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-
101124 @ Override
102125 public String toString () {
126+ StringBuilder sb = new StringBuilder ();
103127 long daysRun = ChronoUnit .DAYS .between (startTime , LocalDateTime .now ());
104128 double annualizedReturn = dailyReturns .isEmpty () ? 0 :
105129 Math .pow (1 + dailyReturns .get (dailyReturns .size () - 1 ), 252 ) - 1 ;
106130
107- return String .format ("""
131+ // Overall Performance
132+ sb .append (String .format ("""
108133 Algorithm Statistics for %s:
109134 Backtest Period: %d days
110135 Total Trades: %d
@@ -114,16 +139,157 @@ public String toString() {
114139 Sharpe Ratio: %.2f
115140 Average Trades Per Day: %.2f
116141 Total Value: $%.2f
142+
143+ Per-Ticker Performance:
144+ =====================
117145 """ ,
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 (),
125148 daysRun > 0 ? (double ) totalTrades .get () / daysRun : 0 ,
126149 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 ();
128206 }
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+ ) {}
129295}
0 commit comments