Skip to content

Conversation

@polynomeer
Copy link
Collaborator

No description provided.

… quotes

- Implemented YahooFinancePriceClient using WebClient to fetch stock prices from Yahoo Finance API
- Added YahooResponseParser to convert raw JSON responses into structured PriceSnapshot records
- Created PriceSnapshot record to encapsulate ticker, price, volume, currency, and timestamp
- Included a basic integration test (YahooFinancePriceClientTest) to validate API response
- Configured Spring Boot application with scheduling and component scanning for batch collector
…ls and multi-store persistence

- Added PriceCollectScheduler to periodically fetch stock prices using @scheduled
- Introduced FetchIntervalService to apply per-ticker fetch intervals before collecting
- Implemented PopularTickerService as a stub for retrieving popular ticker symbols
- Developed PriceCollectorService to fetch prices via YahooFinancePriceClient and store them
- Stored fetched prices into Redis (RedisPriceStore) with TTL and TimescaleDB (TimescalePriceStore) using upsert
- Added robust error handling and logging across all services
- Removed legacy Yahoo Finance integration from price collection flow
- Added AlphaVantageClient to fetch quotes using GLOBAL_QUOTE endpoint
- Implemented AlphaVantageResponseParser to parse quote responses into PriceSnapshot objects
- Updated PriceCollectorService to use Alpha Vantage client and parser
- Added basic integration test for AlphaVantageClient
- Formatted fetched quotes into a JSON array structure for parsing
PostgreSQL's TIMESTAMPTZ maps to java.sql.Timestamp, not Instant.
Using Timestamp.from(Instant) ensures correct and explicit conversion.
- Added domain-level port interface `PriceDataProvider` to decouple app from infra
- Moved `PriceSnapshot` into domain-price model to remove infra dependency
- Implemented `AlphaVantageDataProvider` in infra/external as first provider
  * internally composes AlphaVantageClient + parser to return domain models
- Updated `PriceCollectorService` to depend only on `PriceDataProvider`
  * no longer directly tied to AlphaVantage client/parser
- Reworked unit tests (PriceCollectorServiceTest) to mock `PriceDataProvider`
  * provider implementations now tested in infra module independently

This refactoring enables swapping between Alpha Vantage, Polygon.io, Yahoo Finance
(or future providers) by configuration only, without modifying app logic.
…#35)

