매수, 매도 색상 수정
This commit is contained in:
@@ -25,6 +25,7 @@ import { BollingerBandFillPrimitive } from './BollingerBandFillPrimitive';
|
||||
import { resolveBbBandBackground } from './bollingerConfig';
|
||||
import type { OHLCVBar, ChartType, Theme, IndicatorConfig, Timeframe } from '../types';
|
||||
import { formatChartAxisPrice, formatIndicatorAxisPrice } from './dataGenerator';
|
||||
import { TRADE_BUY_COLOR, TRADE_SELL_COLOR } from './tradeSignalColors';
|
||||
import {
|
||||
DEFAULT_CHART_TIME_FORMAT,
|
||||
formatUnixWithChartPattern,
|
||||
@@ -1181,7 +1182,7 @@ export class ChartManager {
|
||||
time: s.time,
|
||||
position: buy ? ('belowBar' as const) : ('aboveBar' as const),
|
||||
shape: buy ? ('arrowUp' as const) : ('arrowDown' as const),
|
||||
color: s.type === 'PARTIAL_SELL' ? '#FF9800' : (buy ? '#26A69A' : '#EF5350'),
|
||||
color: s.type === 'PARTIAL_SELL' ? '#FF9800' : (buy ? TRADE_BUY_COLOR : TRADE_SELL_COLOR),
|
||||
text,
|
||||
};
|
||||
});
|
||||
@@ -1213,7 +1214,7 @@ export class ChartManager {
|
||||
time: m.time,
|
||||
position: buy ? ('belowBar' as const) : ('aboveBar' as const),
|
||||
shape: buy ? ('arrowUp' as const) : ('arrowDown' as const),
|
||||
color: buy ? '#00BCD4' : '#FF9800', // 백테스팅과 구별: 청록/주황
|
||||
color: buy ? TRADE_BUY_COLOR : TRADE_SELL_COLOR,
|
||||
text,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* 전략 편집기 DSL 직렬화 — DB 평가 원천은 buy/sell condition JSON 만.
|
||||
* flow_layout 은 UI(좌표·START 메타); Logic Expression 은 저장하지 않음.
|
||||
*/
|
||||
import { normalizeLogicRootForPersistence } from './strategyConditionNormalize';
|
||||
import type { LogicNode, ConditionDSL } from './strategyTypes';
|
||||
import { generateNodeId } from './strategyTypes';
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/** 프로젝트 공통 — 매매 시그널 색 (매수: 빨강, 매도: 파랑) */
|
||||
export const TRADE_BUY_COLOR = '#ef5350';
|
||||
export const TRADE_SELL_COLOR = '#4dabf7';
|
||||
export const TRADE_BUY_COLOR_HOVER = '#e53935';
|
||||
export const TRADE_SELL_COLOR_HOVER = '#339af0';
|
||||
@@ -1,7 +1,6 @@
|
||||
import { coerceFiniteNumber } from './safeFormat';
|
||||
import {
|
||||
formatIndicatorValue,
|
||||
isConditionMet,
|
||||
type VirtualConditionRow,
|
||||
} from './virtualStrategyConditions';
|
||||
|
||||
@@ -10,7 +9,7 @@ export type ConditionStatus = 'match' | 'pending' | 'nomatch' | 'unknown';
|
||||
export interface ConditionMetric {
|
||||
row: VirtualConditionRow & { currentValue: number | null };
|
||||
status: ConditionStatus;
|
||||
/** 조건 매칭율 0~100 */
|
||||
/** 조건 매칭율 0~100 — 백엔드 Ta4j satisfied 만 반영 */
|
||||
matchRate: number;
|
||||
/** @deprecated matchRate 와 동일 — 하위 호환 */
|
||||
progress: number | null;
|
||||
@@ -23,102 +22,28 @@ export function resolveHeatTier(matchRate: number, status: ConditionStatus): Hea
|
||||
if (status === 'match' || matchRate >= 100) return 'match';
|
||||
if (status === 'pending') return 'pending';
|
||||
if (status === 'nomatch') return 'nomatch';
|
||||
if (matchRate >= 45) return 'pending';
|
||||
return 'nomatch';
|
||||
}
|
||||
|
||||
export type TrafficLightState = 'red' | 'yellow' | 'blue';
|
||||
|
||||
const PENDING_PROGRESS = 72;
|
||||
|
||||
/** 목표값 대비 현재값 접근률 (0~100, 충족 시 100) */
|
||||
export function computeConditionProgress(
|
||||
conditionType: string,
|
||||
current: number | null | unknown,
|
||||
target: number | null | unknown,
|
||||
): number | null {
|
||||
const cur = coerceFiniteNumber(current);
|
||||
const tgt = coerceFiniteNumber(target);
|
||||
if (cur == null || tgt == null) return null;
|
||||
const met = isConditionMet(conditionType, cur, tgt);
|
||||
if (met === true) return 100;
|
||||
|
||||
const absTarget = Math.abs(tgt);
|
||||
const scale = absTarget > 1e-9 ? absTarget : Math.max(Math.abs(cur), 1);
|
||||
|
||||
switch (conditionType) {
|
||||
case 'LT':
|
||||
case 'CROSS_DOWN':
|
||||
case 'LTE': {
|
||||
if (cur <= tgt) return 100;
|
||||
const gap = cur - tgt;
|
||||
return Math.min(99, Math.max(0, 100 - (gap / scale) * 100));
|
||||
}
|
||||
case 'GT':
|
||||
case 'CROSS_UP':
|
||||
case 'GTE': {
|
||||
if (cur >= tgt) return 100;
|
||||
if (tgt <= 0 && cur <= 0) return 0;
|
||||
return Math.min(99, Math.max(0, (cur / scale) * 100));
|
||||
}
|
||||
case 'EQ': {
|
||||
const diff = Math.abs(cur - tgt);
|
||||
return Math.min(99, Math.max(0, 100 - (diff / scale) * 100));
|
||||
}
|
||||
default:
|
||||
return Math.min(99, Math.max(0, 100 - (Math.abs(cur - tgt) / scale) * 100));
|
||||
}
|
||||
}
|
||||
|
||||
const CROSS_CONDITION_TYPES = new Set([
|
||||
'CROSS_UP', 'CROSS_DOWN', 'SLOPE_UP', 'SLOPE_DOWN',
|
||||
]);
|
||||
|
||||
/** 조건별 매칭율 (0~100) — 백엔드 Ta4j satisfied 우선 */
|
||||
/** 조건별 매칭율 (0~100) — 백엔드 live-conditions 의 satisfied 만 사용 */
|
||||
export function computeConditionMatchRate(
|
||||
row: VirtualConditionRow & { currentValue: number | null },
|
||||
): number {
|
||||
if (row.satisfied === true) return 100;
|
||||
if (row.satisfied === false) {
|
||||
if (CROSS_CONDITION_TYPES.has(row.conditionType)) return 0;
|
||||
const progress = computeConditionProgress(
|
||||
row.conditionType,
|
||||
row.currentValue,
|
||||
row.targetValue,
|
||||
);
|
||||
if (progress == null) return 0;
|
||||
return Math.min(99, Math.max(0, Math.round(progress)));
|
||||
}
|
||||
if (row.currentValue == null) return 0;
|
||||
const progress = computeConditionProgress(
|
||||
row.conditionType,
|
||||
row.currentValue,
|
||||
row.targetValue,
|
||||
);
|
||||
if (progress == null) return 0;
|
||||
return Math.min(99, Math.max(0, Math.round(progress)));
|
||||
if (row.satisfied === false) return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function getConditionStatus(
|
||||
conditionType: string,
|
||||
current: number | null,
|
||||
target: number | null,
|
||||
_conditionType: string,
|
||||
_current: number | null,
|
||||
_target: number | null,
|
||||
satisfied?: boolean | null,
|
||||
): ConditionStatus {
|
||||
if (satisfied === true) return 'match';
|
||||
if (satisfied === false) {
|
||||
if (CROSS_CONDITION_TYPES.has(conditionType)) return 'nomatch';
|
||||
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';
|
||||
}
|
||||
if (satisfied === false) return 'nomatch';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
@@ -196,35 +121,11 @@ export function computeMatchRate(
|
||||
|
||||
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';
|
||||
if (hasPartial || matchRate >= 40) return 'yellow';
|
||||
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', {
|
||||
|
||||
@@ -83,26 +83,6 @@ export function extractVirtualConditions(strategy: StrategyDto | null | undefine
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 조건 충족 여부 (단순 임계값 비교) */
|
||||
export function isConditionMet(
|
||||
conditionType: string,
|
||||
current: number | null | unknown,
|
||||
target: number | null | unknown,
|
||||
): boolean | null {
|
||||
const cur = coerceFiniteNumber(current);
|
||||
const tgt = coerceFiniteNumber(target);
|
||||
if (cur == null || tgt == null) return null;
|
||||
switch (conditionType) {
|
||||
case 'GT': case 'CROSS_UP': return cur > tgt;
|
||||
case 'LT': case 'CROSS_DOWN': return cur < tgt;
|
||||
case 'GTE': return cur >= tgt;
|
||||
case 'LTE': return cur <= tgt;
|
||||
case 'EQ': return Math.abs(cur - tgt) < 1e-6;
|
||||
case 'NEQ': return Math.abs(cur - tgt) >= 1e-6;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatIndicatorValue(v: number | null | unknown): string {
|
||||
const n = coerceFiniteNumber(v);
|
||||
if (n == null) return '—';
|
||||
|
||||
Reference in New Issue
Block a user