From b96fc64c9163e8aa7cbb7f9896b2977c1af88dd2 Mon Sep 17 00:00:00 2001 From: Macbook Date: Thu, 18 Jun 2026 01:57:46 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=84=EB=9E=B5=ED=8E=B8=EC=A7=91=EA=B8=B0?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=EB=B0=A9=EC=8B=9D=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/IndicatorHlineResolver.java | 13 ++- .../service/LiveConditionStatusService.java | 22 +++- .../service/StrategyDslToTa4jAdapter.java | 38 +++++- .../service/IndicatorHlineResolverTest.java | 11 ++ .../service/RsiChartScanIntegrationTest.java | 64 ++++++++++ .../src/components/StrategyEditorPage.tsx | 109 ++++++++++++++---- .../strategyEditor/StrategyListEditor.tsx | 91 ++++++++------- scripts/diag-scan-195.py | 93 +++++++++++++++ 8 files changed, 371 insertions(+), 70 deletions(-) create mode 100644 scripts/diag-scan-195.py diff --git a/backend/src/main/java/com/goldenchart/service/IndicatorHlineResolver.java b/backend/src/main/java/com/goldenchart/service/IndicatorHlineResolver.java index 2eeca3b..dc1ea7c 100644 --- a/backend/src/main/java/com/goldenchart/service/IndicatorHlineResolver.java +++ b/backend/src/main/java/com/goldenchart/service/IndicatorHlineResolver.java @@ -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()) { return cond.path("targetValue").asDouble(); } @@ -49,6 +50,16 @@ public final class IndicatorHlineResolver { 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( String role, String dslIndicatorType, diff --git a/backend/src/main/java/com/goldenchart/service/LiveConditionStatusService.java b/backend/src/main/java/com/goldenchart/service/LiveConditionStatusService.java index 6fe2913..6bd7be4 100644 --- a/backend/src/main/java/com/goldenchart/service/LiveConditionStatusService.java +++ b/backend/src/main/java/com/goldenchart/service/LiveConditionStatusService.java @@ -203,6 +203,23 @@ public class LiveConditionStatusService { 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 임계값 요약 로그 */ private static String summarizeBuyCondition(JsonNode buyDsl) { if (buyDsl == null || buyDsl.isNull()) return "null"; @@ -817,9 +834,10 @@ public class LiveConditionStatusService { } } 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, - primarySeries.getEndIndex(), summarizeBuyCondition(buyDsl)); + primarySeries.getEndIndex(), summarizeBuyCondition(buyDsl), + probeEntryRule(entryRule, buyDsl, ruleCtx, startIdx)); } else { log.debug("[LiveCondition:scan] market={} strategy={} tf={} buy={} sell={}", market, strategyId, primaryTf, buyHits, sellHits); diff --git a/backend/src/main/java/com/goldenchart/service/StrategyDslToTa4jAdapter.java b/backend/src/main/java/com/goldenchart/service/StrategyDslToTa4jAdapter.java index 7701dbe..9173602 100644 --- a/backend/src/main/java/com/goldenchart/service/StrategyDslToTa4jAdapter.java +++ b/backend/src/main/java/com/goldenchart/service/StrategyDslToTa4jAdapter.java @@ -236,7 +236,18 @@ public class StrategyDslToTa4jAdapter { public Double readConditionFieldValue(JsonNode cond, BarSeries series, Map> params, 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; String field = leftSide @@ -251,13 +262,11 @@ public class StrategyDslToTa4jAdapter { int sidePeriod = leftSide ? leftPeriod : rightPeriod; String registryKey = toRegistryKey(indType); - Map indParams = params != null - ? params.getOrDefault(registryKey, Map.of()) + Map indParams = ctx.indicatorParams() != null + ? ctx.indicatorParams().getOrDefault(registryKey, Map.of()) : Map.of(); try { - RuleBuildContext ctx = new RuleBuildContext(series, params != null ? params : Map.of(), - Map.of(), null, null, false, Map.of()); Indicator ind = resolveField(field, indType, indParams, series, condPeriod, sidePeriod, cond, ctx); Num v = ind.getValue(index); return v != null ? v.doubleValue() : null; @@ -762,6 +771,11 @@ public class StrategyDslToTa4jAdapter { if (field == null || field.isBlank() || field.equals("NONE")) { return new ConstantIndicator<>(s, s.numFactory().numOf(0)); } + // K_* 직접입력 — 상수 임계값 (HL_*·지표 필드 해석보다 우선) + if (field.startsWith("K_")) { + Indicator kConst = resolveDirectKConstant(field, indType, cond, ctx, s); + if (kConst != null) return kConst; + } return switch (field) { case "CLOSE_PRICE" -> new ClosePriceIndicator(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())); } + /** K_55 등 직접입력 임계 — ConstantIndicator 또는 null(폴백) */ + private Indicator 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) { // K_ 접두사: 프론트엔드가 hline 실제값으로 생성한 상수 필드 // 예) K_60 → 60, K_-100 → -100, K_0 → 0 diff --git a/backend/src/test/java/com/goldenchart/service/IndicatorHlineResolverTest.java b/backend/src/test/java/com/goldenchart/service/IndicatorHlineResolverTest.java index 1c17172..1f57b63 100644 --- a/backend/src/test/java/com/goldenchart/service/IndicatorHlineResolverTest.java +++ b/backend/src/test/java/com/goldenchart/service/IndicatorHlineResolverTest.java @@ -52,6 +52,17 @@ class IndicatorHlineResolverTest { 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 void blankField_returnsNull() { assertNull(IndicatorHlineResolver.resolveThresholdField( diff --git a/backend/src/test/java/com/goldenchart/service/RsiChartScanIntegrationTest.java b/backend/src/test/java/com/goldenchart/service/RsiChartScanIntegrationTest.java index f6222c8..ad348d5 100644 --- a/backend/src/test/java/com/goldenchart/service/RsiChartScanIntegrationTest.java +++ b/backend/src/test/java/com/goldenchart/service/RsiChartScanIntegrationTest.java @@ -31,6 +31,70 @@ class RsiChartScanIntegrationTest { normalizer = new StrategyDslTimeframeNormalizer(MAPPER); } + @Test + void chartScan_strategy195Exact_k55_vs_hlMid() throws Exception { + String chartTf = "3m"; + List 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 seriesOverrides = OhlcvBarSeriesSupport.buildSeriesOverrides( + primarySeries, chartTf, buyDsl, null); + Map> 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 void chartScan_rsiCrossUpK55_firesSignals() throws Exception { String chartTf = "3m"; diff --git a/frontend/src/components/StrategyEditorPage.tsx b/frontend/src/components/StrategyEditorPage.tsx index bc34666..72b7b1a 100644 --- a/frontend/src/components/StrategyEditorPage.tsx +++ b/frontend/src/components/StrategyEditorPage.tsx @@ -355,12 +355,18 @@ export default function StrategyEditorPage({ const sellConditionRef = useRef(sellCondition); const buyOrphansRef = useRef(buyOrphans); const sellOrphansRef = useRef(sellOrphans); + const selectedIdRef = useRef(selectedId); + const stratNameRef = useRef(stratName); + const stratDescRef = useRef(stratDesc); buyLayoutRef.current = buyLayout; sellLayoutRef.current = sellLayout; buyConditionRef.current = buyCondition; sellConditionRef.current = sellCondition; buyOrphansRef.current = buyOrphans; sellOrphansRef.current = sellOrphans; + selectedIdRef.current = selectedId; + stratNameRef.current = stratName; + stratDescRef.current = stratDesc; const layoutRevisionRef = useRef(0); const strategyAutosaveTimerRef = useRef(null); const layoutPersistReadyRef = useRef(false); @@ -434,6 +440,8 @@ export default function StrategyEditorPage({ [sellCondition, sellLayout], ); const currentEditorState = signalTab === 'buy' ? buyEditorState : sellEditorState; + const currentEditorStateRef = useRef(currentEditorState); + currentEditorStateRef.current = currentEditorState; const selectedLogicNode = useMemo(() => { if (!selectedNodeId) return null; @@ -496,6 +504,38 @@ export default function StrategyEditorPage({ : s)); }, [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(() => { if (strategyAutosaveTimerRef.current != null) { window.clearTimeout(strategyAutosaveTimerRef.current); @@ -732,14 +772,19 @@ export default function StrategyEditorPage({ }; }, [selectedNodeId, selectedLogicNode, stochPairForest, applyStochPairSecondary]); - const handleEditorStateChange = useCallback((next: EditorConditionState) => { - setCurrentRoot(next.root); + const handleEditorStateChange = useCallback(( + next: EditorConditionState | ((prev: EditorConditionState) => EditorConditionState), + ) => { + const resolved = typeof next === 'function' + ? next(currentEditorStateRef.current) + : next; + setCurrentRoot(resolved.root); setCurrentLayout(prev => ({ ...prev, - startMeta: next.startMeta, - extraStartIds: next.extraStartIds, - extraRoots: next.extraRoots, - startCombineOp: normalizeStartCombineOp(next.startCombineOp), + startMeta: resolved.startMeta, + extraStartIds: resolved.extraStartIds, + extraRoots: resolved.extraRoots, + startCombineOp: normalizeStartCombineOp(resolved.startCombineOp), })); scheduleStrategyPersist(); }, [setCurrentRoot, setCurrentLayout, scheduleStrategyPersist]); @@ -784,21 +829,20 @@ export default function StrategyEditorPage({ }; }, [signalTab, layoutSeedKey, buyLayout, sellLayout]); - const persistStrategyToDbRef = useRef(persistStrategyToDb); - persistStrategyToDbRef.current = persistStrategyToDb; - useEffect(() => { - const id = selectedId; - const name = stratName; - return () => { - if (strategyAutosaveTimerRef.current != null) { - window.clearTimeout(strategyAutosaveTimerRef.current); - strategyAutosaveTimerRef.current = null; - } - if (id != null && name.trim()) { - void persistStrategyToDbRef.current().catch(() => { /* unmount flush */ }); - } - }; - }, [selectedId, stratName]); + const persistStrategySnapshotRef = useRef(persistStrategySnapshot); + persistStrategySnapshotRef.current = persistStrategySnapshot; + useEffect(() => () => { + if (strategyAutosaveTimerRef.current != null) { + window.clearTimeout(strategyAutosaveTimerRef.current); + strategyAutosaveTimerRef.current = null; + } + const id = selectedIdRef.current; + const name = stratNameRef.current; + const desc = stratDescRef.current; + if (id != null && name.trim()) { + void persistStrategySnapshotRef.current(id, name, desc).catch(() => { /* unmount flush */ }); + } + }, []); useEffect(() => { if (!layoutPersistReadyRef.current) { @@ -861,7 +905,24 @@ export default function StrategyEditorPage({ }).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) => { + if (selectedId !== s.id) { + flushBeforeStrategySwitch(); + } const buyDecoded = decodeConditionForEditor(s.buyCondition ?? null); const sellDecoded = decodeConditionForEditor(s.sellCondition ?? null); let stored = normalizeStrategyFlowLayout(s.flowLayout); @@ -982,13 +1043,17 @@ export default function StrategyEditorPage({ }, [variant, initialStrategyId, strategies]); const handleNew = () => { + flushBeforeStrategySwitch(); setSelectedId(null); setStratName(''); setStratDesc(''); setBuyCondition(null); setSellCondition(null); + setBuyOrphans([]); + setSellOrphans([]); setSelectedNodeId(null); - resetFlowLayout('draft', signalTab); + resetFlowLayout('draft', 'buy'); + setSignalTab('buy'); }; const openSaveDialog = useCallback((mode: SaveDialogMode) => { diff --git a/frontend/src/components/strategyEditor/StrategyListEditor.tsx b/frontend/src/components/strategyEditor/StrategyListEditor.tsx index aa958c0..bea77f1 100644 --- a/frontend/src/components/strategyEditor/StrategyListEditor.tsx +++ b/frontend/src/components/strategyEditor/StrategyListEditor.tsx @@ -32,6 +32,7 @@ import { import StartTimeframeCheckboxes from './StartTimeframeCheckboxes'; import LogicGateOpToggle from './LogicGateOpToggle'; import { formatStartCandleTypesLabel } from '../../utils/strategyStartNodes'; +import { START_NODE_ID } from '../../utils/strategyFlowLayout'; import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage'; import { findPaletteDropKeyAtPoint, @@ -293,7 +294,7 @@ interface StartSectionBlockProps { def: DefType; dragOverKey: string | null; setDragOverKey: (key: string | null) => void; - onRootChange: (root: LogicNode | null) => void; + onRootChange: (root: LogicNode | null | ((prev: LogicNode | null) => LogicNode | null)) => void; onCandleTypesChange: (candleTypes: string[]) => void; onDeleteStart?: () => void; onAddStart?: () => void; @@ -324,16 +325,14 @@ function StartSectionBlock({ return; } const newNode = makeNode(data.type, data.value, signalTab, def, makeNodeOptionsFromPalette(data)); - if (!root) { - onRootChange(newNode); - return; - } - if (!targetId) { - onRootChange(mergeAtRoot(root, newNode, data.type === 'operator')); - return; - } - onRootChange(addChild(root, targetId, newNode)); - }, [root, signalTab, def, onRootChange, onAddStart]); + onRootChange(prev => { + if (!prev) return newNode; + if (!targetId) { + return mergeAtRoot(prev, newNode, data.type === 'operator'); + } + return addChild(prev, targetId, newNode); + }); + }, [signalTab, def, onRootChange, onAddStart]); const handleSectionDrop = (e: React.DragEvent) => { e.preventDefault(); @@ -408,7 +407,9 @@ interface Props { editorState: EditorConditionState; signalTab: 'buy' | 'sell'; def: DefType; - onEditorStateChange: (next: EditorConditionState) => void; + onEditorStateChange: ( + next: EditorConditionState | ((prev: EditorConditionState) => EditorConditionState), + ) => void; onAddStart?: () => void; onOrphansChange?: (nodes: LogicNode[]) => void; orphans?: LogicNode[]; @@ -438,24 +439,35 @@ export default function StrategyListEditor({ onAddStart(); return; } - onEditorStateChange(addExtraStartSection(editorState)); - }, [onAddStart, onEditorStateChange, editorState]); + onEditorStateChange(prev => addExtraStartSection(prev)); + }, [onAddStart, onEditorStateChange]); - const handleRootChange = useCallback((startId: string, root: LogicNode | null) => { - onEditorStateChange(updateBranchRoot(editorState, startId, root)); - }, [editorState, onEditorStateChange]); + const handleRootChange = useCallback(( + startId: string, + 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[]) => { - onEditorStateChange(updateStartCandleTypes(editorState, startId, candleTypes)); - }, [editorState, onEditorStateChange]); + onEditorStateChange(prev => updateStartCandleTypes(prev, startId, candleTypes)); + }, [onEditorStateChange]); const handleDeleteStart = useCallback((startId: string) => { - const { state, orphanedNodes } = removeStartSection(editorState, startId); - onEditorStateChange(state); - if (orphanedNodes.length && onOrphansChange) { - onOrphansChange([...orphans, ...orphanedNodes]); - } - }, [editorState, onEditorStateChange, onOrphansChange, orphans]); + onEditorStateChange(prev => { + const { state, orphanedNodes } = removeStartSection(prev, startId); + if (orphanedNodes.length && onOrphansChange) { + onOrphansChange([...orphans, ...orphanedNodes]); + } + return state; + }); + }, [onEditorStateChange, onOrphansChange, orphans]); const resolvePaletteNode = useCallback(( data: { type: string; value: string; label: string; composite?: boolean }, @@ -487,26 +499,25 @@ export default function StrategyListEditor({ return; } - const section = collectStartSections(editorState).find(s => s.startId === startId); - if (!section) return; - const newNode = resolvePaletteNode(data); if (!newNode) return; - const root = section.root; - if (!root) { - handleRootChange(startId, newNode); - return; - } - if (!targetId) { - handleRootChange(startId, mergeAtRoot(root, newNode, data.type === 'operator')); - return; - } - handleRootChange(startId, addChild(root, targetId, newNode)); + onEditorStateChange(prev => { + const section = collectStartSections(prev).find(s => s.startId === startId); + if (!section) return prev; + + const root = section.root; + if (!root) { + return updateBranchRoot(prev, startId, newNode); + } + if (!targetId) { + return updateBranchRoot(prev, startId, mergeAtRoot(root, newNode, data.type === 'operator')); + } + return updateBranchRoot(prev, startId, addChild(root, targetId, newNode)); + }); }, [ - editorState, handleAddStart, - handleRootChange, + onEditorStateChange, resolvePaletteNode, ]); diff --git a/scripts/diag-scan-195.py b/scripts/diag-scan-195.py new file mode 100644 index 0000000..ad7c2e0 --- /dev/null +++ b/scripts/diag-scan-195.py @@ -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()