Conversation
Implemented system interfaces and external data integration for share price retrieval using Clean Architecture principles. Added Alpha Vantage support with API key configuration via environment variables and introduced an output size setting to stay within free tier limits. Implemented basic rate limiting of one request per second to prevent throttling when comparing multiple symbols. Improved error handling to detect API specific responses such as Information, Note, and Error Message despite HTTP 200 status codes, with appropriate logging. Fixed parsing logic to reliably extract closing prices from the Time Series Daily data. Ensured data is converted into domain models and cached locally to support offline access and improve system reliability.
There was a problem hiding this comment.
Pull request overview
This PR appears to introduce a new Alpha Vantage–backed market data path (with in-memory persistence) and updates the Thymeleaf UI flow to compare symbols via a GET request, alongside some project configuration/documentation tweaks.
Changes:
- Add Alpha Vantage service + repository abstractions and wire them into a new Spring Boot entrypoint.
- Update the web UI controller and template to fetch/plot comparison data via
GET /compare. - Add runtime configuration/docs, but also (unintentionally) commit multiple
.DS_Storefiles.
Reviewed changes
Copilot reviewed 11 out of 17 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| stockscope/src/main/resources/templates/index.html | Switch compare form to GET and tweak chart/table markup + empty-state messaging |
| stockscope/src/main/resources/application.properties | Add Alpha Vantage key/outputsize config and dev-oriented Spring settings |
| stockscope/src/main/java/com/sharecomparison/StockComparisonApplication.java | Add new Spring Boot entrypoint and explicit beans for repository + market data service |
| stockscope/src/main/java/com/sharecomparison/presentation/WebPageController.java | Replace prior compare flow with direct MarketDataService fetch + chart building in controller |
| stockscope/src/main/java/com/sharecomparison/infrastructure/InMemoryPriceRepository.java | Add simple in-memory store for fetched prices |
| stockscope/src/main/java/com/sharecomparison/infrastructure/AlphaVantageService.java | Implement Alpha Vantage HTTP client, parsing, and rate limiting |
| stockscope/src/main/java/com/sharecomparison/application/PriceRepository.java | Introduce repository interface for storing/loading prices |
| stockscope/src/main/java/com/sharecomparison/application/MarketDataService.java | Introduce service interface for fetching prices |
| stockscope/src/main/java/com/.DS_Store | macOS metadata file added (should not be committed) |
| stockscope/src/main/java/.DS_Store | macOS metadata file added (should not be committed) |
| stockscope/src/main/.DS_Store | macOS metadata file added (should not be committed) |
| stockscope/src/.DS_Store | macOS metadata file added (should not be committed) |
| stockscope/pom.xml | Configure Spring Boot Maven plugin mainClass (and set explicit plugin version) |
| stockscope/.DS_Store | macOS metadata file added (should not be committed) |
| README.md | Update run instructions and add API key setup notes |
| Meeting-Documentation/Meeting-2.md | Adjust task ownership line |
| .DS_Store | macOS metadata file added (should not be committed) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| LocalDate start = (startDate != null && !startDate.isBlank()) ? LocalDate.parse(startDate) : LocalDate.now().minusYears(1); | ||
| LocalDate end = (endDate != null && !endDate.isBlank()) ? LocalDate.parse(endDate) : LocalDate.now(); |
There was a problem hiding this comment.
LocalDate.parse(startDate/endDate) will throw DateTimeParseException for malformed query params; because these are plain Strings, a crafted URL can crash the request with a 500. Consider using @RequestParam @DateTimeFormat LocalDate (as before) or catching parsing errors and returning a user-facing validation message.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| LocalDate start = (startDate != null && !startDate.isBlank()) ? LocalDate.parse(startDate) : LocalDate.now().minusYears(1); | ||
| LocalDate end = (endDate != null && !endDate.isBlank()) ? LocalDate.parse(endDate) : LocalDate.now(); | ||
|
|
||
| LocalDate effectiveEnd = endDate == null ? LocalDate.now() : endDate; | ||
| LocalDate effectiveStart = startDate == null ? effectiveEnd.minusMonths(6) : startDate; | ||
| symbol1 = symbol1.trim().toUpperCase(); | ||
| symbol2 = symbol2.trim().toUpperCase(); | ||
|
|
||
| model.addAttribute("symbol1", s1); | ||
| model.addAttribute("symbol2", s2); | ||
| model.addAttribute("startDate", effectiveStart); | ||
| model.addAttribute("endDate", effectiveEnd); | ||
| List<PriceData> data1 = marketDataService.fetchSharePrices(symbol1, start, end); | ||
| List<PriceData> data2 = marketDataService.fetchSharePrices(symbol2, start, end); | ||
|
|
||
| if (s1.isEmpty() || s2.isEmpty()) { | ||
| model.addAttribute("error", "Both symbols are required."); | ||
| if (data1.isEmpty() || data2.isEmpty()) { | ||
| model.addAttribute("error", "No data available for one or both symbols in this range."); |
There was a problem hiding this comment.
The previous startDate <= endDate validation was removed. When start is after end, the API call/filtering will likely return empty data and show the misleading "No data available" error instead of prompting the user to fix the date range.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| int n = Math.min(data1.size(), data2.size()); | ||
| double xStep = n > 1 ? chartWidth / (n - 1) : 0; | ||
|
|
||
| StringBuilder path1 = new StringBuilder(); | ||
| StringBuilder path2 = new StringBuilder(); | ||
| List<Map<String, Object>> points1 = new ArrayList<>(); | ||
| List<Map<String, Object>> points2 = new ArrayList<>(); | ||
|
|
||
| for (int i = 0; i < n; i++) { | ||
| double x = left + i * xStep; | ||
|
|
||
| PriceData p1 = data1.get(i); | ||
| double y1 = bottom - ((p1.getClosingPrice() - minPrice) / priceRange) * chartHeight; | ||
| path1.append(i == 0 ? "M" : "L").append(x).append(",").append(y1).append(" "); | ||
| points1.add(Map.of("x", x, "y", y1, "value", p1.getClosingPrice())); | ||
|
|
||
| PriceData p2 = data2.get(i); | ||
| double y2 = bottom - ((p2.getClosingPrice() - minPrice) / priceRange) * chartHeight; | ||
| path2.append(i == 0 ? "M" : "L").append(x).append(",").append(y2).append(" "); | ||
| points2.add(Map.of("x", x, "y", y2, "value", p2.getClosingPrice())); | ||
| } |
There was a problem hiding this comment.
Chart building uses n = min(data1.size(), data2.size()) and then pairs points by index, which can compare different dates if one symbol has missing trading days/data points. Align series by date (intersection/union) before plotting so each x-coordinate represents the same date for both symbols.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| // Chart coords calculation | ||
| double chartWidth = 800, chartHeight = 300; | ||
| double left = 80, right = left + chartWidth; | ||
| double bottom = 320, top = bottom - chartHeight; | ||
| double midY = top + chartHeight / 2; | ||
|
|
||
| double minPrice = Double.MAX_VALUE, maxPrice = Double.MIN_VALUE; | ||
| for (PriceData p : data1) { | ||
| minPrice = Math.min(minPrice, p.getClosingPrice()); | ||
| maxPrice = Math.max(maxPrice, p.getClosingPrice()); | ||
| } | ||
| for (PriceData p : data2) { | ||
| minPrice = Math.min(minPrice, p.getClosingPrice()); | ||
| maxPrice = Math.max(maxPrice, p.getClosingPrice()); | ||
| } | ||
| if (maxPrice == minPrice) maxPrice += 1.0; | ||
| double priceRange = maxPrice - minPrice; | ||
|
|
||
| int n = Math.min(data1.size(), data2.size()); | ||
| double xStep = n > 1 ? chartWidth / (n - 1) : 0; | ||
|
|
||
| StringBuilder path1 = new StringBuilder(); | ||
| StringBuilder path2 = new StringBuilder(); | ||
| List<Map<String, Object>> points1 = new ArrayList<>(); | ||
| List<Map<String, Object>> points2 = new ArrayList<>(); | ||
|
|
||
| for (int i = 0; i < n; i++) { | ||
| double x = left + i * xStep; | ||
|
|
||
| PriceData p1 = data1.get(i); | ||
| double y1 = bottom - ((p1.getClosingPrice() - minPrice) / priceRange) * chartHeight; | ||
| path1.append(i == 0 ? "M" : "L").append(x).append(",").append(y1).append(" "); | ||
| points1.add(Map.of("x", x, "y", y1, "value", p1.getClosingPrice())); | ||
|
|
||
| PriceData p2 = data2.get(i); | ||
| double y2 = bottom - ((p2.getClosingPrice() - minPrice) / priceRange) * chartHeight; | ||
| path2.append(i == 0 ? "M" : "L").append(x).append(",").append(y2).append(" "); | ||
| points2.add(Map.of("x", x, "y", y2, "value", p2.getClosingPrice())); | ||
| } | ||
|
|
||
| Map<String, Object> chartData = new HashMap<>(); | ||
| chartData.put("left", left); | ||
| chartData.put("right", right); | ||
| chartData.put("top", top); | ||
| chartData.put("bottom", bottom); | ||
| chartData.put("midY", midY); | ||
| chartData.put("symbol1Path", path1.toString()); | ||
| chartData.put("symbol2Path", path2.toString()); | ||
| chartData.put("symbol1Points", points1); | ||
| chartData.put("symbol2Points", points2); | ||
|
|
||
| Map<String, Object> result = new HashMap<>(); | ||
| result.put("symbol1", symbol1); | ||
| result.put("symbol2", symbol2); | ||
| result.put("startDate", start); | ||
| result.put("endDate", end); | ||
| result.put("chartData", chartData); | ||
| result.put("symbol1Data", data1); | ||
| result.put("symbol2Data", data2); |
There was a problem hiding this comment.
This controller reimplements chart/coordinate logic and returns a loosely-typed Map for "result", even though the codebase already has ComparisonResult/ChartData and a ChartBuilder service. Reusing those existing domain types/services would reduce duplication and keep web + API behavior consistent.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| private List<PriceData> storedPrices = new ArrayList<>(); | ||
|
|
||
| @Override | ||
| public void savePrices(List<PriceData> prices) { | ||
| storedPrices.addAll(prices); | ||
| } | ||
|
|
||
| @Override | ||
| public List<PriceData> loadPrices( | ||
| String symbol, | ||
| LocalDate startDate, | ||
| LocalDate endDate) { | ||
|
|
||
| List<PriceData> results = new ArrayList<>(); | ||
|
|
||
| for (PriceData p : storedPrices) { | ||
|
|
||
| if (p.getSymbol().equals(symbol) | ||
| && !p.getDate().isBefore(startDate) | ||
| && !p.getDate().isAfter(endDate)) { | ||
|
|
||
| results.add(p); | ||
| } |
There was a problem hiding this comment.
storedPrices is a mutable ArrayList accessed without synchronization; in a web app this repository can be hit concurrently, causing data races / ConcurrentModificationException during iteration. Consider using a thread-safe structure (or synchronizing access) and marking the field final.
| @Override | ||
| public List<PriceData> fetchSharePrices(String symbol, LocalDate startDate, LocalDate endDate) { | ||
| try { | ||
| if (symbol == null || symbol.isBlank()) { | ||
| return repository.loadPrices(symbol, startDate, endDate); | ||
| } | ||
|
|
||
| String json = fetchTimeSeriesDailyJson(symbol, outputSize); | ||
| if (json == null) { | ||
| return repository.loadPrices(symbol, startDate, endDate); | ||
| } |
There was a problem hiding this comment.
fetchSharePrices() always calls Alpha Vantage first and only uses the repository as a fallback on errors/throttling. This means cached data is never used to avoid external calls, increasing latency and the chance of hitting rate limits. Consider checking the repository for an existing result (or partially cached range) before making the HTTP request, and only call the API for missing data.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| @SpringBootApplication | ||
| public class StockComparisonApplication { | ||
|
|
||
| public static void main(String[] args) { | ||
| SpringApplication.run(StockComparisonApplication.class, args); | ||
| } | ||
|
|
||
| @Bean | ||
| public PriceRepository priceRepository() { | ||
| return new InMemoryPriceRepository(); | ||
| } | ||
|
|
||
| @Bean | ||
| public MarketDataService marketDataService( | ||
| PriceRepository repository, | ||
| @Value("${alphavantage.key:demo}") String apiKey, | ||
| @Value("${alphavantage.outputsize:compact}") String outputSize) { | ||
| return new AlphaVantageService(repository, apiKey, outputSize); | ||
| } |
There was a problem hiding this comment.
The project already has an @SpringBootApplication entrypoint (com.sharecomparison.presentation.WebUI). Adding a second one can confuse IDE run configurations and make it unclear which application class is authoritative. Consider consolidating to a single entrypoint (either remove WebUI or move these @Bean definitions into the existing entrypoint/config).
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| - Review Sprint 1 for code review + fix any issues (**Luke + Sameer**) | ||
| - Support team with repository processes (**Jack + Azlan**) | ||
| - Final check of architecture diagram to ensure it matches code structure (**Azlan**) | ||
| - Update Kanban with Sprint 2 tasks from feedback (**Luke + Jack**) |
|
@lukepring I've opened a new pull request, #56, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
@lukepring I've opened a new pull request, #57, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
@lukepring I've opened a new pull request, #58, to work on those changes. Once the pull request is ready, I'll request review from you. |
…ageController.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
@lukepring I've opened a new pull request, #59, to work on those changes. Once the pull request is ready, I'll request review from you. |
Co-authored-by: lukepring <68820847+lukepring@users.noreply.github.com>
…MemoryPriceRepository.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: lukepring <68820847+lukepring@users.noreply.github.com>
|
@lukepring I've opened a new pull request, #60, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
@lukepring I've opened a new pull request, #61, to work on those changes. Once the pull request is ready, I'll request review from you. |
Co-authored-by: lukepring <68820847+lukepring@users.noreply.github.com>
Co-authored-by: lukepring <68820847+lukepring@users.noreply.github.com>
Co-authored-by: lukepring <68820847+lukepring@users.noreply.github.com>
Co-authored-by: lukepring <68820847+lukepring@users.noreply.github.com>
Fix chart date alignment: use date intersection instead of index-based pairing
Remove redundant WebUI @SpringBootApplication entrypoint
Cache-first strategy in AlphaVantageService to avoid redundant API calls
Co-authored-by: lukepring <68820847+lukepring@users.noreply.github.com>
Refactor WebPageController to use IPriceController and ComparisonResult
Co-authored-by: lukepring <68820847+lukepring@users.noreply.github.com>
Restore compare flow and add startDate ≤ endDate validation
Fix unhandled DateTimeParseException on malformed date query params
|
@copilot resolve merge conflicts pls |
|
@lukepring I've opened a new pull request, #62, to work on those changes. Once the pull request is ready, I'll request review from you. |
Co-authored-by: lukepring <68820847+lukepring@users.noreply.github.com>
[WIP] Update market data path and Thymeleaf UI flow
No description provided.