모의투자 로직 변경
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
+13
-31
@@ -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);
|
||||
|
||||
+3
-2
@@ -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());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
-- V54: 매매 데이터 초기화 + 비회원(device-only) 실시간 전략 체크 OFF
|
||||
-- 로그인 userId 단일 계좌·자동매매 전환 (기존 device 계좌 데이터 폐기)
|
||||
|
||||
UPDATE gc_live_strategy_settings
|
||||
SET is_live_check = 0
|
||||
WHERE user_id IS NULL;
|
||||
|
||||
DELETE FROM gc_paper_order;
|
||||
DELETE FROM gc_paper_trade;
|
||||
DELETE FROM gc_paper_cash_ledger;
|
||||
DELETE FROM gc_paper_daily_snapshot;
|
||||
DELETE FROM gc_paper_reset_log;
|
||||
DELETE FROM gc_paper_symbol_allocation;
|
||||
DELETE FROM gc_paper_position;
|
||||
DELETE FROM gc_paper_account;
|
||||
|
||||
DELETE FROM gc_trade_signal;
|
||||
DELETE FROM gc_live_trade;
|
||||
Reference in New Issue
Block a user