가상수정2

This commit is contained in:
Macbook
2026-05-25 16:13:25 +09:00
parent 182b82e990
commit aac6454724
4 changed files with 76 additions and 47 deletions
+20 -16
View File
@@ -1,4 +1,5 @@
import {
coerceFiniteNumber,
formatIndicatorValue,
isConditionMet,
type VirtualConditionRow,
@@ -33,37 +34,39 @@ const PENDING_PROGRESS = 72;
/** 목표값 대비 현재값 접근률 (0~100, 충족 시 100) */
export function computeConditionProgress(
conditionType: string,
current: number | null,
target: number | null,
current: number | null | unknown,
target: number | null | unknown,
): number | null {
if (current == null || target == null) return null;
const met = isConditionMet(conditionType, current, target);
const cur = coerceFiniteNumber(current);
const tgt = coerceFiniteNumber(target);
if (cur == null || tgt == null) return null;
const met = isConditionMet(conditionType, cur, tgt);
if (met === true) return 100;
const absTarget = Math.abs(target);
const scale = absTarget > 1e-9 ? absTarget : Math.max(Math.abs(current), 1);
const absTarget = Math.abs(tgt);
const scale = absTarget > 1e-9 ? absTarget : Math.max(Math.abs(cur), 1);
switch (conditionType) {
case 'LT':
case 'CROSS_DOWN':
case 'LTE': {
if (current <= target) return 100;
const gap = current - target;
if (cur <= tgt) return 100;
const gap = cur - tgt;
return Math.min(99, Math.max(0, 100 - (gap / scale) * 100));
}
case 'GT':
case 'CROSS_UP':
case 'GTE': {
if (current >= target) return 100;
if (target <= 0 && current <= 0) return 0;
return Math.min(99, Math.max(0, (current / scale) * 100));
if (cur >= tgt) return 100;
if (tgt <= 0 && cur <= 0) return 0;
return Math.min(99, Math.max(0, (cur / scale) * 100));
}
case 'EQ': {
const diff = Math.abs(current - target);
const diff = Math.abs(cur - tgt);
return Math.min(99, Math.max(0, 100 - (diff / scale) * 100));
}
default:
return Math.min(99, Math.max(0, 100 - (Math.abs(current - target) / scale) * 100));
return Math.min(99, Math.max(0, 100 - (Math.abs(cur - tgt) / scale) * 100));
}
}
@@ -168,10 +171,11 @@ export function buildConditionMetrics(
/** 전체 매매조건 일치율 — 백엔드 matchRate 우선 */
export function computeMatchRate(
metrics: ConditionMetric[],
backendMatchRate?: number,
backendMatchRate?: number | unknown,
): number {
if (backendMatchRate != null && Number.isFinite(backendMatchRate)) {
return Math.round(Math.min(100, Math.max(0, backendMatchRate)));
const backend = coerceFiniteNumber(backendMatchRate);
if (backend != null) {
return Math.round(Math.min(100, Math.max(0, backend)));
}
const evaluable = metrics.filter(m => m.status !== 'unknown');
if (evaluable.length === 0) return 0;
+25 -14
View File
@@ -51,7 +51,7 @@ function walk(
seen.add(key);
const condLabel = CONDITION_LABEL[c.conditionType] ?? c.conditionType;
const target = c.targetValue ?? c.compareValue ?? null;
const target = coerceFiniteNumber(c.targetValue ?? c.compareValue ?? null);
const plotKey = c.leftField && c.leftField !== 'none' ? c.leftField : c.indicatorType;
out.push({
@@ -71,6 +71,14 @@ function walk(
node.children?.forEach(c => walk(c, timeframe, side, out, seen));
}
/** API·DSL에서 문자열로 올 수 있는 값을 안전하게 숫자로 변환 */
export function coerceFiniteNumber(v: unknown): number | null {
if (v == null || v === '') return null;
if (typeof v === 'number') return Number.isFinite(v) ? v : null;
const n = Number(v);
return Number.isFinite(n) ? n : null;
}
export function extractVirtualConditions(strategy: StrategyDto | null | undefined): VirtualConditionRow[] {
if (!strategy) return [];
const out: VirtualConditionRow[] = [];
@@ -83,23 +91,26 @@ export function extractVirtualConditions(strategy: StrategyDto | null | undefine
/** 조건 충족 여부 (단순 임계값 비교) */
export function isConditionMet(
conditionType: string,
current: number | null,
target: number | null,
current: number | null | unknown,
target: number | null | unknown,
): boolean | null {
if (current == null || target == null) return null;
const cur = coerceFiniteNumber(current);
const tgt = coerceFiniteNumber(target);
if (cur == null || tgt == null) return null;
switch (conditionType) {
case 'GT': case 'CROSS_UP': return current > target;
case 'LT': case 'CROSS_DOWN': return current < target;
case 'GTE': return current >= target;
case 'LTE': return current <= target;
case 'EQ': return Math.abs(current - target) < 1e-6;
case 'NEQ': return Math.abs(current - target) >= 1e-6;
case 'GT': case 'CROSS_UP': return cur > tgt;
case 'LT': case 'CROSS_DOWN': return cur < tgt;
case 'GTE': return cur >= tgt;
case 'LTE': return cur <= tgt;
case 'EQ': return Math.abs(cur - tgt) < 1e-6;
case 'NEQ': return Math.abs(cur - tgt) >= 1e-6;
default: return null;
}
}
export function formatIndicatorValue(v: number | null): string {
if (v == null || Number.isNaN(v)) return '—';
if (Math.abs(v) >= 1000) return v.toLocaleString(undefined, { maximumFractionDigits: 0 });
return v.toFixed(2);
export function formatIndicatorValue(v: number | null | unknown): string {
const n = coerceFiniteNumber(v);
if (n == null) return '—';
if (Math.abs(n) >= 1000) return n.toLocaleString(undefined, { maximumFractionDigits: 0 });
return n.toFixed(2);
}