Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/main/java/model/persistence/UserAccountRecord.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package model.persistence;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

/**
* Persisted account metadata for one local profile.
*
Expand All @@ -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,
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/model/stockinfo/CompanyHealth.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
22 changes: 22 additions & 0 deletions src/main/java/model/stockinfo/StockFinancialInfo.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
82 changes: 82 additions & 0 deletions src/main/java/model/stockinfo/StockFinancialInfoProvider.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
79 changes: 76 additions & 3 deletions src/main/java/view/StockDetailView.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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
Expand All @@ -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 =
Expand Down Expand Up @@ -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;"
Expand Down Expand Up @@ -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.");
Expand All @@ -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));

Expand Down Expand Up @@ -234,12 +267,52 @@ public List<String> getDisplayedMarketHistory() {
return List.copyOf(marketHistoryList.getItems());
}

/**
* Returns the revenue line currently shown under company fundamentals.
*
* @return revenue label text
*/
public String getRevenueLabelText() {
return revenueLabel.getText();
}

/**
* Returns the profit line currently shown under company fundamentals.
*
* @return profit label text
*/
public String getProfitLabelText() {
return profitLabel.getText();
}

/**
* Returns the health line currently shown under company fundamentals.
*
* @return health label text
*/
public String getHealthLabelText() {
return healthLabel.getText();
}

/**
* Formats the latest price if one exists.
*
* @param stock stock whose latest price should be shown
* @return formatted latest price or placeholder text
*/
private void clearFundamentalsLabels() {
revenueLabel.setText("Revenue: -");
profitLabel.setText("Profit: -");
healthLabel.setText("Health: -");
}

private void applyFundamentalsLabels(Stock stock) {
StockFinancialInfo fin = financialInfoProvider.forStock(stock);
revenueLabel.setText("Revenue: " + financialInfoProvider.formatMoney(fin.revenue()));
profitLabel.setText("Profit: " + financialInfoProvider.formatMoney(fin.profit()));
healthLabel.setText("Health: " + fin.health().displayLabel());
}

private static String formatLatestPrice(Stock stock) {
if (stock.getHistoricalPrices().isEmpty()) {
return "-";
Expand Down
20 changes: 18 additions & 2 deletions src/main/java/view/StocksListPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
import javafx.scene.text.Text;
import model.Exchange;
import model.Stock;
import model.stockinfo.StockFinancialInfoProvider;
import model.marketevent.MarketEvent;

/**
* JavaFX panel listing all stocks on an {@link Exchange}: symbol, company, and latest price.
* JavaFX panel listing all stocks on an {@link Exchange}: symbol, company, latest price, mock
* revenue, and health indicator.
* Prices reflect the same {@link Stock} instances as the rest of the demo; use {@link #refresh()}
* after trading days advance so the table re-renders updated values.
*
Expand All @@ -36,6 +38,7 @@
public class StocksListPanel extends BorderPane {

private final Exchange exchange;
private final StockFinancialInfoProvider financialInfoProvider = new StockFinancialInfoProvider();
private final Label metaLabel = new Label();
private final TableView<Stock> table = new TableView<>();
private final ObservableList<Stock> rows = FXCollections.observableArrayList();
Expand Down Expand Up @@ -96,7 +99,20 @@ private void buildTable() {
colPrice.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().getSalesPrice().toPlainString()));

table.getColumns().setAll(List.of(colSym, colCompany, colPrice));
TableColumn<Stock, String> colRevenue = new TableColumn<>("Revenue (mock)");
colRevenue.setCellValueFactory(
c ->
new SimpleStringProperty(
financialInfoProvider.formatMoney(
financialInfoProvider.forStock(c.getValue()).revenue())));

TableColumn<Stock, String> colHealth = new TableColumn<>("Health");
colHealth.setCellValueFactory(
c ->
new SimpleStringProperty(
financialInfoProvider.forStock(c.getValue()).health().displayLabel()));

table.getColumns().setAll(List.of(colSym, colCompany, colPrice, colRevenue, colHealth));
}

/**
Expand Down
Loading
Loading