- Define increment(ticker, window, atUtc) and topN(window, limit, nowUtc)
- Add RankItem(ticker, requests) as a domain value object
- Use Instant (UTC) to enable deterministic tests
- Decouple domain from Redis via a clean port interface
…s.unionAndStore and template expire (#34)

- Use opsForZSet().unionAndStore(...) instead of connection.zUnionStore(...)
- Set TTL via StringRedisTemplate.expire(key, Duration)
- Keep a 20s cache on union result key
…and enum-based window parsing (#33)

- Inject PopularQueryUseCase instead of concrete PopularService (DIP)
- Parse window via PopularWindow.fromQuery(...) with default "H1" (was String "1h" + valueOf)
- Delegate US-only/exchange validation to application layer (removed hard-coded whitelist in controller)
- Map response DTO explicitly: new PopularItemDto(i.ticker(), i.name(), i.exchange(), i.requests())
- Keep endpoint GET /api/v1/popular; params unchanged (limit default=10, exchange default=US)
- Count request hits per ticker on successful (2xx) responses for /api/v1/quotes/** and /api/v1/charts/**
- Extract ticker from URI and normalize to upper-case
- Increment multiple windows (M15, H1, D1, D7) through PopularCounterPort (no infra dependency)
- Use afterCompletion to ensure only successful requests are counted
- Wire interceptor in WebMvcConfig with explicit path patterns
private final FetchIntervalService fetchIntervalService;
private final PriceCollectorService priceCollectorService;

@Scheduled(fixedRate = 1000)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어떻게 동작할까요?

public void increment(String ticker, PopularWindow window, Instant atUtc) {
String key = minuteKey(window, atUtc);
redis.opsForZSet().incrementScore(key, ticker, 1.0);
redis.expire(key, window.length().plusSeconds(3600)); // 여유 TTL

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

왜 TTL은 3600으로 설정하셨나요?

…d call counter once (#34)

- Add PopularHitRecorder in app layer with configurable windows (app.popular.count.windows)
- Interceptor now extracts ticker only and calls recorder.recordHit(ticker, now)
- Apply SRP/DIP: interceptor no longer knows concrete windows
…unterAdapter (#34)

- Execute ZINCRBY and EXPIRE for multiple windows in one pipeline to reduce RTT
- Keep single-increment method for compatibility
…erties (#34)

- Add PopularCountProperties (app.popular.count.windows) with default [M15,H1,D1,D7]
- Inject Set<PopularWindow> into PopularHitRecorder; call counter.incrementBatch once
- Remove string parsing/uppercasing utility; rely on Spring Boot type conversion
- Wire @EnableConfigurationProperties(PopularCountProperties) in application bootstrap
- Docs: note YAML can be list or comma-separated string
… hardcoding (#34)

- Add TickerExtractor interface with composite implementation
- Implement UriTemplateVariablesTickerExtractor (default), plus query/header extractors
- Introduce @ConfigurationProperties for candidate keys (path vars, query params, headers)
- Update PopularCountingInterceptor to delegate extraction and remain thin
…g binding (#33, #35)

- Introduce PopularQueryProperties (maxLimit, defaultExchange, allowedExchanges)
- Add ExchangeFilter strategy (UsExchangeFilter) to encapsulate exchange rules
- Add PopularItemAssembler to map ranks+meta → PopularItem with filtering
- Inject Clock for deterministic time handling
- Slim down PopularService to orchestration only (SRP/DIP)
- Verify single incrementBatch call with configured windows
- Assert window set is passed as-is (e.g., M15, H1)
- Record hits only for 2xx responses
- Delegate ticker extraction to TickerExtractor (mocked)
- UriTemplateVariablesTickerExtractor: reads {ticker}/{symbol} from path vars
- QueryParamTickerExtractor: reads ?ticker/ ?symbol
- HeaderTickerExtractor: reads X-Ticker
- CompositeTickerExtractor: respects delegate precedence
- US-group requests allow NASDAQ/NYSE/AMEX
- Specific exchange requests must match exactly
- Default exchange falls back to US when meta is missing
- Map ranks + meta to PopularItem
- Filter out non-US exchanges under US request
- Enforce limit cap (maxLimit=10)
- Compose ranks + meta via assembler and return filtered items
- Use fixed Clock for deterministic now()
- Default params: window=H1, limit=10, exchange=US
- Validate JSON mapping to PopularItemDto
@Override
public void afterCompletion(@NotNull HttpServletRequest req, @NotNull HttpServletResponse res, @NotNull Object handler, Exception ex) {
if (!(handler instanceof HandlerMethod)) return;
if (res.getStatus() < 200 || res.getStatus() >= 300) return;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이러한 필드는 네이밍을 잘 정해서 private 함수로 분리하면 어떨까요?

for (var r : ranks) {
var meta = metas.get(r.ticker());
String name = (meta == null || meta.name() == null) ? "" : meta.name();
String ex = (meta == null || meta.exchange() == null || meta.exchange().isBlank())

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

비슷한 코드가 많은데 util class를 만들어서 분리하면 어떨까요?
테스트 코드 작성하기도 좋을 것 같습니다.

@EnableConfigurationProperties(PopularQueryProperties.class)
public class PopularQueryConfig {
@Bean
public Clock clock() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요건 왜 추가하셨나요??

@PathVariable String tickerCode,
@RequestParam String interval,
@RequestParam ZonedDateTime from,
@RequestParam ZonedDateTime to) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위에서 UTC 를 정의했는데 ZonedDateTime 을 쓰신것과 어떠한 연관이 있나요?
Client 에서 zone 데이터를 다루는걸 의도하셨나요??

public List<PopularItemDto> popular(
@RequestParam(defaultValue = "H1") String window,
@RequestParam(defaultValue = "10") int limit,
@RequestParam(required = false, defaultValue = "US") String exchange

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exchange 에 USSSSS 나 KRRRR 과 같이 의도하지 앟은 데이터가 들어오면 어떻게되나요??

@GetMapping("/popular")
public List<PopularItemDto> popular(
@RequestParam(defaultValue = "H1") String window,
@RequestParam(defaultValue = "10") int limit,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

limit 이 -100 이나 Long.MAX_VALUE 값이 들어오면 어떻게 되나요??


@GetMapping("/popular")
public List<PopularItemDto> popular(
@RequestParam(defaultValue = "H1") String window,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

윈도우가 예상치 못한 "윈도우" 라는 문자열이 들어오면 어떻게 되나요??

) {
PopularWindow w = PopularWindow.fromQuery(window); // 도메인 enum에 fromQuery 제공
return useCase.getPopular(w, limit, exchange).stream()
.map(i -> new PopularItemDto(i.ticker(), i.name(), i.exchange(), i.requests()))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

캡슐화를 위해 new PopularItemDto(i)PopularItemDto.of(i) 와 같이 만들면 어떨까요?

}

@Override
public Optional<String> extract(HttpServletRequest request, @Nullable Object handler) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional 은 무엇인가요?

int top = Math.min(props.getMaxLimit(), Math.max(1, limit));
Instant nowUtc = Instant.now(clock);

var ranks = counterPort.topN(window, top, nowUtc);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드에 일관성이 없는데요 왜 여기선 var 를 사용하셨나요?
다른 코드에선 타입을 정확하게 지정하였는데요

}
}

@Test

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DisplayValue 를 추가하시면 더 좋을것 같습니다.

case "1h" -> H1;
case "24h", "1d" -> D1;
case "7d" -> D7;
default -> H1;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예상하지 못한 값이 들어왔을때 에러 처리가 있으면 좋을것 같아요~!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants