전략편집기 수정
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import type { ConditionStatus } from '../../utils/virtualSignalMetrics';
|
||||
import { coerceFiniteNumber } from '../../utils/safeFormat';
|
||||
|
||||
interface Props {
|
||||
status: ConditionStatus;
|
||||
@@ -22,10 +23,8 @@ const ConditionStatusSignal: React.FC<Props> = ({ status, matchRate, progress, c
|
||||
return <span className="vtd-cond-signal vtd-cond-signal--unknown">—</span>;
|
||||
}
|
||||
|
||||
const rate = matchRate ?? progress;
|
||||
const pct = rate != null && Number.isFinite(rate)
|
||||
? `${Math.round(rate)}%`
|
||||
: null;
|
||||
const rate = coerceFiniteNumber(matchRate ?? progress);
|
||||
const pct = rate != null ? `${Math.round(rate)}%` : null;
|
||||
|
||||
return (
|
||||
<span className={`vtd-cond-signal vtd-cond-signal--${status}${compact ? ' vtd-cond-signal--compact' : ''}`}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import type { ConditionMetric } from '../../utils/virtualSignalMetrics';
|
||||
import { resolveHeatTier } from '../../utils/virtualSignalMetrics';
|
||||
import { coerceFiniteNumber } from '../../utils/safeFormat';
|
||||
|
||||
interface Props {
|
||||
metrics: ConditionMetric[];
|
||||
@@ -41,7 +42,7 @@ const VirtualConditionList: React.FC<Props> = ({ metrics }) => {
|
||||
<ul className="vtd-heat-list" aria-label="전략 지표 매치율">
|
||||
{metrics.map(({ row, status, matchRate }) => {
|
||||
const tier = resolveHeatTier(matchRate, status);
|
||||
const pct = Math.min(100, Math.max(0, matchRate));
|
||||
const pct = Math.min(100, Math.max(0, coerceFiniteNumber(matchRate) ?? 0));
|
||||
const minFill = pct > 0 && tier === 'nomatch' ? 6 : pct > 0 ? 6 : 0;
|
||||
return (
|
||||
<li key={row.id} className={`vtd-heat-row vtd-heat-row--${tier}`}>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import type { ConditionMetric } from '../../utils/virtualSignalMetrics';
|
||||
import { formatStrategyThreshold, resolveHeatTier } from '../../utils/virtualSignalMetrics';
|
||||
import { formatIndicatorValue } from '../../utils/virtualStrategyConditions';
|
||||
import { coerceFiniteNumber } from '../../utils/safeFormat';
|
||||
import ConditionStatusSignal from './ConditionStatusSignal';
|
||||
|
||||
interface Props {
|
||||
@@ -30,7 +31,8 @@ const VirtualIndicatorCompareTable: React.FC<Props> = ({ metrics }) => (
|
||||
</thead>
|
||||
<tbody>
|
||||
{metrics.map(({ row, status, matchRate }) => {
|
||||
const tier = resolveHeatTier(matchRate, status);
|
||||
const rate = coerceFiniteNumber(matchRate) ?? 0;
|
||||
const tier = resolveHeatTier(rate, status);
|
||||
return (
|
||||
<tr key={row.id}>
|
||||
<td className="vtd-sig-table-ind">{row.displayName}</td>
|
||||
@@ -43,13 +45,13 @@ const VirtualIndicatorCompareTable: React.FC<Props> = ({ metrics }) => (
|
||||
<div className="vtd-sig-row-bar vtd-sig-row-bar--heat">
|
||||
<div
|
||||
className={`vtd-sig-row-bar-fill vtd-sig-row-bar-fill--${tier}`}
|
||||
style={{ width: `${matchRate}%` }}
|
||||
style={{ width: `${rate}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="vtd-sig-table-pct">{`${matchRate}%`}</span>
|
||||
<span className="vtd-sig-table-pct">{`${rate}%`}</span>
|
||||
</td>
|
||||
<td className="vtd-sig-table-status-cell">
|
||||
<ConditionStatusSignal status={status} matchRate={matchRate} compact />
|
||||
<ConditionStatusSignal status={status} matchRate={rate} compact />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { coerceFiniteNumber } from '../../utils/safeFormat';
|
||||
|
||||
const SEGMENTS = 25;
|
||||
|
||||
@@ -7,9 +8,10 @@ interface Props {
|
||||
}
|
||||
|
||||
const VirtualSignalEqualizer: React.FC<Props> = ({ matchRate }) => {
|
||||
const rate = coerceFiniteNumber(matchRate) ?? 0;
|
||||
const litCount = useMemo(
|
||||
() => Math.round((Math.min(100, Math.max(0, matchRate)) / 100) * SEGMENTS),
|
||||
[matchRate],
|
||||
() => Math.round((Math.min(100, Math.max(0, rate)) / 100) * SEGMENTS),
|
||||
[rate],
|
||||
);
|
||||
|
||||
const segments = useMemo(() => {
|
||||
@@ -20,7 +22,7 @@ const VirtualSignalEqualizer: React.FC<Props> = ({ matchRate }) => {
|
||||
items.push({ lit: false, tone: 'blue' });
|
||||
continue;
|
||||
}
|
||||
if (matchRate >= 100 && i === SEGMENTS - 1) {
|
||||
if (rate >= 100 && i === SEGMENTS - 1) {
|
||||
items.push({ lit: true, tone: 'peak' });
|
||||
} else if (i >= Math.floor(SEGMENTS * 0.55)) {
|
||||
items.push({ lit: true, tone: 'gold' });
|
||||
@@ -29,7 +31,7 @@ const VirtualSignalEqualizer: React.FC<Props> = ({ matchRate }) => {
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}, [litCount, matchRate]);
|
||||
}, [litCount, rate]);
|
||||
|
||||
const ticks = [100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 0];
|
||||
|
||||
@@ -53,7 +55,7 @@ const VirtualSignalEqualizer: React.FC<Props> = ({ matchRate }) => {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{matchRate >= 100 && (
|
||||
{rate >= 100 && (
|
||||
<div className="vtd-sig-eq-badge">100% MATCH</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -111,9 +111,9 @@ const VirtualTargetCard: React.FC<Props> = ({
|
||||
<span className="vtd-card-sym">{sym}</span>
|
||||
</div>
|
||||
<label className="vtd-card-strategy-field" onClick={e => e.stopPropagation()}>
|
||||
<span className="vtd-card-strategy-label">투자전략</span>
|
||||
<select
|
||||
className="vtd-card-strategy-select"
|
||||
aria-label="투자전략"
|
||||
value={strategyId ?? globalStrategyId ?? ''}
|
||||
onChange={e => {
|
||||
const v = e.target.value;
|
||||
|
||||
@@ -70,9 +70,9 @@ const VirtualTargetFocusView: React.FC<Props> = ({
|
||||
<span className="vtd-card-sym">{sym}</span>
|
||||
</div>
|
||||
<label className="vtd-card-strategy-field" onClick={e => e.stopPropagation()}>
|
||||
<span className="vtd-card-strategy-label">투자전략</span>
|
||||
<select
|
||||
className="vtd-card-strategy-select"
|
||||
aria-label="투자전략"
|
||||
value={strategyId ?? globalStrategyId ?? ''}
|
||||
onChange={e => {
|
||||
const v = e.target.value;
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import React, { memo, useEffect, useRef, useState } from 'react';
|
||||
import type { ChangeDir, TickerData } from '../../hooks/useMarketTicker';
|
||||
|
||||
function toFiniteNumber(v: unknown): number | null {
|
||||
if (v == null || v === '') return null;
|
||||
const n = typeof v === 'number' ? v : Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
import { coerceFiniteNumber, safePercentFromRate, safeToFixed } from '../../utils/safeFormat';
|
||||
|
||||
function fmtKrw(price: number | null | undefined | unknown): string {
|
||||
const p = toFiniteNumber(price);
|
||||
const p = coerceFiniteNumber(price);
|
||||
if (p == null) return '-';
|
||||
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);
|
||||
if (p >= 0.01) return safeToFixed(p, 4, '-');
|
||||
return safeToFixed(p, 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)}%`;
|
||||
return safePercentFromRate(rate, 2).replace('—', '-');
|
||||
}
|
||||
|
||||
const ChangeArrow = memo(function ChangeArrow({ change }: { change: ChangeDir }) {
|
||||
|
||||
Reference in New Issue
Block a user