ai 전략 기능 추가
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.dto.StrategyAgentChatRequest;
|
||||
import com.goldenchart.dto.StrategyAgentChatResponse;
|
||||
import com.goldenchart.service.StrategyAgentChatService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI 전략 에이전트 — 자연어 멀티턴 대화로 전략 DSL 생성/수정.
|
||||
*
|
||||
* POST /api/strategy/agent/chat
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/strategy/agent")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class StrategyAgentController {
|
||||
|
||||
private final StrategyAgentChatService agentService;
|
||||
|
||||
@PostMapping("/chat")
|
||||
public ResponseEntity<?> chat(
|
||||
@RequestBody StrategyAgentChatRequest body,
|
||||
@RequestHeader Map<String, String> headers,
|
||||
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
|
||||
TradingControllerSupport.requireRegisteredUser(headers);
|
||||
try {
|
||||
Long userId = TradingControllerSupport.parseUserId(headers);
|
||||
StrategyAgentChatResponse res = agentService.chat(body, userId, deviceId);
|
||||
return ResponseEntity.ok(res);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
|
||||
} catch (IllegalStateException e) {
|
||||
return ResponseEntity.status(503).body(Map.of("message", e.getMessage()));
|
||||
} catch (Exception e) {
|
||||
log.warn("[StrategyAgent] chat 오류: {}", e.getMessage());
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of("message", "AI 전략 생성 중 오류가 발생했습니다."));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.goldenchart.controller;
|
||||
|
||||
import com.goldenchart.dto.LlmModelsRequest;
|
||||
import com.goldenchart.dto.LlmModelsResponse;
|
||||
import com.goldenchart.dto.LlmTestResponse;
|
||||
import com.goldenchart.dto.StrategyEvaluationAiVerifyJobStartResponse;
|
||||
import com.goldenchart.dto.StrategyEvaluationAiVerifyJobStatusResponse;
|
||||
import com.goldenchart.dto.StrategyEvaluationAiVerifyRequest;
|
||||
import com.goldenchart.dto.StrategyEvaluationAiVerifyResponse;
|
||||
import com.goldenchart.service.StrategyAgentChatService;
|
||||
import com.goldenchart.service.StrategyEvaluationAiVerifyJobService;
|
||||
import com.goldenchart.service.StrategyEvaluationLlmService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -28,6 +31,7 @@ public class StrategyEvaluationController {
|
||||
|
||||
private final StrategyEvaluationLlmService llmService;
|
||||
private final StrategyEvaluationAiVerifyJobService aiVerifyJobService;
|
||||
private final StrategyAgentChatService agentChatService;
|
||||
|
||||
@PostMapping("/ai-verify/jobs")
|
||||
public ResponseEntity<?> startAiVerifyJob(
|
||||
@@ -89,6 +93,20 @@ public class StrategyEvaluationController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/llm-models")
|
||||
public ResponseEntity<?> llmModels(
|
||||
@RequestBody(required = false) LlmModelsRequest body,
|
||||
@RequestHeader Map<String, String> headers) {
|
||||
TradingControllerSupport.requireRegisteredUser(headers);
|
||||
if (body == null || body.getHost() == null || body.getHost().isBlank()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("ok", false, "message", "호스트가 필요합니다."));
|
||||
}
|
||||
LlmModelsResponse res = agentChatService.listModels(
|
||||
body.getHost(), body.getPort(), body.getUseHttps(),
|
||||
body.getModelsPath(), body.getTimeoutMs());
|
||||
return ResponseEntity.ok(res);
|
||||
}
|
||||
|
||||
@PostMapping("/llm-test")
|
||||
public ResponseEntity<?> llmTest(
|
||||
@RequestHeader Map<String, String> headers,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** LLM 모델 목록 조회 — 저장 전 입력값으로 /v1/models 프록시 */
|
||||
@Data
|
||||
public class LlmModelsRequest {
|
||||
private String host;
|
||||
private Integer port;
|
||||
private Boolean useHttps;
|
||||
/** OpenAI 호환 모델 목록 경로 (기본 /v1/models) */
|
||||
private String modelsPath;
|
||||
private Long timeoutMs;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** LLM 모델 목록 조회 결과 */
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LlmModelsResponse {
|
||||
private boolean ok;
|
||||
private List<String> models;
|
||||
private String message;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** AI 전략 에이전트 — 멀티턴 대화 요청 */
|
||||
@Data
|
||||
public class StrategyAgentChatRequest {
|
||||
|
||||
/** user/assistant 대화 히스토리 (system 제외) */
|
||||
private List<ChatMessage> messages;
|
||||
|
||||
/** 현재 편집기 상태 컨텍스트 (buy/sell DSL, candleType, 지표 설정 등) */
|
||||
private JsonNode context;
|
||||
|
||||
@Data
|
||||
public static class ChatMessage {
|
||||
private String role;
|
||||
private String content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.goldenchart.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/** AI 전략 에이전트 — 응답 */
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class StrategyAgentChatResponse {
|
||||
/** 사용자에게 보여줄 한국어 설명 (json 블록 제거됨) */
|
||||
private String reply;
|
||||
/** "ask"(추가 질문) | "build"(전략 생성/수정) */
|
||||
private String action;
|
||||
/** build일 때 매수 조건 LogicNode 트리 (없으면 null) */
|
||||
private JsonNode buyCondition;
|
||||
/** build일 때 매도 조건 LogicNode 트리 (없으면 null) */
|
||||
private JsonNode sellCondition;
|
||||
private String model;
|
||||
private long latencyMs;
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
package com.goldenchart.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.goldenchart.dto.LlmModelsResponse;
|
||||
import com.goldenchart.dto.StrategyAgentChatRequest;
|
||||
import com.goldenchart.dto.StrategyAgentChatResponse;
|
||||
import com.goldenchart.service.LlmSettingsResolver.ResolvedLlmSettings;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* AI 전략 에이전트 — 자연어 멀티턴 대화로 전략 DSL(LogicNode 트리)을 생성/수정한다.
|
||||
*
|
||||
* <p>설정에 저장된 LLM(OpenAI 호환)을 사용하며, 정보가 부족하면 되묻고(action=ask),
|
||||
* 충분하면 전략을 생성/수정한다(action=build).
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class StrategyAgentChatService {
|
||||
|
||||
/** ```json ... ``` 또는 ``` ... ``` 코드펜스 추출 */
|
||||
private static final Pattern JSON_FENCE =
|
||||
Pattern.compile("```(?:json)?\\s*([\\s\\S]*?)```", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final String SYSTEM_PROMPT = """
|
||||
당신은 GoldenChart 전략편집기의 AI 전략 에이전트입니다.
|
||||
사용자의 자연어 요청을 받아 매매 시그널 전략을 GoldenChart 전략 DSL(JSON)로 구성/수정합니다.
|
||||
|
||||
【행동 규칙 — 멀티턴 에이전트】
|
||||
- 요청이 모호하거나 핵심 정보가 빠졌으면, 전략을 만들지 말고 한국어로 짧고 구체적으로 되물으세요. (action=ask)
|
||||
핵심 정보 예: 매수/매도 구분, 지표 기간(예: 일목 9/26/52), 임계값(예: RSI 30), 캔들 종류(예: 5분봉),
|
||||
"N봉 이내" 같은 윈도우 조건 여부.
|
||||
- 정보가 충분하면 전략을 생성하거나, 이미 전략이 있으면 그것을 수정합니다. (action=build)
|
||||
- 현재 편집기 상태(【현재 편집기 상태】의 buyCondition/sellCondition)가 비어있지 않으면, 처음부터 새로
|
||||
만들지 말고 기존 DSL을 최소 변경으로 수정하세요.
|
||||
- 한 번에 한두 가지만 묻고, 합리적인 기본값이 있으면 가정하고 진행하되 가정한 내용을 설명에 명시하세요.
|
||||
|
||||
【출력 형식】
|
||||
- 항상 한국어 설명을 먼저 작성합니다.
|
||||
- action=build일 때만, 설명 뒤에 단 하나의 ```json 코드블록을 추가합니다. 형식:
|
||||
```json
|
||||
{ "action": "build", "buyCondition": <LogicNode 또는 null>, "sellCondition": <LogicNode 또는 null> }
|
||||
```
|
||||
- action=ask일 때는 json 코드블록을 절대 넣지 마세요(질문만).
|
||||
- 매수 전략만 요청되면 sellCondition은 null로 둡니다(반대도 동일).
|
||||
|
||||
【LogicNode 스키마】
|
||||
- 노드: { "id": string, "type": "AND"|"OR"|"NOT"|"CONDITION"|"TIMEFRAME", "children": [LogicNode], "condition": ConditionDSL }
|
||||
- AND/OR: children 배열을 결합. NOT: children[0] 부정. CONDITION: leaf 조건(condition 필수).
|
||||
- TIMEFRAME: { "type":"TIMEFRAME", "candleType":"5m", "children":[...] } — 하위 조건을 해당 분봉으로 평가.
|
||||
- id는 고유 문자열(예: "n1","n2"...).
|
||||
- ConditionDSL: {
|
||||
"indicatorType": string, "conditionType": string,
|
||||
"leftField": string, "rightField": string,
|
||||
"period": number?, "leftPeriod": number?, "rightPeriod": number?,
|
||||
"candleRangeMode": "CURRENT"|"EXISTS_IN"|"NOT_EXISTS_IN"?, "candleRange": number?,
|
||||
"valuePeriodOverride": boolean?, "thresholdOverride": boolean?, "targetValue": number?
|
||||
}
|
||||
|
||||
【conditionType】
|
||||
- 공통: GT, LT, GTE, LTE, EQ, NEQ, CROSS_UP, CROSS_DOWN, SLOPE_UP, SLOPE_DOWN, DIFF_GT, DIFF_LT, HOLD_N_DAYS, LOWEST_LTE, LOWEST_GTE, TREND_LINE_CROSS_UP, TREND_LINE_CROSS_DOWN
|
||||
- 일목 전용(indicatorType=ICHIMOKU): ABOVE_CLOUD, BELOW_CLOUD, IN_CLOUD, CLOUD_BREAK_UP, CLOUD_BREAK_DOWN, SPAN1_GT_SPAN2, SPAN1_LT_SPAN2, SPAN1_CROSS_UP_SPAN2, SPAN1_CROSS_DOWN_SPAN2, LAGGING_GT_PRICE, LAGGING_LT_PRICE, LAGGING_ABOVE_PRIOR_CLOUD, CHIKOU_CROSS_ABOVE_PRIOR_CLOUD
|
||||
- 거래량(indicatorType=VOLUME): VOLUME_GT_MA_RATIO
|
||||
- CROSS_UP/CROSS_DOWN은 "직전봉→현재봉" 교차 이벤트이며 단순 수준 비교(GT/LT)와 다릅니다.
|
||||
|
||||
【indicatorType 과 주요 필드(leftField/rightField)】
|
||||
- 가격/거래량: CLOSE_PRICE, OPEN_PRICE, HIGH_PRICE, LOW_PRICE, VOLUME_VALUE, VOLUME_MA, NONE
|
||||
- RSI: leftField RSI_VALUE / rightField HL_OVER|HL_MID|HL_UNDER 또는 K_{숫자}
|
||||
- MACD: MACD_LINE, SIGNAL_LINE, HISTOGRAM / rightField SIGNAL_LINE 또는 HL_MID(0선)
|
||||
- STOCHASTIC: STOCH_K, STOCH_D / rightField STOCH_D, HL_OVER|HL_UNDER, K_{숫자}
|
||||
- CCI: CCI_VALUE / OVERBOUGHT_100, OVERSOLD_NEG100, K_{숫자}
|
||||
- ADX: ADX_VALUE / ADX_25, K_{숫자}; DMI: PDI, MDI (서로 교차)
|
||||
- MA: MA1..MA11 (period1..11) — SMA 슬롯; EMA: EMA1..EMA11
|
||||
- BOLLINGER: UPPER_BAND, MIDDLE_BAND, LOWER_BAND (가격과 비교)
|
||||
- DONCHIAN: DC_UPPER_{9|10|20|55}, DC_LOWER_{...}, DC_MIDDLE_20
|
||||
- ICHIMOKU: CONVERSION_LINE(전환선), BASE_LINE(기준선), LEADING_SPAN1, LEADING_SPAN2, LAGGING_SPAN(후행스팬)
|
||||
- 임계값 직접지정: rightField "K_{숫자}", thresholdOverride=true, targetValue=숫자
|
||||
|
||||
【기간 지정】
|
||||
- 전략 전용 기간을 쓰려면 valuePeriodOverride=true 와 period 를 함께 지정(예: RSI 9 → period:9).
|
||||
- 일목 기본 9/26/52는 indicatorType=ICHIMOKU의 conditionType만으로 충분(별도 period 불필요).
|
||||
|
||||
【윈도우(N봉 이내)】
|
||||
- "최근 N봉 내 1회라도" → candleRangeMode:"EXISTS_IN", candleRange:N (N>=2)
|
||||
- "최근 N봉 내 한 번도 없음" → "NOT_EXISTS_IN"
|
||||
- 미지정 시 현재 봉만 평가(CURRENT).
|
||||
|
||||
【예시 1 — 정보 부족(ask)】
|
||||
사용자: "골든크로스로 매수해줘"
|
||||
응답: "어떤 이동평균선 조합으로 골든크로스를 잡을까요? 예를 들어 단기 5 / 장기 20처럼 기간을 알려주세요. 캔들은 어떤 분봉인가요?"
|
||||
(json 블록 없음)
|
||||
|
||||
【예시 2 — 일목 전환선이 기준선 상향돌파 매수(build)】
|
||||
응답: "일목균형표 전환선이 기준선을 상향돌파할 때 매수하도록 구성했습니다."
|
||||
```json
|
||||
{ "action": "build",
|
||||
"buyCondition": { "id":"n1","type":"CONDITION","condition":{"indicatorType":"ICHIMOKU","conditionType":"CROSS_UP","leftField":"CONVERSION_LINE","rightField":"BASE_LINE","candleRangeMode":"CURRENT"} },
|
||||
"sellCondition": null }
|
||||
```
|
||||
|
||||
【예시 3 — 후행스팬이 26봉 전 캔들 상향돌파 AND 전환선이 기준선 상향돌파(build)】
|
||||
응답: "후행스팬의 가격 상향돌파와 전환선의 기준선 상향돌파를 모두 만족할 때 매수합니다."
|
||||
```json
|
||||
{ "action": "build",
|
||||
"buyCondition": { "id":"r","type":"AND","children":[
|
||||
{ "id":"a","type":"CONDITION","condition":{"indicatorType":"ICHIMOKU","conditionType":"LAGGING_GT_PRICE","leftField":"LAGGING_SPAN","rightField":"NONE","candleRangeMode":"CURRENT"} },
|
||||
{ "id":"b","type":"CONDITION","condition":{"indicatorType":"ICHIMOKU","conditionType":"CROSS_UP","leftField":"CONVERSION_LINE","rightField":"BASE_LINE","candleRangeMode":"CURRENT"} }
|
||||
] },
|
||||
"sellCondition": null }
|
||||
```
|
||||
""";
|
||||
|
||||
private final WebClient.Builder webClientBuilder;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final LlmSettingsResolver llmSettingsResolver;
|
||||
|
||||
public StrategyAgentChatResponse chat(StrategyAgentChatRequest request, Long userId, String deviceId) throws Exception {
|
||||
ResolvedLlmSettings cfg = llmSettingsResolver.resolve(userId, deviceId);
|
||||
if (!cfg.enabled()) {
|
||||
throw new IllegalStateException("LLM 사용이 비활성화되어 있습니다. 설정 > LLM에서 사용을 켜 주세요.");
|
||||
}
|
||||
if (request == null || request.getMessages() == null || request.getMessages().isEmpty()) {
|
||||
throw new IllegalArgumentException("messages 가 필요합니다.");
|
||||
}
|
||||
|
||||
long started = System.currentTimeMillis();
|
||||
ObjectNode body = buildChatBody(cfg, request);
|
||||
String responseJson = postChat(cfg, body);
|
||||
JsonNode root = objectMapper.readTree(responseJson);
|
||||
String content = extractAssistantContent(root);
|
||||
if (content == null || content.isBlank()) {
|
||||
throw new IllegalStateException("LLM 응답에서 텍스트를 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
return parseAssistantContent(content, root.path("model").asText(cfg.model()),
|
||||
System.currentTimeMillis() - started);
|
||||
}
|
||||
|
||||
/** 저장 전 입력값(host/port/useHttps/modelsPath)으로 /v1/models 프록시 조회 */
|
||||
public LlmModelsResponse listModels(String host, Integer port, Boolean useHttps,
|
||||
String modelsPath, Long timeoutMs) {
|
||||
int p = port != null ? Math.max(1, Math.min(65535, port)) : 16000;
|
||||
boolean https = useHttps != null && useHttps;
|
||||
String baseUrl = LlmSettingsResolver.buildBaseUrl(host, p, https);
|
||||
String path = (modelsPath == null || modelsPath.isBlank()) ? "/v1/models"
|
||||
: (modelsPath.startsWith("/") ? modelsPath.trim() : "/" + modelsPath.trim());
|
||||
long timeout = timeoutMs != null ? Math.max(2_000L, Math.min(60_000L, timeoutMs)) : 15_000L;
|
||||
|
||||
try {
|
||||
WebClient client = webClientBuilder.baseUrl(baseUrl).build();
|
||||
String json = client.get()
|
||||
.uri(path)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
.block(Duration.ofMillis(timeout));
|
||||
if (json == null || json.isBlank()) {
|
||||
return LlmModelsResponse.builder().ok(false).models(List.of())
|
||||
.message("모델 목록 응답이 비어 있습니다.").build();
|
||||
}
|
||||
List<String> models = parseModelIds(objectMapper.readTree(json));
|
||||
if (models.isEmpty()) {
|
||||
return LlmModelsResponse.builder().ok(false).models(List.of())
|
||||
.message("모델 목록을 찾을 수 없습니다. (" + baseUrl + path + ")").build();
|
||||
}
|
||||
return LlmModelsResponse.builder().ok(true).models(models)
|
||||
.message(models.size() + "개 모델").build();
|
||||
} catch (WebClientResponseException e) {
|
||||
log.warn("[StrategyAgent] 모델 목록 HTTP {} — {}", e.getStatusCode(), e.getMessage());
|
||||
return LlmModelsResponse.builder().ok(false).models(List.of())
|
||||
.message("모델 서버 응답 오류: " + e.getStatusCode().value()).build();
|
||||
} catch (Exception e) {
|
||||
log.warn("[StrategyAgent] 모델 목록 조회 실패: {}", e.getMessage());
|
||||
return LlmModelsResponse.builder().ok(false).models(List.of())
|
||||
.message("모델 서버에 연결할 수 없습니다. (" + baseUrl + path + ")").build();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> parseModelIds(JsonNode root) {
|
||||
List<String> out = new ArrayList<>();
|
||||
JsonNode data = root.path("data");
|
||||
if (data.isArray()) {
|
||||
for (JsonNode m : data) {
|
||||
String id = m.path("id").asText(null);
|
||||
if (id != null && !id.isBlank()) out.add(id);
|
||||
}
|
||||
} else if (root.path("models").isArray()) {
|
||||
// 일부 서버는 {"models":[{"name":...}]} 형태(예: Ollama)
|
||||
for (JsonNode m : root.path("models")) {
|
||||
String id = m.path("id").asText(m.path("name").asText(null));
|
||||
if (id != null && !id.isBlank()) out.add(id);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private ObjectNode buildChatBody(ResolvedLlmSettings cfg, StrategyAgentChatRequest request) throws Exception {
|
||||
ObjectNode body = objectMapper.createObjectNode();
|
||||
body.put("model", cfg.model());
|
||||
// DSL 출력은 길 수 있어 최소 2048 토큰 확보
|
||||
body.put("max_tokens", Math.max(cfg.maxTokens(), 2048));
|
||||
body.put("temperature", cfg.temperature());
|
||||
|
||||
StringBuilder system = new StringBuilder(SYSTEM_PROMPT);
|
||||
if (request.getContext() != null && !request.getContext().isNull()) {
|
||||
system.append("\n\n【현재 편집기 상태】\n")
|
||||
.append(objectMapper.writerWithDefaultPrettyPrinter()
|
||||
.writeValueAsString(request.getContext()));
|
||||
}
|
||||
|
||||
ArrayNode messages = body.putArray("messages");
|
||||
messages.addObject().put("role", "system").put("content", system.toString());
|
||||
for (StrategyAgentChatRequest.ChatMessage m : request.getMessages()) {
|
||||
if (m == null || m.getContent() == null || m.getContent().isBlank()) continue;
|
||||
String role = "assistant".equals(m.getRole()) ? "assistant" : "user";
|
||||
messages.addObject().put("role", role).put("content", m.getContent());
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
private String postChat(ResolvedLlmSettings cfg, ObjectNode body) {
|
||||
WebClient client = webClientBuilder.baseUrl(cfg.baseUrl()).build();
|
||||
try {
|
||||
String responseJson = client.post()
|
||||
.uri(cfg.chatPath())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(body)
|
||||
.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
.block(Duration.ofMillis(cfg.timeoutMs()));
|
||||
if (responseJson == null || responseJson.isBlank()) {
|
||||
throw new IllegalStateException("LLM 응답이 비어 있습니다.");
|
||||
}
|
||||
return responseJson;
|
||||
} catch (WebClientResponseException e) {
|
||||
log.warn("[StrategyAgent] LLM HTTP {} — {}", e.getStatusCode(), e.getResponseBodyAsString());
|
||||
throw new IllegalStateException("LLM 서버 응답 오류: " + e.getStatusCode().value());
|
||||
} catch (IllegalStateException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.warn("[StrategyAgent] LLM 연결 실패: {}", e.getMessage());
|
||||
throw new IllegalStateException("LLM 서버에 연결할 수 없습니다. (" + cfg.baseUrl() + cfg.chatPath() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private static String extractAssistantContent(JsonNode root) {
|
||||
JsonNode choices = root.path("choices");
|
||||
if (!choices.isArray() || choices.isEmpty()) return null;
|
||||
JsonNode message = choices.get(0).path("message");
|
||||
if (message.hasNonNull("content")) {
|
||||
return message.path("content").asText(null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 어시스턴트 content에서 설명과 ```json DSL 블록을 분리·파싱 */
|
||||
StrategyAgentChatResponse parseAssistantContent(String content, String model, long latencyMs) {
|
||||
String reply = content.trim();
|
||||
JsonNode buy = null;
|
||||
JsonNode sell = null;
|
||||
String action = "ask";
|
||||
|
||||
Matcher matcher = JSON_FENCE.matcher(content);
|
||||
String jsonBlock = null;
|
||||
while (matcher.find()) {
|
||||
String candidate = matcher.group(1).trim();
|
||||
if (candidate.startsWith("{") || candidate.startsWith("[")) {
|
||||
jsonBlock = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonBlock != null) {
|
||||
try {
|
||||
JsonNode parsed = objectMapper.readTree(jsonBlock);
|
||||
JsonNode buyNode = firstNonMissing(parsed, "buyCondition", "buy");
|
||||
JsonNode sellNode = firstNonMissing(parsed, "sellCondition", "sell");
|
||||
buy = nullIfEmpty(buyNode);
|
||||
sell = nullIfEmpty(sellNode);
|
||||
String parsedAction = parsed.path("action").asText(null);
|
||||
if (buy != null || sell != null) {
|
||||
action = "build";
|
||||
} else if (parsedAction != null) {
|
||||
action = parsedAction;
|
||||
}
|
||||
// 설명에서 json 코드펜스 제거
|
||||
reply = JSON_FENCE.matcher(content).replaceAll("").trim();
|
||||
if (reply.isEmpty()) {
|
||||
reply = action.equals("build") ? "요청하신 조건으로 전략을 구성했습니다." : content.trim();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[StrategyAgent] DSL JSON 파싱 실패: {}", e.getMessage());
|
||||
action = "ask";
|
||||
}
|
||||
}
|
||||
|
||||
return StrategyAgentChatResponse.builder()
|
||||
.reply(reply)
|
||||
.action(action)
|
||||
.buyCondition(buy)
|
||||
.sellCondition(sell)
|
||||
.model(model)
|
||||
.latencyMs(latencyMs)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static JsonNode firstNonMissing(JsonNode parent, String... keys) {
|
||||
for (String key : keys) {
|
||||
JsonNode node = parent.get(key);
|
||||
if (node != null && !node.isMissingNode()) return node;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static JsonNode nullIfEmpty(JsonNode node) {
|
||||
if (node == null || node.isNull() || node.isMissingNode()) return null;
|
||||
if (node.isObject() && node.isEmpty()) return null;
|
||||
return node;
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,34 @@
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
location = /api/strategy/evaluation/llm-models {
|
||||
proxy_pass http://backend:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_read_timeout 90s;
|
||||
proxy_send_timeout 90s;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
# ── AI 전략 에이전트 (멀티턴 LLM — 일반 /api/ 180s 회피) ───
|
||||
location /api/strategy/agent/ {
|
||||
proxy_pass http://backend:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
proxy_buffering off;
|
||||
client_max_body_size 16m;
|
||||
}
|
||||
|
||||
# ── GoldenChart Spring Boot Backend 프록시 ────────────────────────
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8080;
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
} from '../utils/strategyStartNodes';
|
||||
import IndicatorPaletteTab from './strategyEditor/IndicatorPaletteTab';
|
||||
import TemplatePaletteTab, { type TemplatePaletteRow } from './strategyEditor/TemplatePaletteTab';
|
||||
import AiStrategyPanel, { type AiStrategyContext } from './strategyEditor/AiStrategyPanel';
|
||||
import SidewaysFilterPaletteTab from './strategyEditor/SidewaysFilterPaletteTab';
|
||||
import TrendLinePaletteTab from './strategyEditor/TrendLinePaletteTab';
|
||||
import { buildTrendLineConditionNode } from '../utils/trendLinePalette';
|
||||
@@ -320,7 +321,7 @@ export default function StrategyEditorPage({
|
||||
const [buyCondition, setBuyCondition] = useState<LogicNode | null>(null);
|
||||
const [sellCondition, setSellCondition] = useState<LogicNode | null>(null);
|
||||
const [signalTab, setSignalTab] = useState<SignalTab>('buy');
|
||||
const [rightTab, setRightTab] = useState<'indicators' | 'templates'>('indicators');
|
||||
const [rightTab, setRightTab] = useState<'indicators' | 'templates' | 'ai'>('indicators');
|
||||
const [indicatorSubTab, setIndicatorSubTab] = useState<'auxiliary' | 'composite' | 'range' | 'trendline'>('auxiliary');
|
||||
const [auxiliaryPalette, setAuxiliaryPalette] = useState<PaletteItem[]>(() => loadPaletteItems('auxiliary'));
|
||||
const [compositePalette, setCompositePalette] = useState<PaletteItem[]>(() => loadPaletteItems('composite'));
|
||||
@@ -1463,6 +1464,45 @@ export default function StrategyEditorPage({
|
||||
[templateRows, templateSearch],
|
||||
);
|
||||
|
||||
const buildAiContext = useCallback((): AiStrategyContext => {
|
||||
const buyEncoded = encodeConditionForSave(buyEditorState);
|
||||
const sellEncoded = encodeConditionForSave(sellEditorState);
|
||||
return {
|
||||
buyCondition: buyEncoded,
|
||||
sellCondition: sellEncoded,
|
||||
signalTab: buySellTab(signalTab),
|
||||
candleType: currentEditorState.startMeta[START_NODE_ID]?.candleType,
|
||||
};
|
||||
}, [buyEditorState, sellEditorState, signalTab, currentEditorState]);
|
||||
|
||||
const applyAiStrategy = useCallback((buyDsl: LogicNode | null, sellDsl: LogicNode | null) => {
|
||||
if (editorMode === 'graph') layoutFlushRef.current?.();
|
||||
|
||||
const buyDecoded = decodeConditionForEditor(buyDsl);
|
||||
const sellDecoded = decodeConditionForEditor(sellDsl);
|
||||
|
||||
setBuyCondition(buyDecoded.root);
|
||||
setSellCondition(sellDecoded.root);
|
||||
setBuyOrphans([]);
|
||||
setSellOrphans([]);
|
||||
setSelectedNodeId(null);
|
||||
setBuyLayout(prev => ({
|
||||
...prev,
|
||||
startCombineOp: normalizeStartCombineOp(buyDecoded.startCombineOp),
|
||||
}));
|
||||
setSellLayout(prev => ({
|
||||
...prev,
|
||||
startCombineOp: normalizeStartCombineOp(sellDecoded.startCombineOp),
|
||||
}));
|
||||
|
||||
resetFlowLayout(layoutStrategyKey, 'buy');
|
||||
resetFlowLayout(layoutStrategyKey, 'sell');
|
||||
|
||||
const targetTab: BuySellTab = buyDsl ? 'buy' : (sellDsl ? 'sell' : buySellTab(signalTab));
|
||||
if (targetTab !== signalTab) setSignalTab(targetTab);
|
||||
scheduleStrategyPersist();
|
||||
}, [editorMode, layoutStrategyKey, resetFlowLayout, signalTab, scheduleStrategyPersist]);
|
||||
|
||||
const handleTemplate = useCallback((tmpl: StrategyTemplateDef) => {
|
||||
if (editorMode === 'graph') layoutFlushRef.current?.();
|
||||
|
||||
@@ -2418,6 +2458,7 @@ export default function StrategyEditorPage({
|
||||
<div className="se-right-tabs">
|
||||
<button type="button" className={rightTab === 'indicators' ? 'se-right-tab se-right-tab--on' : 'se-right-tab'} onClick={() => setRightTab('indicators')}>지표</button>
|
||||
<button type="button" className={rightTab === 'templates' ? 'se-right-tab se-right-tab--on' : 'se-right-tab'} onClick={() => setRightTab('templates')}>템플릿</button>
|
||||
<button type="button" className={rightTab === 'ai' ? 'se-right-tab se-right-tab--on' : 'se-right-tab'} onClick={() => setRightTab('ai')}>AI 전략</button>
|
||||
</div>
|
||||
<div className="se-right-body">
|
||||
{rightTab === 'indicators' && (
|
||||
@@ -2593,6 +2634,12 @@ export default function StrategyEditorPage({
|
||||
onDelete={handleTemplateToolbarDelete}
|
||||
/>
|
||||
)}
|
||||
{rightTab === 'ai' && (
|
||||
<AiStrategyPanel
|
||||
getContext={buildAiContext}
|
||||
onApplyStrategy={applyAiStrategy}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import type { LlmAppSettings } from '../../utils/llmSettings';
|
||||
import { buildLlmEndpointUrl, buildLlmBaseUrl } from '../../utils/llmSettings';
|
||||
import { fetchLlmConnectionTest } from '../../utils/backendApi';
|
||||
import { fetchLlmConnectionTest, fetchLlmModels } from '../../utils/backendApi';
|
||||
|
||||
interface Props {
|
||||
settings: LlmAppSettings;
|
||||
@@ -34,6 +34,9 @@ const SettingSection: React.FC<{ title: string; children: React.ReactNode }> = (
|
||||
const LlmSettingsPanel: React.FC<Props> = ({ settings, onChange }) => {
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState<{ ok: boolean; message: string } | null>(null);
|
||||
const [loadingModels, setLoadingModels] = useState(false);
|
||||
const [models, setModels] = useState<string[]>([]);
|
||||
const [modelsMessage, setModelsMessage] = useState<{ ok: boolean; message: string } | null>(null);
|
||||
|
||||
const patch = (p: Partial<LlmAppSettings>) => {
|
||||
setTestResult(null);
|
||||
@@ -57,6 +60,36 @@ const LlmSettingsPanel: React.FC<Props> = ({ settings, onChange }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleFetchModels = async () => {
|
||||
setLoadingModels(true);
|
||||
setModelsMessage(null);
|
||||
try {
|
||||
const res = await fetchLlmModels({
|
||||
host: settings.host,
|
||||
port: settings.port,
|
||||
useHttps: settings.useHttps,
|
||||
modelsPath: settings.modelsPath,
|
||||
});
|
||||
setModels(res.models ?? []);
|
||||
setModelsMessage({ ok: res.ok, message: res.message ?? (res.ok ? '조회 완료' : '조회 실패') });
|
||||
if (res.ok && res.models?.length && !res.models.includes(settings.model)) {
|
||||
patch({ model: res.models[0] });
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : '모델 조회 실패';
|
||||
setModels([]);
|
||||
setModelsMessage({ ok: false, message: msg });
|
||||
} finally {
|
||||
setLoadingModels(false);
|
||||
}
|
||||
};
|
||||
|
||||
const modelOptions = useMemo(() => {
|
||||
const set = new Set(models);
|
||||
if (settings.model) set.add(settings.model);
|
||||
return Array.from(set);
|
||||
}, [models, settings.model]);
|
||||
|
||||
return (
|
||||
<div className="llm-settings-panel">
|
||||
<SettingSection title="LLM 사용">
|
||||
@@ -114,6 +147,15 @@ const LlmSettingsPanel: React.FC<Props> = ({ settings, onChange }) => {
|
||||
placeholder="/v1/chat/completions"
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow label="모델 목록 경로" desc="OpenAI 호환 모델 목록 엔드포인트 (드롭다운 조회용)">
|
||||
<input
|
||||
className="stg-input stg-input--wide"
|
||||
type="text"
|
||||
value={settings.modelsPath}
|
||||
onChange={e => patch({ modelsPath: e.target.value })}
|
||||
placeholder="/v1/models"
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow label="연결 URL 미리보기" desc="저장 후 백엔드가 이 주소로 LLM에 요청합니다.">
|
||||
<code className="stg-code-preview">{endpointPreview}</code>
|
||||
</SettingRow>
|
||||
@@ -137,7 +179,36 @@ const LlmSettingsPanel: React.FC<Props> = ({ settings, onChange }) => {
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection title="모델·생성 옵션">
|
||||
<SettingRow label="모델" desc="LLM 서버에 등록된 모델 ID">
|
||||
<SettingRow label="모델" desc="URL 입력 후 '모델 조회'로 목록을 받아 선택하세요. 조회가 안 되면 직접 입력할 수 있습니다.">
|
||||
<div className="stg-row-inline">
|
||||
<select
|
||||
className="stg-input stg-input--wide"
|
||||
value={settings.model}
|
||||
onChange={e => patch({ model: e.target.value })}
|
||||
>
|
||||
{modelOptions.length === 0 && <option value="">모델 없음 — 조회하세요</option>}
|
||||
{modelOptions.map(m => (
|
||||
<option key={m} value={m}>{m}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
className="stg-btn-test"
|
||||
disabled={loadingModels || !settings.host}
|
||||
onClick={handleFetchModels}
|
||||
>
|
||||
{loadingModels ? '조회 중…' : '모델 조회'}
|
||||
</button>
|
||||
</div>
|
||||
</SettingRow>
|
||||
{modelsMessage && (
|
||||
<SettingRow label="" desc="">
|
||||
<span className={`stg-badge ${modelsMessage.ok ? 'stg-badge--ok' : 'stg-badge--err'}`}>
|
||||
● {modelsMessage.message}
|
||||
</span>
|
||||
</SettingRow>
|
||||
)}
|
||||
<SettingRow label="모델 직접 입력" desc="목록에 없는 모델 ID를 직접 입력할 때 사용">
|
||||
<input
|
||||
className="stg-input stg-input--wide"
|
||||
type="text"
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import type { LogicNode } from '../../utils/strategyTypes';
|
||||
import {
|
||||
requestStrategyAgentChat,
|
||||
type StrategyAgentChatMessage,
|
||||
} from '../../utils/strategyAgentChat';
|
||||
|
||||
export interface AiStrategyContext {
|
||||
buyCondition: LogicNode | null;
|
||||
sellCondition: LogicNode | null;
|
||||
signalTab: 'buy' | 'sell';
|
||||
candleType?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
/** 전송·스냅샷 시점의 현재 편집기 상태를 반환 */
|
||||
getContext: () => AiStrategyContext;
|
||||
/** 매수·매도 조건을 편집기에 교체 적용 (null = 해당 측 비움) */
|
||||
onApplyStrategy: (buy: LogicNode | null, sell: LogicNode | null) => void;
|
||||
}
|
||||
|
||||
type Turn = {
|
||||
id: number;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
action?: 'ask' | 'build';
|
||||
/** build 턴 — 적용 직전 스냅샷(되돌리기용) */
|
||||
snapshot?: { buy: LogicNode | null; sell: LogicNode | null };
|
||||
/** build 턴 — 적용된 결과 */
|
||||
produced?: { buy: LogicNode | null; sell: LogicNode | null };
|
||||
reverted?: boolean;
|
||||
};
|
||||
|
||||
const SUGGESTIONS = [
|
||||
'전환선이 기준선을 상향돌파하면 매수',
|
||||
'26봉 전 후행스팬이 종가를 상향돌파하고 전환선이 기준선을 상향돌파하면 매수',
|
||||
'RSI 30 이하에서 상향돌파하면 매수, 70 이상이면 매도',
|
||||
'지금 전략을 더 엄격하게 만들어줘',
|
||||
];
|
||||
|
||||
const AiStrategyPanel: React.FC<Props> = ({ getContext, onApplyStrategy }) => {
|
||||
const [turns, setTurns] = useState<Turn[]>([]);
|
||||
const [input, setInput] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const idRef = useRef(0);
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const nextId = () => ++idRef.current;
|
||||
|
||||
const scrollToBottom = useCallback(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const el = scrollRef.current;
|
||||
if (el) el.scrollTop = el.scrollHeight;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const history = useMemo<StrategyAgentChatMessage[]>(
|
||||
() => turns.map(t => ({ role: t.role, content: t.content })),
|
||||
[turns],
|
||||
);
|
||||
|
||||
const send = useCallback(async (text: string) => {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed || busy) return;
|
||||
|
||||
setError(null);
|
||||
const userTurn: Turn = { id: nextId(), role: 'user', content: trimmed };
|
||||
const outgoing: StrategyAgentChatMessage[] = [...history, { role: 'user', content: trimmed }];
|
||||
setTurns(prev => [...prev, userTurn]);
|
||||
setInput('');
|
||||
setBusy(true);
|
||||
scrollToBottom();
|
||||
|
||||
const ctx = getContext();
|
||||
try {
|
||||
const res = await requestStrategyAgentChat(outgoing, ctx);
|
||||
|
||||
if (res.action === 'build' && res.hasStrategy) {
|
||||
const snapshot = { buy: ctx.buyCondition, sell: ctx.sellCondition };
|
||||
// AI가 생략한 측은 기존 조건 유지
|
||||
const produced = {
|
||||
buy: res.buyCondition ?? ctx.buyCondition,
|
||||
sell: res.sellCondition ?? ctx.sellCondition,
|
||||
};
|
||||
onApplyStrategy(produced.buy, produced.sell);
|
||||
setTurns(prev => [...prev, {
|
||||
id: nextId(),
|
||||
role: 'assistant',
|
||||
content: res.reply || '요청하신 조건으로 전략을 구성했습니다.',
|
||||
action: 'build',
|
||||
snapshot,
|
||||
produced,
|
||||
}]);
|
||||
} else {
|
||||
setTurns(prev => [...prev, {
|
||||
id: nextId(),
|
||||
role: 'assistant',
|
||||
content: res.reply || '요청을 더 구체적으로 알려주세요.',
|
||||
action: 'ask',
|
||||
}]);
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'AI 전략 생성 중 오류가 발생했습니다.';
|
||||
setError(msg);
|
||||
setTurns(prev => [...prev, {
|
||||
id: nextId(),
|
||||
role: 'assistant',
|
||||
content: `오류: ${msg}`,
|
||||
action: 'ask',
|
||||
}]);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
scrollToBottom();
|
||||
}
|
||||
}, [busy, history, getContext, onApplyStrategy, scrollToBottom]);
|
||||
|
||||
const handleRevert = useCallback((turnId: number) => {
|
||||
setTurns(prev => prev.map(t => {
|
||||
if (t.id !== turnId || !t.snapshot) return t;
|
||||
onApplyStrategy(t.snapshot.buy, t.snapshot.sell);
|
||||
return { ...t, reverted: true };
|
||||
}));
|
||||
}, [onApplyStrategy]);
|
||||
|
||||
const handleReapply = useCallback((turnId: number) => {
|
||||
setTurns(prev => prev.map(t => {
|
||||
if (t.id !== turnId || !t.produced) return t;
|
||||
onApplyStrategy(t.produced.buy, t.produced.sell);
|
||||
return { ...t, reverted: false };
|
||||
}));
|
||||
}, [onApplyStrategy]);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
if (busy) return;
|
||||
setTurns([]);
|
||||
setError(null);
|
||||
}, [busy]);
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
void send(input);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="se-ai-panel">
|
||||
<div className="se-ai-head">
|
||||
<span className="se-ai-title">AI 전략 에이전트</span>
|
||||
<button
|
||||
type="button"
|
||||
className="se-ai-reset"
|
||||
onClick={handleReset}
|
||||
disabled={busy || turns.length === 0}
|
||||
>
|
||||
대화 초기화
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="se-ai-messages" ref={scrollRef}>
|
||||
{turns.length === 0 && (
|
||||
<div className="se-ai-empty">
|
||||
<p>자연어로 매매 전략을 설명하면 조건을 자동 구성해 편집기에 적용합니다.</p>
|
||||
<p className="se-ai-empty-sub">정보가 부족하면 먼저 질문하고, 이어지는 대화로 전략을 다듬습니다.</p>
|
||||
<div className="se-ai-suggestions">
|
||||
{SUGGESTIONS.map(s => (
|
||||
<button key={s} type="button" className="se-ai-suggestion" onClick={() => void send(s)} disabled={busy}>
|
||||
{s}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{turns.map(t => (
|
||||
<div key={t.id} className={`se-ai-msg se-ai-msg--${t.role}`}>
|
||||
<div className="se-ai-bubble">
|
||||
{t.content.split('\n').map((line, i) => <p key={i}>{line || '\u00A0'}</p>)}
|
||||
</div>
|
||||
{t.action === 'build' && t.produced && (
|
||||
<div className="se-ai-apply-row">
|
||||
{t.reverted ? (
|
||||
<>
|
||||
<span className="se-ai-tag se-ai-tag--reverted">되돌림</span>
|
||||
<button type="button" className="se-ai-mini-btn" onClick={() => handleReapply(t.id)} disabled={busy}>
|
||||
다시 적용
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="se-ai-tag se-ai-tag--applied">편집기에 적용됨</span>
|
||||
{t.snapshot && (
|
||||
<button type="button" className="se-ai-mini-btn" onClick={() => handleRevert(t.id)} disabled={busy}>
|
||||
되돌리기
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{busy && (
|
||||
<div className="se-ai-msg se-ai-msg--assistant">
|
||||
<div className="se-ai-bubble se-ai-bubble--loading">생성 중…</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && <div className="se-ai-error">{error}</div>}
|
||||
|
||||
<div className="se-ai-input-row">
|
||||
<textarea
|
||||
className="se-ai-input"
|
||||
placeholder="예) 전환선이 기준선을 상향돌파하면 매수 (Enter 전송 · Shift+Enter 줄바꿈)"
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
rows={2}
|
||||
disabled={busy}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="se-ai-send"
|
||||
onClick={() => void send(input)}
|
||||
disabled={busy || !input.trim()}
|
||||
>
|
||||
전송
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AiStrategyPanel;
|
||||
@@ -1914,6 +1914,131 @@
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ── AI 전략 에이전트 탭 ───────────────────────────────────── */
|
||||
.se-ai-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.se-ai-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid var(--se-border);
|
||||
}
|
||||
.se-ai-title { font-size: 0.78rem; font-weight: 600; color: var(--se-text); }
|
||||
.se-ai-reset {
|
||||
font-size: 0.7rem;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--se-input-border);
|
||||
background: var(--se-input-bg);
|
||||
color: var(--se-text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
.se-ai-reset:disabled { opacity: 0.5; cursor: default; }
|
||||
.se-ai-messages {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.se-ai-empty { color: var(--se-text-muted); font-size: 0.76rem; }
|
||||
.se-ai-empty p { margin: 0 0 6px; line-height: 1.5; }
|
||||
.se-ai-empty-sub { opacity: 0.8; }
|
||||
.se-ai-suggestions { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
|
||||
.se-ai-suggestion {
|
||||
text-align: left;
|
||||
padding: 7px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--se-input-border);
|
||||
background: var(--se-input-bg);
|
||||
color: var(--se-text);
|
||||
font-size: 0.73rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.se-ai-suggestion:hover { border-color: var(--se-tab-active); }
|
||||
.se-ai-suggestion:disabled { opacity: 0.5; cursor: default; }
|
||||
.se-ai-msg { display: flex; flex-direction: column; max-width: 92%; }
|
||||
.se-ai-msg--user { align-self: flex-end; align-items: flex-end; }
|
||||
.se-ai-msg--assistant { align-self: flex-start; align-items: flex-start; }
|
||||
.se-ai-bubble {
|
||||
padding: 8px 11px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.se-ai-bubble p { margin: 0; }
|
||||
.se-ai-msg--user .se-ai-bubble {
|
||||
background: var(--se-tab-active);
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
.se-ai-msg--assistant .se-ai-bubble {
|
||||
background: var(--se-input-bg);
|
||||
border: 1px solid var(--se-input-border);
|
||||
color: var(--se-text);
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
.se-ai-bubble--loading { opacity: 0.7; font-style: italic; }
|
||||
.se-ai-apply-row { display: flex; align-items: center; gap: 8px; margin-top: 4px; }
|
||||
.se-ai-tag { font-size: 0.68rem; padding: 2px 7px; border-radius: 999px; }
|
||||
.se-ai-tag--applied { background: color-mix(in srgb, #16a34a 18%, transparent); color: #16a34a; }
|
||||
.se-ai-tag--reverted { background: color-mix(in srgb, #d97706 18%, transparent); color: #d97706; }
|
||||
.se-ai-mini-btn {
|
||||
font-size: 0.68rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--se-input-border);
|
||||
background: transparent;
|
||||
color: var(--se-text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
.se-ai-mini-btn:disabled { opacity: 0.5; cursor: default; }
|
||||
.se-ai-error {
|
||||
margin: 0 10px 6px;
|
||||
padding: 6px 9px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.72rem;
|
||||
background: color-mix(in srgb, #dc2626 14%, transparent);
|
||||
color: #dc2626;
|
||||
}
|
||||
.se-ai-input-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
border-top: 1px solid var(--se-border);
|
||||
}
|
||||
.se-ai-input {
|
||||
flex: 1;
|
||||
resize: none;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--se-input-border);
|
||||
background: var(--se-input-bg);
|
||||
color: var(--se-text);
|
||||
font-size: 0.76rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
.se-ai-send {
|
||||
align-self: stretch;
|
||||
padding: 0 14px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: var(--se-tab-active);
|
||||
color: #fff;
|
||||
font-size: 0.76rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.se-ai-send:disabled { opacity: 0.5; cursor: default; }
|
||||
|
||||
.se-palette-search {
|
||||
margin: 10px 10px 6px;
|
||||
padding: 8px 10px;
|
||||
|
||||
@@ -1835,6 +1835,63 @@ export async function fetchLlmConnectionTest(): Promise<LlmConnectionTestRespons
|
||||
);
|
||||
}
|
||||
|
||||
export interface LlmModelsQuery {
|
||||
host: string;
|
||||
port: number;
|
||||
useHttps: boolean;
|
||||
modelsPath?: string;
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface LlmModelsResult {
|
||||
ok: boolean;
|
||||
models: string[];
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/** 설정 화면 — 입력한(미저장) 서버에서 사용 가능한 모델 목록 조회 */
|
||||
export async function fetchLlmModels(query: LlmModelsQuery): Promise<LlmModelsResult> {
|
||||
return requestOrThrowWithTimeout<LlmModelsResult>(
|
||||
'/strategy/evaluation/llm-models',
|
||||
{
|
||||
method: 'POST',
|
||||
cache: 'no-store',
|
||||
body: JSON.stringify(query),
|
||||
},
|
||||
60_000,
|
||||
);
|
||||
}
|
||||
|
||||
export interface StrategyAgentChatMessage {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface StrategyAgentChatApiResponse {
|
||||
reply: string;
|
||||
action: 'ask' | 'build';
|
||||
buyCondition?: unknown | null;
|
||||
sellCondition?: unknown | null;
|
||||
model?: string;
|
||||
latencyMs?: number;
|
||||
}
|
||||
|
||||
/** AI 전략 에이전트 — 멀티턴 대화로 전략 DSL 생성/수정 */
|
||||
export async function postStrategyAgentChat(
|
||||
messages: StrategyAgentChatMessage[],
|
||||
context: unknown,
|
||||
): Promise<StrategyAgentChatApiResponse> {
|
||||
return requestOrThrowWithTimeout<StrategyAgentChatApiResponse>(
|
||||
'/strategy/agent/chat',
|
||||
{
|
||||
method: 'POST',
|
||||
cache: 'no-store',
|
||||
body: JSON.stringify({ messages, context }),
|
||||
},
|
||||
620_000,
|
||||
);
|
||||
}
|
||||
|
||||
/** 전략 DSL에 포함된 평가 시간봉 목록 (실시간 체크·STOMP 구독용) */
|
||||
export async function loadStrategyTimeframes(strategyId: number): Promise<string[]> {
|
||||
if (!hasRegisteredUser()) return ['1m'];
|
||||
|
||||
@@ -13,6 +13,8 @@ export interface LlmAppSettings {
|
||||
useHttps: boolean;
|
||||
/** OpenAI 호환 chat completions 경로 */
|
||||
chatPath: string;
|
||||
/** OpenAI 호환 모델 목록 경로 (드롭다운 조회용) */
|
||||
modelsPath: string;
|
||||
/** 모델 ID */
|
||||
model: string;
|
||||
/** 최대 생성 토큰 */
|
||||
@@ -29,6 +31,7 @@ export const DEFAULT_LLM_APP_SETTINGS: LlmAppSettings = {
|
||||
port: 16000,
|
||||
useHttps: false,
|
||||
chatPath: '/v1/chat/completions',
|
||||
modelsPath: '/v1/models',
|
||||
model: 'mlx-community/Qwen2.5-7B-Instruct-4bit',
|
||||
maxTokens: 1024,
|
||||
temperature: 0.1,
|
||||
@@ -56,6 +59,9 @@ export function resolveLlmAppSettings(
|
||||
const chatPath = typeof raw.chatPath === 'string' && raw.chatPath.trim()
|
||||
? (raw.chatPath.startsWith('/') ? raw.chatPath.trim() : `/${raw.chatPath.trim()}`)
|
||||
: d.chatPath;
|
||||
const modelsPath = typeof raw.modelsPath === 'string' && raw.modelsPath.trim()
|
||||
? (raw.modelsPath.startsWith('/') ? raw.modelsPath.trim() : `/${raw.modelsPath.trim()}`)
|
||||
: d.modelsPath;
|
||||
const model = typeof raw.model === 'string' && raw.model.trim() ? raw.model.trim() : d.model;
|
||||
return {
|
||||
enabled: raw.enabled === undefined ? d.enabled : Boolean(raw.enabled),
|
||||
@@ -63,6 +69,7 @@ export function resolveLlmAppSettings(
|
||||
port: clampInt(raw.port, 1, 65535, d.port),
|
||||
useHttps: raw.useHttps === undefined ? d.useHttps : Boolean(raw.useHttps),
|
||||
chatPath,
|
||||
modelsPath,
|
||||
model,
|
||||
maxTokens: clampInt(raw.maxTokens, 64, 8192, d.maxTokens),
|
||||
temperature: clampFloat(raw.temperature, 0, 2, d.temperature),
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* AI 전략 에이전트 — 멀티턴 대화 API 클라이언트 + 응답 DSL 검증·정규화.
|
||||
*/
|
||||
import {
|
||||
postStrategyAgentChat,
|
||||
type StrategyAgentChatMessage,
|
||||
type StrategyAgentChatApiResponse,
|
||||
} from './backendApi';
|
||||
import { INDICATOR_CONDITIONS, generateNodeId, type ConditionDSL, type LogicNode, type LogicNodeType } from './strategyTypes';
|
||||
|
||||
export type { StrategyAgentChatMessage } from './backendApi';
|
||||
|
||||
export interface StrategyAgentResult {
|
||||
reply: string;
|
||||
action: 'ask' | 'build';
|
||||
/** 검증·정규화된 매수 조건 (build이고 유효할 때만) */
|
||||
buyCondition: LogicNode | null;
|
||||
/** 검증·정규화된 매도 조건 */
|
||||
sellCondition: LogicNode | null;
|
||||
/** action=build인데 DSL이 모두 비어 검증 후 사라진 경우 */
|
||||
hasStrategy: boolean;
|
||||
model?: string;
|
||||
latencyMs?: number;
|
||||
}
|
||||
|
||||
const NODE_TYPES: ReadonlySet<LogicNodeType> = new Set<LogicNodeType>([
|
||||
'AND', 'OR', 'NOT', 'CONDITION', 'TIMEFRAME',
|
||||
]);
|
||||
|
||||
const WINDOW_MODES = new Set(['EXISTS_IN', 'NOT_EXISTS_IN']);
|
||||
|
||||
function asRecord(v: unknown): Record<string, unknown> | null {
|
||||
return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : null;
|
||||
}
|
||||
|
||||
/** LLM이 생성한 LogicNode 트리를 편집기/백엔드가 처리 가능한 형태로 검증·정규화. */
|
||||
export function sanitizeAiLogicNode(raw: unknown): LogicNode | null {
|
||||
const obj = asRecord(raw);
|
||||
if (!obj) return null;
|
||||
|
||||
const type = obj.type as LogicNodeType;
|
||||
if (!NODE_TYPES.has(type)) return null;
|
||||
|
||||
const id = typeof obj.id === 'string' && obj.id.trim() ? obj.id.trim() : generateNodeId();
|
||||
|
||||
if (type === 'CONDITION') {
|
||||
const cond = asRecord(obj.condition);
|
||||
if (!cond) return null;
|
||||
const indicatorType = typeof cond.indicatorType === 'string' ? cond.indicatorType.trim() : '';
|
||||
const conditionType = typeof cond.conditionType === 'string' ? cond.conditionType.trim() : '';
|
||||
if (!indicatorType || !conditionType) return null;
|
||||
// 알려진 지표면 conditionType 매트릭스로 검증, 모르는 지표는 백엔드 폴백에 위임
|
||||
const allowed = INDICATOR_CONDITIONS[indicatorType];
|
||||
if (allowed && !allowed.includes(conditionType)) return null;
|
||||
|
||||
const condition = { ...cond, indicatorType, conditionType } as unknown as ConditionDSL;
|
||||
// 윈도우 모드일 때 candleRange 보정
|
||||
if (condition.candleRangeMode && WINDOW_MODES.has(condition.candleRangeMode)) {
|
||||
const n = Number(condition.candleRange);
|
||||
condition.candleRange = Number.isFinite(n) && n >= 2 ? Math.floor(n) : 4;
|
||||
}
|
||||
return { id, type, condition };
|
||||
}
|
||||
|
||||
// AND / OR / NOT / TIMEFRAME
|
||||
const children = Array.isArray(obj.children)
|
||||
? (obj.children.map(sanitizeAiLogicNode).filter(Boolean) as LogicNode[])
|
||||
: [];
|
||||
|
||||
if (type === 'NOT') {
|
||||
if (children.length === 0) return null;
|
||||
return { id, type, children: [children[0]] } as LogicNode;
|
||||
}
|
||||
|
||||
if (type === 'TIMEFRAME') {
|
||||
if (children.length === 0) return null;
|
||||
const node: LogicNode = { id, type, children };
|
||||
if (typeof obj.candleType === 'string') node.candleType = obj.candleType;
|
||||
if (Array.isArray(obj.candleTypes)) {
|
||||
node.candleTypes = obj.candleTypes.filter((c): c is string => typeof c === 'string');
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
// AND / OR — 자식이 없으면 무의미하므로 제거, 하나뿐이면 그대로 승격하지 않고 유지
|
||||
if (children.length === 0) return null;
|
||||
return { id, type, children } as LogicNode;
|
||||
}
|
||||
|
||||
/** 멀티턴 대화 요청 → 응답 파싱·DSL 검증 */
|
||||
export async function requestStrategyAgentChat(
|
||||
messages: StrategyAgentChatMessage[],
|
||||
context: unknown,
|
||||
): Promise<StrategyAgentResult> {
|
||||
const res: StrategyAgentChatApiResponse = await postStrategyAgentChat(messages, context);
|
||||
const action = res.action === 'build' ? 'build' : 'ask';
|
||||
const buyCondition = action === 'build' ? sanitizeAiLogicNode(res.buyCondition) : null;
|
||||
const sellCondition = action === 'build' ? sanitizeAiLogicNode(res.sellCondition) : null;
|
||||
return {
|
||||
reply: res.reply ?? '',
|
||||
action,
|
||||
buyCondition,
|
||||
sellCondition,
|
||||
hasStrategy: Boolean(buyCondition || sellCondition),
|
||||
model: res.model,
|
||||
latencyMs: res.latencyMs,
|
||||
};
|
||||
}
|
||||
+396
@@ -0,0 +1,396 @@
|
||||
# 드로잉선 → 전략 조건 검토
|
||||
|
||||
실시간 차트 **드로잉 툴바**(TradingView 스타일)에 제공되는 선·채널·피보나치 등 도구 중,
|
||||
현재 구현된 **추세선 돌파(`TREND_LINE_CROSS_*`)** 와 같이 **봉마다 자동 계산 → Ta4j Rule → 실시간·전략평가 시그널**로 확장 가능한 항목을 정리한 문서입니다.
|
||||
|
||||
> **관련:** [매매전략조건.md](./매매전략조건.md) · [전략편집기 로직 및 동작흐름.md](./전략편집기%20로직%20및%20동작흐름.md)
|
||||
> **코드 기준:** `DrawingToolbar.tsx` · `DrawingToolId` · `StrategyDslToTa4jAdapter` · `TrendLineMath`
|
||||
|
||||
---
|
||||
|
||||
## 1. 현재 추세선 전략 조건 (기준 구현)
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| DSL | `TREND_LINE_CROSS_UP` / `TREND_LINE_CROSS_DOWN` |
|
||||
| 드로잉 대응 | **추세줄**(`trendline`) · **익스텐디드 라인**(`extended_line`)과 개념적으로 동일 (2점 직선 + 연장) |
|
||||
| 계산 | fit 구간 `[i−N, i−1]` · **스윙 피벗** (상승=저점↑ / 하락=고점↓) · 시그널 봉 `i`까지 연장 |
|
||||
| 판정 | 직전 봉 ≤ 추세선 → 현재 봉 > 추세선 (상향) / 반대 (하향) |
|
||||
| 설정 | 설정 → 전략 설정 → 추세선 (모드·N·스윙 k) |
|
||||
| 파이프라인 | `StrategyDslToTa4jAdapter` → `BacktestingService` · `LiveConditionStatusService` · 전략평가 차트 |
|
||||
|
||||
**핵심 패턴:** 차트에 사용자가 그리는 **고정 좌표**가 아니라, 매 봉 `i`마다 **Indicator가 Y값(또는 시간)을 반환**하고, 기존 `CROSS_UP`/`CROSS_DOWN` Rule과 결합합니다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 드로잉 툴바 선·채널 도구 목록
|
||||
|
||||
`DrawingToolbar` **라인 / 피보나치 / 채널 / 프로젝션** 그룹 기준 (측정·도형·텍스트·패턴 제외).
|
||||
|
||||
### 2.1 라인 그룹
|
||||
|
||||
| ID | UI 이름 | 점 수 | 기하 |
|
||||
|----|---------|-------|------|
|
||||
| `trendline` | 추세줄 | 2 | 2점 직선, **양방향** 연장 |
|
||||
| `ray` | 빛(레이) | 2 | 2점 직선, **우측** 연장 |
|
||||
| `extended_line` | 익스텐디드 라인 | 2 | 2점 직선, **양방향** 연장 (`trendline`과 동일 렌더) |
|
||||
| `arrow` | 화살표 추세선 | 2 | 레이 + 화살표 |
|
||||
| `trend_angle` | 추세각 | 2 | 추세선 + 각도 표시 |
|
||||
| `info_line` | 인포 라인 | 2 | 2점 + 거리·각도 라벨 |
|
||||
| `hline` | 가로줄 | 1 | **수평선** (가격 고정) |
|
||||
| `hline_ray` | 가로빛 | 1 | 앵커 시점부터 **우측** 수평 |
|
||||
| `vline` | 수직선 | 1 | **수직선** (시간 고정) |
|
||||
| `cross_line` | 크로스 라인 | 1 | 수평+수직 교차 |
|
||||
| `channel` | 추세 채널 | 3 | 기준 추세선 + 폭 |
|
||||
| `parallel_channel` | 패러렐 채널 | 2 | 평행 2선 |
|
||||
| `disjoint_channel` | 디스조인트 채널 | 4 | 비평행 2선 |
|
||||
| `flat_top_bottom` | 플랫 탑/바텀 | 2+ | 상단 수평 + 하단 추세 |
|
||||
|
||||
### 2.2 피보나치·갠·피치포크
|
||||
|
||||
| ID | UI 이름 | 산출물 |
|
||||
|----|---------|--------|
|
||||
| `fib` | 피보나치 되돌림 | 2점 구간 **수평 레벨** 다수 (0, 23.6, …, 100%) |
|
||||
| `fib_ext` | 추세기반 피보나치 확장 | 되돌림 + **확장** 수평 레벨 |
|
||||
| `fib_channel` | 피보나치 채널 | 기울어진 채널 + 피보나치 분할 |
|
||||
| `fibtz` | 피보나치 타임존 | 2점 시간 구간 **수직선** |
|
||||
| `fib_speed_fan` | 스피드 리지스턴스 팬 | 1점에서 **다발 레이** |
|
||||
| `fib_trend_time` | 추세기반 피보나치 시간 | 추세선 + 시간축 피보나치 |
|
||||
| `fib_circles` / `fib_spiral` / `fib_speed_arc` / `fib_wedge` | 원·스파이럴·아크·웻지 | 곡선/영역 |
|
||||
| `pitchfork` / `pitchfork_schiff` / `pitchfork_inside` | 피치포크 | 3점 → **중심선 + 평행 2선** |
|
||||
| `gann_box` / `gann_square` / `gann_fan` | 갠 | 격자·팬 **다발 직선** |
|
||||
|
||||
### 2.3 프로젝션·기타 (선 성격)
|
||||
|
||||
| ID | UI 이름 | 비고 |
|
||||
|----|---------|------|
|
||||
| `anchored_vwap` | 앵커드 VWAP | 앵커 시점부터 **누적 VWAP 곡선** |
|
||||
| `price_range` | 가격범위 | 구간 **상·하단 수평** |
|
||||
| `date_range` | 날짜 범위 | **수직** 구간 |
|
||||
| `guide_line` | 가이드선 | 보조 수평 (차트 UI) |
|
||||
| `forecast` / `projection_tool` | 예측·프로젝션 | 2점+ **연장** |
|
||||
|
||||
---
|
||||
|
||||
## 3. 전략 조건화 평가 기준
|
||||
|
||||
드로잉 도구를 **실시간 매매 시그널**로 쓰려면 아래가 필요합니다.
|
||||
|
||||
| # | 요건 | 설명 |
|
||||
|---|------|------|
|
||||
| 1 | **봉 인덱스 결정론** | 사용자 클릭 없이 OHLCV·지표만으로 매 봉 동일 Y(또는 시간) 계산 |
|
||||
| 2 | **교차·돌파 정의** | `CROSS_UP`/`DOWN` 또는 `GT`/`LT`로 표현 가능 |
|
||||
| 3 | **Ta4j Indicator** | `getValue(i)` 또는 Rule 한 개로 encapsulation |
|
||||
| 4 | **워밍업·NaN** | lookback·스윙 k만큼 불안정 구간 처리 |
|
||||
| 5 | **멀티 TF·좌필드** | 종가/RSI 등 left 지표와 스케일 일치 |
|
||||
|
||||
**등급**
|
||||
|
||||
- **A — 즉시 확장 권장:** 추세선과 동일 패턴(동적 1~2선 + CROSS), 구현 난이도 중, 실전 활용도 높음
|
||||
- **B — 부분 가능 / DSL已有:** 수평·채널·고저가 등 **이미 다른 필드로 구현**됐거나, 파라미터만 추가하면 됨
|
||||
- **C — 전략 자동화 부적합:** 주관적 패턴·다점 수동 입력·시간만 조건·시각 주석 위주
|
||||
|
||||
---
|
||||
|
||||
## 4. 요약 매트릭스
|
||||
|
||||
| 드로잉 도구 | 등급 | 전략 활용 | 비고 |
|
||||
|-------------|------|-----------|------|
|
||||
| **추세줄 / 익스텐디드 / 레이** | **A** | ✅ 구현됨 (`TREND_LINE_*`) | 레이=연장 방향만 다름 → `extendMode` 파라미터로 통합 가능 |
|
||||
| **가로줄** | **B** | ✅ 대체 가능 | `CROSS_*` + 고정값·`NH_PRIOR_N`·볼린저·MA |
|
||||
| **가로빛** | **A** | ⚠️ 미구현 | 앵커 이후 구간 수평 = **구간 고점/저점** 또는 **앵커 VWAP** |
|
||||
| **수직선 / 크로스 / 피보나치 TZ / 날짜범위** | **B~C** | △ 시간 조건 | 가격 돌파가 아닌 **봉 번호·세션·시간** 조건 (`TIMEFRAME`·`HOLD` 확장) |
|
||||
| **추세·패러렐·플랫탑 채널** | **A** | ⚠️ 미구현 | **채널 상·하단 돌파** — Donchian과 유사하나 **기울기** 있음 |
|
||||
| **디스조인트 채널** | **B** | △ | 4점 자유도 높음 → 자동화 시 규칙 고정 필요 |
|
||||
| **피보나치 되돌림/확장** | **A** | ⚠️ 미구현 | 스윙 고저 + **수평 레벨** 돌파 (0.382, 0.5, 0.618…) |
|
||||
| **피치포크 (중심선)** | **A** | ⚠️ 미구현 | 3 스윙점 → **중앙 추세선** 돌파 (추세선 일반화) |
|
||||
| **피보나치 팬 / 갠 팬** | **B** | △ | 다중 레이 — **특정 각도 레이** 돌파로 축소 가능 |
|
||||
| **앵커드 VWAP** | **B** | △ | VWAP 지표 + **앵커 lookback** 조건 (별도 지표 DSL) |
|
||||
| **가격범위** | **A** | ⚠️ 미구현 | N봉 **Range 상·하단** 돌파 (Donchian과 동일 family) |
|
||||
| **추세각 / 인포 / 화살표** | **C** | ✗ | 표시·측정용, 시그널 의미 없음 |
|
||||
| **패턴(XABCD·H&S·엘리엇)** | **C** | ✗ | 형태 인식·주관적 — 별도 패턴 엔진 필요 |
|
||||
| **원·호·브러시·텍스트** | **C** | ✗ | 시각 주석 |
|
||||
|
||||
---
|
||||
|
||||
## 5. A등급 — 구현 권장 (추세선과 동일 아키텍처)
|
||||
|
||||
### 5.1 레이 / 익스텐디드 (`ray`, `extended_line`)
|
||||
|
||||
**드로잉 의미:** 2점 직선, 연장 방향만 다름 (우측 only vs 양방향).
|
||||
|
||||
**전략 정의 (제안):**
|
||||
|
||||
- 기존 `TREND_LINE_*` + 파라미터 `extendMode: RIGHT | BOTH | LEFT`
|
||||
- fit·피벗 로직은 **현재 `TrendLineMath`와 동일**
|
||||
- `ray` = 피벗 선을 **평가 봉 i까지만** extrapolation (좌측 연장 없음)
|
||||
|
||||
**조건 예:**
|
||||
|
||||
| 조건 | 의미 |
|
||||
|------|------|
|
||||
| `TREND_LINE_CROSS_UP` + `extendMode=RIGHT` | 하락 저항 **레이** 상향 돌파 |
|
||||
| `TREND_LINE_CROSS_DOWN` | 상승 지지선 하향 이탈 |
|
||||
|
||||
**구현:** `TrendLineMath`에 연장 방향만 추가 — **난이도: 낮음**.
|
||||
|
||||
---
|
||||
|
||||
### 5.2 추세 채널 / 패러렐 채널 (`channel`, `parallel_channel`)
|
||||
|
||||
**드로잉 의미:** 평행한 **상·하 2선**; 가격이 채널 밖으로 나가면 추세 가속·전환.
|
||||
|
||||
**전략 정의 (제안):**
|
||||
|
||||
| DSL (제안) | 계산 | 매수·매도 |
|
||||
|------------|------|-----------|
|
||||
| `CHANNEL_BREAK_UP` | 상단선 = 기준 추세선 + offset (또는 2번째 피벗 평행) | 종가 **상단** 상향 돌파 |
|
||||
| `CHANNEL_BREAK_DOWN` | 하단선 = 기준 추세선 − offset | 종가 **하단** 하향 돌파 |
|
||||
| `CHANNEL_BOUNCE_UP` | 하단선 터치 후 반등 | `CROSS_UP` on lower band |
|
||||
|
||||
**알고리즘 (패러렐 채널 자동화):**
|
||||
|
||||
1. `[i−N, i−1]`에서 **상승 추세선(저점)** 또는 **하락 추세선(고점)** 1본 계산 (현행과 동일)
|
||||
2. offset = 구간 내 **최대 High − 추세선값** (상단 채널) 또는 **추세선값 − 최소 Low** (하단)
|
||||
3. 또는 **스윙 고점 2개 + 스윙 저점 2개**로 평행선 쌍 결정
|
||||
|
||||
**Ta4j:** `UpperChannelIndicator` / `LowerChannelIndicator` (커스텀) + `CROSS_*`.
|
||||
|
||||
**기존 DSL과 관계:** `DONCHIAN` `DC_UPPER_N` / `DC_LOWER_N`은 **수평 채널**(기울기 0). 본 조건은 **기울어진 채널** — 차별화됨.
|
||||
|
||||
**난이도: 중.**
|
||||
|
||||
---
|
||||
|
||||
### 5.3 플랫 탑/바텀 (`flat_top_bottom`)
|
||||
|
||||
**드로잉 의미:** 한쪽은 **수평**(저항/지지), 다른 쪽은 **기울어진** 변 연결 — 삼각 수렴·확장 패턴의 일부.
|
||||
|
||||
**전략 정의 (제안):**
|
||||
|
||||
| DSL (제안) | 구성 |
|
||||
|------------|------|
|
||||
| `FLAT_TOP_BREAK_UP` | `[i−N,i−1]` **최고가 수평선** (flat top) 상향 돌파 |
|
||||
| `FLAT_BOTTOM_BREAK_DOWN` | **최저가 수평선** (flat bottom) 하향 돌파 |
|
||||
| `WEDGE_BREAK_*` | flat + 대각 추세선 **수렴 후** 돌파 (확장) |
|
||||
|
||||
flat top = `NH_PRIOR` 구간 max(High)와 유사하나 **동일 구간** 명시.
|
||||
**난이도: 중 (채널 특수 케이스).**
|
||||
|
||||
---
|
||||
|
||||
### 5.4 피보나치 되돌림 / 확장 (`fib`, `fib_ext`)
|
||||
|
||||
**드로잉 의미:** 스윙 A→B 구간에 **비율별 수평선** (0%, 38.2%, 50%, 61.8%, 100% …).
|
||||
|
||||
**전략 정의 (제안):**
|
||||
|
||||
```
|
||||
FIB_LEVEL_CROSS_UP / DOWN
|
||||
- swingHigh, swingLow: [i−N, i−1] 스윙 (또는 구간 고/저)
|
||||
- level: 0.382 | 0.5 | 0.618 | 0.786 | 1.272 (확장)
|
||||
- left: CLOSE_PRICE (또는 지표)
|
||||
```
|
||||
|
||||
| 활용 | 조건 |
|
||||
|------|------|
|
||||
| 되돌림 매수 | 61.8% **지지** 후 `CROSS_UP` |
|
||||
| 되돌림 매도 | 38.2% **저항** 후 `CROSS_DOWN` |
|
||||
| 확장 목표 | 1.272 / 1.618 **돌파** 추적 |
|
||||
|
||||
**Indicator:** `FibLevelIndicator(swingA, swingB, ratio)` → bar i에서 해당 레벨 Y.
|
||||
|
||||
**차트 표시:** 전략평가 시 선택 봉 기준 **해당 레벨 1개** 또는 **주요 3선**만 overlay (추세선과 동일 UX).
|
||||
|
||||
**난이도: 중.**
|
||||
|
||||
---
|
||||
|
||||
### 5.5 피치포크 중심선 (`pitchfork` 계열)
|
||||
|
||||
**드로잉 의미:** 3점(스윙1, 스윙2, 스윙3) → **중앙 추세선** + 위·아래 평행선.
|
||||
|
||||
**전략 정의 (제안):**
|
||||
|
||||
| DSL (제안) | 의미 |
|
||||
|------------|------|
|
||||
| `PITCHFORK_MEDIAN_CROSS_UP/DOWN` | **중심선** 돌파 |
|
||||
| `PITCHFORK_UPPER_TOUCH` | 상단 평행선 도달 |
|
||||
| `PITCHFORK_LOWER_BOUNCE` | 하단 평행선 반등 |
|
||||
|
||||
**알고리즘:** 3 스윙 저점(상승) 또는 고점(하락) → 중점·기울기 → median line at i.
|
||||
Andrews / Schiff / Inside는 **중심선 계산식**만 다름 — 설정에서 `pitchforkVariant`.
|
||||
|
||||
**난이도: 중~상.**
|
||||
|
||||
---
|
||||
|
||||
### 5.6 N봉 가격 레인지 (`price_range` 드로잉 ↔ Range 돌파)
|
||||
|
||||
**드로잉 의미:** 구간 **상단·하단** 박스.
|
||||
|
||||
**전략 정의:** Donchian과 동일 family.
|
||||
|
||||
| 기존 DSL | 드로잉 대응 |
|
||||
|----------|-------------|
|
||||
| `CLOSE CROSS_UP NH_PRIOR_20` | 20봉 **신고가(상단)** 돌파 |
|
||||
| `CLOSE CROSS_DOWN NL_PRIOR_20` | 20봉 **신저가(하단)** 이탈 |
|
||||
| `DONCHIAN` + `DC_UPPER_N` | 채널 상단 |
|
||||
|
||||
**추가 제안:** `RANGE_BREAK_UP/DOWN` — lookback N 명시적 alias (UI는 **가격범위** 드로잉과 매핑).
|
||||
|
||||
**난이도: 낮음 (이미 80% 구현).**
|
||||
|
||||
---
|
||||
|
||||
## 6. B등급 — 부분 가능 / 기존 DSL로 커버
|
||||
|
||||
### 6.1 가로줄 (`hline`)
|
||||
|
||||
| 드로잉 | 전략 DSL 대체 |
|
||||
|--------|----------------|
|
||||
| 임의 가격 수평선 | `GT`/`LT`/`CROSS_*` + `compareValue` 또는 지표 임계 |
|
||||
| N봉 고가/저가 | `NH_PRIOR_N` / `NL_PRIOR_N` · `DC_UPPER_N` / `DC_LOWER_N` |
|
||||
| 볼린저 상·중·하 | `BOLLINGER` + `UPPER`/`MIDDLE`/`LOWER` |
|
||||
| 전고·전저 | `NEW_HIGH` / `NEW_LOW` 지표 |
|
||||
|
||||
**신규 DSL 필요성:** 낮음. 전략편집기 **「가로줄 = 수준 돌파」** 프리셋 UI만 추가해도 충분.
|
||||
|
||||
---
|
||||
|
||||
### 6.2 가로빛 (`hline_ray`)
|
||||
|
||||
**의미:** 특정 **시점 이후**에만 유효한 수평선 (예: 돌파봉 종가, 당일 시가).
|
||||
|
||||
**제안:**
|
||||
|
||||
| DSL (제안) | 정의 |
|
||||
|------------|------|
|
||||
| `SESSION_OPEN` | 당일/세션 시가 (수평) |
|
||||
| `BAR_N_CLOSE` | N봉 전 종가 수평 (앵커) |
|
||||
| `SWING_LEVEL` | 마지막 스윙 고/저 **수평 레이** |
|
||||
|
||||
**난이도: 중** (앵커 bar index 추적).
|
||||
|
||||
---
|
||||
|
||||
### 6.3 앵커드 VWAP (`anchored_vwap`)
|
||||
|
||||
**의미:** 특정 봉부터 누적 VWAP.
|
||||
|
||||
**전략:** `VWAP` 지표(또는 신규 `ANCHORED_VWAP`) + `CROSS_UP/DOWN`.
|
||||
앵커 = `i−anchorLookback` 또는 **세션 시작**.
|
||||
|
||||
**난이도: 중** (볼륨·누적 합).
|
||||
|
||||
---
|
||||
|
||||
### 6.4 피보나치 팬 / 갠 팬 (`fib_speed_fan`, `gann_fan`)
|
||||
|
||||
**의미:** 1점에서 **여러 각도**의 레이.
|
||||
|
||||
**전략 축소:** 팬 전체가 아닌 **특정 비율 1선**만 — 예: 1×1, 2×1 갠 각도 = 기울기 `k`인 **동적 추세선**.
|
||||
|
||||
| 제안 | 조건 |
|
||||
|------|------|
|
||||
| `GANN_FAN_CROSS` | 스윙 앵커 + `slopeRatio` (1/1, 1/2…) 레이 돌파 |
|
||||
|
||||
**난이도: 중** (추세선 + 고정 기울기).
|
||||
|
||||
---
|
||||
|
||||
### 6.5 수직선·시간 (`vline`, `fibtz`, `date_range`)
|
||||
|
||||
**의미:** **가격**이 아니라 **시간/봉** 조건.
|
||||
|
||||
| 활용 | DSL 방향 |
|
||||
|------|----------|
|
||||
| 특정 시각 이후만 매수 | `TIMEFRAME` + 세션 필터 |
|
||||
| N봉 경과 후 | `HOLD_N_DAYS` · `BAR_INDEX ≥ anchor` |
|
||||
| 피보나치 시간대 | `TIME_FIB_ZONE` (i가 [t0+φⁿ·Δt] 근처) |
|
||||
|
||||
실시간 **가격 돌파** 시그널과는 별도 — **필터/타이밍** 조건으로 AND 결합.
|
||||
|
||||
**난이도: 중** (가격 Indicator와 분리).
|
||||
|
||||
---
|
||||
|
||||
## 7. C등급 — 자동 전략 조건 부적합
|
||||
|
||||
| 유형 | 이유 |
|
||||
|------|------|
|
||||
| **추세각·인포라인·화살표** | 측정·주석; Y(t) 단일 함수 아님 |
|
||||
| **harmonic 패턴** (XABCD, ABCD, Cypher, H&S) | 4~5점 **형태 매칭** — ML/규칙 트리 별도 |
|
||||
| **엘리엇 파동** 드로잉 | 파동 카운트 주관적; ta4j Elliott는 분석용 |
|
||||
| **고스트피드·봉패턴** | 미래 봉 투영 — 백테스트 **룩어헤드** 위험 |
|
||||
| **long/short position** 박스 | RR 시각화; 진입·손절은 **별도 주문 DSL** |
|
||||
| **브러시·텍스트·핀** | 사용자 주석 |
|
||||
| **원·타원·삼각형** | 영역 포함 판정 — `IN_RECT`류는 가능하나 드로잉과 무관 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 공통 구현 패턴 (추세선과 동일)
|
||||
|
||||
신규 드로잉型 조건 추가 시 **권장 스택:**
|
||||
|
||||
```
|
||||
1. DSL conditionType (예: CHANNEL_BREAK_UP)
|
||||
2. Math/Indicator (bar i, lookback, config → Y 또는 time)
|
||||
3. StrategyDslToTa4jAdapter → buildXxxCrossRule (고정 창 [i−N,i−1])
|
||||
4. BacktestSettings (전역 default N, k, ratio…)
|
||||
5. 전략편집기 팔레트 (추세 탭과 동일 PaletteChip UX)
|
||||
6. 전략평가 차트 (선택 봉 1개 overlay only)
|
||||
7. LiveConditionStatusService 라벨
|
||||
```
|
||||
|
||||
**교차 Rule 템플릿 (현행 추세선):**
|
||||
|
||||
```text
|
||||
직전 봉: left[i−1] ≤ line[i−1]
|
||||
현재 봉: left[i] > line[i] → CROSS_UP
|
||||
line[i±k]는 동일 fit 창 [i−N, i−1]에서 extrapolation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 구현 우선순위 (로드맵 제안)
|
||||
|
||||
| 순위 | 기능 | 드로잉 대응 | 이유 |
|
||||
|------|------|-------------|------|
|
||||
| 1 | `extendMode` on `TREND_LINE` | ray / extended | 코드 변경 최소 |
|
||||
| 2 | `CHANNEL_BREAK_*` | channel, parallel | 돈치안보다 실전 TA에 가까움 |
|
||||
| 3 | `FIB_LEVEL_CROSS_*` | fib, fib_ext | 되돌림·확장 매매 빈도 높음 |
|
||||
| 4 | `FLAT_TOP/BOTTOM_*` | flat_top_bottom | 삼각·깃발 패턴 기초 |
|
||||
| 5 | `PITCHFORK_MEDIAN_*` | pitchfork | 중심선 트레이딩 |
|
||||
| 6 | 가로줄 프리셋 UI | hline | 신규 엔진 없이 DSL 노출 |
|
||||
| 7 | `ANCHORED_VWAP_CROSS` | anchored_vwap | VWAP 전략 수요 |
|
||||
| 8 | 시간 조건 (`TIME_*`) | vline, fibtz | 필터로 활용 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 실시간 매매 체크 연동
|
||||
|
||||
현재 파이프라인은 **드로잉 객체(JSON)를 읽지 않습니다.**
|
||||
차트에 사용자가 그린 선과 **전략 시그널은 독립**이며, 전략은 **동일 수학 규칙**으로 매 봉 재계산합니다.
|
||||
|
||||
| 단계 | 서비스 |
|
||||
|------|--------|
|
||||
| 설정 로드 | `BacktestSettingsService` → `TrendLineConfig` |
|
||||
| Rule 생성 | `StrategyDslToTa4jAdapter.toRule` |
|
||||
| 백테스트·스캔 | `BacktestingService` |
|
||||
| 실시간 | `LiveConditionStatusService` (조건별 충족/미충족) |
|
||||
| UI | `StrategyEvaluationChart` · 가상/실전 차트 시그널 마커 |
|
||||
|
||||
신규 선 조건도 **드로잉 저장소와 연동할 필요 없음** — 추세선과 같이 **Indicator 기반**이면 실시간 체크에 바로 투입 가능.
|
||||
|
||||
---
|
||||
|
||||
## 11. 한 줄 결론
|
||||
|
||||
> **추세선(`TREND_LINE`)과 같이 “매 봉 Y값을 계산하는 1~2차원 직선·수평 레벨”** 이면 대부분 `CROSS_*` Rule로 실시간 전략에 넣을 수 있다.
|
||||
> **우선순위는 채널(평행선)·피보나치 수준·레이 연장 방향**이며, **가로줄·Donchian·볼린저**는 이미 DSL로 커버된다.
|
||||
> **패턴·팬 전체·브러시**는 드로잉 툴로는 유용하나 **자동 시그널**에는 부적합하거나 별도 엔진이 필요하다.
|
||||
|
||||
---
|
||||
|
||||
*문서 버전: 2026-06 — goldenChart 드로잉 툴바·TREND_LINE 구현 기준*
|
||||
+505
@@ -0,0 +1,505 @@
|
||||
# 전략편집기 — 우측 패널 지표 컨트롤 ↔ 매매 시그널 감지
|
||||
|
||||
전략편집기 우측 **지표** 패널에 있는 컨트롤(칩)을 클릭·드래그했을 때 생성되는 DSL 조건이,
|
||||
실제 차트·백테스트·실시간 전략 평가에서 **어떻게 매수/매도 시그널로 판정되는지**를 정리한 문서입니다.
|
||||
|
||||
> **평가 원천:** `StrategyDslToTa4jAdapter` → Ta4j `Rule` → 봉 `i`에서 `rule.isSatisfied(i)`
|
||||
> **관련:** [매매전략조건.md](./매매전략조건.md) · [드로잉선_전략조건.md](./드로잉선_전략조건.md) · [전략편집기 로직 및 동작흐름.md](./전략편집기%20로직%20및%20동작흐름.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 전체 흐름
|
||||
|
||||
```text
|
||||
[우측 PaletteChip 클릭/드래그]
|
||||
↓
|
||||
makeNode / buildXxxConditionNode → LogicNode (CONDITION 또는 AND/OR/NOT 트리)
|
||||
↓
|
||||
전략 저장 (buyConditionJson / sellConditionJson)
|
||||
↓
|
||||
StrategyDslToTa4jAdapter.toRule() — leaf CONDITION → Ta4j Rule
|
||||
↓
|
||||
봉 i마다 isSatisfied(i) — true면 해당 봉에 BUY 또는 SELL 시그널
|
||||
↓
|
||||
전략평가 차트 마커 · LiveConditionStatusService · BacktestingService
|
||||
```
|
||||
|
||||
### 1.1 매수/매도 탭과 시그널 방향
|
||||
|
||||
| 편집기 탭 | DSL 저장 위치 | 시그널 타입 |
|
||||
|-----------|---------------|-------------|
|
||||
| **매수** (`buySellTab = buy`) | `buyConditionJson` | `BUY` |
|
||||
| **매도** (`buySellTab = sell`) | `sellConditionJson` | `SELL` |
|
||||
|
||||
같은 칩이라도 **어느 탭에서 추가했는지**에 따라 `conditionType` 기본값이 달라집니다.
|
||||
예: 보조 RSI 칩 → 매수 탭 `CROSS_UP`, 매도 탭 `CROSS_DOWN`.
|
||||
|
||||
### 1.2 positionMode (설정 → 전략 설정)
|
||||
|
||||
| 모드 | 동작 |
|
||||
|------|------|
|
||||
| **SIGNAL_ONLY** | 매수·매도 Rule **독립** — 보유 여부와 무관하게 봉마다 시그널 |
|
||||
| **LONG_ONLY** | **미보유**일 때만 entry(BUY), **보유**일 때만 exit(SELL) — `TradingRecord` 추적 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 우측 패널 구조
|
||||
|
||||
**파일:** `StrategyEditorPage.tsx`, `StrategyEvaluationIndicatorPalettePanel.tsx`
|
||||
|
||||
| 영역 | 내용 |
|
||||
|------|------|
|
||||
| 검색 | `paletteSearch` — 라벨·설명 필터 |
|
||||
| **시작·논리** | START, AND, OR, NOT |
|
||||
| **밴드·추세** | MA, EMA, BOLLINGER, DONCHIAN, ICHIMOKU (고정 5칩) |
|
||||
| **지표 서브탭** | 보조지표 · 복합지표 · 횡보필터 · 추세 |
|
||||
| **템플릿 탭** | 내장·사용자 템플릿 (복합/횡보 항목 재노출) |
|
||||
|
||||
칩 추가 경로:
|
||||
|
||||
- **클릭** → `applyPalette` / `applySidewaysFilter` / `applyTrendLine`
|
||||
- **드래그** → `PaletteChip` → `StrategyEditorCanvas.resolvePaletteDropNode`
|
||||
|
||||
---
|
||||
|
||||
## 3. 논리 노드 (START / AND / OR / NOT / TIMEFRAME)
|
||||
|
||||
### 3.1 START
|
||||
|
||||
- 캔버스 **시작 섹션** 추가용. 조건 노드가 아님.
|
||||
- 실제 평가는 START 아래 **자식 AND/OR 트리**가 담당.
|
||||
|
||||
### 3.2 AND / OR
|
||||
|
||||
| 노드 | Ta4j | 시그널 조건 |
|
||||
|------|------|-------------|
|
||||
| **AND** | `AndRule(자식…)` | **모든** 자식 Rule이 같은 봉 `i`에서 true |
|
||||
| **OR** | `OrRule(자식…)` | **하나라도** true면 true |
|
||||
|
||||
빈 AND/OR → 항상 false (시그널 없음).
|
||||
|
||||
### 3.3 NOT
|
||||
|
||||
- **첫 번째 자식만** `NotRule`로 감쌈.
|
||||
- 예: 횡보필터 **exclude** 모드 → `NOT(횡보 조건)` = 추세 구간만 통과.
|
||||
|
||||
### 3.4 TIMEFRAME (분봉 분기)
|
||||
|
||||
```json
|
||||
{ "type": "TIMEFRAME", "candleTypes": ["1h"], "children": [ … ] }
|
||||
```
|
||||
|
||||
| 단계 | 동작 |
|
||||
|------|------|
|
||||
| 1 | `candleTypes[0]` 분봉으로 OHLCV **별도 BarSeries** 구성 |
|
||||
| 2 | 자식 Rule을 그 분봉에서 평가 |
|
||||
| 3 | 차트( primary ) 분봉 index `i`와 **시간 정렬** (`CrossSeriesRule`) |
|
||||
|
||||
**백테스트/전략평가:** primary 봉 시각에 맞는 **확정 분봉** index로 매핑.
|
||||
**실시간:** `LiveStrategyEvaluator` + `StrategyTriggerBranchEvaluator` — 트리거 분봉 마감 시 분기 평가, AND 다중 TF는 **캐시** 결합.
|
||||
|
||||
---
|
||||
|
||||
## 4. 공통 CONDITION 구조
|
||||
|
||||
```typescript
|
||||
interface ConditionDSL {
|
||||
indicatorType: string; // RSI, MA, TREND_LINE, …
|
||||
conditionType: string; // CROSS_UP, GT, TREND_LINE_CROSS_UP, …
|
||||
leftField?: string; // RSI_VALUE, CLOSE_PRICE, MA20, …
|
||||
rightField?: string; // HL_OVER, DC_UPPER_20, NONE, …
|
||||
period?: number;
|
||||
lookbackPeriod?: number; // 추세선 N, LOWEST N봉
|
||||
compareValue?: number; // DIFF_GT, VOLUME_GT_MA_RATIO
|
||||
slopePeriod?: number; // SLOPE_UP/DOWN
|
||||
holdDays?: number; // HOLD_N_DAYS
|
||||
candleRange?: number;
|
||||
candleRangeMode?: 'CURRENT' | 'EXISTS_IN' | 'NOT_EXISTS_IN';
|
||||
composite?: boolean;
|
||||
leftPeriod / rightPeriod / leftCandleType / rightCandleType;
|
||||
params?: Record<string, …>;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.1 conditionType → 차트에서의 의미
|
||||
|
||||
| conditionType | 판정 방식 | 적용 봉 |
|
||||
|---------------|-----------|---------|
|
||||
| **GT / LT / GTE / LTE / EQ / NEQ** | `left[i]` vs `right[i]` **수준 비교** | 현재 봉 `i` |
|
||||
| **CROSS_UP** | `left[i−1] ≤ right[i−1]` **그리고** `left[i] > right[i]` | 교차 **1봉** |
|
||||
| **CROSS_DOWN** | `left[i−1] ≥ right[i−1]` **그리고** `left[i] < right[i]` | 교차 **1봉** |
|
||||
| **TREND_LINE_CROSS_UP/DOWN** | CROSS와 동일 형식, **right = N봉 피벗 추세선** (fit `[i−N,i−1]`) | 교차 1봉 |
|
||||
| **SLOPE_UP / SLOPE_DOWN** | `left`가 `slopePeriod`봉 **연속 상승/하락** | 현재 봉 |
|
||||
| **DIFF_GT / DIFF_LT** | `|left − right| >/< compareValue` | 현재 봉 |
|
||||
| **HOLD_N_DAYS** | `left > right`가 **N봉 연속** | 현재 봉 |
|
||||
| **LOWEST_LTE / LOWEST_GTE** | `min(left, i−N..i)` vs `right` | 현재 봉 |
|
||||
|
||||
**NaN·미확정:** left/right NaN → 해당 봉 false. `CROSS_*`는 `index < 1`이면 false.
|
||||
|
||||
### 4.2 leftField / rightField 해석
|
||||
|
||||
백엔드 `resolveField()`가 심볼 → Ta4j `Indicator`로 변환합니다.
|
||||
|
||||
| 심볼 예 | 의미 |
|
||||
|---------|------|
|
||||
| `CLOSE_PRICE` | 종가 |
|
||||
| `RSI_VALUE` | RSI 본선 (period는 지표 설정·조건 상속) |
|
||||
| `HL_OVER` / `HL_MID` / `HL_UNDER` | 과열/중립/침체 **임계값** (설정·지표별) |
|
||||
| `K_80`, `K_20` | Stoch 직접 입력 임계 |
|
||||
| `DC_UPPER_20` | 20봉 Donchian **상단** (직전 N봉 high max, 당일 제외 방식은 NH_PRIOR 참고) |
|
||||
| `NH_PRIOR_9` | **직전 9봉** 최고가 (당일 봉 **미포함**) |
|
||||
| `NL_PRIOR_9` | **직전 9봉** 최저가 |
|
||||
| `NONE` | 일목 구름·추세선 등 **단항** 조건 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 밴드·추세 (고정 5칩)
|
||||
|
||||
**출처:** `BAND_PALETTE_CHIP_ITEMS` (`strategyPaletteStorage.ts`)
|
||||
|
||||
### 5.1 이동평균 (MA)
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 기본 (매수) | `MA` · `CROSS_UP` · `MA1` vs `MA2` |
|
||||
| 기본 (매도) | `MA` · `CROSS_DOWN` · `MA1` vs `MA2` |
|
||||
| **시그널** | 단기 MA가 장기 MA **상향/하향 교차**한 **그 봉** |
|
||||
|
||||
MA1/MA2 기간 → 보조지표 설정 `period1`, `period2` (SMA 설정).
|
||||
|
||||
### 5.2 EMA
|
||||
|
||||
MA와 동일 구조. `EMA1` vs `EMA2`, `EMAIndicator` 사용.
|
||||
|
||||
### 5.3 볼린저밴드 (BOLLINGER)
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 기본 (매수) | `CLOSE_PRICE` `CROSS_UP` `UPPER_BAND` |
|
||||
| 기본 (매도) | `CLOSE_PRICE` `CROSS_DOWN` `LOWER_BAND` (편집기에서 변경 가능) |
|
||||
| **시그널** | 종가가 **밴드 경계**를 돌파한 봉 |
|
||||
|
||||
다른 조건: `GT`/`LT`로 밴드 **내부/외부** 체류 판정.
|
||||
|
||||
### 5.4 돈치안 (DONCHIAN)
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 기본 (매수) | `CLOSE_PRICE` `CROSS_UP` `DC_UPPER_20` |
|
||||
| 기본 (매도) | `CLOSE_PRICE` `CROSS_DOWN` `DC_LOWER_20` |
|
||||
| **시그널** | 종가가 N봉 채널 **상단/하단** 교차 |
|
||||
|
||||
`DC_UPPER_n` = Donchian 상단 Indicator. (신고가 `NH_PRIOR_n`과 계산 방식·당일 포함 여부 주의 — §7.4)
|
||||
|
||||
### 5.5 일목균형표 (ICHIMOKU)
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 기본 (매수) | `CONVERSION_LINE` `CROSS_UP` `BASE_LINE` |
|
||||
| 기본 (매도) | `CONVERSION_LINE` `CROSS_DOWN` `BASE_LINE` |
|
||||
| **시그널** | **전환선·기준선 골든/데드크로스** |
|
||||
|
||||
**추가 전용 conditionType** (left/right 자동 설정):
|
||||
|
||||
| conditionType | 시그널 의미 |
|
||||
|---------------|-------------|
|
||||
| `ABOVE_CLOUD` | 종가 > 구름 상단 |
|
||||
| `BELOW_CLOUD` | 종가 < 구름 하단 |
|
||||
| `IN_CLOUD` | 종가가 구름 **내부** |
|
||||
| `CLOUD_BREAK_UP` | 종가 **구름 상단** 상향 돌파 (Ta4j CrossedUp) |
|
||||
| `CLOUD_BREAK_DOWN` | 종가 **구름 하단** 하향 이탈 |
|
||||
| `SPAN1_CROSS_UP_SPAN2` | 선행1 **선행2** 상향 교차 |
|
||||
| `LAGGING_GT_PRICE` | 후행스팬 > **26봉 전** 종가 |
|
||||
| `LAGGING_ABOVE_PRIOR_CLOUD` | 후행 > **26봉 전** 구름 상단 |
|
||||
| `CHIKOU_CROSS_ABOVE_PRIOR_CLOUD` | 후행 **구름 상단** 1회 돌파 |
|
||||
|
||||
파라미터: 전환 9 · 기준 26 · 선행/후행 26 · 선행2 52 (설정 상속).
|
||||
|
||||
---
|
||||
|
||||
## 6. 보조지표 탭 (`auxiliary`)
|
||||
|
||||
**내장 17종** + 사용자 추가 (`DEFAULT_AUXILIARY_ITEMS`).
|
||||
단일 칩 → **CONDITION 1개** (`makeNode` → `getDefaultConditionFields`).
|
||||
|
||||
### 6.1 오실레이터 (RSI, Stochastic, CCI, Williams %R, BWI)
|
||||
|
||||
| 지표 | 기본 left | 기본 right | 기본 condition (매수) |
|
||||
|------|-----------|------------|------------------------|
|
||||
| RSI | `RSI_VALUE` | `HL_OVER` (과열) | `CROSS_UP` → 침체→과열 **교차** 아님, **과열선 돌파** |
|
||||
| Stochastic | `STOCH_K` | `STOCH_D` | `%K` vs `%D` 교차 |
|
||||
| CCI | `CCI_VALUE` | `CCI_SIGNAL` | CCI vs 시그널선 교차 |
|
||||
| Williams %R | `WILLIAMS_R_VALUE` | `HL_OVER` | 과매도 구간 **탈출** 등 (필드 변경 가능) |
|
||||
|
||||
**추가 (오실레이터 전용):**
|
||||
|
||||
| conditionType | 시그널 |
|
||||
|---------------|--------|
|
||||
| `LOWEST_LTE` | 최근 **N봉**(lookback, 기본 20) `left` **최저** ≤ 침체선 — “최근 N봉 안 침체 터치” |
|
||||
| `LOWEST_GTE` | 최근 N봉 최저 ≥ 임계 — 침체 이탈 유지 |
|
||||
|
||||
**실전 예 (Stoch 재매수 억제):** `AND( CROSS_UP 80, LOWEST_LTE 20 )` — 80 돌파 **且** 최근 20봉 내 20 이하 경험.
|
||||
|
||||
### 6.2 추세·거래량 (ADX, DMI, MACD, OBV, TRIX, Volume OSC, VR)
|
||||
|
||||
| 지표 | 기본 left → right | 전형적 시그널 |
|
||||
|------|-------------------|---------------|
|
||||
| ADX | `ADX_VALUE` → `HL_MID` | ADX > 25 추세 강도 |
|
||||
| DMI | `PDI` → `MDI` | +DI **−DI** 교차 |
|
||||
| MACD | `MACD_LINE` → `SIGNAL_LINE` | MACD **시그널** 골든크로스 |
|
||||
| OBV | `OBV_LINE` → `OBV_SIGNAL` | OBV vs OBV 시그널 |
|
||||
| TRIX | `TRIX_VALUE` → `TRIX_SIGNAL` | TRIX 교차 |
|
||||
| Volume OSC | `VOLUME_OSC_VALUE` → `HL_MID` | 거래량 OSC 중립 돌파 |
|
||||
| VR | `VR_VALUE` → `HL_OVER` | VR 과열 |
|
||||
|
||||
### 6.3 심리·이격 (DISPARITY, PSY*, INVEST_PSY)
|
||||
|
||||
- `DISPARITY{n}` vs `HL_MID` — 이격도 과열/침체.
|
||||
- 심리도 계열 — `PSY_VALUE` vs `HL_OVER` 등.
|
||||
|
||||
### 6.4 거래량 (VOLUME)
|
||||
|
||||
| conditionType | 시그널 |
|
||||
|---------------|--------|
|
||||
| `GT` / `CROSS_UP` | 거래량 vs `VOLUME_MA` |
|
||||
| **`VOLUME_GT_MA_RATIO`** | `volume[i] ≥ MA(length) × compareValue` (기본 MA 5, 비율 1.5) |
|
||||
|
||||
### 6.5 신고가 / 신저가 (NEW_HIGH / NEW_LOW)
|
||||
|
||||
| 항목 | 매수 (NEW_HIGH) | 매도 (NEW_LOW) |
|
||||
|------|-----------------|----------------|
|
||||
| 기본 | `CLOSE` `CROSS_UP` `NH_PRIOR_9` | `CLOSE` `CROSS_DOWN` `NL_PRIOR_9` |
|
||||
| **right 의미** | **직전 9봉** 최고가 수평선 | **직전 9봉** 최저가 |
|
||||
| **시그널** | 종가가 **신고가선** 상향 **교차** | 종가가 **신저가선** 하향 **교차** |
|
||||
|
||||
백엔드: `NH_PRIOR_n` = `max(high[t−n] … high[t−1])` (**현재 봉 high 미포함**).
|
||||
`CROSS_UP` + right≠NH_PRIOR 시 **GTE**로 정규화 (Donchian 당일 포함 CROSS 방지).
|
||||
|
||||
---
|
||||
|
||||
## 7. 복합지표 탭 (`composite`)
|
||||
|
||||
### 7.1 동일 지표 2기간 교차 (`COMPOSITE_INDICATOR_ITEMS`)
|
||||
|
||||
**플래그:** `composite: true`, `leftPeriod`, `rightPeriod`, `leftField`, `rightField`, (선택) `leftCandleType` / `rightCandleType`.
|
||||
|
||||
| 칩 | left → right (예) | 시그널 |
|
||||
|----|-------------------|--------|
|
||||
| 돈치안 | `DC_UPPER_9` `CROSS_UP` `DC_UPPER_20` | **단기** 채널상단이 **장기** 상단 돌파 |
|
||||
| RSI | `RSI_VALUE_9` `CROSS_UP` `RSI_VALUE_18` | 단기 RSI **장기** RSI 상향 교차 |
|
||||
| CCI, ADX, Williams, TRIX, VR, PSY, 이격도 | 동일 패턴 | 기간 교차 |
|
||||
|
||||
**멀티 TF:** `leftCandleType ≠ rightCandleType` → `CrossTimeframeCompositeRule` (각 분봉에서 left/right 취한 뒤 교차 공식).
|
||||
|
||||
### 7.2 Stoch 과열×보조 (`STOCH_OVERBOUGHT_PAIR`)
|
||||
|
||||
**LogicNode OR 트리** (단일 CONDITION 아님):
|
||||
|
||||
```text
|
||||
OR(
|
||||
AND( Stoch %K ≥ 80, 보조지표 CROSS_UP 중앙 ),
|
||||
AND( 보조지표 ≥ 0, Stoch %K CROSS_UP 80 )
|
||||
)
|
||||
```
|
||||
|
||||
- **시그널:** 과열 구간 Stoch×보조 **동시 충족** 또는 **역순** 동시 충족.
|
||||
- 메타: `stochPair.secondaryIndicatorType` (기본 CCI).
|
||||
|
||||
### 7.3 9·20일 신고/신저 (`NEW_HIGH_9_20_BUY` / `NEW_LOW_9_20_SELL`)
|
||||
|
||||
**LogicNode AND:**
|
||||
|
||||
- `NEW_HIGH` `CROSS_UP` `NH_PRIOR_9` (또는 20)
|
||||
- **필터** (filterLevel: balanced / strict) — ADX·거래량 등 추가 leaf
|
||||
|
||||
**시그널:** 단기 신고가 돌파 **且** 필터 통과.
|
||||
|
||||
### 7.4 33변곡 (`INFLECTION_33_BUY/SELL`)
|
||||
|
||||
- EMA33 **변곡** + Donchian/필터 AND.
|
||||
- **시그널:** 33봉 EMA 기울기 전환 + 채널 확인.
|
||||
|
||||
### 7.5 일목×볼린저 (`ICHIMOKU_BB_BUY/SELL`)
|
||||
|
||||
단일 CONDITION:
|
||||
|
||||
```text
|
||||
indicatorType: ICHIMOKU_BB
|
||||
leftField: LAGGING_SPAN
|
||||
rightField: UPPER_BAND | LOWER_BAND
|
||||
conditionType: CROSS_UP | CROSS_DOWN
|
||||
params.chikouDisplacement: 26 (기본)
|
||||
```
|
||||
|
||||
**시그널:** **26봉 전** 후행스팬 vs **26봉 전** BB 밴드 교차 (후행·밴드 모두 lag 적용).
|
||||
|
||||
### 7.6 일목 상황별 (23종, `ICHIMOKU_SIT_*`)
|
||||
|
||||
| 카테고리 | 예 | 내부 구조 |
|
||||
|----------|-----|-----------|
|
||||
| 기본 매수 5 | 골든크로스, 기준선 돌파, 구름 돌파… | AND/OR **복수 CONDITION** |
|
||||
| 호전/역전 | 호전(정배열), 역전(역배열) | 4~5 조건 AND |
|
||||
| 엘리어트 10 | 1·2·3·4·5파, A·B·C | 파동 단계별 AND |
|
||||
|
||||
**시그널:** 해당 **상황 ID**에 정의된 leaf 조건이 **같은 봉**에서 모두(AND) 또는 일부(OR) 충족.
|
||||
|
||||
### 7.7 추천 안정형 (18종, `TURTLE_S1_BUY` 등)
|
||||
|
||||
| 전략 ID | 시그널 요지 |
|
||||
|---------|-------------|
|
||||
| TURTLE_S1 | 20일 **최고가** 돌파 매수 / 10일 **최저** 이탈 청산 |
|
||||
| TURTLE_S2 | 55/20일 터틀 장기 |
|
||||
| DONCHIAN_20 | 20일 채널 상·하단 |
|
||||
| MA_TREND | MA 교차 + ADX |
|
||||
| ICHIMOKU_TREND / SANYAKU / GOELEMENT | 일목 조합 |
|
||||
| MACD_MOMENTUM / BOLLINGER_BOX | 모멘텀·박스 |
|
||||
|
||||
각 항목은 `stableStrategyPairs.ts`에 **고정 LogicNode**로 정의 — 편집기에서 leaf 펼치면 MA·DONCHIAN·ICHIMOKU CONDITION들.
|
||||
|
||||
---
|
||||
|
||||
## 8. 횡보필터 탭 (`range`)
|
||||
|
||||
**목적:** 매수/매도 **본 조건**과 AND로 묶어 **횡보 구간만** 또는 **횡보만 제외**하고 시그널.
|
||||
|
||||
| mode | LogicNode | 시그널 해석 |
|
||||
|------|-----------|-------------|
|
||||
| **include** | `buildInner()` 그대로 | **횡보일 때만** true인 leaf |
|
||||
| **exclude** | `NOT(buildInner())` | **추세일 때만** true |
|
||||
|
||||
**카테고리 10종 · 내장 52필터** (`sidewaysFilterPalette.ts`):
|
||||
|
||||
| 카테고리 | 대표 필터 | leaf 조건 예 | true인 시장 |
|
||||
|----------|-----------|--------------|-------------|
|
||||
| **ADX** | ADX < 25 | `ADX LT HL_MID` | **약한 추세** (횡보) |
|
||||
| | ADX ≥ 25 (exclude용) | `ADX GTE` | 강한 추세 |
|
||||
| **볼린저** | 밴드 수축 | `DIFF_LT` UPPER−LOWER `< 2%` | 변동성 **축소** |
|
||||
| | 종가≈중심 | `CLOSE` ≈ `MIDDLE` | 밴드 중앙 횡보 |
|
||||
| **MA** | MA5≈MA20 | `DIFF_LT` MA5−MA20 `< N` | 이평 **수렴** |
|
||||
| **일목** | 구름 안 | `IN_CLOUD` | 구름 **내부** |
|
||||
| | 구름 얇음 | `DIFF_LT` SPAN1−SPAN2 | 얇은 구름 |
|
||||
| **Donchian** | 9/20 채널 좁음 | `DIFF_LT` UPPER−LOWER | **좁은 레인지** |
|
||||
| **오실레이터** | RSI 40~60 | `GTE`+`LTE` 밴드 | **중립** |
|
||||
| **cycle** | Stoch N봉 침체 | `LOWEST_LTE` `%K` | lookback 내 **20 이하** 경험 |
|
||||
| **DMI** | +DI≈−DI | `DIFF_LT` PDI−MDI | 방향성 **없음** |
|
||||
| **volume** | 거래량 < MA | `VOLUME LT VOLUME_MA` | **저거래** |
|
||||
| **combo** | ADX+구름+RSI | AND | **복합 횡보** |
|
||||
|
||||
**사용 예:**
|
||||
|
||||
```text
|
||||
매수 시그널 = AND( 본래 매수 조건, 횡보필터 exclude「ADX≥25」 )
|
||||
→ 추세가 있을 때만 매수
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 추세 탭 (`trendline`)
|
||||
|
||||
**10칩** (`TREND_LINE_PALETTE_ITEMS`). 추세선 **정의**는 설정 → 전략 설정 (피벗·N·스윙 k).
|
||||
|
||||
| 칩 | indicatorType | leftField | conditionType |
|
||||
|----|---------------|-----------|---------------|
|
||||
| 종가 | MA | `CLOSE_PRICE` | buy: `TREND_LINE_CROSS_UP` |
|
||||
| MA / EMA | MA / EMA | `MA20` / `EMA20` | sell: `TREND_LINE_CROSS_DOWN` |
|
||||
| RSI, MACD, Stoch… | 해당 타입 | 지표선 | 동일 |
|
||||
|
||||
**공통 DSL:** `rightField: NONE`, `lookbackPeriod` (기본 10, 설정 `trendLineDefaultLookback`).
|
||||
|
||||
### 9.1 백엔드 시그널 알고리즘 (`TrendLineMath` + `buildTrendLineCrossUpRule`)
|
||||
|
||||
1. **fit 구간:** `[i − N, i − 1]` (현재 봉 **제외**)
|
||||
2. **피벗 선정:**
|
||||
- **상향 돌파 (매수):** **하락 저항선** — 스윙 **고점** 2개, `High₂ < High₁`, **High** 가격 연결
|
||||
- **하향 돌파 (매도):** **상승 지지선** — 스윙 **저점** 2개, `Low₂ > Low₁`, **Low** 가격 연결
|
||||
3. **동일 선**을 `i−1`, `i`에서 extrapolation → `left`와 **CROSS** 판정
|
||||
4. **오실레이터 left:** 스윙을 **지표값** 기준, 선도 지표 축
|
||||
|
||||
**시그널:** 해당 봉에서 종가(또는 left 지표)가 **N봉 피벗 추세선**을 **상향/하향 돌파**한 **그 봉 1개**.
|
||||
|
||||
전략평가 차트: **선택 봉** 위치의 추세선만 overlay (`buildSignalTrendLineDrawings`).
|
||||
|
||||
---
|
||||
|
||||
## 10. candleRange (N봉 윈도우) — 조건 편집기
|
||||
|
||||
팔레트 기본은 `candleRange: 1`, `candleRangeMode` 미설정 (= **CURRENT**).
|
||||
|
||||
| candleRangeMode | 의미 |
|
||||
|-----------------|------|
|
||||
| **CURRENT** | 해당 봉에서만 core Rule |
|
||||
| **EXISTS_IN** | 최근 N봉 **중 하나라도** core true |
|
||||
| **NOT_EXISTS_IN** | 최근 N봉 **동안 core true 없음** |
|
||||
|
||||
`SLOPE_*` + EXISTS_IN: 최근 N봉 **전 구간** 상승/하락 추세.
|
||||
|
||||
---
|
||||
|
||||
## 11. 시그널이 차트에 표시되는 경로
|
||||
|
||||
| 기능 | 서비스 | Rule 빌드 | 스캔 |
|
||||
|------|--------|-----------|------|
|
||||
| **전략평가** | `BacktestingService` / chart scan | `toRule(buy/sell)` | evalStart..end 각 `i` |
|
||||
| **전략편집기 평가 탭** | 동일 (`StrategyEvaluationChart`) | 동일 | 동일 |
|
||||
| **실시간 조건 패널** | `LiveConditionStatusService.evaluate` | leaf별 + overall | 현재/선택 봉 |
|
||||
| **실시간 자동매매** | `LiveStrategyEvaluator` | TriggerBranch | 분봉 **마감** 시 |
|
||||
|
||||
**마커:** `isSatisfied(i) === true`인 봉 `i`의 **시각·종가**에 BUY/SELL 마커.
|
||||
**LONG_ONLY:** BUY 후 exit Rule true 봉까지 **재진입 BUY 억제**.
|
||||
|
||||
---
|
||||
|
||||
## 12. 컨트롤 → 시그널 빠른 참조표
|
||||
|
||||
| 패널 영역 | 컨트롤 | 생성물 | 한 줄 시그널 요약 |
|
||||
|-----------|--------|--------|-------------------|
|
||||
| 논리 | AND | AND 노드 | 모든 하위 조건 **동시** 충족 봉 |
|
||||
| 논리 | OR | OR 노드 | 하나라도 충족 봉 |
|
||||
| 논리 | NOT | NOT 노드 | 자식 조건 **반대** |
|
||||
| 밴드 | MA/EMA | 1 CONDITION | 이평 **골든/데드크로스** 봉 |
|
||||
| 밴드 | BOLLINGER | 1 CONDITION | 종가 **밴드** 돌파 |
|
||||
| 밴드 | DONCHIAN | 1 CONDITION | 종가 **채널** 돌파 |
|
||||
| 밴드 | ICHIMOKU | 1 CONDITION | 전환/기준 **교차** 또는 구름/후행 **전용** 조건 |
|
||||
| 보조 | RSI 등 | 1 CONDITION | 지표 vs 임계/시그널 **교차·수준** |
|
||||
| 보조 | NEW_HIGH | 1 CONDITION | **직전 N봉 신고가** 교차 |
|
||||
| 복합 | 2기간 RSI… | 1 composite CONDITION | **단기 vs 장기** 교차 |
|
||||
| 복합 | Stoch×CCI | OR 트리 | 과열+보조 **복합** |
|
||||
| 복합 | 9·20 신고가 | AND 트리 | 신고가+**필터** |
|
||||
| 복합 | 일목 23종 | AND/OR 트리 | **상황별** 다중 조건 |
|
||||
| 복합 | 안정형 18종 | AND/OR 트리 | **템플릿 전략** |
|
||||
| 횡보 | ADX<25 등 | AND/NOT 트리 | **횡보/추세 필터** (타 조건과 AND) |
|
||||
| 추세 | 종가/RSI… | 1 CONDITION | **N봉 피벗 추세선** 돌파 |
|
||||
|
||||
---
|
||||
|
||||
## 13. 편집기에서 시그널 확인 방법
|
||||
|
||||
1. **전략평가** 또는 편집기 **평가** 탭 실행 → 차트 **BUY/SELL 마커**
|
||||
2. 하단 **봉 선택기**로 봉 이동 → `LiveConditionStatusService` 스타일로 **조건별 true/false** (평가 패널)
|
||||
3. **추세** 조건: 선택 봉에서 **점선 추세선** overlay
|
||||
4. DSL **CondEditor**에서 `conditionType`·`leftField`·`lookbackPeriod` 변경 → 위 표의 Rule **즉시 반영**
|
||||
|
||||
---
|
||||
|
||||
## 14. 코드 인덱스
|
||||
|
||||
| 영역 | 경로 |
|
||||
|------|------|
|
||||
| 팔레트 UI | `frontend/src/components/StrategyEditorPage.tsx` |
|
||||
| 칩 | `frontend/src/components/strategyEditor/PaletteChip.tsx` |
|
||||
| DSL 타입 | `frontend/src/utils/strategyTypes.ts` |
|
||||
| 노드 생성 | `frontend/src/utils/strategyEditorShared.tsx` |
|
||||
| 보조/복합 목록 | `frontend/src/utils/strategyPaletteStorage.ts` |
|
||||
| 복합 2기간 | `frontend/src/utils/compositeIndicators.ts` |
|
||||
| 횡보 47필터 | `frontend/src/utils/sidewaysFilterPalette.ts` |
|
||||
| 추세 9칩 | `frontend/src/utils/trendLinePalette.ts` |
|
||||
| 일목 23 | `frontend/src/utils/ichimokuSituationStrategy.ts` |
|
||||
| 안정형 18 | `frontend/src/utils/stableStrategyPairs.ts` |
|
||||
| Rule 변환 | `backend/.../StrategyDslToTa4jAdapter.java` |
|
||||
| 추세선 수학 | `backend/.../TrendLineMath.java` |
|
||||
| 백테스트 스캔 | `backend/.../BacktestingService.java` |
|
||||
| 실시간 | `backend/.../LiveConditionStatusService.java` |
|
||||
|
||||
---
|
||||
|
||||
*문서 버전: 2026-06 — goldenChart 전략편집기·StrategyDslToTa4jAdapter 기준*
|
||||
@@ -0,0 +1,382 @@
|
||||
# 전략편집기 — 템플릿 탭 ↔ 매매 시그널 감지
|
||||
|
||||
전략편집기 우측 **템플릿** 탭에 나열된 각 전략을 적용했을 때 생성·저장되는 DSL이,
|
||||
실제 차트·백테스트·실시간 평가에서 **어떻게 매수/매도 시그널로 판정되는지**를 정리한 문서입니다.
|
||||
|
||||
> **평가 원천:** `StrategyDslToTa4jAdapter` → Ta4j `Rule` → 봉 `i`에서 `rule.isSatisfied(i)`
|
||||
> **관련:** [전략지표컨트롤.md](./전략지표컨트롤.md) · [매매전략조건.md](./매매전략조건.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 템플릿 탭 구조
|
||||
|
||||
**파일:** `StrategyEditorPage.tsx`, `strategyTemplateList.ts`, `TemplatePaletteTab.tsx`
|
||||
|
||||
템플릿 목록은 **4종 출처**를 하나의 리스트로 합칩니다 (`buildStrategyTemplatePaletteRows`).
|
||||
|
||||
| 출처 (`source`) | 배지 | 개수(기본) | 생성·적용 |
|
||||
|-----------------|------|------------|-----------|
|
||||
| **builtin** | (없음) | 28 | `getStrategyTemplates()` |
|
||||
| **composite-palette** | 복합지표 | ~58 | 복합지표 팔레트와 동일 항목 |
|
||||
| **sideways-palette** | 횡보지표 | 52 | 횡보필터 팔레트와 동일 항목 |
|
||||
| **user** | 내 템플릿 | 사용자 저장 | `ui_preferences.strategyEditor.userTemplates` |
|
||||
|
||||
검색(`templateSearch`)은 **라벨·설명·매수/매도·배지** 문자열로 필터합니다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 템플릿 적용 방식 (중요)
|
||||
|
||||
템플릿을 **더블클릭/적용**할 때 동작이 출처마다 다릅니다.
|
||||
|
||||
| 출처 | 적용 함수 | 캔버스 동작 |
|
||||
|------|-----------|-------------|
|
||||
| **내장 (builtin)** | `handleTemplate` | 해당 **매수 또는 매도 탭 전체를 교체** (반대 탭·고아 노드 초기화) |
|
||||
| **내 템플릿 (user)** | `applyUserTemplate` | 저장된 **LogicNode 트리로 해당 탭 전체 교체** |
|
||||
| **복합지표 (composite-palette)** | `applyPaletteItem` | **현재 탭 루트에 AND 병합** (기존 조건 유지 + 추가) |
|
||||
| **횡보지표 (sideways-palette)** | `applySidewaysFilter` | **현재 탭 루트에 AND 병합** (필터 노드 추가) |
|
||||
|
||||
```text
|
||||
[내장/내 템플릿 적용]
|
||||
→ buyCondition 또는 sellCondition = 템플릿 루트 1개 (교체)
|
||||
|
||||
[복합·횡보 템플릿 적용]
|
||||
→ mergeAtRoot(기존 루트, 새 노드, AND) — 다른 조건과 결합 가능
|
||||
```
|
||||
|
||||
**전략평가 탭**에서는 `buildTemplateEvaluationPayload`가 내장/내 템플릿을 **새 평가 전략**으로 만들고, 복합·횡보는 동일 빌더로 DSL을 생성합니다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 시그널 판정 공통 규칙
|
||||
|
||||
템플릿도 최종적으로 **LogicNode 트리**이므로, leaf `CONDITION`마다 아래 규칙이 적용됩니다.
|
||||
|
||||
| conditionType | 봉 `i`에서 true 조건 |
|
||||
|---------------|----------------------|
|
||||
| **CROSS_UP** | `left[i−1] ≤ right[i−1]` **且** `left[i] > right[i]` |
|
||||
| **CROSS_DOWN** | `left[i−1] ≥ right[i−1]` **且** `left[i] < right[i]` |
|
||||
| **GT / LT / GTE / LTE** | `left[i]` vs `right[i]` 수준 비교 |
|
||||
| **AND** | 모든 자식 true |
|
||||
| **OR** | 하나라도 true |
|
||||
| **NOT** | 자식 false |
|
||||
| **EXISTS_IN** (N봉) | 최근 N봉 중 core Rule **1회 이상** true |
|
||||
|
||||
**필터 강도:** 복합 전략(9·20 신고가, 33변곡, 안정형 등)은 기본 **`relaxed`(완화)** — ADX·거래량 등 보조 필터 **없음**이 기본값입니다. 편집기에서 **강함/보통**으로 변경 가능.
|
||||
|
||||
**positionMode:** 설정 → 전략 설정. `LONG_ONLY`면 BUY 후 exit(SELL) true 전까지 재진입 BUY 억제.
|
||||
|
||||
---
|
||||
|
||||
## 4. 내장 템플릿 (28종)
|
||||
|
||||
**출처:** `strategyPresets.ts` → `getStrategyTemplates(def)`
|
||||
|
||||
MA 기간은 설정의 `maLines[0]`, `maLines[1]` (기본 5·10일)을 사용합니다.
|
||||
|
||||
### 4.1 단순 템플릿 (`kind: 'simple'`) — 16종
|
||||
|
||||
CONDITION **1개**. `simpleTemplateToNode()`로 변환.
|
||||
|
||||
| 라벨 | signal | indicator | condition | left → right | 시그널 의미 |
|
||||
|------|--------|-----------|-----------|--------------|-------------|
|
||||
| MA 골든크로스 매수 | buy | MA | CROSS_UP | MA5 → MA10 | 단기 MA **장기 MA 상향 교차** 봉 |
|
||||
| MA 데드크로스 매도 | sell | MA | CROSS_DOWN | MA5 → MA10 | 단기 MA **장기 MA 하향 교차** 봉 |
|
||||
| RSI 과매도 매수 | buy | RSI | **LTE** | RSI_VALUE → OVERSOLD_30 | RSI **≤ 30** (과매도 구간 **체류**, 교차 아님) |
|
||||
| RSI 과매수 매도 | sell | RSI | **GTE** | RSI_VALUE → OVERBOUGHT_70 | RSI **≥ 70** |
|
||||
| MACD 상향 돌파 | buy | MACD | CROSS_UP | MACD_LINE → SIGNAL_LINE | MACD **시그널선 골든크로스** |
|
||||
| MACD 하향 돌파 | sell | MACD | CROSS_DOWN | MACD_LINE → SIGNAL_LINE | MACD **데드크로스** |
|
||||
| 볼린저 하단 돌파 매수 | buy | BOLLINGER | CROSS_UP | CLOSE → LOWER_BAND | 종가 **하단밴드 상향 돌파** (반등) |
|
||||
| 볼린저 상단 돌파 매도 | sell | BOLLINGER | CROSS_UP | CLOSE → UPPER_BAND | 종가 **상단밴드 상향 돌파** (과열 이탈) |
|
||||
| CCI 과매도 반등 | buy | CCI | CROSS_UP | CCI_VALUE → OVERSOLD_NEG100 | CCI **−100선 상향 돌파** |
|
||||
| 심리도 과매도 반등 매수 | buy | PSY | CROSS_UP | PSY_VALUE → OVERSOLD_25 | 심리도 **25선 상향 돌파** |
|
||||
| 스토캐스틱 골든크로스 | buy | STOCH | CROSS_UP | STOCH_K → STOCH_D | %K **%D 상향 교차** |
|
||||
| ADX 중앙선 상향 돌파 | buy | ADX | CROSS_UP | ADX_VALUE → ADX_25 | ADX **25선 상향 돌파** |
|
||||
| 9일 신고가 돌파 | buy | NEW_HIGH | CROSS_UP | CLOSE → NH_PRIOR_9 | 종가 **직전 9봉 최고가** 교차 |
|
||||
| 20일 신고가 돌파 | buy | NEW_HIGH | CROSS_UP | CLOSE → NH_PRIOR_20 | 종가 **직전 20봉 최고가** 교차 |
|
||||
| 9일 신저가 이탈 | sell | NEW_LOW | CROSS_DOWN | CLOSE → NL_PRIOR_9 | 종가 **직전 9봉 최저가** 하향 교차 |
|
||||
| 20일 신저가 이탈 | sell | NEW_LOW | CROSS_DOWN | CLOSE → NL_PRIOR_20 | 종가 **직전 20봉 최저가** 하향 교차 |
|
||||
|
||||
**NH_PRIOR_n / NL_PRIOR_n:** `max(high[t−n..t−1])` / `min(low[t−n..t−1])` — **현재 봉 high/low 미포함**.
|
||||
|
||||
### 4.2 복합 내장 템플릿 (`kind: 'composite'`) — 12종
|
||||
|
||||
`build()` 함수가 **AND/OR LogicNode**를 반환. 적용 시 **해당 탭 전체**가 이 트리로 교체됩니다.
|
||||
|
||||
#### 4.2.1 9·20일 신고/신저가
|
||||
|
||||
| 라벨 | signal | 트리 | 기본(relaxed) 시그널 |
|
||||
|------|--------|------|----------------------|
|
||||
| 9·20일 신고가 매수 | buy | `AND` | `CLOSE CROSS_UP NH_PRIOR_20` **만** |
|
||||
| 9·20일 신저가 매도 | sell | `AND` | `CLOSE CROSS_DOWN NL_PRIOR_20` **만** |
|
||||
|
||||
**필터 강도별 추가 leaf (편집기에서 변경 가능):**
|
||||
|
||||
| 강도 | 매수 추가 | 매도 추가 |
|
||||
|------|-----------|-----------|
|
||||
| **strict** | 9일 CROSS + ADX≥25 + 거래량>MA + 고가≥20일선 | ADX≥20 + 거래량>MA |
|
||||
| **balanced** | ADX≥18 | ADX≥15 |
|
||||
| **relaxed** | (없음) | (없음) |
|
||||
|
||||
#### 4.2.2 33변곡
|
||||
|
||||
| 라벨 | signal | 트리 | 기본(relaxed) 시그널 |
|
||||
|------|--------|------|----------------------|
|
||||
| 33변곡 매수 | buy | `AND` | EMA33 **1봉 SLOPE_UP** + CLOSE **CROSS_UP** CLOSE_MAX_33 |
|
||||
| 33변곡 매도 | sell | `OR` | CLOSE **CROSS_DOWN** CLOSE_MIN_33 **만** (EMA 분기 생략) |
|
||||
|
||||
**balanced/strict** 시 매수: EMA 2봉 변곡, 종가>EMA33, ADX·거래량 필터 추가.
|
||||
**매도 strict/balanced:** EMA 하락+종가<EMA33 **OR** 채널 이탈.
|
||||
|
||||
#### 4.2.3 돈천·터틀·일목 (Donchian / Ichimoku)
|
||||
|
||||
| 라벨 | signal | DSL 핵심 | 시그널 |
|
||||
|------|--------|----------|--------|
|
||||
| 돈천 채널(20일) 매수 | buy | DONCHIAN CROSS_UP CLOSE → DC_UPPER_20 | 20일 채널 **상단** 돌파 |
|
||||
| 돈천 채널(20일) 매도 | sell | DONCHIAN CROSS_DOWN CLOSE → DC_LOWER_20 | 20일 채널 **하단** 이탈 |
|
||||
| 터틀 S1 매수 | buy | DC_UPPER_**20** CROSS_UP | 20일 **최고가** 돌파 진입 |
|
||||
| 터틀 S1 청산 | sell | DC_LOWER_**10** CROSS_DOWN | 10일 **최저가** 이탈 청산 |
|
||||
| 터틀 S2 매수 | buy | DC_UPPER_**55** CROSS_UP | 55일 최고가 돌파 |
|
||||
| 터틀 S2 청산 | sell | DC_LOWER_**20** CROSS_DOWN | 20일 최저가 이탈 |
|
||||
| 일목 전환/기준선 매수 | buy | ICHIMOKU CROSS_UP CONVERSION → BASE | **전환·기준 골든크로스** |
|
||||
| 일목 전환/기준선 매도 | sell | ICHIMOKU CROSS_DOWN CONVERSION → BASE | **데드크로스** |
|
||||
|
||||
> **참고:** 터틀 S1 매수 = 돈천 20일 매수와 **동일 DSL**. S1 청산만 10일 하단으로 다름.
|
||||
|
||||
---
|
||||
|
||||
## 5. 복합지표 템플릿 (~58종, 배지「복합지표」)
|
||||
|
||||
**출처:** `buildCompositePaletteTemplateRows` — `DEFAULT_COMPOSITE_ITEMS` (`strategyPaletteStorage.ts`)
|
||||
|
||||
지표 탭 **복합** 서브탭과 **동일 항목**이 템플릿 탭에도 노출됩니다.
|
||||
적용 시 **현재 탭 조건에 AND 병합** (`applyPaletteItem`).
|
||||
|
||||
### 5.1 Stoch 과열×보조 (1종)
|
||||
|
||||
| 라벨 | signal | 트리 | 시그널 |
|
||||
|------|--------|------|--------|
|
||||
| Stoch 과열×보조 | buy | `OR` | `(Stoch≥80 ∧ 보조 CROSS_UP 중앙) ∨ (보조≥중앙 ∧ Stoch CROSS_UP 80)` |
|
||||
|
||||
기본 보조: **CCI**. 편집기에서 RSI·ADX 등 변경 가능.
|
||||
|
||||
### 5.2 9·20 신고/신저 · 33변곡 · 일목×BB (6종)
|
||||
|
||||
내장 §4.2와 **동일 빌더**. 템플릿 탭에서는 **추가(병합)** 방식만 다름.
|
||||
|
||||
| 항목 | 빌더 | 기본 시그널 요약 |
|
||||
|------|------|------------------|
|
||||
| 9·20일 신고가 매수 | `buildNewHigh920BuyTree` | 20일 신고가 CROSS + (필터) |
|
||||
| 9·20일 신저가 매도 | `buildNewLow920SellTree` | 20일 신저가 CROSS + (필터) |
|
||||
| 33변곡 매수/매도 | `buildInflection33Tree` | EMA33 변곡 + 33봉 종가 채널 |
|
||||
| 일목×BB 매수 | `buildIchimokuBbBuyTree` | **26봉 전** 후행 vs **26봉 전** BB **상한** CROSS_UP |
|
||||
| 일목×BB 매도 | `buildIchimokuBbSellTree` | **26봉 전** 후행 vs **26봉 전** BB **하한** CROSS_DOWN |
|
||||
|
||||
### 5.3 일목 상황별 (23종)
|
||||
|
||||
**빌더:** `ichimokuSituationStrategy.ts` — `BUILDERS[situationId][mode]`
|
||||
|
||||
| ID | 라벨(요) | signal | 논리 | leaf 조건 (동일 봉 AND/OR) |
|
||||
|----|----------|--------|------|---------------------------|
|
||||
| GOLDEN_CROSS | 골든크로스 매수 | buy | AND | 전환 **CROSS_UP** 기준 |
|
||||
| BASE_BREAKUP | 기준선 돌파 매수 | buy | AND | 종가 **CROSS_UP** 기준선 |
|
||||
| CLOUD_BREAKUP | 구름 돌파 매수 | buy | AND | **CLOUD_BREAK_UP** |
|
||||
| LAGGING_BULL | 후행 호전 매수 | buy | AND | 후행 **>** 26봉 전 종가 |
|
||||
| CLOUD_SUPPORT | 구름 지지 매수 | buy | AND | 종가>선행1 **且** 종가>기준선 |
|
||||
| HOJEON | 호전(정배열) 매수 | buy | AND | 전환>기준 + 구름 위 + 양운 + 후행>종가 |
|
||||
| DEAD_CROSS | 데드크로스 매도 | sell | OR | 전환 **CROSS_DOWN** 기준 |
|
||||
| BASE_BREAKDOWN | 기준선 이탈 매도 | sell | OR | 종가 **CROSS_DOWN** 기준선 |
|
||||
| CLOUD_BREAKDOWN | 구름 이탈 매도 | sell | OR | **CLOUD_BREAK_DOWN** |
|
||||
| LAGGING_BEAR | 후행 역전 매도 | sell | OR | 후행 **<** 26봉 전 종가 |
|
||||
| CLOUD_RESISTANCE | 구름 저항 매도 | sell | AND | 종가<선행1 **且** 종가<기준선 |
|
||||
| SANYAKU_REVERSE | 삼역역전 매도 | sell | AND | 전환<기준 + 구름 아래 + 후행 역전 |
|
||||
| GYEOKJEON | 역전(역배열) 매도 | sell | AND | 전환<기준 + 구름 아래 + 음운 + 후행 역전 |
|
||||
| EW1 | 1파 매수 | buy | AND | 종가>기준 CROSS + 전환>기준 |
|
||||
| EW2 | 2파 매수/손절 | buy/sell | AND/OR | 매수: 기준·선행2 지지+전환>기준 / 매도: 기준 이탈 OR 구름 이탈 |
|
||||
| EW3 | 3파 매수 | buy | AND | 전환>기준 + 구름 위 + 후행 + 양운 |
|
||||
| EW4 | 4파 재매수/익절 | buy/sell | AND/OR | 매수: 선행1·전환 지지 / 매도: 기준·구름 이탈 |
|
||||
| EW5 | 5파 분할매도 | sell | OR | 종가<전환 OR 데드 OR 구름 이탈 |
|
||||
| EW_A | A파 매도 | sell | OR | 기준·데드 이탈 |
|
||||
| EW_B | B파 매도 | sell | AND | 기준 아래 + 구름 아래 + 전환<기준 |
|
||||
| EW_C | C파 매도 | sell | AND | 구름 이탈 + 구름 아래 + 전환<기준 + 후행 역전 |
|
||||
|
||||
### 5.4 추천 안정형 (18종)
|
||||
|
||||
**빌더:** `stableStrategyPairs.ts` — 기본 필터 **`relaxed`**
|
||||
|
||||
| strategyId | 매수 핵심 | 매도 핵심 | relaxed 시그널 |
|
||||
|------------|-----------|-----------|----------------|
|
||||
| **TURTLE_S1** | DC_UPPER_20 CROSS_UP | DC_LOWER_10 CROSS_DOWN | §4.2.3과 동일 |
|
||||
| **TURTLE_S2** | DC_UPPER_55 CROSS_UP | DC_LOWER_20 CROSS_DOWN | 장기 터틀 |
|
||||
| **DONCHIAN_20** | DC_UPPER_20 CROSS_UP | DC_LOWER_20 CROSS_DOWN | 20일 채널 |
|
||||
| **MA_TREND** | MA20 CROSS_UP MA60 | MA20 CROSS_DOWN MA60 **OR** 종가<MA20 | 이평 추세 |
|
||||
| **ICHIMOKU_TREND** | 전환/기준 CROSS_UP | CROSS_DOWN **OR** 구름 아래(balanced+) | 일목 추세 |
|
||||
| **ICHIMOKU_SANYAKU** | 구름 돌파 + (종가>전환 등) | 전환/기준/구름 **하향 CROSS** OR | 삼역호전 |
|
||||
| **ICHIMOKU_PERFECT** | 전환>기준 + 구름 위 | 전환<기준 + 구름 아래 | 5원소(양운·후행은 strict) |
|
||||
| **MACD_MOMENTUM** | MACD CROSS_UP | MACD CROSS_DOWN **OR** 종가<EMA60 | 모멘텀 |
|
||||
| **BOLLINGER_RANGE** | RSI≤30 + ADX약 + 하단밴드 CROSS_UP | RSI≥70 **OR** 상단밴드 CROSS_UP | 박스권 |
|
||||
|
||||
**필터 강도 (strict/balanced) 추가 예:**
|
||||
|
||||
| 전략 | strict 추가 |
|
||||
|------|-------------|
|
||||
| TURTLE / DONCHIAN 매수 | ADX≥25 + 거래량>MA |
|
||||
| MA_TREND 매수 | ADX≥25 + 거래량>MA |
|
||||
| ICHIMOKU_SANYAKU 매수 | EXISTS_IN 윈도우(12~26봉) + MA60·거래량 |
|
||||
| MACD_MOMENTUM 매수 | EMA60 위 + ADX·거래량 |
|
||||
|
||||
### 5.5 동일 지표 2기간 교차 (10종)
|
||||
|
||||
**적용:** `makeNode(..., { composite: true })` — 현재 **매수/매도 탭**에 따라 `CROSS_UP` / `CROSS_DOWN`.
|
||||
|
||||
| 칩 | left → right (기본 기간) | 시그널 |
|
||||
|----|--------------------------|--------|
|
||||
| 돈치안 | DC_UPPER_9 CROSS DC_UPPER_20 | **단기 채널상단**이 **장기 상단** 돌파 |
|
||||
| RSI | RSI_VALUE_9 CROSS RSI_VALUE_18 | 단기 RSI **장기** RSI 교차 |
|
||||
| CCI | CCI_VALUE_9 CROSS CCI_VALUE_26 | 동일 패턴 |
|
||||
| ADX | ADX_VALUE_14 CROSS ADX_VALUE_28 | 동일 |
|
||||
| Williams %R | WILLIAMS_R 14 vs 28 | 동일 |
|
||||
| TRIX | TRIX 12 vs 24 | 동일 |
|
||||
| VR | VR 26 vs 52 | 동일 |
|
||||
| 심리도 / 투자심리도 | 설정 기간 ×2 | 동일 |
|
||||
| 이격도 | dispShort vs dispMid | 동일 |
|
||||
|
||||
기간은 **보조지표 설정**(`DefType`)에서 상속.
|
||||
|
||||
---
|
||||
|
||||
## 6. 횡보지표 템플릿 (52종, 배지「횡보지표」)
|
||||
|
||||
**출처:** `buildSidewaysPaletteTemplateRows` — `DEFAULT_SIDEWAYS_FILTER_ITEMS`
|
||||
|
||||
적용: `buildSidewaysFilterLogic(templateId, mode, def)` → **현재 탭에 AND 병합**.
|
||||
|
||||
| mode | LogicNode | 시그널 해석 |
|
||||
|------|-----------|-------------|
|
||||
| **include** | 필터 leaf 그대로 | **횡보일 때** true — 횡보 구간 **포함** 필터 |
|
||||
| **exclude** | `NOT(필터 leaf)` | **추세일 때** true — 횡보 **제외** |
|
||||
|
||||
**템플릿 탭 signal 표시:** include → **buy**, exclude → **sell** (UI 라벨용; 실제로는 **필터 노드**이며 단독 매매 시그널이 아님).
|
||||
|
||||
### 6.1 카테고리별 시그널 조건
|
||||
|
||||
| 카테고리 | 대표 템플릿 | true = 횡보(include) |
|
||||
|----------|-------------|----------------------|
|
||||
| **ADX** | ADX < 25 | ADX **< 25** (비추세) |
|
||||
| | ADX ≥ 25 (exclude) | 추세 — NOT으로 **횡보 제외** |
|
||||
| **볼린저** | BB 수축 | 상·하단 간격 **DIFF_LT** (Squeeze) |
|
||||
| | 종가 ≈ 중심 | 종가 **≈** MIDDLE_BAND |
|
||||
| **MA** | MA5≈MA20 | MA 스프레드 **DIFF_LT** |
|
||||
| **일목** | 구름 안 | **IN_CLOUD** |
|
||||
| **Donchian** | 9/20일 채널 좁음 | DC_UPPER − DC_LOWER **DIFF_LT** |
|
||||
| **오실레이터** | RSI 40~60 | RSI **GTE 40 ∧ LTE 60** |
|
||||
| **cycle** | Stoch 20봉 침체 | **LOWEST_LTE** %K ≤ 20 (lookback 20) |
|
||||
| **DMI** | +DI ≈ −DI | PDI−MDI **DIFF_LT** |
|
||||
| **volume** | 거래량 < MA | VOLUME **LT** VOLUME_MA |
|
||||
| **combo** | ADX+구름+RSI | 위 조건 **AND** (3중 횡보) |
|
||||
|
||||
**실전 조합 예:**
|
||||
|
||||
```text
|
||||
매수 = AND( MA 골든크로스, NOT「ADX<25」 ) ← exclude ADX≥25 = 추세에서만 매수
|
||||
매수 = AND( RSI 과매도, 「RSI 40~60」 ) ← include = 횡보 확인 (드묾)
|
||||
```
|
||||
|
||||
전체 52항목 ID·설명은 `sidewaysFilterPalette.ts` `DEFAULT_SIDEWAYS_FILTER_ITEMS` 참고.
|
||||
|
||||
---
|
||||
|
||||
## 7. 내 템플릿 (사용자 저장)
|
||||
|
||||
**저장:** 현재 **매수 또는 매도** 탭의 `EditorConditionState` + `orphans` 스냅샷.
|
||||
|
||||
| 필드 | 의미 |
|
||||
|------|------|
|
||||
| `signal` | buy / sell — 적용 시 **해당 탭만** 채움 |
|
||||
| `editorState.root` | LogicNode 트리 |
|
||||
| `orphans` | 캔버스 고아 노드 |
|
||||
|
||||
**시그널:** 저장 시점 DSL 그대로 `StrategyDslToTa4jAdapter` 평가.
|
||||
내용은 사용자가 만든 **AND/OR/CONDITION** 조합이므로, leaf별로 §3 공통 규칙 적용.
|
||||
|
||||
**적용:** 반대 탭·고아 초기화 후 **전체 교체** (`applyUserTemplate`).
|
||||
|
||||
---
|
||||
|
||||
## 8. 내장 vs 복합지표 중복
|
||||
|
||||
다음 항목은 **내장 템플릿**과 **복합지표 템플릿**에 **둘 다** 나타날 수 있습니다.
|
||||
|
||||
| 항목 | 차이 |
|
||||
|------|------|
|
||||
| 9·20 신고/신저, 33변곡, 터틀, 돈천, 일목 전환/기준 | **DSL 동일** |
|
||||
| 적용 방식 | 내장 = **탭 교체** / 복합 = **AND 추가** |
|
||||
|
||||
숨김: 템플릿 삭제 → `hiddenBuiltinTemplateIds`에 key 저장 (복원 가능).
|
||||
|
||||
---
|
||||
|
||||
## 9. 차트까지의 평가 경로
|
||||
|
||||
```text
|
||||
[템플릿 적용]
|
||||
↓
|
||||
buyConditionJson / sellConditionJson (전략 저장)
|
||||
↓
|
||||
StrategyDslToTa4jAdapter.toRule(buy) / toRule(sell)
|
||||
↓
|
||||
BacktestingService · StrategyEvaluationChart · LiveConditionStatusService
|
||||
↓
|
||||
isSatisfied(i) === true → 봉 i에 BUY/SELL 마커
|
||||
```
|
||||
|
||||
| 기능 | 템플릿 사용처 |
|
||||
|------|---------------|
|
||||
| **전략편집기** | 템플릿 탭 적용 → 편집 후 전략 저장 |
|
||||
| **전략평가** | 템플릿 선택 → `[템플릿] {라벨}` 임시 전략 생성 |
|
||||
| **실시간** | 저장된 전략 DSL → 분봉 마감 시 TriggerBranch 평가 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 템플릿별 시그널 빠른 참조
|
||||
|
||||
| 템플릿 유형 | 대표 예 | 한 줄 시그널 |
|
||||
|-------------|---------|--------------|
|
||||
| 단순 내장 | MA 골든크로스 | 이평 **교차** 1봉 |
|
||||
| 단순 내장 | RSI 과매도 | RSI **≤30** 수준 |
|
||||
| 단순 내장 | 20일 신고가 | **직전 20봉 고가** CROSS_UP |
|
||||
| 복합 내장 | 9·20 신고가 | 20일 CROSS + (선택 필터) AND |
|
||||
| 복합 내장 | 터틀 S1 | 20일 돌파 매수 / 10일 이탈 매도 |
|
||||
| 복합지표 | Stoch×CCI | 과열+보조 **OR** 조합 |
|
||||
| 복합지표 | 일목 EW3 | 4~5 일목 leaf **AND** |
|
||||
| 복합지표 | MA 추세 | MA20/60 + (ADX) |
|
||||
| 복합지표 | RSI 2기간 | RSI 단기 **장기** CROSS |
|
||||
| 횡보 | ADX<25 include | **횡보 필터** (타 조건과 AND) |
|
||||
| 횡보 | ADX≥25 exclude | **추세만** 통과 NOT |
|
||||
| 내 템플릿 | (사용자) | 저장 DSL 그대로 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 코드 인덱스
|
||||
|
||||
| 영역 | 경로 |
|
||||
|------|------|
|
||||
| 템플릿 목록 병합 | `frontend/src/utils/strategyTemplateList.ts` |
|
||||
| 내장 28종 정의 | `frontend/src/utils/strategyPresets.ts` |
|
||||
| 복합·횡보 행 변환 | `frontend/src/utils/strategyEditorTemplatePaletteRows.ts` |
|
||||
| 템플릿 UI | `frontend/src/components/strategyEditor/TemplatePaletteTab.tsx` |
|
||||
| 적용 (편집기) | `frontend/src/components/StrategyEditorPage.tsx` — `handleTemplateApply` |
|
||||
| 적용 (전략평가) | `frontend/src/utils/strategyEvaluationTemplateApply.ts` |
|
||||
| 9·20 / 33변곡 | `priceExtremeBreakoutPair.ts`, `inflection33Strategy.ts` |
|
||||
| Stoch×보조 | `stochOverboughtPair.ts` |
|
||||
| 일목×BB | `ichimokuBbStrategy.ts` |
|
||||
| 일목 23상황 | `ichimokuSituationStrategy.ts` |
|
||||
| 안정형 18 | `stableStrategyPairs.ts` |
|
||||
| 2기간 교차 | `compositeIndicators.ts` |
|
||||
| 횡보 52 | `sidewaysFilterPalette.ts` |
|
||||
| 사용자 템플릿 | `strategyTemplateStorage.ts` |
|
||||
| Rule 변환 | `backend/.../StrategyDslToTa4jAdapter.java` |
|
||||
|
||||
---
|
||||
|
||||
*문서 버전: 2026-06 — goldenChart 전략편집기 템플릿 탭·strategyPresets 기준*
|
||||
Reference in New Issue
Block a user