diff --git a/src/main/java/millions/controller/GameController.java b/src/main/java/millions/controller/GameController.java index a437790..17157dc 100644 --- a/src/main/java/millions/controller/GameController.java +++ b/src/main/java/millions/controller/GameController.java @@ -1,5 +1,6 @@ package millions.controller; +import java.io.FileNotFoundException; import java.math.BigDecimal; import java.math.RoundingMode; import java.nio.file.Path; @@ -38,7 +39,7 @@ public void startGame( player = new Player(name, startingMoney); - } catch (UncheckedFileNotFoundException e) { + } catch (FileNotFoundException e) { throw new UncheckedFileNotFoundException(e.getMessage()); } catch (InvalidFormatException e) { throw new InvalidFormatException(e.getMessage()); diff --git a/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java b/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java index 95f7b56..051e1f0 100644 --- a/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java +++ b/src/main/java/millions/controller/fileIO/CSV/CSVFileHandler.java @@ -6,6 +6,7 @@ import millions.model.Stock; +import java.io.FileNotFoundException; import java.nio.file.Path; import java.util.List; @@ -28,7 +29,7 @@ public CSVFileHandler() { * @throws InvalidFormatException Throws an InvalidFormatException received from parser * @throws UncheckedFileNotFoundException Upon Receiving a FilenotFoundException */ - public List getStocksFromFile(Path filePath) { + public List getStocksFromFile(Path filePath) throws FileNotFoundException { try { StockFileReader reader = new StockFileReader(); List lines = reader.readFile(filePath); @@ -38,8 +39,8 @@ public List getStocksFromFile(Path filePath) { } catch (InvalidFormatException e) { throw new InvalidFormatException(e.getMessage()); - } catch (UncheckedFileNotFoundException e) { - throw new UncheckedFileNotFoundException(e.getMessage()); + } catch (FileNotFoundException e) { + throw new FileNotFoundException(e.getMessage()); } } } diff --git a/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java index 2a5d1ae..caf1097 100644 --- a/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java +++ b/src/main/java/millions/controller/fileIO/CSV/CSVStockFileParser.java @@ -21,14 +21,14 @@ public CSVStockFileParser() {} public boolean verifyCSV(List lines) { return lines.stream() .filter(l -> !(l.startsWith("#") || l.isBlank())) - .noneMatch(l -> l.split(",").length != 3); + .noneMatch(l -> l.split(",").length != 4 || l.split(",")[3].split(";").length != 6); } /** * Parses the supplied lines if they satisfy the correct format expectations * * @param lines

lines to be parsed.
- * Each line must contain three data fields: String,String,BigDecimal
+ * Each line must contain four data fields: String,String,BigDecimal,String
* (Fields cannot be blank)
* blank lines or lines beginning with '#' are ignored *

@@ -49,9 +49,14 @@ public List parse(List lines) { String symbol = split[0]; String company = split[1]; BigDecimal price = new BigDecimal(split[2]); - stocks.add(new Stock(symbol, company, price)); + String[] functionValues = split[3].split(";"); + List convertedFunctionValues = new ArrayList<>(); + for (String functionValue : functionValues) { + convertedFunctionValues.add(new BigDecimal(functionValue)); + } + stocks.add(new Stock(symbol, company, price, convertedFunctionValues)); } catch (NumberFormatException e) { - throw new InvalidFormatException("Error with number conversion on line: " + l + "\n" + "Last field must be a number"); + throw new InvalidFormatException("Error with number conversion on line: " + l + "\n" + "ensure all number fields are actually numbers"); } catch (IllegalArgumentException e) { throw new InvalidFormatException("Illegal argument on line: " + l + "\n" + e.getMessage()); } diff --git a/src/main/java/millions/controller/fileIO/StockFileReader.java b/src/main/java/millions/controller/fileIO/StockFileReader.java index f7121cf..6c8069d 100644 --- a/src/main/java/millions/controller/fileIO/StockFileReader.java +++ b/src/main/java/millions/controller/fileIO/StockFileReader.java @@ -23,7 +23,7 @@ public StockFileReader() {} * @return List of each line in the file as a string * @throws UncheckedFileNotFoundException Upon encountering a FileNotFoundException */ - public List readFile(Path path) { + public List readFile(Path path) throws FileNotFoundException { File file = new File(path.toString()); List lines = new ArrayList<>(); try (Reader reader = new FileReader(file); @@ -34,7 +34,7 @@ public List readFile(Path path) { } } catch (IOException e) { if (e instanceof FileNotFoundException) { - throw new UncheckedFileNotFoundException("Couldn't find file at specified path"); + throw new FileNotFoundException("Couldn't find file at specified path"); } else { logger.log(Level.SEVERE, "Encountered unexpected IOException: ", e.getMessage()); diff --git a/src/main/java/millions/model/Exchange.java b/src/main/java/millions/model/Exchange.java index 55b2ab7..fbbec8d 100644 --- a/src/main/java/millions/model/Exchange.java +++ b/src/main/java/millions/model/Exchange.java @@ -10,12 +10,14 @@ import java.util.Map; import java.util.Random; import java.util.stream.Collectors; +import millions.model.calculators.PriceChangeCalculator; import millions.model.factories.PurchaseFactory; import millions.model.factories.SaleFactory; import millions.model.factories.TransactionFactory; /** - * The stock exchange where players buy and sell shares. Manages stocks and simulates weekly price changes. + * The stock exchange where players buy and sell shares. Manages stocks and simulates weekly price + * changes. */ public class Exchange { private String name; @@ -139,19 +141,22 @@ public List getLosers(int limit) { .collect(Collectors.toList()); } - /** - * Advances the current game week by performing new price calculations for all stocks. - */ + /** Advances the current game week by performing new price calculations for all stocks. */ public void advance() { + PriceChangeCalculator priceChangeCalculator = new PriceChangeCalculator(); this.weekNumber++; for (Stock stock : this.stocks.values()) { - double change = 0.9 + random.nextDouble() * 0.2; - stock.addNewSalesPrice( - stock - .getSalesPrice() - .multiply(BigDecimal.valueOf(change)) - .setScale(2, RoundingMode.HALF_UP)); - // RoundingMode from AI suggestion + BigDecimal change = priceChangeCalculator.calculateChange(stock); + stock.addNewSalesPrice(stock.getSalesPrice().add(change).setScale(2, RoundingMode.HALF_UP)); + // Round to stop crazy values + + // double change = 0.9 + random.nextDouble() * 0.2; + // stock.addNewSalesPrice( + // stock + // .getSalesPrice() + // .multiply(BigDecimal.valueOf(change)) + // .setScale(2, RoundingMode.HALF_UP)); + // // RoundingMode from AI suggestion } notifyWeekAdvanced(); } diff --git a/src/main/java/millions/model/Stock.java b/src/main/java/millions/model/Stock.java index e21fb9e..a5445fe 100644 --- a/src/main/java/millions/model/Stock.java +++ b/src/main/java/millions/model/Stock.java @@ -8,19 +8,27 @@ public class Stock { String symbol; String company; + List volatility; List prices; /** * @param symbol Stock ticker symbol * @param company company name * @param prices List of prices + * @param volatilityParameters numbers used for price change calculation functions * @throws IllegalArgumentException */ - public Stock(String symbol, String company, List prices) { + public Stock(String symbol, String company, List prices, List volatilityParameters) { this.symbol = symbol; this.company = company; this.prices = new ArrayList<>(prices); + this.volatility = volatilityParameters; + + if (volatilityParameters.size() != 6) { + throw new IllegalArgumentException("Invalid volatility function count"); + } + if (symbol == null || symbol.isBlank()) { throw new IllegalArgumentException("Symbol cannot be null or blank"); } @@ -30,9 +38,10 @@ public Stock(String symbol, String company, List prices) { } } + /** Stock() with single price instead of list. */ - public Stock(String symbol, String company, BigDecimal initialPrice) { - this(symbol, company, new ArrayList<>(List.of(initialPrice))); + public Stock(String symbol, String company, BigDecimal initialPrice, List volatilityFunctions) { + this(symbol, company, new ArrayList<>(List.of(initialPrice)), volatilityFunctions); } /** @@ -110,6 +119,10 @@ public BigDecimal getLatestPriceChange() { return currentPrice.subtract(lastPrice); } + public List getVolatilityParameters() { + return this.volatility; + } + @Override public String toString() { return "Stock [symbol: " + symbol + ", company: " + company + ", prices: " + prices + "]"; diff --git a/src/main/java/millions/model/calculators/PriceChangeCalculator.java b/src/main/java/millions/model/calculators/PriceChangeCalculator.java new file mode 100644 index 0000000..7ce275a --- /dev/null +++ b/src/main/java/millions/model/calculators/PriceChangeCalculator.java @@ -0,0 +1,64 @@ +package millions.model.calculators; + +import java.math.BigDecimal; +import java.util.List; +import millions.model.Stock; + +public class PriceChangeCalculator { + + public PriceChangeCalculator() {} + + public BigDecimal calculateChange(Stock stock) { + List values = stock.getVolatilityParameters(); + int week = stock.getHistoricalPrices().size(); + BigDecimal change = BigDecimal.ZERO; + change = change.add(drift(values.get(0))); + change = change.add(volatility(values.get(1))); + change = change.add(cycle(values.get(2), values.get(3), week)); + change = change.add(explosion(values.get(4), values.get(5))); + return stock.getSalesPrice().multiply(change); + } + + /* + * Flat slope change, eg upwards/downwards slope + */ + private BigDecimal drift(BigDecimal input) { + if (input.equals(BigDecimal.ZERO)) { + return BigDecimal.ZERO; + } + return input; + } + + /* + * Random noise of size x + */ + private BigDecimal volatility(BigDecimal input) { + if (input.equals(BigDecimal.ZERO)) { + return BigDecimal.ZERO; + } + return input.multiply(BigDecimal.valueOf(Math.random() * 2 - 1)); + } + + /* + * Sinus curve based on week, times size of the curve + */ + private BigDecimal cycle(BigDecimal speed, BigDecimal size, int week) { + if (speed.equals(BigDecimal.ZERO) || size.equals(BigDecimal.ZERO)) { + return BigDecimal.ZERO; + } + return size.multiply(BigDecimal.valueOf(Math.sin(week * speed.doubleValue()))); + } + + /* + * probability% change of an explosion of size% positive or negative + */ + private BigDecimal explosion(BigDecimal probability, BigDecimal size) { + if (probability.equals(BigDecimal.ZERO) || size.equals(BigDecimal.ZERO)) { + return BigDecimal.ZERO; + } + if (Math.random() >= probability.doubleValue()) { + return BigDecimal.ZERO; + } + return Math.random() < 0.5 ? size : size.negate(); + } +} diff --git a/src/main/java/millions/view/GameView.java b/src/main/java/millions/view/GameView.java index 64acd71..ad63e4e 100644 --- a/src/main/java/millions/view/GameView.java +++ b/src/main/java/millions/view/GameView.java @@ -2,6 +2,7 @@ import java.math.BigDecimal; import java.util.List; +import javafx.beans.property.SimpleStringProperty; // For data binding for table string import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; @@ -13,6 +14,9 @@ import javafx.scene.control.Slider; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; import javafx.scene.control.TextField; import javafx.scene.control.TextFormatter; import javafx.scene.layout.BorderPane; @@ -39,8 +43,8 @@ public class GameView extends BorderPane implements PlayerListener, ExchangeList private final TextField searchField = new TextField(); private final ListView stocksList = new ListView<>(); - private final ListView portfolioList = new ListView<>(); - private final ListView transactionsList = new ListView<>(); + private final TableView portfolioTable = new TableView<>(); + private final TableView transactionsTable = new TableView<>(); private final Label selectedStockLabel = new Label("Select a stock to see chart"); private final Label ownedQuantityLabel = new Label("Owned: 0"); private final Label actionStatusLabel = new Label(); @@ -127,13 +131,81 @@ private Tab createStocksTab() { } private Tab createPortfolioTab() { - portfolioList.setPlaceholder(new Label("No shares yet")); - return new Tab("Portfolio", portfolioList); + portfolioTable.setPlaceholder(new Label("No shares yet")); + + TableColumn symbolColumn = new TableColumn<>("Symbol"); + symbolColumn.setCellValueFactory( + data -> new SimpleStringProperty(data.getValue().getStock().getSymbol())); + + TableColumn quantityColumn = new TableColumn<>("Quantity"); + quantityColumn.setCellValueFactory( + data -> new SimpleStringProperty(data.getValue().getQuantity().toPlainString())); + + TableColumn purchasePriceColumn = new TableColumn<>("Purchase price"); + purchasePriceColumn.setCellValueFactory( + data -> new SimpleStringProperty(data.getValue().getPurchasePrice().toPlainString())); + + TableColumn currentPriceColumn = new TableColumn<>("Current price"); + currentPriceColumn.setCellValueFactory( + data -> + new SimpleStringProperty(data.getValue().getStock().getSalesPrice().toPlainString())); + + TableColumn profitColumn = new TableColumn<>("Profit"); + profitColumn.setCellValueFactory( + data -> + new SimpleStringProperty(ViewUtils.getShareProfit(data.getValue()).toPlainString())); + profitColumn.setCellFactory( + column -> + new TableCell<>() { + @Override + protected void updateItem(String profit, boolean empty) { + super.updateItem(profit, empty); + if (empty) { + setText(null); + setStyle(""); + return; + } + setText(profit); + setStyle(profit.startsWith("-") ? "-fx-text-fill: red;" : "-fx-text-fill: green;"); + } + }); + + portfolioTable + .getColumns() + .addAll( + symbolColumn, quantityColumn, purchasePriceColumn, currentPriceColumn, profitColumn); + return new Tab("Portfolio", portfolioTable); } private Tab createTransactionsTab() { - transactionsList.setPlaceholder(new Label("No transactions yet")); - return new Tab("Transactions", transactionsList); + transactionsTable.setPlaceholder(new Label("No transactions yet")); + + TableColumn typeColumn = new TableColumn<>("Type"); + typeColumn.setCellValueFactory( + data -> new SimpleStringProperty(data.getValue().getClass().getSimpleName())); + + TableColumn symbolColumn = new TableColumn<>("Symbol"); + symbolColumn.setCellValueFactory( + data -> new SimpleStringProperty(data.getValue().getShare().getStock().getSymbol())); + + TableColumn quantityColumn = new TableColumn<>("Quantity"); + quantityColumn.setCellValueFactory( + data -> new SimpleStringProperty(data.getValue().getShare().getQuantity().toPlainString())); + + TableColumn weekColumn = new TableColumn<>("Week"); + weekColumn.setCellValueFactory( + data -> new SimpleStringProperty(String.valueOf(data.getValue().getWeek()))); + + TableColumn totalColumn = new TableColumn<>("Total"); + totalColumn.setCellValueFactory( + data -> + new SimpleStringProperty( + data.getValue().getCalculator().calculateTotal().toPlainString())); + + transactionsTable + .getColumns() + .addAll(typeColumn, symbolColumn, quantityColumn, weekColumn, totalColumn); + return new Tab("Transactions", transactionsTable); } private void configureButtons() { @@ -266,12 +338,7 @@ private void refreshQuantityControls(Stock stock) { private void refreshPortfolio() { Player player = controller.getPlayer(); - portfolioList - .getItems() - .setAll( - player.getPortfolio().getShares().stream() - .map(ViewUtils::formatPortfolioShare) - .toList()); + portfolioTable.getItems().setAll(player.getPortfolio().getShares()); } private void refreshTransactions() { @@ -280,12 +347,7 @@ private void refreshTransactions() { return; } - transactionsList - .getItems() - .setAll( - player.getTransactionArchive().getTransactions().stream() - .map(ViewUtils::formatTransaction) - .toList()); + transactionsTable.getItems().setAll(player.getTransactionArchive().getTransactions()); } private void buySelectedStock() {