가상수정2
This commit is contained in:
@@ -1,19 +1,27 @@
|
|||||||
import React, { memo, useEffect, useRef, useState } from 'react';
|
import React, { memo, useEffect, useRef, useState } from 'react';
|
||||||
import type { ChangeDir, TickerData } from '../../hooks/useMarketTicker';
|
import type { ChangeDir, TickerData } from '../../hooks/useMarketTicker';
|
||||||
|
|
||||||
function fmtKrw(price: number | null | undefined): string {
|
function toFiniteNumber(v: unknown): number | null {
|
||||||
if (price == null || !Number.isFinite(price)) return '-';
|
if (v == null || v === '') return null;
|
||||||
if (price >= 1_000_000) return price.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
|
const n = typeof v === 'number' ? v : Number(v);
|
||||||
if (price >= 1_000) return price.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
|
return Number.isFinite(n) ? n : null;
|
||||||
if (price >= 1) return price.toLocaleString('ko-KR', { maximumFractionDigits: 2 });
|
|
||||||
if (price >= 0.01) return price.toFixed(4);
|
|
||||||
return price.toFixed(8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtPct(rate: number | null | undefined): string {
|
function fmtKrw(price: number | null | undefined | unknown): string {
|
||||||
if (rate == null || !Number.isFinite(rate)) return '-';
|
const p = toFiniteNumber(price);
|
||||||
const sign = rate >= 0 ? '+' : '';
|
if (p == null) return '-';
|
||||||
return `${sign}${(rate * 100).toFixed(2)}%`;
|
if (p >= 1_000_000) return p.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
|
||||||
|
if (p >= 1_000) return p.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
|
||||||
|
if (p >= 1) return p.toLocaleString('ko-KR', { maximumFractionDigits: 2 });
|
||||||
|
if (p >= 0.01) return p.toFixed(4);
|
||||||
|
return p.toFixed(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtPct(rate: number | null | undefined | unknown): string {
|
||||||
|
const r = toFiniteNumber(rate);
|
||||||
|
if (r == null) return '-';
|
||||||
|
const sign = r >= 0 ? '+' : '';
|
||||||
|
return `${sign}${(r * 100).toFixed(2)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChangeArrow = memo(function ChangeArrow({ change }: { change: ChangeDir }) {
|
const ChangeArrow = memo(function ChangeArrow({ change }: { change: ChangeDir }) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '../utils/backendApi';
|
} from '../utils/backendApi';
|
||||||
import { formatIndicatorDisplayLabel } from '../utils/indicatorRegistry';
|
import { formatIndicatorDisplayLabel } from '../utils/indicatorRegistry';
|
||||||
import {
|
import {
|
||||||
|
coerceFiniteNumber,
|
||||||
extractVirtualConditions,
|
extractVirtualConditions,
|
||||||
type VirtualConditionRow,
|
type VirtualConditionRow,
|
||||||
} from '../utils/virtualStrategyConditions';
|
} from '../utils/virtualStrategyConditions';
|
||||||
@@ -42,13 +43,13 @@ function liveRowToVirtual(r: LiveConditionRowDto): VirtualConditionRow & { curre
|
|||||||
displayName: formatIndicatorDisplayLabel(r.indicatorType),
|
displayName: formatIndicatorDisplayLabel(r.indicatorType),
|
||||||
conditionType: r.conditionType,
|
conditionType: r.conditionType,
|
||||||
conditionLabel: r.conditionLabel,
|
conditionLabel: r.conditionLabel,
|
||||||
targetValue: r.targetValue,
|
targetValue: coerceFiniteNumber(r.targetValue),
|
||||||
timeframe: r.timeframe,
|
timeframe: r.timeframe,
|
||||||
side: r.side,
|
side: r.side,
|
||||||
plotKey,
|
plotKey,
|
||||||
satisfied: r.satisfied,
|
satisfied: r.satisfied,
|
||||||
thresholdLabel: r.thresholdLabel,
|
thresholdLabel: r.thresholdLabel,
|
||||||
currentValue: r.currentValue,
|
currentValue: coerceFiniteNumber(r.currentValue),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,8 +77,8 @@ function mergeRows(
|
|||||||
...row,
|
...row,
|
||||||
satisfied: liveRow.satisfied,
|
satisfied: liveRow.satisfied,
|
||||||
thresholdLabel: liveRow.thresholdLabel,
|
thresholdLabel: liveRow.thresholdLabel,
|
||||||
currentValue: liveRow.currentValue,
|
currentValue: coerceFiniteNumber(liveRow.currentValue),
|
||||||
targetValue: liveRow.targetValue ?? row.targetValue,
|
targetValue: coerceFiniteNumber(liveRow.targetValue) ?? row.targetValue,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -102,7 +103,12 @@ async function buildSnapshot(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = await fetchLiveConditionStatus(market, strategyId);
|
let status: Awaited<ReturnType<typeof fetchLiveConditionStatus>> = null;
|
||||||
|
try {
|
||||||
|
status = await fetchLiveConditionStatus(market, strategyId);
|
||||||
|
} catch {
|
||||||
|
status = null;
|
||||||
|
}
|
||||||
if (!status || status.market !== market || status.strategyId !== strategyId) {
|
if (!status || status.market !== market || status.strategyId !== strategyId) {
|
||||||
if (baseRows.length === 0) return null;
|
if (baseRows.length === 0) return null;
|
||||||
return {
|
return {
|
||||||
@@ -121,7 +127,7 @@ async function buildSnapshot(
|
|||||||
timeframe: status.timeframe || [...new Set(baseRows.map(r => r.timeframe))].join(', '),
|
timeframe: status.timeframe || [...new Set(baseRows.map(r => r.timeframe))].join(', '),
|
||||||
rows: mergeRows(baseRows, status.rows),
|
rows: mergeRows(baseRows, status.rows),
|
||||||
updatedAt: status.updatedAt || Date.now(),
|
updatedAt: status.updatedAt || Date.now(),
|
||||||
matchRate: status.matchRate,
|
matchRate: coerceFiniteNumber(status.matchRate) ?? 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
coerceFiniteNumber,
|
||||||
formatIndicatorValue,
|
formatIndicatorValue,
|
||||||
isConditionMet,
|
isConditionMet,
|
||||||
type VirtualConditionRow,
|
type VirtualConditionRow,
|
||||||
@@ -33,37 +34,39 @@ const PENDING_PROGRESS = 72;
|
|||||||
/** 목표값 대비 현재값 접근률 (0~100, 충족 시 100) */
|
/** 목표값 대비 현재값 접근률 (0~100, 충족 시 100) */
|
||||||
export function computeConditionProgress(
|
export function computeConditionProgress(
|
||||||
conditionType: string,
|
conditionType: string,
|
||||||
current: number | null,
|
current: number | null | unknown,
|
||||||
target: number | null,
|
target: number | null | unknown,
|
||||||
): number | null {
|
): number | null {
|
||||||
if (current == null || target == null) return null;
|
const cur = coerceFiniteNumber(current);
|
||||||
const met = isConditionMet(conditionType, current, target);
|
const tgt = coerceFiniteNumber(target);
|
||||||
|
if (cur == null || tgt == null) return null;
|
||||||
|
const met = isConditionMet(conditionType, cur, tgt);
|
||||||
if (met === true) return 100;
|
if (met === true) return 100;
|
||||||
|
|
||||||
const absTarget = Math.abs(target);
|
const absTarget = Math.abs(tgt);
|
||||||
const scale = absTarget > 1e-9 ? absTarget : Math.max(Math.abs(current), 1);
|
const scale = absTarget > 1e-9 ? absTarget : Math.max(Math.abs(cur), 1);
|
||||||
|
|
||||||
switch (conditionType) {
|
switch (conditionType) {
|
||||||
case 'LT':
|
case 'LT':
|
||||||
case 'CROSS_DOWN':
|
case 'CROSS_DOWN':
|
||||||
case 'LTE': {
|
case 'LTE': {
|
||||||
if (current <= target) return 100;
|
if (cur <= tgt) return 100;
|
||||||
const gap = current - target;
|
const gap = cur - tgt;
|
||||||
return Math.min(99, Math.max(0, 100 - (gap / scale) * 100));
|
return Math.min(99, Math.max(0, 100 - (gap / scale) * 100));
|
||||||
}
|
}
|
||||||
case 'GT':
|
case 'GT':
|
||||||
case 'CROSS_UP':
|
case 'CROSS_UP':
|
||||||
case 'GTE': {
|
case 'GTE': {
|
||||||
if (current >= target) return 100;
|
if (cur >= tgt) return 100;
|
||||||
if (target <= 0 && current <= 0) return 0;
|
if (tgt <= 0 && cur <= 0) return 0;
|
||||||
return Math.min(99, Math.max(0, (current / scale) * 100));
|
return Math.min(99, Math.max(0, (cur / scale) * 100));
|
||||||
}
|
}
|
||||||
case 'EQ': {
|
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));
|
return Math.min(99, Math.max(0, 100 - (diff / scale) * 100));
|
||||||
}
|
}
|
||||||
default:
|
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 우선 */
|
/** 전체 매매조건 일치율 — 백엔드 matchRate 우선 */
|
||||||
export function computeMatchRate(
|
export function computeMatchRate(
|
||||||
metrics: ConditionMetric[],
|
metrics: ConditionMetric[],
|
||||||
backendMatchRate?: number,
|
backendMatchRate?: number | unknown,
|
||||||
): number {
|
): number {
|
||||||
if (backendMatchRate != null && Number.isFinite(backendMatchRate)) {
|
const backend = coerceFiniteNumber(backendMatchRate);
|
||||||
return Math.round(Math.min(100, Math.max(0, backendMatchRate)));
|
if (backend != null) {
|
||||||
|
return Math.round(Math.min(100, Math.max(0, backend)));
|
||||||
}
|
}
|
||||||
const evaluable = metrics.filter(m => m.status !== 'unknown');
|
const evaluable = metrics.filter(m => m.status !== 'unknown');
|
||||||
if (evaluable.length === 0) return 0;
|
if (evaluable.length === 0) return 0;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ function walk(
|
|||||||
seen.add(key);
|
seen.add(key);
|
||||||
|
|
||||||
const condLabel = CONDITION_LABEL[c.conditionType] ?? c.conditionType;
|
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;
|
const plotKey = c.leftField && c.leftField !== 'none' ? c.leftField : c.indicatorType;
|
||||||
|
|
||||||
out.push({
|
out.push({
|
||||||
@@ -71,6 +71,14 @@ function walk(
|
|||||||
node.children?.forEach(c => walk(c, timeframe, side, out, seen));
|
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[] {
|
export function extractVirtualConditions(strategy: StrategyDto | null | undefined): VirtualConditionRow[] {
|
||||||
if (!strategy) return [];
|
if (!strategy) return [];
|
||||||
const out: VirtualConditionRow[] = [];
|
const out: VirtualConditionRow[] = [];
|
||||||
@@ -83,23 +91,26 @@ export function extractVirtualConditions(strategy: StrategyDto | null | undefine
|
|||||||
/** 조건 충족 여부 (단순 임계값 비교) */
|
/** 조건 충족 여부 (단순 임계값 비교) */
|
||||||
export function isConditionMet(
|
export function isConditionMet(
|
||||||
conditionType: string,
|
conditionType: string,
|
||||||
current: number | null,
|
current: number | null | unknown,
|
||||||
target: number | null,
|
target: number | null | unknown,
|
||||||
): boolean | null {
|
): 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) {
|
switch (conditionType) {
|
||||||
case 'GT': case 'CROSS_UP': return current > target;
|
case 'GT': case 'CROSS_UP': return cur > tgt;
|
||||||
case 'LT': case 'CROSS_DOWN': return current < target;
|
case 'LT': case 'CROSS_DOWN': return cur < tgt;
|
||||||
case 'GTE': return current >= target;
|
case 'GTE': return cur >= tgt;
|
||||||
case 'LTE': return current <= target;
|
case 'LTE': return cur <= tgt;
|
||||||
case 'EQ': return Math.abs(current - target) < 1e-6;
|
case 'EQ': return Math.abs(cur - tgt) < 1e-6;
|
||||||
case 'NEQ': return Math.abs(current - target) >= 1e-6;
|
case 'NEQ': return Math.abs(cur - tgt) >= 1e-6;
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatIndicatorValue(v: number | null): string {
|
export function formatIndicatorValue(v: number | null | unknown): string {
|
||||||
if (v == null || Number.isNaN(v)) return '—';
|
const n = coerceFiniteNumber(v);
|
||||||
if (Math.abs(v) >= 1000) return v.toLocaleString(undefined, { maximumFractionDigits: 0 });
|
if (n == null) return '—';
|
||||||
return v.toFixed(2);
|
if (Math.abs(n) >= 1000) return n.toLocaleString(undefined, { maximumFractionDigits: 0 });
|
||||||
|
return n.toFixed(2);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user