가상투자 메뉴 기능 구현
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
formatIndicatorValue,
|
||||
isConditionMet,
|
||||
type VirtualConditionRow,
|
||||
} from './virtualStrategyConditions';
|
||||
|
||||
export type ConditionStatus = 'match' | 'pending' | 'nomatch' | 'unknown';
|
||||
|
||||
export interface ConditionMetric {
|
||||
row: VirtualConditionRow & { currentValue: number | null };
|
||||
status: ConditionStatus;
|
||||
progress: number | null;
|
||||
}
|
||||
|
||||
export type TrafficLightState = 'red' | 'yellow' | 'blue';
|
||||
|
||||
const PENDING_PROGRESS = 72;
|
||||
|
||||
/** 목표값 대비 현재값 접근률 (0~100, 충족 시 100) */
|
||||
export function computeConditionProgress(
|
||||
conditionType: string,
|
||||
current: number | null,
|
||||
target: number | null,
|
||||
): number | null {
|
||||
if (current == null || target == null) return null;
|
||||
const met = isConditionMet(conditionType, current, target);
|
||||
if (met === true) return 100;
|
||||
|
||||
const absTarget = Math.abs(target);
|
||||
const scale = absTarget > 1e-9 ? absTarget : Math.max(Math.abs(current), 1);
|
||||
|
||||
switch (conditionType) {
|
||||
case 'LT':
|
||||
case 'CROSS_DOWN':
|
||||
case 'LTE': {
|
||||
if (current <= target) return 100;
|
||||
const gap = current - target;
|
||||
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));
|
||||
}
|
||||
case 'EQ': {
|
||||
const diff = Math.abs(current - target);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
export function getConditionStatus(
|
||||
conditionType: string,
|
||||
current: number | null,
|
||||
target: number | null,
|
||||
satisfied?: boolean | null,
|
||||
): ConditionStatus {
|
||||
if (satisfied === true) return 'match';
|
||||
if (satisfied === false) {
|
||||
const progress = computeConditionProgress(conditionType, current, target);
|
||||
if (progress != null && progress >= PENDING_PROGRESS) return 'pending';
|
||||
return 'nomatch';
|
||||
}
|
||||
const met = isConditionMet(conditionType, current, target);
|
||||
if (met === true) return 'match';
|
||||
if (met === false) {
|
||||
const progress = computeConditionProgress(conditionType, current, target);
|
||||
if (progress != null && progress >= PENDING_PROGRESS) return 'pending';
|
||||
return 'nomatch';
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
export function formatStrategyThreshold(
|
||||
conditionType: string,
|
||||
target: number | null,
|
||||
conditionLabel: string,
|
||||
): string {
|
||||
if (target == null) return '—';
|
||||
const v = formatIndicatorValue(target);
|
||||
switch (conditionType) {
|
||||
case 'LT':
|
||||
case 'CROSS_DOWN':
|
||||
return `< ${v}`;
|
||||
case 'GT':
|
||||
case 'CROSS_UP':
|
||||
return `> ${v}`;
|
||||
case 'GTE':
|
||||
return `≥ ${v}`;
|
||||
case 'LTE':
|
||||
return `≤ ${v}`;
|
||||
case 'EQ':
|
||||
return `= ${v}`;
|
||||
case 'NEQ':
|
||||
return `≠ ${v}`;
|
||||
default:
|
||||
return `${conditionLabel} ${v}`;
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}));
|
||||
}
|
||||
|
||||
/** 전체 매매조건 일치율 — 백엔드 matchRate 우선 */
|
||||
export function computeMatchRate(
|
||||
metrics: ConditionMetric[],
|
||||
backendMatchRate?: number,
|
||||
): number {
|
||||
if (backendMatchRate != null && Number.isFinite(backendMatchRate)) {
|
||||
return Math.round(Math.min(100, Math.max(0, backendMatchRate)));
|
||||
}
|
||||
const evaluable = metrics.filter(m => m.status !== 'unknown');
|
||||
if (evaluable.length === 0) return 0;
|
||||
const met = evaluable.filter(m => m.status === 'match').length;
|
||||
return Math.round((met / evaluable.length) * 100);
|
||||
}
|
||||
|
||||
export function getTrafficLightState(matchRate: number, metrics: ConditionMetric[]): TrafficLightState {
|
||||
if (matchRate >= 100) return 'blue';
|
||||
const hasPending = metrics.some(m => m.status === 'pending');
|
||||
const hasPartial = metrics.some(m => m.status === 'match');
|
||||
if (hasPending || hasPartial || matchRate >= 40) return 'yellow';
|
||||
return 'red';
|
||||
}
|
||||
|
||||
export function formatUpdatedTime(ts: number | undefined): string {
|
||||
if (!ts) return '—';
|
||||
return new Date(ts).toLocaleTimeString('ko-KR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user