전략빌더 수정
This commit is contained in:
@@ -12,6 +12,9 @@ import org.ta4j.core.indicators.adx.*;
|
|||||||
import org.ta4j.core.indicators.averages.*;
|
import org.ta4j.core.indicators.averages.*;
|
||||||
import org.ta4j.core.indicators.bollinger.*;
|
import org.ta4j.core.indicators.bollinger.*;
|
||||||
import org.ta4j.core.indicators.helpers.*;
|
import org.ta4j.core.indicators.helpers.*;
|
||||||
|
import org.ta4j.core.indicators.donchian.DonchianChannelLowerIndicator;
|
||||||
|
import org.ta4j.core.indicators.donchian.DonchianChannelMiddleIndicator;
|
||||||
|
import org.ta4j.core.indicators.donchian.DonchianChannelUpperIndicator;
|
||||||
import org.ta4j.core.indicators.ichimoku.*;
|
import org.ta4j.core.indicators.ichimoku.*;
|
||||||
import org.ta4j.core.indicators.statistics.StandardDeviationIndicator;
|
import org.ta4j.core.indicators.statistics.StandardDeviationIndicator;
|
||||||
import org.ta4j.core.indicators.CachedIndicator;
|
import org.ta4j.core.indicators.CachedIndicator;
|
||||||
@@ -81,6 +84,7 @@ public class StrategyDslToTa4jAdapter {
|
|||||||
Map.entry("BWI", "BBBandWidth"),
|
Map.entry("BWI", "BBBandWidth"),
|
||||||
Map.entry("VR", "VR"),
|
Map.entry("VR", "VR"),
|
||||||
Map.entry("DISPARITY", "Disparity"),
|
Map.entry("DISPARITY", "Disparity"),
|
||||||
|
Map.entry("DONCHIAN", "DonchianChannels"),
|
||||||
Map.entry("VOLUME", "VOLUME")
|
Map.entry("VOLUME", "VOLUME")
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -352,6 +356,19 @@ public class StrategyDslToTa4jAdapter {
|
|||||||
return newPsychIndicator(s, intP(p, "length", 10), true);
|
return newPsychIndicator(s, intP(p, "length", 10), true);
|
||||||
// Williams %R
|
// Williams %R
|
||||||
if (field.equals("WILLIAMS_R_VALUE")) return new WilliamsRIndicator(s, intP(p, "length", 14));
|
if (field.equals("WILLIAMS_R_VALUE")) return new WilliamsRIndicator(s, intP(p, "length", 14));
|
||||||
|
// Donchian Channel — DC_UPPER_20 / DC_LOWER_10 등 기간 접미사
|
||||||
|
if (field.startsWith("DC_UPPER_")) {
|
||||||
|
int period = parseTrailingInt(field, "DC_UPPER_", intP(p, "length", 20));
|
||||||
|
return new DonchianChannelUpperIndicator(s, period);
|
||||||
|
}
|
||||||
|
if (field.startsWith("DC_LOWER_")) {
|
||||||
|
int period = parseTrailingInt(field, "DC_LOWER_", intP(p, "length", 20));
|
||||||
|
return new DonchianChannelLowerIndicator(s, period);
|
||||||
|
}
|
||||||
|
if (field.startsWith("DC_MIDDLE_")) {
|
||||||
|
int period = parseTrailingInt(field, "DC_MIDDLE_", intP(p, "length", 20));
|
||||||
|
return new DonchianChannelMiddleIndicator(s, period);
|
||||||
|
}
|
||||||
// Bollinger Bands
|
// Bollinger Bands
|
||||||
if (field.equals("UPPER_BAND") || field.equals("LOWER_BAND") || field.equals("MIDDLE_BAND")) {
|
if (field.equals("UPPER_BAND") || field.equals("LOWER_BAND") || field.equals("MIDDLE_BAND")) {
|
||||||
int len = intP(p, "length", 20);
|
int len = intP(p, "length", 20);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
deleteStrategyFlowLayout,
|
deleteStrategyFlowLayout,
|
||||||
type SignalFlowLayoutSnapshot,
|
type SignalFlowLayoutSnapshot,
|
||||||
type FlowLayoutChangePayload,
|
type FlowLayoutChangePayload,
|
||||||
|
type StrategyFlowLayoutStore,
|
||||||
} from '../utils/strategyEditorLayoutStorage';
|
} from '../utils/strategyEditorLayoutStorage';
|
||||||
import LogicExpressionPreview from './strategyEditor/LogicExpressionPreview';
|
import LogicExpressionPreview from './strategyEditor/LogicExpressionPreview';
|
||||||
import StrategyEditorCanvas, { type FlowLayoutSeed } from './strategyEditor/StrategyEditorCanvas';
|
import StrategyEditorCanvas, { type FlowLayoutSeed } from './strategyEditor/StrategyEditorCanvas';
|
||||||
@@ -42,6 +43,15 @@ import {
|
|||||||
simpleTemplateToNode,
|
simpleTemplateToNode,
|
||||||
type StrategyTemplateDef,
|
type StrategyTemplateDef,
|
||||||
} from '../utils/strategyPresets';
|
} from '../utils/strategyPresets';
|
||||||
|
import {
|
||||||
|
buildStrategyExportPayload,
|
||||||
|
buildStrategyListExportPayload,
|
||||||
|
downloadStrategyJson,
|
||||||
|
listImportItemToStrategyDto,
|
||||||
|
parseStrategyImportFile,
|
||||||
|
pickJsonFile,
|
||||||
|
strategyDtoToListExportItem,
|
||||||
|
} from '../utils/strategyImportExport';
|
||||||
import PaletteChip from './strategyEditor/PaletteChip';
|
import PaletteChip from './strategyEditor/PaletteChip';
|
||||||
import { readStoredSize, storeSize, usePanelResize } from './strategyEditor/usePanelResize';
|
import { readStoredSize, storeSize, usePanelResize } from './strategyEditor/usePanelResize';
|
||||||
import '../styles/strategyEditor.css';
|
import '../styles/strategyEditor.css';
|
||||||
@@ -393,6 +403,113 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
|
|||||||
showSnack('전략이 삭제되었습니다');
|
showSnack('전략이 삭제되었습니다');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applyImportedFlowLayout = useCallback((layout: StrategyFlowLayoutStore | undefined, tab: 'buy' | 'sell' = signalTab) => {
|
||||||
|
if (layout) {
|
||||||
|
setBuyLayout({
|
||||||
|
positions: layout.buy.positions ?? {},
|
||||||
|
edgeHandles: layout.buy.edgeHandles ?? {},
|
||||||
|
});
|
||||||
|
setSellLayout({
|
||||||
|
positions: layout.sell.positions ?? {},
|
||||||
|
edgeHandles: layout.sell.edgeHandles ?? {},
|
||||||
|
});
|
||||||
|
setBuyOrphans(layout.buy.orphans ?? []);
|
||||||
|
setSellOrphans(layout.sell.orphans ?? []);
|
||||||
|
saveStrategyFlowLayout('draft', layout);
|
||||||
|
} else {
|
||||||
|
resetFlowLayout('draft', tab);
|
||||||
|
}
|
||||||
|
bumpLayoutSeed('draft', tab);
|
||||||
|
}, [resetFlowLayout, bumpLayoutSeed, signalTab]);
|
||||||
|
|
||||||
|
const handleExport = useCallback(() => {
|
||||||
|
if (!buyCondition && !sellCondition) {
|
||||||
|
showSnack('내보낼 조건이 없습니다', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (editorMode === 'graph') layoutFlushRef.current?.();
|
||||||
|
const payload = buildStrategyExportPayload({
|
||||||
|
name: stratName,
|
||||||
|
description: stratDesc,
|
||||||
|
buyCondition,
|
||||||
|
sellCondition,
|
||||||
|
flowLayout: {
|
||||||
|
buy: { ...buyLayoutRef.current, orphans: buyOrphans },
|
||||||
|
sell: { ...sellLayoutRef.current, orphans: sellOrphans },
|
||||||
|
},
|
||||||
|
editorMode,
|
||||||
|
});
|
||||||
|
const safeName = (stratName || '전략').replace(/[^\w\uAC00-\uD7A3-]+/g, '_');
|
||||||
|
downloadStrategyJson(`${safeName}_${new Date().toISOString().slice(0, 10)}`, payload);
|
||||||
|
showSnack('JSON으로 내보냈습니다');
|
||||||
|
}, [buyCondition, sellCondition, stratName, stratDesc, buyOrphans, sellOrphans, editorMode]);
|
||||||
|
|
||||||
|
const handleImport = useCallback(async () => {
|
||||||
|
const text = await pickJsonFile();
|
||||||
|
if (!text) return;
|
||||||
|
try {
|
||||||
|
const result = parseStrategyImportFile(text);
|
||||||
|
if (result.kind === 'list') {
|
||||||
|
showSnack('단일 전략 파일이 아닙니다. 전체 가져오기(⬆)를 사용하세요', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (editorMode === 'graph') layoutFlushRef.current?.();
|
||||||
|
const { data } = result;
|
||||||
|
setBuyCondition(data.buyCondition ?? null);
|
||||||
|
setSellCondition(data.sellCondition ?? null);
|
||||||
|
setStratName(data.name ?? '가져온 전략');
|
||||||
|
setStratDesc(data.description ?? '');
|
||||||
|
setSelectedId(null);
|
||||||
|
setSelectedNodeId(null);
|
||||||
|
applyImportedFlowLayout(data.flowLayout);
|
||||||
|
if (data.editorMode === 'list' || data.editorMode === 'graph') {
|
||||||
|
setEditorMode(data.editorMode);
|
||||||
|
saveEditorMode(data.editorMode);
|
||||||
|
}
|
||||||
|
showSnack('전략을 가져왔습니다');
|
||||||
|
} catch (e) {
|
||||||
|
showSnack(e instanceof Error ? e.message : '파일 읽기 실패', false);
|
||||||
|
}
|
||||||
|
}, [editorMode, applyImportedFlowLayout]);
|
||||||
|
|
||||||
|
const handleExportAll = useCallback(() => {
|
||||||
|
if (strategies.length === 0) {
|
||||||
|
showSnack('내보낼 전략이 없습니다', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const items = strategies.map(s => strategyDtoToListExportItem(
|
||||||
|
s,
|
||||||
|
loadStrategyFlowLayout(String(s.id)),
|
||||||
|
));
|
||||||
|
downloadStrategyJson(
|
||||||
|
`전략목록_${new Date().toISOString().slice(0, 10)}`,
|
||||||
|
buildStrategyListExportPayload(items),
|
||||||
|
);
|
||||||
|
showSnack(`${strategies.length}개 전략을 내보냈습니다`);
|
||||||
|
}, [strategies]);
|
||||||
|
|
||||||
|
const handleImportAll = useCallback(async () => {
|
||||||
|
const text = await pickJsonFile();
|
||||||
|
if (!text) return;
|
||||||
|
try {
|
||||||
|
const result = parseStrategyImportFile(text);
|
||||||
|
if (result.kind !== 'list') {
|
||||||
|
showSnack('전략 목록 형식 파일이 필요합니다', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const baseId = Date.now();
|
||||||
|
const imported = result.data.strategies.map((item, index) => {
|
||||||
|
const id = baseId + index;
|
||||||
|
if (item.flowLayout) saveStrategyFlowLayout(String(id), item.flowLayout);
|
||||||
|
return listImportItemToStrategyDto(item, id);
|
||||||
|
});
|
||||||
|
setStrategies(prev => [...prev, ...imported]);
|
||||||
|
showSnack(`${imported.length}개 전략을 가져왔습니다`);
|
||||||
|
} catch (e) {
|
||||||
|
showSnack(e instanceof Error ? e.message : '파일 읽기 실패', false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const applyPalette = useCallback((type: string, value: string, label: string) => {
|
const applyPalette = useCallback((type: string, value: string, label: string) => {
|
||||||
const newNode = makeNode(type, value, signalTab, DEF);
|
const newNode = makeNode(type, value, signalTab, DEF);
|
||||||
const root = currentRoot;
|
const root = currentRoot;
|
||||||
@@ -404,21 +521,23 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
|
|||||||
const templates = useMemo(() => getStrategyTemplates(DEF), [DEF]);
|
const templates = useMemo(() => getStrategyTemplates(DEF), [DEF]);
|
||||||
|
|
||||||
const handleTemplate = useCallback((tmpl: StrategyTemplateDef) => {
|
const handleTemplate = useCallback((tmpl: StrategyTemplateDef) => {
|
||||||
if (tmpl.signal !== signalTab) switchSignalTab(tmpl.signal);
|
if (editorMode === 'graph') layoutFlushRef.current?.();
|
||||||
const setRoot = tmpl.signal === 'buy' ? setBuyCondition : setSellCondition;
|
|
||||||
const root = tmpl.signal === 'buy' ? buyCondition : sellCondition;
|
|
||||||
|
|
||||||
if (tmpl.kind === 'composite') {
|
const targetTab = tmpl.signal;
|
||||||
setRoot(tmpl.build());
|
const nextRoot = tmpl.kind === 'composite' ? tmpl.build() : simpleTemplateToNode(tmpl);
|
||||||
showSnack(`"${tmpl.label}" 템플릿이 적용됐습니다`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newNode = simpleTemplateToNode(tmpl);
|
setBuyCondition(null);
|
||||||
if (!root) setRoot(newNode);
|
setSellCondition(null);
|
||||||
else setRoot(mergeAtRoot(root, newNode, false));
|
setSelectedNodeId(null);
|
||||||
showSnack(`"${tmpl.label}" 추가됨`);
|
|
||||||
}, [signalTab, switchSignalTab, buyCondition, sellCondition, setBuyCondition, setSellCondition]);
|
if (targetTab === 'buy') setBuyCondition(nextRoot);
|
||||||
|
else setSellCondition(nextRoot);
|
||||||
|
|
||||||
|
resetFlowLayout(layoutStrategyKey, targetTab);
|
||||||
|
if (targetTab !== signalTab) setSignalTab(targetTab);
|
||||||
|
|
||||||
|
showSnack(`"${tmpl.label}" 템플릿이 적용됐습니다`);
|
||||||
|
}, [editorMode, layoutStrategyKey, resetFlowLayout, signalTab]);
|
||||||
|
|
||||||
const operators = [
|
const operators = [
|
||||||
{ type: 'operator' as const, value: 'AND', label: 'AND', color: 'logic-and' },
|
{ type: 'operator' as const, value: 'AND', label: 'AND', color: 'logic-and' },
|
||||||
@@ -430,6 +549,7 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
|
|||||||
{ type: 'indicator' as const, value: 'MA', label: 'MA', desc: '이동평균', color: 'band' },
|
{ type: 'indicator' as const, value: 'MA', label: 'MA', desc: '이동평균', color: 'band' },
|
||||||
{ type: 'indicator' as const, value: 'EMA', label: 'EMA', desc: '지수이동평균', color: 'band' },
|
{ type: 'indicator' as const, value: 'EMA', label: 'EMA', desc: '지수이동평균', color: 'band' },
|
||||||
{ type: 'indicator' as const, value: 'BOLLINGER', label: 'Bollinger', desc: '볼린저밴드', color: 'band' },
|
{ type: 'indicator' as const, value: 'BOLLINGER', label: 'Bollinger', desc: '볼린저밴드', color: 'band' },
|
||||||
|
{ type: 'indicator' as const, value: 'DONCHIAN', label: 'Donchian', desc: '돈치안 채널', color: 'band' },
|
||||||
{ type: 'indicator' as const, value: 'ICHIMOKU', label: 'Ichimoku', desc: '일목균형표', color: 'band' },
|
{ type: 'indicator' as const, value: 'ICHIMOKU', label: 'Ichimoku', desc: '일목균형표', color: 'band' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -484,6 +604,40 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" className="se-btn se-btn--ghost" onClick={handleNew}>+ 새 전략</button>
|
<button type="button" className="se-btn se-btn--ghost" onClick={handleNew}>+ 새 전략</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="se-btn se-btn--ghost se-btn--icon"
|
||||||
|
title="현재 전략 JSON 내보내기"
|
||||||
|
onClick={handleExport}
|
||||||
|
disabled={!buyCondition && !sellCondition}
|
||||||
|
>
|
||||||
|
↓
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="se-btn se-btn--ghost se-btn--icon"
|
||||||
|
title="JSON 파일에서 전략 가져오기"
|
||||||
|
onClick={() => { void handleImport(); }}
|
||||||
|
>
|
||||||
|
↑
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="se-btn se-btn--ghost se-btn--icon"
|
||||||
|
title="전체 전략 목록 JSON 내보내기"
|
||||||
|
onClick={handleExportAll}
|
||||||
|
disabled={strategies.length === 0}
|
||||||
|
>
|
||||||
|
⬇
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="se-btn se-btn--ghost se-btn--icon"
|
||||||
|
title="전체 전략 목록 JSON 가져오기"
|
||||||
|
onClick={() => { void handleImportAll(); }}
|
||||||
|
>
|
||||||
|
⬆
|
||||||
|
</button>
|
||||||
<button type="button" className="se-btn se-btn--gold" onClick={() => setSaveOpen(true)}>저장하기</button>
|
<button type="button" className="se-btn se-btn--gold" onClick={() => setSaveOpen(true)}>저장하기</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -459,6 +459,18 @@ const getFieldOpts = (ind: string, signalType: 'buy'|'sell', DEF: DefType): Opt[
|
|||||||
{ value:'HIGH_PRICE', label:'고가' },
|
{ value:'HIGH_PRICE', label:'고가' },
|
||||||
{ value:'LOW_PRICE', label:'저가' },
|
{ value:'LOW_PRICE', label:'저가' },
|
||||||
]);
|
]);
|
||||||
|
case 'DONCHIAN': return condOpts([
|
||||||
|
{ value:'DC_UPPER_10', label:'상단(10일 최고가)' },
|
||||||
|
{ value:'DC_UPPER_20', label:'상단(20일 최고가)' },
|
||||||
|
{ value:'DC_UPPER_55', label:'상단(55일 최고가)' },
|
||||||
|
{ value:'DC_MIDDLE_20', label:'중심(20일)' },
|
||||||
|
{ value:'DC_LOWER_10', label:'하단(10일 최저가)' },
|
||||||
|
{ value:'DC_LOWER_20', label:'하단(20일 최저가)' },
|
||||||
|
{ value:'DC_LOWER_55', label:'하단(55일 최저가)' },
|
||||||
|
{ value:'CLOSE_PRICE', label:'종가' },
|
||||||
|
{ value:'LOW_PRICE', label:'저가' },
|
||||||
|
{ value:'HIGH_PRICE', label:'고가' },
|
||||||
|
]);
|
||||||
case 'ICHIMOKU': return condOpts([
|
case 'ICHIMOKU': return condOpts([
|
||||||
{ value:'CONVERSION_LINE', label:`전환선(${DEF.ichTenkan}일)` },
|
{ value:'CONVERSION_LINE', label:`전환선(${DEF.ichTenkan}일)` },
|
||||||
{ value:'BASE_LINE', label:`기준선(${DEF.ichKijun}일)` },
|
{ value:'BASE_LINE', label:`기준선(${DEF.ichKijun}일)` },
|
||||||
@@ -475,7 +487,6 @@ const getFieldOpts = (ind: string, signalType: 'buy'|'sell', DEF: DefType): Opt[
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getDefaultFields = (ind: string, signalType: 'buy'|'sell', DEF: DefType): { l: string; r: string } => {
|
const getDefaultFields = (ind: string, signalType: 'buy'|'sell', DEF: DefType): { l: string; r: string } => {
|
||||||
void signalType;
|
|
||||||
// 저장된 hline 과열선 값 우선 사용
|
// 저장된 hline 과열선 값 우선 사용
|
||||||
const over = (def: number) => kv(DEF.hlThresh[ind]?.over ?? def);
|
const over = (def: number) => kv(DEF.hlThresh[ind]?.over ?? def);
|
||||||
const mid = (def: number) => kv(DEF.hlThresh[ind]?.mid ?? def);
|
const mid = (def: number) => kv(DEF.hlThresh[ind]?.mid ?? def);
|
||||||
@@ -502,6 +513,9 @@ const getDefaultFields = (ind: string, signalType: 'buy'|'sell', DEF: DefType):
|
|||||||
VOLUME_OSC: { l:'VOLUME_OSC_VALUE', r: mid(0) },
|
VOLUME_OSC: { l:'VOLUME_OSC_VALUE', r: mid(0) },
|
||||||
MACD: { l:'MACD_LINE', r:'SIGNAL_LINE' },
|
MACD: { l:'MACD_LINE', r:'SIGNAL_LINE' },
|
||||||
BOLLINGER: { l:'CLOSE_PRICE', r:'UPPER_BAND' },
|
BOLLINGER: { l:'CLOSE_PRICE', r:'UPPER_BAND' },
|
||||||
|
DONCHIAN: signalType === 'buy'
|
||||||
|
? { l:'CLOSE_PRICE', r:'DC_UPPER_20' }
|
||||||
|
: { l:'CLOSE_PRICE', r:'DC_LOWER_20' },
|
||||||
ICHIMOKU: { l:'CONVERSION_LINE', r:'BASE_LINE' },
|
ICHIMOKU: { l:'CONVERSION_LINE', r:'BASE_LINE' },
|
||||||
};
|
};
|
||||||
return map[ind] ?? { l:'NONE', r:'NONE' };
|
return map[ind] ?? { l:'NONE', r:'NONE' };
|
||||||
@@ -1218,6 +1232,7 @@ export const StrategyPage: React.FC<Props> = ({ activeIndicators = [] }) => {
|
|||||||
{ type:'indicator' as const, value:'MA', label:'MA (이동평균)', color:'primary' },
|
{ type:'indicator' as const, value:'MA', label:'MA (이동평균)', color:'primary' },
|
||||||
{ type:'indicator' as const, value:'EMA', label:'EMA (지수이동평균)', color:'primary' },
|
{ type:'indicator' as const, value:'EMA', label:'EMA (지수이동평균)', color:'primary' },
|
||||||
{ type:'indicator' as const, value:'BOLLINGER',label:'볼린저밴드', color:'primary' },
|
{ type:'indicator' as const, value:'BOLLINGER',label:'볼린저밴드', color:'primary' },
|
||||||
|
{ type:'indicator' as const, value:'DONCHIAN', label:'돈치안 채널', color:'primary' },
|
||||||
{ type:'indicator' as const, value:'ICHIMOKU', label:'일목균형표', color:'primary' },
|
{ type:'indicator' as const, value:'ICHIMOKU', label:'일목균형표', color:'primary' },
|
||||||
];
|
];
|
||||||
const indicatorItems = [
|
const indicatorItems = [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/** 팔레트·Logic Expression 공통 — 지표 카테고리 */
|
/** 팔레트·Logic Expression 공통 — 지표 카테고리 */
|
||||||
|
|
||||||
export const BAND_INDICATORS = new Set(['MA', 'EMA', 'BOLLINGER', 'ICHIMOKU']);
|
export const BAND_INDICATORS = new Set(['MA', 'EMA', 'BOLLINGER', 'DONCHIAN', 'ICHIMOKU']);
|
||||||
|
|
||||||
export type PaletteIndicatorCategory = 'band' | 'ind';
|
export type PaletteIndicatorCategory = 'band' | 'ind';
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,16 @@
|
|||||||
}
|
}
|
||||||
.se-btn:hover { border-color: color-mix(in srgb, var(--se-accent) 50%, transparent); }
|
.se-btn:hover { border-color: color-mix(in srgb, var(--se-accent) 50%, transparent); }
|
||||||
.se-btn--ghost { background: transparent; }
|
.se-btn--ghost { background: transparent; }
|
||||||
|
.se-btn--icon {
|
||||||
|
min-width: 34px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.se-btn--icon:disabled {
|
||||||
|
opacity: 0.35;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
.se-btn--gold {
|
.se-btn--gold {
|
||||||
background: var(--se-btn-gold-bg);
|
background: var(--se-btn-gold-bg);
|
||||||
border-color: var(--se-btn-gold-border);
|
border-color: var(--se-btn-gold-border);
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ export function buildDef(activeIndicators: IndicatorConfig[]): DefType {
|
|||||||
INVEST_PSYCHOLOGICAL: 'InvestPsychological',
|
INVEST_PSYCHOLOGICAL: 'InvestPsychological',
|
||||||
OBV: 'OBV',
|
OBV: 'OBV',
|
||||||
BOLLINGER: 'BollingerBands',
|
BOLLINGER: 'BollingerBands',
|
||||||
|
DONCHIAN: 'DonchianChannels',
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractThresh = (dslType: string): HLThresh => {
|
const extractThresh = (dslType: string): HLThresh => {
|
||||||
@@ -417,6 +418,18 @@ export const getFieldOpts = (ind: string, signalType: 'buy'|'sell', DEF: DefType
|
|||||||
{ value:'HIGH_PRICE', label:'고가' },
|
{ value:'HIGH_PRICE', label:'고가' },
|
||||||
{ value:'LOW_PRICE', label:'저가' },
|
{ value:'LOW_PRICE', label:'저가' },
|
||||||
]);
|
]);
|
||||||
|
case 'DONCHIAN': return condOpts([
|
||||||
|
{ value:'DC_UPPER_10', label:'상단(10일 최고가)' },
|
||||||
|
{ value:'DC_UPPER_20', label:'상단(20일 최고가)' },
|
||||||
|
{ value:'DC_UPPER_55', label:'상단(55일 최고가)' },
|
||||||
|
{ value:'DC_MIDDLE_20', label:'중심(20일)' },
|
||||||
|
{ value:'DC_LOWER_10', label:'하단(10일 최저가)' },
|
||||||
|
{ value:'DC_LOWER_20', label:'하단(20일 최저가)' },
|
||||||
|
{ value:'DC_LOWER_55', label:'하단(55일 최저가)' },
|
||||||
|
{ value:'CLOSE_PRICE', label:'종가' },
|
||||||
|
{ value:'LOW_PRICE', label:'저가' },
|
||||||
|
{ value:'HIGH_PRICE', label:'고가' },
|
||||||
|
]);
|
||||||
case 'ICHIMOKU': return condOpts([
|
case 'ICHIMOKU': return condOpts([
|
||||||
{ value:'CONVERSION_LINE', label:`전환선(${DEF.ichTenkan}일)` },
|
{ value:'CONVERSION_LINE', label:`전환선(${DEF.ichTenkan}일)` },
|
||||||
{ value:'BASE_LINE', label:`기준선(${DEF.ichKijun}일)` },
|
{ value:'BASE_LINE', label:`기준선(${DEF.ichKijun}일)` },
|
||||||
@@ -433,7 +446,6 @@ export const getFieldOpts = (ind: string, signalType: 'buy'|'sell', DEF: DefType
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getDefaultFields = (ind: string, signalType: 'buy'|'sell', DEF: DefType): { l: string; r: string } => {
|
const getDefaultFields = (ind: string, signalType: 'buy'|'sell', DEF: DefType): { l: string; r: string } => {
|
||||||
void signalType;
|
|
||||||
// 저장된 hline 과열선 값 우선 사용
|
// 저장된 hline 과열선 값 우선 사용
|
||||||
const over = (def: number) => kv(DEF.hlThresh[ind]?.over ?? def);
|
const over = (def: number) => kv(DEF.hlThresh[ind]?.over ?? def);
|
||||||
const mid = (def: number) => kv(DEF.hlThresh[ind]?.mid ?? def);
|
const mid = (def: number) => kv(DEF.hlThresh[ind]?.mid ?? def);
|
||||||
@@ -460,6 +472,9 @@ const getDefaultFields = (ind: string, signalType: 'buy'|'sell', DEF: DefType):
|
|||||||
VOLUME_OSC: { l:'VOLUME_OSC_VALUE', r: mid(0) },
|
VOLUME_OSC: { l:'VOLUME_OSC_VALUE', r: mid(0) },
|
||||||
MACD: { l:'MACD_LINE', r:'SIGNAL_LINE' },
|
MACD: { l:'MACD_LINE', r:'SIGNAL_LINE' },
|
||||||
BOLLINGER: { l:'CLOSE_PRICE', r:'UPPER_BAND' },
|
BOLLINGER: { l:'CLOSE_PRICE', r:'UPPER_BAND' },
|
||||||
|
DONCHIAN: signalType === 'buy'
|
||||||
|
? { l:'CLOSE_PRICE', r:'DC_UPPER_20' }
|
||||||
|
: { l:'CLOSE_PRICE', r:'DC_LOWER_20' },
|
||||||
ICHIMOKU: { l:'CONVERSION_LINE', r:'BASE_LINE' },
|
ICHIMOKU: { l:'CONVERSION_LINE', r:'BASE_LINE' },
|
||||||
};
|
};
|
||||||
return map[ind] ?? { l:'NONE', r:'NONE' };
|
return map[ind] ?? { l:'NONE', r:'NONE' };
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import type { LogicNode } from './strategyTypes';
|
||||||
|
import type { StrategyFlowLayoutStore } from './strategyEditorLayoutStorage';
|
||||||
|
import type { StrategyEditorMode } from './strategyEditorModeStorage';
|
||||||
|
import type { StrategyDto } from './strategyEditorShared';
|
||||||
|
|
||||||
|
export const STRATEGY_EXPORT_VERSION = '1.1';
|
||||||
|
|
||||||
|
export interface StrategyExportPayload {
|
||||||
|
version: string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
buyCondition?: LogicNode | null;
|
||||||
|
sellCondition?: LogicNode | null;
|
||||||
|
exportedAt: string;
|
||||||
|
source?: 'strategy-builder' | 'strategy-page';
|
||||||
|
flowLayout?: StrategyFlowLayoutStore;
|
||||||
|
editorMode?: StrategyEditorMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StrategyListExportItem {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
buyCondition?: LogicNode | null;
|
||||||
|
sellCondition?: LogicNode | null;
|
||||||
|
enabled?: boolean;
|
||||||
|
flowLayout?: StrategyFlowLayoutStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StrategyListExportPayload {
|
||||||
|
version: string;
|
||||||
|
exportedAt: string;
|
||||||
|
totalCount: number;
|
||||||
|
source?: 'strategy-builder' | 'strategy-page';
|
||||||
|
strategies: StrategyListExportItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StrategyImportResult =
|
||||||
|
| { kind: 'single'; data: StrategyExportPayload }
|
||||||
|
| { kind: 'list'; data: StrategyListExportPayload };
|
||||||
|
|
||||||
|
function isLogicNode(value: unknown): value is LogicNode {
|
||||||
|
return !!value && typeof value === 'object' && typeof (value as LogicNode).type === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasStrategyConditions(data: Record<string, unknown>): boolean {
|
||||||
|
return isLogicNode(data.buyCondition) || isLogicNode(data.sellCondition);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildStrategyExportPayload(input: {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
buyCondition?: LogicNode | null;
|
||||||
|
sellCondition?: LogicNode | null;
|
||||||
|
flowLayout?: StrategyFlowLayoutStore;
|
||||||
|
editorMode?: StrategyEditorMode;
|
||||||
|
}): StrategyExportPayload {
|
||||||
|
return {
|
||||||
|
version: STRATEGY_EXPORT_VERSION,
|
||||||
|
source: 'strategy-builder',
|
||||||
|
name: input.name,
|
||||||
|
description: input.description,
|
||||||
|
buyCondition: input.buyCondition ?? null,
|
||||||
|
sellCondition: input.sellCondition ?? null,
|
||||||
|
flowLayout: input.flowLayout,
|
||||||
|
editorMode: input.editorMode,
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildStrategyListExportPayload(
|
||||||
|
strategies: StrategyListExportItem[],
|
||||||
|
): StrategyListExportPayload {
|
||||||
|
return {
|
||||||
|
version: STRATEGY_EXPORT_VERSION,
|
||||||
|
source: 'strategy-builder',
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
totalCount: strategies.length,
|
||||||
|
strategies,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseStrategyImportFile(text: string): StrategyImportResult {
|
||||||
|
let parsed: unknown;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
throw new Error('JSON 파싱에 실패했습니다');
|
||||||
|
}
|
||||||
|
if (!parsed || typeof parsed !== 'object') {
|
||||||
|
throw new Error('유효하지 않은 JSON 형식입니다');
|
||||||
|
}
|
||||||
|
const data = parsed as Record<string, unknown>;
|
||||||
|
|
||||||
|
if (Array.isArray(data.strategies)) {
|
||||||
|
const strategies = data.strategies.filter((item): item is StrategyListExportItem => {
|
||||||
|
if (!item || typeof item !== 'object') return false;
|
||||||
|
const row = item as StrategyListExportItem;
|
||||||
|
return !!row.name && (isLogicNode(row.buyCondition) || isLogicNode(row.sellCondition));
|
||||||
|
});
|
||||||
|
if (strategies.length === 0) {
|
||||||
|
throw new Error('가져올 전략 조건이 없습니다');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kind: 'list',
|
||||||
|
data: {
|
||||||
|
version: String(data.version ?? STRATEGY_EXPORT_VERSION),
|
||||||
|
exportedAt: String(data.exportedAt ?? new Date().toISOString()),
|
||||||
|
totalCount: strategies.length,
|
||||||
|
source: data.source === 'strategy-page' ? 'strategy-page' : 'strategy-builder',
|
||||||
|
strategies,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasStrategyConditions(data)) {
|
||||||
|
throw new Error('buyCondition 또는 sellCondition이 필요합니다');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: 'single',
|
||||||
|
data: {
|
||||||
|
version: String(data.version ?? '1.0'),
|
||||||
|
source: data.source === 'strategy-page' ? 'strategy-page' : 'strategy-builder',
|
||||||
|
name: typeof data.name === 'string' ? data.name : undefined,
|
||||||
|
description: typeof data.description === 'string' ? data.description : undefined,
|
||||||
|
buyCondition: isLogicNode(data.buyCondition) ? data.buyCondition : null,
|
||||||
|
sellCondition: isLogicNode(data.sellCondition) ? data.sellCondition : null,
|
||||||
|
flowLayout: data.flowLayout as StrategyFlowLayoutStore | undefined,
|
||||||
|
editorMode: data.editorMode === 'list' || data.editorMode === 'graph'
|
||||||
|
? data.editorMode
|
||||||
|
: undefined,
|
||||||
|
exportedAt: String(data.exportedAt ?? new Date().toISOString()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadStrategyJson(filename: string, data: unknown): void {
|
||||||
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
|
const href = URL.createObjectURL(blob);
|
||||||
|
const anchor = document.createElement('a');
|
||||||
|
anchor.href = href;
|
||||||
|
anchor.download = filename.endsWith('.json') ? filename : `${filename}.json`;
|
||||||
|
document.body.appendChild(anchor);
|
||||||
|
anchor.click();
|
||||||
|
document.body.removeChild(anchor);
|
||||||
|
URL.revokeObjectURL(href);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickJsonFile(): Promise<string | null> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.json,application/json';
|
||||||
|
input.onchange = async () => {
|
||||||
|
const file = input.files?.[0];
|
||||||
|
if (!file) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
resolve(await file.text());
|
||||||
|
} catch {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function strategyDtoToListExportItem(
|
||||||
|
strategy: StrategyDto,
|
||||||
|
flowLayout?: StrategyFlowLayoutStore | null,
|
||||||
|
): StrategyListExportItem {
|
||||||
|
return {
|
||||||
|
name: strategy.name,
|
||||||
|
description: strategy.description,
|
||||||
|
buyCondition: strategy.buyCondition ?? null,
|
||||||
|
sellCondition: strategy.sellCondition ?? null,
|
||||||
|
enabled: strategy.enabled,
|
||||||
|
flowLayout: flowLayout ?? undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listImportItemToStrategyDto(item: StrategyListExportItem, id: number): StrategyDto {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: item.name,
|
||||||
|
description: item.description,
|
||||||
|
buyCondition: item.buyCondition ?? null,
|
||||||
|
sellCondition: item.sellCondition ?? null,
|
||||||
|
enabled: item.enabled ?? true,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -40,6 +40,26 @@ function andTree(...children: LogicNode[]): LogicNode {
|
|||||||
return { id: genId(), type: 'AND', children };
|
return { id: genId(), type: 'AND', children };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 돈천 채널 — N일 최고가 상향 돌파 */
|
||||||
|
function donchianBreakoutBuy(period: number): LogicNode {
|
||||||
|
return condition('DONCHIAN', 'CROSS_UP', 'CLOSE_PRICE', `DC_UPPER_${period}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 돈천 채널 — N일 최저가 하향 이탈 */
|
||||||
|
function donchianBreakdownSell(period: number): LogicNode {
|
||||||
|
return condition('DONCHIAN', 'CROSS_DOWN', 'CLOSE_PRICE', `DC_LOWER_${period}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 일목균형표 — 전환선(9일) / 기준선(26일) 골든·데드크로스 */
|
||||||
|
function ichimokuTenkanKijunCross(signal: 'buy' | 'sell'): LogicNode {
|
||||||
|
return condition(
|
||||||
|
'ICHIMOKU',
|
||||||
|
signal === 'buy' ? 'CROSS_UP' : 'CROSS_DOWN',
|
||||||
|
'CONVERSION_LINE',
|
||||||
|
'BASE_LINE',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** 33변곡 매수 — 바닥권 시간변곡(약 33거래일 주기) 전환 구간 */
|
/** 33변곡 매수 — 바닥권 시간변곡(약 33거래일 주기) 전환 구간 */
|
||||||
export function build33InflectionBuyTree(): LogicNode {
|
export function build33InflectionBuyTree(): LogicNode {
|
||||||
return andTree(
|
return andTree(
|
||||||
@@ -93,6 +113,62 @@ export function getStrategyTemplates(def: DefType): StrategyTemplateDef[] {
|
|||||||
description: '상승 5파 끝 변곡 — RSI 과매수 + MA 데드크로스 + 20일선·이격도 이탈',
|
description: '상승 5파 끝 변곡 — RSI 과매수 + MA 데드크로스 + 20일선·이격도 이탈',
|
||||||
build: build33InflectionSellTree,
|
build: build33InflectionSellTree,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
kind: 'composite',
|
||||||
|
signal: 'buy',
|
||||||
|
label: '돈천 채널(20일) 매수',
|
||||||
|
description: '종가가 직전 20거래일 최고가(상단선)를 상향 돌파 — 한 달 박스권 돌파',
|
||||||
|
build: () => donchianBreakoutBuy(20),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'composite',
|
||||||
|
signal: 'sell',
|
||||||
|
label: '돈천 채널(20일) 매도',
|
||||||
|
description: '종가가 직전 20거래일 최저가(하단선)를 하향 이탈 — 추세 이탈 청산',
|
||||||
|
build: () => donchianBreakdownSell(20),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'composite',
|
||||||
|
signal: 'buy',
|
||||||
|
label: '터틀 S1 매수',
|
||||||
|
description: '단기 시스템 — 20일 최고가 돌파 진입 (리처드 데니스 터틀 트레이딩)',
|
||||||
|
build: () => donchianBreakoutBuy(20),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'composite',
|
||||||
|
signal: 'sell',
|
||||||
|
label: '터틀 S1 청산',
|
||||||
|
description: '단기 시스템 — 10일 최저가 이탈 시 즉시 청산 (진입보다 짧은 손절)',
|
||||||
|
build: () => donchianBreakdownSell(10),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'composite',
|
||||||
|
signal: 'buy',
|
||||||
|
label: '터틀 S2 매수',
|
||||||
|
description: '장기 시스템 — 55일 최고가 돌파 진입 (큰 추세 포착)',
|
||||||
|
build: () => donchianBreakoutBuy(55),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'composite',
|
||||||
|
signal: 'sell',
|
||||||
|
label: '터틀 S2 청산',
|
||||||
|
description: '장기 시스템 — 20일 최저가 이탈 시 청산 (S1보다 여유, S2보다 빠른 탈출)',
|
||||||
|
build: () => donchianBreakdownSell(20),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'composite',
|
||||||
|
signal: 'buy',
|
||||||
|
label: '일목 전환/기준선 매수',
|
||||||
|
description: '전환선(9일 중간값)이 기준선(26일 중간값)을 상향 돌파 — 골든크로스',
|
||||||
|
build: () => ichimokuTenkanKijunCross('buy'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'composite',
|
||||||
|
signal: 'sell',
|
||||||
|
label: '일목 전환/기준선 매도',
|
||||||
|
description: '전환선(9일)이 기준선(26일)을 하향 이탈 — 데드크로스',
|
||||||
|
build: () => ichimokuTenkanKijunCross('sell'),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export const INDICATOR_CONDITIONS: Record<string, string[]> = {
|
|||||||
VOLUME: COMMON_CONDITIONS,
|
VOLUME: COMMON_CONDITIONS,
|
||||||
MA: COMMON_CONDITIONS, EMA: COMMON_CONDITIONS,
|
MA: COMMON_CONDITIONS, EMA: COMMON_CONDITIONS,
|
||||||
BOLLINGER: COMMON_CONDITIONS,
|
BOLLINGER: COMMON_CONDITIONS,
|
||||||
|
DONCHIAN: COMMON_CONDITIONS,
|
||||||
ICHIMOKU: [
|
ICHIMOKU: [
|
||||||
...COMMON_CONDITIONS,
|
...COMMON_CONDITIONS,
|
||||||
'ABOVE_CLOUD', 'BELOW_CLOUD', 'IN_CLOUD', 'CLOUD_BREAK_UP', 'CLOUD_BREAK_DOWN',
|
'ABOVE_CLOUD', 'BELOW_CLOUD', 'IN_CLOUD', 'CLOUD_BREAK_UP', 'CLOUD_BREAK_DOWN',
|
||||||
@@ -108,6 +109,11 @@ export const LEFT_FIELD_OPTIONS: Record<string, FieldOption[]> = {
|
|||||||
{ value: 'UPPER', label: '상단밴드' }, { value: 'MIDDLE', label: '중단밴드' },
|
{ value: 'UPPER', label: '상단밴드' }, { value: 'MIDDLE', label: '중단밴드' },
|
||||||
{ value: 'LOWER', label: '하단밴드' },
|
{ value: 'LOWER', label: '하단밴드' },
|
||||||
],
|
],
|
||||||
|
DONCHIAN: [
|
||||||
|
{ value: 'CLOSE_PRICE', label: '종가' },
|
||||||
|
{ value: 'LOW_PRICE', label: '저가' },
|
||||||
|
{ value: 'HIGH_PRICE', label: '고가' },
|
||||||
|
],
|
||||||
ICHIMOKU: [
|
ICHIMOKU: [
|
||||||
{ value: 'CLOSE_PRICE', label: '종가' },
|
{ value: 'CLOSE_PRICE', label: '종가' },
|
||||||
{ value: 'CONVERSION_LINE', label: '전환선' }, { value: 'BASE_LINE', label: '기준선' },
|
{ value: 'CONVERSION_LINE', label: '전환선' }, { value: 'BASE_LINE', label: '기준선' },
|
||||||
@@ -178,6 +184,16 @@ export const RIGHT_FIELD_OPTIONS: Record<string, FieldOption[]> = {
|
|||||||
{ value: 'UPPER', label: '상단밴드' }, { value: 'MIDDLE', label: '중단밴드' },
|
{ value: 'UPPER', label: '상단밴드' }, { value: 'MIDDLE', label: '중단밴드' },
|
||||||
{ value: 'LOWER', label: '하단밴드' }, { value: 'CUSTOM', label: '직접 입력' },
|
{ value: 'LOWER', label: '하단밴드' }, { value: 'CUSTOM', label: '직접 입력' },
|
||||||
],
|
],
|
||||||
|
DONCHIAN: [
|
||||||
|
{ value: 'DC_UPPER_10', label: '상단(10일 최고가)' },
|
||||||
|
{ value: 'DC_UPPER_20', label: '상단(20일 최고가)' },
|
||||||
|
{ value: 'DC_UPPER_55', label: '상단(55일 최고가)' },
|
||||||
|
{ value: 'DC_MIDDLE_20', label: '중심(20일)' },
|
||||||
|
{ value: 'DC_LOWER_10', label: '하단(10일 최저가)' },
|
||||||
|
{ value: 'DC_LOWER_20', label: '하단(20일 최저가)' },
|
||||||
|
{ value: 'DC_LOWER_55', label: '하단(55일 최저가)' },
|
||||||
|
{ value: 'CUSTOM', label: '직접 입력' },
|
||||||
|
],
|
||||||
ICHIMOKU: [
|
ICHIMOKU: [
|
||||||
{ value: 'CONVERSION_LINE', label: '전환선' }, { value: 'BASE_LINE', label: '기준선' },
|
{ value: 'CONVERSION_LINE', label: '전환선' }, { value: 'BASE_LINE', label: '기준선' },
|
||||||
{ value: 'LEADING_SPAN1', label: '선행1' }, { value: 'LEADING_SPAN2', label: '선행2' },
|
{ value: 'LEADING_SPAN1', label: '선행1' }, { value: 'LEADING_SPAN2', label: '선행2' },
|
||||||
@@ -207,6 +223,7 @@ export const MA_BAND_ITEMS: PaletteItem[] = [
|
|||||||
{ id: 'ind-ma', type: 'indicator', label: 'MA (이동평균)', value: 'MA', color: 'primary' },
|
{ id: 'ind-ma', type: 'indicator', label: 'MA (이동평균)', value: 'MA', color: 'primary' },
|
||||||
{ id: 'ind-ema', type: 'indicator', label: 'EMA (지수이동평균)', value: 'EMA', color: 'primary' },
|
{ id: 'ind-ema', type: 'indicator', label: 'EMA (지수이동평균)', value: 'EMA', color: 'primary' },
|
||||||
{ id: 'ind-bollinger',type: 'indicator', label: '볼린저밴드', value: 'BOLLINGER', color: 'primary' },
|
{ id: 'ind-bollinger',type: 'indicator', label: '볼린저밴드', value: 'BOLLINGER', color: 'primary' },
|
||||||
|
{ id: 'ind-donchian', type: 'indicator', label: '돈치안 채널', value: 'DONCHIAN', color: 'primary' },
|
||||||
{ id: 'ind-ichimoku', type: 'indicator', label: '일목균형표', value: 'ICHIMOKU', color: 'primary' },
|
{ id: 'ind-ichimoku', type: 'indicator', label: '일목균형표', value: 'ICHIMOKU', color: 'primary' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user