Skip to content

Commit

Permalink
Feat: Changed how portfolio manages shares
Browse files Browse the repository at this point in the history
Previously, the portfolio held a list over multiple share objects. Now, it holds a map keyed with symbol and value is a share determined by the quantity of all shares of that symbol. This enables a more intuitive way of storing shares.
  • Loading branch information
tommyah committed May 25, 2026
1 parent e75440c commit 7b3de60
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 254 deletions.
75 changes: 24 additions & 51 deletions src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/engine/Exchange.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
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.TransactionFactory;
import edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionType;
import edu.ntnu.idi.idatt2003.g40.mappe.utils.Validator;
Expand All @@ -16,7 +13,7 @@
import java.util.List;
import java.util.Map;
import java.util.Random;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;

/**
Expand All @@ -30,7 +27,7 @@
* <p>Advances week.</p>
*
* @see Player
* @see TransactionCalculator
* @see edu.ntnu.idi.idatt2003.g40.mappe.service.TransactionCalculator
*
* @version 1.0.0
* */
Expand Down Expand Up @@ -98,12 +95,12 @@ public int getWeek() {
}

/**
* Getter method for the {@link IntegerProperty} object of week.
* Getter method for the {@link ReadOnlyIntegerProperty} object of week.
*
* @return week.
* */
public IntegerProperty weekProperty() {
return week;
public ReadOnlyIntegerProperty weekProperty() {
return week.getReadOnlyProperty();
}

/**
Expand Down Expand Up @@ -177,9 +174,8 @@ public Transaction buy(final String symbol,
}
Stock stock = getStock(symbol);
Share share = new Share(stock, quantity, stock.getSalesPrice());
TransactionCalculator calculator = new PurchaseCalculator(share);
Transaction purchase = TransactionFactory.createTransaction(
TransactionType.PURCHASE, share, getWeek(), calculator
TransactionType.PURCHASE, share, getWeek()
);
player.handleTransaction(purchase);
return purchase;
Expand All @@ -200,9 +196,8 @@ public Transaction sell(final Share share, final Player player)
if (share == null || player == null) {
throw new IllegalArgumentException("Invalid sell!");
}
TransactionCalculator calculator = new SaleCalculator(share);
Transaction sale = TransactionFactory.createTransaction(
TransactionType.SALE, share, getWeek(), calculator
TransactionType.SALE, share, getWeek()
);
player.handleTransaction(sale);
return sale;
Expand All @@ -212,9 +207,7 @@ TransactionType.SALE, share, getWeek(), calculator
* Method called when a player sells share,
* defined by an amount instead of specific {@link Share} object.
*
* <p>Might split shares into multiples to ensure proper selling.
* Uses the {@link edu.ntnu.idi.idatt2003.g40.mappe.model.Portfolio}
* to split.</p>
* <p>{@link edu.ntnu.idi.idatt2003.g40.mappe.model.Portfolio}</p>
*
* @param amount the amount of "shares" to sell.
* @param stockSymbol the stock to sell shares in.
Expand All @@ -227,58 +220,38 @@ TransactionType.SALE, share, getWeek(), calculator
* or if player does not own any shares
* of the given stock.
* */
public List<Transaction> sell(BigDecimal amount,
public Transaction sell(BigDecimal amount,
final String stockSymbol,
final Player player)
throws IllegalArgumentException {
if (amount == null
|| player == null
|| !Validator.VALID_STOCK_SYMBOL.isValid(stockSymbol)) {
throw new IllegalArgumentException("Invalid sell!");
|| player == null
|| !Validator.VALID_STOCK_SYMBOL.isValid(stockSymbol)) {
throw new IllegalArgumentException("Invalid sell parameters!");
}

// Get all shares the player owns of the stock given in the parameter.
List<Share> sharesOfStock = player.getPortfolio().getShares().stream()
.filter(s -> s.getStock().getSymbol().equals(stockSymbol))
.toList();
List<Share> matchingShares = player.getPortfolio().getShares(stockSymbol);

// Throws error if player does not own any shares of the given stock.
if (sharesOfStock.isEmpty()) {
throw new IllegalArgumentException("Player does not own"
+ " any shares of this stock!");
if (matchingShares.isEmpty()) {
throw new IllegalArgumentException("Player does not own any shares of this stock!");
}

// Gets the total quantity amount of shares owned of given stock.
BigDecimal totalOwned = player.getPortfolio()
.getTotalSharesBySymbol(stockSymbol);
Share ownedPosition = matchingShares.getFirst();
BigDecimal totalOwned = ownedPosition.getQuantity();

// If amount wanted to sell is greater than total owned,
// sells entire collection.
if (amount.compareTo(totalOwned) > 0) {
amount = totalOwned;
}
ArrayList<Transaction> transactions = new ArrayList<>();
BigDecimal remainingToSell = amount;

// For every share owned, sells if the quantity is less than
// or equal to the remaining amount of shares to sell.
// Splits share if the remaining amount to sell is less than share quantity.
for (Share share : sharesOfStock) {
Stock stock = ownedPosition.getStock();
Share shareToSell = new Share(stock, amount, stock.getSalesPrice());

BigDecimal shareQty = share.getQuantity();
Transaction sale = TransactionFactory.createTransaction(
TransactionType.SALE, shareToSell, getWeek()
);
player.handleTransaction(sale);

if (shareQty.compareTo(remainingToSell) <= 0) {
remainingToSell = remainingToSell.subtract(shareQty);
transactions.add(sell(share, player));
} else {
Share newShare = player.getPortfolio().splitShare(
share, remainingToSell
);
transactions.add(sell(newShare, player));
break;
}
}
return transactions;
return sale;
}

/**
Expand Down
133 changes: 78 additions & 55 deletions src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/model/Portfolio.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package edu.ntnu.idi.idatt2003.g40.mappe.model;

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.utils.Validator;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.Map;

/**
* Represents a player's portfolio of shares.
Expand All @@ -18,9 +15,9 @@
public final class Portfolio {

/**
* List of shares.
* Map used to handle internal shares.
* */
private final List<Share> shares = new ArrayList<>();
private final Map<String, Share> shares = new HashMap<>();

/**
* Creates an empty portfolio.
Expand All @@ -30,24 +27,41 @@ public Portfolio() {
}

/**
* Adds a share to the portfolio.
* Adds a share to the portfolio. If share already exists, merges shares.
*
* @param share the share to add
*
* @return {@code true} if the share was added, {@code false} otherwise
*
* @throws IllegalArgumentException if share is null.
*/
public boolean addShare(final Share share) throws IllegalArgumentException {
public void addShare(final Share share) throws IllegalArgumentException {
if (share == null) {
throw new IllegalArgumentException("Invalid share!");
}
return shares.add(share);
String symbol = share.getStock().getSymbol().toUpperCase();

if (shares.containsKey(symbol)) {
Share existingShare = shares.get(symbol);
BigDecimal totalQuantity =
existingShare.getQuantity().add(share.getQuantity());

shares.put(symbol,
new Share(
share.getStock(),
totalQuantity, existingShare.getPurchasePrice()
)
);
} else {
shares.put(symbol, share);
}
}

/**
* Removes a share from the portfolio.
*
* <p>Uses the quantity value to deduct share amount from the map.
* If quantity to remove is equal to amount held, removes share entirely.
* If not, splits the share.</p>
*
* @param share the share to remove
*
* @return {@code true} if the share was removed,
Expand All @@ -56,11 +70,37 @@ public boolean addShare(final Share share) throws IllegalArgumentException {
* @throws IllegalArgumentException if share is null.
*
*/
public boolean removeShare(final Share share) throws IllegalArgumentException {
public boolean removeShare(final Share share)
throws IllegalArgumentException {
if (share == null) {
throw new IllegalArgumentException("Invalid share!");
}
return shares.remove(share);
String symbol = share.getStock().getSymbol().toUpperCase();
if (!shares.containsKey(symbol)) {
return false;
}

Share ownedShare = shares.get(symbol);
int comparison = ownedShare.getQuantity().compareTo(share.getQuantity());

if (comparison < 0) {
throw new IllegalArgumentException(
"Cannot remove more shares than are currently owned!");
} else if (comparison == 0) {

shares.remove(symbol);
} else {
BigDecimal remainingQuantity =
ownedShare.getQuantity().subtract(share.getQuantity());
shares.put(symbol,
new Share(
share.getStock(),
remainingQuantity,
ownedShare.getPurchasePrice()
)
);
}
return true;
}

/**
Expand All @@ -69,7 +109,7 @@ public boolean removeShare(final Share share) throws IllegalArgumentException {
* @return a list of shares
*/
public List<Share> getShares() {
return List.copyOf(shares);
return List.copyOf(shares.values());
}

/**
Expand All @@ -81,14 +121,14 @@ public List<Share> getShares() {
*
* @throws IllegalArgumentException if symbol is invalid.
*/
public List<Share> getShares(final String symbol) throws IllegalArgumentException {
public List<Share> getShares(final String symbol)
throws IllegalArgumentException {
if (!Validator.VALID_STOCK_SYMBOL.isValid(symbol)) {
throw new IllegalArgumentException(
Validator.VALID_STOCK_SYMBOL.getErrorMessage());
}
return shares.stream()
.filter(s -> symbol.equalsIgnoreCase(s.getStock().getSymbol()))
.toList();
Share share = shares.get(symbol.toUpperCase());
return share != null ? List.of(share) : List.of();
}

/**
Expand All @@ -101,11 +141,15 @@ public List<Share> getShares(final String symbol) throws IllegalArgumentExceptio
*
* @throws IllegalArgumentException if share is null.
*/
public boolean contains(final Share share) throws IllegalArgumentException {
public boolean contains(final Share share)
throws IllegalArgumentException {
if (share == null) {
throw new IllegalArgumentException("Invalid share!");
}
return shares.contains(share);
String symbol = share.getStock().getSymbol().toUpperCase();
Share owned = shares.get(symbol);
return owned != null
&& owned.getQuantity().compareTo(share.getQuantity()) >= 0;
}

/**
Expand All @@ -115,49 +159,28 @@ public boolean contains(final Share share) throws IllegalArgumentException {
* @return the net worth.
* */
public BigDecimal getNetWorth() {
BigDecimal netWorth = new BigDecimal("0");

for (Share s : shares) {
SaleCalculator calculator = new SaleCalculator(s);
netWorth = netWorth.add(calculator.calculateTotal());
BigDecimal netWorth = BigDecimal.ZERO;
for (Share s : shares.values()) {
netWorth = netWorth.add(
s.getQuantity().multiply(s.getStock().getSalesPrice())
);
}
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.
* @param symbol the symbol of the stock to check for shares.
*
* @return the split share from the original to the split amount.
*
* @throws IllegalArgumentException if share or split amount is invalid.
* @return BigDecimal representing total quantity of all
* shares of this symbol.
* */
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!");
public BigDecimal getTotalSharesBySymbol(final String symbol) {
if (symbol == null) {
return BigDecimal.ZERO;
}
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;
Share share = shares.get(symbol.toUpperCase());
return share != null ? share.getQuantity() : BigDecimal.ZERO;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,15 +149,13 @@ protected void initInteractions() {
getViewElement().setOnAction(DashBoardActions.SELL_SHARES, () -> {
if (Validator.NOT_EMPTY.isValid(getViewElement().getQuantityInputField().getText())
&& Float.parseFloat(getViewElement().getQuantityInputField().getText()) > 0) {
List<Transaction> transactions = exchange.sell(
Transaction sale = exchange.sell(
new BigDecimal(getViewElement().getQuantityInputField().getText()),
getViewElement().getCurrentStock().getSymbol(),
player);

for (Transaction t : transactions) {
if(t.isCommited()) {
getViewElement().addOwnedShares(-t.getShare().getQuantity().floatValue());
}
if(sale.isCommited()) {
getViewElement().addOwnedShares(-sale.getShare().getQuantity().floatValue());
}
}
});
Expand Down
Loading

0 comments on commit 7b3de60

Please sign in to comment.