가상투자 화면 목록 선택 동작

This commit is contained in:
Macbook
2026-05-25 14:05:13 +09:00
parent 20f11f6c4b
commit 3102169541
24 changed files with 130 additions and 27 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ import type { Timeframe } from '../types';
/** 차트 Timeframe → 백엔드 candleType */
export function timeframeToCandleType(tf: Timeframe): string {
switch (tf) {
case '1m': case '3m': case '5m': case '15m': case '30m': case '1h': case '4h':
case '1m': case '3m': case '5m': case '10m': case '15m': case '30m': case '1h': case '4h':
return tf;
case '1D': return '1d';
case '1W': case '1M': return '1d';
+2 -2
View File
@@ -10,11 +10,11 @@ export const SYMBOLS = [
];
const TF_SECONDS: Record<Timeframe, number> = {
'1m':60,'3m':180,'5m':300,'15m':900,'30m':1800,'1h':3600,'4h':14400,'1D':86400,'1W':604800,'1M':2592000,
'1m':60,'3m':180,'5m':300,'10m':600,'15m':900,'30m':1800,'1h':3600,'4h':14400,'1D':86400,'1W':604800,'1M':2592000,
};
const TF_BARS: Record<Timeframe, number> = {
'1m':300,'3m':300,'5m':300,'15m':300,'30m':300,'1h':500,'4h':500,'1D':500,'1W':300,'1M':120,
'1m':300,'3m':300,'5m':300,'10m':300,'15m':300,'30m':300,'1h':500,'4h':500,'1D':500,'1W':300,'1M':120,
};
const SEED_MAP: Record<string, number> = {
@@ -47,6 +47,7 @@ const CANDLE_KO: Record<string, string> = {
'1m': '1분봉',
'3m': '3분봉',
'5m': '5분봉',
'10m': '10분봉',
'15m': '15분봉',
'30m': '30분봉',
'1h': '1시간봉',
+1 -1
View File
@@ -4,7 +4,7 @@ export const START_NODE_ID = '__strategy_start__';
export const DEFAULT_START_CANDLE = '1m';
export const STRATEGY_CANDLE_TYPES = [
'1m', '3m', '5m', '15m', '30m', '1h', '4h', '1d',
'1m', '3m', '5m', '10m', '15m', '30m', '1h', '4h', '1d',
] as const;
export type StrategyCandleType = (typeof STRATEGY_CANDLE_TYPES)[number];
@@ -72,7 +72,7 @@ export function candleTypeToTimeframe(candleType: string): Timeframe {
if (c === '1d') return '1D';
if (c === '1h') return '1h';
if (c === '4h') return '4h';
if (['1m', '3m', '5m', '15m', '30m'].includes(c)) return c as Timeframe;
if (['1m', '3m', '5m', '10m', '15m', '30m'].includes(c)) return c as Timeframe;
return '1m';
}
@@ -85,7 +85,7 @@ export function resolveStrategyTimeframes(strategy: StrategyDto | undefined): Ti
}
if (set.size === 0) set.add('1m');
return [...set].sort((a, b) => {
const order: Timeframe[] = ['1m', '3m', '5m', '15m', '30m', '1h', '4h', '1D', '1W', '1M'];
const order: Timeframe[] = ['1m', '3m', '5m', '10m', '15m', '30m', '1h', '4h', '1D', '1W', '1M'];
return order.indexOf(a) - order.indexOf(b);
});
}
+1
View File
@@ -43,6 +43,7 @@ const CANDLE_TYPE_KO: Record<string, string> = {
'1m': '1분봉',
'3m': '3분봉',
'5m': '5분봉',
'10m': '10분봉',
'15m': '15분봉',
'30m': '30분봉',
'1h': '1시간봉',
+2 -1
View File
@@ -77,6 +77,7 @@ function candleEndpoint(
case '1m': return `${base}/minutes/1?market=${market}&count=${count}${toParam}`;
case '3m': return `${base}/minutes/3?market=${market}&count=${count}${toParam}`;
case '5m': return `${base}/minutes/5?market=${market}&count=${count}${toParam}`;
case '10m': return `${base}/minutes/10?market=${market}&count=${count}${toParam}`;
case '15m': return `${base}/minutes/15?market=${market}&count=${count}${toParam}`;
case '30m': return `${base}/minutes/30?market=${market}&count=${count}${toParam}`;
case '1h': return `${base}/minutes/60?market=${market}&count=${count}${toParam}`;
@@ -318,7 +319,7 @@ export class UpbitWebSocketClient {
// ─── 타임프레임 초 단위 ───────────────────────────────────────────────────
export const TF_SECONDS: Record<Timeframe, number> = {
'1m': 60, '3m': 180, '5m': 300, '15m': 900, '30m': 1800,
'1m': 60, '3m': 180, '5m': 300, '10m': 600, '15m': 900, '30m': 1800,
'1h': 3600, '4h': 14400, '1D': 86400, '1W': 604800, '1M': 2592000,
};
@@ -143,6 +143,29 @@ export function getTrafficLightState(matchRate: number, metrics: ConditionMetric
return 'red';
}
/** 매수·매도 조건 충족률 기준 타이밍 (TradeAlertModal: 매수=블루, 매도=레드) */
export type VirtualTradeTiming = 'buy' | 'sell' | 'neutral';
function sideMatchRate(metrics: ConditionMetric[], side: 'buy' | 'sell'): number {
const rows = metrics.filter(m => m.row.side === side && m.status !== 'unknown');
if (rows.length === 0) return 0;
const met = rows.filter(m => m.status === 'match').length;
return Math.round((met / rows.length) * 100);
}
export function resolveVirtualTradeTiming(metrics: ConditionMetric[]): VirtualTradeTiming {
const buyRate = sideMatchRate(metrics, 'buy');
const sellRate = sideMatchRate(metrics, 'sell');
if (buyRate === 0 && sellRate === 0) return 'neutral';
if (buyRate >= 100 && buyRate > sellRate) return 'buy';
if (sellRate >= 100 && sellRate > buyRate) return 'sell';
if (buyRate >= 100 && sellRate < 100) return 'buy';
if (sellRate >= 100 && buyRate < 100) return 'sell';
if (buyRate > sellRate && buyRate >= 50) return 'buy';
if (sellRate > buyRate && sellRate >= 50) return 'sell';
return 'neutral';
}
export function formatUpdatedTime(ts: number | undefined): string {
if (!ts) return '—';
return new Date(ts).toLocaleTimeString('ko-KR', {