diff --git a/src/main/java/model/persistence/UserAccountRecord.java b/src/main/java/model/persistence/UserAccountRecord.java index 7b9a659..42e9d73 100644 --- a/src/main/java/model/persistence/UserAccountRecord.java +++ b/src/main/java/model/persistence/UserAccountRecord.java @@ -1,5 +1,7 @@ package model.persistence; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + /** * Persisted account metadata for one local profile. * @@ -8,6 +10,7 @@ * @param saltBase64 random salt used for PIN hashing * @param pinHashBase64 PBKDF2 hash of the PIN */ +@JsonIgnoreProperties(ignoreUnknown = true) public record UserAccountRecord( String username, String normalizedUsername, diff --git a/src/main/java/model/stockinfo/CompanyHealth.java b/src/main/java/model/stockinfo/CompanyHealth.java new file mode 100644 index 0000000..b8020d7 --- /dev/null +++ b/src/main/java/model/stockinfo/CompanyHealth.java @@ -0,0 +1,28 @@ +package model.stockinfo; + +/** + * Qualitative label for mock company financial health shown in stock views. + */ +public enum CompanyHealth { + + /** Healthy mock fundamentals (sufficient profit margin). */ + STRONG("Strong"), + + /** Stressed mock fundamentals (low or negative margin). */ + WEAK("Weak"); + + private final String displayLabel; + + CompanyHealth(String displayLabel) { + this.displayLabel = displayLabel; + } + + /** + * Returns the short label shown in tables and detail panels. + * + * @return user-facing health text + */ + public String displayLabel() { + return displayLabel; + } +} diff --git a/src/main/java/model/stockinfo/StockFinancialInfo.java b/src/main/java/model/stockinfo/StockFinancialInfo.java new file mode 100644 index 0000000..7e17d9a --- /dev/null +++ b/src/main/java/model/stockinfo/StockFinancialInfo.java @@ -0,0 +1,22 @@ +package model.stockinfo; + +import java.math.BigDecimal; + +/** + * Mock revenue, profit, and health snapshot for a listed stock. Values are not persisted and are + * derived deterministically from the symbol. + * + * @param revenue mock total revenue in currency units (same scale as {@link #profit}) + * @param profit mock net profit in currency units + * @param health qualitative indicator derived from the mock margin + */ +public record StockFinancialInfo(BigDecimal revenue, BigDecimal profit, CompanyHealth health) { + + /** + * Creates a financial snapshot with defensive copies of decimal fields. + */ + public StockFinancialInfo { + revenue = revenue.stripTrailingZeros(); + profit = profit.stripTrailingZeros(); + } +} diff --git a/src/main/java/model/stockinfo/StockFinancialInfoProvider.java b/src/main/java/model/stockinfo/StockFinancialInfoProvider.java new file mode 100644 index 0000000..041b3fc --- /dev/null +++ b/src/main/java/model/stockinfo/StockFinancialInfoProvider.java @@ -0,0 +1,82 @@ +package model.stockinfo; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Objects; +import java.util.Random; +import model.Stock; + +/** + * Supplies deterministic mock fundamentals per stock symbol so UI can show simplified financials + * without extending {@link Stock} or persistence. + */ +public class StockFinancialInfoProvider { + + private static final BigDecimal MILLION = BigDecimal.valueOf(1_000_000L); + private static final BigDecimal MIN_REVENUE_M = BigDecimal.valueOf(10); + private static final BigDecimal REVENUE_SPAN_M = BigDecimal.valueOf(90); + /** Minimum positive profit-to-revenue ratio to classify as {@link CompanyHealth#STRONG}. */ + private static final BigDecimal STRONG_MARGIN = new BigDecimal("0.10"); + + /** + * Builds mock fundamentals for the given ticker symbol. + * + * @param symbol non-null stock symbol + * @return immutable mock financial snapshot + */ + public StockFinancialInfo forSymbol(String symbol) { + Objects.requireNonNull(symbol, "symbol"); + Random rnd = new Random(symbol.hashCode()); + BigDecimal revenueMillions = MIN_REVENUE_M.add(randomFraction(rnd).multiply(REVENUE_SPAN_M)) + .setScale(1, RoundingMode.HALF_UP); + BigDecimal revenue = revenueMillions.multiply(MILLION); + double margin = rnd.nextDouble() * 0.30 - 0.05; + BigDecimal profit = + revenue.multiply(BigDecimal.valueOf(margin)).setScale(2, RoundingMode.HALF_UP); + CompanyHealth health = classifyHealth(revenue, profit); + return new StockFinancialInfo(revenue, profit, health); + } + + /** + * Same as {@link #forSymbol(String)} using the stock's symbol. + * + * @param stock non-null stock + * @return mock financial snapshot + */ + public StockFinancialInfo forStock(Stock stock) { + Objects.requireNonNull(stock, "stock"); + return forSymbol(stock.getSymbol()); + } + + /** + * Formats a mock monetary amount for display (millions with one decimal when large). + * + * @param amount full currency amount + * @return compact USD-style string + */ + public String formatMoney(BigDecimal amount) { + Objects.requireNonNull(amount, "amount"); + BigDecimal abs = amount.abs(); + if (abs.compareTo(MILLION) >= 0) { + BigDecimal millions = amount.divide(MILLION, 1, RoundingMode.HALF_UP); + String sign = amount.signum() < 0 ? "-" : ""; + return sign + "$" + millions.abs().toPlainString() + "M"; + } + return "$" + amount.setScale(2, RoundingMode.HALF_UP).toPlainString(); + } + + private static BigDecimal randomFraction(Random rnd) { + return BigDecimal.valueOf(rnd.nextDouble()); + } + + private static CompanyHealth classifyHealth(BigDecimal revenue, BigDecimal profit) { + if (revenue.signum() <= 0) { + return CompanyHealth.WEAK; + } + if (profit.signum() <= 0) { + return CompanyHealth.WEAK; + } + BigDecimal margin = profit.divide(revenue, 4, RoundingMode.HALF_UP); + return margin.compareTo(STRONG_MARGIN) >= 0 ? CompanyHealth.STRONG : CompanyHealth.WEAK; + } +} diff --git a/src/main/java/view/StockDetailView.java b/src/main/java/view/StockDetailView.java index a947e50..0e0700a 100644 --- a/src/main/java/view/StockDetailView.java +++ b/src/main/java/view/StockDetailView.java @@ -16,16 +16,20 @@ import javafx.scene.text.FontWeight; import model.Stock; import model.marketevent.MarketEvent; +import model.stockinfo.StockFinancialInfo; +import model.stockinfo.StockFinancialInfoProvider; import recommendation.StockRecommendation; import recommendation.StockRecommendationService; import view.components.chart.StockChart; import view.components.recommendation.StockRecommendationLabel; /** - * Dedicated stock detail view showing summary data, trend-based recommendation, and price history. + * Dedicated stock detail view showing summary data, mock company fundamentals, trend-based + * recommendation, and price history. * *
The recommendation is computed through {@link StockRecommendationService}, keeping expert
- * advice presentation separate from the stock's actual price-update logic.
+ * advice presentation separate from the stock's actual price-update logic. Mock revenue and profit
+ * come from {@link StockFinancialInfoProvider}.
*
* @author kaamyashinde
* @version 1.0.0
@@ -34,12 +38,18 @@
public class StockDetailView extends BorderPane {
private final StockRecommendationService recommendationService = new StockRecommendationService();
+ private final StockFinancialInfoProvider financialInfoProvider = new StockFinancialInfoProvider();
private final Label titleLabel = new Label("Stock details");
private final Label subtitleLabel = new Label("Select a stock to inspect its latest trend.");
private final Label dayLabel = new Label("Trading day: -");
private final Label latestPriceLabel = new Label("Latest price: -");
private final Label marketEventLabel = new Label("Latest market event: none");
+ private final Label fundamentalsHeading = new Label("Company fundamentals");
+ private final Label revenueLabel = new Label("Revenue: -");
+ private final Label profitLabel = new Label("Profit: -");
+ private final Label healthLabel = new Label("Health: -");
+ private final VBox fundamentalsBox;
private final Label marketHistoryHeading = new Label("Past events");
private final Label basisLabel = new Label("Recommendation basis: recent price trend");
private final StockRecommendationLabel recommendationLabel =
@@ -72,7 +82,28 @@ public StockDetailView() {
marketHistoryList.setMouseTransparent(true);
marketHistoryList.setMaxHeight(140);
- VBox header = new VBox(6, titleLabel, subtitleLabel, dayLabel, latestPriceLabel, marketEventLabel);
+ fundamentalsHeading.setFont(Font.font("System", FontWeight.BOLD, 14));
+ revenueLabel.setWrapText(true);
+ profitLabel.setWrapText(true);
+ healthLabel.setWrapText(true);
+ fundamentalsBox =
+ new VBox(4, fundamentalsHeading, revenueLabel, profitLabel, healthLabel);
+ fundamentalsBox.setStyle(
+ "-fx-background-color: #f9fafc;"
+ + "-fx-border-color: #e2e6ee;"
+ + "-fx-background-radius: 10;"
+ + "-fx-border-radius: 10;"
+ + "-fx-padding: 12;");
+
+ VBox header =
+ new VBox(
+ 10,
+ titleLabel,
+ subtitleLabel,
+ dayLabel,
+ latestPriceLabel,
+ marketEventLabel,
+ fundamentalsBox);
recommendationBox = new VBox(8, new Label("Recommendation"), recommendationLabel, basisLabel);
recommendationBox.setStyle(
"-fx-background-color: #f6f7fb;"
@@ -133,6 +164,7 @@ public void showStock(
subtitleLabel.setText("Select a stock to inspect its latest trend.");
latestPriceLabel.setText("Latest price: -");
marketEventLabel.setText("Latest market event: none");
+ clearFundamentalsLabels();
marketHistoryList.setItems(FXCollections.observableArrayList());
recommendationLabel.setRecommendation(StockRecommendation.HOLD);
placeholderLabel.setText("Choose a stock from the list to view chart and recommendation details.");
@@ -144,6 +176,7 @@ public void showStock(
subtitleLabel.setText("Single-stock detail view with trend recommendation.");
latestPriceLabel.setText("Latest price: " + formatLatestPrice(stock));
marketEventLabel.setText(buildMarketEventText(stock, marketEvent));
+ applyFundamentalsLabels(stock);
marketHistoryList.setItems(FXCollections.observableArrayList(buildMarketHistoryItems(marketHistory)));
recommendationLabel.setRecommendation(recommendationService.recommend(stock));
@@ -234,12 +267,52 @@ public List