모의투자 로직 변경

This commit is contained in:
Macbook
2026-05-31 14:05:46 +09:00
parent ead97dad5d
commit d6eedf19bb
33 changed files with 1456 additions and 330 deletions
@@ -40,8 +40,8 @@ public class LiveStrategyStartupRunner implements ApplicationRunner {
for (GcWatchlist w : watchlistRepo.findAll()) {
Long userId = w.getUserId();
String deviceId = w.getDeviceId();
if (userId == null && deviceId == null) continue;
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
if (userId == null) continue;
GcAppSettings app = appSettingsService.getEntity(userId, null);
if (!Boolean.TRUE.equals(app.getLiveStrategyCheck()) || app.getLiveStrategyId() == null) {
continue;
}
@@ -8,6 +8,8 @@ import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 백테스팅 실행 REST API.
*
@@ -40,42 +42,32 @@ public class BacktestingController {
@PostMapping("/run")
public ResponseEntity<BacktestResponse> run(
@RequestBody BacktestRequest req,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
@RequestHeader Map<String, String> headers) {
if (req.getBars() == null || req.getBars().isEmpty()) {
return ResponseEntity.badRequest().build();
}
Long userId = parseUserId(userIdHeader);
long userId = TradingControllerSupport.requireRegisteredUser(headers);
// DB 저장 파라미터를 기반으로, 요청 params 로 덮어쓰기
// (요청에 params 없으면 DB 값만 사용 → 사용자가 변경한 파라미터 항상 반영)
if (userId != null || deviceId != null) {
java.util.Map<String, java.util.Map<String, Object>> dbParams =
indicatorSettingsService.getAll(userId, deviceId);
if (!dbParams.isEmpty()) {
java.util.Map<String, java.util.Map<String, Object>> merged =
new java.util.HashMap<>(dbParams);
if (req.getIndicatorParams() != null) {
req.getIndicatorParams().forEach((type, params) ->
merged.merge(type, params, (dbP, reqP) -> {
java.util.Map<String, Object> m = new java.util.HashMap<>(dbP);
m.putAll(reqP);
return m;
})
);
}
req.setIndicatorParams(merged);
java.util.Map<String, java.util.Map<String, Object>> dbParams =
indicatorSettingsService.getAll(userId, null);
if (!dbParams.isEmpty()) {
java.util.Map<String, java.util.Map<String, Object>> merged =
new java.util.HashMap<>(dbParams);
if (req.getIndicatorParams() != null) {
req.getIndicatorParams().forEach((type, params) ->
merged.merge(type, params, (dbP, reqP) -> {
java.util.Map<String, Object> m = new java.util.HashMap<>(dbP);
m.putAll(reqP);
return m;
})
);
}
req.setIndicatorParams(merged);
}
BacktestResponse response = backtestingService.run(req);
return ResponseEntity.ok(response);
}
private Long parseUserId(String header) {
if (header == null || header.isBlank()) return null;
try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; }
}
}
@@ -6,6 +6,8 @@ import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 가상투자 — 실시간 조건 충족 현황 API.
*
@@ -22,14 +24,8 @@ public class LiveConditionController {
public ResponseEntity<LiveConditionStatusDto> get(
@RequestParam String market,
@RequestParam long strategyId,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
return ResponseEntity.ok(service.evaluate(parseUserId(userIdHeader), deviceId, market, strategyId));
}
private Long parseUserId(String header) {
if (header == null || header.isBlank()) return null;
try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; }
@RequestHeader Map<String, String> headers) {
long uid = TradingControllerSupport.requireRegisteredUser(headers);
return ResponseEntity.ok(service.evaluate(uid, null, market, strategyId));
}
}
@@ -8,15 +8,8 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 실시간 전략 체크 설정 REST API.
*
* <pre>
* GET /api/strategy/settings?market=KRW-BTC → 현재 설정 조회
* PUT /api/strategy/settings → 설정 저장/갱신
* </pre>
*/
@RestController
@RequestMapping("/strategy/settings")
@RequiredArgsConstructor
@@ -27,42 +20,31 @@ public class LiveStrategySettingsController {
@GetMapping
public ResponseEntity<LiveStrategySettingsDto> get(
@RequestParam(value = "market", defaultValue = "KRW-BTC") String market,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
return ResponseEntity.ok(service.get(parseUserId(userIdHeader), deviceId, market));
@RequestHeader Map<String, String> headers) {
long uid = TradingControllerSupport.requireRegisteredUser(headers);
return ResponseEntity.ok(service.get(uid, null, market));
}
@PutMapping
public ResponseEntity<LiveStrategySettingsDto> put(
@RequestBody LiveStrategySettingsDto dto,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
return ResponseEntity.ok(service.save(parseUserId(userIdHeader), deviceId, dto));
@RequestHeader Map<String, String> headers) {
long uid = TradingControllerSupport.requireRegisteredUser(headers);
return ResponseEntity.ok(service.save(uid, null, dto));
}
/** 실시간 체크 ON + 전략 지정된 종목 목록 (현재 디바이스/유저) */
@GetMapping("/active")
public ResponseEntity<List<LiveStrategySettingsDto>> listActive(
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
return ResponseEntity.ok(service.listActive(parseUserId(userIdHeader), deviceId));
@RequestHeader Map<String, String> headers) {
long uid = TradingControllerSupport.requireRegisteredUser(headers);
return ResponseEntity.ok(service.listActive(uid, null));
}
/** 관심종목 등 여러 마켓에 동일 설정 일괄 저장 */
@PutMapping("/bulk")
public ResponseEntity<List<LiveStrategySettingsDto>> putBulk(
@RequestBody LiveStrategyBulkRequest req,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
return ResponseEntity.ok(service.saveBulk(parseUserId(userIdHeader), deviceId, req));
}
private Long parseUserId(String header) {
if (header == null || header.isBlank()) return null;
try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; }
@RequestHeader Map<String, String> headers) {
long uid = TradingControllerSupport.requireRegisteredUser(headers);
return ResponseEntity.ok(service.saveBulk(uid, null, req));
}
}
@@ -1,6 +1,5 @@
package com.goldenchart.controller;
import com.goldenchart.dto.LiveOrderRequest;
import com.goldenchart.dto.LiveOrderRequest;
import com.goldenchart.dto.LiveSummaryDto;
import com.goldenchart.dto.LiveTradeDto;
@@ -21,27 +20,20 @@ public class LiveTradingController {
@GetMapping("/summary")
public ResponseEntity<LiveSummaryDto> summary(@RequestHeader Map<String, String> h) {
return ResponseEntity.ok(liveTradingService.getSummary(userId(h), deviceId(h)));
long uid = TradingControllerSupport.requireRegisteredUser(h);
return ResponseEntity.ok(liveTradingService.getSummary(uid));
}
@GetMapping("/trades")
public ResponseEntity<List<LiveTradeDto>> trades(@RequestHeader Map<String, String> h) {
return ResponseEntity.ok(liveTradingService.listTrades(userId(h), deviceId(h)));
long uid = TradingControllerSupport.requireRegisteredUser(h);
return ResponseEntity.ok(liveTradingService.listTrades(uid));
}
@PostMapping("/orders")
public ResponseEntity<LiveTradeDto> placeOrder(@RequestHeader Map<String, String> h,
@RequestBody LiveOrderRequest body) {
return ResponseEntity.ok(liveTradingService.placeManualOrder(userId(h), deviceId(h), body));
}
private Long userId(Map<String, String> h) {
String v = h.get("x-user-id");
if (v == null || v.isBlank()) return null;
try { return Long.parseLong(v); } catch (NumberFormatException e) { return null; }
}
private String deviceId(Map<String, String> h) {
return h.getOrDefault("x-device-id", "anonymous");
long uid = TradingControllerSupport.requireRegisteredUser(h);
return ResponseEntity.ok(liveTradingService.placeManualOrder(uid, body));
}
}
@@ -19,37 +19,32 @@ public class PaperTradingController {
private final PaperTradingService paperTradingService;
private Long userId(Map<String, String> h) {
String v = h.get("x-user-id");
return v != null ? Long.parseLong(v) : null;
}
private String deviceId(Map<String, String> h) {
return h.getOrDefault("x-device-id", "anonymous");
}
@GetMapping("/summary")
public ResponseEntity<PaperSummaryDto> summary(@RequestHeader Map<String, String> h) {
return ResponseEntity.ok(paperTradingService.getSummary(userId(h), deviceId(h), null));
long uid = TradingControllerSupport.requireRegisteredUser(h);
return ResponseEntity.ok(paperTradingService.getSummary(uid, null));
}
@PostMapping("/summary")
public ResponseEntity<PaperSummaryDto> summaryWithMarks(
@RequestHeader Map<String, String> h,
@RequestBody(required = false) Map<String, Double> markPrices) {
return ResponseEntity.ok(paperTradingService.getSummary(userId(h), deviceId(h), markPrices));
long uid = TradingControllerSupport.requireRegisteredUser(h);
return ResponseEntity.ok(paperTradingService.getSummary(uid, markPrices));
}
@GetMapping("/allocations")
public ResponseEntity<List<PaperAllocationDto>> allocations(@RequestHeader Map<String, String> h) {
return ResponseEntity.ok(paperTradingService.listAllocations(userId(h), deviceId(h), null));
long uid = TradingControllerSupport.requireRegisteredUser(h);
return ResponseEntity.ok(paperTradingService.listAllocations(uid, null));
}
@PutMapping("/allocations/bulk")
public ResponseEntity<List<PaperAllocationDto>> bulkAllocations(
@RequestHeader Map<String, String> h,
@RequestBody PaperAllocationBulkRequest body) {
return ResponseEntity.ok(paperTradingService.bulkAllocations(userId(h), deviceId(h), body));
long uid = TradingControllerSupport.requireRegisteredUser(h);
return ResponseEntity.ok(paperTradingService.bulkAllocations(uid, body));
}
@PatchMapping("/allocations/{symbol}")
@@ -57,7 +52,8 @@ public class PaperTradingController {
@RequestHeader Map<String, String> h,
@PathVariable String symbol,
@RequestBody PaperAllocationPatchRequest body) {
return ResponseEntity.ok(paperTradingService.patchAllocation(userId(h), deviceId(h), symbol, body));
long uid = TradingControllerSupport.requireRegisteredUser(h);
return ResponseEntity.ok(paperTradingService.patchAllocation(uid, symbol, body));
}
@GetMapping("/trades")
@@ -70,12 +66,13 @@ public class PaperTradingController {
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime to,
@RequestParam(defaultValue = "0") int page,
@RequestParam(required = false) Integer size) {
long uid = TradingControllerSupport.requireRegisteredUser(h);
if (size == null && symbol == null && side == null && source == null && from == null && to == null) {
return ResponseEntity.ok(paperTradingService.listTradesLegacy(userId(h), deviceId(h)));
return ResponseEntity.ok(paperTradingService.listTradesLegacy(uid));
}
int pageSize = size != null ? size : 50;
return ResponseEntity.ok(paperTradingService.listTrades(
userId(h), deviceId(h), symbol, side, source, from, to, page, pageSize));
uid, symbol, side, source, from, to, page, pageSize));
}
@GetMapping("/ledger")
@@ -85,7 +82,8 @@ public class PaperTradingController {
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime to,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "50") int size) {
return ResponseEntity.ok(paperTradingService.listLedger(userId(h), deviceId(h), from, to, page, size));
long uid = TradingControllerSupport.requireRegisteredUser(h);
return ResponseEntity.ok(paperTradingService.listLedger(uid, from, to, page, size));
}
@GetMapping("/snapshots/daily")
@@ -93,30 +91,35 @@ public class PaperTradingController {
@RequestHeader Map<String, String> h,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
return ResponseEntity.ok(paperTradingService.listDailySnapshots(userId(h), deviceId(h), from, to));
long uid = TradingControllerSupport.requireRegisteredUser(h);
return ResponseEntity.ok(paperTradingService.listDailySnapshots(uid, from, to));
}
@GetMapping("/orders")
public ResponseEntity<List<PaperOrderDto>> pendingOrders(@RequestHeader Map<String, String> h) {
return ResponseEntity.ok(paperTradingService.listPendingOrders(userId(h), deviceId(h)));
long uid = TradingControllerSupport.requireRegisteredUser(h);
return ResponseEntity.ok(paperTradingService.listPendingOrders(uid));
}
@PostMapping("/orders")
public ResponseEntity<PaperPlaceOrderResult> placeOrder(
@RequestHeader Map<String, String> h,
@RequestBody PaperOrderRequest body) {
return ResponseEntity.ok(paperTradingService.placeOrder(userId(h), deviceId(h), body));
long uid = TradingControllerSupport.requireRegisteredUser(h);
return ResponseEntity.ok(paperTradingService.placeOrder(uid, body));
}
@PostMapping("/orders/{id}/cancel")
public ResponseEntity<PaperOrderDto> cancelOrder(
@RequestHeader Map<String, String> h,
@PathVariable Long id) {
return ResponseEntity.ok(paperTradingService.cancelOrder(userId(h), deviceId(h), id));
long uid = TradingControllerSupport.requireRegisteredUser(h);
return ResponseEntity.ok(paperTradingService.cancelOrder(uid, id));
}
@PostMapping("/reset")
public ResponseEntity<PaperSummaryDto> reset(@RequestHeader Map<String, String> h) {
return ResponseEntity.ok(paperTradingService.resetAccount(userId(h), deviceId(h)));
long uid = TradingControllerSupport.requireRegisteredUser(h);
return ResponseEntity.ok(paperTradingService.resetAccount(uid));
}
}
@@ -8,6 +8,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 투자전략 CRUD REST API.
@@ -29,14 +30,17 @@ public class StrategyController {
/** 전략 목록 조회 */
@GetMapping
public ResponseEntity<List<StrategyDto>> list(
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
return ResponseEntity.ok(service.list(parseUserId(userIdHeader), deviceId));
@RequestHeader Map<String, String> headers) {
long uid = TradingControllerSupport.requireRegisteredUser(headers);
return ResponseEntity.ok(service.list(uid, null));
}
/** 단일 전략 조회 */
@GetMapping("/{id}")
public ResponseEntity<StrategyDto> get(@PathVariable Long id) {
public ResponseEntity<StrategyDto> get(
@PathVariable Long id,
@RequestHeader Map<String, String> headers) {
TradingControllerSupport.requireRegisteredUser(headers);
return service.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
@@ -46,20 +50,26 @@ public class StrategyController {
@PostMapping
public ResponseEntity<StrategyDto> save(
@RequestBody StrategyDto dto,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
return ResponseEntity.ok(service.save(dto, parseUserId(userIdHeader), deviceId));
@RequestHeader Map<String, String> headers) {
long uid = TradingControllerSupport.requireRegisteredUser(headers);
return ResponseEntity.ok(service.save(dto, uid, null));
}
/** 전략 DSL에 포함된 평가 시간봉 목록 */
@GetMapping("/{id}/timeframes")
public ResponseEntity<List<String>> timeframes(@PathVariable Long id) {
public ResponseEntity<List<String>> timeframes(
@PathVariable Long id,
@RequestHeader Map<String, String> headers) {
TradingControllerSupport.requireRegisteredUser(headers);
return ResponseEntity.ok(conditionTimeframes.collectForStrategyList(id));
}
/** TIMEFRAME 래핑 누락 DSL 보정 (3분봉 전략이 1분봉으로 평가되는 경우) */
@PostMapping("/{id}/repair-timeframes")
public ResponseEntity<StrategyDto> repairTimeframes(@PathVariable Long id) {
public ResponseEntity<StrategyDto> repairTimeframes(
@PathVariable Long id,
@RequestHeader Map<String, String> headers) {
TradingControllerSupport.requireRegisteredUser(headers);
return service.repairTimeframeWrappers(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
@@ -67,13 +77,11 @@ public class StrategyController {
/** 전략 삭제 */
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
public ResponseEntity<Void> delete(
@PathVariable Long id,
@RequestHeader Map<String, String> headers) {
TradingControllerSupport.requireRegisteredUser(headers);
service.delete(id);
return ResponseEntity.noContent().build();
}
private Long parseUserId(String header) {
if (header == null || header.isBlank()) return null;
try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; }
}
}
@@ -7,18 +7,8 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 매매 시그널 이력 REST API.
*
* <pre>
* GET /api/trade-signals → 전체 이력
* GET /api/trade-signals?market= → 종목별 이력
* DELETE /api/trade-signals/{id} → 단건 삭제
* DELETE /api/trade-signals → 전체 삭제
* POST /api/trade-signals/delete-batch → ID 목록 삭제
* </pre>
*/
@RestController
@RequestMapping("/trade-signals")
@RequiredArgsConstructor
@@ -29,44 +19,36 @@ public class TradeSignalController {
@GetMapping
public ResponseEntity<List<TradeSignalDto>> list(
@RequestParam(required = false) String market,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
Long userId = parseUserId(userIdHeader);
@RequestHeader Map<String, String> headers) {
long uid = TradingControllerSupport.requireRegisteredUser(headers);
List<TradeSignalDto> result = (market != null && !market.isBlank())
? service.listByMarket(userId, deviceId, market)
: service.list(userId, deviceId);
? service.listByMarket(uid, null, market)
: service.list(uid, null);
return ResponseEntity.ok(result);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteOne(
@PathVariable Long id,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
service.deleteOne(id, parseUserId(userIdHeader), deviceId);
@RequestHeader Map<String, String> headers) {
long uid = TradingControllerSupport.requireRegisteredUser(headers);
service.deleteOne(id, uid, null);
return ResponseEntity.noContent().build();
}
@DeleteMapping
public ResponseEntity<java.util.Map<String, Integer>> deleteAll(
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
int deleted = service.deleteAll(parseUserId(userIdHeader), deviceId);
return ResponseEntity.ok(java.util.Map.of("deleted", deleted));
public ResponseEntity<Map<String, Integer>> deleteAll(@RequestHeader Map<String, String> headers) {
long uid = TradingControllerSupport.requireRegisteredUser(headers);
int deleted = service.deleteAll(uid, null);
return ResponseEntity.ok(Map.of("deleted", deleted));
}
@PostMapping("/delete-batch")
public ResponseEntity<java.util.Map<String, Integer>> deleteBatch(
@RequestBody java.util.List<Long> ids,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
int deleted = service.deleteBatch(ids, parseUserId(userIdHeader), deviceId);
return ResponseEntity.ok(java.util.Map.of("deleted", deleted));
}
private Long parseUserId(String h) {
if (h == null || h.isBlank()) return null;
try { return Long.parseLong(h); } catch (NumberFormatException e) { return null; }
public ResponseEntity<Map<String, Integer>> deleteBatch(
@RequestBody List<Long> ids,
@RequestHeader Map<String, String> headers) {
long uid = TradingControllerSupport.requireRegisteredUser(headers);
int deleted = service.deleteBatch(ids, uid, null);
return ResponseEntity.ok(Map.of("deleted", deleted));
}
}
@@ -0,0 +1,33 @@
package com.goldenchart.controller;
import com.goldenchart.trading.TradingAccess;
import java.util.Map;
/** 매매·모의·전략 API — 정식 로그인(userId) 필수 */
public final class TradingControllerSupport {
private TradingControllerSupport() {}
public static long requireRegisteredUser(Map<String, String> headers) {
String raw = headers.get("x-user-id");
if (raw == null || raw.isBlank()) {
throw new com.goldenchart.trading.TradingAccessDeniedException();
}
try {
return TradingAccess.requireUserId(Long.parseLong(raw.trim()));
} catch (NumberFormatException e) {
throw new com.goldenchart.trading.TradingAccessDeniedException();
}
}
public static Long parseUserId(Map<String, String> headers) {
String raw = headers.get("x-user-id");
if (raw == null || raw.isBlank()) return null;
try {
return Long.parseLong(raw.trim());
} catch (NumberFormatException e) {
return null;
}
}
}
@@ -18,6 +18,9 @@ public interface GcLiveStrategySettingsRepository
/** isLiveCheck = true 인 모든 설정 (스케줄러 순회용) */
List<GcLiveStrategySettings> findAllByIsLiveCheckTrue();
/** 정식 로그인 사용자만 — 게스트(device-only) 제외 */
List<GcLiveStrategySettings> findAllByIsLiveCheckTrueAndUserIdIsNotNull();
/** 디바이스별 실시간 체크 ON 설정 */
List<GcLiveStrategySettings> findByDeviceIdAndIsLiveCheckTrue(String deviceId);
@@ -8,6 +8,8 @@ import java.util.Optional;
public interface GcLiveTradeRepository extends JpaRepository<GcLiveTrade, Long> {
List<GcLiveTrade> findTop100ByDeviceIdOrderByCreatedAtDesc(String deviceId);
List<GcLiveTrade> findTop100ByUserIdOrderByCreatedAtDesc(Long userId);
Optional<GcLiveTrade> findTopByDeviceIdAndSymbolAndSideOrderByCreatedAtDesc(
String deviceId, String symbol, String side);
List<GcLiveTrade> findBySymbolAndSideOrderByCreatedAtDesc(String symbol, String side);
@@ -58,6 +58,7 @@ public class BarCloseStrategyEvaluationService {
long candleTimeEpoch = barEndEpoch - signalBar.getTimePeriod().getSeconds();
for (GcLiveStrategySettings s : activeSettings) {
if (s.getUserId() == null) continue;
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), ct)) continue;
String closeSignal = "NONE";
@@ -82,9 +83,9 @@ public class BarCloseStrategyEvaluationService {
market, s.getStrategyId(), null,
closeSignal, signalBar.getClosePrice().doubleValue(),
candleTimeEpoch, ct, signalExecType);
if (s.getDeviceId() != null) {
if (s.getUserId() != null) {
orderExecutionQueue.submitSignal(
s.getDeviceId(), s.getUserId(), market,
s.getUserId(), market,
s.getStrategyId(), closeSignal,
signalBar.getClosePrice().doubleValue());
}
@@ -51,22 +51,27 @@ public class DashboardService {
: watchlistRepo.findByDeviceIdOrderByDisplayOrderAsc(deviceId).size();
out.put("watchlistCount", watchlistCount);
int liveCheckMarkets = (int) liveSettingsRepo.findAllByIsLiveCheckTrue().stream()
.filter(s -> deviceId.equals(s.getDeviceId()) || (userId != null && userId.equals(s.getUserId())))
.count();
int liveCheckMarkets = userId != null
? (int) liveSettingsRepo.findAllByIsLiveCheckTrueAndUserIdIsNotNull().stream()
.filter(s -> userId.equals(s.getUserId()))
.count()
: 0;
out.put("liveCheckMarkets", liveCheckMarkets);
List<LiveStrategySettingsDto> active = liveStrategySettingsService.listActive(userId, deviceId);
out.put("monitoredMarkets", active.size());
PaperSummaryDto paper = paperTradingService.getSummary(userId, deviceId, null);
out.put("paper", paper);
if (userId != null) {
out.put("paper", paperTradingService.getSummary(userId, null));
out.put("live", liveTradingService.getSummary(userId));
} else {
out.put("paper", PaperSummaryDto.builder().build());
out.put("live", LiveSummaryDto.builder().build());
}
LiveSummaryDto live = liveTradingService.getSummary(userId, deviceId);
out.put("live", live);
List<TradeSignalDto> recentSignals = signalRepo
.findByDeviceIdOrderByCreatedAtDesc(deviceId)
List<TradeSignalDto> recentSignals = (userId != null
? signalRepo.findByUserIdOrderByCreatedAtDesc(userId)
: signalRepo.findByDeviceIdOrderByCreatedAtDesc(deviceId != null ? deviceId : ""))
.stream().limit(8)
.map(s -> TradeSignalDto.builder()
.id(s.getId())
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.goldenchart.dto.BacktestSettingsDto;
import com.goldenchart.entity.GcAppSettings;
import com.goldenchart.repository.GcAppSettingsRepository;
import com.goldenchart.trading.TradingAccess;
import com.goldenchart.trading.TradingExecutionService;
import com.goldenchart.trading.TradingMode;
import com.goldenchart.trading.pipeline.OrderRequest;
@@ -50,8 +51,9 @@ public class LiveRiskMonitorService {
if (!Boolean.TRUE.equals(app.getLiveAutoTradeEnabled())) continue;
if (!liveTradingService.hasApiKeys(app)) continue;
if (!TradingMode.fromString(app.getTradingMode()).useLive()) continue;
String deviceId = app.getDeviceId();
if (deviceId == null || deviceId.isBlank()) continue;
Long userId = app.getUserId();
if (userId == null || userId <= 0) continue;
String cacheKey = TradingAccess.accountDeviceKey(userId);
try {
var creds = appSettingsService.resolveUpbitCredentials(app);
if (!creds.isComplete()) continue;
@@ -67,9 +69,9 @@ public class LiveRiskMonitorService {
coins.put(cur, new double[]{bal, avg});
}
}
accountCache.put(deviceId, coins);
accountCache.put(cacheKey, coins);
} catch (Exception e) {
log.debug("[LiveRisk] account sync {}: {}", deviceId, e.getMessage());
log.debug("[LiveRisk] account sync userId={}: {}", userId, e.getMessage());
}
}
}
@@ -79,40 +81,51 @@ public class LiveRiskMonitorService {
String currency = market.startsWith("KRW-") ? market.substring(4) : market;
for (var entry : accountCache.entrySet()) {
String deviceId = entry.getKey();
String cacheKey = entry.getKey();
double[] pos = entry.getValue().get(currency);
if (pos == null || pos[0] <= 0 || pos[1] <= 0) continue;
String key = deviceId + ":" + market;
String cooldownKey = cacheKey + ":" + market;
long now = System.currentTimeMillis();
Long last = exitCooldown.get(key);
Long last = exitCooldown.get(cooldownKey);
if (last != null && now - last < COOLDOWN_MS) continue;
GcAppSettings app = appSettingsRepo.findByDeviceId(deviceId).orElse(null);
Long userId = parseUserIdFromCacheKey(cacheKey);
if (userId == null) continue;
GcAppSettings app = appSettingsRepo.findByUserId(userId).orElse(null);
if (app == null) continue;
BacktestSettingsDto risk = backtestSettingsService.get(deviceId);
BacktestSettingsDto risk = backtestSettingsService.get(cacheKey);
double avg = pos[1];
double pnlPct = (tradePrice - avg) / avg * 100.0;
if (Boolean.TRUE.equals(risk.getStopLossEnabled())
&& risk.getStopLossPct() != null
&& pnlPct <= -risk.getStopLossPct().doubleValue()) {
exitCooldown.put(key, now);
exitCooldown.put(cooldownKey, now);
tradingExecutionService.executeRiskExit(
OrderRequest.stopLoss(deviceId, app.getUserId(), market, tradePrice,
OrderRequest.stopLoss(cacheKey, userId, market, tradePrice,
risk.getStopLossPct().doubleValue()));
return;
}
if (Boolean.TRUE.equals(risk.getTakeProfitEnabled())
&& risk.getTakeProfitPct() != null
&& pnlPct >= risk.getTakeProfitPct().doubleValue()) {
exitCooldown.put(key, now);
exitCooldown.put(cooldownKey, now);
tradingExecutionService.executeRiskExit(
OrderRequest.takeProfit(deviceId, app.getUserId(), market, tradePrice,
OrderRequest.takeProfit(cacheKey, userId, market, tradePrice,
risk.getTakeProfitPct().doubleValue()));
return;
}
}
}
private static Long parseUserIdFromCacheKey(String cacheKey) {
if (cacheKey == null || !cacheKey.startsWith("user:")) return null;
try {
return Long.parseLong(cacheKey.substring(5));
} catch (NumberFormatException e) {
return null;
}
}
}
@@ -8,6 +8,7 @@ import com.goldenchart.dto.UpbitApiCredentials;
import com.goldenchart.entity.GcAppSettings;
import com.goldenchart.entity.GcLiveTrade;
import com.goldenchart.repository.GcLiveTradeRepository;
import com.goldenchart.trading.TradingAccess;
import com.goldenchart.trading.TradingMode;
import com.goldenchart.upbit.UpbitOrderApiClient;
import lombok.RequiredArgsConstructor;
@@ -36,8 +37,9 @@ public class LiveTradingService {
private final GcLiveTradeRepository tradeRepo;
@Transactional(readOnly = true)
public LiveSummaryDto getSummary(Long userId, String deviceId) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
public LiveSummaryDto getSummary(Long userId) {
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
UpbitApiCredentials creds = appSettingsService.resolveUpbitCredentials(app);
boolean configured = creds.isComplete();
double krw = 0;
@@ -76,22 +78,27 @@ public class LiveTradingService {
}
@Transactional(readOnly = true)
public List<LiveTradeDto> listTrades(Long userId, String deviceId) {
String dev = resolveDeviceId(deviceId);
return tradeRepo.findTop100ByDeviceIdOrderByCreatedAtDesc(dev)
public List<LiveTradeDto> listTrades(Long userId) {
long uid = TradingAccess.requireUserId(userId);
return tradeRepo.findTop100ByUserIdOrderByCreatedAtDesc(uid)
.stream().map(this::toDto).toList();
}
/**
* 전략 시그널·SL/TP 에서 호출.
*/
public void tryExecuteOnSignal(String deviceId, Long userId, String market,
public void tryExecuteOnSignal(Long userId, String market,
Long strategyId, String signalType, double price,
String source) {
if (deviceId == null || deviceId.isBlank()) return;
long uid;
try {
uid = TradingAccess.requireUserId(userId);
} catch (Exception e) {
return;
}
if (!"BUY".equals(signalType) && !"SELL".equals(signalType)) return;
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
TradingMode mode = TradingMode.fromString(app.getTradingMode());
if (!mode.useLive()) return;
if (!Boolean.TRUE.equals(app.getLiveAutoTradeEnabled())) return;
@@ -118,7 +125,7 @@ public class LiveTradingService {
return;
}
JsonNode res = upbitApi.placeMarketBuy(access, secret, market, budget);
recordTrade(deviceId, userId, market, "BUY", source, strategyId, res, price, budget);
recordTrade(uid, market, "BUY", source, strategyId, res, price, budget);
log.info("[LiveTrading] BUY {} ~{} KRW", market, (long) budget);
} else {
double vol = upbitApi.getCoinBalance(access, secret, market);
@@ -127,7 +134,7 @@ public class LiveTradingService {
return;
}
JsonNode res = upbitApi.placeMarketSell(access, secret, market, vol);
recordTrade(deviceId, userId, market, "SELL", source, strategyId, res, price, vol);
recordTrade(uid, market, "SELL", source, strategyId, res, price, vol);
log.info("[LiveTrading] SELL {} vol={}", market, vol);
}
} catch (UpbitOrderApiClient.UpbitApiException e) {
@@ -138,11 +145,12 @@ public class LiveTradingService {
/**
* 알림 모달·수동 주문 — 시장가 매수/매도.
*/
public LiveTradeDto placeManualOrder(Long userId, String deviceId, LiveOrderRequest req) {
public LiveTradeDto placeManualOrder(Long userId, LiveOrderRequest req) {
long uid = TradingAccess.requireUserId(userId);
if (req == null || req.getMarket() == null || req.getSide() == null) {
throw new IllegalArgumentException("market, side required");
}
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
UpbitApiCredentials creds = appSettingsService.resolveUpbitCredentials(app);
if (!creds.isComplete()) {
throw new IllegalStateException("업비트 API 키가 등록되지 않았습니다.");
@@ -151,8 +159,6 @@ public class LiveTradingService {
String secret = creds.secretKey();
String market = req.getMarket();
String side = req.getSide().toUpperCase();
String dev = resolveDeviceId(deviceId);
try {
if ("BUY".equals(side)) {
double budget = req.getKrwAmount() != null && req.getKrwAmount() > 0
@@ -162,8 +168,8 @@ public class LiveTradingService {
throw new IllegalStateException("주문 가능 금액이 부족합니다.");
}
JsonNode res = upbitApi.placeMarketBuy(access, secret, market, budget);
recordTrade(dev, userId, market, "BUY", "MANUAL", req.getStrategyId(), res, 0, budget);
return listTrades(userId, deviceId).get(0);
recordTrade(uid, market, "BUY", "MANUAL", req.getStrategyId(), res, 0, budget);
return listTrades(uid).get(0);
}
if ("SELL".equals(side)) {
double vol = req.getQuantity() != null && req.getQuantity() > 0
@@ -171,8 +177,8 @@ public class LiveTradingService {
: upbitApi.getCoinBalance(access, secret, market);
if (vol <= 0) throw new IllegalStateException("매도 가능 수량이 없습니다.");
JsonNode res = upbitApi.placeMarketSell(access, secret, market, vol);
recordTrade(dev, userId, market, "SELL", "MANUAL", req.getStrategyId(), res, 0, vol);
return listTrades(userId, deviceId).get(0);
recordTrade(uid, market, "SELL", "MANUAL", req.getStrategyId(), res, 0, vol);
return listTrades(uid).get(0);
}
throw new IllegalArgumentException("side must be BUY or SELL");
} catch (UpbitOrderApiClient.UpbitApiException e) {
@@ -198,7 +204,7 @@ public class LiveTradingService {
return appSettingsService.hasUpbitApiKeys(app);
}
private void recordTrade(String deviceId, Long userId, String market, String side,
private void recordTrade(long userId, String market, String side,
String source, Long strategyId, JsonNode res,
double refPrice, double amountOrVol) {
String uuid = res != null ? res.path("uuid").asText(null) : null;
@@ -209,7 +215,7 @@ public class LiveTradingService {
: BigDecimal.valueOf(refPrice * executed).setScale(2, RM);
tradeRepo.save(GcLiveTrade.builder()
.deviceId(resolveDeviceId(deviceId))
.deviceId(TradingAccess.accountDeviceKey(userId))
.userId(userId)
.symbol(market)
.side(side)
@@ -239,7 +245,4 @@ public class LiveTradingService {
.build();
}
private static String resolveDeviceId(String deviceId) {
return deviceId != null && !deviceId.isBlank() ? deviceId : "anonymous";
}
}
@@ -5,6 +5,7 @@ import com.goldenchart.dto.*;
import com.goldenchart.entity.*;
import com.goldenchart.repository.*;
import com.goldenchart.storage.Ta4jStorage;
import com.goldenchart.trading.TradingAccess;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
@@ -51,9 +52,10 @@ public class PaperTradingService {
// ── 조회 ─────────────────────────────────────────────────────────────────
@Transactional(readOnly = true)
public PaperSummaryDto getSummary(Long userId, String deviceId, Map<String, Double> markPrices) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
public PaperSummaryDto getSummary(Long userId, Map<String, Double> markPrices) {
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
GcPaperAccount account = getOrCreateAccount(uid, app);
List<GcPaperPosition> positions = positionRepo.findByAccountIdOrderBySymbolAsc(account.getId());
double stockEval = 0;
@@ -121,38 +123,40 @@ public class PaperTradingService {
}
@Transactional(readOnly = true)
public List<PaperAllocationDto> listAllocations(Long userId, String deviceId,
Map<String, Double> markPrices) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
PaperSummaryDto s = getSummary(userId, deviceId, markPrices);
public List<PaperAllocationDto> listAllocations(Long userId, Map<String, Double> markPrices) {
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
GcPaperAccount account = getOrCreateAccount(uid, app);
PaperSummaryDto s = getSummary(uid, markPrices);
return allocationService.listWithMetrics(account.getId(), markPrices, s.getTotalAsset());
}
public List<PaperAllocationDto> bulkAllocations(Long userId, String deviceId,
PaperAllocationBulkRequest req) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
public List<PaperAllocationDto> bulkAllocations(Long userId, PaperAllocationBulkRequest req) {
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
GcPaperAccount account = getOrCreateAccount(uid, app);
BigDecimal defaultMax = account.getInitialCapitalSnapshot() != null
? account.getInitialCapitalSnapshot()
: app.getPaperInitialCapital();
return allocationService.bulkUpsert(account.getId(), req, defaultMax);
}
public PaperAllocationDto patchAllocation(Long userId, String deviceId, String symbol,
public PaperAllocationDto patchAllocation(Long userId, String symbol,
PaperAllocationPatchRequest req) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
GcPaperAccount account = getOrCreateAccount(uid, app);
return allocationService.patch(account.getId(), symbol, req);
}
@Transactional(readOnly = true)
public PaperPageDto<PaperTradeDto> listTrades(Long userId, String deviceId,
public PaperPageDto<PaperTradeDto> listTrades(Long userId,
String symbol, String side, String source,
LocalDateTime from, LocalDateTime to,
int page, int size) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
GcPaperAccount account = getOrCreateAccount(uid, app);
PageRequest pr = PageRequest.of(Math.max(0, page), Math.min(100, Math.max(1, size)));
Page<GcPaperTrade> result = tradeRepo.findFiltered(
account.getId(), symbol, side, source, from, to, pr);
@@ -165,39 +169,43 @@ public class PaperTradingService {
.build();
}
public List<PaperTradeDto> listTradesLegacy(Long userId, String deviceId) {
return listTrades(userId, deviceId, null, null, null, null, null, 0, 100).getContent();
public List<PaperTradeDto> listTradesLegacy(Long userId) {
return listTrades(userId, null, null, null, null, null, 0, 100).getContent();
}
@Transactional(readOnly = true)
public PaperPageDto<PaperLedgerEntryDto> listLedger(Long userId, String deviceId,
public PaperPageDto<PaperLedgerEntryDto> listLedger(Long userId,
LocalDateTime from, LocalDateTime to,
int page, int size) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
GcPaperAccount account = getOrCreateAccount(uid, app);
return ledgerService.list(account.getId(), from, to, page, size);
}
@Transactional(readOnly = true)
public List<PaperDailySnapshotDto> listDailySnapshots(Long userId, String deviceId,
public List<PaperDailySnapshotDto> listDailySnapshots(Long userId,
LocalDate from, LocalDate to) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
GcPaperAccount account = getOrCreateAccount(uid, app);
return snapshotService.list(account.getId(), from, to);
}
@Transactional(readOnly = true)
public List<PaperOrderDto> listPendingOrders(Long userId, String deviceId) {
GcPaperAccount account = getOrCreateAccount(userId, deviceId,
appSettingsService.getEntity(userId, deviceId));
public List<PaperOrderDto> listPendingOrders(Long userId) {
long uid = TradingAccess.requireUserId(userId);
GcPaperAccount account = getOrCreateAccount(uid,
appSettingsService.getEntity(uid, null));
return orderRepo.findByAccountIdAndStatusOrderByCreatedAtDesc(account.getId(), "PENDING")
.stream().map(this::toOrderDto).toList();
}
// ── 주문 ─────────────────────────────────────────────────────────────────
public PaperPlaceOrderResult placeOrder(Long userId, String deviceId, PaperOrderRequest req) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
public PaperPlaceOrderResult placeOrder(Long userId, PaperOrderRequest req) {
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
if (!Boolean.TRUE.equals(app.getPaperTradingEnabled())) {
throw new IllegalStateException("모의투자가 비활성화되어 있습니다.");
}
@@ -217,7 +225,7 @@ public class PaperTradingService {
String source = req.getSource() != null ? req.getSource() : "MANUAL";
String orderKind = req.getOrderKind() != null ? req.getOrderKind() : "limit";
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
GcPaperAccount account = getOrCreateAccount(uid, app);
double qty = req.getQuantity() != null ? req.getQuantity() : 0;
if (qty <= 0) {
qty = resolveAutoQuantity(account, app, req.getMarket(), side, price,
@@ -233,14 +241,15 @@ public class PaperTradingService {
return PaperPlaceOrderResult.builder().order(toOrderDto(order)).build();
}
PaperTradeDto trade = executeTrade(userId, deviceId, app, req.getMarket(), side, orderKind, source,
PaperTradeDto trade = executeTrade(uid, app, req.getMarket(), side, orderKind, source,
req.getStrategyId(), price, qty, null, false);
return PaperPlaceOrderResult.builder().trade(trade).build();
}
public PaperOrderDto cancelOrder(Long userId, String deviceId, Long orderId) {
GcPaperAccount account = getOrCreateAccount(userId, deviceId,
appSettingsService.getEntity(userId, deviceId));
public PaperOrderDto cancelOrder(Long userId, Long orderId) {
long uid = TradingAccess.requireUserId(userId);
GcPaperAccount account = getOrCreateAccount(uid,
appSettingsService.getEntity(uid, null));
GcPaperOrder order = orderRepo.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException("주문을 찾을 수 없습니다."));
if (!order.getAccountId().equals(account.getId())) {
@@ -255,12 +264,17 @@ public class PaperTradingService {
return toOrderDto(order);
}
public void tryExecuteOnSignal(String deviceId, Long userId, String market,
public void tryExecuteOnSignal(Long userId, String market,
Long strategyId, String signalType, double price) {
if (deviceId == null || deviceId.isBlank()) return;
long uid;
try {
uid = TradingAccess.requireUserId(userId);
} catch (Exception e) {
return;
}
if (!"BUY".equals(signalType) && !"SELL".equals(signalType)) return;
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
if (!Boolean.TRUE.equals(app.getPaperTradingEnabled())) return;
if (!Boolean.TRUE.equals(app.getPaperAutoTradeEnabled())) return;
List<GcLiveStrategySettings> liveForMarket = liveSettingsRepo.findActiveByMarket(market).stream()
@@ -270,7 +284,7 @@ public class PaperTradingService {
if (!Boolean.TRUE.equals(app.getLiveStrategyCheck()) && !marketActive) return;
try {
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
GcPaperAccount account = getOrCreateAccount(uid, app);
String positionMode = liveForMarket.stream()
.map(GcLiveStrategySettings::getPositionMode)
.filter(m -> m != null && !m.isBlank())
@@ -299,7 +313,7 @@ public class PaperTradingService {
if (denom <= 0 || budget < minOrder) return;
double qty = budget / denom;
if (qty * execPrice < minOrder) return;
executeTrade(userId, deviceId, app, market, "BUY", "market", "STRATEGY",
executeTrade(uid, app, market, "BUY", "market", "STRATEGY",
strategyId, price, qty, null, true);
log.info("[Paper] STRATEGY BUY {} qty≈{} @ {}", market, qty, price);
} else {
@@ -308,7 +322,7 @@ public class PaperTradingService {
if (pos == null || pos.getQuantity().doubleValue() <= 0) return;
double avail = availableSellQty(account.getId(), market, pos.getQuantity().doubleValue());
if (avail <= 0) return;
executeTrade(userId, deviceId, app, market, "SELL", "market", "STRATEGY",
executeTrade(uid, app, market, "SELL", "market", "STRATEGY",
strategyId, price, avail, null, true);
log.info("[Paper] STRATEGY SELL {} qty={} @ {}", market, avail, price);
}
@@ -332,11 +346,13 @@ public class PaperTradingService {
GcPaperAccount account = accountRepo.findById(order.getAccountId()).orElse(null);
if (account == null) return;
GcAppSettings app = appSettingsService.getEntity(account.getUserId(), account.getDeviceId());
if (account.getUserId() == null) return;
long uid = account.getUserId();
GcAppSettings app = appSettingsService.getEntity(uid, null);
releaseReservation(account, order);
double qty = order.getQuantity().doubleValue();
executeTrade(account.getUserId(), account.getDeviceId(), app, order.getSymbol(),
executeTrade(uid, app, order.getSymbol(),
order.getSide(), order.getOrderKind(), order.getSource(), order.getStrategyId(),
limit, qty, null, "STRATEGY".equals(order.getSource()));
@@ -361,9 +377,10 @@ public class PaperTradingService {
return 0;
}
public PaperSummaryDto resetAccount(Long userId, String deviceId) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
public PaperSummaryDto resetAccount(Long userId) {
long uid = TradingAccess.requireUserId(userId);
GcAppSettings app = appSettingsService.getEntity(uid, null);
GcPaperAccount account = getOrCreateAccount(uid, app);
BigDecimal initial = app.getPaperInitialCapital() != null
? app.getPaperInitialCapital() : BigDecimal.valueOf(10_000_000);
BigDecimal cashBefore = account.getCashBalance();
@@ -405,8 +422,8 @@ public class PaperTradingService {
accountRepo.save(account);
allocationService.seedFromPositions(account.getId(), initial);
log.info("[Paper] account reset device={} capital={}", deviceId, initial);
return getSummary(userId, deviceId, null);
log.info("[Paper] account reset userId={} capital={}", uid, initial);
return getSummary(uid, null);
}
// ── 내부 ─────────────────────────────────────────────────────────────────
@@ -481,11 +498,11 @@ public class PaperTradingService {
return Math.max(0, held - reserved);
}
private PaperTradeDto executeTrade(Long userId, String deviceId, GcAppSettings app,
private PaperTradeDto executeTrade(long userId, GcAppSettings app,
String market, String side, String orderKind, String source,
Long strategyId, double inputPrice, double inputQty,
String koreanName, boolean autoTrade) {
GcPaperAccount account = getOrCreateAccount(userId, deviceId, app);
GcPaperAccount account = getOrCreateAccount(userId, app);
double feeRate = pct(app.getPaperFeeRatePct(), 0.05);
double slip = pct(app.getPaperSlippagePct(), 0);
double minOrder = app.getPaperMinOrderKrw() != null
@@ -638,14 +655,14 @@ public class PaperTradingService {
positionRepo.save(pos);
}
private GcPaperAccount getOrCreateAccount(Long userId, String deviceId, GcAppSettings app) {
String dev = deviceId != null && !deviceId.isBlank() ? deviceId : "anonymous";
return accountRepo.findByDeviceId(dev).orElseGet(() -> {
private GcPaperAccount getOrCreateAccount(long userId, GcAppSettings app) {
return accountRepo.findByUserId(userId).orElseGet(() -> {
String devKey = TradingAccess.accountDeviceKey(userId);
BigDecimal initial = app.getPaperInitialCapital() != null
? app.getPaperInitialCapital() : BigDecimal.valueOf(10_000_000);
GcPaperAccount a = GcPaperAccount.builder()
.userId(userId)
.deviceId(dev)
.deviceId(devKey)
.cashBalance(initial)
.realizedPnl(BigDecimal.ZERO)
.reservedCash(BigDecimal.ZERO)
@@ -0,0 +1,26 @@
package com.goldenchart.trading;
/**
* 로그인 사용자(userId) 전용 매매·모의·전략 접근 규칙.
* 게스트(앱 입장만 한 비로그인)는 deviceId만으로 매매 기능을 사용할 수 없다.
*/
public final class TradingAccess {
private TradingAccess() {}
/** 모의 계좌 device_id 컬럼(NOT NULL UK)용 합성 키 — 실제 기기와 무관 */
public static String accountDeviceKey(long userId) {
return "user:" + userId;
}
public static long requireUserId(Long userId) {
if (userId == null || userId <= 0) {
throw new TradingAccessDeniedException();
}
return userId;
}
public static boolean isRegisteredUser(Long userId) {
return userId != null && userId > 0;
}
}
@@ -0,0 +1,22 @@
package com.goldenchart.trading;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
/**
* 게스트(비로그인) 또는 미등록 사용자가 매매·모의·전략 API에 접근할 때.
*/
@ResponseStatus(HttpStatus.FORBIDDEN)
public class TradingAccessDeniedException extends RuntimeException {
public static final String DEFAULT_MESSAGE =
"매매·모의투자·투자전략·자동매매 기능은 정식 로그인 후 이용할 수 있습니다.";
public TradingAccessDeniedException() {
super(DEFAULT_MESSAGE);
}
public TradingAccessDeniedException(String message) {
super(message);
}
}
@@ -2,6 +2,7 @@ package com.goldenchart.trading;
import com.goldenchart.entity.GcAppSettings;
import com.goldenchart.service.AppSettingsService;
import com.goldenchart.trading.TradingAccess;
import com.goldenchart.service.LiveTradingService;
import com.goldenchart.service.PaperTradingService;
import com.goldenchart.trading.pipeline.OrderRequest;
@@ -21,32 +22,34 @@ public class TradingExecutionService {
private final PaperTradingService paperTradingService;
private final LiveTradingService liveTradingService;
public void executeSignal(String deviceId, Long userId, String market,
public void executeSignal(Long userId, String market,
Long strategyId, String side, double price) {
GcAppSettings app = appSettingsService.getEntity(userId, deviceId);
if (!TradingAccess.isRegisteredUser(userId)) return;
GcAppSettings app = appSettingsService.getEntity(userId, null);
TradingMode mode = TradingMode.fromString(app.getTradingMode());
if (mode.usePaper() && Boolean.TRUE.equals(app.getPaperTradingEnabled())) {
paperTradingService.tryExecuteOnSignal(deviceId, userId, market, strategyId, side, price);
paperTradingService.tryExecuteOnSignal(userId, market, strategyId, side, price);
}
if (mode.useLive()) {
liveTradingService.tryExecuteOnSignal(
deviceId, userId, market, strategyId, side, price, "STRATEGY");
userId, market, strategyId, side, price, "STRATEGY");
}
}
public void executeRiskExit(OrderRequest req) {
GcAppSettings app = appSettingsService.getEntity(req.userId(), req.deviceId());
if (!TradingAccess.isRegisteredUser(req.userId())) return;
GcAppSettings app = appSettingsService.getEntity(req.userId(), null);
TradingMode mode = TradingMode.fromString(app.getTradingMode());
String source = req.kind() == OrderRequest.OrderKind.STOP_LOSS ? "STOP_LOSS" : "TAKE_PROFIT";
if (mode.usePaper() && Boolean.TRUE.equals(app.getPaperTradingEnabled())) {
paperTradingService.tryExecuteOnSignal(
req.deviceId(), req.userId(), req.market(), null, "SELL", req.price());
req.userId(), req.market(), null, "SELL", req.price());
}
if (mode.useLive()) {
liveTradingService.tryExecuteOnSignal(
req.deviceId(), req.userId(), req.market(), null, "SELL", req.price(), source);
req.userId(), req.market(), null, "SELL", req.price(), source);
}
}
}
@@ -1,5 +1,6 @@
package com.goldenchart.trading.pipeline;
import com.goldenchart.trading.TradingAccess;
import com.goldenchart.trading.TradingExecutionService;
import com.google.common.util.concurrent.RateLimiter;
import jakarta.annotation.PostConstruct;
@@ -64,9 +65,11 @@ public class OrderExecutionQueue {
}
}
public void submitSignal(String deviceId, Long userId, String market,
public void submitSignal(Long userId, String market,
Long strategyId, String signal, double price) {
submit(OrderRequest.strategySignal(deviceId, userId, market, strategyId, signal, price));
if (!TradingAccess.isRegisteredUser(userId)) return;
submit(OrderRequest.strategySignal(
TradingAccess.accountDeviceKey(userId), userId, market, strategyId, signal, price));
}
private void runLoop() {
@@ -88,12 +91,12 @@ public class OrderExecutionQueue {
}
private void executeNow(OrderRequest req) {
if (req.deviceId() == null || req.deviceId().isBlank()) return;
if (!TradingAccess.isRegisteredUser(req.userId())) return;
if (!"BUY".equals(req.side()) && !"SELL".equals(req.side())) return;
try {
if (req.kind() == OrderRequest.OrderKind.STRATEGY_SIGNAL) {
tradingExecutionService.executeSignal(
req.deviceId(), req.userId(), req.market(),
req.userId(), req.market(),
req.strategyId(), req.side(), req.price());
} else {
tradingExecutionService.executeRiskExit(req);
@@ -8,6 +8,7 @@ import com.goldenchart.repository.GcPaperAccountRepository;
import com.goldenchart.repository.GcPaperPositionRepository;
import com.goldenchart.service.AppSettingsService;
import com.goldenchart.service.BacktestSettingsService;
import com.goldenchart.trading.TradingAccess;
import com.goldenchart.trading.TradingExecutionService;
import com.goldenchart.trading.TradingMode;
import lombok.RequiredArgsConstructor;
@@ -53,13 +54,12 @@ public class RealtimePositionMonitor {
GcPaperAccount account = accountRepo.findById(pos.getAccountId()).orElse(null);
if (account == null) continue;
String deviceId = account.getDeviceId();
Long userId = account.getUserId();
if (deviceId == null || deviceId.isBlank()) continue;
if (userId == null || userId <= 0) continue;
GcAppSettings app;
try {
app = appSettingsService.getEntity(userId, deviceId);
app = appSettingsService.getEntity(userId, null);
} catch (Exception e) {
continue;
}
@@ -75,7 +75,7 @@ public class RealtimePositionMonitor {
double avg = pos.getAvgPrice().doubleValue();
if (avg <= 0) continue;
BacktestSettingsDto risk = backtestSettingsService.get(deviceId);
BacktestSettingsDto risk = backtestSettingsService.get(TradingAccess.accountDeviceKey(userId));
double pnlPct = (tradePrice - avg) / avg * 100.0;
if (Boolean.TRUE.equals(risk.getStopLossEnabled())
@@ -84,7 +84,7 @@ public class RealtimePositionMonitor {
lastExitMs.put(cooldownKey, now);
double sl = risk.getStopLossPct().doubleValue();
tradingExecutionService.executeRiskExit(
OrderRequest.stopLoss(deviceId, userId, market, tradePrice, sl));
OrderRequest.stopLoss(TradingAccess.accountDeviceKey(userId), userId, market, tradePrice, sl));
log.info("[SL] {} @ {} pnl={}% (limit -{}%)", market, tradePrice,
String.format("%.2f", pnlPct), sl);
return;
@@ -96,7 +96,7 @@ public class RealtimePositionMonitor {
lastExitMs.put(cooldownKey, now);
double tp = risk.getTakeProfitPct().doubleValue();
tradingExecutionService.executeRiskExit(
OrderRequest.takeProfit(deviceId, userId, market, tradePrice, tp));
OrderRequest.takeProfit(TradingAccess.accountDeviceKey(userId), userId, market, tradePrice, tp));
log.info("[TP] {} @ {} pnl={}% (target +{}%)", market, tradePrice,
String.format("%.2f", pnlPct), tp);
return;
@@ -48,7 +48,8 @@ public class LiveStrategyScheduler {
*/
@Scheduled(fixedDelay = 3000)
public void tick() {
List<GcLiveStrategySettings> activeSettings = settingsRepo.findAllByIsLiveCheckTrue();
List<GcLiveStrategySettings> activeSettings =
settingsRepo.findAllByIsLiveCheckTrueAndUserIdIsNotNull();
if (activeSettings.isEmpty()) return;
for (GcLiveStrategySettings s : activeSettings) {
@@ -104,9 +105,9 @@ public class LiveStrategyScheduler {
lastBar.getClosePrice().doubleValue(),
barStartEpoch, candleType, "REALTIME_TICK"
);
if (s.getDeviceId() != null) {
if (s.getUserId() != null) {
orderExecutionQueue.submitSignal(
s.getDeviceId(), s.getUserId(), market,
s.getUserId(), market,
s.getStrategyId(), signal,
lastBar.getClosePrice().doubleValue());
}