전략편집기 목록방식 오류 수정
This commit is contained in:
@@ -34,7 +34,8 @@ public final class IndicatorHlineResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (override) {
|
// thresholdOverride·targetValue 는 임계값(rightField) 전용 — RSI_VALUE 등 본선 필드에는 적용하지 않음
|
||||||
|
if (override && isThresholdRoleField(field)) {
|
||||||
if (cond.has("targetValue") && !cond.path("targetValue").isNull()) {
|
if (cond.has("targetValue") && !cond.path("targetValue").isNull()) {
|
||||||
return cond.path("targetValue").asDouble();
|
return cond.path("targetValue").asDouble();
|
||||||
}
|
}
|
||||||
@@ -49,6 +50,16 @@ public final class IndicatorHlineResolver {
|
|||||||
return legacyNumericField(field);
|
return legacyNumericField(field);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** HL_*·K_*·OVERBOUGHT_70 등 임계값 필드 — RSI_VALUE 등 지표 본선은 제외 */
|
||||||
|
private static boolean isThresholdRoleField(String field) {
|
||||||
|
if (field == null || field.isBlank()) return false;
|
||||||
|
if (field.startsWith("K_")) return true;
|
||||||
|
if ("HL_OVER".equals(field) || "HL_MID".equals(field) || "HL_UNDER".equals(field)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return legacyNumericField(field) != null;
|
||||||
|
}
|
||||||
|
|
||||||
private static Double fromHlineRole(
|
private static Double fromHlineRole(
|
||||||
String role,
|
String role,
|
||||||
String dslIndicatorType,
|
String dslIndicatorType,
|
||||||
|
|||||||
@@ -203,6 +203,23 @@ public class LiveConditionStatusService {
|
|||||||
return bestIdx;
|
return bestIdx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String probeEntryRule(Rule entryRule, JsonNode buyDsl,
|
||||||
|
StrategyDslToTa4jAdapter.RuleBuildContext ruleCtx,
|
||||||
|
int startIdx) {
|
||||||
|
JsonNode cond = findFirstCondition(buyDsl);
|
||||||
|
if (cond == null) return "no-cond";
|
||||||
|
BarSeries series = ruleCtx.primarySeries();
|
||||||
|
int probeIdx = Math.min(series.getEndIndex(), startIdx + 13);
|
||||||
|
Double left = adapter.readConditionFieldValue(cond, ruleCtx, probeIdx, true);
|
||||||
|
Double right = adapter.readConditionFieldValue(cond, ruleCtx, probeIdx, false);
|
||||||
|
int hits = 0;
|
||||||
|
for (int i = startIdx; i <= series.getEndIndex(); i++) {
|
||||||
|
if (entryRule.isSatisfied(i, null)) hits++;
|
||||||
|
}
|
||||||
|
return String.format("rule=%s hits=%d probe@%d L=%s R=%s",
|
||||||
|
entryRule.getClass().getSimpleName(), hits, probeIdx, left, right);
|
||||||
|
}
|
||||||
|
|
||||||
/** scan 0건 시 DSL 임계값 요약 로그 */
|
/** scan 0건 시 DSL 임계값 요약 로그 */
|
||||||
private static String summarizeBuyCondition(JsonNode buyDsl) {
|
private static String summarizeBuyCondition(JsonNode buyDsl) {
|
||||||
if (buyDsl == null || buyDsl.isNull()) return "null";
|
if (buyDsl == null || buyDsl.isNull()) return "null";
|
||||||
@@ -817,9 +834,10 @@ public class LiveConditionStatusService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (signals.isEmpty()) {
|
if (signals.isEmpty()) {
|
||||||
log.info("[LiveCondition:scan] 0 signals market={} strategy={} tf={} bars={} eval={}..{} cond={}",
|
log.info("[LiveCondition:scan] 0 signals market={} strategy={} tf={} bars={} eval={}..{} cond={} probe={}",
|
||||||
market, strategyId, primaryTf, primarySeries.getBarCount(), startIdx,
|
market, strategyId, primaryTf, primarySeries.getBarCount(), startIdx,
|
||||||
primarySeries.getEndIndex(), summarizeBuyCondition(buyDsl));
|
primarySeries.getEndIndex(), summarizeBuyCondition(buyDsl),
|
||||||
|
probeEntryRule(entryRule, buyDsl, ruleCtx, startIdx));
|
||||||
} else {
|
} else {
|
||||||
log.debug("[LiveCondition:scan] market={} strategy={} tf={} buy={} sell={}",
|
log.debug("[LiveCondition:scan] market={} strategy={} tf={} buy={} sell={}",
|
||||||
market, strategyId, primaryTf, buyHits, sellHits);
|
market, strategyId, primaryTf, buyHits, sellHits);
|
||||||
|
|||||||
@@ -236,7 +236,18 @@ public class StrategyDslToTa4jAdapter {
|
|||||||
public Double readConditionFieldValue(JsonNode cond, BarSeries series,
|
public Double readConditionFieldValue(JsonNode cond, BarSeries series,
|
||||||
Map<String, Map<String, Object>> params,
|
Map<String, Map<String, Object>> params,
|
||||||
int index, boolean leftSide) {
|
int index, boolean leftSide) {
|
||||||
if (cond == null || cond.isNull() || series == null || index < 0) return null;
|
RuleBuildContext ctx = new RuleBuildContext(series, params != null ? params : Map.of(),
|
||||||
|
Map.of(), null, null, false, Map.of());
|
||||||
|
return readConditionFieldValue(cond, ctx, index, leftSide);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** scan·진단 — Rule 빌드와 동일 RuleBuildContext 사용 */
|
||||||
|
public Double readConditionFieldValue(JsonNode cond, RuleBuildContext ctx,
|
||||||
|
int index, boolean leftSide) {
|
||||||
|
if (cond == null || cond.isNull() || ctx == null || ctx.primarySeries() == null || index < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
BarSeries series = ctx.primarySeries();
|
||||||
if (index >= series.getBarCount()) return null;
|
if (index >= series.getBarCount()) return null;
|
||||||
|
|
||||||
String field = leftSide
|
String field = leftSide
|
||||||
@@ -251,13 +262,11 @@ public class StrategyDslToTa4jAdapter {
|
|||||||
int sidePeriod = leftSide ? leftPeriod : rightPeriod;
|
int sidePeriod = leftSide ? leftPeriod : rightPeriod;
|
||||||
|
|
||||||
String registryKey = toRegistryKey(indType);
|
String registryKey = toRegistryKey(indType);
|
||||||
Map<String, Object> indParams = params != null
|
Map<String, Object> indParams = ctx.indicatorParams() != null
|
||||||
? params.getOrDefault(registryKey, Map.of())
|
? ctx.indicatorParams().getOrDefault(registryKey, Map.of())
|
||||||
: Map.of();
|
: Map.of();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
RuleBuildContext ctx = new RuleBuildContext(series, params != null ? params : Map.of(),
|
|
||||||
Map.of(), null, null, false, Map.of());
|
|
||||||
Indicator<Num> ind = resolveField(field, indType, indParams, series, condPeriod, sidePeriod, cond, ctx);
|
Indicator<Num> ind = resolveField(field, indType, indParams, series, condPeriod, sidePeriod, cond, ctx);
|
||||||
Num v = ind.getValue(index);
|
Num v = ind.getValue(index);
|
||||||
return v != null ? v.doubleValue() : null;
|
return v != null ? v.doubleValue() : null;
|
||||||
@@ -762,6 +771,11 @@ public class StrategyDslToTa4jAdapter {
|
|||||||
if (field == null || field.isBlank() || field.equals("NONE")) {
|
if (field == null || field.isBlank() || field.equals("NONE")) {
|
||||||
return new ConstantIndicator<>(s, s.numFactory().numOf(0));
|
return new ConstantIndicator<>(s, s.numFactory().numOf(0));
|
||||||
}
|
}
|
||||||
|
// K_* 직접입력 — 상수 임계값 (HL_*·지표 필드 해석보다 우선)
|
||||||
|
if (field.startsWith("K_")) {
|
||||||
|
Indicator<Num> kConst = resolveDirectKConstant(field, indType, cond, ctx, s);
|
||||||
|
if (kConst != null) return kConst;
|
||||||
|
}
|
||||||
return switch (field) {
|
return switch (field) {
|
||||||
case "CLOSE_PRICE" -> new ClosePriceIndicator(s);
|
case "CLOSE_PRICE" -> new ClosePriceIndicator(s);
|
||||||
case "OPEN_PRICE" -> new OpenPriceIndicator(s);
|
case "OPEN_PRICE" -> new OpenPriceIndicator(s);
|
||||||
@@ -792,6 +806,20 @@ public class StrategyDslToTa4jAdapter {
|
|||||||
new RuleBuildContext(s, Map.of(), Map.of(), null, null, false, Map.of()));
|
new RuleBuildContext(s, Map.of(), Map.of(), null, null, false, Map.of()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** K_55 등 직접입력 임계 — ConstantIndicator 또는 null(폴백) */
|
||||||
|
private Indicator<Num> resolveDirectKConstant(String field, String indType,
|
||||||
|
JsonNode cond, RuleBuildContext ctx,
|
||||||
|
BarSeries s) {
|
||||||
|
Double val = IndicatorHlineResolver.resolveThresholdField(
|
||||||
|
cond, field, indType, ctx != null ? ctx.indicatorVisual() : Map.of());
|
||||||
|
if (val == null) {
|
||||||
|
double parsed = resolveConstantField(field);
|
||||||
|
if (!Double.isNaN(parsed)) val = parsed;
|
||||||
|
}
|
||||||
|
if (val == null) return null;
|
||||||
|
return new ConstantIndicator<>(s, s.numFactory().numOf(val));
|
||||||
|
}
|
||||||
|
|
||||||
private double resolveConstantField(String field) {
|
private double resolveConstantField(String field) {
|
||||||
// K_ 접두사: 프론트엔드가 hline 실제값으로 생성한 상수 필드
|
// K_ 접두사: 프론트엔드가 hline 실제값으로 생성한 상수 필드
|
||||||
// 예) K_60 → 60, K_-100 → -100, K_0 → 0
|
// 예) K_60 → 60, K_-100 → -100, K_0 → 0
|
||||||
|
|||||||
@@ -52,6 +52,17 @@ class IndicatorHlineResolverTest {
|
|||||||
assertEquals(48.0, val, 0.0001);
|
assertEquals(48.0, val, 0.0001);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rsiValue_withThresholdOverride_doesNotUseTargetValue() throws Exception {
|
||||||
|
var cond = MAPPER.readTree("""
|
||||||
|
{"thresholdOverride": true, "targetValue": 55}
|
||||||
|
""");
|
||||||
|
assertNull(IndicatorHlineResolver.resolveThresholdField(
|
||||||
|
cond, "RSI_VALUE", "RSI", Map.of()));
|
||||||
|
assertEquals(55.0, IndicatorHlineResolver.resolveThresholdField(
|
||||||
|
cond, "K_55", "RSI", Map.of()), 0.0001);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void blankField_returnsNull() {
|
void blankField_returnsNull() {
|
||||||
assertNull(IndicatorHlineResolver.resolveThresholdField(
|
assertNull(IndicatorHlineResolver.resolveThresholdField(
|
||||||
|
|||||||
@@ -31,6 +31,70 @@ class RsiChartScanIntegrationTest {
|
|||||||
normalizer = new StrategyDslTimeframeNormalizer(MAPPER);
|
normalizer = new StrategyDslTimeframeNormalizer(MAPPER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void chartScan_strategy195Exact_k55_vs_hlMid() throws Exception {
|
||||||
|
String chartTf = "3m";
|
||||||
|
List<OhlcvBar> bars = buildOscillatingOhlcvBars(300, chartTf);
|
||||||
|
BarSeries primarySeries = OhlcvBarSeriesSupport.buildSeries(bars, chartTf);
|
||||||
|
|
||||||
|
JsonNode buyDslRaw = MAPPER.readTree("""
|
||||||
|
{
|
||||||
|
"type": "TIMEFRAME",
|
||||||
|
"candleTypes": ["3m"],
|
||||||
|
"candleType": "3m",
|
||||||
|
"children": [{
|
||||||
|
"type": "CONDITION",
|
||||||
|
"condition": {
|
||||||
|
"indicatorType": "RSI",
|
||||||
|
"conditionType": "CROSS_UP",
|
||||||
|
"leftField": "RSI_VALUE",
|
||||||
|
"rightField": "K_55",
|
||||||
|
"targetValue": 55,
|
||||||
|
"thresholdOverride": true,
|
||||||
|
"valuePeriodOverride": false,
|
||||||
|
"leftCandleType": "3m",
|
||||||
|
"rightCandleType": "3m",
|
||||||
|
"candleRange": 1
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
JsonNode buyDsl = normalizer.remapForChartTimeframe(
|
||||||
|
StrategyConditionThresholdNormalizer.normalizeTree(buyDslRaw), chartTf);
|
||||||
|
Map<String, BarSeries> seriesOverrides = OhlcvBarSeriesSupport.buildSeriesOverrides(
|
||||||
|
primarySeries, chartTf, buyDsl, null);
|
||||||
|
Map<String, Map<String, Object>> params = Map.of(
|
||||||
|
"RSI", Map.of("length", 9, "maLength", 5, "maType", "SMA", "src", "close"));
|
||||||
|
|
||||||
|
StrategyDslToTa4jAdapter.RuleBuildContext ctx =
|
||||||
|
new StrategyDslToTa4jAdapter.RuleBuildContext(
|
||||||
|
primarySeries, params, Map.of(), "KRW-BTC", null, false, seriesOverrides);
|
||||||
|
Rule ruleK55 = adapter.toRule(buyDsl, ctx);
|
||||||
|
int hits55 = countHitsFrom(ruleK55, primarySeries, 100);
|
||||||
|
|
||||||
|
JsonNode buyHlMid = buyDsl.deepCopy();
|
||||||
|
JsonNode cond = buyHlMid.path("children").get(0).path("condition");
|
||||||
|
var condObj = (com.fasterxml.jackson.databind.node.ObjectNode) cond;
|
||||||
|
condObj.put("rightField", "HL_MID");
|
||||||
|
condObj.remove("targetValue");
|
||||||
|
condObj.put("thresholdOverride", false);
|
||||||
|
Rule ruleMid = adapter.toRule(buyHlMid, ctx);
|
||||||
|
int hitsMid = countHitsFrom(ruleMid, primarySeries, 100);
|
||||||
|
|
||||||
|
assertFalse(hits55 == 0, "strategy195-like K_55 should produce signals, got " + hits55);
|
||||||
|
assertTrue(hits55 <= hitsMid,
|
||||||
|
"K_55 hits=" + hits55 + " should be <= HL_MID hits=" + hitsMid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int countHitsFrom(Rule rule, BarSeries series, int startIdx) {
|
||||||
|
int hits = 0;
|
||||||
|
for (int i = startIdx; i <= series.getEndIndex(); i++) {
|
||||||
|
if (rule.isSatisfied(i, null)) hits++;
|
||||||
|
}
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void chartScan_rsiCrossUpK55_firesSignals() throws Exception {
|
void chartScan_rsiCrossUpK55_firesSignals() throws Exception {
|
||||||
String chartTf = "3m";
|
String chartTf = "3m";
|
||||||
|
|||||||
@@ -355,12 +355,18 @@ export default function StrategyEditorPage({
|
|||||||
const sellConditionRef = useRef(sellCondition);
|
const sellConditionRef = useRef(sellCondition);
|
||||||
const buyOrphansRef = useRef(buyOrphans);
|
const buyOrphansRef = useRef(buyOrphans);
|
||||||
const sellOrphansRef = useRef(sellOrphans);
|
const sellOrphansRef = useRef(sellOrphans);
|
||||||
|
const selectedIdRef = useRef(selectedId);
|
||||||
|
const stratNameRef = useRef(stratName);
|
||||||
|
const stratDescRef = useRef(stratDesc);
|
||||||
buyLayoutRef.current = buyLayout;
|
buyLayoutRef.current = buyLayout;
|
||||||
sellLayoutRef.current = sellLayout;
|
sellLayoutRef.current = sellLayout;
|
||||||
buyConditionRef.current = buyCondition;
|
buyConditionRef.current = buyCondition;
|
||||||
sellConditionRef.current = sellCondition;
|
sellConditionRef.current = sellCondition;
|
||||||
buyOrphansRef.current = buyOrphans;
|
buyOrphansRef.current = buyOrphans;
|
||||||
sellOrphansRef.current = sellOrphans;
|
sellOrphansRef.current = sellOrphans;
|
||||||
|
selectedIdRef.current = selectedId;
|
||||||
|
stratNameRef.current = stratName;
|
||||||
|
stratDescRef.current = stratDesc;
|
||||||
const layoutRevisionRef = useRef(0);
|
const layoutRevisionRef = useRef(0);
|
||||||
const strategyAutosaveTimerRef = useRef<number | null>(null);
|
const strategyAutosaveTimerRef = useRef<number | null>(null);
|
||||||
const layoutPersistReadyRef = useRef(false);
|
const layoutPersistReadyRef = useRef(false);
|
||||||
@@ -434,6 +440,8 @@ export default function StrategyEditorPage({
|
|||||||
[sellCondition, sellLayout],
|
[sellCondition, sellLayout],
|
||||||
);
|
);
|
||||||
const currentEditorState = signalTab === 'buy' ? buyEditorState : sellEditorState;
|
const currentEditorState = signalTab === 'buy' ? buyEditorState : sellEditorState;
|
||||||
|
const currentEditorStateRef = useRef(currentEditorState);
|
||||||
|
currentEditorStateRef.current = currentEditorState;
|
||||||
|
|
||||||
const selectedLogicNode = useMemo(() => {
|
const selectedLogicNode = useMemo(() => {
|
||||||
if (!selectedNodeId) return null;
|
if (!selectedNodeId) return null;
|
||||||
@@ -496,6 +504,38 @@ export default function StrategyEditorPage({
|
|||||||
: s));
|
: s));
|
||||||
}, [selectedId, stratName, stratDesc, buildCurrentFlowLayout]);
|
}, [selectedId, stratName, stratDesc, buildCurrentFlowLayout]);
|
||||||
|
|
||||||
|
/** 전략 전환·새 전략 전 — ref 스냅샷을 지정 id 로 저장 (selectedId 변경 후 빈 DSL 덮어쓰기 방지) */
|
||||||
|
const persistStrategySnapshot = useCallback(async (
|
||||||
|
strategyId: number,
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
) => {
|
||||||
|
const buy = toEditorState(buyConditionRef.current, buyLayoutRef.current);
|
||||||
|
const sell = toEditorState(sellConditionRef.current, sellLayoutRef.current);
|
||||||
|
const aligned = alignBuySellStartCandleTypesForSave(buy, sell);
|
||||||
|
const encodedBuy = encodeConditionForSave(aligned.buy);
|
||||||
|
const encodedSell = encodeConditionForSave(aligned.sell);
|
||||||
|
if (!encodedBuy && !encodedSell) return;
|
||||||
|
const flowLayout = buildCurrentFlowLayout();
|
||||||
|
const saved = await persistStrategyEditorState(
|
||||||
|
strategyId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
aligned.buy,
|
||||||
|
aligned.sell,
|
||||||
|
flowLayout,
|
||||||
|
);
|
||||||
|
setStrategies(prev => prev.map(s => s.id === strategyId
|
||||||
|
? {
|
||||||
|
...s,
|
||||||
|
buyCondition: encodedBuy,
|
||||||
|
sellCondition: encodedSell,
|
||||||
|
flowLayout,
|
||||||
|
updatedAt: saved?.updatedAt ?? s.updatedAt,
|
||||||
|
}
|
||||||
|
: s));
|
||||||
|
}, [buildCurrentFlowLayout]);
|
||||||
|
|
||||||
const flushStrategyPersist = useCallback(() => {
|
const flushStrategyPersist = useCallback(() => {
|
||||||
if (strategyAutosaveTimerRef.current != null) {
|
if (strategyAutosaveTimerRef.current != null) {
|
||||||
window.clearTimeout(strategyAutosaveTimerRef.current);
|
window.clearTimeout(strategyAutosaveTimerRef.current);
|
||||||
@@ -732,14 +772,19 @@ export default function StrategyEditorPage({
|
|||||||
};
|
};
|
||||||
}, [selectedNodeId, selectedLogicNode, stochPairForest, applyStochPairSecondary]);
|
}, [selectedNodeId, selectedLogicNode, stochPairForest, applyStochPairSecondary]);
|
||||||
|
|
||||||
const handleEditorStateChange = useCallback((next: EditorConditionState) => {
|
const handleEditorStateChange = useCallback((
|
||||||
setCurrentRoot(next.root);
|
next: EditorConditionState | ((prev: EditorConditionState) => EditorConditionState),
|
||||||
|
) => {
|
||||||
|
const resolved = typeof next === 'function'
|
||||||
|
? next(currentEditorStateRef.current)
|
||||||
|
: next;
|
||||||
|
setCurrentRoot(resolved.root);
|
||||||
setCurrentLayout(prev => ({
|
setCurrentLayout(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
startMeta: next.startMeta,
|
startMeta: resolved.startMeta,
|
||||||
extraStartIds: next.extraStartIds,
|
extraStartIds: resolved.extraStartIds,
|
||||||
extraRoots: next.extraRoots,
|
extraRoots: resolved.extraRoots,
|
||||||
startCombineOp: normalizeStartCombineOp(next.startCombineOp),
|
startCombineOp: normalizeStartCombineOp(resolved.startCombineOp),
|
||||||
}));
|
}));
|
||||||
scheduleStrategyPersist();
|
scheduleStrategyPersist();
|
||||||
}, [setCurrentRoot, setCurrentLayout, scheduleStrategyPersist]);
|
}, [setCurrentRoot, setCurrentLayout, scheduleStrategyPersist]);
|
||||||
@@ -784,21 +829,20 @@ export default function StrategyEditorPage({
|
|||||||
};
|
};
|
||||||
}, [signalTab, layoutSeedKey, buyLayout, sellLayout]);
|
}, [signalTab, layoutSeedKey, buyLayout, sellLayout]);
|
||||||
|
|
||||||
const persistStrategyToDbRef = useRef(persistStrategyToDb);
|
const persistStrategySnapshotRef = useRef(persistStrategySnapshot);
|
||||||
persistStrategyToDbRef.current = persistStrategyToDb;
|
persistStrategySnapshotRef.current = persistStrategySnapshot;
|
||||||
useEffect(() => {
|
useEffect(() => () => {
|
||||||
const id = selectedId;
|
|
||||||
const name = stratName;
|
|
||||||
return () => {
|
|
||||||
if (strategyAutosaveTimerRef.current != null) {
|
if (strategyAutosaveTimerRef.current != null) {
|
||||||
window.clearTimeout(strategyAutosaveTimerRef.current);
|
window.clearTimeout(strategyAutosaveTimerRef.current);
|
||||||
strategyAutosaveTimerRef.current = null;
|
strategyAutosaveTimerRef.current = null;
|
||||||
}
|
}
|
||||||
|
const id = selectedIdRef.current;
|
||||||
|
const name = stratNameRef.current;
|
||||||
|
const desc = stratDescRef.current;
|
||||||
if (id != null && name.trim()) {
|
if (id != null && name.trim()) {
|
||||||
void persistStrategyToDbRef.current().catch(() => { /* unmount flush */ });
|
void persistStrategySnapshotRef.current(id, name, desc).catch(() => { /* unmount flush */ });
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
}, [selectedId, stratName]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!layoutPersistReadyRef.current) {
|
if (!layoutPersistReadyRef.current) {
|
||||||
@@ -861,7 +905,24 @@ export default function StrategyEditorPage({
|
|||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const flushBeforeStrategySwitch = useCallback(() => {
|
||||||
|
if (editorMode === 'graph') layoutFlushRef.current?.();
|
||||||
|
if (strategyAutosaveTimerRef.current != null) {
|
||||||
|
window.clearTimeout(strategyAutosaveTimerRef.current);
|
||||||
|
strategyAutosaveTimerRef.current = null;
|
||||||
|
}
|
||||||
|
const prevId = selectedIdRef.current;
|
||||||
|
const prevName = stratNameRef.current;
|
||||||
|
const prevDesc = stratDescRef.current;
|
||||||
|
if (prevId != null && prevName.trim()) {
|
||||||
|
void persistStrategySnapshot(prevId, prevName, prevDesc).catch(() => { /* switch flush */ });
|
||||||
|
}
|
||||||
|
}, [editorMode, persistStrategySnapshot]);
|
||||||
|
|
||||||
const handleSelectStrategy = (s: StrategyDto) => {
|
const handleSelectStrategy = (s: StrategyDto) => {
|
||||||
|
if (selectedId !== s.id) {
|
||||||
|
flushBeforeStrategySwitch();
|
||||||
|
}
|
||||||
const buyDecoded = decodeConditionForEditor(s.buyCondition ?? null);
|
const buyDecoded = decodeConditionForEditor(s.buyCondition ?? null);
|
||||||
const sellDecoded = decodeConditionForEditor(s.sellCondition ?? null);
|
const sellDecoded = decodeConditionForEditor(s.sellCondition ?? null);
|
||||||
let stored = normalizeStrategyFlowLayout(s.flowLayout);
|
let stored = normalizeStrategyFlowLayout(s.flowLayout);
|
||||||
@@ -982,13 +1043,17 @@ export default function StrategyEditorPage({
|
|||||||
}, [variant, initialStrategyId, strategies]);
|
}, [variant, initialStrategyId, strategies]);
|
||||||
|
|
||||||
const handleNew = () => {
|
const handleNew = () => {
|
||||||
|
flushBeforeStrategySwitch();
|
||||||
setSelectedId(null);
|
setSelectedId(null);
|
||||||
setStratName('');
|
setStratName('');
|
||||||
setStratDesc('');
|
setStratDesc('');
|
||||||
setBuyCondition(null);
|
setBuyCondition(null);
|
||||||
setSellCondition(null);
|
setSellCondition(null);
|
||||||
|
setBuyOrphans([]);
|
||||||
|
setSellOrphans([]);
|
||||||
setSelectedNodeId(null);
|
setSelectedNodeId(null);
|
||||||
resetFlowLayout('draft', signalTab);
|
resetFlowLayout('draft', 'buy');
|
||||||
|
setSignalTab('buy');
|
||||||
};
|
};
|
||||||
|
|
||||||
const openSaveDialog = useCallback((mode: SaveDialogMode) => {
|
const openSaveDialog = useCallback((mode: SaveDialogMode) => {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
import StartTimeframeCheckboxes from './StartTimeframeCheckboxes';
|
import StartTimeframeCheckboxes from './StartTimeframeCheckboxes';
|
||||||
import LogicGateOpToggle from './LogicGateOpToggle';
|
import LogicGateOpToggle from './LogicGateOpToggle';
|
||||||
import { formatStartCandleTypesLabel } from '../../utils/strategyStartNodes';
|
import { formatStartCandleTypesLabel } from '../../utils/strategyStartNodes';
|
||||||
|
import { START_NODE_ID } from '../../utils/strategyFlowLayout';
|
||||||
import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage';
|
import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage';
|
||||||
import {
|
import {
|
||||||
findPaletteDropKeyAtPoint,
|
findPaletteDropKeyAtPoint,
|
||||||
@@ -293,7 +294,7 @@ interface StartSectionBlockProps {
|
|||||||
def: DefType;
|
def: DefType;
|
||||||
dragOverKey: string | null;
|
dragOverKey: string | null;
|
||||||
setDragOverKey: (key: string | null) => void;
|
setDragOverKey: (key: string | null) => void;
|
||||||
onRootChange: (root: LogicNode | null) => void;
|
onRootChange: (root: LogicNode | null | ((prev: LogicNode | null) => LogicNode | null)) => void;
|
||||||
onCandleTypesChange: (candleTypes: string[]) => void;
|
onCandleTypesChange: (candleTypes: string[]) => void;
|
||||||
onDeleteStart?: () => void;
|
onDeleteStart?: () => void;
|
||||||
onAddStart?: () => void;
|
onAddStart?: () => void;
|
||||||
@@ -324,16 +325,14 @@ function StartSectionBlock({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newNode = makeNode(data.type, data.value, signalTab, def, makeNodeOptionsFromPalette(data));
|
const newNode = makeNode(data.type, data.value, signalTab, def, makeNodeOptionsFromPalette(data));
|
||||||
if (!root) {
|
onRootChange(prev => {
|
||||||
onRootChange(newNode);
|
if (!prev) return newNode;
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!targetId) {
|
if (!targetId) {
|
||||||
onRootChange(mergeAtRoot(root, newNode, data.type === 'operator'));
|
return mergeAtRoot(prev, newNode, data.type === 'operator');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
onRootChange(addChild(root, targetId, newNode));
|
return addChild(prev, targetId, newNode);
|
||||||
}, [root, signalTab, def, onRootChange, onAddStart]);
|
});
|
||||||
|
}, [signalTab, def, onRootChange, onAddStart]);
|
||||||
|
|
||||||
const handleSectionDrop = (e: React.DragEvent) => {
|
const handleSectionDrop = (e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -408,7 +407,9 @@ interface Props {
|
|||||||
editorState: EditorConditionState;
|
editorState: EditorConditionState;
|
||||||
signalTab: 'buy' | 'sell';
|
signalTab: 'buy' | 'sell';
|
||||||
def: DefType;
|
def: DefType;
|
||||||
onEditorStateChange: (next: EditorConditionState) => void;
|
onEditorStateChange: (
|
||||||
|
next: EditorConditionState | ((prev: EditorConditionState) => EditorConditionState),
|
||||||
|
) => void;
|
||||||
onAddStart?: () => void;
|
onAddStart?: () => void;
|
||||||
onOrphansChange?: (nodes: LogicNode[]) => void;
|
onOrphansChange?: (nodes: LogicNode[]) => void;
|
||||||
orphans?: LogicNode[];
|
orphans?: LogicNode[];
|
||||||
@@ -438,24 +439,35 @@ export default function StrategyListEditor({
|
|||||||
onAddStart();
|
onAddStart();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onEditorStateChange(addExtraStartSection(editorState));
|
onEditorStateChange(prev => addExtraStartSection(prev));
|
||||||
}, [onAddStart, onEditorStateChange, editorState]);
|
}, [onAddStart, onEditorStateChange]);
|
||||||
|
|
||||||
const handleRootChange = useCallback((startId: string, root: LogicNode | null) => {
|
const handleRootChange = useCallback((
|
||||||
onEditorStateChange(updateBranchRoot(editorState, startId, root));
|
startId: string,
|
||||||
}, [editorState, onEditorStateChange]);
|
root: LogicNode | null | ((prev: LogicNode | null) => LogicNode | null),
|
||||||
|
) => {
|
||||||
|
onEditorStateChange(prev => {
|
||||||
|
const currentRoot = startId === START_NODE_ID
|
||||||
|
? prev.root
|
||||||
|
: (prev.extraRoots[startId] ?? null);
|
||||||
|
const resolved = typeof root === 'function' ? root(currentRoot) : root;
|
||||||
|
return updateBranchRoot(prev, startId, resolved);
|
||||||
|
});
|
||||||
|
}, [onEditorStateChange]);
|
||||||
|
|
||||||
const handleCandleTypesChange = useCallback((startId: string, candleTypes: string[]) => {
|
const handleCandleTypesChange = useCallback((startId: string, candleTypes: string[]) => {
|
||||||
onEditorStateChange(updateStartCandleTypes(editorState, startId, candleTypes));
|
onEditorStateChange(prev => updateStartCandleTypes(prev, startId, candleTypes));
|
||||||
}, [editorState, onEditorStateChange]);
|
}, [onEditorStateChange]);
|
||||||
|
|
||||||
const handleDeleteStart = useCallback((startId: string) => {
|
const handleDeleteStart = useCallback((startId: string) => {
|
||||||
const { state, orphanedNodes } = removeStartSection(editorState, startId);
|
onEditorStateChange(prev => {
|
||||||
onEditorStateChange(state);
|
const { state, orphanedNodes } = removeStartSection(prev, startId);
|
||||||
if (orphanedNodes.length && onOrphansChange) {
|
if (orphanedNodes.length && onOrphansChange) {
|
||||||
onOrphansChange([...orphans, ...orphanedNodes]);
|
onOrphansChange([...orphans, ...orphanedNodes]);
|
||||||
}
|
}
|
||||||
}, [editorState, onEditorStateChange, onOrphansChange, orphans]);
|
return state;
|
||||||
|
});
|
||||||
|
}, [onEditorStateChange, onOrphansChange, orphans]);
|
||||||
|
|
||||||
const resolvePaletteNode = useCallback((
|
const resolvePaletteNode = useCallback((
|
||||||
data: { type: string; value: string; label: string; composite?: boolean },
|
data: { type: string; value: string; label: string; composite?: boolean },
|
||||||
@@ -487,26 +499,25 @@ export default function StrategyListEditor({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const section = collectStartSections(editorState).find(s => s.startId === startId);
|
|
||||||
if (!section) return;
|
|
||||||
|
|
||||||
const newNode = resolvePaletteNode(data);
|
const newNode = resolvePaletteNode(data);
|
||||||
if (!newNode) return;
|
if (!newNode) return;
|
||||||
|
|
||||||
|
onEditorStateChange(prev => {
|
||||||
|
const section = collectStartSections(prev).find(s => s.startId === startId);
|
||||||
|
if (!section) return prev;
|
||||||
|
|
||||||
const root = section.root;
|
const root = section.root;
|
||||||
if (!root) {
|
if (!root) {
|
||||||
handleRootChange(startId, newNode);
|
return updateBranchRoot(prev, startId, newNode);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (!targetId) {
|
if (!targetId) {
|
||||||
handleRootChange(startId, mergeAtRoot(root, newNode, data.type === 'operator'));
|
return updateBranchRoot(prev, startId, mergeAtRoot(root, newNode, data.type === 'operator'));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
handleRootChange(startId, addChild(root, targetId, newNode));
|
return updateBranchRoot(prev, startId, addChild(root, targetId, newNode));
|
||||||
|
});
|
||||||
}, [
|
}, [
|
||||||
editorState,
|
|
||||||
handleAddStart,
|
handleAddStart,
|
||||||
handleRootChange,
|
onEditorStateChange,
|
||||||
resolvePaletteNode,
|
resolvePaletteNode,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def scan(sid):
|
||||||
|
bars = json.loads(urllib.request.urlopen(
|
||||||
|
"http://localhost:8080/api/candles/history?market=KRW-BTC&type=3m&count=300"
|
||||||
|
).read())
|
||||||
|
payload = {
|
||||||
|
"market": "KRW-BTC",
|
||||||
|
"strategyId": sid,
|
||||||
|
"timeframe": "3m",
|
||||||
|
"evaluationBarCount": 200,
|
||||||
|
"bars": [
|
||||||
|
{
|
||||||
|
"time": b["time"],
|
||||||
|
"open": b["open"],
|
||||||
|
"high": b["high"],
|
||||||
|
"low": b["low"],
|
||||||
|
"close": b["close"],
|
||||||
|
"volume": b["volume"],
|
||||||
|
}
|
||||||
|
for b in bars
|
||||||
|
],
|
||||||
|
}
|
||||||
|
req = urllib.request.Request(
|
||||||
|
"http://localhost:8080/api/strategy/live-conditions/scan-signals",
|
||||||
|
json.dumps(payload).encode(),
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-user-id": "1",
|
||||||
|
"x-device-id": "202b4020-2b4d-43f0-8baa-72513df2eb9e",
|
||||||
|
},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
return len(json.loads(urllib.request.urlopen(req).read()))
|
||||||
|
|
||||||
|
|
||||||
|
def get_json():
|
||||||
|
out = subprocess.check_output([
|
||||||
|
"docker", "exec", "gc-mysql", "mysql", "-ustock", "-panalyzer",
|
||||||
|
"stockAnalyzer", "-N", "-e", "SELECT buy_condition_json FROM gc_strategy WHERE id=195",
|
||||||
|
])
|
||||||
|
return out.decode().strip()
|
||||||
|
|
||||||
|
|
||||||
|
def set_json(js: str):
|
||||||
|
# pass via stdin to mysql to avoid escaping hell
|
||||||
|
sql = "UPDATE gc_strategy SET buy_condition_json=%s WHERE id=195"
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
["docker", "exec", "-i", "gc-mysql", "mysql", "-ustock", "-panalyzer", "stockAnalyzer"],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
proc.communicate(f"UPDATE gc_strategy SET buy_condition_json='{js.replace(chr(39), chr(39)+chr(39))}' WHERE id=195;".encode())
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
orig = get_json()
|
||||||
|
j = json.loads(orig)
|
||||||
|
variants = {}
|
||||||
|
|
||||||
|
c50 = json.loads(json.dumps(j))
|
||||||
|
c50["children"][0]["condition"]["rightField"] = "K_50"
|
||||||
|
c50["children"][0]["condition"]["targetValue"] = 50
|
||||||
|
c50["children"][0]["condition"]["thresholdOverride"] = True
|
||||||
|
variants["K_50"] = json.dumps(c50, ensure_ascii=False)
|
||||||
|
|
||||||
|
cH = json.loads(json.dumps(j))
|
||||||
|
cond = cH["children"][0]["condition"]
|
||||||
|
cond["rightField"] = "HL_MID"
|
||||||
|
cond.pop("targetValue", None)
|
||||||
|
cond["thresholdOverride"] = False
|
||||||
|
variants["HL_MID"] = json.dumps(cH, ensure_ascii=False)
|
||||||
|
|
||||||
|
c55n = json.loads(json.dumps(j))
|
||||||
|
c55n["children"][0]["condition"]["thresholdOverride"] = False
|
||||||
|
variants["K_55_no_override"] = json.dumps(c55n, ensure_ascii=False)
|
||||||
|
|
||||||
|
variants["K_55_orig"] = orig
|
||||||
|
|
||||||
|
for name, js in variants.items():
|
||||||
|
set_json(js)
|
||||||
|
n = scan(195)
|
||||||
|
print(f"{name}: {n} signals")
|
||||||
|
|
||||||
|
set_json(orig)
|
||||||
|
print("restored original")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user