가상 수정

This commit is contained in:
Macbook
2026-05-25 16:02:52 +09:00
parent 3102169541
commit 182b82e990
18 changed files with 747 additions and 105 deletions
+1
View File
@@ -1083,6 +1083,7 @@ export interface LiveConditionRowDto {
satisfied: boolean | null;
timeframe: string;
side: 'buy' | 'sell';
plotKey?: string;
}
export interface LiveConditionStatusDto {
+10
View File
@@ -384,6 +384,16 @@ export function getIndicatorDef(type: string): IndicatorDef | undefined {
return INDICATOR_REGISTRY.find(d => d.type === type);
}
/** 가상투자·조건 목록 — 한글명 (영문약어) 예: 상품채널지수 (CCI) */
export function formatIndicatorDisplayLabel(indicatorType: string): string {
const def = getIndicatorDef(indicatorType);
const abbr = def?.shortName ?? indicatorType;
const ko = def?.koreanName ?? abbr;
if (!ko || ko === abbr) return abbr;
if (ko.includes(`(${abbr})`)) return ko;
return `${ko} (${abbr})`;
}
/** 실시간 차트 pane·범례 타이틀 (심리도·투자심리도·이격도 등 한글명 우선) */
export function getIndicatorChartTitle(type: string): string {
const resolved = type === 'NewPsychological' ? 'Psychological' : type;
+58 -14
View File
@@ -9,9 +9,23 @@ export type ConditionStatus = 'match' | 'pending' | 'nomatch' | 'unknown';
export interface ConditionMetric {
row: VirtualConditionRow & { currentValue: number | null };
status: ConditionStatus;
/** 조건 매칭율 0~100 */
matchRate: number;
/** @deprecated matchRate 와 동일 — 하위 호환 */
progress: number | null;
}
export type HeatTier = 'nomatch' | 'pending' | 'match';
/** Heatmap 색상 — Mismatch(빨강) · Pending(오렌지) · Match(푸른색) */
export function resolveHeatTier(matchRate: number, status: ConditionStatus): HeatTier {
if (status === 'match' || matchRate >= 100) return 'match';
if (status === 'pending') return 'pending';
if (status === 'nomatch') return 'nomatch';
if (matchRate >= 45) return 'pending';
return 'nomatch';
}
export type TrafficLightState = 'red' | 'yellow' | 'blue';
const PENDING_PROGRESS = 72;
@@ -53,6 +67,35 @@ export function computeConditionProgress(
}
}
const CROSS_CONDITION_TYPES = new Set([
'CROSS_UP', 'CROSS_DOWN', 'SLOPE_UP', 'SLOPE_DOWN',
]);
/** 조건별 매칭율 (0~100) — 백엔드 Ta4j satisfied 우선 */
export function computeConditionMatchRate(
row: VirtualConditionRow & { currentValue: number | null },
): number {
if (row.satisfied === true) return 100;
if (row.satisfied === false) {
if (CROSS_CONDITION_TYPES.has(row.conditionType)) return 0;
const progress = computeConditionProgress(
row.conditionType,
row.currentValue,
row.targetValue,
);
if (progress == null) return 0;
return Math.min(99, Math.max(0, Math.round(progress)));
}
if (row.currentValue == null) return 0;
const progress = computeConditionProgress(
row.conditionType,
row.currentValue,
row.targetValue,
);
if (progress == null) return 0;
return Math.min(99, Math.max(0, Math.round(progress)));
}
export function getConditionStatus(
conditionType: string,
current: number | null,
@@ -61,6 +104,7 @@ export function getConditionStatus(
): ConditionStatus {
if (satisfied === true) return 'match';
if (satisfied === false) {
if (CROSS_CONDITION_TYPES.has(conditionType)) return 'nomatch';
const progress = computeConditionProgress(conditionType, current, target);
if (progress != null && progress >= PENDING_PROGRESS) return 'pending';
return 'nomatch';
@@ -105,20 +149,20 @@ export function formatStrategyThreshold(
export function buildConditionMetrics(
rows: Array<VirtualConditionRow & { currentValue: number | null }>,
): ConditionMetric[] {
return rows.map(row => ({
row,
status: getConditionStatus(
row.conditionType,
row.currentValue,
row.targetValue,
row.satisfied,
),
progress: row.satisfied === true
? 100
: row.satisfied === false
? (computeConditionProgress(row.conditionType, row.currentValue, row.targetValue) ?? 0)
: computeConditionProgress(row.conditionType, row.currentValue, row.targetValue),
}));
return rows.map(row => {
const matchRate = computeConditionMatchRate(row);
return {
row,
status: getConditionStatus(
row.conditionType,
row.currentValue,
row.targetValue,
row.satisfied,
),
matchRate,
progress: matchRate,
};
});
}
/** 전체 매매조건 일치율 — 백엔드 matchRate 우선 */
@@ -4,7 +4,7 @@
import type { LogicNode } from './strategyTypes';
import { CONDITION_LABEL } from './strategyTypes';
import type { StrategyDto } from './backendApi';
import { getIndicatorDef } from './indicatorRegistry';
import { formatIndicatorDisplayLabel } from './indicatorRegistry';
export interface VirtualConditionRow {
id: string;
@@ -38,13 +38,18 @@ function walk(
return;
}
if (node.type === 'NOT') {
const notChild = node.children?.[0] ?? (node as LogicNode & { child?: LogicNode }).child;
if (notChild) walk(notChild, timeframe, side, out, seen);
return;
}
if (node.type === 'CONDITION' && node.condition) {
const c = node.condition;
const key = `${side}:${timeframe}:${c.indicatorType}:${c.conditionType}:${c.targetValue ?? ''}:${c.leftField ?? ''}`;
if (seen.has(key)) return;
seen.add(key);
const def = getIndicatorDef(c.indicatorType);
const condLabel = CONDITION_LABEL[c.conditionType] ?? c.conditionType;
const target = c.targetValue ?? c.compareValue ?? null;
const plotKey = c.leftField && c.leftField !== 'none' ? c.leftField : c.indicatorType;
@@ -52,7 +57,7 @@ function walk(
out.push({
id: `${node.id}-${side}`,
indicatorType: c.indicatorType,
displayName: def?.koreanName ?? def?.shortName ?? c.indicatorType,
displayName: formatIndicatorDisplayLabel(c.indicatorType),
conditionType: c.conditionType,
conditionLabel: condLabel,
targetValue: target,