추세검색 설정 추가
This commit is contained in:
@@ -9,6 +9,7 @@ public final class MenuIds {
|
||||
public static final List<String> ALL = List.of(
|
||||
"dashboard", "chart", "paper", "virtual", "trend-search", "strategy", "strategy-editor", "backtest", "notifications", "settings",
|
||||
"settings_general", "settings_chart", "settings_indicators", "settings_backtest",
|
||||
"settings_strategy", "settings_paper", "settings_alert", "settings_network", "settings_admin"
|
||||
"settings_strategy", "settings_paper", "settings_trend-search",
|
||||
"settings_alert", "settings_network", "settings_admin"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,4 +50,8 @@ public class LiveStrategySettingsDto {
|
||||
|
||||
/** true면 gc_app_settings 전역 템플릿 갱신 생략 */
|
||||
private Boolean skipGlobalTemplate;
|
||||
|
||||
/** 가상투자 투자대상 고정 — ON 이면 목록에서 삭제 불가 (JSON: isPinned) */
|
||||
@JsonProperty("isPinned")
|
||||
private Boolean pinned;
|
||||
}
|
||||
|
||||
@@ -184,6 +184,11 @@ public class GcAppSettings {
|
||||
@Builder.Default
|
||||
private BigDecimal paperAutoTradeBudgetPct = BigDecimal.valueOf(95);
|
||||
|
||||
/** 가상매매 투자대상 목록 최대 종목 수 */
|
||||
@Column(name = "virtual_target_max_count", nullable = false)
|
||||
@Builder.Default
|
||||
private Integer virtualTargetMaxCount = 20;
|
||||
|
||||
/** PAPER | LIVE | BOTH — 자동매매 실행 대상 */
|
||||
@Column(name = "trading_mode", nullable = false, length = 10)
|
||||
@Builder.Default
|
||||
@@ -213,6 +218,11 @@ public class GcAppSettings {
|
||||
@Builder.Default
|
||||
private Boolean fcmPushEnabled = false;
|
||||
|
||||
/** 추세검색 기본 설정 JSON */
|
||||
@Column(name = "trend_search_settings_json", columnDefinition = "JSON")
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
private String trendSearchSettingsJson;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
|
||||
@@ -56,6 +56,11 @@ public class GcLiveStrategySettings {
|
||||
@Builder.Default
|
||||
private String positionMode = "LONG_ONLY";
|
||||
|
||||
/** 가상투자 투자대상 고정 — ON 이면 목록에서 삭제 불가 */
|
||||
@Column(name = "is_pinned", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean isPinned = false;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
|
||||
@@ -138,6 +138,10 @@ public class AppSettingsService {
|
||||
Boolean.parseBoolean(d.get("paperAutoTradeEnabled").toString()));
|
||||
if (d.containsKey("paperAutoTradeBudgetPct")) s.setPaperAutoTradeBudgetPct(
|
||||
new java.math.BigDecimal(d.get("paperAutoTradeBudgetPct").toString()));
|
||||
if (d.containsKey("virtualTargetMaxCount")) {
|
||||
int max = Integer.parseInt(d.get("virtualTargetMaxCount").toString());
|
||||
s.setVirtualTargetMaxCount(Math.min(100, Math.max(1, max)));
|
||||
}
|
||||
if (d.containsKey("tradingMode")) {
|
||||
String mode = d.get("tradingMode").toString().toUpperCase();
|
||||
s.setTradingMode("LIVE".equals(mode) || "BOTH".equals(mode) ? mode : "PAPER");
|
||||
@@ -164,6 +168,9 @@ public class AppSettingsService {
|
||||
new java.math.BigDecimal(d.get("liveAutoTradeBudgetPct").toString()));
|
||||
if (d.containsKey("fcmPushEnabled")) s.setFcmPushEnabled(
|
||||
Boolean.parseBoolean(d.get("fcmPushEnabled").toString()));
|
||||
if (d.containsKey("trendSearchSettings")) {
|
||||
s.setTrendSearchSettingsJson(toJson(d.get("trendSearchSettings")));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> toMap(GcAppSettings s) {
|
||||
@@ -202,6 +209,8 @@ public class AppSettingsService {
|
||||
m.put("paperAutoTradeEnabled", s.getPaperAutoTradeEnabled() != null ? s.getPaperAutoTradeEnabled() : false);
|
||||
m.put("paperAutoTradeBudgetPct", s.getPaperAutoTradeBudgetPct() != null
|
||||
? s.getPaperAutoTradeBudgetPct().doubleValue() : 95);
|
||||
m.put("virtualTargetMaxCount", s.getVirtualTargetMaxCount() != null
|
||||
? s.getVirtualTargetMaxCount() : 20);
|
||||
m.put("tradingMode", s.getTradingMode() != null ? s.getTradingMode() : "PAPER");
|
||||
m.put("liveAutoTradeEnabled", s.getLiveAutoTradeEnabled() != null ? s.getLiveAutoTradeEnabled() : false);
|
||||
UpbitApiCredentials creds = resolveUpbitCredentials(s);
|
||||
@@ -211,6 +220,7 @@ public class AppSettingsService {
|
||||
m.put("liveAutoTradeBudgetPct", s.getLiveAutoTradeBudgetPct() != null
|
||||
? s.getLiveAutoTradeBudgetPct().doubleValue() : 95);
|
||||
m.put("fcmPushEnabled", s.getFcmPushEnabled() != null ? s.getFcmPushEnabled() : false);
|
||||
m.put("trendSearchSettings", parseJson(s.getTrendSearchSettingsJson()));
|
||||
return m;
|
||||
}
|
||||
|
||||
|
||||
@@ -224,6 +224,9 @@ public class LiveStrategySettingsService {
|
||||
"REALTIME_TICK".equals(dto.getExecutionType()) ? "REALTIME_TICK" : "CANDLE_CLOSE");
|
||||
entity.setPositionMode(
|
||||
"SIGNAL_ONLY".equals(dto.getPositionMode()) ? "SIGNAL_ONLY" : "LONG_ONLY");
|
||||
if (dto.getPinned() != null) {
|
||||
entity.setIsPinned(Boolean.TRUE.equals(dto.getPinned()));
|
||||
}
|
||||
|
||||
repo.save(entity);
|
||||
liveStrategyEvaluator.invalidateCache(market);
|
||||
@@ -278,6 +281,7 @@ public class LiveStrategySettingsService {
|
||||
.liveCheck(Boolean.TRUE.equals(e.getIsLiveCheck()))
|
||||
.executionType(e.getExecutionType())
|
||||
.positionMode(e.getPositionMode() != null ? e.getPositionMode() : "LONG_ONLY")
|
||||
.pinned(Boolean.TRUE.equals(e.getIsPinned()))
|
||||
.build();
|
||||
enrichStrategyTimeframes(dto);
|
||||
return dto;
|
||||
|
||||
@@ -64,10 +64,11 @@ public class TrendSearchService {
|
||||
int wantBars = wantBarsForScan(r);
|
||||
int minBars = requiredMinBars(r);
|
||||
|
||||
List<String> markets = fetchKrwMarketsByVolume(scanLimit);
|
||||
KrwMarketSlice slice = fetchKrwMarketsByVolumeSlice(scanLimit);
|
||||
List<String> markets = slice.markets();
|
||||
if (markets.isEmpty()) return List.of();
|
||||
|
||||
Map<String, TickerSnap> tickers = fetchTickers(markets);
|
||||
Map<String, TickerSnap> tickers = fetchTickers(markets, slice.koreanNames());
|
||||
int minMatchRate = Math.min(100, Math.max(0, r.getMinMatchRate()));
|
||||
int enabledFilters = countEnabledFilters(r);
|
||||
boolean useBullishScore = r.isBullishTrendEnabled();
|
||||
@@ -84,7 +85,8 @@ public class TrendSearchService {
|
||||
BarSeries series = toSeries(candles, tf);
|
||||
if (series.getBarCount() < minBars) continue;
|
||||
|
||||
TickerSnap tick = tickers.getOrDefault(market, TickerSnap.empty(market));
|
||||
TickerSnap tick = tickers.getOrDefault(market,
|
||||
TickerSnap.empty(market, slice.koreanNames().get(market)));
|
||||
MetricValues m = computeMetrics(series, candles, tick, r);
|
||||
if (useBullishScore) {
|
||||
m.bullishTrendScore = computeBullishTrendScore(m, r);
|
||||
@@ -133,8 +135,9 @@ public class TrendSearchService {
|
||||
.market(market).matchRate(0).conditions(List.of()).build();
|
||||
}
|
||||
|
||||
Map<String, TickerSnap> tickers = fetchTickers(List.of(market));
|
||||
TickerSnap tick = tickers.getOrDefault(market, TickerSnap.empty(market));
|
||||
Map<String, String> koreanNames = fetchKoreanNamesForMarkets(List.of(market));
|
||||
Map<String, TickerSnap> tickers = fetchTickers(List.of(market), koreanNames);
|
||||
TickerSnap tick = tickers.getOrDefault(market, TickerSnap.empty(market, koreanNames.get(market)));
|
||||
BarSeries series = toSeries(candles, tf);
|
||||
MetricValues m = computeMetrics(series, candles, tick, r);
|
||||
if (r.isBullishTrendEnabled()) {
|
||||
@@ -675,7 +678,37 @@ public class TrendSearchService {
|
||||
|
||||
// ── Upbit helpers ───────────────────────────────────────────────────────
|
||||
|
||||
private List<String> fetchKrwMarketsByVolume(int limit) {
|
||||
private record KrwMarketSlice(List<String> markets, Map<String, String> koreanNames) {}
|
||||
|
||||
/** 거래대금 상위 KRW 마켓 + 한글명 (market/all 기준) */
|
||||
private KrwMarketSlice fetchKrwMarketsByVolumeSlice(int limit) {
|
||||
try {
|
||||
Map<String, String> allNames = fetchKoreanNamesFromMarketAll();
|
||||
if (allNames.isEmpty()) return new KrwMarketSlice(List.of(), Map.of());
|
||||
|
||||
List<String> krw = new ArrayList<>(allNames.keySet());
|
||||
Map<String, TickerSnap> tickers = fetchTickers(krw, allNames);
|
||||
List<String> sorted = krw.stream()
|
||||
.sorted(Comparator.comparingDouble((String m) ->
|
||||
tickers.getOrDefault(m, TickerSnap.empty(m, allNames.get(m))).accTradePrice24h).reversed())
|
||||
.limit(limit)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, String> names = new HashMap<>();
|
||||
for (String m : sorted) {
|
||||
names.put(m, allNames.getOrDefault(m, m.replace("KRW-", "")));
|
||||
}
|
||||
return new KrwMarketSlice(sorted, names);
|
||||
} catch (Exception e) {
|
||||
log.warn("[TrendSearch] market fetch fail: {}", e.getMessage());
|
||||
List<String> fallback = List.of("KRW-BTC", "KRW-ETH", "KRW-XRP", "KRW-SOL", "KRW-ADA");
|
||||
Map<String, String> names = new HashMap<>();
|
||||
for (String m : fallback) names.put(m, m.replace("KRW-", ""));
|
||||
return new KrwMarketSlice(fallback, names);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> fetchKoreanNamesFromMarketAll() {
|
||||
try {
|
||||
WebClient client = webClientBuilder.baseUrl(upbitBaseUrl).build();
|
||||
String marketsJson = client.get()
|
||||
@@ -683,29 +716,36 @@ public class TrendSearchService {
|
||||
.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
.block();
|
||||
if (marketsJson == null) return List.of();
|
||||
if (marketsJson == null) return Map.of();
|
||||
|
||||
List<Map<String, Object>> all = objectMapper.readValue(marketsJson, new TypeReference<>() {});
|
||||
List<String> krw = all.stream()
|
||||
.map(m -> (String) m.get("market"))
|
||||
.filter(m -> m != null && m.startsWith("KRW-"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (krw.isEmpty()) return List.of();
|
||||
|
||||
Map<String, TickerSnap> tickers = fetchTickers(krw);
|
||||
return krw.stream()
|
||||
.sorted(Comparator.comparingDouble((String m) ->
|
||||
tickers.getOrDefault(m, TickerSnap.empty(m)).accTradePrice24h).reversed())
|
||||
.limit(limit)
|
||||
.collect(Collectors.toList());
|
||||
Map<String, String> names = new HashMap<>();
|
||||
for (Map<String, Object> m : all) {
|
||||
String market = (String) m.get("market");
|
||||
if (market == null || !market.startsWith("KRW-")) continue;
|
||||
Object kn = m.get("korean_name");
|
||||
if (kn != null && !kn.toString().isBlank()) {
|
||||
names.put(market, kn.toString());
|
||||
}
|
||||
}
|
||||
return names;
|
||||
} catch (Exception e) {
|
||||
log.warn("[TrendSearch] market fetch fail: {}", e.getMessage());
|
||||
return List.of("KRW-BTC", "KRW-ETH", "KRW-XRP", "KRW-SOL", "KRW-ADA");
|
||||
log.warn("[TrendSearch] korean names fetch fail: {}", e.getMessage());
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, TickerSnap> fetchTickers(List<String> markets) {
|
||||
private Map<String, String> fetchKoreanNamesForMarkets(List<String> markets) {
|
||||
if (markets.isEmpty()) return Map.of();
|
||||
Map<String, String> all = fetchKoreanNamesFromMarketAll();
|
||||
Map<String, String> out = new HashMap<>();
|
||||
for (String m : markets) {
|
||||
out.put(m, all.getOrDefault(m, m.replace("KRW-", "")));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private Map<String, TickerSnap> fetchTickers(List<String> markets, Map<String, String> koreanNames) {
|
||||
if (markets.isEmpty()) return Map.of();
|
||||
try {
|
||||
WebClient client = webClientBuilder.baseUrl(upbitBaseUrl).build();
|
||||
@@ -722,9 +762,12 @@ public class TrendSearchService {
|
||||
JsonNode arr = objectMapper.readTree(json);
|
||||
for (JsonNode n : arr) {
|
||||
String market = n.path("market").asText();
|
||||
String ko = koreanNames != null
|
||||
? koreanNames.getOrDefault(market, market.replace("KRW-", ""))
|
||||
: market.replace("KRW-", "");
|
||||
map.put(market, new TickerSnap(
|
||||
market,
|
||||
n.path("korean_name").asText(market),
|
||||
ko,
|
||||
n.path("trade_price").asDouble(0),
|
||||
n.path("signed_change_rate").asDouble(0) * 100,
|
||||
n.path("acc_trade_price_24h").asDouble(0)
|
||||
@@ -777,8 +820,11 @@ public class TrendSearchService {
|
||||
|
||||
private record TickerSnap(String market, String koreanName, double tradePrice,
|
||||
double changeRate, double accTradePrice24h) {
|
||||
static TickerSnap empty(String market) {
|
||||
return new TickerSnap(market, market.replace("KRW-", ""), 0, 0, 0);
|
||||
static TickerSnap empty(String market, String koreanName) {
|
||||
String ko = koreanName != null && !koreanName.isBlank()
|
||||
? koreanName
|
||||
: market.replace("KRW-", "");
|
||||
return new TickerSnap(market, ko, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user