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
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.autoinvestor.application;

import io.autoinvestor.domain.Asset;
import io.autoinvestor.domain.AssetRepository;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;


@Service
public class GetAllAssetsCommandHandler {

private final AssetRepository repository;

public GetAllAssetsCommandHandler(AssetRepository repository) {
this.repository = repository;
}

public List<GetAssetResponse> handle() {
List<Asset> assets = this.repository.findAll();
return assets.stream()
.map(asset -> new GetAssetResponse(
asset.id(),
asset.mic(),
asset.ticker(),
asset.name()
))
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.autoinvestor.application;

public record GetAssetCommand(
String assetId
) {}

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.autoinvestor.application;

import io.autoinvestor.domain.Asset;
import io.autoinvestor.domain.AssetId;
import io.autoinvestor.domain.AssetRepository;
import org.springframework.stereotype.Service;


@Service
public class GetAssetCommandHandler {

private final AssetRepository repository;

public GetAssetCommandHandler(AssetRepository repository) {
this.repository = repository;
}

public GetAssetResponse handle(GetAssetCommand command) {
Asset asset = repository.findById(AssetId.of(command.assetId()))
.orElseThrow(() -> new AssetNotFoundException(
"Asset not found with id: " + command.assetId()));

return new GetAssetResponse(
asset.id(),
asset.mic(),
asset.ticker(),
asset.name()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.autoinvestor.application;

public record GetAssetResponse(
String assetId,
String mic,
String ticker,
String name
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public RegisterAssetCommandHandler(AssetRepository repository,
this.eventPublisher = eventPublisher;
}

public void handle(RegisterAssetCommand command) {
public RegisterAssetResponse handle(RegisterAssetCommand command) {
if (this.repository.exists(command.mic(), command.ticker())) {
throw new AssetAlreadyExists("Duplicated asset for this mic: " + command.mic() + " and ticker: " + command.ticker());
}
Expand All @@ -27,5 +27,12 @@ public void handle(RegisterAssetCommand command) {

this.repository.save(asset);
this.eventPublisher.publish(asset.releaseEvents());

return new RegisterAssetResponse(
asset.id(),
asset.mic(),
asset.ticker(),
asset.name()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.autoinvestor.application;

public record RegisterAssetResponse(
String assetId,
String mic,
String ticker,
String name
) {}
4 changes: 4 additions & 0 deletions src/main/java/io/autoinvestor/domain/Asset.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ public String name() {
return name.value();
}

public String id() {
return id.value();
}

public AssetId getId() {
return id;
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/io/autoinvestor/domain/AssetRepository.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package io.autoinvestor.domain;


import java.util.List;
import java.util.Optional;

public interface AssetRepository {
void save(Asset asset);
boolean exists(String mic, String ticker);
Optional<Asset> findById(AssetId assetId);
List<Asset> findAll();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import io.autoinvestor.domain.Asset;
import io.autoinvestor.domain.AssetPriceFetcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import yahoofinance.YahooFinance;
import yahoofinance.Stock;
Expand All @@ -15,12 +17,20 @@
import java.util.List;
import java.util.TimeZone;


@Component
public class YFinanceAssetPriceFetcher implements AssetPriceFetcher {

private static final Logger logger = LoggerFactory.getLogger(YFinanceAssetPriceFetcher.class);
private static final int DAYS_LOOKBACK_BUFFER = 7;

static {
System.setProperty("http.agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
System.setProperty(
"yahoofinance.baseurl.quotesquery1v7",
"https://query1.finance.yahoo.com/v6/finance/quote"
);
}

@Override
public float priceOn(Asset asset, Date date) {
Calendar target = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
Expand All @@ -34,25 +44,33 @@ public float priceOn(Asset asset, Date date) {

try {
Stock stock = YahooFinance.get(asset.ticker(), from, to, Interval.DAILY);
List<HistoricalQuote> history = stock.getHistory();
if (stock == null) {
throw new PriceNotAvailableException("No data returned for " + asset);
}

List<HistoricalQuote> history = stock.getHistory();
HistoricalQuote bar = history.stream()
.filter(h -> h.getDate() != null)
.sorted((a, b) -> b.getDate().compareTo(a.getDate()))
.filter(h -> !h.getDate().after(target))
.findFirst()
.orElseThrow(() -> new PriceNotAvailableException(
"No historical bar found for %s on/Before %s".formatted(asset, date)));
String.format("No historical bar found for %s on or before %s", asset, date)
));

BigDecimal close = bar.getClose() != null ? bar.getClose() : bar.getAdjClose();
if (close == null) {
throw new PriceNotAvailableException(
"Close price missing for %s on %s".formatted(asset, date));
String.format("Close price missing for %s on %s", asset, date)
);
}

return close.floatValue();
} catch (IOException ex) {
logger.error("Error fetching price for {} on {}: {}", asset, date, ex.getMessage(), ex);
throw new PriceFetchFailedException(
"Unable to fetch price for %s from Yahoo Finance".formatted(asset));
String.format("Unable to fetch price for %s from Yahoo Finance", asset)
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Repository;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.*;

@Repository
@Profile("local")
Expand All @@ -31,6 +29,11 @@ public Optional<Asset> findById(AssetId assetId) {
return Optional.ofNullable(assetStore.get(assetId.value()));
}

@Override
public List<Asset> findAll() {
return new ArrayList<>(assetStore.values());
}

public void clear() {
assetStore.clear();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;


Expand Down Expand Up @@ -42,4 +44,15 @@ public Optional<Asset> findById(AssetId assetId) {
return Optional.ofNullable(template.findById(assetId.value(), AssetDocument.class))
.map(mapper::toDomain);
}

@Override
public List<Asset> findAll() {
var q = new Query();
var assetDocuments = template.find(q, AssetDocument.class);
List<Asset> assets = new ArrayList<>();
for (AssetDocument assetDocument : assetDocuments) {
assets.add(mapper.toDomain(assetDocument));
}
return assets;
}
}
11 changes: 11 additions & 0 deletions src/main/java/io/autoinvestor/ui/AssetDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.autoinvestor.ui;

import com.fasterxml.jackson.annotation.JsonFormat;

public record AssetDTO(
@JsonFormat(shape = JsonFormat.Shape.STRING)
String assetId,
String mic,
String ticker,
String name
) {}
29 changes: 29 additions & 0 deletions src/main/java/io/autoinvestor/ui/GetAllAssetsController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.autoinvestor.ui;

import io.autoinvestor.application.GetAllAssetsCommandHandler;
import io.autoinvestor.application.GetAssetResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/assets")
public class GetAllAssetsController {

private final GetAllAssetsCommandHandler handler;

public GetAllAssetsController(GetAllAssetsCommandHandler handler) {
this.handler = handler;
}

@GetMapping
public ResponseEntity<List<AssetDTO>> getAllAssets() {
List<GetAssetResponse> assets = handler.handle();
List<AssetDTO> dtos = assets.stream()
.map(a -> new AssetDTO(a.assetId(), a.mic(), a.ticker(), a.name()))
.collect(Collectors.toList());
return ResponseEntity.ok(dtos);
}
}
30 changes: 30 additions & 0 deletions src/main/java/io/autoinvestor/ui/GetAssetController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.autoinvestor.ui;

import io.autoinvestor.application.GetAssetCommand;
import io.autoinvestor.application.GetAssetCommandHandler;
import io.autoinvestor.application.GetAssetResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/assets")
public class GetAssetController {

private final GetAssetCommandHandler handler;

public GetAssetController(GetAssetCommandHandler handler) {
this.handler = handler;
}

@GetMapping("/{assetId}")
public ResponseEntity<AssetDTO> getAsset(@PathVariable String assetId) {
GetAssetResponse response = handler.handle(new GetAssetCommand(assetId));
AssetDTO dto = new AssetDTO(
response.assetId(),
response.mic(),
response.ticker(),
response.name()
);
return ResponseEntity.ok(dto);
}
}
17 changes: 12 additions & 5 deletions src/main/java/io/autoinvestor/ui/RegisterAssetController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.autoinvestor.application.RegisterAssetCommand;
import io.autoinvestor.application.RegisterAssetCommandHandler;
import io.autoinvestor.application.RegisterAssetResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -10,7 +11,7 @@


@RestController
@RequestMapping("/register")
@RequestMapping("/assets")
public class RegisterAssetController {

private final RegisterAssetCommandHandler commandHandler;
Expand All @@ -20,10 +21,16 @@ public RegisterAssetController(RegisterAssetCommandHandler commandHandler) {
}

@PostMapping
public ResponseEntity<Void> handle(@RequestBody RegisterAssetDTO dto) {
this.commandHandler.handle(new RegisterAssetCommand(
dto.mic(), dto.ticker(), dto.name()
public ResponseEntity<AssetDTO> handle(@RequestBody RegisterAssetDTO queryDto) {
RegisterAssetResponse response = this.commandHandler.handle(new RegisterAssetCommand(
queryDto.mic(), queryDto.ticker(), queryDto.name()
));
return ResponseEntity.ok().build();
AssetDTO dto = new AssetDTO(
response.assetId(),
response.mic(),
response.ticker(),
response.name()
);
return ResponseEntity.ok(dto);
}
}