저장팝업 텍스트 색상 수정
This commit is contained in:
@@ -1948,6 +1948,10 @@ export class ChartManager {
|
||||
}
|
||||
|
||||
entry.config = config;
|
||||
|
||||
if (this._candleOnlyLayout && this._isSubPaneIndicator(entry)) {
|
||||
this._hideSubPaneIndicator(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/** 메인 차트(캔들) 색상 스타일 즉시 업데이트 */
|
||||
@@ -2146,6 +2150,7 @@ export class ChartManager {
|
||||
} catch { /* ok */ }
|
||||
this.container.style.height = '';
|
||||
this._resetPaneHeightsCandleFullscreen(availableHeight);
|
||||
this._hideAllSubPaneIndicators();
|
||||
} else {
|
||||
this.restoreFromCandleFullscreen(availableHeight);
|
||||
}
|
||||
@@ -2166,6 +2171,8 @@ export class ChartManager {
|
||||
|
||||
this._resetMainPricePan();
|
||||
|
||||
this._restoreSubPaneIndicatorVisibility();
|
||||
|
||||
const H = (availableHeight && availableHeight > 0)
|
||||
? availableHeight
|
||||
: this.container.clientHeight;
|
||||
@@ -2181,6 +2188,62 @@ export class ChartManager {
|
||||
this.autoScale();
|
||||
}
|
||||
|
||||
/** pane 2+ 하단 보조지표 여부 (pane 0 오버레이 제외) */
|
||||
private _isSubPaneIndicator(entry: IndicatorEntry): boolean {
|
||||
return (entry.paneIndex ?? 0) >= 2;
|
||||
}
|
||||
|
||||
/** 캔들 전체보기 — 하단 보조지표 pane 시리즈·기준선 숨김 */
|
||||
private _hideSubPaneIndicator(entry: IndicatorEntry): void {
|
||||
for (const series of entry.seriesList) {
|
||||
try {
|
||||
series.applyOptions({
|
||||
visible: false,
|
||||
lastValueVisible: false,
|
||||
title: '',
|
||||
} as Parameters<ISeriesApi<SeriesType>['applyOptions']>[0]);
|
||||
} catch { /* ok */ }
|
||||
}
|
||||
const first = entry.seriesList[0];
|
||||
if (first) {
|
||||
for (const pl of entry.hlineRefs) {
|
||||
try { first.removePriceLine(pl); } catch { /* ok */ }
|
||||
}
|
||||
entry.hlineRefs = [];
|
||||
if (entry.fillPrimitive) {
|
||||
try { first.detachPrimitive(entry.fillPrimitive); } catch { /* ok */ }
|
||||
}
|
||||
}
|
||||
if (entry.cloudPlugin) {
|
||||
for (const s of entry.seriesList) {
|
||||
try { s.detachPrimitive(entry.cloudPlugin); } catch { /* ok */ }
|
||||
}
|
||||
}
|
||||
detachBbBandFill(entry);
|
||||
}
|
||||
|
||||
private _hideAllSubPaneIndicators(): void {
|
||||
for (const entry of this.indicators.values()) {
|
||||
if (this._isSubPaneIndicator(entry)) {
|
||||
this._hideSubPaneIndicator(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 캔들 전체보기 해제 — 하단 보조지표 표시·스타일 복원 */
|
||||
private _restoreSubPaneIndicatorVisibility(): void {
|
||||
for (const entry of this.indicators.values()) {
|
||||
if (!this._isSubPaneIndicator(entry)) continue;
|
||||
if (entry.cloudPlugin) {
|
||||
const host = seriesByPlotId(entry, 'plot3') ?? entry.seriesList[0];
|
||||
if (host) {
|
||||
try { host.attachPrimitive(entry.cloudPlugin); } catch { /* ok */ }
|
||||
}
|
||||
}
|
||||
this.applyIndicatorStyle(entry.config);
|
||||
}
|
||||
}
|
||||
|
||||
/** 실제 보조지표가 붙어 있는 pane index 집합 (2+) */
|
||||
private _activeIndicatorPaneIndices(): Set<number> {
|
||||
const indices = new Set<number>();
|
||||
@@ -2954,6 +3017,8 @@ export class ChartManager {
|
||||
panes[i].setStretchFactor(i === 0 ? 1 : ORPHAN_STRETCH);
|
||||
}
|
||||
|
||||
this._hideAllSubPaneIndicators();
|
||||
|
||||
return H;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/** 지표 추가 팝업 — 사용자 정의 탭 (localStorage) */
|
||||
|
||||
export interface IndicatorCustomTab {
|
||||
id: string;
|
||||
name: string;
|
||||
/** INDICATOR_REGISTRY type 목록 (표시 순서) */
|
||||
indicatorTypes: string[];
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'gc-indicator-custom-tabs-v1';
|
||||
|
||||
function newId(): string {
|
||||
return `tab-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
|
||||
}
|
||||
|
||||
export function loadCustomTabs(): IndicatorCustomTab[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return [];
|
||||
const arr = JSON.parse(raw) as unknown;
|
||||
if (!Array.isArray(arr)) return [];
|
||||
return arr.filter(
|
||||
(t): t is IndicatorCustomTab =>
|
||||
t != null
|
||||
&& typeof t === 'object'
|
||||
&& typeof (t as IndicatorCustomTab).id === 'string'
|
||||
&& typeof (t as IndicatorCustomTab).name === 'string'
|
||||
&& Array.isArray((t as IndicatorCustomTab).indicatorTypes),
|
||||
);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function saveCustomTabs(tabs: IndicatorCustomTab[]): void {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(tabs));
|
||||
}
|
||||
|
||||
export function createCustomTab(name: string, indicatorTypes: string[]): IndicatorCustomTab {
|
||||
const tab: IndicatorCustomTab = {
|
||||
id: newId(),
|
||||
name: name.trim(),
|
||||
indicatorTypes: [...new Set(indicatorTypes)],
|
||||
};
|
||||
const tabs = loadCustomTabs();
|
||||
tabs.push(tab);
|
||||
saveCustomTabs(tabs);
|
||||
return tab;
|
||||
}
|
||||
|
||||
export function updateCustomTab(
|
||||
id: string,
|
||||
patch: Partial<Pick<IndicatorCustomTab, 'name' | 'indicatorTypes'>>,
|
||||
): IndicatorCustomTab | null {
|
||||
const tabs = loadCustomTabs();
|
||||
const idx = tabs.findIndex(t => t.id === id);
|
||||
if (idx < 0) return null;
|
||||
const next: IndicatorCustomTab = {
|
||||
...tabs[idx],
|
||||
...patch,
|
||||
name: patch.name != null ? patch.name.trim() : tabs[idx].name,
|
||||
indicatorTypes: patch.indicatorTypes != null
|
||||
? [...new Set(patch.indicatorTypes)]
|
||||
: tabs[idx].indicatorTypes,
|
||||
};
|
||||
tabs[idx] = next;
|
||||
saveCustomTabs(tabs);
|
||||
return next;
|
||||
}
|
||||
|
||||
export function deleteCustomTab(id: string): void {
|
||||
saveCustomTabs(loadCustomTabs().filter(t => t.id !== id));
|
||||
}
|
||||
@@ -1,7 +1,17 @@
|
||||
import type { IndicatorCategory } from './indicatorRegistry';
|
||||
|
||||
/** 지표 선택 팝업 탭 키 */
|
||||
export type IndicatorTabKey = IndicatorCategory | 'All' | 'Main';
|
||||
export type IndicatorTabKey = 'All' | 'Main' | `custom:${string}`;
|
||||
|
||||
export function customTabKey(id: string): IndicatorTabKey {
|
||||
return `custom:${id}`;
|
||||
}
|
||||
|
||||
export function parseCustomTabKey(key: IndicatorTabKey): string | null {
|
||||
return key.startsWith('custom:') ? key.slice(7) : null;
|
||||
}
|
||||
|
||||
export function isCustomTabKey(key: IndicatorTabKey): key is `custom:${string}` {
|
||||
return key.startsWith('custom:');
|
||||
}
|
||||
|
||||
/** Main 탭 고정 지표 (표시 순서) */
|
||||
export const MAIN_INDICATOR_TYPES: readonly string[] = [
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
parsePriorExtremePeriod,
|
||||
syncPriceExtremeSimpleFields,
|
||||
} from '../utils/priceExtremeIndicators';
|
||||
import { getStrategyIndicatorDisplayName } from '../utils/strategyPaletteStorage';
|
||||
import ComboFieldSelect from '../components/strategyEditor/ComboFieldSelect';
|
||||
import {
|
||||
getCompositeFieldOpts,
|
||||
@@ -635,6 +636,7 @@ export const nodeToText = (node: LogicNode, DEF: DefType = DEF_DEFAULTS): string
|
||||
if (!node) return '';
|
||||
if (node.type === 'CONDITION' && node.condition) {
|
||||
const c = normalizeCompositeCondition(node.condition);
|
||||
const indName = getStrategyIndicatorDisplayName(c.indicatorType);
|
||||
const opts = getFieldOpts(c.indicatorType, 'buy', DEF, c);
|
||||
const C = condLabel(c.conditionType);
|
||||
if (c.composite && c.leftPeriod && c.rightPeriod) {
|
||||
@@ -647,15 +649,15 @@ export const nodeToText = (node: LogicNode, DEF: DefType = DEF_DEFAULTS): string
|
||||
const lCt = getCompositeLeftCandleType(c);
|
||||
const rCt = getCompositeRightCandleType(c);
|
||||
const tf = lCt !== rCt ? ` [${lCt}/${rCt}]` : ` [${lCt}]`;
|
||||
return `${c.indicatorType} - ${L} ${C} ${R}${tf}`;
|
||||
return `${indName} - ${L} ${C} ${R}${tf}`;
|
||||
}
|
||||
const lv = resolveFieldOptionValue(c.indicatorType, c.leftField);
|
||||
const rv = resolveFieldOptionValue(c.indicatorType, c.rightField);
|
||||
const L = opts.find(o => o.value === lv)?.label ?? c.leftField ?? c.indicatorType;
|
||||
const L = opts.find(o => o.value === lv)?.label ?? c.leftField ?? indName;
|
||||
const R = opts.find(o => o.value === rv)?.label
|
||||
?? (rv && parseThresholdField(rv) != null ? `임계(${parseThresholdField(rv)})` : rv ?? '');
|
||||
if (R && R !== '선택안함') return `${c.indicatorType} - ${L} ${C} ${R}`;
|
||||
return `${c.indicatorType} - ${L} ${C}`;
|
||||
if (R && R !== '선택안함') return `${indName} - ${L} ${C} ${R}`;
|
||||
return `${indName} - ${L} ${C}`;
|
||||
}
|
||||
if (node.type === 'AND') {
|
||||
const parts = (node.children ?? []).map(c => nodeToText(c, DEF));
|
||||
|
||||
@@ -136,6 +136,22 @@ export function resetPaletteItems(kind: PaletteItemKind): PaletteItem[] {
|
||||
return items;
|
||||
}
|
||||
|
||||
/** 전략편집기·전략빌더에 표시할 지표명 (우측 팔레트 label 우선) */
|
||||
const STRATEGY_INDICATOR_TYPE_ALIASES: Record<string, string> = {
|
||||
NEW_PSYCHOLOGICAL: 'PSYCHOLOGICAL',
|
||||
};
|
||||
|
||||
export function getStrategyIndicatorDisplayName(indicatorType: string): string {
|
||||
const resolved = STRATEGY_INDICATOR_TYPE_ALIASES[indicatorType] ?? indicatorType;
|
||||
|
||||
for (const kind of ['auxiliary', 'composite'] as const) {
|
||||
const item = loadPaletteItems(kind).find(i => i.value === resolved);
|
||||
if (item?.label) return item.label;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function createPaletteItemId(): string {
|
||||
return `custom-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user