5d96417c42
- isIchimokuCloudVisible: bullish/bearish 둘 다 false이면 DB 잘못된 저장값으로 간주하고 구름을 표시 (하나만 false인 경우는 사용자 의도로 유지) - _applyIchimokuCloudPlugin: 동일 로직으로 effectiveBullish/effectiveBearish 적용 - config.hidden 체크로만 클라우드 데이터 초기화 - isIchimokuCloudVisible 의존 제거 서버 DB에 cloudColors.bullishVisible=false, bearishVisible=false 가 잘못 저장된 경우에도 구름이 표시되도록 보호 로직 추가. Co-authored-by: Cursor <cursoragent@cursor.com>
217 lines
7.2 KiB
TypeScript
217 lines
7.2 KiB
TypeScript
/**
|
|
* 일목균형표(IchimokuCloud) 설정
|
|
*/
|
|
import type { IndicatorConfig } from '../types';
|
|
import type { PlotDef } from './indicatorRegistry';
|
|
|
|
/** 설정 화면 한 줄 = 플롯 1개 + (선택) 기간 파라미터 */
|
|
export interface IchimokuLineRowDef {
|
|
plotIndex: number;
|
|
plotId: string;
|
|
/** 플롯 행 기간 입력에 쓰는 params 키 */
|
|
paramKey?: string;
|
|
}
|
|
|
|
/** registry plot ID → 차트·설정 화면 한글 라벨 */
|
|
export const ICHIMOKU_PLOT_TITLES: Record<string, string> = {
|
|
plot0: '전환선',
|
|
plot1: '기준선',
|
|
plot2: '후행스팬',
|
|
plot3: '선행스팬1',
|
|
plot4: '선행스팬2',
|
|
};
|
|
|
|
export function getIchimokuPlotTitle(plotId: string): string {
|
|
return ICHIMOKU_PLOT_TITLES[plotId] ?? plotId;
|
|
}
|
|
|
|
export const ICHIMOKU_LINE_ROWS: IchimokuLineRowDef[] = [
|
|
{ plotIndex: 0, plotId: 'plot0', paramKey: 'conversionPeriods' },
|
|
{ plotIndex: 1, plotId: 'plot1', paramKey: 'basePeriods' },
|
|
{ plotIndex: 2, plotId: 'plot2', paramKey: 'chikouDisplacement' },
|
|
{ plotIndex: 3, plotId: 'plot3', paramKey: 'senkouDisplacement' },
|
|
{ plotIndex: 4, plotId: 'plot4', paramKey: 'laggingSpan2Periods' },
|
|
];
|
|
|
|
export const ICHIMOKU_DEFAULT_PARAMS: Record<string, number> = {
|
|
conversionPeriods: 9,
|
|
basePeriods: 26,
|
|
laggingSpan2Periods: 52,
|
|
/** @deprecated senkouDisplacement 우선 — API·구버전 호환 */
|
|
displacement: 26,
|
|
chikouDisplacement: 26,
|
|
senkouDisplacement: 26,
|
|
};
|
|
|
|
/** 후행·선행 이동 기간 (구 displacement 단일 키 마이그레이션) */
|
|
export function resolveIchimokuDisplacements(
|
|
params: Record<string, number | string | boolean> | undefined,
|
|
): { chikou: number; senkou: number } {
|
|
const legacy = typeof params?.displacement === 'number' && params.displacement >= 1
|
|
? params.displacement
|
|
: 26;
|
|
const chikou = typeof params?.chikouDisplacement === 'number' && params.chikouDisplacement >= 1
|
|
? params.chikouDisplacement
|
|
: legacy;
|
|
const senkou = typeof params?.senkouDisplacement === 'number' && params.senkouDisplacement >= 1
|
|
? params.senkouDisplacement
|
|
: legacy;
|
|
return { chikou, senkou };
|
|
}
|
|
|
|
export function createDefaultIchimokuParams(): Record<string, number> {
|
|
return { ...ICHIMOKU_DEFAULT_PARAMS };
|
|
}
|
|
|
|
export function createDefaultIchimokuPlotVisibility(): Record<string, boolean> {
|
|
const vis: Record<string, boolean> = {};
|
|
for (const row of ICHIMOKU_LINE_ROWS) vis[row.plotId] = true;
|
|
return vis;
|
|
}
|
|
|
|
export interface IchimokuCloudColors {
|
|
/** 선행스팬1 > 선행스팬2 (상승 구름) */
|
|
bullishColor: string;
|
|
/** 선행스팬1 < 선행스팬2 (하락 구름) */
|
|
bearishColor: string;
|
|
/** 상승 구름 영역 표시 */
|
|
bullishVisible?: boolean;
|
|
/** 하락 구름 영역 표시 */
|
|
bearishVisible?: boolean;
|
|
}
|
|
|
|
/** #RRGGBBAA — 상승 구름 빨강 20%, 하락 구름 청록 20% */
|
|
export const DEFAULT_ICHIMOKU_CLOUD_COLORS: IchimokuCloudColors = {
|
|
bullishColor: '#EF535033',
|
|
bearishColor: '#26A69A33',
|
|
bullishVisible: true,
|
|
bearishVisible: true,
|
|
};
|
|
|
|
/** registry plot ID → lightweight-charts-indicators plot ID */
|
|
export const ICHIMOKU_REGISTRY_TO_LIB: Record<string, string> = {
|
|
plot0: 'plot0',
|
|
plot1: 'plot1',
|
|
plot2: 'plot3',
|
|
plot3: 'plot4',
|
|
plot4: 'plot2',
|
|
};
|
|
|
|
export const ICHIMOKU_LIB_TO_REGISTRY: Record<string, string> = {
|
|
plot0: 'plot0',
|
|
plot1: 'plot1',
|
|
plot2: 'plot4',
|
|
plot3: 'plot2',
|
|
plot4: 'plot3',
|
|
};
|
|
|
|
/** 선행스팬1·2 라인이 켜져 있어 구름 계산이 가능한지 */
|
|
export function areIchimokuSpanLinesVisible(config: IndicatorConfig): boolean {
|
|
return (
|
|
config.plotVisibility?.['plot3'] !== false &&
|
|
config.plotVisibility?.['plot4'] !== false
|
|
);
|
|
}
|
|
|
|
/** 구름 영역을 그릴 수 있는지 (지표 표시 on + 구름 색상 설정)
|
|
*
|
|
* 선행스팬 라인의 개별 표시 여부(plotVisibility)와 구름 표시는 독립적으로 동작.
|
|
* 서버 DB에 저장된 구버전 설정에서 plot3·plot4 가 false 여도 구름은 표시해야 하므로
|
|
* areIchimokuSpanLinesVisible 검사를 생략한다.
|
|
*
|
|
* bullishVisible/bearishVisible 이 둘 다 false 이면 DB 잘못된 저장값으로 간주하고
|
|
* 구름을 표시한다 (하나만 false 인 경우는 사용자 의도이므로 유지).
|
|
*/
|
|
export function isIchimokuCloudVisible(config: IndicatorConfig): boolean {
|
|
if (config.hidden === true) return false;
|
|
const colors = resolveIchimokuCloudColors(config.cloudColors);
|
|
const bullishVis = colors.bullishVisible !== false;
|
|
const bearishVis = colors.bearishVisible !== false;
|
|
// 둘 다 false → DB 잘못된 저장값, 기본값으로 복원 간주 → 표시
|
|
if (!bullishVis && !bearishVis) return true;
|
|
return bullishVis || bearishVis;
|
|
}
|
|
|
|
export function resolveIchimokuCloudColors(
|
|
colors?: Partial<IchimokuCloudColors>,
|
|
): IchimokuCloudColors {
|
|
return {
|
|
bullishColor: colors?.bullishColor ?? DEFAULT_ICHIMOKU_CLOUD_COLORS.bullishColor,
|
|
bearishColor: colors?.bearishColor ?? DEFAULT_ICHIMOKU_CLOUD_COLORS.bearishColor,
|
|
bullishVisible: colors?.bullishVisible !== false,
|
|
bearishVisible: colors?.bearishVisible !== false,
|
|
};
|
|
}
|
|
|
|
export function normalizeIchimokuConfig(config: IndicatorConfig): IndicatorConfig {
|
|
if (config.type !== 'IchimokuCloud') return config;
|
|
|
|
const params: Record<string, number | string | boolean> = {
|
|
...createDefaultIchimokuParams(),
|
|
...config.params,
|
|
};
|
|
for (const key of Object.keys(ICHIMOKU_DEFAULT_PARAMS)) {
|
|
const v = params[key];
|
|
if (typeof v !== 'number' || v < 1) {
|
|
params[key] = ICHIMOKU_DEFAULT_PARAMS[key];
|
|
}
|
|
}
|
|
|
|
const { chikou, senkou } = resolveIchimokuDisplacements(params);
|
|
params.chikouDisplacement = chikou;
|
|
params.senkouDisplacement = senkou;
|
|
params.displacement = senkou;
|
|
|
|
const plotVisibility: Record<string, boolean> = {
|
|
...createDefaultIchimokuPlotVisibility(),
|
|
...config.plotVisibility,
|
|
};
|
|
|
|
return {
|
|
...config,
|
|
params,
|
|
plotVisibility,
|
|
cloudColors: resolveIchimokuCloudColors(config.cloudColors),
|
|
};
|
|
}
|
|
|
|
/** registry 기본 플롯과 병합된 일목 플롯 목록 */
|
|
export function mergeIchimokuPlots(
|
|
saved: PlotDef[] | undefined,
|
|
defaults: PlotDef[],
|
|
): PlotDef[] {
|
|
const savedMap = new Map((saved ?? []).map(p => [p.id, p]));
|
|
return defaults.map(def => {
|
|
const s = savedMap.get(def.id);
|
|
const title = getIchimokuPlotTitle(def.id);
|
|
return s
|
|
? { ...def, ...s, id: def.id, title, type: 'line' as const }
|
|
: { ...def, title };
|
|
});
|
|
}
|
|
|
|
/** #RRGGBB 또는 #RRGGBBAA → rgba(r,g,b,a) 표시 문자열 */
|
|
export function colorToRgbaString(color: string): string {
|
|
if (color.startsWith('rgba(')) return color;
|
|
const hex = color.startsWith('#') ? color.slice(1) : color;
|
|
if (hex.length < 6) return color;
|
|
const r = parseInt(hex.slice(0, 2), 16);
|
|
const g = parseInt(hex.slice(2, 4), 16);
|
|
const b = parseInt(hex.slice(4, 6), 16);
|
|
const a = hex.length >= 8
|
|
? Math.round(parseInt(hex.slice(6, 8), 16) / 255 * 100) / 100
|
|
: 1;
|
|
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
}
|
|
|
|
export function cloudColorOpacityPercent(color: string): number {
|
|
if (color.startsWith('rgba(')) {
|
|
const m = color.match(/,\s*([\d.]+)\s*\)$/);
|
|
return m ? Math.round(parseFloat(m[1]) * 100) : 100;
|
|
}
|
|
if (color.length >= 9) {
|
|
return Math.round(parseInt(color.slice(7, 9), 16) / 255 * 100);
|
|
}
|
|
return 100;
|
|
}
|