diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java index 88ca0b0..9c5796a 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java @@ -6,9 +6,7 @@ import edu.ntnu.idi.idatt2003.g40.mappe.model.Share; import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; import edu.ntnu.idi.idatt2003.g40.mappe.model.Transaction; -import edu.ntnu.idi.idatt2003.g40.mappe.service.PurchaseCalculator; -import edu.ntnu.idi.idatt2003.g40.mappe.service.SaleCalculator; -import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionCalculator; +import edu.ntnu.idi.idatt2003.g40.mappe.service.*; import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventData; import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventPublisher; @@ -199,11 +197,59 @@ public Transaction sell(final Share share, final Player player) return sale; } + /** + * Method called when a player sells share. + * + * @param amount the amount of "shares" to sell. + * @param stockSymbol the stock to sell shares in. + * @param player the player buying stock. + * + * @return Transaction representing the transaction. + * + * @throws IllegalArgumentException if any parameter is null, or if player does not have enough shares. + * */ + public List sell(BigDecimal amount, final String stockSymbol, final Player player) + throws IllegalArgumentException { + if (amount == null || player == null || !Validator.NOT_EMPTY.isValid(stockSymbol)) { + throw new IllegalArgumentException("Invalid sell!"); + } else { + + List sharesOfStock = player.getPortfolio().getShares().stream() + .filter(s -> s.getStock().getSymbol().equals(stockSymbol)) + .toList(); + + BigDecimal totalOwned = player.getPortfolio().getTotalSharesBySymbol(stockSymbol); + + if (amount.compareTo(totalOwned) > 0) { + amount = totalOwned; + } + ArrayList transactions = new ArrayList<>(); + BigDecimal remainingToSell = amount; + + for (Share share : sharesOfStock) { + if (remainingToSell.compareTo(BigDecimal.ZERO) <= 0) { + break; + } + + BigDecimal shareQty = share.getQuantity(); + + if (shareQty.compareTo(remainingToSell) <= 0) { + remainingToSell = remainingToSell.subtract(shareQty); + transactions.add(sell(share, player)); + } else { + Share newShare = player.getPortfolio().splitShare(share, remainingToSell); + remainingToSell = BigDecimal.ZERO; + transactions.add(sell(newShare, player)); + } + } + return transactions; + } + } + /** * Method for advancing time, increasing the amount of weeks. * */ public void advance() { - week.set(week.get() + 1); for (Stock stock : stockMap.values()) { BigDecimal currentPrice = stock.getSalesPrice(); @@ -213,6 +259,7 @@ public void advance() { BigDecimal newPrice = currentPrice.multiply(factor); stock.addNewSalesPrice(newPrice); } + week.set(week.get() + 1); } /** diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java index 27af458..9cac71e 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Player.java @@ -41,6 +41,11 @@ public final class Player { * */ private final FloatProperty networthAsFloatProp = new SimpleFloatProperty(0); + /** + * Current money of player as a listenable {@link FloatProperty} object. + * */ + private final FloatProperty moneyAsFloatProp = new SimpleFloatProperty(0); + /** * The players' portfolio, holding their shares. * */ @@ -68,6 +73,7 @@ public Player(final String name, final BigDecimal startingMoney) throws IllegalA this.startingMoney = startingMoney; this.money = this.startingMoney; this.networthAsFloatProp.setValue(this.startingMoney); + this.moneyAsFloatProp.setValue(this.startingMoney); this.portfolio = new Portfolio(); this.transactionArchive = new TransactionArchive(); } @@ -115,7 +121,6 @@ public void addMoney(final BigDecimal amount) { */ public void withdrawMoney(final BigDecimal amount) { money = money.subtract(amount); - } /** @@ -155,6 +160,15 @@ public FloatProperty getNetWorthAsFloatProperty() { return networthAsFloatProp; } + /** + * Get money as a {@link FloatProperty} object, allowing listening for changes. + * + * @return FloatProperty. + * */ + public FloatProperty getMoneyAsFloatProperty() { + return moneyAsFloatProp; + } + /** * Getter method for players' current status. * @@ -172,17 +186,20 @@ public PlayerStatus getStatus() { * @param transaction the transaction to handle. * */ public void handleTransaction(final Transaction transaction) { - if (money.floatValue() > transaction.getCalculator().calculateTotal().floatValue()) { - transactionArchive.add(transaction); - if (transaction instanceof Purchase purchase) { + if (transaction instanceof Purchase purchase) { + if (money.floatValue() > transaction.getCalculator().calculateTotal().floatValue()) { withdrawMoney(purchase.getCalculator().calculateTotal()); portfolio.addShare(purchase.getShare()); - } else if (transaction instanceof Sale sale) { - addMoney(sale.getCalculator().calculateTotal()); - portfolio.removeShare(sale.getShare()); + transactionArchive.add(transaction); + transaction.commit(this); } - networthAsFloatProp.setValue(getNetWorth().floatValue()); + } else if (transaction instanceof Sale sale) { + addMoney(sale.getCalculator().calculateTotal()); + portfolio.removeShare(sale.getShare()); + transactionArchive.add(transaction); transaction.commit(this); } + networthAsFloatProp.setValue(getNetWorth().floatValue()); + moneyAsFloatProp.setValue(money); } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java index f83ea00..483adb8 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java @@ -123,4 +123,41 @@ public BigDecimal getNetWorth() { } return netWorth; } + + /** + * Helper method to get total amount of shares owned in a specific stock. + * + * @param symbol the symbol of the stock to check for shares. + * */ + public BigDecimal getTotalSharesBySymbol(final String symbol) { + return shares.stream() + .filter(s -> s.getStock().getSymbol().equals(symbol)) + .map(Share::getQuantity) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + /** + * "Splits" a share in two pieces based on an amount. + * + * @param share the share to split. + * @param splitAmount the amount to split by. + * + * @return the split share from the original to the split amount. + * + * @throws IllegalArgumentException if share or split amount is invalid. + * */ + public Share splitShare(final Share share, final BigDecimal splitAmount) + throws IllegalArgumentException { + if (!contains(share) || splitAmount.compareTo(share.getQuantity()) > 0) { + throw new IllegalArgumentException("Cannot split share!"); + } + BigDecimal remainingAmount = share.getQuantity().subtract(splitAmount); + + Share newShare1 = new Share(share.getStock(), splitAmount, share.getPurchasePrice()); + Share newShare2 = new Share(share.getStock(), remainingAmount, share.getPurchasePrice()); + removeShare(share); + addShare(newShare1); + addShare(newShare2); + return newShare1; + } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardActions.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardActions.java index e6fa822..75b6639 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardActions.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardActions.java @@ -1,7 +1,36 @@ package edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.dashboard; +/** + * Enum representing actions done in an instance of {@link DashBoardView}. + * */ public enum DashBoardActions { + /** + * Buying shares. + * */ BUY_SHARES, + + /** + * Selling shares. + * */ SELL_SHARES, - SELECT_STOCK; + + /** + * Decreasing quantity of shares to buy/sell by five. + * */ + DECREASE_5, + + /** + * Decreasing quantity of shares to buy/sell by one. + * */ + DECREASE_1, + + /** + * Increasing quantity of shares to buy/sell by one. + * */ + INCREASE_1, + + /** + * Increasing quantity of shares to buy/sell by five. + * */ + INCREASE_5; } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java index b6fe1bd..d6334f0 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardController.java @@ -1,22 +1,47 @@ package edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.dashboard; import edu.ntnu.idi.idatt2003.g40.mappe.engine.Exchange; -import edu.ntnu.idi.idatt2003.g40.mappe.model.Player; -import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; +import edu.ntnu.idi.idatt2003.g40.mappe.model.*; import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator; import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewController; import javafx.scene.control.Button; import javafx.scene.control.TextFormatter; - import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.List; +/** + * Controller for {@link DashBoardView}. + * */ public class DashBoardController extends ViewController { - private Player player; - private Exchange exchange; - private List stockList; + /** + * This player instance. + * */ + private final Player player; + + /** + * This exchange instance. + * */ + private final Exchange exchange; + + /** + * List of stocks in the program. + * */ + private final List stockList; + + /** + * Time range chosen. + * + * @see DashBoardTimeRange + * */ + private DashBoardTimeRange selectedTimeRange; + + /** + * Selected search keyword. + * */ + private String selectedFilter; /** * {@inheritDoc} @@ -30,44 +55,132 @@ public DashBoardController(final DashBoardView viewElement, this.player = player; this.stockList = stockList; this.exchange = exchange; + this.selectedFilter = ""; + this.selectedTimeRange = DashBoardTimeRange.DEFAULT; super(viewElement, eventManager); } - private void handleStockSelection(Stock stock) { - getViewElement().setCurrentStock(stock); - getViewElement().updateGraph(); + /** + * Sets the current stock of the view. + * + * @param stock the stock to set. + * @param amountOwned the amount of shares owned in this stock. + * */ + private void handleStockSelection(final Stock stock, final float amountOwned) { + getViewElement().setCurrentStock(stock, amountOwned); + getViewElement().updateGraph(selectedTimeRange); } - private void populateStockList() { + /** + * Updates the sidebar including the list of relevant stocks + * based on a search filter. + * + * @param filter the keyword to search for. + * */ + private void populateStockList(final String filter) { getViewElement().clearStockList(); + String search = filter.toLowerCase(); for (Stock s : stockList) { - Button stockBtn = getViewElement().createStockButton(s.getSymbol()); + if (s.getSymbol().toLowerCase().contains(search) + || s.getCompany().toLowerCase().contains(search)) { + + float rangePercent = 0; + + if (s.getHistoricalPrices().size() >= 2) { + BigDecimal currentPrice = s.getHistoricalPrices().get(s.getHistoricalPrices().size() - 1); + + int lookbackIndex = Math.max(0, s.getHistoricalPrices().size() - 1 - selectedTimeRange.getWeeks()); + BigDecimal pastPrice = s.getHistoricalPrices().get(lookbackIndex); + + if (pastPrice.compareTo(BigDecimal.ZERO) != 0) { + rangePercent = currentPrice.subtract(pastPrice) + .divide(pastPrice, 4, RoundingMode.HALF_UP) + .multiply(new BigDecimal(100)) + .floatValue(); + } + } + + String sign = rangePercent >= 0 ? "+" : ""; + String buttonText = String.format("%s (%s%.2f%%)", + s.getSymbol(), sign, rangePercent); + Button stockBtn = getViewElement().createStockButton(buttonText); + + if (rangePercent >= 0) { + stockBtn.setStyle("-fx-text-fill: green;"); + } else { + stockBtn.setStyle("-fx-text-fill: red;"); + } - getViewElement().setOnStockAction(stockBtn, s, selectedStock -> { - handleStockSelection(selectedStock); - }); + getViewElement().setOnStockAction(stockBtn, s, (Stock stock) -> { + BigDecimal amountOfSharesOwned = player.getPortfolio() + .getTotalSharesBySymbol(s.getSymbol()); + handleStockSelection(stock, amountOfSharesOwned.floatValue()); + }); + } } } - + /** + * {@inheritDoc}. + * */ @Override protected void initInteractions() { - populateStockList(); - getViewElement().setCurrentStock(stockList.getFirst()); - getViewElement().updateGraph(); + populateStockList(""); + getViewElement().setCurrentStock(stockList.getFirst(), 0); + getViewElement().updateGraph(selectedTimeRange); getViewElement().setOnAction(DashBoardActions.BUY_SHARES, () -> { if (Validator.NOT_EMPTY.isValid(getViewElement().getQuantityInputField().getText())) { BigDecimal amountToBuy = new BigDecimal(getViewElement().getQuantityInputField().getText()); - exchange.buy( + Transaction purchase = exchange.buy( getViewElement().getCurrentStock().getSymbol(), amountToBuy, player ); + if (purchase.isCommited()) { + getViewElement().addOwnedShares(purchase.getShare().getQuantity().floatValue()); + } + } + }); + + getViewElement().setOnAction(DashBoardActions.SELL_SHARES, () -> { + if (Validator.NOT_EMPTY.isValid(getViewElement().getQuantityInputField().getText())) { + List transactions = exchange.sell( + new BigDecimal(getViewElement().getQuantityInputField().getText()), + getViewElement().getCurrentStock().getSymbol(), + player); + + for (Transaction t : transactions) { + if(t.isCommited()) { + getViewElement().addOwnedShares(-t.getShare().getQuantity().floatValue()); + } + } } }); + getViewElement().setOnAction(DashBoardActions.DECREASE_5, () -> { + getViewElement().getQuantityInputField().setText( + new BigDecimal(getViewElement().getQuantityInputField().getText()) + .subtract(new BigDecimal("5")).toString()); + }); + getViewElement().setOnAction(DashBoardActions.DECREASE_1, () -> { + getViewElement().getQuantityInputField().setText( + new BigDecimal(getViewElement().getQuantityInputField().getText()) + .subtract(new BigDecimal("1")).toString()); + }); + getViewElement().setOnAction(DashBoardActions.INCREASE_1, () -> { + getViewElement().getQuantityInputField().setText( + new BigDecimal(getViewElement().getQuantityInputField().getText()) + .add(new BigDecimal("1")).toString()); + }); + getViewElement().setOnAction(DashBoardActions.INCREASE_5, () -> { + getViewElement().getQuantityInputField().setText( + new BigDecimal(getViewElement().getQuantityInputField().getText()) + .add(new BigDecimal("5")).toString()); + }); + exchange.weekProperty().addListener((observable,o,n) -> { - getViewElement().updateGraph(); + getViewElement().updateGraph(selectedTimeRange); + populateStockList(selectedFilter); }); getViewElement().getQuantityInputField().setTextFormatter(new TextFormatter<>(change -> { @@ -76,5 +189,16 @@ protected void initInteractions() { } return null; })); + + getViewElement().getSideBarSearchField().textProperty().addListener((observable, o, n) -> { + selectedFilter = n; + populateStockList(selectedFilter); + }); + + getViewElement().getTimeRangeSelector().setOnAction(e -> { + selectedTimeRange = getViewElement().getTimeRangeSelector().getValue(); + getViewElement().updateGraph(selectedTimeRange); + populateStockList(selectedFilter); + }); } } diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardTimeRange.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardTimeRange.java new file mode 100644 index 0000000..42d0680 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardTimeRange.java @@ -0,0 +1,62 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.dashboard; + +/** + * Controls what time range the player has chosen for the dashboard. + * */ +public enum DashBoardTimeRange { + + /** + * One week (a single change). + * */ + ONE_WEEK("1 Week", 1), + + /** + * 22 weeks (default). + * */ + DEFAULT("22 Weeks (Default)", 22), + + /** + * One month. + * */ + ONE_MONTH("1 Month", 4), + + /** + * One year. + * */ + ONE_YEAR("1 Year", 52); + + /** + * The label, used in the dropdown menu. + * */ + private final String label; + + /** + * Weeks used for calculating and updating graph and percentages. + * */ + private final int weeks; + + /** + * Constructor. + * + * @param label the label for this enum. + * @param weeks the amount of weeks. + * */ + DashBoardTimeRange(final String label, final int weeks) { + this.label = label; + this.weeks = weeks; + } + + /** + * Getter method for amount of weeks a constant represent. + * + * @return weeks. + * */ + public int getWeeks() { + return weeks; + } + + @Override + public String toString() { + return label; + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardView.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardView.java index 2710660..c1d53a6 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardView.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/dashboard/DashBoardView.java @@ -2,78 +2,242 @@ import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewElement; - import java.math.BigDecimal; -import java.util.ArrayList; +import java.math.RoundingMode; import java.util.List; import java.util.function.Consumer; - -import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.financialsummary.SummaryView; -import javafx.geometry.Insets; +import javafx.geometry.HPos; import javafx.geometry.Pos; import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; +import javafx.scene.control.Separator; import javafx.scene.control.TextField; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; -public class DashBoardView extends ViewElement { +/** + * Dashboard view. Used to give an overview of all stocks in the application. + * + *

Extends {@link ViewElement}

+ * + *

Acts like a {@link HBox}

+ * */ +public final class DashBoardView extends ViewElement { + + /** + * The currently owned shares for this stock. + * */ + private float ownedStocks = 0; + + /** + * Chart element. + * */ private LineChart chart; + + /** + * Data series for the chart. + * */ private XYChart.Series dataSeries; - private VBox sidebar; - private ArrayList