goldenChat base source add
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
package com.goldenchart.trading.pipeline;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.goldenchart.storage.Ta4jStorage;
|
||||
import com.goldenchart.websocket.UpbitWebSocketClient;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BarSeries;
|
||||
import org.ta4j.core.BaseBarSeriesBuilder;
|
||||
import org.ta4j.core.bars.TimeBarBuilderFactory;
|
||||
import org.ta4j.core.num.DoubleNumFactory;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* 5분마다 REST 로 최근 캔들을 조회해 WS 갭을 멱등적으로 보정한다.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class CandleGapBackfillService {
|
||||
|
||||
private final Ta4jStorage ta4jStorage;
|
||||
private final UpbitWebSocketClient upbitWebSocketClient;
|
||||
private final TradingPipelineProperties props;
|
||||
private final WebClient.Builder webClientBuilder;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Value("${upbit.api.base-url:https://api.upbit.com}")
|
||||
private String upbitBaseUrl;
|
||||
|
||||
@Value("${upbit.api.request-delay-ms:110}")
|
||||
private long requestDelayMs;
|
||||
|
||||
private static final String[] BACKFILL_TYPES = {"1m", "3m", "5m", "15m", "30m", "1h", "4h", "1d"};
|
||||
|
||||
private volatile long lastRunAtMs;
|
||||
private final AtomicInteger lastRunMarkets = new AtomicInteger();
|
||||
private final AtomicLong lastRunBarsMerged = new AtomicLong();
|
||||
private volatile String lastRunError = "";
|
||||
|
||||
public Map<String, Object> monitorSnapshot() {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("cron", props.getGapBackfillCron());
|
||||
m.put("candleCount", props.getGapBackfillCandleCount());
|
||||
m.put("lastRunAtMs", lastRunAtMs);
|
||||
m.put("lastRunMarkets", lastRunMarkets.get());
|
||||
m.put("lastRunBarsMerged", lastRunBarsMerged.get());
|
||||
m.put("lastRunError", lastRunError);
|
||||
return m;
|
||||
}
|
||||
|
||||
@Scheduled(cron = "${goldenchart.trading.pipeline.gap-backfill-cron:0 */5 * * * *}")
|
||||
public void scheduledBackfill() {
|
||||
if (!props.isEnabled()) return;
|
||||
Set<String> markets = upbitWebSocketClient.getSubscribedMarkets();
|
||||
if (markets.isEmpty()) return;
|
||||
|
||||
lastRunAtMs = System.currentTimeMillis();
|
||||
lastRunMarkets.set(markets.size());
|
||||
lastRunBarsMerged.set(0);
|
||||
lastRunError = "";
|
||||
|
||||
int count = props.getGapBackfillCandleCount();
|
||||
long mergedTotal = 0;
|
||||
for (String market : markets) {
|
||||
for (String type : BACKFILL_TYPES) {
|
||||
if (!ta4jStorage.exists(market, type)) continue;
|
||||
try {
|
||||
Thread.sleep(requestDelayMs);
|
||||
mergedTotal += backfillMarket(market, type, count);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
lastRunError = e.getMessage();
|
||||
log.debug("[GapBackfill] {} {}: {}", market, type, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
lastRunBarsMerged.set(mergedTotal);
|
||||
}
|
||||
|
||||
int backfillMarket(String market, String candleType, int count) throws Exception {
|
||||
String path = Ta4jStorage.CANDLE_TYPE_MAP.getOrDefault(candleType, "minutes/1");
|
||||
Duration duration = Ta4jStorage.parseDuration(candleType);
|
||||
String url = upbitBaseUrl + "/v1/candles/" + path
|
||||
+ "?market=" + market + "&count=" + count;
|
||||
|
||||
String json = webClientBuilder.build()
|
||||
.get().uri(url)
|
||||
.retrieve().bodyToMono(String.class).block();
|
||||
if (json == null || json.isBlank()) return 0;
|
||||
|
||||
List<Map<String, Object>> page =
|
||||
objectMapper.readValue(json, new TypeReference<>() {});
|
||||
if (page.isEmpty()) return 0;
|
||||
|
||||
page.sort(Comparator.comparingLong(r ->
|
||||
parseEpoch((String) r.get("candle_date_time_utc"))));
|
||||
|
||||
int merged = 0;
|
||||
for (Map<String, Object> r : page) {
|
||||
long epochSec = parseEpoch((String) r.get("candle_date_time_utc"));
|
||||
Instant endInstant = Instant.ofEpochSecond(epochSec).plus(duration);
|
||||
double open = toDouble(r.get("opening_price"));
|
||||
double high = toDouble(r.get("high_price"));
|
||||
double low = toDouble(r.get("low_price"));
|
||||
double close = toDouble(r.get("trade_price"));
|
||||
double vol = toDouble(r.get("candle_acc_trade_volume"));
|
||||
|
||||
Bar bar = new BaseBarSeriesBuilder()
|
||||
.withBarBuilderFactory(new TimeBarBuilderFactory())
|
||||
.withNumFactory(DoubleNumFactory.getInstance())
|
||||
.build()
|
||||
.barBuilder()
|
||||
.timePeriod(duration)
|
||||
.endTime(endInstant)
|
||||
.openPrice(open).highPrice(high).lowPrice(low)
|
||||
.closePrice(close).volume(vol)
|
||||
.build();
|
||||
|
||||
ta4jStorage.addBar(market, candleType, bar);
|
||||
merged++;
|
||||
}
|
||||
if (merged > 0) {
|
||||
BarSeries series = ta4jStorage.getOrCreate(market, candleType);
|
||||
log.debug("[GapBackfill] {} {} — REST {}봉 병합 (series={})",
|
||||
market, candleType, merged, series.getBarCount());
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
private static long parseEpoch(String utc) {
|
||||
if (utc == null) return 0;
|
||||
String s = utc.endsWith("Z") ? utc : utc + "Z";
|
||||
return ZonedDateTime.parse(s).toEpochSecond();
|
||||
}
|
||||
|
||||
private static double toDouble(Object o) {
|
||||
if (o == null) return 0;
|
||||
if (o instanceof Number n) return n.doubleValue();
|
||||
return Double.parseDouble(o.toString());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user