Skip to content

Commit 82d150c

Browse files
feat: much more detailed statistics
feat: improved error handling feat: auto sell on backtrack conclusion
1 parent 6473e3d commit 82d150c

File tree

9 files changed

+450
-48
lines changed

9 files changed

+450
-48
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>net.swofty</groupId>
88
<artifactId>stockmarkettester</artifactId>
9-
<version>1.0-SNAPSHOT</version>
9+
<version>1.1.0</version>
1010

1111
<properties>
1212
<maven.compiler.source>21</maven.compiler.source>
Lines changed: 205 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
package 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;
47
import java.time.temporal.ChronoUnit;
5-
import java.util.ArrayList;
6-
import java.util.List;
8+
import java.util.*;
79
import java.util.concurrent.atomic.AtomicInteger;
810
import java.util.concurrent.atomic.DoubleAdder;
11+
import java.util.concurrent.ConcurrentHashMap;
912

1013
public 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("\nWeekly 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
}

src/main/java/net/swofty/Portfolio.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ public synchronized void coverShort(String ticker, int quantity, double price) {
105105
marginAvailable += quantity * shortPosition.entryPrice() * MARGIN_REQUIREMENT;
106106
}
107107

108+
public Map<String, Position> getAllPositions() {
109+
return positions;
110+
}
111+
112+
public Map<String, net.swofty.orders.Short> getAllShortPositions() {
113+
return shortPositions;
114+
}
115+
108116
// Stop order methods
109117
public void setStopLoss(String ticker, double stopPrice, int quantity) {
110118
totalPositions++;

src/main/java/net/swofty/builtin/TestAlgorithm.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313

1414
public class TestAlgorithm {
1515
public static void main(String[] args) {
16-
AlphaVantageFetcher.setup("KEY", Path.of("PATH/StockMarketAlgorithmMaker/vantage-cache/"));
17-
1816
// Create market service
1917
HistoricalMarketService marketService = new HistoricalMarketService(
2018
new AlphaVantageProvider("KEY"),
@@ -36,6 +34,7 @@ public static void main(String[] args) {
3634
.withInterval(Duration.ofMinutes(1))
3735
.withRunOnMarketClosed(true) // Enable running outside market hours
3836
.withProvider(marketService) // Use the initialized service
37+
.withAutomaticallySellOnFinish(true)
3938
.withAlgorithm(new SimpleBuyAndHoldAlgorithm("simple-day-trader", tickers), 1_000_000)
4039
.run()
4140
.thenAccept(results -> {

src/main/java/net/swofty/data/HistoricalMarketService.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ public CompletableFuture<Void> initialize(Set<String> tickers, int previousDays,
108108
if (cachedData != null) {
109109
historicalCache.put(ticker, cachedData);
110110
System.out.println("Loaded cached data for " + ticker);
111+
success = true;
111112
continue;
112113
}
113114
}
@@ -120,6 +121,8 @@ public CompletableFuture<Void> initialize(Set<String> tickers, int previousDays,
120121
Set.of(ticker), start, end, marketConfig).get();
121122
historicalCache.put(ticker, data);
122123

124+
long cooldown = (60 / provider.getRateLimit()) * 1000;
125+
123126
// Save to cache if enabled
124127
if (cacheDirectory.isPresent()) {
125128
saveToCache(ticker, data, start, end);
@@ -129,7 +132,11 @@ public CompletableFuture<Void> initialize(Set<String> tickers, int previousDays,
129132
}
130133

131134
success = true;
132-
Thread.sleep((60 / provider.getRateLimit()) * 1000);
135+
int amountOfTickersLeft = tickers.size() - attempts;
136+
if (amountOfTickersLeft > 0) {
137+
System.out.println("Waiting " + cooldown + "ms due to rate limits");
138+
Thread.sleep(cooldown);
139+
}
133140
} else {
134141
throw new RuntimeException("Provider is not available");
135142
}

src/main/java/net/swofty/data/providers/AlphaVantageProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ private void validateResponse(HttpResponse<String> response) {
116116
throw new MarketDataException("API Limit Reached: " + root.get("Note").asText(), null);
117117
}
118118
} catch (Exception e) {
119-
throw new MarketDataException("Failed to validate response", e);
119+
throw new MarketDataException("Failed to validate response, your ticker may be invalid", e);
120120
}
121121
}
122122

0 commit comments

Comments
 (0)