PositionDisplay.java(クリックで開く)
'''java
import java.io.;
import java.math.BigDecimal;
import java.util.;
public class PositionDisplay {
public static void displayPositions() {
System.out.println("保有ポジションを表示します。");
try {
// PositionCalculatorを使用してポジションデータを計算
List<PositionCalculator.PositionData> positions = PositionCalculator.calculateAllPositions();
if (positions.isEmpty()) {
System.out.println("保有ポジションがありません。");
return;
}
// 表形式で表示
displayPositionTable(positions);
} catch (FileNotFoundException e) {
System.out.println("エラー: 取引データファイルが見つかりません: transactions.csv");
} catch (Exception e) {
System.out.println("エラー: " + e.getMessage());
}
}
// 既存の取引データから保有数量を計算(publicメソッドとして公開)
public static Map<String, Integer> calculatePositions() throws IOException {
Map<String, Integer> positionMap = new HashMap<>();
try (BufferedReader reader = new BufferedReader(new FileReader("transactions.csv"))) {
String headerLine = reader.readLine(); // ヘッダー行をスキップ
if (headerLine == null) {
return positionMap;
}
String line;
while ((line = reader.readLine()) != null) {
String[] fields = line.split(",");
if (fields.length >= 4) {
String ticker = fields[1].trim();
String side = fields[2].trim();
int quantity = Integer.parseInt(fields[3].trim());
// 現在の保有数量を取得(なければ0)
int currentQuantity = positionMap.getOrDefault(ticker, 0);
// B(買い)の場合は加算、S(売り)の場合は減算
if ("B".equals(side)) {
positionMap.put(ticker, currentQuantity + quantity);
} else if ("S".equals(side)) {
positionMap.put(ticker, currentQuantity - quantity);
}
}
}
}
return positionMap;
}
// 特定の銘柄の現在の保有数量を取得
public static int getCurrentPosition(String ticker) throws IOException {
Map<String, Integer> positionMap = calculatePositions();
return positionMap.getOrDefault(ticker, 0);
}
// 新しい取引を追加した場合の保有数量を計算
public static int calculatePositionAfterTransaction(String ticker, String side, int quantity) throws IOException {
int currentPosition = getCurrentPosition(ticker);
if ("B".equals(side)) {
return currentPosition + quantity;
} else if ("S".equals(side)) {
return currentPosition - quantity;
}
return currentPosition;
}
private static void displayPositionTable(List<PositionCalculator.PositionData> positions) {
System.out.println();
System.out.println("+------+----------------------+------------+------------+------------+------------+------------+");
System.out.println("| 銘柄コード | 銘柄名 | 保有数量 | 平均取得単価 | 実現損益 | 評価額 | 評価損益 |");
System.out.println("+------+----------------------+------------+------------+------------+------------+------------+");
for (PositionCalculator.PositionData position : positions) {
String productName = position.getProductName();
// 銘柄名が20文字以上の場合は省略
if (productName.length() > 20) {
productName = productName.substring(0, 17) + "...";
}
// 平均取得単価の表示
String avgPriceStr = formatPrice(position.getAverageUnitPrice());
// 実現損益の表示
String realizedProfitStr = formatPrice(position.getRealizedProfit());
// 評価額の表示
String valuationStr = formatPrice(position.getValuation());
// 評価損益の表示
String unrealizedProfitStr = formatPrice(position.getUnrealizedProfit());
System.out.printf("| %-6s | %-20s | %10s | %10s | %10s | %10s | %10s |%n",
position.getTicker(),
productName,
String.format("%,d", position.getQuantity()),
avgPriceStr,
realizedProfitStr,
valuationStr,
unrealizedProfitStr);
}
System.out.println("+------+----------------------+------------+------------+------------+------------+------------+");
}
// 価格をフォーマット(nullの場合はN/A、右詰め、3桁ごとのカンマ区切り、小数点第2位まで)
private static String formatPrice(BigDecimal price) {
if (price == null) {
return "N/A";
}
if (price.compareTo(BigDecimal.ZERO) == 0) {
return "0.00";
}
return String.format("%,.2f", price);
}
}
Main.java(クリックで開く)
'''java
import java.io.;
import java.util.;
public class Main {
public static void main(String[] args) {
System.out.println("株式取引管理システムを開始します。");
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println();
System.out.println("A: 銘柄マスター覧表示");
System.out.println("B: 銘柄マスタ新規登録");
System.out.println("C: 取引入力");
System.out.println("D: 取引一覧表示");
System.out.println("E: 保有ポジション表示");
System.out.println("Q: アプリケーションを終了する");
System.out.println();
System.out.print("操作するメニューを選んでください。入力してください: ");
String input = scanner.nextLine();
switch (input) {
case "A":
displayStocks();
break;
case "B":
// \\ 新規登録機能を実装
StockRegistrar registrar = new StockRegistrar(scanner);
registrar.registerNewStock();
break;
case "C":
// \\ 取引入力機能を実装
TransactionInput transactionInput = new TransactionInput(scanner);
transactionInput.inputTransaction();
break;
case "D":
// \\ 取引一覧表示機能を実装
TransactionDisplay.displayTransactions();
break;
case "E":
// \\ 保有ポジション表示機能を実装
PositionDisplay.displayPositions();
break;
case "Q":
System.out.println("アプリケーションを終了します。");
return;
default:
System.out.println("無効な選択です。");
}
}
}
private static void displayStocks() {
System.out.println("銘柄マスタを表示します。");
try (BufferedReader reader = new BufferedReader(new FileReader("Masterdata.csv"))) {
String headerLine = reader.readLine(); // ヘッダー行を読み込み
// ヘッダー行の列数と並び順をチェック
if (headerLine == null) {
throw new IOException("CSVファイルが空です。");
}
String[] headerFields = headerLine.split(",");
if (headerFields.length != 4) {
throw new IOException("CSVファイルの列数が正しくありません。");
}
if (!isValidHeaderOrder(headerFields)) {
throw new IOException("CSVファイルの列の並び順が仕様と異なっています。");
}
System.out.println();
System.out.println("+------+----------------------+----------+----------------+");
System.out.println("| Ticker | Product Name | Market | Shares Issued |");
System.out.println("+------+----------------------+----------+----------------+");
String line;
while ((line = reader.readLine()) != null) {
String[] fields = line.split(",");
if (fields.length != 4) {
throw new IOException("CSVファイルの列数が正しくありません。");
}
String ticker = fields[0];
// \\ 銘柄名の冒頭および末尾の空白文字を除去
String productName = fields[1].trim();
String market = fields[2];
long sharesIssued = Long.parseLong(fields[3]);
// \\ 銘柄名が20文字以上の場合は「...」で省略
if (productName.length() > 20) {
productName = productName.substring(0, 17) + "...";
}
// \\ Marketのenumを使用して市場名を取得
String displayMarket = getMarketDisplayName(market);
System.out.printf("| %-6s | %-20s | %-8s | %14s |%n",
ticker, productName, displayMarket, String.format("%,d", sharesIssued));
}
System.out.println("+------+----------------------+----------+----------------+");
} catch (FileNotFoundException e) {
System.out.println("エラー: CSVファイルが見つかりません: Masterdata.csv");
} catch (Exception e) {
System.out.println("エラー: " + e.getMessage());
}
}
private static boolean isValidHeaderOrder(String[] headerFields) {
if (headerFields.length != 4) return false;
String ticker = headerFields[0].trim();
String productName = headerFields[1].trim();
String market = headerFields[2].trim();
String sharesIssued = headerFields[3].trim();
return "ticker".equals(ticker) &&
"product_name".equals(productName) &&
"market".equals(market) &&
"shares_issued".equals(sharesIssued);
}
// \\ Marketのenumを使用して市場コードから表示名を取得
private static String getMarketDisplayName(String marketCode) {
for (Market market : Market.values()) {
if (market.getCode().equals(marketCode)) {
return market.getDisplayName();
}
}
return marketCode; // 見つからない場合は元のコードを返す
}
}
PositionCalculator.java
'''java
import java.io.;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.;
public class PositionCalculator {
// ポジションデータクラス
public static class PositionData {
private String ticker;
private String productName;
private int quantity;
private BigDecimal averageUnitPrice; // nullの場合はN/A
private BigDecimal realizedProfit; // 実現損益
private BigDecimal valuation; // 評価額(nullの場合はN/A)
private BigDecimal unrealizedProfit; // 評価損益(nullの場合はN/A)
public PositionData(String ticker, String productName, int quantity,
BigDecimal averageUnitPrice, BigDecimal realizedProfit,
BigDecimal valuation, BigDecimal unrealizedProfit) {
this.ticker = ticker;
this.productName = productName;
this.quantity = quantity;
this.averageUnitPrice = averageUnitPrice;
this.realizedProfit = realizedProfit;
this.valuation = valuation;
this.unrealizedProfit = unrealizedProfit;
}
public String getTicker() { return ticker; }
public String getProductName() { return productName; }
public int getQuantity() { return quantity; }
public BigDecimal getAverageUnitPrice() { return averageUnitPrice; }
public BigDecimal getRealizedProfit() { return realizedProfit; }
public BigDecimal getValuation() { return valuation; }
public BigDecimal getUnrealizedProfit() { return unrealizedProfit; }
}
// 全銘柄のポジションデータを計算
public static List<PositionData> calculateAllPositions() throws IOException {
// 取引データを時系列順に読み込む
List<TransactionRecord> transactions = loadTransactions();
// 銘柄マスターから銘柄名を取得
Map<String, String> tickerToNameMap = loadTickerToNameMap();
// 銘柄ごとのポジション状態を管理
Map<String, PositionState> positionStates = new HashMap<>();
// 取引を時系列順に処理
for (TransactionRecord transaction : transactions) {
String ticker = transaction.ticker;
PositionState state = positionStates.getOrDefault(ticker, new PositionState());
if ("B".equals(transaction.side)) {
// 買い取引:移動平均法で平均取得単価を更新
state.processBuy(transaction.quantity, transaction.price);
} else if ("S".equals(transaction.side)) {
// 売り取引:実現損益を計算
state.processSell(transaction.quantity, transaction.price);
}
positionStates.put(ticker, state);
}
// 時価情報を読み込む
Map<String, Double> marketPriceMap = MarketPriceReader.loadMarketPrices();
// ポジションデータを作成
List<PositionData> positions = new ArrayList<>();
for (Map.Entry<String, PositionState> entry : positionStates.entrySet()) {
String ticker = entry.getKey();
PositionState state = entry.getValue();
// 保有数量が0でない銘柄のみを対象
if (state.quantity != 0) {
String productName = tickerToNameMap.getOrDefault(ticker, "不明");
// 平均取得単価(保有数量が0の場合は0またはN/A)
BigDecimal avgPrice = null;
if (state.quantity > 0 && state.totalCost.compareTo(BigDecimal.ZERO) > 0) {
avgPrice = state.totalCost.divide(
BigDecimal.valueOf(state.quantity), 2, RoundingMode.HALF_UP);
} else if (state.quantity == 0) {
// クローズされたポジションは0
avgPrice = BigDecimal.ZERO;
}
// 時価を取得
Double marketPrice = marketPriceMap.get(ticker);
// 評価額と評価損益
BigDecimal valuation = null;
BigDecimal unrealizedProfit = null;
if (marketPrice != null && state.quantity > 0) {
BigDecimal marketPriceBD = BigDecimal.valueOf(marketPrice);
// 評価額 = 保有数量 × 時価
valuation = marketPriceBD.multiply(BigDecimal.valueOf(state.quantity))
.setScale(2, RoundingMode.HALF_UP);
if (avgPrice != null && avgPrice.compareTo(BigDecimal.ZERO) > 0) {
// 取得価額 = 保有数量 × 平均取得単価
BigDecimal acquisitionCost = avgPrice.multiply(BigDecimal.valueOf(state.quantity))
.setScale(2, RoundingMode.HALF_UP);
// 評価損益 = 評価額 - 取得価額
unrealizedProfit = valuation.subtract(acquisitionCost)
.setScale(2, RoundingMode.HALF_UP);
}
}
positions.add(new PositionData(ticker, productName, state.quantity,
avgPrice, state.realizedProfit, valuation, unrealizedProfit));
}
}
// 銘柄コードの昇順でソート
Collections.sort(positions, (p1, p2) -> p1.getTicker().compareTo(p2.getTicker()));
return positions;
}
// 取引データを時系列順に読み込む
private static List<TransactionRecord> loadTransactions() throws IOException {
List<TransactionRecord> transactions = new ArrayList<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
try (BufferedReader reader = new BufferedReader(new FileReader("transactions.csv"))) {
String headerLine = reader.readLine(); // ヘッダー行をスキップ
if (headerLine == null) {
return transactions;
}
String line;
while ((line = reader.readLine()) != null) {
String[] fields = line.split(",");
if (fields.length >= 5) {
try {
LocalDateTime transactionDateTime = LocalDateTime.parse(fields[0].trim(), formatter);
String ticker = fields[1].trim();
String side = fields[2].trim();
int quantity = Integer.parseInt(fields[3].trim());
double price = Double.parseDouble(fields[4].trim());
transactions.add(new TransactionRecord(transactionDateTime, ticker, side, quantity, price));
} catch (Exception e) {
// パースエラーは無視
}
}
}
}
// 取引日時の昇順でソート
Collections.sort(transactions, (t1, t2) -> t1.transactionDateTime.compareTo(t2.transactionDateTime));
return transactions;
}
// 銘柄マスターから銘柄名を取得
private static Map<String, String> loadTickerToNameMap() {
Map<String, String> tickerToNameMap = new HashMap<>();
try (BufferedReader reader = new BufferedReader(new FileReader("Masterdata.csv"))) {
reader.readLine(); // ヘッダー行をスキップ
String line;
while ((line = reader.readLine()) != null) {
String[] fields = line.split(",");
if (fields.length >= 2) {
String ticker = fields[0].trim();
String productName = fields[1].trim();
tickerToNameMap.put(ticker, productName);
}
}
} catch (IOException e) {
// エラーは無視
}
return tickerToNameMap;
}
// ポジション状態を管理する内部クラス
private static class PositionState {
int quantity = 0;
BigDecimal totalCost = BigDecimal.ZERO; // 総取得コスト
BigDecimal realizedProfit = BigDecimal.ZERO; // 実現損益
void processBuy(int buyQuantity, double buyPrice) {
// 移動平均法による平均取得単価の計算
// 買い付け後の平均取得単価 = (q_old × p_old + q_new × p_new) / (q_old + q_new)
// ここでは、totalCost = q_old × p_old + q_new × p_new として保持
BigDecimal buyPriceBD = BigDecimal.valueOf(buyPrice);
BigDecimal buyCost = buyPriceBD.multiply(BigDecimal.valueOf(buyQuantity));
// 総取得コストを更新(新しい買い付けコストを加算)
totalCost = totalCost.add(buyCost);
quantity += buyQuantity;
}
void processSell(int sellQuantity, double sellPrice) {
if (quantity == 0) {
return; // エラーケース(通常は発生しない)
}
// 現在の平均取得単価を計算(移動平均法に基づく)
// 平均取得単価 = totalCost / quantity
BigDecimal avgPrice = totalCost.divide(
BigDecimal.valueOf(quantity), 2, RoundingMode.HALF_UP);
// 実現損益を計算:実現損益 = 売却数量 × (売却価格 - 取得単価)
BigDecimal sellPriceBD = BigDecimal.valueOf(sellPrice);
BigDecimal profitPerShare = sellPriceBD.subtract(avgPrice);
BigDecimal realizedProfitForThisSale = profitPerShare.multiply(
BigDecimal.valueOf(sellQuantity)).setScale(2, RoundingMode.HALF_UP);
realizedProfit = realizedProfit.add(realizedProfitForThisSale);
// 売却した数量分のコストを計算
BigDecimal soldCost = avgPrice.multiply(BigDecimal.valueOf(sellQuantity))
.setScale(2, RoundingMode.HALF_UP);
// 保有数量と総コストを更新(売却前のtotalCostから売却分を引く)
quantity -= sellQuantity;
if (quantity > 0) {
// 残りの数量に対する総コスト = 売却前のtotalCost - 売却したコスト
totalCost = totalCost.subtract(soldCost).setScale(2, RoundingMode.HALF_UP);
} else {
totalCost = BigDecimal.ZERO;
}
}
}
// 取引レコード
private static class TransactionRecord {
LocalDateTime transactionDateTime;
String ticker;
String side;
int quantity;
double price;
TransactionRecord(LocalDateTime transactionDateTime, String ticker,
String side, int quantity, double price) {
this.transactionDateTime = transactionDateTime;
this.ticker = ticker;
this.side = side;
this.quantity = quantity;
this.price = price;
}
}
}
MarketPriceReader.java
'''java
import java.io.;
import java.util.;
public class MarketPriceReader {
// 時価ファイルのパス(デフォルトはmarket_price.csv)
private static final String MARKET_PRICE_FILE = "market_price.csv";
// 時価情報を読み込む(必要になるたびにファイルを読み込む方式)
public static Map<String, Double> loadMarketPrices() {
Map<String, Double> marketPriceMap = new HashMap<>();
try (BufferedReader reader = new BufferedReader(new FileReader(MARKET_PRICE_FILE))) {
String headerLine = reader.readLine(); // ヘッダー行をスキップ
if (headerLine == null) {
return marketPriceMap;
}
String line;
while ((line = reader.readLine()) != null) {
String[] fields = line.split(",");
if (fields.length >= 2) {
String ticker = fields[0].trim();
try {
double price = Double.parseDouble(fields[1].trim());
marketPriceMap.put(ticker, price);
} catch (NumberFormatException e) {
// パースエラーは無視
}
}
}
} catch (FileNotFoundException e) {
// ファイルが見つからない場合は空のマップを返す
} catch (IOException e) {
// 読み込みエラーは無視
}
return marketPriceMap;
}
// 特定の銘柄の時価を取得
public static Double getMarketPrice(String ticker) {
Map<String, Double> marketPriceMap = loadMarketPrices();
return marketPriceMap.get(ticker);
}
}
market_price.csv
'''java
ticker,market_price
7203,2570.50
6861,69240.00
1111,550.00
2A2A,150.00
TransactionInput.java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.io.*;
import java.util.Scanner;
public class TransactionInput {
private Scanner scanner;
public TransactionInput(Scanner scanner) {
this.scanner = scanner;
}
public void inputTransaction() {
System.out.println("取引データを入力します。");
String transactionDateTimeStr = getTransactionDateTime();
String ticker = getTicker();
String side = getSide();
int quantity = getQuantity();
double price = getPrice();
// 保有数量チェック
if (!TransactionValidator.isValidPositionAfterTransaction(ticker, side, quantity)) {
System.out.println("エラー: " + TransactionValidator.getPositionErrorMessage());
return;
}
// 取引時間チェック(同一銘柄の最新取引時間より未来である必要がある)
if (!TransactionValidator.isValidTransactionDateTimeForTicker(ticker, transactionDateTimeStr)) {
System.out.println("エラー: " + TransactionValidator.getTransactionDateTimeForTickerErrorMessage(ticker));
return;
}
// 取引データを作成
LocalDateTime transactionDateTime = LocalDateTime.parse(transactionDateTimeStr,
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
LocalDateTime inputDateTime = LocalDateTime.now();
Transaction transaction = new Transaction(transactionDateTime, ticker, side,
quantity, price, inputDateTime);
// CSVファイルに保存
try {
saveTransactionToCSV(transaction);
System.out.println("取引データを登録しました。");
} catch (IOException e) {
System.out.println("エラー: 取引データの保存に失敗しました。");
}
}
private String getTransactionDateTime() {
while (true) {
System.out.print("取引日時 (yyyy-MM-dd HH:mm)> ");
String input = scanner.nextLine().trim();
if (TransactionValidator.isValidTransactionDateTime(input)) {
return input;
} else {
System.out.println(TransactionValidator.getTransactionDateTimeErrorMessage());
}
}
}
private String getTicker() {
while (true) {
System.out.print("銘柄コード> ");
String input = scanner.nextLine().trim();
if (TransactionValidator.isValidTicker(input)) {
return input;
} else {
System.out.println(TransactionValidator.getTickerErrorMessage());
}
}
}
private String getSide() {
while (true) {
System.out.print("売買区分 (B/S)> ");
String input = scanner.nextLine().trim();
if (TransactionValidator.isValidSide(input)) {
return input;
} else {
System.out.println(TransactionValidator.getSideErrorMessage());
}
}
}
private int getQuantity() {
while (true) {
System.out.print("数量> ");
String input = scanner.nextLine().trim();
if (TransactionValidator.isValidQuantity(input)) {
return Integer.parseInt(input);
} else {
System.out.println(TransactionValidator.getQuantityErrorMessage());
}
}
}
private double getPrice() {
while (true) {
System.out.print("取引単価> ");
String input = scanner.nextLine().trim();
if (TransactionValidator.isValidPrice(input)) {
return Double.parseDouble(input);
} else {
System.out.println(TransactionValidator.getPriceErrorMessage());
}
}
}
private void saveTransactionToCSV(Transaction transaction) throws IOException {
File file = new File("transactions.csv");
boolean fileExists = file.exists();
try (FileWriter writer = new FileWriter(file, true)) {
// ファイルが新規作成の場合はヘッダーを書き込み
if (!fileExists) {
writer.write("transaction_date_time,ticker,side,quantity,price,input_date_time\n");
}
writer.write(transaction.toCSVLine() + "\n");
}
}
}
</details> '''
<details>
<summary>TransactionValidator.java</summary>
<div/>
'''java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.io.*;
public class TransactionValidator {
public static boolean isValidTransactionDateTime(String dateTimeStr) {
if (dateTimeStr == null || dateTimeStr.trim().isEmpty()) {
return false;
}
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
LocalDateTime transactionDateTime = LocalDateTime.parse(dateTimeStr.trim(), formatter);
LocalDateTime now = LocalDateTime.now();
// 現在時刻より過去である必要がある
if (!transactionDateTime.isBefore(now)) {
return false;
}
// 平日(月曜=1、火曜=2、...、金曜=5)9:00-15:30
int dayOfWeek = transactionDateTime.getDayOfWeek().getValue();
int hour = transactionDateTime.getHour();
int minute = transactionDateTime.getMinute();
if (dayOfWeek < 2 || dayOfWeek > 5) { // 火曜(2)から金曜(5)まで
return false;
}
if (hour < 9 || hour > 15) {
return false;
}
if (hour == 15 && minute > 30) {
return false;
}
return true;
} catch (DateTimeParseException e) {
return false;
}
}
public static boolean isValidTicker(String ticker) {
// 銘柄マスターに存在するかチェック
try (BufferedReader reader = new BufferedReader(new FileReader("Masterdata.csv"))) {
reader.readLine(); // ヘッダー行をスキップ
String line;
while ((line = reader.readLine()) != null) {
String[] fields = line.split(",");
if (fields.length >= 1 && fields[0].trim().equals(ticker.trim())) {
return true;
}
}
return false;
} catch (IOException e) {
return false;
}
}
public static boolean isValidSide(String side) {
return "B".equals(side) || "S".equals(side);
}
public static boolean isValidQuantity(String quantityStr) {
if (quantityStr == null || quantityStr.trim().isEmpty()) {
return false;
}
try {
int quantity = Integer.parseInt(quantityStr.trim());
return quantity > 0;
} catch (NumberFormatException e) {
return false;
}
}
public static boolean isValidPrice(String priceStr) {
if (priceStr == null || priceStr.trim().isEmpty()) {
return false;
}
try {
double price = Double.parseDouble(priceStr.trim());
if (price <= 0) {
return false;
}
// 小数点第2位まで入力されているかチェック
String trimmed = priceStr.trim();
if (trimmed.contains(".")) {
String[] parts = trimmed.split("\\.");
if (parts.length == 2 && parts[1].length() != 2) {
return false; // 小数点第2位まで入力されていない
}
} else {
return false; // 小数点が含まれていない
}
return true;
} catch (NumberFormatException e) {
return false;
}
}
// 保有数量が負になるかチェック
public static boolean isValidPositionAfterTransaction(String ticker, String side, int quantity) {
try {
int positionAfter = PositionDisplay.calculatePositionAfterTransaction(ticker, side, quantity);
return positionAfter >= 0;
} catch (IOException e) {
return false;
}
}
// 取引時間が既存の取引より過去でないかチェック
public static boolean isValidTransactionDateTimeForTicker(String ticker, String transactionDateTimeStr) {
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
LocalDateTime newTransactionDateTime = LocalDateTime.parse(transactionDateTimeStr.trim(), formatter);
// 既存の取引データから同一銘柄の最新の取引時間を取得
LocalDateTime latestTransactionDateTime = getLatestTransactionDateTimeForTicker(ticker);
if (latestTransactionDateTime == null) {
// 既存の取引がない場合は有効
return true;
}
// 新しい取引時間が最新の取引時間より未来である必要がある
return newTransactionDateTime.isAfter(latestTransactionDateTime);
} catch (Exception e) {
return false;
}
}
// 特定の銘柄の最新の取引時間を取得
private static LocalDateTime getLatestTransactionDateTimeForTicker(String ticker) throws IOException {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
LocalDateTime latestDateTime = null;
try (BufferedReader reader = new BufferedReader(new FileReader("transactions.csv"))) {
String headerLine = reader.readLine(); // ヘッダー行をスキップ
if (headerLine == null) {
return null;
}
String line;
while ((line = reader.readLine()) != null) {
String[] fields = line.split(",");
if (fields.length >= 2) {
String existingTicker = fields[1].trim();
if (existingTicker.equals(ticker.trim())) {
try {
LocalDateTime transactionDateTime = LocalDateTime.parse(fields[0].trim(), formatter);
if (latestDateTime == null || transactionDateTime.isAfter(latestDateTime)) {
latestDateTime = transactionDateTime;
}
} catch (Exception e) {
// パースエラーは無視
}
}
}
}
}
return latestDateTime;
}
public static String getTransactionDateTimeErrorMessage() {
return "取引日時は yyyy-MM-dd HH:mm 形式で入力してください。現在時刻より過去で、平日(火曜-金曜)の9:00-15:30の間である必要があります。";
}
public static String getTickerErrorMessage() {
return "銘柄コードは銘柄マスターに登録されている4桁の銘柄コードを入力してください。";
}
public static String getSideErrorMessage() {
return "売買区分は「B」または「S」を入力してください。";
}
public static String getQuantityErrorMessage() {
return "数量は1以上の整数を入力してください。";
}
public static String getPriceErrorMessage() {
return "取引単価は0より大きい数値を小数点第2位まで入力してください(例:123.45)。";
}
public static String getPositionErrorMessage() {
return "この取引を入力すると、保有数量が負の値になります。取引を入力できません。";
}
public static String getTransactionDateTimeForTickerErrorMessage(String ticker) {
return "この取引の取引時間は、既に登録されている同一銘柄の取引の最も新しい取引時間より過去です。取引を入力できません。";
}
}
<div/>
</details> '''
//*
2. TransactionValidator.java
追加したメソッド(3つ)
1. isValidPositionAfterTransaction (109-117行目)
public static boolean isValidPositionAfterTransaction(String ticker, String side, int quantity)
機能: 取引実行後に保有数量が負にならないかチェック
戻り値: 負にならない場合true
2. isValidTransactionDateTimeForTicker (119-138行目)
public static boolean isValidTransactionDateTimeForTicker(String ticker, String transactionDateTimeStr)
機能: 同一銘柄の最新取引時間より新しいかチェック
戻り値: 未来の場合true
3. getLatestTransactionDateTimeForTicker (140-172行目)
追加したエラーメッセージメソッド(2つ)
4. getPositionErrorMessage (194-196行目)
public static String getPositionErrorMessage()
機能: 保有数量が負になる場合のエラーメッセージを返す
5. getTransactionDateTimeForTickerErrorMessage (198-200行目)
public static String getTransactionDateTimeForTickerErrorMessage(String ticker)
機能: 取引時間が過去の場合のエラーメッセージを返す
3. TransactionInput.java
変更したメソッド
inputTransactionメソッド (13-49行目) にバリデーション処理を追加
22-26行目: 保有数量チェックを追加
// 保有数量チェック if (!TransactionValidator.isValidPositionAfterTransaction(ticker, side, quantity)) { System.out.println("エラー: " + TransactionValidator.getPositionErrorMessage()); return; }
28-32行目: 取引時間チェックを追加
// 取引時間チェック(同一銘柄の最新取引時間より未来である必要がある) if (!TransactionValidator.isValidTransactionDateTimeForTicker(ticker, transactionDateTimeStr)) { System.out.println("エラー: " + TransactionValidator.getTransactionDateTimeForTickerErrorMessage(ticker)); return; }
4. PositionDisplay.java
追加したメソッド(3つ)
1. calculatePositions (30-62行目)
public static Map<String, Integer> calculatePositions() throws IOException
機能: 取引データから各銘柄の保有数量を計算
戻り値: 銘柄コードをキー、保有数量を値とするMap
用途: TransactionValidatorから呼び出される
2. getCurrentPosition (65-68行目)
public static int getCurrentPosition(String ticker) throws IOException
機能: 指定銘柄の現在の保有数量を取得
戻り値: 保有数量(ない場合は0)
3. calculatePositionAfterTransaction (71-79行目)
public static int calculatePositionAfterTransaction(String ticker, String side, int quantity) throws IOException
機能: 新規取引実行後の保有数量を計算
戻り値: 取引後の保有数量
用途: TransactionValidatorのバリデーションで使用
変更したメソッド
4. displayPositions (7-27行目)
PositionCalculatorを使用してポジションデータを計算するように変更
5. displayPositionTable (81-118行目)
表示項目を拡張(平均取得単価、実現損益、評価額、評価損益を追加)
追加したヘルパーメソッド
6. formatPrice (121-129行目)
private static String formatPrice(BigDecimal price)
機能: 価格をフォーマット(nullは"N/A"、3桁カンマ区切り、小数点第2位まで)