가상투자 메뉴 기능 구현

This commit is contained in:
Macbook
2026-05-25 06:41:45 +09:00
parent 0cfe7fc84c
commit 8b373b11e3
33 changed files with 3897 additions and 99 deletions
+154
View File
@@ -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,
});
}